From c834684f5328065e1d32ce3e352f556f341c1aea Mon Sep 17 00:00:00 2001 From: Nitin Kumar <59679977+lazysegtree@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:47:04 +0530 Subject: [PATCH] feat: Allow parsing Inf,NaN in float, double and long double --- include/cxxopts.hpp | 83 ++++++++++++++++++++++++++++++++++++ test/options.cpp | 101 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/include/cxxopts.hpp b/include/cxxopts.hpp index 0bae743..4ebc7b4 100644 --- a/include/cxxopts.hpp +++ b/include/cxxopts.hpp @@ -580,6 +580,16 @@ class incorrect_argument_type : public parsing ) { } + explicit incorrect_argument_type + ( + const std::string& arg, + const std::string& message + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse becuause " + message + ) + { + } }; } // namespace exceptions @@ -604,6 +614,25 @@ void throw_or_mimic(const std::string& text) #endif } +template +void throw_or_mimic(const std::string& text, const std::string& msg) +{ + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{text, msg}; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text, msg}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif +} + using OptionNames = std::vector; namespace values { @@ -1120,6 +1149,60 @@ parse_value(const std::string& text, bool& value) throw_or_mimic(text); } +inline +void +fparse(const std::string& text, float& value){ + value = std::stof(text); +} + +inline +void +fparse(const std::string& text, double& value){ + value = std::stod(text); +} + +inline +void +fparse(const std::string& text, long double& value){ + value = std::stold(text); +} + +template +inline +void +fparse_with_exception(const std::string& text, T& value) +{ + try{ + fparse(text, value); + } catch(const std::out_of_range&) { + throw_or_mimic(text, "out of range"); + } catch(const std::invalid_argument&) { + throw_or_mimic(text, "invalid arguement"); + } +} + +inline +void +parse_value(const std::string& text, float& value) +{ + fparse_with_exception(text, value); +} + +inline +void +parse_value(const std::string& text, double& value) +{ + fparse_with_exception(text, value); +} + +inline +void +parse_value(const std::string& text, long double& value) +{ + fparse_with_exception(text, value); +} + + inline void parse_value(const std::string& text, std::string& value) diff --git a/test/options.cpp b/test/options.cpp index 5293291..668d14e 100644 --- a/test/options.cpp +++ b/test/options.cpp @@ -1,5 +1,6 @@ #include "catch.hpp" #include +#include #include @@ -934,6 +935,106 @@ TEST_CASE("Floats", "[options]") CHECK(positional[3] == -1.5e6); } +TEST_CASE("Floating special values and parse failures", "[options]") +{ + cxxopts::Options options("floating_special_values", "floating special values and parse failures"); + options.add_options() + ("f,float", "Float", cxxopts::value()) + ("d,double", "Double", cxxopts::value()) + ("l,long-double", "Long double", cxxopts::value()); + + enum class expected_result { + pos_inf, + neg_inf, + nan, + parse_error + }; + + struct testcase { + std::string name; + Argv argv; + expected_result expected; + } tests[] = { + { + "parses infinity", + Argv{"floating_special_values", "--float", "infinity", "--double", "infinity", "--long-double", "infinity"}, + expected_result::pos_inf, + }, + { + "parses negative inf", + Argv{"floating_special_values", "--float", "-INF", "--double", "-inf", "--long-double", "-Inf"}, + expected_result::neg_inf, + }, + { + "parses nan", + Argv{"floating_special_values", "--float", "nan", "--double", "nan", "--long-double", "nan"}, + expected_result::nan, + }, + { + "rejects invalid token for float", + Argv{"floating_special_values", "--float", "adsfas"}, + expected_result::parse_error, + }, + { + "rejects invalid token for double", + Argv{"floating_special_values", "--double", "adsfas"}, + expected_result::parse_error, + }, + { + "rejects invalid token for long double", + Argv{"floating_special_values", "--long-double", "adsfas"}, + expected_result::parse_error, + }, + { + "rejects out of range token for float", + Argv{"floating_special_values", "--float", "1000000000000000000000000000000000000000"}, + expected_result::parse_error, + }, + }; + + for (const auto& tc : tests) { + SECTION(tc.name) { + if (tc.expected == expected_result::parse_error) { + CHECK_THROWS_AS(options.parse(tc.argv.argc(), tc.argv.argv()), cxxopts::exceptions::incorrect_argument_type); + continue; + } + + auto result = options.parse(tc.argv.argc(), tc.argv.argv()); + + auto check_value = [&](long double value) { + switch (tc.expected) { + case expected_result::pos_inf: + CHECK(std::isinf(value)); + CHECK(value > 0.0L); + break; + case expected_result::neg_inf: + CHECK(std::isinf(value)); + CHECK(value < 0.0L); + break; + case expected_result::nan: + CHECK(std::isnan(value)); + break; + case expected_result::parse_error: + // Handled above + break; + } + }; + + check_value(result["float"].as()); + check_value(result["double"].as()); + check_value(result["long-double"].as()); + } + } + + SECTION("parses finite values") { + Argv argv{"floating_special_values", "--float", "1.25", "--double", "-2.5", "--long-double", "3.75"}; + auto result = options.parse(argv.argc(), argv.argv()); + CHECK(result["float"].as() == 1.25f); + CHECK(result["double"].as() == -2.5); + CHECK(result["long-double"].as() == 3.75L); + } +} + TEST_CASE("Invalid integers", "[integer]") { cxxopts::Options options("invalid_integers", "rejects invalid integers"); options.add_options()