diff --git a/autogen/.gitignore b/autogen/.gitignore deleted file mode 100644 index 5de84783e..000000000 --- a/autogen/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -json2class.exe -test diff --git a/autogen/prototype.json b/autogen/prototype.json index c09994388..527936a9e 100644 --- a/autogen/prototype.json +++ b/autogen/prototype.json @@ -11,7 +11,10 @@ " Optional (defaults to GNDStk).", "", "JSONDir", - " Directory where the listed .json input files are located", + " Directory where the .json input files are located", + "", + "JSONFiles", + " The .json input files", "", "TO AUTOGENERATE THE PROTOTYPE IN THE REAL GNDStk HIERARCHY", " Set Path to ../.. if you run json2class.exe from within", diff --git a/autogen/v1.9.json b/autogen/v1.9.json index fdc6ad62e..83fc57b83 100644 --- a/autogen/v1.9.json +++ b/autogen/v1.9.json @@ -1,5 +1,5 @@ { - "GNDSDir": "test", + "Path": "../..", "Version": "v1.9", "JSONDir": "v1.9", diff --git a/src/GNDStk.hpp b/src/GNDStk.hpp index 5b36be20e..338f6972f 100644 --- a/src/GNDStk.hpp +++ b/src/GNDStk.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/src/GNDStk/Component.hpp b/src/GNDStk/Component.hpp index f96d03d9e..a8b1dfc36 100644 --- a/src/GNDStk/Component.hpp +++ b/src/GNDStk/Component.hpp @@ -94,6 +94,26 @@ class Component : public BlockData } } + // has + // Usable in C++ "compile-time if" (a.k.a. "if constexpr") statements + template< + class EXTRACTOR, class THIS = DERIVED, + class = decltype(std::declval()(THIS{})) + > + static constexpr bool has(const Lookup &) + { + return true; + } + + template< + class EXTRACTOR, bool F, + class = std::enable_if_t + > + static constexpr bool has(const Lookup &) + { + return false; + } + // Component << string // Meaning: read the string's content (currently XML, JSON, or HDF5) into // an object of the Component's DERIVED class. Uses Node's << string, which diff --git a/src/GNDStk/HDF5.hpp b/src/GNDStk/HDF5.hpp index 802defb0c..d8553809b 100644 --- a/src/GNDStk/HDF5.hpp +++ b/src/GNDStk/HDF5.hpp @@ -9,7 +9,10 @@ class HDF5 { public: - static inline bool flat = true; + // todo: It's possible that we'll want these to be non-static, so that they + // can differ between HDF5 objects. That might, however, constitute excessive + // generality. Think about this. + static inline bool reduced = true; static inline bool typed = true; // data diff --git a/src/GNDStk/HDF5/src/detail.hpp b/src/GNDStk/HDF5/src/detail.hpp index bb8041184..34fe3779c 100644 --- a/src/GNDStk/HDF5/src/detail.hpp +++ b/src/GNDStk/HDF5/src/detail.hpp @@ -13,9 +13,21 @@ inline std::string guessType(std::istringstream &iss, const std::string &type) iss.clear(); iss.seekg(0); + // Below, the extra test involving iss.peek() proved to be necessary in + // a couple of situations. Without it, a "date" string like, for example, + // 2011-10-15, actually reads into three ints: 2011, -10, and -15, so that + // the type guesser thinks "array of ints". One might have thought that + // the >> operator would want whitespace between successive ints, but it + // doesn't. :-/ A similar thing happened with, for example, a "version" + // string like "8.0.1", which without the extra test looked to the type + // guesser as two doubles. To fix this, we now directly check that after + // any value is successfully read, we see either whitespace or EOF. (The + // EOF check is done first; it's no doubt faster, and we anticipate that + // many times we're checking short strings with just one value in them, + // so that the EOF is likely to be there.) T value; std::size_t count = 0; for ( ; !iss.eof() ; ++count) - if (!(iss >> value)) + if (!(iss >> value && (iss.peek() == EOF || isspace(iss.peek())))) return ""; return count ? count > 1 ? type+"s" : type : ""; } diff --git a/src/GNDStk/HDF5/test/HDF5.test.cpp b/src/GNDStk/HDF5/test/HDF5.test.cpp index 92a40df51..f05d0fdc6 100644 --- a/src/GNDStk/HDF5/test/HDF5.test.cpp +++ b/src/GNDStk/HDF5/test/HDF5.test.cpp @@ -6,12 +6,355 @@ using namespace njoy::GNDStk; + +// ----------------------------------------------------------------------------- +// writeReadHDF5 +// Helper function +// ----------------------------------------------------------------------------- + +void writeReadHDF5( + const Tree &oldTree, + const bool reduced, const bool typed, + const std::string &correct, + const std::string &baseName +) { + // Set flags + HDF5::reduced = reduced; + HDF5::typed = typed; + + // Compute file names + const std::string newFile = baseName + ".h5"; + const std::string vettedFile = "correct-" + newFile; + + // Write the Tree to an HDF5 file + oldTree.write(newFile); + + // Read from the HDF5 file into a brand-new Tree + Tree newTree(newFile); + + // Test #1. Ensure that the newly-read Tree prints (in our debug format) + // in exactly the same way as the original Tree did. Given that the present + // function is called with each combination of the "reduced" and "typed" + // flags, which affect precisely how the HDF5 file is created, this tests + // that, regardless of which flags we used when writing the HDF5 file, the + // reading process correctly recovers what we originally had in the internal + // Tree format. IMPORTANT NOTE: It's actually not hard to create situations + // where the Tree would look slightly different. Leading or trailing white + // space in #pcdata Nodes could, in some cases, disappear during the process + // of write-to-HDF5-then-read-back-in. Also, in the HDF5::typed cases, + // strings that look like floating-point numbers may be converted to doubles + // and back again. That could lead to differences, depending on how floating- + // point numbers are written. We've *tried* to write our test files in such + // a way that these issues won't appear here; if they do arise, somehow, + // then we're sure we'll hear about it. :-) + std::ostringstream oss; + newTree.sort().top().write(oss,"text"); + CHECK(oss.str() == correct); + + // Test #2. Ensure that file newFile (written above, from the original Tree) + // is identical to the vetted HDF file vettedFile that we put into the git + // repository. BUT...we'll actually have this test if'd out in the repo! We + // believe that that's the safe thing to do, because HDF5 is a binary format. + // We can't know if the HDF5 files produced on any particular platform will + // compare favorably with the vetted binary files we've placed into the repo. + // So, this is more a "know what you're doing" test that can be put back in, + // say by writing #if 1, by people who know how to deal with this situation. + // ADDITIONAL REMARK. Even on the same computer, I'm seeing non-comparable + // results, between runs, for each of the two HDF5::reduced == true cases. + // The offending files are equal in size (for what limited worth that has), + // and appear to be the same when I compare the output that the h5dump tool + // gives for each. (Also: in terms of recovering original Tree information - + // our Test #1 above - the newly created files check out.) todo: Determine + // what's going on with the vetted-vs-new-file comparison. Could a timestamp, + // or some other non-constant construct, be going into the file? And why does + // this happen only when HDF5::reduced == true? That flag simply means that + // the HDF5 writer simplifies certain special Tree constructs (relating to + // #cdata, #comment, and #pcdata). (In a predictable and reversible manner, + // of course, hence Test #1 working.) UPDATE: h5diff says the files match. + // Also, google [hdf5 file comparison] to see some relevant discussions. +#if 0 + std::ifstream ifsWant(vettedFile); + std::stringstream bufWant; + bufWant << ifsWant.rdbuf(); + std::cout << "bufWant.str().size() == " << bufWant.str().size() << std::endl; + + std::ifstream ifsHave(newFile); + std::stringstream bufHave; + bufHave << ifsHave.rdbuf(); + std::cout << "bufHave.str().size() == " << bufHave.str().size() << std::endl; + + CHECK(bufWant.str() == bufHave.str()); +#endif +} + + // ----------------------------------------------------------------------------- // SCENARIO // ----------------------------------------------------------------------------- -SCENARIO("Testing GNDStk HDF5") { +SCENARIO("Testing GNDStk HDF5, Part I") { + WHEN("We create a Tree from an XML with various constructs in it") { + // Read Tree + Tree tree("various.xml"); + + // Write to a string, in our simple debug format + std::ostringstream oss; + tree.sort().top().write(oss,"text"); + const std::string correct = oss.str(); + + // Write/read to/from HDF5, for each combination of the available flags + // for doing so: HDF5::reduced = false/true (x) HDF5::typed = false/true + writeReadHDF5(tree, false, false, correct, "raw-string"); + writeReadHDF5(tree, false, true, correct, "raw-typed"); + writeReadHDF5(tree, true, false, correct, "reduced-string"); + writeReadHDF5(tree, true, true, correct, "reduced-typed"); + } +} + + +// ----------------------------------------------------------------------------- +// SCENARIO +// ----------------------------------------------------------------------------- + +SCENARIO("Testing GNDStk HDF5, Part II") { + + // read an HDF5 + HDF5 h("n-069_Tm_170-covar.hdf5"); + + // construct a Tree from the HDF5 + Tree t(h); + + // the Tree should be non-empty + CHECK(!t.empty()); + + // ------------------------ + // clear + // ------------------------ + + WHEN("We clear() an HDF5, and convert() it to a Tree") { + convert(h.clear(),t); + THEN("The Tree should have only an empty declaration node") { + CHECK(t.children.size() == 1); + CHECK(t.has_decl()); + CHECK(t.decl().name == "#hdf5"); + CHECK(t.decl().metadata.size() == 0); + CHECK(t.decl().children.size() == 0); + } + } + + // ------------------------ + // empty + // ------------------------ + + WHEN("We call empty() on a clear()d HDF5") { + h.clear(); + CHECK(h.empty()); + } + + // ------------------------ + // constructors + // ------------------------ + + // default + WHEN("We call HDF5's default constructor") { + const HDF5 h; + CHECK(h.empty()); + } + + // move + WHEN("We call HDF5's move constructor") { + const HDF5 h(HDF5{"n-069_Tm_170-covar.hdf5"}); + CHECK(!h.empty()); + // and ensure it moved correctly + std::ostringstream oss1; oss1 << HDF5{"n-069_Tm_170-covar.hdf5"}; + std::ostringstream oss2; oss2 << h; + CHECK(oss1.str() == oss2.str()); + } + + // copy + WHEN("We call HDF5's copy constructor") { + const HDF5 a("n-069_Tm_170-covar.hdf5"); + const HDF5 h(a); + CHECK(!h.empty()); + // and ensure it copied correctly + std::ostringstream oss1; oss1 << a; + std::ostringstream oss2; oss2 << h; + CHECK(oss1.str() == oss2.str()); + } + + // Note: below, HDF5::typed sometimes had to be set to false (as opposed + // to its default, true) in order for the relevant test to pass. The issue + // with HDF5::typed == true wasn't really a failure, in principle; rather, + // it related to how floating-point numbers look, relative to an original + // text form, after being converted to binary, read back, and then written + // again as text. An example we actually saw here: begin with "1.35e-4", + // see "0.000135" later. Using HDF5::typed == false means values will be + // kept in string form, so that we don't see this problem. + + // For the next three tests... + HDF5::reduced = true; HDF5::typed = false; + + // from XML + WHEN("We construct an HDF5 from an XML") { + const XML x("n-069_Tm_170-covar.xml"); + const HDF5 h(x); + THEN("They should produce equivalent Trees") { + CHECK(!h.empty()); + CHECK(Tree(x) == Tree(h)); + } + } + + // from Tree + HDF5::reduced = true; HDF5::typed = false; + WHEN("We construct an HDF5 from a Tree") { + const Tree t("n-069_Tm_170-covar.xml"); + const HDF5 h(t); + THEN("It should produce an equivalent Tree") { + CHECK(!h.empty()); + CHECK(t == Tree(h)); + } + } + + // from file + HDF5::reduced = true; HDF5::typed = true; + WHEN("We construct an HDF5 from a file") { + const HDF5 h("n-069_Tm_170-covar.hdf5"); + THEN("It should produce an equivalent to the Tree made from the file") { + CHECK(!h.empty()); + std::ostringstream oss1; oss1 << Tree("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss2; oss2 << Tree(h); + CHECK(oss1.str() == oss2.str()); + } + } + + // For here on out, it seems that we can return to the default... + HDF5::reduced = true; HDF5::typed = true; + + // from istream + WHEN("We construct an HDF5 from an istream") { + std::ifstream ifs("n-069_Tm_170-covar.hdf5"); + const HDF5 h(ifs); + THEN("It should produce an equivalent to the Tree made from the file") { + CHECK(!h.empty()); + std::ostringstream oss1; oss1 << Tree("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss2; oss2 << Tree(h); + CHECK(oss1.str() == oss2.str()); + } + } + + // ------------------------ + // assignment + // ------------------------ + + // move + WHEN("We call HDF5's move assignment") { + HDF5 h; + h = HDF5{"n-069_Tm_170-covar.hdf5"}; + CHECK(!h.empty()); + // and ensure it moved correctly + std::ostringstream oss1; oss1 << HDF5{"n-069_Tm_170-covar.hdf5"}; + std::ostringstream oss2; oss2 << h; + CHECK(oss1.str() == oss2.str()); + } + + // copy + WHEN("We call HDF5's copy assignment") { + const HDF5 a("n-069_Tm_170-covar.hdf5"); + HDF5 h; + h = a; + CHECK(!h.empty()); + // and ensure it copied correctly + std::ostringstream oss1; oss1 << a; + std::ostringstream oss2; oss2 << h; + CHECK(oss1.str() == oss2.str()); + } + + // ------------------------ + // read + // ------------------------ + + // from istream + WHEN("We read an HDF5 from an istream") { + std::ifstream ifs("n-069_Tm_170-covar.hdf5"); + const HDF5 h(ifs); + THEN("It should produce an equivalent to the Tree made from the file") { + CHECK(!h.empty()); + std::ostringstream oss1; oss1 << Tree("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss2; oss2 << Tree(h); + CHECK(oss1.str() == oss2.str()); + } + } + + // from file + WHEN("We read an HDF5 from a file") { + const HDF5 h("n-069_Tm_170-covar.hdf5"); + THEN("It should produce an equivalent to the Tree made from the file") { + CHECK(!h.empty()); + std::ostringstream oss1; oss1 << Tree("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss2; oss2 << Tree(h); + CHECK(oss1.str() == oss2.str()); + } + } + + // ------------------------ + // string_real_hdf5, for + // use in upcoming tests + // ------------------------ + + // Read our test .hdf5 file into a string, which we'll call string_real_hdf5. + // For the JSON tests analogous to our HDF5 tests below, we used a *literal* + // string called string_real_json. That isn't so viable here, because HDF5 + // is binary. We *could* use some command-line tool to convert a binary file + // into a form that's usable directly in C++ code. But then there's the issue + // of compute platforms, across which binaries aren't generally compatible + // anyway. The behavior of what we're testing, below, should be such that the + // original file's contents, placed here into string_real_hdf5, compare as in + // the CHECK calls below. + std::ifstream ifs("n-069_Tm_170-covar.hdf5"); // original file + std::stringstream buf; buf << ifs.rdbuf(); // read into a stringstream + const std::string string_real_hdf5 = buf.str(); // make into a string + + // ------------------------ + // write + // ------------------------ + + // to ostream + WHEN("We write an HDF5 to an ostream") { + const HDF5 h("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss; + h.write(oss); + THEN("We can check that the result is what we expect") { + // Now, compare the HDF5 that was written to the ostream (h.write(oss), + // above - the thing we're testing) with the original file's contents. + CHECK(oss.str() == string_real_hdf5); + } + } + + // ------------------------ + // stream I/O + // ------------------------ - /// fixme adapt code from the corresponding JSON test + // operator>> + WHEN("We do istream >> HDF5") { + std::ifstream ifs("n-069_Tm_170-covar.hdf5"); + HDF5 h; + ifs >> h; + THEN("It should give what we expect") { + // Well, we also end up testing << here, which we test + // more directly in the next block, below + std::ostringstream oss; + oss << h; + CHECK(oss.str() == string_real_hdf5); + } + } + // operator<< + WHEN("We do ostream << HDF5") { + const HDF5 h("n-069_Tm_170-covar.hdf5"); + std::ostringstream oss; + oss << h; + THEN("It should give what we expect") { + CHECK(oss.str() == string_real_hdf5); + } + } } diff --git a/src/GNDStk/HDF5/test/detail.test.cpp b/src/GNDStk/HDF5/test/detail.test.cpp index 6062818d1..ff72d08fc 100644 --- a/src/GNDStk/HDF5/test/detail.test.cpp +++ b/src/GNDStk/HDF5/test/detail.test.cpp @@ -190,7 +190,8 @@ SCENARIO("Testing HDF5 detail:: functionality") { } // ------------------------ - // For use in double[s] + // Some values for use + // in the double[s] // and string[s] cases // ------------------------ @@ -308,4 +309,23 @@ SCENARIO("Testing HDF5 detail:: functionality") { } } + // ------------------------ + // The cases initially + // foiled the type guesser, + // so they're tested now + // ------------------------ + + GIVEN("A couple of patterns that are common in GNDS files") { + WHEN("We call guessType()") { + THEN("We get the expected answers") { + // A typical "date" metadatum; + // is a string, NOT the ints 2011, -10, and -1 + CHECK(guessType("2011-10-01") == "string"); + // A typical "version" metadatum; + // is a string, NOT the doubles 8.0 and .1 + CHECK(guessType("8.0.1") == "string"); + } + } + } + } // SCENARIO diff --git a/src/GNDStk/HDF5/test/resources/.gitignore b/src/GNDStk/HDF5/test/resources/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/src/GNDStk/HDF5/test/resources/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/src/GNDStk/HDF5/test/resources/correct-raw-string.h5 b/src/GNDStk/HDF5/test/resources/correct-raw-string.h5 new file mode 100644 index 000000000..66d9df68c Binary files /dev/null and b/src/GNDStk/HDF5/test/resources/correct-raw-string.h5 differ diff --git a/src/GNDStk/HDF5/test/resources/correct-raw-typed.h5 b/src/GNDStk/HDF5/test/resources/correct-raw-typed.h5 new file mode 100644 index 000000000..0cd2ed4aa Binary files /dev/null and b/src/GNDStk/HDF5/test/resources/correct-raw-typed.h5 differ diff --git a/src/GNDStk/HDF5/test/resources/correct-reduced-string.h5 b/src/GNDStk/HDF5/test/resources/correct-reduced-string.h5 new file mode 100644 index 000000000..64a765579 Binary files /dev/null and b/src/GNDStk/HDF5/test/resources/correct-reduced-string.h5 differ diff --git a/src/GNDStk/HDF5/test/resources/correct-reduced-typed.h5 b/src/GNDStk/HDF5/test/resources/correct-reduced-typed.h5 new file mode 100644 index 000000000..8243a9a1e Binary files /dev/null and b/src/GNDStk/HDF5/test/resources/correct-reduced-typed.h5 differ diff --git a/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.hdf5 b/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.hdf5 new file mode 100644 index 000000000..7d3ddfcf7 Binary files /dev/null and b/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.hdf5 differ diff --git a/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.xml b/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.xml new file mode 100644 index 000000000..3ea648853 --- /dev/null +++ b/src/GNDStk/HDF5/test/resources/n-069_Tm_170-covar.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + 0.015 0 0 0 4.5e-5 0.015 3e-2 0 0 0 1.35e-4 0.015 2e-2 0 0 0 1.5e-3 0.012 5e-2 0 0 0 1.875e-3 6e-2 5e-2 0 0 0 1.05e-4 0.015 0.1 0 0 0 6e-4 0.012 0.1 0 0 0 2.25e-4 0.012 0.2 0 0 0 5.25e-3 0.012 0.2 0 0 0 3.45e-3 0.012 0.3 0 0 0 4.5e-4 0.012 0.3 0 0 0 3e-3 0.012 0.4 0 0 0 9e-3 0.012 0.4 0 0 0 1.425e-3 0.012 + + + + + diff --git a/src/GNDStk/HDF5/test/resources/various.xml b/src/GNDStk/HDF5/test/resources/various.xml new file mode 100644 index 000000000..18b7ca3e9 --- /dev/null +++ b/src/GNDStk/HDF5/test/resources/various.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + 1.2 + 1.2 3.45 6.789 + + 1 + 12 345 6789 + + + + + + ab + cd efg hijk + + + + + + + + 1 2 3 4 + 9.87 6.54 3.21 + + + a b c d e f g + + diff --git a/src/GNDStk/Node/src/write.hpp b/src/GNDStk/Node/src/write.hpp index 614cfceb5..8c149388a 100644 --- a/src/GNDStk/Node/src/write.hpp +++ b/src/GNDStk/Node/src/write.hpp @@ -80,8 +80,7 @@ std::ostream &write(std::ostream &os, const int level) const // with filename go through ostream first, and call the earlier write() helper // with const int level). Decide if there's any reason to keep it. If we do keep // it, then it needs to be exercised in the test suite, which it isn't now. -#if 0 - +/* bool write(const std::string &filename, const int level) const { // open file @@ -101,8 +100,7 @@ bool write(const std::string &filename, const int level) const // done return true; } - -#endif +*/ diff --git a/src/GNDStk/convert/src/HDF5.hpp b/src/GNDStk/convert/src/HDF5.hpp index de2c2d9a9..2fedf3e25 100644 --- a/src/GNDStk/convert/src/HDF5.hpp +++ b/src/GNDStk/convert/src/HDF5.hpp @@ -21,7 +21,7 @@ inline bool convert(const Node &node, HDF5 &h, const std::string &name) // Probably a regular Node... if (node.name != "") { - const bool ret = detail::Node2HDF5(node,*h.filePtr); + const bool ret = detail::node2hdf5(node,*h.filePtr); h.filePtr->flush(); return ret; } @@ -67,7 +67,7 @@ inline bool convert(const Node &node, HDF5 &h, const std::string &name) ); log::function(context); } - const bool ret = detail::Node2HDF5(*c,*h.filePtr); + const bool ret = detail::node2hdf5(*c,*h.filePtr); h.filePtr->flush(); if (!ret) return false; diff --git a/src/GNDStk/convert/src/Tree.hpp b/src/GNDStk/convert/src/Tree.hpp index 8518ad6ce..37699ae20 100644 --- a/src/GNDStk/convert/src/Tree.hpp +++ b/src/GNDStk/convert/src/Tree.hpp @@ -267,11 +267,11 @@ inline bool convert(const HDF5 &h, Node &node, const bool decl) // into the Node's "#hdf5" child that would have been created above if (decl) for (auto &attrName : group.listAttributeNames()) - if (!detail::HDF5attr2Node(group.getAttribute(attrName),*declnode)) + if (!detail::attr2node(group.getAttribute(attrName),*declnode)) return false; // visit the rest of "/" - if (!detail::HDF52Node(group, "/", node, !decl)) + if (!detail::hdf52node(group, "/", node, !decl)) return false; } catch (...) { log::function("convert(HDF5,Node)"); diff --git a/src/GNDStk/convert/src/detail-hdf52node.hpp b/src/GNDStk/convert/src/detail-hdf52node.hpp index dad950ab4..b5d2e9799 100644 --- a/src/GNDStk/convert/src/detail-hdf52node.hpp +++ b/src/GNDStk/convert/src/detail-hdf52node.hpp @@ -1,12 +1,24 @@ +// Helper: error_hdf52node +inline void error_hdf52node(const std::string &message) +{ + log::error( + "Internal error in hdf52node(HighFive::Group, std::string, Node):\n" + "Message: \"{}\".", + message + ); + throw std::exception{}; +} + + // ----------------------------------------------------------------------------- -// HDF5attr2Node +// attr2node // For HighFive::Attribute // ----------------------------------------------------------------------------- -// Helper: attrTYPE2node +// helper template -bool attrTYPE2node(const HighFive::Attribute &attr, NODE &node) +bool attr2node(const HighFive::Attribute &attr, NODE &node) { if (attr.getDataType() == HighFive::AtomicType{}) { const std::string attrName = attr.getName(); @@ -35,9 +47,9 @@ bool attrTYPE2node(const HighFive::Attribute &attr, NODE &node) return false; } -// HDF5attr2Node +// attr2node template -bool HDF5attr2Node(const HighFive::Attribute &attr, NODE &node) +bool attr2node(const HighFive::Attribute &attr, NODE &node) { // HighFive's documentation leaves much to be desired. I used what I found // in HighFive/include/highfive/bits/H5DataType_misc.hpp to get an idea of @@ -46,56 +58,60 @@ bool HDF5attr2Node(const HighFive::Attribute &attr, NODE &node) // well. It didn't have long double, which I'd have liked, but that's fine. // I won't bother with fixed-length strings or with std::complex right now, // but will support the rest. - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; + if (attr2node(attr,node)) return true; log::error( - "HDF5 Attribute \"{}\"'s DataType \"{}\" is not handled at this time.", + "HDF5 Attribute \"{}\"'s DataType \"{}\" is not supported at this time.", attr.getName(), attr.getDataType().string()); - log::function("HDF5attr2Node(HighFive::Attribute, Node)"); + log::function("attr2node(HighFive::Attribute, Node)"); return false; } // ----------------------------------------------------------------------------- -// HDF5data2node +// dset2node // For HighFive::DataSet // ----------------------------------------------------------------------------- -// Helper: dataTYPE2node +// helper template -bool dataTYPE2node(const HighFive::DataSet &data, NODE &node) +bool dset2node(const HighFive::DataSet &dset, NODE &node) { - if (data.getDataType() == HighFive::AtomicType{}) { - // Remarks as in the similar helper function attrTYPE2node()... - const std::size_t dataSize = data.getElementCount(); + if (dset.getDataType() == HighFive::AtomicType{}) { + // Remarks as in the similar helper function attr2node()... + const std::size_t dataSize = dset.getElementCount(); if (dataSize == 1) { T scalar; - data.read(scalar); - node.add("#pcdata").add("#text",scalar); + dset.read(scalar); + node.name == "#pcdata" + ? node.add("#text",scalar) + : node.add("#pcdata").add("#text",scalar); return true; } if constexpr (!std::is_same_v) { std::vector vector; vector.reserve(dataSize); - data.read(vector); - node.add("#pcdata").add("#text",vector); + dset.read(vector); + node.name == "#pcdata" + ? node.add("#text",vector) + : node.add("#pcdata").add("#text",vector); return true; } } @@ -103,73 +119,61 @@ bool dataTYPE2node(const HighFive::DataSet &data, NODE &node) return false; } -// HDF5data2node +// dset2node template -bool HDF5data2node( - const HighFive::DataSet &data, const std::string &dataName, - NODE &node +bool dset2node( + const HighFive::Group &group, const std::string &dsetName, + NODE &parent ) { - // node name - node.name = dataName; + Node &node = parent.add(beginsin(dsetName,"#pcdata") ? "#pcdata" : dsetName); - // the data set's attributes - for (auto &attrName : data.listAttributeNames()) - if (!HDF5attr2Node(data.getAttribute(attrName), node)) + // the DataSet's attributes + const HighFive::DataSet &dset = group.getDataSet(dsetName); + for (const std::string &attrName : dset.listAttributeNames()) + if (!attr2node(dset.getAttribute(attrName), node)) return false; - // the data set's data - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; + // the DataSet's data + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; + if (dset2node(dset,node)) return true; log::error( - "HDF5 DataSet \"{}\"'s DataType \"{}\" is not handled at this time.", - dataName, data.getDataType().string()); - log::function("HDF5data2node(HighFive::DataSet, dataName, Node)"); + "HDF5 DataSet \"{}\"'s DataType \"{}\" is not supported at this time.", + dsetName, dset.getDataType().string()); + log::function("dset2node(HighFive::DataSet, dsetName, Node)"); return false; } // ----------------------------------------------------------------------------- -// HDF52Node +// hdf52node // ----------------------------------------------------------------------------- -// Helper: error_HDF52Node -inline void error_HDF52Node(const std::string &message) -{ - log::error( - "Internal error in HDF52Node(HighFive::Group, std::string, Node):\n" - "Message: \"{}\".", - message - ); - throw std::exception{}; -} - -// HDF52Node template -bool HDF52Node( +bool hdf52node( const HighFive::Group &group, const std::string &groupName, NODE &node, const bool requireEmpty = true ) { static const std::string context = - "HDF52Node(HighFive::Group, std::string, Node)"; + "hdf52node(HighFive::Group, std::string, Node)"; - // The node sent here should be fresh, ready to receive entries + // the node sent here should be fresh, ready to receive entries... if (requireEmpty && !node.empty()) - error_HDF52Node("!node.empty()"); + error_hdf52node("!node.empty()"); // ------------------------ // HDF5 group name @@ -185,31 +189,59 @@ bool HDF52Node( // ==> metadata // ------------------------ - // if "/" then attributes were handled, in a special way, by the caller - if (groupName != "/") - for (auto &attrName : group.listAttributeNames()) - if (!HDF5attr2Node(group.getAttribute(attrName), node)) - return false; + // if "/", then attributes were handled, in a special way, by the caller + if (groupName != "/") { + for (const std::string &attrName : group.listAttributeNames()) { + if (attrName == "#nodeName") { + // #nodeName + // Handled not as a regular attribute, but as the present node's + // true name. The following line is basically a compressed version + // of attr2node (see early in this file). It assumes, + // in short, that this #nodeName attribute is one (not a vector of) + // T == std::string. And that's precisely what it should be, given + // how GNDStk creates #nodeName attributes in the first place. + group.getAttribute(attrName).read(node.name); + } else if (beginsin(attrName,"#cdata")) { + // #cdata, possibly with a numeric suffix + // Expand into a child node #cdata with a #text attribute. + std::string value; + group.getAttribute(attrName).read(value); + node.add("#cdata").add("#text",value); + } else if (beginsin(attrName,"#comment")) { + // #comment, possibly with a numeric suffix + // Expand into a child node #comment with a #text attribute. + std::string value; + group.getAttribute(attrName).read(value); + node.add("#comment").add("#text",value); + } else { + // Regular attribute + // Create a metadatum. Note that this gives the correct result for + // attributes with regular names, and also for attrName == "#text". + if (!attr2node(group.getAttribute(attrName), node)) + return false; + } // else if + } // for + } // if // ------------------------ // HDF5 sub-groups // ==> children // ------------------------ - for (auto &elemName : group.listObjectNames()) { + for (const std::string &elemName : group.listObjectNames()) { switch (group.getObjectType(elemName)) { // File // NOT EXPECTED IN THIS CONTEXT case HighFive::ObjectType::File : - error_HDF52Node("ObjectType \"File\" not expected here"); + error_hdf52node("ObjectType \"File\" is not expected here"); break; // Group - // ACTION: call the present function recursively + // ACTION: Call the present function recursively case HighFive::ObjectType::Group : try { - if (!HDF52Node(group.getGroup(elemName), elemName, node.add())) + if (!hdf52node(group.getGroup(elemName), elemName, node.add())) return false; } catch (...) { log::function(context); @@ -218,26 +250,24 @@ bool HDF52Node( break; // UserDataType - // NOT HANDLED; perhaps we could provide something in the future + // NOT HANDLED; These may or may not ever be needed case HighFive::ObjectType::UserDataType : - error_HDF52Node("ObjectType \"UserDataType\" not handled"); + error_hdf52node("ObjectType \"UserDataType\" is not supported"); break; - // DataSpace (not to be confused with Dataset) + // DataSpace (not to be confused with DataSet) // NOT EXPECTED IN THIS CONTEXT case HighFive::ObjectType::DataSpace : - error_HDF52Node("ObjectType \"DataSpace\" not expected here"); + error_hdf52node("ObjectType \"DataSpace\" is not expected here"); break; - // Dataset - // ACTION: handle the DataSet's data + // DataSet + // ACTION: Handle the DataSet's data + // Note: HighFive actually calls the following Dataset (lower-case s), + // not DataSet (upper-case S), but uses "DataSet" elsewhere. :-/ case HighFive::ObjectType::Dataset : try { - if (!HDF5data2node( - group.getDataSet(elemName), - elemName, - node.add() - )) + if (!dset2node(group,elemName,node)) return false; } catch (...) { log::function(context); @@ -253,19 +283,19 @@ bool HDF52Node( // because we already handled attributes earlier. So, here, we just // produce an error if ObjectType::Attribute inexplicably made an // appearance here, where we don't expect it. - error_HDF52Node("ObjectType \"Attribute\" not expected here"); + error_hdf52node("ObjectType \"Attribute\" is not expected here"); break; // Other - // NOT HANDLED; we're not sure when this would arise + // NOT HANDLED; We're not sure when this would arise case HighFive::ObjectType::Other : - error_HDF52Node("ObjectType \"Other\" not handled"); + error_hdf52node("ObjectType \"Other\" is not supported"); break; // default - // NOT HANDLED; presumably our switch has covered all bases already + // NOT HANDLED; our switch() should have covered all bases default: - error_HDF52Node("ObjectType [unknown] not handled"); + error_hdf52node("ObjectType [unknown] is not supported"); break; } // switch diff --git a/src/GNDStk/convert/src/detail-node2hdf5.hpp b/src/GNDStk/convert/src/detail-node2hdf5.hpp index 02e2e9614..116217715 100644 --- a/src/GNDStk/convert/src/detail-node2hdf5.hpp +++ b/src/GNDStk/convert/src/detail-node2hdf5.hpp @@ -25,7 +25,11 @@ HighFive::Attribute vectorAttr( return hdf.createAttribute(key,vector); } +// ------------------------ // vecDataSet +// ------------------------ + +// helper template HighFive::DataSet vecDataSet( const std::string &key, const std::string &value, @@ -36,35 +40,40 @@ HighFive::DataSet vecDataSet( return hdf.createDataSet(key,vector); } -// vecDataSet, w/ type string +// w/ type string template HighFive::DataSet vecDataSet( const std::string &key, const std::string &value, OBJECT &hdf ) { - const std::string type = guessType(value); - return - type == "int" || type == "ints" ? - vecDataSet(key,value,hdf) : - type == "uint" || type == "uints" ? - vecDataSet(key,value,hdf) : - type == "long" || type == "longs" ? - vecDataSet(key,value,hdf) : - type == "ulong" || type == "ulongs" ? - vecDataSet(key,value,hdf) : - type == "double" || type == "doubles" ? - vecDataSet(key,value,hdf) : - // "string", "strings", or "" - vecDataSet(key,value,hdf) ; + if (HDF5::typed) { + const std::string type = guessType(value); + if (type == "int" || type == "ints" ) + return vecDataSet(key,value,hdf); + if (type == "uint" || type == "uints" ) + return vecDataSet(key,value,hdf); + if (type == "long" || type == "longs" ) + return vecDataSet(key,value,hdf); + if (type == "ulong" || type == "ulongs" ) + return vecDataSet(key,value,hdf); + if (type == "double" || type == "doubles") + return vecDataSet(key,value,hdf); + } + return vecDataSet(key,value,hdf); } // ----------------------------------------------------------------------------- -// typedHDF5 +// meta2hdf5_typed +// meta2hdf5_plain // ----------------------------------------------------------------------------- +// ------------------------ +// meta2hdf5_typed +// ------------------------ + template -void typedHDF5(const NODE &node, OBJECT &hdf) +void meta2hdf5_typed(const NODE &node, OBJECT &hdf) { const std::string &parent = node.name; @@ -72,97 +81,128 @@ void typedHDF5(const NODE &node, OBJECT &hdf) const std::string &key = meta.first; const std::string &value = meta.second; + // ------------------------ + // Special cases + // ------------------------ + // *** #cdata/#text // *** #comment/#text // ACTION: Write these as-is. That is, do NOT apply our type-guessing code // to a comment, or to the contents of a block like those // that we see in existing XML-format GNDS files. The type guesser would // see words, and think "vector of [whitespace-separated] strings," which - // would be painfully wrong for what are clearly free-form strings. - if (key == "#text" && (parent == "#cdata" || parent == "#comment")) { + // would be wrong for what are clearly intended to be free-form strings. + if ((parent == "#cdata" || parent == "#comment") && key == "#text") { hdf.createAttribute(key,value); // just a simple string attribute continue; } // *** #pcdata/#text // ACTION: Apply our type-guessing code, but write *vectors* only, never - // scalars. So, 10 would be interpreted as a vector with - // one element, NOT as a scalar; while 10 20 30 would be - // interpreted as a vector with three elements. What may look like scalars - // are folded into vectors because we think that reflects what nodes like - // this are intended to represent. (If something was really just a scalar, - // then surely it would be placed in the metadata, not in a node such as - // scalar.) - if constexpr (std::is_same_v) { - if (key == "#text" && parent == "#pcdata") { - vecDataSet(key,value,hdf); // DataSet will have name "#text" - continue; - } + // scalars. So, 10 produces a vector with one element, + // NOT a scalar; while 10 20 30 would produces a vector + // with three elements. What may look like scalars are made into vectors + // because we think that reflects what these (#pcdata) nodes are intended + // to represent. (If something was really just a scalar, then surely it + // would be placed into standard metadata (in <...>), not #pcdata. + if (parent == "#pcdata" && key == "#text") { + std::string type = guessType(value); + if (type == "int" || type == "ints") + vectorAttr(key,value,hdf); + else if (type == "uint" || type == "uints") + vectorAttr(key,value,hdf); + else if (type == "long" || type == "longs") + vectorAttr(key,value,hdf); + else if (type == "ulong" || type == "ulongs") + vectorAttr(key,value,hdf); + else if (type == "double" || type == "doubles") + vectorAttr(key,value,hdf); + else + vectorAttr(key,value,hdf); + continue; } - // *** key/#text not expected except as already handled + // *** key/#text not expected, except as already handled if (key == "#text") { log::warning("Metadatum name \"#text\" not expected here; " "writing anyway"); - log::function("detail::typedHDF5(Node named \"{}\", ...)", parent); + log::function("detail::meta2hdf5_typed(Node named \"{}\", ...)", + parent); } - // *** key/value + // ------------------------ // General case - // ACTION: Apply our type-guessing code almost fully, except that for - // a metadatum="that looks like this", interpret "..." as essentially - // a single descriptive string. So, don't split it up into a vector. + // ------------------------ + + // *** key/value + // ACTION: Apply our type-guessing code. + // Here we have normal metadata, as in . + // For numeric types we might produce vectors, if there appear to be + // multiple values. But for string types, we'll assume that the value + // is probably intended to be a free-form, human-readable descriptive + // string, which shouldn't be split into tokens and made into a vector. const std::string type = guessType(value); - type == "int" ? scalarAttr(key,value,hdf) : - type == "ints" ? vectorAttr(key,value,hdf) : - type == "uint" ? scalarAttr(key,value,hdf) : - type == "uints" ? vectorAttr(key,value,hdf) : - type == "long" ? scalarAttr(key,value,hdf) : - type == "longs" ? vectorAttr(key,value,hdf) : - type == "ulong" ? scalarAttr(key,value,hdf) : - type == "ulongs" ? vectorAttr(key,value,hdf) : - type == "double" ? scalarAttr(key,value,hdf) : - type == "doubles" ? vectorAttr(key,value,hdf) : - /* else........ */ scalarAttr(key,value,hdf) ; + if (type == "int" ) scalarAttr(key,value,hdf); else + if (type == "ints" ) vectorAttr(key,value,hdf); else + if (type == "uint" ) scalarAttr(key,value,hdf); else + if (type == "uints" ) vectorAttr(key,value,hdf); else + if (type == "long" ) scalarAttr(key,value,hdf); else + if (type == "longs" ) vectorAttr(key,value,hdf); else + if (type == "ulong" ) scalarAttr(key,value,hdf); else + if (type == "ulongs" ) vectorAttr(key,value,hdf); else + if (type == "double" ) scalarAttr(key,value,hdf); else + if (type == "doubles") vectorAttr(key,value,hdf); else + /* string or strings*/ scalarAttr(key,value,hdf); } +} // meta2hdf5_typed + + +// ------------------------ +// meta2hdf5_plain +// ------------------------ + +template +void meta2hdf5_plain(const NODE &node, OBJECT &hdf) +{ + for (auto &meta : node.metadata) + hdf.createAttribute(meta.first, meta.second); } // ----------------------------------------------------------------------------- -// Meta2HDF5 +// meta2hdf5 // ----------------------------------------------------------------------------- // Here, OBJECT hdf is either a HighFive::Group or a HighFive::DataSet template -void Meta2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix) +void meta2hdf5(const NODE &node, OBJECT &hdf, const std::string &suffix) { // #nodeName if appropriate (as with JSON, allows recovery of original name) if (suffix != "") hdf.createAttribute(std::string("#nodeName"), node.name); - // Existing attributes + // Existing metadata if (HDF5::typed) { - // Use our "guess what's in the string" code to try to infer what certain - // "string" values actually contain (a single int, say, or a vector of - // doubles). Then, use the inferred types in the HDF5 file. - typedHDF5(node,hdf); + // Use our "guess what's in the string" code to try to infer what each + // metadatum's string value actually contains (a single int, say, or + // a vector of doubles). Use the inferred types in the HDF5 file. + meta2hdf5_typed(node,hdf); } else { - // Write simple HDF5 where all data (metadata, and the content of "cdata" - // and "pcdata" nodes) end up being strings. Not even vectors of strings - // (as from H He Li ...), but single strings. - for (auto &meta : node.metadata) - hdf.createAttribute(meta.first, meta.second); + // Write simple HDF5 in which all data (metadata, as well as the contents + // of "cdata" and "pcdata" nodes) end up being strings. Not even vectors + // of strings, as from H He Li ..., but single strings. + meta2hdf5_plain(node,hdf); } } // ----------------------------------------------------------------------------- -// Node2HDF5 +// node2hdf5 // ----------------------------------------------------------------------------- // Here, OBJECT hdf is either a HighFive::File or a HighFive::Group template -bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") +bool node2hdf5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") { // As with JSON; see the remark in node2json() const std::string nameOriginal = node.name; @@ -172,14 +212,14 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") // Specific cases // ------------------------ - if (HDF5::flat) { + if (HDF5::reduced) { // #cdata or #comment // #text the only metadatum // no children // Reduce to: a string attribute, w/name == (#cdata or #comment) + suffix - // Brief: + // Sketch: // +-----------------+ +-----------+ - // | #cdata/#comment | ==> | Attribute | + // | #cdata/#comment | ==> | Attribute | name: #... + suffix // | #text | | value | // | - | +-----------+ // +-----------------+ @@ -199,9 +239,9 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") // #text the only metadatum // no children // Reduce to: a data set, w/name == #pcdata + suffix - // Brief: + // Sketch: // +----------+ +---------+ - // | #pcdata | ==> | DataSet | + // | #pcdata | ==> | DataSet | name: #pcdata + suffix // | #text | | data | // +----------+ +---------+ // @@ -210,6 +250,21 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") node.metadata.size() == 1 && node.metadata[0].first == "#text" ) { + // Remark. This case (basically, #pcdata/#text) may look superficially + // like it would have been handled, in the case immediately below here, + // in the previous (next-up) recurse of the present function. Often + // it would have, but not always. Below, someName/#pcdata/#text (three + // levels, so to speak) reduces to one level (DataSet someName), but + // only if someName has ONE child - the #pcdata. That's true when we + // have (in XML) something like 1 2 3, as the pcdata, + // i.e. the 1 2 3 part, is ' only child node. However, it's + // actually possible (though I don't see it in current GNDS files) to + // have something like: 1 2 3. There, the + // outer someName node () has child foo and child #pcdata, and + // thus can't be reduced in the manner that's done if only #pcdata is + // there. In short, then, the present situation comes to pass if and + // when #pcdata has sibling nodes. + // dataset const std::string value = node.metadata[0].second; HighFive::DataSet dataset = vecDataSet(nameSuffixed,value,hdf); @@ -217,19 +272,19 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") } // someName (think e.g. values, as in XML ) - // any number of metadata + // any number of metadata (possible 0) // #pcdata the only child // #text the only metadatum // no children // Reduce to: a data set, w/name == someName + suffix - // Brief: - // +--------------+ +---------------+ - // | someName | ==> | DataSet | - // | metadata | | Attributes | - // | #pcdata | | data | - // | #text | +---------------+ - // | - | - // +--------------+ + // Sketch: + // +---------------+ +----------------+ + // | someName | ==> | DataSet | name: someName + suffix + // | [metadata] | | [Attributes] | + // | #pcdata | | data | + // | #text | +----------------+ + // | - | + // +---------------+ // if (node.children.size() == 1 && node.children[0]->name == "#pcdata" && @@ -241,10 +296,10 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") const std::string value = node.children[0]->metadata[0].second; HighFive::DataSet dataset = vecDataSet(nameSuffixed,value,hdf); // metadata, then done; the #pcdata child was rolled into the data set - Meta2HDF5(node,dataset,suffix); + meta2hdf5(node,dataset,suffix); return true; } - } + } // if (HDF5::reduced) // ------------------------ // General case @@ -254,7 +309,7 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") HighFive::Group group = hdf.createGroup(nameSuffixed); // metadata - Meta2HDF5(node,group,suffix); + meta2hdf5(node,group,suffix); // children // Logic is as with JSON; see the remark in node2json(). @@ -268,7 +323,7 @@ bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") } for (auto &c : node.children) { const std::size_t counter = childNames.find(c->name)->second++; - if (!Node2HDF5(*c, group, counter ? std::to_string(counter-1) : "")) + if (!node2hdf5(*c, group, counter ? std::to_string(counter-1) : "")) return false; } diff --git a/src/GNDStk/utility.hpp b/src/GNDStk/utility.hpp index 0056974a6..dceb24a7b 100644 --- a/src/GNDStk/utility.hpp +++ b/src/GNDStk/utility.hpp @@ -372,7 +372,7 @@ bool convert(const HDF5 &, HDF5 &); // Utility constructs // The functions here could possibly go into the detail namespace, but could // arguably be useful, in their own right, to users. So, I'll leave them out -// in the overall project namespace (which enclosed the #include of this file). +// in the overall project namespace. // ----------------------------------------------------------------------------- // endsin @@ -382,6 +382,12 @@ inline bool endsin(const std::string &str, const std::string &end) return str.size() >= end.size() && &str[str.size()-end.size()] == end; } +// beginsin +inline bool beginsin(const std::string &str, const std::string &begin) +{ + return strncmp(&str[0], &begin[0], begin.size()) == 0; +} + // nocasecmp // Case-insensitive string comparison. // The old C-language strcasecmp() is nonstandard. A modern, true caseless