Idiomatische Lösung gesucht: in Plaintext-Dateiformat serialisieren



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

    [ScalarFunction1D]
    %Name=Section0
    x [µm]	z [nm]
    -4	-1564.73
    -3.21995	-1565.17
    -2.4399	-1561.13
    -1.65985	-1551.37
    -0.879797	-1533.07
    -0.0997463	-1505.58
    0.680304	-1461.58
    1.46036	-1390.1
    2.24041	-1285.65
    ...
    [Range1D]
    %Name=Section158.MarkedRange
    From	To
    4	18.9694
    [Range1D]
    %Name=Section159.MarkedRange
    From	To
    4	19.2007
    

    Wie man sieht, eine Mixtur aus INI-Dateiformat und TSV mit Header; das ist aber nur ein Beispiel, ich habe öfters derartige Dateiformate. Ich bin hier in der komfortablen Situation, jeweils korrespondierende Klassentypen ( (Discrete/Sparse)ScalarFunction1D , Range1D etc.) zu haben, das ist aber nicht immer so.

    Meine erste Lösung hier war im Prinzip C-Code in einer objektorientierten Klassenhierarchie:

    class TextFileStorageReader : public StorageReader
    {
        ...
        virtual bool tryReadHeader (std::string labels[], std::string units[], unsigned dim)
        {
            int c = file.peek ();
            if (c == EOF || c == '[' || isNumberChar (c))
                return false;
            else
            {
                std::string line = file.readLine ();
                if (line.empty ())
                    return dim == 0;
                char* s = &line[0];
                for (unsigned i = 0; i != dim; ++i)
                {
                    s = skipSpace (s);
                    char* sStart = s;
                    while (!std::isspace (*s) && *s != '[' && *s != '\0')
                        ++s;
                    if (labels)
                        labels[i].assign (sStart, s);
                    s = skipSpace (s);
                    if (*s == '[')
                    {
                        sStart = ++s;
                        while (*s != ']' && *s != '\0')
                            ++s;
                        if (units)
                            units[i].assign (sStart, s);
                        if (*s == ']')
                            ++s;
                    }
                }
                return true;
            }
        }
    public:
        TextFileStorageReader (const char* fileName)
         : file (fileName, "rt")
        {
            readEntitiesTo (storage); // allgemeine Leselogik der Basisklasse; ruft die (einzig formatspezifischen) virtuellen Funktionen wie tryReadHeader() auf
        }
        inline Storage getStorage (void) { return storage; }
    };
    Storage readFromTextFile (const char* fileName)
    {
        return TextFileStorageReader (fileName).getStorage ();
    }
    

    Das ist natürlich arg unschön, fehleranfällig und so, aber es war das erste, was mir einfiel, und es funktionierte nach recht geringem Aufwand zuverlässig. Wo ich kann (weil das Format statisch genug ist), nehme ich scanf(). Ich weiß, daß ich die Tokenisierung oben mit den Stringstreams einfacher haben kann, aber dann parse ich ja immer noch von Hand. Wie macht man sowas, wenn man sich den Parser sparen will? Schon dafür einen Parser-Generator? Die EBNF-Grammatik zu schreiben wäre nicht schneller gegangen. Oder reguläre Ausdrücke?



  • 1.) Keine Ahnung, wie statisch das Format ist oder wie flexible die Loesung sein soll. Das bestimmt im wesentlichen den Umfang.
    2.) Das ist kein C++, sieht mehr nach C mit Klassen Aus.
    3.) Virtuelle Methoden im Konstruktor wuerde ich als ungeeignet einstufen.
    4.) Warum Klassenhierarchie?



  • knivil schrieb:

    1.) Keine Ahnung, wie statisch das Format ist

    Mehr Syntaxelemente als in dem Beispiel gibt es nicht. Die Zahl der Spalten kann variieren, hängt vom Typen ab. Aber meine Frage kann ja vielleicht auch nutzbringend allgemein beantwortet werden; es ist nicht das einzige derartige Format, mit dem ich Umgang habe, und ich wüßte einfach gern, wie andere mit solchen Anforderungen umgehen.

    knivil schrieb:

    2.) Das ist kein C++, sieht mehr nach C mit Klassen Aus.

    Der geringe Teil, den ich herzeige, ist sogar C fast ohne Klassen. Und weil mich die Zweifel packten, ob ich denn damit so richtig liege, schrieb ich dieses Posting. Du hast das insoweit also richtig erfaßt.

    knivil schrieb:

    3.) Virtuelle Methoden im Header...^WKonstruktor wuerde ich für AnfängerWW als ungeeignet einstufen.

    Nett von dir, daß du die "virtuellen Methoden im Header" wieder rauseditiert hast. Auf meine Unfähigkeit zu schließen, weil du das Pattern nicht verstanden hast, fand ich nicht so toll 🙂

    Aber die virtuellen Methoden im Konstruktor, um die gehts ja eigentlich nicht - aber willst du vielleicht beschreiben, warum du das so siehst?



  • Oh, ich war noch nicht fertig, dann mache ich eben hier weiter:

    Normalerweise sind die Daten in einer Datei ja fuer irgendwas gut, beispielsweise Programmeinstellungen. Dann hat die Klasse Settings bzw. Configuration eine Methode/freie Funktion loadFromFile. Dagegen wirkt TextStorageReader ziemlich komisch. Dann haben die Daten ein bestimmtes Format, in diesem Beispiel eine Tabelle. Wie soll diese Tabelle spaeter benutzt werden, wie sieht die Datenstruktur aus? Deswegen hier ein Vorschlag anhand des Quelltext, nicht anhand des Problems.

    Im konkreten Fall:
    1.) Datei komplett einlesen: file.read(vector.data(), filesize)
    2.) Umwandeln in einen String (optional)
    3.) Direktes Suchen der Sektion: find("[mySection]")
    4.) Suche des Endes der Sektion: find("]") (mit Startposition aus Schritt 3)
    5.) Spezifisches Einlesen der Tabelle mit readTableFromMemory(pointer, size).

    // allgemeine Leselogik der Basisklasse; ruft die (einzig formatspezifischen) virtuellen Funktionen wie tryReadHeader() auf
    

    Es deutet an, dass die spezialisierte Methode von TextFileStorageReader im Basisklassenkonstruktor aufgerufen werden soll. Wird es aber nicht tun, da wird nur diejenige genommen, die fuer die Basisklasse in der virtuellen Methodentabelle eingetragen ist.

    Pattern nicht verstanden hast

    Pattern um eine einfache Textdatei zu lesen? Nein, mir kam nichteinmal der Gedanke, dass das eine Patternumsetzung werden soll. Falls dir ein spezielles Pattern vorschwebt, vergiss alles was ich als Anleitung beigetragen habe.



  • knivil schrieb:

    Im konkreten Fall:
    1.) Datei komplett einlesen: file.read(vector.data(), filesize)
    2.) Umwandeln in einen String (optional)

    Lass das Werner nicht lesen, der wird dich fragen wozu 😉 Man kann auch einfach über den Streambuf iterieren 🙂

    Man kann das Ganze relativ simpel parsen, da es ja ein rein Zeilenbasiertes Format zu sein scheint.

    audacia schrieb:

    Meine erste Lösung hier war im Prinzip C-Code in einer objektorientierten Klassenhierarchie

    Man muss nicht alles auf Teufel komm raus in eine Klassenhierarchie zwängen. Wenn das Modell keine Hierarchie hergibt, gibts eben keine. Und C-artiger Code ist nicht schlimm, wenn man sich im low-level-Bereich befindet (z.B. einzelne Zeichen parsen).



  • knivil schrieb:

    4.) Warum Klassenhierarchie?

    pumuckl schrieb:

    Man muss nicht alles auf Teufel komm raus in eine Klassenhierarchie zwängen. Wenn das Modell keine Hierarchie hergibt, gibts eben keine.

    Liebe Leute, vielleicht hätte ich einfach gar nichts über den Kontext schreiben und nur meinen C-fast-ohne-Klassen-Code herzeigen sollen, um den es mir geht 😉

    Um also die Spekulation abzukürzen, zeige ich kurz, was ich mache. Die Klassenhierarchie ist recht einfach: zunächst gibt es Klassen, die eine Funktion (ScalarFunction1D, Range1D) repräsentieren, die sehen etwa so aus wie man sich das vorstellt (Kapselung mal zwecks Übersichtlichkeit vernachlässigt):

    struct Range1D
    {
        double min, max;
        // ein Haufen nützlicher Methoden
    };
    struct DiscreteScalarFunction1D
    {
        Range1Df argumentRange, valueRange;
        double resolution;
        std::string argumentUnit, valueUnit;
        std::vector<double> data;
    };
    

    Nun finde ich es doof, wenn eine solche Datenstruktur selbst für ihre Serialisierung verantwortlich ist, da a) es gut sein kann, daß ich N verschiedene Formate und Serialisierungsmethoden für dieselben Datenstrukturen brauche, und ich b) viel von separation of concerns halte. Deshalb gibt es sowas wie Adapterklassen:

    struct SerializableEntity
    {
        std::string name;
        // virtuelle Methoden für das ganze Serialisierungszeugs
    };
    struct SerializableRange1D : SerializableEntity
    {
        Range1D range;
        // Implementierung der Serialisierung über virtuelle Methoden
    };
    

    ... die in einer Klasse Storage gesammelt werden:

    struct Storage
    {
        std::vector<SerializableRange1D> ranges1D;
        std::vector<SerializableDiscreteScalarFunction1D> scalarFunctions1D;
    };
    

    ... die wiederum von StorageReader und StorageWriter gelesen und geschrieben wird:

    class StorageReader
    {
    protected:
        void readEntitiesTo (Storage& storage);
        ...
    };
    class StorageWriter
    {
    protected:
        void writeEntitiesFrom (const Storage& storage);
        ...
    };
    

    Ich hoffe, knivil, das beantwortet auch deine Frage nach den Datenstrukturen. Was dein konkreter Vorschlag (komplett in den Speicher einlesen, in String umwandeln, Volltextsuche) mir an Vorteilen bringen soll, verstehe ich aber nicht. Was ist, wenn jemand mal irgendwelche höherdimensionalen Daten in einer Textdatei speichert, die als solche dadurch ein paar GB groß wird, obwohl die Daten im Speicher nur MBs groß sind? Dann kann ich sie nicht mehr einlesen. Und warum sollte ich in-memory-Suche nach Initialsymbolen benutzen, wenn ich das Ding einfach zeilenweise parsen kann, weil sich das Format so offensichtlich dafür eignet?

    pumuckl schrieb:

    Man kann das Ganze relativ simpel parsen, da es ja ein rein Zeilenbasiertes Format zu sein scheint.

    Klar; der Code, der sich konkret ums Lesen und Schreiben der Textdatei kümmert, hat nur so um die 130 Zeilen. Es ist nur die Kleinschrittigkeit dieses Codes, die mich ein wenig zweifeln macht.

    Vielleicht könnte man eine Art lightweight-Lexer/Tokenizer davorschalten; ähnlich wie die Stringstreams, aber nicht so restriktiv und eher zeichenmengenbasiert. Damit werde ich mal rumprobieren, aber es ist eher nichts C++-typisches, sowas sieht man eher in Pascal, wo es Typen wie "set of (Ansi)Char" gibt.

    pumuckl schrieb:

    Und C-artiger Code ist nicht schlimm, wenn man sich im low-level-Bereich befindet (z.B. einzelne Zeichen parsen).

    Wegen ein bißchen C bekomme ich keine Komplexe, aber danke für die Bestärkung 🙂

    knivil schrieb:

    // allgemeine Leselogik der Basisklasse; ruft die (einzig formatspezifischen) virtuellen Funktionen wie tryReadHeader() auf
    

    Es deutet an, dass die spezialisierte Methode von TextFileStorageReader im Basisklassenkonstruktor aufgerufen werden soll.

    Nein, tut es nicht, der Kommentar bezieht sich klar auf den nebenstehenden Aufruf der Basisklassenfunktion readEntitiesTo() im Konstruktor der Leaf-Klasse:

    audacia schrieb:

    TextFileStorageReader (const char* fileName)
         : file (fileName, "rt")
        {
            readEntitiesTo (storage); // allgemeine Leselogik der Basisklasse; ruft die (einzig formatspezifischen) virtuellen Funktionen wie tryReadHeader() auf
        }
    

    knivil schrieb:

    Pattern nicht verstanden hast

    Pattern um eine einfache Textdatei zu lesen?

    Neinnein. Daß du den Kommentar mit den virtuellen Methoden im Header wieder entfernt hast, schien mir anzudeuten, daß dir inzwischen aufgegangen ist, warum TextStorageReader natürlich nicht im Header steht. Ob man es als Pattern bezeichnen kann, wenn man single-purpose-Klassen hinter Funktionen wie readFromTextFile() versteckt, stelle ich zur Debatte, aber pimpl macht auch sowas ähnliches und heißt sich Pattern, also warum nicht.



  • audacia schrieb:

    a) es gut sein kann, daß ich N verschiedene Formate und Serialisierungsmethoden für dieselben Datenstrukturen brauche, und ich b) viel von separation of concerns halte. Deshalb gibt es sowas wie Adapterklassen:

    a) Ob nun N freie Funktionen oder N Klassen mit ein paar virtuellen Methoden, who cares?
    b)Ja, das ist was gutes, d.h. aber nicht, dass der Usus von Java auch das beste fuer C++ ist. Adapterklassen als logische Konsequenz halte ich fuer falsch.

    mir an Vorteilen bringen soll, verstehe ich aber nicht.

    Es ist einfach, nah an C und erspart dir das Suchen in einem Stream.

    Was ist, wenn jemand mal ..

    Definiere Aunwendungsszenario, Anforderungen und moegliche Erweiterungen. Dann sehen wir weiter. Normalerweise wird niemand irgendwie sowas wollen. Und wenn, dann ist es nicht sinnvoll, alles durch ein Konzept erschlagen zu wollen.

    weil sich das Format so offensichtlich dafür eignet

    Widerspricht sich mit "N verschiedenen Formaten". Sorry, ich kann nicht in deinen Kopf sehen, ich weiss nicht was du dir denkst oder offensichtlich fuer dich ist.

    single-purpose-Klassen hinter Funktionen wie readFromTextFile()

    Nein, man braucht keine Klasse, die Funktionalitaet kann auch gleich direkt implementiert werden.



  • audacia schrieb:

    Vielleicht könnte man eine Art lightweight-Lexer/Tokenizer davorschalten; ähnlich wie die Stringstreams, aber nicht so restriktiv und eher zeichenmengenbasiert. Damit werde ich mal rumprobieren, aber es ist eher nichts C++-typisches, sowas sieht man eher in Pascal, wo es Typen wie "set of (Ansi)Char" gibt.

    Du könntest dir mal boost.Tokenizer anschauen, vielleicht ist das was für dich 🙂

    Ansonsten sehe ich aktuell nichts verbesserungswürdiges an deiner Umsetzung. 130 Zeilen C-Code als Parser sind überschaubar genug, da muss man keinen großen VOerkill betreiben um das "schön", "elegant" oder sonstwie aussehen zu lassen. Der Code sieht lesbar und verständlich aus, er macht ja offenbar was er soll - wenn das so bleibt kann der Code auch so bleiben 😉

    @knivil: "suchen im stream" scheint mir bei der simplen Syntax keine Anforderung zu sein. Bzgl. möglicher Erweiterungen stimme ich zu - über die sollte man sich erst den Kopf zerbrechen, wenn absehbar ist, dass sie kommen. Ebenso ack bzgl. packen von einer Funktion in eine Klasse. C++ ist eben nicht nur OOP, man kann auch wunderbar prozedural (oder auch in gewissem Sinne funktional) programmieren.



  • pumuckl schrieb:

    audacia schrieb:

    Vielleicht könnte man eine Art lightweight-Lexer/Tokenizer davorschalten; ähnlich wie die Stringstreams, aber nicht so restriktiv und eher zeichenmengenbasiert. Damit werde ich mal rumprobieren, aber es ist eher nichts C++-typisches, sowas sieht man eher in Pascal, wo es Typen wie "set of (Ansi)Char" gibt.

    Du könntest dir mal boost.Tokenizer anschauen, vielleicht ist das was für dich 🙂

    Auf den ersten Blick scheint es zu passen, auch wenn dieser charset-basierte Delphi-Tokenizer, den ich mir mal geschrieben hatte, irgendwie intuitiver war.
    Mal schauen, ob BCC mit Boost.Tokenizer klarkommt; die Header ziehen offenbar Teile von MPL hinein, was mich nicht so glücklich macht 😕

    pumuckl schrieb:

    Der Code sieht lesbar und verständlich aus, er macht ja offenbar was er soll - wenn das so bleibt kann der Code auch so bleiben 😉

    Das ist ein Wort 😉

    pumuckl schrieb:

    Ebenso ack bzgl. packen von einer Funktion in eine Klasse. C++ ist eben nicht nur OOP, man kann auch wunderbar prozedural (oder auch in gewissem Sinne funktional) programmieren.

    Im Allgemeinen ist dagegen nichts anzuwenden, aber in meinem Fall halte ich die Kritik für uninformiert und fehlgeleitet. Die Basisklasse (StorageReader/Writer) enthält die Serialisierungslogik, die Leaf-Klasse (TextFileStorageReader/Writer) implementiert das Lesen/Schreiben in einem konkreten Format mithilfe virtueller Funktionen. Wie und warum sollte ich das jetzt ohne Klasse implementieren?

    @knivil: ich glaube, es wäre produktiv, wenn du aufhören würdest, mir krampfhaft irgendwelche Fehler nachzuweisen. Seit deinem ersten Posting hier tust du im Grunde nichts anderes, als meine Frage schnell beiseitezuschieben (zu wenig Anforderungen; wie sieht die Datenstruktur aus; definiere ...) und dich fortan damit zu beschäftigen, mir zu zeigen, was ich angeblich alles falsch mache (virtuelle Funktionen im Header - ach nein, doch nicht; virtuelle Funktionen im Basisklassenkonstruktor - ach nein, doch nicht; Serialisierung gehört in die Klasse, die serialisiert wird - ach nein, doch nicht; aber wer Adapterklassen baut, macht Java, die Konsequenz ist in C++ falsch; du willst alles durch ein Konzept erschlagen, das macht man nicht; du brauchst diese Klasse nicht, kannst du auch ohne implementieren).



  • Nein, ich sage nur: ESerialisierungsframework fuer diesen Anwendungsfall ist kontraproduktiv. Das ist mein Vorschlag + Begruendung. Wenn es dir nicht passt, dann ignoriere ihn. Trotzdem habe ich den Eindruck, dass dich auf ein Konzept eingeschossen hast. Deswegen die Reibungspunkte.

    Beispiel, deine Storage-Klasse: Warum ein Array von serialisierbaren Objekten hier SerializableRange1D halten? Du kannst auch gleich ein Array von Range1D benutzen, beim Serialisieren in eine Datei wegen mir dein Adapterobjekt ad hoc konstruieren und abspeichern. D.h. der Zweck deiner Storage-Klasse ist mir nicht ersichtlich. Ich haette es anders gemacht, was deine urspruengliche Frage beantwortet.



  • 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


Log in to reply