Skip to content

Commit

Permalink
Parse floating-point numbers in .scan
Browse files Browse the repository at this point in the history
fixes: p-ranav#63
  • Loading branch information
lichray committed Nov 26, 2019
1 parent 426a5db commit 00bdf13
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
71 changes: 71 additions & 0 deletions include/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ SOFTWARE.
#pragma once
#include <algorithm>
#include <any>
#include <cerrno>
#include <charconv>
#include <cstdlib>
#include <functional>
Expand Down Expand Up @@ -190,6 +191,76 @@ template <class T> struct parse_number<T> {
}
};

template <class T> constexpr auto generic_strtod = nullptr;
template <> constexpr auto generic_strtod<float> = strtof;
template <> constexpr auto generic_strtod<double> = strtod;
template <> constexpr auto generic_strtod<long double> = strtold;

template <class T> inline auto do_strtod(std::string const &s) -> T {
if (std::isspace(static_cast<unsigned char>(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<T>(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 <class T> struct parse_number<T, chars_format::general> {
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<T>(s);
}
};

template <class T> struct parse_number<T, chars_format::hex> {
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<T>(s);
}
};

template <class T> struct parse_number<T, chars_format::scientific> {
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<T>(s);
}
};

template <class T> struct parse_number<T, chars_format::fixed> {
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<T>(s);
}
};

} // namespace details

class ArgumentParser;
Expand Down
165 changes: 165 additions & 0 deletions test/test_scan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,168 @@ TEST_CASE_TEMPLATE("Parse integer argument of any format" * test_suite("scan"),
std::invalid_argument);
}
}

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<T>("-n") == 0.);
}

SUBCASE("non-negative") {
program.parse_args({"test", "-n", "3.14"});
REQUIRE(program.get<T>("-n") == doctest::Approx(3.14));
}

SUBCASE("negative") {
program.parse_args({"test", "-n", "-0.12"});
REQUIRE(program.get<T>("-n") == doctest::Approx(-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<T>("-n") == 0x0.p0);
}

SUBCASE("non-negative") {
program.parse_args({"test", "-n", "0x1a.3p+1"});
REQUIRE(program.get<T>("-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<T>("-n") == 0e0);
}

SUBCASE("non-negative") {
program.parse_args({"test", "-n", "3.14e-1"});
REQUIRE(program.get<T>("-n") == doctest::Approx(3.14e-1));
}

SUBCASE("negative") {
program.parse_args({"test", "-n", "-0.12e+1"});
REQUIRE(program.get<T>("-n") == doctest::Approx(-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<T>("-n") == .0);
}

SUBCASE("non-negative") {
program.parse_args({"test", "-n", "3.14"});
REQUIRE(program.get<T>("-n") == doctest::Approx(3.14));
}

SUBCASE("negative") {
program.parse_args({"test", "-n", "-0.12"});
REQUIRE(program.get<T>("-n") == doctest::Approx(-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);
}
}

0 comments on commit 00bdf13

Please sign in to comment.