mirror of
https://github.com/KeqingMoe/argparse.git
synced 2025-07-04 07:04: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:
|
||||
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<char>::eof();
|
||||
|
||||
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;
|
||||
char *tPtr;
|
||||
strtol(aValue.c_str(), &tPtr, 10);
|
||||
return (*tPtr == 0);
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
post_decimal_point:
|
||||
if (is_digit(lookahead(s))) {
|
||||
s = consume_digits(s);
|
||||
goto exponent_part_opt;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_positional(const std::string &aName) {
|
||||
return !is_optional(aName);
|
||||
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_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"});
|
||||
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