Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify parsing numeric arguments with .scan #65

Merged
merged 3 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ matrix:
dist: bionic
language: cpp
compiler: gcc
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-8
env: CXX=g++-8 CC=gcc-8
- os: osx
osx_image: xcode10.2
language: cpp
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,10 +587,13 @@ $ ./main fex
baz
```

## Supported Compilers
* GCC >= 7.0.0
* Clang >= 4.0
* MSVC >= 2017
## Supported Toolchains

| Compiler | Standard Library | Test Environment |
| :------------------- | :--------------- | :----------------- |
| GCC >= 8.3.0 | libstdc++ | Ubuntu 18.04 |
| Clang >= 7.0.0 | libc++ | Xcode 10.2 |
| MSVC >= 14.16 | Microsoft STL | Visual Studio 2017 |

## Contributing
Contributions are welcome, have a look at the [CONTRIBUTING.md](CONTRIBUTING.md) document for more information.
Expand Down
209 changes: 209 additions & 0 deletions include/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ SOFTWARE.
#pragma once
#include <algorithm>
#include <any>
#include <cerrno>
#include <charconv>
#include <cstdlib>
#include <functional>
#include <iostream>
Expand Down Expand Up @@ -76,6 +78,25 @@ struct is_string_like
: std::conjunction<std::is_constructible<std::string, T>,
std::is_convertible<T, std::string_view>> {};

template <typename T> constexpr bool standard_signed_integer = false;
template <> constexpr bool standard_signed_integer<signed char> = true;
template <> constexpr bool standard_signed_integer<short int> = true;
template <> constexpr bool standard_signed_integer<int> = true;
template <> constexpr bool standard_signed_integer<long int> = true;
template <> constexpr bool standard_signed_integer<long long int> = true;

template <typename T> constexpr bool standard_unsigned_integer = false;
template <> constexpr bool standard_unsigned_integer<unsigned char> = true;
template <> constexpr bool standard_unsigned_integer<unsigned short int> = true;
template <> constexpr bool standard_unsigned_integer<unsigned int> = true;
template <> constexpr bool standard_unsigned_integer<unsigned long int> = true;
template <>
constexpr bool standard_unsigned_integer<unsigned long long int> = true;

template <typename T>
constexpr bool standard_integer =
standard_signed_integer<T> || standard_unsigned_integer<T>;

template <class F, class Tuple, class Extra, size_t... I>
constexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x,
std::index_sequence<I...>) {
Expand All @@ -91,6 +112,155 @@ constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) {
std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

constexpr auto pointer_range(std::string_view s) noexcept {
return std::tuple(s.data(), s.data() + s.size());
}

template <class CharT, class Traits>
constexpr bool starts_with(std::basic_string_view<CharT, Traits> prefix,
std::basic_string_view<CharT, Traits> s) noexcept {
return s.substr(0, prefix.size()) == prefix;
}

enum class chars_format {
scientific = 0x1,
fixed = 0x2,
hex = 0x4,
general = fixed | scientific
};

struct consume_hex_prefix_result {
bool is_hexadecimal;
std::string_view rest;
};

using namespace std::literals;

constexpr auto consume_hex_prefix(std::string_view s)
-> consume_hex_prefix_result {
if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) {
s.remove_prefix(2);
return {true, s};
} else {
return {false, s};
}
}

template <class T, auto Param>
inline auto do_from_chars(std::string_view s) -> T {
T x;
auto [first, last] = pointer_range(s);
auto [ptr, ec] = std::from_chars(first, last, x, Param);
if (ec == std::errc()) {
if (ptr == last)
return x;
else
throw std::invalid_argument{"pattern does not match to the end"};
} else if (ec == std::errc::invalid_argument) {
throw std::invalid_argument{"pattern not found"};
} else if (ec == std::errc::result_out_of_range) {
throw std::range_error{"not representable"};
} else {
return x; // unreachable
}
}

template <class T, auto Param = 0> struct parse_number {
auto operator()(std::string_view s) -> T {
return do_from_chars<T, Param>(s);
}
};

template <class T> struct parse_number<T, 16> {
auto operator()(std::string_view s) -> T {
if (auto [ok, rest] = consume_hex_prefix(s); ok)
return do_from_chars<T, 16>(rest);
else
throw std::invalid_argument{"pattern not found"};
}
};

template <class T> struct parse_number<T> {
auto operator()(std::string_view s) -> T {
if (auto [ok, rest] = consume_hex_prefix(s); ok)
return do_from_chars<T, 16>(rest);
else if (starts_with("0"sv, s))
return do_from_chars<T, 8>(rest);
else
return do_from_chars<T, 10>(rest);
}
};

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 (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 Expand Up @@ -161,6 +331,45 @@ class Argument {
return *this;
}

template <char Shape, typename T>
auto scan() -> std::enable_if_t<std::is_arithmetic_v<T>, Argument &> {
static_assert(!(std::is_const_v<T> || std::is_volatile_v<T>),
"T should not be cv-qualified");
auto is_one_of = [](char c, auto... x) constexpr {
return ((c == x) || ...);
};

if constexpr (is_one_of(Shape, 'd') && details::standard_integer<T>)
action(details::parse_number<T, 10>());
else if constexpr (is_one_of(Shape, 'i') && details::standard_integer<T>)
action(details::parse_number<T>());
else if constexpr (is_one_of(Shape, 'u') &&
details::standard_unsigned_integer<T>)
action(details::parse_number<T, 10>());
else if constexpr (is_one_of(Shape, 'o') &&
details::standard_unsigned_integer<T>)
action(details::parse_number<T, 8>());
else if constexpr (is_one_of(Shape, 'x', 'X') &&
details::standard_unsigned_integer<T>)
action(details::parse_number<T, 16>());
else if constexpr (is_one_of(Shape, 'a', 'A') &&
std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::hex>());
else if constexpr (is_one_of(Shape, 'e', 'E') &&
std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::scientific>());
else if constexpr (is_one_of(Shape, 'f', 'F') &&
std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::fixed>());
else if constexpr (is_one_of(Shape, 'g', 'G') &&
std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::general>());
else
static_assert(alignof(T) == 0, "No scan specification for T");

return *this;
}

Argument &nargs(int aNumArgs) {
if (aNumArgs < 0)
throw std::logic_error("Number of arguments must be non-negative");
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ file(GLOB ARGPARSE_TEST_SOURCES
test_parse_args.cpp
test_positional_arguments.cpp
test_required_arguments.cpp
test_scan.cpp
test_value_semantics.cpp
)
set_source_files_properties(main.cpp
Expand Down
Loading