diff --git a/include/argparse.hpp b/include/argparse.hpp index 30c42da..6bf6383 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -301,33 +301,179 @@ public: } private: - static bool is_integer(const std::string &aValue) { - if (aValue.empty() || - ((!isdigit(aValue[0])) && (aValue[0] != '-') && (aValue[0] != '+'))) + static constexpr int eof = std::char_traits::eof(); + + static auto lookahead(std::string_view s) -> int { + if (s.empty()) + return eof; + else + return static_cast(static_cast(s[0])); + } + + /* + * decimal-literal: + * '0' + * nonzero-digit digit-sequence_opt + * integer-part fractional-part + * fractional-part + * integer-part '.' exponent-part_opt + * integer-part exponent-part + * + * integer-part: + * digit-sequence + * + * fractional-part: + * '.' post-decimal-point + * + * post-decimal-point: + * digit-sequence exponent-part_opt + * + * exponent-part: + * 'e' post-e + * 'E' post-e + * + * post-e: + * sign_opt digit-sequence + * + * sign: one of + * '+' '-' + */ + static bool is_decimal_literal(std::string_view s) { + auto is_digit = [](auto c) constexpr { + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + default: + return false; + } + }; + + // precondition: we have consumed or will consume at least one digit + auto consume_digits = [=](std::string_view s) { + auto it = std::find_if_not(begin(s), end(s), is_digit); + return s.substr(it - begin(s)); + }; + + switch (lookahead(s)) { + case '0': { + s.remove_prefix(1); + if (s.empty()) + return true; + else + goto integer_part; + } + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + s = consume_digits(s); + if (s.empty()) + return true; + else + goto integer_part_consumed; + } + case '.': { + s.remove_prefix(1); + goto post_decimal_point; + } + default: return false; - char *tPtr; - strtol(aValue.c_str(), &tPtr, 10); - return (*tPtr == 0); + } + + integer_part: + s = consume_digits(s); + integer_part_consumed: + switch (lookahead(s)) { + case '.': { + s.remove_prefix(1); + if (is_digit(lookahead(s))) + goto post_decimal_point; + else + goto exponent_part_opt; + } + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_decimal_point: + if (is_digit(lookahead(s))) { + s = consume_digits(s); + goto exponent_part_opt; + } else { + return false; + } + + exponent_part_opt: + switch (lookahead(s)) { + case eof: + return true; + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_e: + switch (lookahead(s)) { + case '-': + case '+': + s.remove_prefix(1); + } + if (is_digit(lookahead(s))) { + s = consume_digits(s); + return s.empty(); + } else { + return false; + } } - static bool is_float(const std::string &aValue) { - std::istringstream tStream(aValue); - float tFloat; - // noskipws considers leading whitespace invalid - tStream >> std::noskipws >> tFloat; - // Check the entire string was consumed - // and if either failbit or badbit is set - return tStream.eof() && !tStream.fail(); + static bool is_optional(std::string_view aName) { + return !is_positional(aName); } - // If an argument starts with "-" or "--", then it's optional - static bool is_optional(const std::string &aName) { - return (aName.size() > 1 && aName[0] == '-' && !is_integer(aName) && - !is_float(aName)); - } - - static bool is_positional(const std::string &aName) { - return !is_optional(aName); + /* + * positional: + * _empty_ + * '-' + * '-' decimal-literal + * !'-' anything + */ + static bool is_positional(std::string_view aName) { + switch (lookahead(aName)) { + case eof: + return true; + case '-': { + aName.remove_prefix(1); + if (aName.empty()) + return true; + else + return is_decimal_literal(aName); + } + default: + return true; + } } /* diff --git a/test/test_negative_numbers.cpp b/test/test_negative_numbers.cpp index 5402e7d..2eeadd1 100644 --- a/test/test_negative_numbers.cpp +++ b/test/test_negative_numbers.cpp @@ -97,3 +97,170 @@ TEST_CASE("Parse numbers in E notation (capital E)" * program.parse_args({"./main", "-1.32E4"}); REQUIRE(program.get("number") == -13200.0); } + +TEST_CASE("Recognize negative decimal numbers" * + test_suite("positional_arguments")) { + argparse::ArgumentParser program("test"); + program.add_argument("positional"); + + SUBCASE("zero") { REQUIRE_NOTHROW(program.parse_args({"test", "-0"})); } + SUBCASE("not a decimal") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-00"}), std::runtime_error); + } + SUBCASE("looks like an octal") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-003"}), std::runtime_error); + } + SUBCASE("nonzero-digit") { + REQUIRE_NOTHROW(program.parse_args({"test", "-9"})); + } + SUBCASE("nonzero-digit digit-sequence") { + REQUIRE_NOTHROW(program.parse_args({"test", "-92180"})); + } + SUBCASE("zero dot") { REQUIRE_NOTHROW(program.parse_args({"test", "-0."})); } + SUBCASE("nonzero-digit dot") { + REQUIRE_NOTHROW(program.parse_args({"test", "-8."})); + } + SUBCASE("nonzero-digit digit-sequence dot") { + REQUIRE_NOTHROW(program.parse_args({"test", "-200."})); + } + SUBCASE("integer-part dot") { + REQUIRE_NOTHROW(program.parse_args({"test", "-003."})); + } + SUBCASE("dot digit-sequence") { + REQUIRE_NOTHROW(program.parse_args({"test", "-.0927"})); + } + SUBCASE("not a single dot") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-."}), std::runtime_error); + } + SUBCASE("not a single e") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-e"}), std::runtime_error); + } + SUBCASE("not dot e") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-.e"}), std::runtime_error); + } + SUBCASE("integer-part exponent-part without sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-1e32"})); + } + SUBCASE("integer-part exponent-part with positive sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-1e+32"})); + } + SUBCASE("integer-part exponent-part with negative sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-00e-0"})); + } + SUBCASE("missing mantissa") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-e32"}), std::runtime_error); + } + SUBCASE("missing mantissa but with positive sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-e+7"}), std::runtime_error); + } + SUBCASE("missing mantissa but with negative sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-e-1"}), std::runtime_error); + } + SUBCASE("nothing after e followed by zero") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-0e"}), std::runtime_error); + } + SUBCASE("nothing after e followed by integer-part") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-13e"}), std::runtime_error); + } + SUBCASE("integer-part dot exponent-part without sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-18.e0"})); + } + SUBCASE("integer-part dot exponent-part with positive sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-18.e+92"})); + } + SUBCASE("integer-part dot exponent-part with negative sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-0.e-92"})); + } + SUBCASE("nothing after e followed by integer-part dot") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-13.e"}), + std::runtime_error); + } + SUBCASE("dot digit-sequence exponent-part without sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-.023e0"})); + } + SUBCASE("dot digit-sequence exponent-part with positive sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-.2e+92"})); + } + SUBCASE("dot digit-sequence exponent-part with negative sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-.71564e-92"})); + } + SUBCASE("nothing after e in fractional-part") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-.283e"}), + std::runtime_error); + } + SUBCASE("exponent-part followed by only a dot") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-.e3"}), std::runtime_error); + } + SUBCASE("exponent-part followed by only a dot but with positive sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-.e+3"}), + std::runtime_error); + } + SUBCASE("exponent-part followed by only a dot but with negative sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-.e-3"}), + std::runtime_error); + } + SUBCASE("integer-part dot digit-sequence exponent-part without sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-02.023e4000"})); + } + SUBCASE("integer-part dot digit-sequence exponent-part with positive sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-3.239e+76"})); + } + SUBCASE("integer-part dot digit-sequence exponent-part with negative sign") { + REQUIRE_NOTHROW(program.parse_args({"test", "-238237.0e-2"})); + } + SUBCASE("nothing after e") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-3.14e"}), + std::runtime_error); + } + SUBCASE("nothing after e and positive sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-2.17e+"}), + std::runtime_error); + } + SUBCASE("nothing after e and negative sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-13.6e-"}), + std::runtime_error); + } + SUBCASE("more than one sign present in exponent-part") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-13.6e+-23"}), + std::runtime_error); + } + SUBCASE("sign at wrong position") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-3.6e23+"}), + std::runtime_error); + } + SUBCASE("more than one exponent-part") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-3.6e2e9"}), + std::runtime_error); + } + SUBCASE("more than one fractional-part") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-3.6.3"}), + std::runtime_error); + } + SUBCASE("number has its own sign") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-+42"}), std::runtime_error); + } + SUBCASE("looks like hexadecimal integer") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-0x0"}), std::runtime_error); + } + SUBCASE("looks like hexadecimal floating-point") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-0x27.8p1"}), + std::runtime_error); + } + SUBCASE("looks like hexadecimal floating-point without prefix") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-3.8p1"}), + std::runtime_error); + } + SUBCASE("Richard's pp-number") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-0x1e+2"}), + std::runtime_error); + } + SUBCASE("Infinity") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-inf"}), std::runtime_error); + REQUIRE_THROWS_AS(program.parse_args({"test", "-INFINITY"}), + std::runtime_error); + } + SUBCASE("NaN") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-nan"}), std::runtime_error); + REQUIRE_THROWS_AS(program.parse_args({"test", "-NAN"}), std::runtime_error); + } +}