diff --git a/README.md b/README.md index dd717300..9638e348 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ The `add_option_function(...` function will typically require the template 🚧 Flag options specified through the `add_flag*` functions allow a syntax for the option names to default particular options to a false value or any other value if some flags are passed. For example: ```cpp -app.add_flag("--flag,!--no-flag,result,"help for flag"); // 🚧 +app.add_flag("--flag,!--no-flag",result,"help for flag"); // 🚧 ``` specifies that if `--flag` is passed on the command line result will be true or contain a value of 1. If `--no-flag` is @@ -341,6 +341,8 @@ CLI11 has several Validators built-in that perform some common checks - `CLI::IsMember(...)`: 🚧 Require an option be a member of a given set. See [Transforming Validators](#transforming-validators) for more details. - `CLI::Transformer(...)`: 🚧 Modify the input using a map. See [Transforming Validators](#transforming-validators) for more details. - `CLI::CheckedTransformer(...)`: 🚧 Modify the input using a map, and require that the input is either in the set or already one of the outputs of the set. See [Transforming Validators](#transforming-validators) for more details. +- `CLI::AsNumberWithUnit(...)`: Modify the ` ` pair by matching the unit and multiplying the number by the corresponding factor. It can be used as a base for transformers, that accept things like size values (`1 KB`) or durations (`0.33 ms`). +- `CLI::AsSizeValue(...)`: Convert inputs like `100b`, `42 KB`, `101 Mb`, `11 Mib` to absolute values. `KB` can be configured to be interpreted as 10^3 or 2^10. - `CLI::ExistingFile`: Requires that the file exists if given. - `CLI::ExistingDirectory`: Requires that the directory exists. - `CLI::ExistingPath`: Requires that the path (file or directory) exists. diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index aee833ab..bb4944f7 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -193,6 +193,11 @@ inline bool valid_name_string(const std::string &str) { return true; } +/// Verify that str consists of letters only +inline bool isalpha(const std::string &str) { + return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); +} + /// Return a lower case version of a string inline std::string to_lower(std::string str) { std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { diff --git a/include/CLI/Validators.hpp b/include/CLI/Validators.hpp index 54294743..ee4011ee 100644 --- a/include/CLI/Validators.hpp +++ b/include/CLI/Validators.hpp @@ -6,8 +6,10 @@ #include "CLI/StringTools.hpp" #include "CLI/TypeTools.hpp" +#include #include #include +#include #include #include @@ -443,14 +445,17 @@ template std::string generate_set(const T &set) { } /// Generate a string representation of a map -template std::string generate_map(const T &map) { +template std::string generate_map(const T &map, bool key_only = false) { using element_t = typename detail::element_type::type; using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair std::string out(1, '{'); out.append(detail::join(detail::smart_deref(map), - [](const iteration_type_t &v) { - return detail::as_string(detail::pair_adaptor::first(v)) + "->" + - detail::as_string(detail::pair_adaptor::second(v)); + [key_only](const iteration_type_t &v) { + auto res = detail::as_string(detail::pair_adaptor::first(v)); + if(!key_only) { + res += "->" + detail::as_string(detail::pair_adaptor::second(v)); + } + return res; }, ",")); out.push_back('}'); @@ -505,6 +510,31 @@ auto search(const T &set, const V &val, const std::function &filter_functi return {(it != std::end(setref)), it}; } +/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. +template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + if(a == 0 || b == 0) { + a *= b; + return true; + } + T c = a * b; + if(c / a != b) { + return false; + } + a = c; + return true; +} + +/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. +template +typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + T c = a * b; + if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { + return false; + } + a = c; + return true; +} + } // namespace detail /// Verify items are in a set class IsMember : public Validator { @@ -707,7 +737,7 @@ class CheckedTransformer : public Validator { : CheckedTransformer(std::forward(mapping), [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, other...) {} -}; // namespace CLI +}; /// Helper function to allow ignore_case to be passed to IsMember or Transform inline std::string ignore_case(std::string item) { return detail::to_lower(item); } @@ -722,6 +752,197 @@ inline std::string ignore_space(std::string item) { return item; } +/// Multiply a number by a factor using given mapping. +/// Can be used to write transforms for SIZE or DURATION inputs. +/// +/// Example: +/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` +/// one can recognize inputs like "100", "12kb", "100 MB", +/// that will be automatically transformed to 100, 14448, 104857600. +/// +/// Output number type matches the type in the provided mapping. +/// Therefore, if it is required to interpret real inputs like "0.42 s", +/// the mapping should be of a type or . +class AsNumberWithUnit : public Validator { + public: + /// Adjust AsNumberWithUnit behavior. + /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. + /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError + /// if UNIT_REQUIRED is set and unit literal is not found. + enum Options { + CASE_SENSITIVE = 0, + CASE_INSENSITIVE = 1, + UNIT_OPTIONAL = 0, + UNIT_REQUIRED = 2, + DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL + }; + + template + explicit AsNumberWithUnit(std::map mapping, + Options opts = DEFAULT, + const std::string &unit_name = "UNIT") { + description(generate_description(unit_name, opts)); + validate_mapping(mapping, opts); + + // transform function + func_ = [mapping, opts](std::string &input) -> std::string { + Number num; + + detail::rtrim(input); + if(input.empty()) { + throw ValidationError("Input is empty"); + } + + // Find split position between number and prefix + auto unit_begin = input.end(); + while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { + --unit_begin; + } + + std::string unit{unit_begin, input.end()}; + input.resize(std::distance(input.begin(), unit_begin)); + detail::trim(input); + + if(opts & UNIT_REQUIRED && unit.empty()) { + throw ValidationError("Missing mandatory unit"); + } + if(opts & CASE_INSENSITIVE) { + unit = detail::to_lower(unit); + } + + bool converted = detail::lexical_cast(input, num); + if(!converted) { + throw ValidationError("Value " + input + " could not be converted to " + detail::type_name()); + } + + if(unit.empty()) { + // No need to modify input if no unit passed + return {}; + } + + // find corresponding factor + auto it = mapping.find(unit); + if(it == mapping.end()) { + throw ValidationError(unit + + " unit not recognized. " + "Allowed values: " + + detail::generate_map(mapping, true)); + } + + // perform safe multiplication + bool ok = detail::checked_multiply(num, it->second); + if(!ok) { + throw ValidationError(detail::as_string(num) + " multiplied by " + unit + + " factor would cause number overflow. Use smaller value."); + } + input = detail::as_string(num); + + return {}; + }; + } + + private: + /// Check that mapping contains valid units. + /// Update mapping for CASE_INSENSITIVE mode. + template static void validate_mapping(std::map &mapping, Options opts) { + for(auto &kv : mapping) { + if(kv.first.empty()) { + throw ValidationError("Unit must not be empty."); + } + if(!detail::isalpha(kv.first)) { + throw ValidationError("Unit must contain only letters."); + } + } + + // make all units lowercase if CASE_INSENSITIVE + if(opts & CASE_INSENSITIVE) { + std::map lower_mapping; + for(auto &kv : mapping) { + auto s = detail::to_lower(kv.first); + if(lower_mapping.count(s)) { + throw ValidationError("Several matching lowercase unit representations are found: " + s); + } + lower_mapping[detail::to_lower(kv.first)] = kv.second; + } + mapping = std::move(lower_mapping); + } + } + + /// Generate description like this: NUMBER [UNIT] + template static std::string generate_description(const std::string &name, Options opts) { + std::stringstream out; + out << detail::type_name() << ' '; + if(opts & UNIT_REQUIRED) { + out << name; + } else { + out << '[' << name << ']'; + } + return out.str(); + } +}; + +/// Converts a human-readable size string (with unit literal) to uin64_t size. +/// Example: +/// "100" => 100 +/// "1 b" => 100 +/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) +/// "10 KB" => 10240 +/// "10 kb" => 10240 +/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) +/// "10kb" => 10240 +/// "2 MB" => 2097152 +/// "2 EiB" => 2^61 // Units up to exibyte are supported +class AsSizeValue : public AsNumberWithUnit { + public: + using result_t = uint64_t; + + /// If kb_is_1000 is true, + /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 + /// (same applies to higher order units as well). + /// Otherwise, interpret all literals as factors of 1024. + /// The first option is formally correct, but + /// the second interpretation is more wide-spread + /// (see https://en.wikipedia.org/wiki/Binary_prefix). + explicit AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { + if(kb_is_1000) { + description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); + } else { + description("SIZE [b, kb(=1024b), ...]"); + } + } + + private: + /// Get mapping + static std::map init_mapping(bool kb_is_1000) { + std::map m; + result_t k_factor = kb_is_1000 ? 1000 : 1024; + result_t ki_factor = 1024; + result_t k = 1; + result_t ki = 1; + m["b"] = 1; + for(std::string p : {"k", "m", "g", "t", "p", "e"}) { + k *= k_factor; + ki *= ki_factor; + m[p] = k; + m[p + "b"] = k; + m[p + "i"] = ki; + m[p + "ib"] = ki; + } + return m; + } + + /// Cache calculated mapping + static std::map get_mapping(bool kb_is_1000) { + if(kb_is_1000) { + static auto m = init_mapping(true); + return m; + } else { + static auto m = init_mapping(false); + return m; + } + } +}; + namespace detail { /// Split a string into a program name and command line arguments /// the string is assumed to contain a file name followed by other arguments diff --git a/tests/HelpersTest.cpp b/tests/HelpersTest.cpp index ca376b84..6f2e0234 100644 --- a/tests/HelpersTest.cpp +++ b/tests/HelpersTest.cpp @@ -105,6 +105,18 @@ TEST(StringTools, flagValues) { EXPECT_EQ(CLI::detail::to_flag_value("475555233"), 475555233); } +TEST(StringTools, Validation) { + EXPECT_TRUE(CLI::detail::isalpha("")); + EXPECT_TRUE(CLI::detail::isalpha("a")); + EXPECT_TRUE(CLI::detail::isalpha("abcd")); + EXPECT_FALSE(CLI::detail::isalpha("_")); + EXPECT_FALSE(CLI::detail::isalpha("2")); + EXPECT_FALSE(CLI::detail::isalpha("test test")); + EXPECT_FALSE(CLI::detail::isalpha("test ")); + EXPECT_FALSE(CLI::detail::isalpha(" test")); + EXPECT_FALSE(CLI::detail::isalpha("test2")); +} + TEST(Trim, Various) { std::string s1{" sdlfkj sdflk sd s "}; std::string a1{"sdlfkj sdflk sd s"}; @@ -360,6 +372,194 @@ TEST(Validators, ProgramNameSplit) { EXPECT_TRUE(res.second.empty()); } +TEST(CheckedMultiply, Int) { + int a = 10; + int b = -20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, -200); + + a = 0; + b = -20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, 0); + + a = 20; + b = 0; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, 0); + + a = std::numeric_limits::max(); + b = 1; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::max(); + b = 2; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::max(); + b = -1; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, -std::numeric_limits::max()); + + a = std::numeric_limits::max(); + b = std::numeric_limits::max(); + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::min(); + b = std::numeric_limits::max(); + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::min()); + + a = std::numeric_limits::min(); + b = 1; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::min()); + + a = std::numeric_limits::min(); + b = -1; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::min()); + + a = std::numeric_limits::min() / 100; + b = 99; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::min() / 100 * 99); +} + +TEST(CheckedMultiply, SizeT) { + size_t a = 10; + size_t b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, 200u); + + a = 0u; + b = 20u; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, 0u); + + a = 20u; + b = 0u; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, 0u); + + a = std::numeric_limits::max(); + b = 1u; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::max(); + b = 2u; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::max(); + b = std::numeric_limits::max(); + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max()); + + a = std::numeric_limits::max() / 100; + b = 99u; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_EQ(a, std::numeric_limits::max() / 100u * 99u); +} + +TEST(CheckedMultiply, Float) { + float a = 10; + float b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, 200); + + a = 0; + b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, 0); + + a = INFINITY; + b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, INFINITY); + + a = 2; + b = -INFINITY; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, -INFINITY); + + a = std::numeric_limits::max() / 100; + b = 1; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, std::numeric_limits::max() / 100); + + a = std::numeric_limits::max() / 100; + b = 99; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, std::numeric_limits::max() / 100 * 99); + + a = std::numeric_limits::max() / 100; + b = 101; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, std::numeric_limits::max() / 100); + + a = std::numeric_limits::max() / 100; + b = -99; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, std::numeric_limits::max() / 100 * -99); + + a = std::numeric_limits::max() / 100; + b = -101; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_FLOAT_EQ(a, std::numeric_limits::max() / 100); +} + +TEST(CheckedMultiply, Double) { + double a = 10; + double b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, 200); + + a = 0; + b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, 0); + + a = INFINITY; + b = 20; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, INFINITY); + + a = 2; + b = -INFINITY; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, -INFINITY); + + a = std::numeric_limits::max() / 100; + b = 1; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, std::numeric_limits::max() / 100); + + a = std::numeric_limits::max() / 100; + b = 99; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, std::numeric_limits::max() / 100 * 99); + + a = std::numeric_limits::max() / 100; + b = 101; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, std::numeric_limits::max() / 100); + + a = std::numeric_limits::max() / 100; + b = -99; + ASSERT_TRUE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, std::numeric_limits::max() / 100 * -99); + + a = std::numeric_limits::max() / 100; + b = -101; + ASSERT_FALSE(CLI::detail::checked_multiply(a, b)); + ASSERT_DOUBLE_EQ(a, std::numeric_limits::max() / 100); +} + // Yes, this is testing an app_helper :) TEST(AppHelper, TempfileCreated) { std::string name = "TestFileNotUsed.txt"; diff --git a/tests/TransformTest.cpp b/tests/TransformTest.cpp index ba0e11a9..8bb055f2 100644 --- a/tests/TransformTest.cpp +++ b/tests/TransformTest.cpp @@ -431,3 +431,406 @@ TEST_F(TApp, BoundTests) { EXPECT_TRUE(help.find("bounded to") != std::string::npos); EXPECT_TRUE(help.find("[3.4 - 5.9]") != std::string::npos); } + +TEST_F(TApp, NumberWithUnitCorrecltySplitNumber) { + std::map mapping{{"a", 10}, {"b", 100}, {"cc", 1000}}; + + int value = 0; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping)); + + args = {"-n", "42"}; + run(); + EXPECT_EQ(value, 42); + + args = {"-n", "42a"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", " 42 cc "}; + run(); + EXPECT_EQ(value, 42000); + args = {"-n", " -42 cc "}; + run(); + EXPECT_EQ(value, -42000); +} + +TEST_F(TApp, NumberWithUnitFloatTest) { + std::map mapping{{"a", 10}, {"b", 100}, {"cc", 1000}}; + double value = 0; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping)); + + args = {"-n", "42"}; + run(); + EXPECT_DOUBLE_EQ(value, 42); + + args = {"-n", ".5"}; + run(); + EXPECT_DOUBLE_EQ(value, .5); + + args = {"-n", "42.5 a"}; + run(); + EXPECT_DOUBLE_EQ(value, 425); + + args = {"-n", "42.cc"}; + run(); + EXPECT_DOUBLE_EQ(value, 42000); +} + +TEST_F(TApp, NumberWithUnitCaseSensitive) { + std::map mapping{{"a", 10}, {"A", 100}}; + + int value = 0; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping, CLI::AsNumberWithUnit::CASE_SENSITIVE)); + + args = {"-n", "42a"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", "42A"}; + run(); + EXPECT_EQ(value, 4200); +} + +TEST_F(TApp, NumberWithUnitCaseInsensitive) { + std::map mapping{{"a", 10}, {"B", 100}}; + + int value = 0; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping, CLI::AsNumberWithUnit::CASE_INSENSITIVE)); + + args = {"-n", "42a"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", "42A"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", "42b"}; + run(); + EXPECT_EQ(value, 4200); + + args = {"-n", "42B"}; + run(); + EXPECT_EQ(value, 4200); +} + +TEST_F(TApp, NumberWithUnitMandatoryUnit) { + std::map mapping{{"a", 10}, {"A", 100}}; + + int value; + app.add_option("-n", value) + ->transform(CLI::AsNumberWithUnit(mapping, + CLI::AsNumberWithUnit::Options(CLI::AsNumberWithUnit::UNIT_REQUIRED | + CLI::AsNumberWithUnit::CASE_SENSITIVE))); + + args = {"-n", "42a"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", "42A"}; + run(); + EXPECT_EQ(value, 4200); + + args = {"-n", "42"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NumberWithUnitMandatoryUnit2) { + std::map mapping{{"a", 10}, {"B", 100}}; + + int value; + app.add_option("-n", value) + ->transform(CLI::AsNumberWithUnit(mapping, + CLI::AsNumberWithUnit::Options(CLI::AsNumberWithUnit::UNIT_REQUIRED | + CLI::AsNumberWithUnit::CASE_INSENSITIVE))); + + args = {"-n", "42A"}; + run(); + EXPECT_EQ(value, 420); + + args = {"-n", "42b"}; + run(); + EXPECT_EQ(value, 4200); + + args = {"-n", "42"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NumberWithUnitBadMapping) { + EXPECT_THROW(CLI::AsNumberWithUnit(std::map{{"a", 10}, {"A", 100}}, + CLI::AsNumberWithUnit::CASE_INSENSITIVE), + CLI::ValidationError); + EXPECT_THROW(CLI::AsNumberWithUnit(std::map{{"a", 10}, {"9", 100}}), CLI::ValidationError); + EXPECT_THROW(CLI::AsNumberWithUnit(std::map{{"a", 10}, {"AA A", 100}}), CLI::ValidationError); + EXPECT_THROW(CLI::AsNumberWithUnit(std::map{{"a", 10}, {"", 100}}), CLI::ValidationError); +} + +TEST_F(TApp, NumberWithUnitBadInput) { + std::map mapping{{"a", 10}, {"b", 100}}; + + int value; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping)); + + args = {"-n", "13 a b"}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", "13 c"}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", "a"}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", "12.0a"}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", "a5"}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", ""}; + EXPECT_THROW(run(), CLI::ValidationError); + args = {"-n", "13 a-"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NumberWithUnitIntOverflow) { + std::map mapping{{"a", 1000000}, {"b", 100}, {"c", 101}}; + + int32_t value; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping)); + + args = {"-n", "1000 a"}; + run(); + EXPECT_EQ(value, 1000000000); + + args = {"-n", "1000000 a"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-n", "-1000000 a"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-n", "21474836 b"}; + run(); + EXPECT_EQ(value, 2147483600); + + args = {"-n", "21474836 c"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NumberWithUnitFloatOverflow) { + std::map mapping{{"a", 2}, {"b", 1}, {"c", 0}}; + + float value; + app.add_option("-n", value)->transform(CLI::AsNumberWithUnit(mapping)); + + args = {"-n", "3e+38 a"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-n", "3e+38 b"}; + run(); + EXPECT_FLOAT_EQ(value, 3e+38); + + args = {"-n", "3e+38 c"}; + run(); + EXPECT_FLOAT_EQ(value, 0); +} + +TEST_F(TApp, AsSizeValue1000_1024) { + uint64_t value; + app.add_option("-s", value)->transform(CLI::AsSizeValue(true)); + + args = {"-s", "10240"}; + run(); + EXPECT_EQ(value, 10240u); + + args = {"-s", "1b"}; + run(); + EXPECT_FLOAT_EQ(value, 1); + + uint64_t k_value = 1000u; + uint64_t ki_value = 1024u; + args = {"-s", "1k"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1kb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1 Kb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1ki"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1kib"}; + run(); + EXPECT_EQ(value, ki_value); + + k_value = 1000ull * 1000u; + ki_value = 1024ull * 1024u; + args = {"-s", "1m"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1mb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1mi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1mib"}; + run(); + EXPECT_EQ(value, ki_value); + + k_value = 1000ull * 1000u * 1000u; + ki_value = 1024ull * 1024u * 1024u; + args = {"-s", "1g"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1gb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1gi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1gib"}; + run(); + EXPECT_EQ(value, ki_value); + + k_value = 1000ull * 1000u * 1000u * 1000u; + ki_value = 1024ull * 1024u * 1024u * 1024u; + args = {"-s", "1t"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1tb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1ti"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1tib"}; + run(); + EXPECT_EQ(value, ki_value); + + k_value = 1000ull * 1000u * 1000u * 1000u * 1000u; + ki_value = 1024ull * 1024u * 1024u * 1024u * 1024u; + args = {"-s", "1p"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1pb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1pi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1pib"}; + run(); + EXPECT_EQ(value, ki_value); + + k_value = 1000ull * 1000u * 1000u * 1000u * 1000u * 1000u; + ki_value = 1024ull * 1024u * 1024u * 1024u * 1024u * 1024u; + args = {"-s", "1e"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1eb"}; + run(); + EXPECT_EQ(value, k_value); + args = {"-s", "1ei"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1eib"}; + run(); + EXPECT_EQ(value, ki_value); +} + +TEST_F(TApp, AsSizeValue1024) { + uint64_t value; + app.add_option("-s", value)->transform(CLI::AsSizeValue(false)); + + args = {"-s", "10240"}; + run(); + EXPECT_EQ(value, 10240u); + + args = {"-s", "1b"}; + run(); + EXPECT_FLOAT_EQ(value, 1); + + uint64_t ki_value = 1024u; + args = {"-s", "1k"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1kb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1 Kb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1ki"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1kib"}; + run(); + EXPECT_EQ(value, ki_value); + + ki_value = 1024ull * 1024u; + args = {"-s", "1m"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1mb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1mi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1mib"}; + run(); + EXPECT_EQ(value, ki_value); + + ki_value = 1024ull * 1024u * 1024u; + args = {"-s", "1g"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1gb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1gi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1gib"}; + run(); + EXPECT_EQ(value, ki_value); + + ki_value = 1024ull * 1024u * 1024u * 1024u; + args = {"-s", "1t"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1tb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1ti"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1tib"}; + run(); + EXPECT_EQ(value, ki_value); + + ki_value = 1024ull * 1024u * 1024u * 1024u * 1024u; + args = {"-s", "1p"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1pb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1pi"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1pib"}; + run(); + EXPECT_EQ(value, ki_value); + + ki_value = 1024ull * 1024u * 1024u * 1024u * 1024u * 1024u; + args = {"-s", "1e"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1eb"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1ei"}; + run(); + EXPECT_EQ(value, ki_value); + args = {"-s", "1eib"}; + run(); + EXPECT_EQ(value, ki_value); +}