diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index e0e1b89078..90b0642706 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -1,27 +1,29 @@ add_library( base + applicative.cpp cbor.cpp - enumOptionsBase.cpp - geometry.cpp - lineParser.cpp - lock.cpp - messenger.cpp - outputHandler.cpp - sysFunc.cpp - timer.cpp - units.cpp - version.cpp enumOption.h - enumOptionsBase.h enumOptions.h + enumOptionsBase.cpp + enumOptionsBase.h + geometry.cpp geometry.h + lineParser.cpp lineParser.h + lock.cpp lock.h + messenger.cpp messenger.h + outputHandler.cpp outputHandler.h + parserLibrary.cpp + sysFunc.cpp sysFunc.h + timer.cpp timer.h + units.cpp units.h + version.cpp version.h ) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp new file mode 100644 index 0000000000..4030614e44 --- /dev/null +++ b/src/base/applicative.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "applicative.h" + +#include +#include +#include + +namespace Parsers +{ + +// A parser that expects an exact string +Parser literal(std::string_view constant) { return Parser(constant); } + +Parser takeWhile(std::function f) +{ + Parser result( + [f](auto &input) -> ParserOutput + { + if (input.eof()) + return {}; + std::string result = ""; + while (f(input.peek())) + result.push_back(input.get()); + if (result.empty()) + return {}; + return {{result, input}}; + }); + return result; +} + +// A parser that accepts and amount of whitespace +Parser spaces() +{ + return takeWhile([](const char c) { return isspace(c); }); +} +// A parser that accepts space and tab +Parser inlineSpaces() +{ + return takeWhile([](const char c) { return c == ' ' || c == '\t'; }); +} +// A parse that accepts any amount of visible characters +Parser graphs() +{ + return takeWhile([](const char c) { return std::isgraph(c); }); +} +// A parse that accepts any amount of alphanumeric characters +Parser alphanums() +{ + return takeWhile([](const char c) { return std::isalnum(c); }); +} +// A parse that accepts any amount of letters +Parser alphas() +{ + return takeWhile([](const char c) { return std::isalpha(c); }); +} +// A parse that accepts any amount of upper case letters +Parser uppers() +{ + return takeWhile([](const char c) { return std::isupper(c); }); +} +// A parse that accepts any amount of lower case letters +Parser lowers() +{ + return takeWhile([](const char c) { return std::islower(c); }); +} +// A parse that accepts any amount of punctuation case letters +Parser punctuations() +{ + return takeWhile([](const char c) { return std::ispunct(c); }); +} +// A parser that accepts any amount of digit characters +Parser digits() +{ + return takeWhile([](const char c) { return std::isdigit(c); }); +} + +// A parser that continues until a newline +Parser inlines() +{ + return takeWhile([](const auto c) { return c != '\r' && c != '\n'; }); +} + +// A parser that matches newline characters +Parser newlines() { return "\r\n"_p | "\n"_p; } + +// A quick wrapper for easily making parses from strings +Parser operator""_p(const char *text, size_t size) { return literal(std::string_view(text, size)); } +} // namespace Parsers diff --git a/src/base/applicative.h b/src/base/applicative.h new file mode 100644 index 0000000000..d7f1a786b5 --- /dev/null +++ b/src/base/applicative.h @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Parsers +{ + +// A concept to check if a type is a tuple. This is built in in +// C++23, be we can do this for now. +template +concept TupleLike = requires { std::tuple_size::value; }; + +// The simplest definition of an applicative parser is a function that +// takes a stream and, if the parse succeeds, returns the parsed +// value and the rest of the stream. To make life simpler, we +// define the parser_output for the return type of the function. +// This *could* have further implications because there are more +// complicated parsers we could create. +template using ParserOutput = std::optional>; + +// This concept type checks that a given lambda matches the definition +// given in the paragraph above. +template +concept ApParse = requires(Lambda lam, std::istream input) { + { lam(input) } -> std::convertible_to>; +}; + +// It's fully possible to just use the functions as the parsers, but, +// if we wrap them in a struct, we can use operator overloading to +// more easily combine smaller parsers into larger parsers. +template class Parser +{ + + public: + // Wrap a parser lambda in a structure + template + requires ApParse + Parser(Lambda lambda) : lambda_(lambda) + { + } + + // Create a parser that matches and exact string + template ::value>> + Parser(std::string_view constant) + : lambda_( + [constant](std::istream &input) -> ParserOutput + { + for (auto c : constant) + if (c != input.get()) + return {}; + return {{constant, input}}; + }) + { + } + + private: + // The actual function that we have wrapped. We need to wrap this + // in a std::function instead of using a lambda so that we don't + // have to handle the exact type of the bound lambda + std::function(std::istream &)> lambda_; + + public: + // Parse a string and, if possible, return the value and the remainder + ParserOutput parse(std::istream &input) const { return lambda_(input); }; + // Parse a string and, if possible, return the value and the remainder + ParserOutput operator()(std::istream &input) const { return lambda_(input); } + // Parse a string and enforce that it parsed the entire input + std::optional exact(std::istream &input) const + { + auto result = lambda_(input); + if (result && input.get() == -1) + return std::get<0>(*result); + return {}; + } + + // Create a new parser that takes the output of the old parser and + // passes it through a function + template auto map(Lambda f) -> Parser()))> + { + auto &method = lambda_; + Parser()))> result( + [method, f](std::istream &input) -> ParserOutput()))> + { + auto first = method(input); + if (first) + { + auto &[body, remainder] = *first; + return {{f(body), remainder}}; + } + else + return {}; + }); + return result; + } + + // Same as map, but unpacks the tuple and passes the individual + // parts as arguments. This greatly simplifies creating mapping + // functions + template + requires(TupleLike) + auto apply(Lambda f) -> Parser()))> + { + return map([f](const T tup) { return std::apply(f, tup); }); + } + + // Insist that this parse is followed by another parse, but we + // ignore the output of that other parser + template Parser operator<<(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](std::istream &input) -> ParserOutput + { + auto first = method(input); + if (first) + { + auto &[body, middle] = *first; + auto second = other(middle); + if (second) + return {{body, std::get<1>(*second)}}; + else + return {}; + } + else + { + return {}; + } + }); + return result; + } + + // Confirm that this parser passes, but ignore its output and return the value of a subsequent parser + template Parser operator>>(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](std::istream &input) -> ParserOutput + { + auto first = method(input); + if (first) + { + auto &[body, middle] = *first; + return other(middle); + } + else + return {}; + }); + return result; + } + + // If this parser fails, try an alternate parser instead of + // immediately failing. + Parser operator|(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](std::istream &input) -> ParserOutput + { + auto location = input.tellg(); + auto first = method(input); + if (first) + return first; + input.clear(); + input.seekg(location); + return other(input); + }); + return result; + } + + // If this parser fails, try an alternate literal string parser + // instead of immediately failing. + template ::value>> + Parser operator|(std::string_view other) + { + return Parser(other) | *this; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(!TupleLike && !TupleLike) + auto operator&(Parser other) -> Parser> + { + auto &method = lambda_; + Parser> result( + [method, other](std::istream &input) -> ParserOutput> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other(middle); + if (second) + { + auto &[snd, final] = *second; + return {{{fst, snd}, final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(TupleLike && !TupleLike) + auto operator&(Parser other) -> Parser(), std::make_tuple(std::declval())))> + { + auto &method = lambda_; + Parser(), std::make_tuple(std::declval())))> result( + [method, other](std::istream &input) + -> ParserOutput(), std::make_tuple(std::declval())))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(fst, std::make_tuple(snd)), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(!TupleLike && TupleLike) + auto operator&(Parser other) -> Parser()), std::declval()))> + { + auto &method = lambda_; + Parser()), std::declval()))> result( + [method, other](std::istream &input) + -> ParserOutput()), std::declval()))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(std::make_tuple(fst), snd), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(TupleLike && TupleLike) + auto operator&(Parser other) -> Parser(), std::declval()))> + { + auto &method = lambda_; + Parser(), std::declval()))> result( + [method, other](std::istream &input) -> ParserOutput(), std::declval()))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(fst, snd), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } +}; + +// Create a parser that always succeeds and returns a constant value +template Parser pure(T constant) +{ + Parser result([constant](std::istream &input) -> ParserOutput { return {{constant, input}}; }); + return result; +} + +// A parser that always fails +template Parser null() +{ + Parser result = ([](const auto x) -> ParserOutput { return std::nullopt; }); + return result; +} + +// Modify a parser so that the parsed value is wrapped in a +// std::optional. If the parser would have failed, act as though +// the parse succeeded, but make the std::optional empty. +template Parser> maybe(Parser inner) +{ + Parser> result( + [inner](std::istream &input) -> ParserOutput> + { + auto location = input.tellg(); + auto first = inner(input); + if (first) + { + auto &[body, remainder] = *first; + return {{{body}, remainder}}; + } + else + { + input.clear(); + input.seekg(location); + return {{{}, input}}; + } + }); + return result; +} + +// Insist that a parser passes at least once, but collect as many +// parsed values as possible. +template Parser> some(Parser inner) +{ + Parser> result( + [inner](std::istream &input) -> ParserOutput> + { + std::vector collection; + auto location = input.tellg(); + while (!input.eof()) + { + auto trial = inner(input); + if (trial) + { + auto &[body, _] = *trial; + collection.push_back(body); + location = input.tellg(); + } + else + { + input.clear(); + input.seekg(location); + break; + } + } + if (collection.empty()) + return {}; + return {{collection, input}}; + }); + return result; +} + +// A parser that expects an exact string +Parser literal(std::string_view constant); +// A parser that accepts any amount of digit characters +Parser digits(); + +// A parser that accepts any amount of whitespace +Parser spaces(); +// A parser that accepts space and tab +Parser inlineSpaces(); + +// A parser that accepts any amount of visible characters +Parser graphs(); +// A parser that accepts any amount of alphanumeric characters +Parser alphanums(); +// A parser that accepts any amount of letters +Parser alphas(); +// A parser that accepts any amount of upper case letters +Parser uppers(); +// A parser that accepts any amount of lower case letters +Parser lowers(); +// A parser that accepts any amount of punctuation case letters +Parser punctuations(); +// A parser that continues until a newline +Parser inlines(); +// A parser that matches newline characters +Parser newlines(); + +// A quick wrapper for easily making parses from strings +Parser operator""_p(const char *text, size_t size); + +// A quick overload to easily ignore strings around a parser +template Parser operator<<(Parser body, std::string_view other) +{ + return body << Parser(other); +} + +// A quick overload to easily ignore strings around a parser +template Parser operator>>(std::string_view other, Parser body) +{ + return Parser(other) >> body; +} + +// A quick overload to easily require strings around a parser +template auto operator&(Parser self, std::string_view other) -> decltype(self & Parser(other)) +{ + return self & Parser(other); +} + +// A quick overload to easily require strings around a parser +template auto operator&(std::string_view other, Parser self) -> decltype(Parser(other) & self) +{ + return Parser(other) & self; +} + +}; // namespace Parsers diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp new file mode 100644 index 0000000000..fe5202978f --- /dev/null +++ b/src/base/parserLibrary.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "base/parserLibrary.h" +#include + +namespace Parsers +{ + +// A parser that accepts an integer greater than or equal to zero +Parser natural() +{ + auto result = digits(); + return result.map( + [](const auto terms) -> int + { + int total = 0; + for (auto term : terms) + total = 10 * total + (term - '0'); + return total; + }); +} + +// Take an optional minus sign and a whole number to create an integer +int nat2int(std::optional minus, int number) +{ + if (minus) + return -number; + return number; +} + +// A parser that accepts an integer +Parser integer() { return (maybe("-"_p) & natural()).apply(nat2int); } + +// Take the numbers before the decimal, some optional digits after the period, and an optional exponent, and return a double. +double nat2dbl(std::optional minus, std::string_view front, std::optional decimals, + std::optional, std::string_view>> exponent) +{ + auto basic = std::format("{}{}", minus.value_or(""), front); + if (decimals) + basic += std::format(".{}", *decimals); + if (exponent) + basic += std::format("e{}{}", std::get<0>(*exponent).value_or(""), std::get<1>(*exponent)); + return std::stod(basic); +} + +// A parser that accepts a real, floating point number +Parser real() +{ + auto result = maybe("-"_p) & digits() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> maybe("-"_p | "+"_p) & digits()); + return result.apply(nat2dbl); +} + +// A parser that accepts a 3-vector of floating point numbers +Parser vector3() +{ + return (real() & spaces() >> real() & spaces() >> real()) + .apply([](double x, double y, double z) { return Vector3(x, y, z); }); +} + +} // namespace Parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h new file mode 100644 index 0000000000..25772cc015 --- /dev/null +++ b/src/base/parserLibrary.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#pragma once + +#include "base/applicative.h" +#include "math/vector3.h" + +namespace Parsers +{ + +// A parser that accepts an integer greater than or equal to zero +Parser natural(); + +// A parser that accepts an integer +Parser integer(); + +// A parser that accepts a real, floating point number +Parser real(); + +// A parser that accepts a 3-vector of floating point numbers +Parser vector3(); + +}; // namespace Parsers diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index 1dac55eb2e..4313b2e30b 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -2,6 +2,9 @@ // Copyright (c) 2026 Team Dissolve and contributors #include "nodes/importXYZStructure.h" +#include "base/parserLibrary.h" +#include +#include ImportXYZStructureNode::ImportXYZStructureNode(Graph *parentGraph) : Node(parentGraph) { @@ -31,32 +34,42 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() { structure_.clear(); - // Open file and check that we're OK to proceed importing from it - LineParser parser; - if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + std::ifstream infile{filePath_}; + if (!infile) return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); - - return read(parser, structure_); + return read(infile, structure_); } -// Read structure from the specified file parser -NodeConstants::ProcessResult ImportXYZStructureNode::read(LineParser &parser, Structure &structure) +// Read structure from the specified input stream +NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { - // Read natoms - if (parser.getArgsDelim() != LineParser::Success) - return NodeConstants::ProcessResult::Failed; - auto nAtoms = parser.argi(0); + using namespace Parsers; + auto xyz = structureBlock().parse(input); - // Skip title - if (parser.skipLines(1) != LineParser::Success) + if (!xyz) return NodeConstants::ProcessResult::Failed; + auto &rest = std::get<1>(*xyz); + auto &[nAtoms, atoms] = std::get<0>(*xyz); - for (auto n = 0; n < nAtoms; ++n) - { - if (parser.getArgsDelim() != LineParser::Success) - return NodeConstants::ProcessResult::Failed; - structure.addAtom(Elements::element(parser.argsv(0)), parser.arg3d(1), parser.hasArg(4) ? parser.argd(4) : 0.0); - } + structure.clear(); + for (auto &[elem, v, q] : atoms) + structure.addAtom(Elements::element(elem), v, q.value_or(0.0)); + assert(nAtoms == atoms.size()); return NodeConstants::ProcessResult::Success; -} \ No newline at end of file +} + +Parsers::Parser>> ImportXYZStructureNode::structureAtom() +{ + using namespace Parsers; + auto parser = alphas() & inlineSpaces() >> vector3() & maybe(inlineSpaces() >> real() << maybe(inlineSpaces())); + return parser; +} + +Parsers::Parser>>>> +ImportXYZStructureNode::structureBlock() +{ + using namespace Parsers; + return maybe(inlineSpaces()) >> natural() << newlines() & + inlines() >> newlines() >> some(structureAtom() << maybe(inlineSpaces()) << maybe(newlines())); +} diff --git a/src/nodes/importXYZStructure.h b/src/nodes/importXYZStructure.h index 1cbe74e924..1838396920 100644 --- a/src/nodes/importXYZStructure.h +++ b/src/nodes/importXYZStructure.h @@ -3,6 +3,7 @@ #pragma once +#include "base/applicative.h" #include "classes/structure.h" #include "nodes/node.h" @@ -39,5 +40,9 @@ class ImportXYZStructureNode : public Node public: // Read structure from the specified file parser - static NodeConstants::ProcessResult read(LineParser &parser, Structure &structure); -}; \ No newline at end of file + static NodeConstants::ProcessResult read(std::istream &input, Structure &structure); + static Parsers::Parser>> structureAtom(); + + static Parsers::Parser>>>> + structureBlock(); +}; diff --git a/src/nodes/importXYZTrajectory.cpp b/src/nodes/importXYZTrajectory.cpp index ac308b7c86..0e0e91a72a 100644 --- a/src/nodes/importXYZTrajectory.cpp +++ b/src/nodes/importXYZTrajectory.cpp @@ -40,26 +40,21 @@ NodeConstants::ProcessResult ImportXYZTrajectoryNode::process() { message("Reading XYZ trajectory file frame from '{}'...\n", filePath_); - // Open the file - LineParser parser; - if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + std::ifstream infile{filePath_}; + if (!infile) { error("Couldn't open trajectory file '{}'.\n", filePath_); return NodeConstants::ProcessResult::Failed; } - - // Seek to the next file position - parser.seekg(filePosition_); - - structure_.clear(); + infile.seekg(filePosition_); // Get the frame read result - auto frameResult = ImportXYZStructureNode::read(parser, structure_); + auto frameResult = ImportXYZStructureNode::read(infile, structure_); if (frameResult != NodeConstants::ProcessResult::Success) return frameResult; // Store the new trajectory file position - filePosition_ = parser.tellg(); + filePosition_ = infile.tellg(); return NodeConstants::ProcessResult::Success; } diff --git a/src/nodes/importXYZTrajectory.h b/src/nodes/importXYZTrajectory.h index 25bebaf1b9..7b842f0072 100644 --- a/src/nodes/importXYZTrajectory.h +++ b/src/nodes/importXYZTrajectory.h @@ -28,7 +28,7 @@ class ImportXYZTrajectoryNode : public Node // File path std::string filePath_; // Last read file position - std::streampos filePosition_; + std::streampos filePosition_{0}; // Structure Structure structure_; @@ -38,4 +38,4 @@ class ImportXYZTrajectoryNode : public Node protected: // Perform processing NodeConstants::ProcessResult process() override; -}; \ No newline at end of file +}; diff --git a/tests/io/CMakeLists.txt b/tests/io/CMakeLists.txt index 6ab8d5340d..299d86ea9b 100644 --- a/tests/io/CMakeLists.txt +++ b/tests/io/CMakeLists.txt @@ -1,3 +1,4 @@ +dissolve_add_test(SRC applicative.cpp) dissolve_add_test(SRC cbor.cpp) dissolve_add_test(SRC intraParameterParse.cpp) dissolve_add_test(SRC version.cpp) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp new file mode 100644 index 0000000000..7660ec5357 --- /dev/null +++ b/tests/io/applicative.cpp @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "base/applicative.h" +#include "base/parserLibrary.h" +#include "nodes/importXYZStructure.h" +#include +#include +#include + +namespace UnitTest +{ + +using namespace Parsers; +using namespace std::literals; + +template void testParser(std::string_view input, Parser parser, std::optional expected) +{ + std::cout << "Base string:\t" << input << std::endl; + std::istringstream stream{std::string(input)}; + auto result = parser(stream); + if (!result) + EXPECT_FALSE(expected); + else + EXPECT_EQ(std::get<0>(*result), *expected); +} +template void testExact(std::string_view input, Parser parser, T expected) +{ + std::cout << "Base string:\t" << input << std::endl; + std::istringstream stream{std::string(input)}; + auto result = parser.exact(stream); + ASSERT_TRUE(result); + EXPECT_EQ(*result, expected); +} + +TEST(ApplicativeTest, BasicStrings) +{ + testParser("Foo", "Fo"_p, {"Fo"}); + testParser("Foobar", "Foo"_p, {"Foo"}); +} +TEST(ApplicativeTest, Ignoring) +{ + testExact("Foobar", "Foo"_p << "bar"_p, "Foo"sv); + testExact("Foobar", "Foo" >> "bar"_p, "bar"sv); +} +TEST(ApplicativeTest, Joining) +{ + testExact("Foobar", "Foo"_p & "bar", {"Foo", "bar"}); + testParser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{1, 2, 3, 4}}); + testExact("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {"Fo", "ob", "a", "r"}); + testExact("Foobar", ("Fo"_p & "ob") & "ar", {"Fo", "ob", "ar"}); + testExact("Foobar", "Fo" & ("ob"_p & "ar"), {"Fo", "ob", "ar"}); + testParser("Foobar", (pure(1) & pure(2)) & pure(3), {{1, 2, 3}}); + testParser("Foobar", pure(1) & (pure(2) & pure(3)), {{1, 2, 3}}); +} + +TEST(ApplicativeTest, Choices) +{ + testExact("Foo", "Foo"_p | "Bar", "Foo"sv); + testExact("Bar", "Foo"_p | "Bar", "Bar"sv); + testParser("Quux", "Foo"_p | "Bar", {}); +} + +TEST(ApplicativeTest, NaturalNumbers) +{ + testParser("123foo", natural(), {123}); + auto triplet = natural() & "," >> natural() & "," >> natural(); + testExact("123,456,789", triplet, {123, 456, 789}); + auto vecsum = triplet.apply([](const auto x, const auto y, const auto z) -> int { return x + y + z; }); + testExact("123,456,789", vecsum, 123 + 456 + 789); +} + +TEST(ApplicativeTest, Optionals) +{ + testExact("123,456", maybe(natural() << ",") & natural(), {{123}, 456}); + testExact("456", maybe(natural() << ",") & natural(), {std::nullopt, 456}); + testExact("-456", integer(), -456); + testExact("456", integer(), 456); +} + +TEST(ApplicativeTest, MultipleTerms) +{ + // Parse multiple terms + testExact("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {123, 456, 789, 12}); + // Completely fail the parse if no copies are present + testParser("789", some(integer() << ","), {}); +} + +TEST(ApplicativeTest, RealNumbers) +{ + testExact("-12.0543", real(), -12.0543); + testExact("1.02E-3", real(), 1.02e-3); + testExact("-3E-4", real(), -3e-4); + testExact("-71.2e3", real(), -71.2e3); +} + +TEST(ApplicativeTest, BasicParser) +{ + testParser(" \t foo", spaces(), {" \t "}); + testExact("HW", alphas(), "HW"s); + testParser("1qaz.QAZ foo", graphs(), {"1qaz.QAZ"}); + testParser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), {{"1", "qaz", ".", "QAZ", " "}}); + + testExact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"s); +} + +TEST(ApplicativeTest, Vector) { testExact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } + +TEST(ApplicativeTest, StructureAtom) +{ + testExact("HW 1 2.5 -3e-4 5.6", ImportXYZStructureNode::structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); + testExact("He 0.5 0.5 0.5", ImportXYZStructureNode::structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); +} + +TEST(ApplicativeTest, XYZStructure) +{ + std::ifstream infile{"xyz/c2so3.xyz"}; + ASSERT_TRUE(infile); + auto xyz = ImportXYZStructureNode::structureBlock().parse(infile); + + ASSERT_TRUE(xyz); + auto &[value, rest] = *xyz; + EXPECT_EQ(rest.get(), -1); + auto &terms = std::get<1>(value); + EXPECT_EQ(terms.size(), std::get<0>(value)); + + std::vector> expected{ + + {"S", {0.010001, 0.000000, -0.000012}}, {"O", {1.465001, 0.000000, -0.000012}}, + {"O", {-0.475688, -1.371543, -0.000012}}, {"O", {-0.475688, 0.685772, 1.187779}}, + {"C", {-0.588181, 0.844607, -1.462914}}, {"C", {-0.079239, 0.124469, -2.712002}}, + {"H", {-1.678181, 0.844607, -1.462914}}, {"H", {-0.225364, 1.872451, -1.463546}}, + {"H", {-0.442057, -0.903375, -2.712634}}, {"H", {-0.443088, 0.638209, -3.601825}}, + {"H", {1.000760, 0.124469, -2.713254}}, + + }; + auto index = 0; + for (auto &[elem, r] : expected) + { + EXPECT_EQ(elem, std::get<0>(terms[index])); + EXPECT_EQ(r, std::get<1>(terms[index])); + ++index; + } +} + +TEST(ApplicativeTest, Helium) +{ + std::ifstream infile{"xyz/voxelDensity-helium.xyz"}; + ASSERT_TRUE(infile); + auto xyz = ImportXYZStructureNode::structureBlock().parse(infile); + ASSERT_TRUE(xyz); + auto &[value, rest] = *xyz; + EXPECT_EQ(rest.get(), -1); + auto &terms = std::get<1>(value); + EXPECT_EQ(terms.size(), std::get<0>(value)); + auto index = 0; + for (auto &[elem, r, q] : terms) + EXPECT_EQ(elem, "He"); +} + +} // namespace UnitTest