Idiomatische Lösung gesucht: in Plaintext-Dateiformat serialisieren



  • knivil schrieb:

    Warum ein Array von serialisierbaren Objekten hier SerializableRange1D halten? Du kannst auch gleich ein Array von Range1D benutzen

    Weil eine SerializableEntity weitere Attribute hat. Etwa das "name"-Attribut habe ich in beiden Beispielen gezeigt.

    knivil schrieb:

    D.h. der Zweck deiner Storage-Klasse ist mir nicht ersichtlich.

    Wieder was anderes. Storage ist bloß ein Container, in den ich alles, was ich serialisieren will, reintun und mit Attributen (Name etc.) versehen kann, um es dann zu speichern, nach Namen zu filtern oder irgendwohin zu übergeben.

    Anyway, es ist sehr zuvorkommend, daß du dir Sorgen um meine Klassenhierarchie machst, aber der Punkt ist einfach, daß es nicht darum geht. Schon von Anfang an nicht.

    knivil schrieb:

    Wenn es dir nicht passt, dann ignoriere ihn.

    So soll es sein.



  • audacia schrieb:

    Was wäre eine möglichst C++-artige, aufwandsarme Lösung, um Dateien wie diese hier zu parsen und zu generieren?

    Hallo audica,

    Also die Lösung, die ich favorisiere und als C++-artig bezeichnen würde, besteht darin, zunächst Klassen oder Strukturen zu schaffen, wie man sie später im Programm benötigt. Wichtig ist dabei, das Serialisieren zunächst zu ignorieren - die Strukturen sollten zu dem Programm/Algorithmen passen, unabhängig von ihrer Ein- und Ausgabe.

    Z.B.:

    // --   [Range1D]
    struct Range1D
    {
        double min, max;
    };
    

    ich nehme an, das ist eine der Strukturen, so wie Du sie benötigst.

    Und erst im zweiten Schritt schaffe man sich dann einen Streamingoperator um diese struct Range1D z.B. einzulesen. Die Signatur ist damit quasi vorgegeben:

    std::istream& operator>>( std::istream& in, Range1D& rng1d );
    

    Eine Ausgabe würde äquivalent funktionieren:

    std::ostream& operator<<( std::ostream& out, const Range1D& rng1d );
    

    Anschließend kann man dann Instanzen dieser Struktur einlesen und ausgeben wie einen int oder double.

    Jetzt weiß ich natürlich zu wenig über das Format, das hier vorgegeben ist. Du schreibst Zwischending von INI und TSV. Also können die Block-Kennzeichnungen beliebig in der Datei stehen. Ich nehme mal der Einfachheit halber an, dass ab einem bestimmten Block (z.B. "[Range1D]") ein ganz bestimmtes Format kommt. In diesem Fall zwei Zeilen, die ohne Interesse sind und anschließend die beiden Werte 'min' und 'max' (bezeichnet mit 'From' und 'To').

    Also der Blockname definiert das , was dann kommt. Er gehört damit nicht selbst zu dem, was mit dem Streamingoperator von Range1D gelesen wird. Das muss ja vorher passieren. Wieder angenommen es existiert ein Manipulator skipline so ist die Implementierung der Lesefunktion kein Problem mehr:

    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    

    zweimal eine Zeile überlesen dann die beiden Werte - fertig!

    skipline selber ist wie folgt implementiert:

    #include <istream>
    #include <limits> // numeric_limits
    //      skipline: überliest eine Zeile
    std::istream& skipline( std::istream& in )
    {
        return in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
    }
    

    genauso geht man dann bei DiscreteScalarFunction1D alias ScalarFunction1D vor. Wobei mir hier nicht klar ist, wo die X- und Z-Werte abgelegt werden sollen.
    Ich würde Dir dazu eine kleine struct XZ (einen besseren Namen weiß ich nicht) vorschlagen. Und DiscreteScalarFunction1D sollte einen Container mit diesen Werten haben.

    Ohne mich jetzt in weiteren Erklärungen zu verlieren poste ich Dir mal einen Code, der die von Dir beschriebene Datei einliest. Wie Du das in Deine Serialisierungsobjekte einbaust, überlasse ich Dir.

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <limits> // numeric_limits
    
    // --   Helferlein skipline, Char
    //      skipline: überliest eine Zeile
    std::istream& skipline( std::istream& in )
    {
        return in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
    }
    
    //      Char<'X'> überliest das definierte Zeichen 'X'
    template< char C >
    std::istream& Char( std::istream& in )
    {
        char c;
        if( in >> c && c != C )
            in.setstate( std::ios_base::failbit );
        return in;
    }
    
    // --   [Range1D]
    struct Range1D
    {
        double min, max;
    }; 
    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    
    // --   [ScalarFunction1D]
    struct XZ
    {
        double x_, z_;
    };
    std::istream& operator>>( std::istream& in, XZ& xz )
    {
        // Bem.: ggf. z_ von [nm] nach [um] konvertieren !?
        return in >> xz.x_ >> xz.z_;
    }
    
    struct DiscreteScalarFunction1D
    {
        Range1D argumentRange, valueRange;
        double resolution;
        std::string argumentUnit, valueUnit;
        std::vector<double> data;
        std::vector< XZ > xz_;
    };
    //      %Name=Section0
    //      x [µm]    z [nm]
    //      -4    -1564.73
    //      -3.21995    -1565.17
    //      ...
    std::istream& operator>>( std::istream& in, DiscreteScalarFunction1D& val )
    {
        val.xz_.clear();
        in >> skipline >> skipline;
        for( XZ xz; (in >> std::ws).good() && char(in.peek()) != '[' && in >> xz; )
            val.xz_.push_back( xz );
        //  Range1D argumentRange, valueRange;  // ??
        //  double resolution;
        //  std::string argumentUnit, valueUnit;
        //  std::vector<double> data;
        return in;
    }
    
    int main()
    {
        using namespace std;
        ifstream datei( "input.txt" );
        if( !datei.is_open() )
        {
            cerr << "Fehler beim Oeffnen der Datei" << endl;
            return -2;
        }
        for( string block; getline( datei >> Char<'['>, block, ']' ) >> skipline;  )
        {
            if( block == "Range1D" )
            {
                Range1D r;
                if( datei >> r )
                {
                    cout << "[Range1D] " << endl;
                    // usw. mache was mit 'r'
                }
            }
            else if( block == "ScalarFunction1D" )
            {
                DiscreteScalarFunction1D s;
                if( datei >> s )
                {
                    cout << "[ScalarFunction1D] " << " " << s.xz_.size() << "Eintraege" << endl;
                    // usw. mache was mit 's'
                }
            }
            else
            {
                cerr << "unbekannter Block: " << block <<  endl;
                datei.setstate( ios_base::failbit );
            }
        }
        if( datei.eof() )
        {
            cout << "alles gelesen" << endl;
        }
        return 0;
    }
    

    Wie Du siehst ersetzt hier allein das Mittelstück der Zeile 88 die ganze Methode tryReadHeader aus Deinem ersten Posting.

    Die Ausgabe ist hier

    [ScalarFunction1D]  9Eintraege
    [Range1D]
    [Range1D]
    alles gelesen
    

    Die einzige echte Schwierigkeit bestand darin, dass die Anzahl der XZ-Eelemte dynamisch sein soll (so nehme ich es an). Das heißt hier muss mit dem Lesen aufgehört werden, wenn ein neuer Block ('[') erscheint. Sauberer wäre vielleicht auf ein Zeichen abzufragen, was zu einer Zahl gehören kann - aber so ist es einfacher (siehe Zeile 68).
    Btw.: was soll mit den anderen Membern von DiscreteScalarFunction1D beim Einlesen passieren? Und was haben die Member DiscreteScalarFunction1D::argumentRange und DiscreteScalarFunction1D::valueRange mit dem [Range1D] in der Datei zu tun?

    Gruß
    Werner



  • Werner Salomon schrieb:

    Hallo audica,

    audacia 🙂

    Werner Salomon schrieb:

    Und erst im zweiten Schritt schaffe man sich dann einen Streamingoperator um diese struct Range1D z.B. einzulesen. Die Signatur ist damit quasi vorgegeben:

    std::istream& operator>>( std::istream& in, Range1D& rng1d );
    

    Eine Ausgabe würde äquivalent funktionieren:

    std::ostream& operator<<( std::ostream& out, const Range1D& rng1d );
    

    Okay, wäre möglich. Ich mag die C++-Streamoperatoren aber nicht sehr, und einer der Gründe ist, daß sie einen Datentyp auf eine bestimmte Art der Serialisierung festlegen. Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    Werner Salomon schrieb:

    Jetzt weiß ich natürlich zu wenig über das Format, das hier vorgegeben ist. Du schreibst Zwischending von INI und TSV. Also können die Block-Kennzeichnungen beliebig in der Datei stehen. Ich nehme mal der Einfachheit halber an, dass ab einem bestimmten Block (z.B. "[Range1D]") ein ganz bestimmtes Format kommt. In diesem Fall zwei Zeilen, die ohne Interesse sind und anschließend die beiden Werte 'min' und 'max' (bezeichnet mit 'From' und 'To'). Also der Blockname definiert das , was dann kommt. Er gehört damit nicht selbst zu dem, was mit dem Streamingoperator von Range1D gelesen wird. Das muss ja vorher passieren.

    Die Zeilen dazwischen sind nicht ohne Interesse, das sind eben Attribute, sonst ist deine Lesart aber zutreffend.

    Werner Salomon schrieb:

    Wieder angenommen es existiert ein Manipulator skipline so ist die Implementierung der Lesefunktion kein Problem mehr:

    //      %Name=Section158.MarkedRange
    //      From    To
    //      4    18.9694
    std::istream& operator>>( std::istream& in, Range1D& rng1d )
    {
        return in >> skipline >> skipline >> rng1d.min >> rng1d.max;
    }
    

    zweimal eine Zeile überlesen dann die beiden Werte - fertig!

    skipline selber ist wie folgt implementiert:[...]

    Tatsächlich, sehr nützlich; von Manipulatoren hatte ich gelesen, den einen oder anderen auch schon benutzt, aber nie selbst welche geschrieben.

    Werner Salomon schrieb:

    Wie Du siehst ersetzt hier allein das Mittelstück der Zeile 88 die ganze Methode tryReadHeader aus Deinem ersten Posting.

    Zeile 88 ist sehr elegant und reizt mich, mir die ungeliebten C++-Streams doch nocheinmal genauer anzuschauen, aber mit tryReadHeader() hat sie nichts zu tun. tryReadHeader() parst Header mit oder ohne Einheiten von beliebiger Dimension, also etwa

    X [µm]	Y [nm]
    

    oder

    Col1	Col2[m]	Col3 <...> Col99 [m/s]
    

    oder

    From To
    

    . Das Pendant zu Zeile 88-113 sieht so aus (nur die Logik, die den Typnamen verarbeitet, ist woanders):

    virtual bool tryReadEntityName (std::string& name)
        {
            char buf[256];
            int result = file.scanf ("[%255[^]]]\n", buf);
            if (result == EOF)
                return false;
            else if (result != 1)
                throw std::runtime_error ("Cannot read line '" + file.readLine () + "': invalid type name format (expected '[<name>]'");
            else
            {
                name = buf;
                return true;
            }
        }
    

    Werner Salomon schrieb:

    Btw.: was soll mit den anderen Membern von DiscreteScalarFunction1D beim Einlesen passieren?

    Kommt mir vielleicht schon zu selbstverständlich vor. Die x-Werte sind bei DiscreteScalarFunction1D äquidistant und werden also nicht alle gebraucht. Stattdessen rechne ich sie in argumentRange und resolution um. Hier ist mein Deserialisierungscode:

    void Storage::NamedScalarFunction1D::readData (StorageReader* reader)
    {
        std::string labels[2], units[2];
        if (tryReadHeader (reader, labels, units, 2))
        {
            xLabel = labels[0];
            yLabel = labels[1];
            scalarFunction.argumentUnit = units[0];
            scalarFunction.valueUnit = units[1];
        }
    
        double data[2];
        scalarFunction.data.clear ();
        double min = std::numeric_limits<double>::max (),
               max = -std::numeric_limits<double>::max ();
        while (tryReadDataLine (reader, data, 2))
        {
            scalarFunction.data.push_back (data[1]);
            min = std::min (min, data[0]);
            max = std::max (max, data[0]);
        }
        scalarFunction.argumentRange = Range1Df (min, max);
        scalarFunction.resolution = scalarFunction.data.size () / scalarFunction.argumentRange.width (); // resolution ist aus irgendwelchen Gründen in [px/argumentUnit]
        scalarFunction.obtainValueRange (); // berechnet scalarFunction.valueRange anhand von scalarFunction.data
    }
    

    Beachte, daß readData() nichts von dem Header- oder Datenformat weiß. Ich könnte die Daten auch aus einem äquivalenten Binärformat beziehen, wenn tryReadHeader()/tryReadDataLine() vom jeweiligen StorageReader entsprechend implementiert wären, oder ich könnte auch leicht einen XML-Importer/-Exporter hinzufügen, falls irgendjemandes Richtlinie das erfordert. Natürlich ist die schiere Definition von tryReadHeader()/tryReadDataLine() auf dieses zeilenbasierte Format hin optimiert, aber immerhin wäre es nicht allzu umständlich möglich, für dieselben Serialisierungsmethoden ein anderes Format-Backend zu schreiben. Das ist hier nicht explizit erforderlich, aber ich finde es wichtig, daß sowas geht, und ich sehe nicht, wie man das mit Streams umsetzen würde.

    Vielen Dank für deine Mühe und den streambasierten Ansatz; das ist genau so etwas, was ich selber nie geschrieben hätte (da zu wenig vertraut mit und sehr argwöhnisch gegenüber den C++-Streams - bei sowas wie datei.setstate(ios_base::failbit); läufts mir immer kalt den Rücken runter), was aber dem üblichen oder zumindest vom Sprachstandard nahegelegten Idiom wohl nahe kommt.



  • audacia schrieb:

    Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    dem kann ich jetzt nicht folgen. kannst das mal kurz erlaeutern ?

    Meep Meep



  • Meep Meep schrieb:

    audacia schrieb:

    Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ohne irgendwie ganz unschön mit using namespace zu zaubern.

    dem kann ich jetzt nicht folgen. kannst das mal kurz erlaeutern ?

    Je nach Dateiformat will man denselben Typen anders serialisieren. Wenn ich XML ausgebe, will ich aus einer Range1D vielleicht sowas machen:

    <Range1D>
      <Name>Section42.MarkedRange</Name>
      <From>42.0</From>
      <To>47.7</To>
    </Range1D>
    

    In eine Binärdatei schreibe ich vielleicht den Namen als längenpräfizierten String und die min/max-Werte einfach als IEEE754-Double.
    In beiden Fällen muß das gleiche serialisiert werden: Name, min, max. Das weiß entweder Range1D selbst oder alternativ irgendeine Adapterklasse. Wie diese Information aber in der Datei landet, sollte Range1D und die Adapterklasse nicht interessieren, das kann man unabhängig davon allgemein behandeln.

    Mein Problem mit den Streamoperatoren ist, daß sie by design beides auf einmal machen, die Serialisierung (Name, min, max) und die Formatierung (XML, binär, ...), und daß du sie üblicherweise in den globalen Namespace packst (oder in den Namespace deines Typs, damit sie per ADL gefunden werden) und dein Streamoperator-Overload auf diese Weise zum Standard erhoben wird.



  • audacia schrieb:

    Werner Salomon schrieb:

    Hallo audica,

    audacia 🙂

    audacia, die Kühnheit; nein - wie konnte ich das überlesen - mea culpa 😉

    audacia schrieb:

    Ich mag die C++-Streamoperatoren aber nicht sehr, und einer der Gründe ist, daß sie einen Datentyp auf eine bestimmte Art der Serialisierung festlegen. Wenn du einen Streamoperator für Dateiformat A schreibst, kannst du schlecht einen für Dateiformat B schreiben, ...

    Das kann man sehr wohl. Man kann 'von außen' das Format auswählen. Das funktioniert über die 'ios_base storage functions'. Jedes std::ios_base-Objekt - und jeder Stream ist ein solches Objekt - besitzt einen erweiterbaren Speicher (storage), in dem man Integer- und Pointer-Werte ablegen und wieder abrufen kann. Das ganze funktioniert mit xalloc, iword und pword.

    Beispiele findest Du auch hier im Forum. Suche nach xalloc - z.B. hier.

    Gruß
    Werner



  • ah, ich glaub ich weiß jetz was du meinst.
    ich hab ein aehnliches problem ungefaehr so geloest:

    struct name
    {
       std::string vorname, nachname;
    }; /* struct name */
    
    class links
    {
       public: 
          links(const name &n) : m_n(n) { }
    
          void operator()(std::ostream &os) const
          {
             os << m_n.nachname.c_str() << ", " << m_n.vorname.c_str();
          }
    
       private:
          const name &m_n;
    };
    
    class rechts
    {
       public: 
          rechts(const name &n) : m_n(n) { }
    
          void operator()(std::ostream &os) const
          {
             os << m_n.vorname.c_str() << ", " << m_n.nachname.c_str();
          }
    
       private:
          const name &m_n;
    };
    
    std::ostream& operator<<(std::ostream &os, const rechts &n)
    {
       n(os);
       return os;
    }
    
    std::ostream& operator<<(std::ostream &os, const links &n)
    {
       n(os);
       return os;
    }
    
    int main()
    {
       name n;
       n.vorname = "Vorname";
       n.nachname = "Nachname";
    
       std::cout << links(n) << std::endl;
       std::cout << rechts(n) << std::endl;
    }
    
    Ausgabe:
    Nachname, Vorname
    Vorname, Nachname
    

    Meep Meep



  • Meep Meep schrieb:

    ah, ich glaub ich weiß jetz was du meinst.
    ich hab ein aehnliches problem ungefaehr so geloest:

    struct name
    {
       std::string vorname, nachname;
    }; /* struct name */
       // ... skip
    
       std::cout << links(n) << std::endl;
       std::cout << rechts(n) << std::endl;
    }
    

    Hallo Meep Meep,

    ich weiß nicht, aus was sich das was (s.o.) bezieht. Wenn Du die Antwort von audacia meinst, passt die Antwort nicht und wenn Du meinen letzten Beitrag meinst, dann ist es nicht das was ich meine.

    Dein Ansatz ist in Ordnung. Er hat aber den Nachteil, dass Du - in Deinem Fall 'links' und/oder 'rechts' jedes mal hinschreiben musst. Wenn Du aber eine größere hierarchische Struktur einlesen oder ausgeben willst, so würde das bedeuten, dass Du den Code zweimal schreibst; einmal mit 'links' und einmal mit 'rechts', um bei Deinem Beispiel zu bleiben.

    Besser ist, dass man z.B. nach dem Öffenen einer bestimmten Datei einfach schreiben kann:

    ifstream datei("...");
        ifstream >> format_b; // alles weitere im Format B einlesen
    

    Und dann ist es egal, wie komplex der einzulesende Code ist, der Stream ist dann mit B markiert und jede Einlesefunktion kann intern darauf Rücksicht nehmen.

    Gruß
    Werner



  • Hallo audacia,

    audacia schrieb:

    Mein Problem mit den Streamoperatoren ist, daß sie by design beides auf einmal machen, die Serialisierung (Name, min, max) und die Formatierung (XML, binär, ...), ...

    eher nicht - so hatte ich das nicht gemeint. Wenn Du Deine Serialisierungs-Klassenstruktur beibehalten willst, so bilden die Aufrufe der Streaming-Operatoren nicht die oberste Stufe. Sondern sie werden von den Funktionen aufgerufen, bei denen entschieden ist, dass jetzt z.B. aus einer Textdatei gelesen werden soll.
    Gerade das Lesen von Objekten (nicht das Parsen!) aus einer XML-Datei ist mit Streaming nicht zu machen. Eine XML-Datei von ihrer logischen Struktur ist eben kein Stream, sondern eher eine Baumstruktur oder ein assoziativer Container.

    Schaue Dir dazu mal boost.program_options bzw. boost.property_tree an. In beiden Fällen können Daten aus unterschiedlichen Quellen gelesen werden. Trotzdem bieten sie ein einheitliches User-Interface an - unabhängig von der Quelle.

    Gruß
    Werner



  • hallo Werner

    Werner Salomon schrieb:

    ich weiß nicht, aus was sich das was (s.o.) bezieht

    es bezieht sich auf folgendes:

    audacia schrieb:

    Je nach Dateiformat will man denselben Typen anders serialisieren. Wenn ich XML ausgebe, will ich aus einer Range1D vielleicht sowas machen:

    <Range1D>
    <Name>Section42.MarkedRange</Name>
    <From>42.0</From>
    <To>47.7</To>
    </Range1D>	
    Code:
    <Range1D>
      <Name>Section42.MarkedRange</Name>
      <From>42.0</From>
      <To>47.7</To>
    </Range1D>
    

    In eine Binärdatei schreibe ich vielleicht den Namen als längenpräfizierten String und die min/max-Werte einfach als IEEE754-Double.

    ich finde es nicht schlecht wenn man sowas dann so machen kann:

    class Range1D
    {
    ...
    };
    
    /* nun aehnlicher code wie fuer links und rechts */
    
    Range1D r1d;
    ...
    myfilestream << binary(r1d);
    // oder
    myfilestream << xml(r1d);
    

    hier kann man natuerlich auch viele andere wege gehen um zum selben ergebnis zu kommen. mir ist der stil aber einfach sympathischer vor allem wenn ich sowieso mit den streams arbeite. ich hab mir z.b. auch einige formatierungsobjekte geschrieben weil ich sie intuitiver finde:

    std::cout << base16("string") << oct(43) ...
    

    Meep Meep


Anmelden zum Antworten