mirror of
https://github.com/KeqingMoe/argparse.git
synced 2025-07-04 15:14:39 +00:00
Determine negative numeric values with a grammar
Two differences that diverge from the existing behavior: 1. Leading zeros are not allowed for integers. Negative octal numbers such as `-066` are not meant to be treated as positional arguments, but existing code recognize them as decimal numbers. Note that negative floating-point numbers with leading zeros (`-003.`) are unambiguous and are recognized. 2. Inf and NaN are not recognized. This is because options like `-inf` is indistinguishable from a compound argument that meant to be a shorthand for `-i -n -f`. fixes: p-ranav/argparse#55
This commit is contained in:
parent
473d550ea3
commit
964790cf3c
@ -301,33 +301,179 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool is_integer(const std::string &aValue) {
|
static constexpr int eof = std::char_traits<char>::eof();
|
||||||
if (aValue.empty() ||
|
|
||||||
((!isdigit(aValue[0])) && (aValue[0] != '-') && (aValue[0] != '+')))
|
static auto lookahead(std::string_view s) -> int {
|
||||||
|
if (s.empty())
|
||||||
|
return eof;
|
||||||
|
else
|
||||||
|
return static_cast<int>(static_cast<unsigned char>(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;
|
return false;
|
||||||
char *tPtr;
|
|
||||||
strtol(aValue.c_str(), &tPtr, 10);
|
|
||||||
return (*tPtr == 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_float(const std::string &aValue) {
|
integer_part:
|
||||||
std::istringstream tStream(aValue);
|
s = consume_digits(s);
|
||||||
float tFloat;
|
integer_part_consumed:
|
||||||
// noskipws considers leading whitespace invalid
|
switch (lookahead(s)) {
|
||||||
tStream >> std::noskipws >> tFloat;
|
case '.': {
|
||||||
// Check the entire string was consumed
|
s.remove_prefix(1);
|
||||||
// and if either failbit or badbit is set
|
if (is_digit(lookahead(s)))
|
||||||
return tStream.eof() && !tStream.fail();
|
goto post_decimal_point;
|
||||||
|
else
|
||||||
|
goto exponent_part_opt;
|
||||||
|
}
|
||||||
|
case 'e':
|
||||||
|
case 'E': {
|
||||||
|
s.remove_prefix(1);
|
||||||
|
goto post_e;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an argument starts with "-" or "--", then it's optional
|
post_decimal_point:
|
||||||
static bool is_optional(const std::string &aName) {
|
if (is_digit(lookahead(s))) {
|
||||||
return (aName.size() > 1 && aName[0] == '-' && !is_integer(aName) &&
|
s = consume_digits(s);
|
||||||
!is_float(aName));
|
goto exponent_part_opt;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_positional(const std::string &aName) {
|
exponent_part_opt:
|
||||||
return !is_optional(aName);
|
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_optional(std::string_view aName) {
|
||||||
|
return !is_positional(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -97,3 +97,170 @@ TEST_CASE("Parse numbers in E notation (capital E)" *
|
|||||||
program.parse_args({"./main", "-1.32E4"});
|
program.parse_args({"./main", "-1.32E4"});
|
||||||
REQUIRE(program.get<double>("number") == -13200.0);
|
REQUIRE(program.get<double>("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user