diff --git a/include/argparse.hpp b/include/argparse.hpp index b5ef1ed..32ab73a 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -30,6 +30,7 @@ SOFTWARE. #pragma once #include #include +#include #include #include #include @@ -190,6 +191,76 @@ template struct parse_number { } }; +template constexpr auto generic_strtod = nullptr; +template <> constexpr auto generic_strtod = strtof; +template <> constexpr auto generic_strtod = strtod; +template <> constexpr auto generic_strtod = strtold; + +template inline auto do_strtod(std::string const &s) -> T { + if (isspace(static_cast(s[0])) || s[0] == '+') + throw std::invalid_argument{"pattern not found"}; + + auto [first, last] = pointer_range(s); + char *ptr; + + errno = 0; + if (auto x = generic_strtod(first, &ptr); errno == 0) { + if (ptr == last) + return x; + else + throw std::invalid_argument{"pattern does not match to the end"}; + } else if (errno == ERANGE) { + throw std::range_error{"not representable"}; + } else { + return x; // unreachable + } +} + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::general does not parse hexfloat"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) + throw std::invalid_argument{"chars_format::hex parses hexfloat"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::scientific does not parse hexfloat"}; + if (s.find_first_of("eE") == s.npos) + throw std::invalid_argument{ + "chars_format::scientific requires exponent part"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::fixed does not parse hexfloat"}; + if (s.find_first_of("eE") != s.npos) + throw std::invalid_argument{ + "chars_format::fixed does not parse exponent part"}; + + return do_strtod(s); + } +}; + } // namespace details class ArgumentParser; diff --git a/test/test_scan.cpp b/test/test_scan.cpp index c55e5a8..64b6f58 100644 --- a/test/test_scan.cpp +++ b/test/test_scan.cpp @@ -171,3 +171,178 @@ TEST_CASE_TEMPLATE("Parse integer argument of any format" * test_suite("scan"), std::invalid_argument); } } + +#define FLOAT_G(t, literal) \ + ([] { \ + if constexpr (std::is_same_v) \ + return literal##f; \ + else if constexpr (std::is_same_v) \ + return literal; \ + else if constexpr (std::is_same_v) \ + return literal##l; \ + }()) + +TEST_CASE_TEMPLATE("Parse floating-point argument of general format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'g', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0"}); + REQUIRE(program.get("-n") == 0.); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12)); + } + + SUBCASE("left-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "\t.32"}), + std::invalid_argument); + } + + SUBCASE("right-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", ".32\n"}), + std::invalid_argument); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12"}), + std::invalid_argument); + } + + SUBCASE("plus sign after padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", " +.12"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1a.3p+1"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "1.3e+5000"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse hexadecimal floating-point argument" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'a', T>(); + + SUBCASE("zero") { + // binary-exponent-part is not optional in C++ grammar + program.parse_args({"test", "-n", "0x0"}); + REQUIRE(program.get("-n") == 0x0.p0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "0x1a.3p+1"}); + REQUIRE(program.get("-n") == 0x1a.3p+1); + } + + SUBCASE("minus sign produces an optional argument") { + // XXX may worth a fix + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-0x0.12p1"}), + std::runtime_error); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+0x1p0"}), + std::invalid_argument); + } + + SUBCASE("general format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14"}), + std::invalid_argument); + } +} + +TEST_CASE_TEMPLATE("Parse floating-point argument of scientific format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'e', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0e0"}); + REQUIRE(program.get("-n") == 0e0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14e-1"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14e-1)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12e+1"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12e+1)); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12e+1"}), + std::invalid_argument); + } + + SUBCASE("fixed format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1.33p+0"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "1.3e+5000"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse floating-point argument of fixed format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'f', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", ".0"}); + REQUIRE(program.get("-n") == .0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12)); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12"}), + std::invalid_argument); + } + + SUBCASE("scientific format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14e+0"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1.33p+0"}), + std::invalid_argument); + } +}