Skip to content

Commit 59a3656

Browse files
metopahenryiii
authored andcommitted
Support for size values (640 KB) and numbers with unit in general (#253)
* [WIP] Initial implementation * Add mapping validation * More documentation * Add support for floats in checked_multiply and add tests * Place SuffixedNumber declaration correctly * Add tests * Refactor SuffixedNumber * Add as size value * Update README * SFINAE for checked_multiply() * Mark ctors as explicit * Small fixes * Clang format * Clang format * Adding GCC 4.7 support * Rename SuffixedNumber to AsNumberWithUnit
1 parent b6e3fb1 commit 59a3656

File tree

5 files changed

+837
-6
lines changed

5 files changed

+837
-6
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ The `add_option_function<type>(...` function will typically require the template
243243
🚧 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:
244244

245245
```cpp
246-
app.add_flag("--flag,!--no-flag,result,"help for flag"); // 🚧
246+
app.add_flag("--flag,!--no-flag",result,"help for flag"); // 🚧
247247
```
248248

249249
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
341341
- `CLI::IsMember(...)`: 🚧 Require an option be a member of a given set. See [Transforming Validators](#transforming-validators) for more details.
342342
- `CLI::Transformer(...)`: 🚧 Modify the input using a map. See [Transforming Validators](#transforming-validators) for more details.
343343
- `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.
344+
- `CLI::AsNumberWithUnit(...)`: Modify the `<NUMBER> <UNIT>` 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`).
345+
- `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.
344346
- `CLI::ExistingFile`: Requires that the file exists if given.
345347
- `CLI::ExistingDirectory`: Requires that the directory exists.
346348
- `CLI::ExistingPath`: Requires that the path (file or directory) exists.

include/CLI/StringTools.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ inline bool valid_name_string(const std::string &str) {
193193
return true;
194194
}
195195

196+
/// Verify that str consists of letters only
197+
inline bool isalpha(const std::string &str) {
198+
return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); });
199+
}
200+
196201
/// Return a lower case version of a string
197202
inline std::string to_lower(std::string str) {
198203
std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) {

include/CLI/Validators.hpp

Lines changed: 226 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
#include "CLI/StringTools.hpp"
77
#include "CLI/TypeTools.hpp"
88

9+
#include <cmath>
910
#include <functional>
1011
#include <iostream>
12+
#include <map>
1113
#include <memory>
1214
#include <string>
1315

@@ -443,14 +445,17 @@ template <typename T> std::string generate_set(const T &set) {
443445
}
444446

445447
/// Generate a string representation of a map
446-
template <typename T> std::string generate_map(const T &map) {
448+
template <typename T> std::string generate_map(const T &map, bool key_only = false) {
447449
using element_t = typename detail::element_type<T>::type;
448450
using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
449451
std::string out(1, '{');
450452
out.append(detail::join(detail::smart_deref(map),
451-
[](const iteration_type_t &v) {
452-
return detail::as_string(detail::pair_adaptor<element_t>::first(v)) + "->" +
453-
detail::as_string(detail::pair_adaptor<element_t>::second(v));
453+
[key_only](const iteration_type_t &v) {
454+
auto res = detail::as_string(detail::pair_adaptor<element_t>::first(v));
455+
if(!key_only) {
456+
res += "->" + detail::as_string(detail::pair_adaptor<element_t>::second(v));
457+
}
458+
return res;
454459
},
455460
","));
456461
out.push_back('}');
@@ -505,6 +510,31 @@ auto search(const T &set, const V &val, const std::function<V(V)> &filter_functi
505510
return {(it != std::end(setref)), it};
506511
}
507512

513+
/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise.
514+
template <typename T> typename std::enable_if<std::is_integral<T>::value, bool>::type checked_multiply(T &a, T b) {
515+
if(a == 0 || b == 0) {
516+
a *= b;
517+
return true;
518+
}
519+
T c = a * b;
520+
if(c / a != b) {
521+
return false;
522+
}
523+
a = c;
524+
return true;
525+
}
526+
527+
/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise.
528+
template <typename T>
529+
typename std::enable_if<std::is_floating_point<T>::value, bool>::type checked_multiply(T &a, T b) {
530+
T c = a * b;
531+
if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) {
532+
return false;
533+
}
534+
a = c;
535+
return true;
536+
}
537+
508538
} // namespace detail
509539
/// Verify items are in a set
510540
class IsMember : public Validator {
@@ -707,7 +737,7 @@ class CheckedTransformer : public Validator {
707737
: CheckedTransformer(std::forward<T>(mapping),
708738
[filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
709739
other...) {}
710-
}; // namespace CLI
740+
};
711741

712742
/// Helper function to allow ignore_case to be passed to IsMember or Transform
713743
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) {
722752
return item;
723753
}
724754

755+
/// Multiply a number by a factor using given mapping.
756+
/// Can be used to write transforms for SIZE or DURATION inputs.
757+
///
758+
/// Example:
759+
/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}`
760+
/// one can recognize inputs like "100", "12kb", "100 MB",
761+
/// that will be automatically transformed to 100, 14448, 104857600.
762+
///
763+
/// Output number type matches the type in the provided mapping.
764+
/// Therefore, if it is required to interpret real inputs like "0.42 s",
765+
/// the mapping should be of a type <string, float> or <string, double>.
766+
class AsNumberWithUnit : public Validator {
767+
public:
768+
/// Adjust AsNumberWithUnit behavior.
769+
/// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched.
770+
/// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError
771+
/// if UNIT_REQUIRED is set and unit literal is not found.
772+
enum Options {
773+
CASE_SENSITIVE = 0,
774+
CASE_INSENSITIVE = 1,
775+
UNIT_OPTIONAL = 0,
776+
UNIT_REQUIRED = 2,
777+
DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL
778+
};
779+
780+
template <typename Number>
781+
explicit AsNumberWithUnit(std::map<std::string, Number> mapping,
782+
Options opts = DEFAULT,
783+
const std::string &unit_name = "UNIT") {
784+
description(generate_description<Number>(unit_name, opts));
785+
validate_mapping(mapping, opts);
786+
787+
// transform function
788+
func_ = [mapping, opts](std::string &input) -> std::string {
789+
Number num;
790+
791+
detail::rtrim(input);
792+
if(input.empty()) {
793+
throw ValidationError("Input is empty");
794+
}
795+
796+
// Find split position between number and prefix
797+
auto unit_begin = input.end();
798+
while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) {
799+
--unit_begin;
800+
}
801+
802+
std::string unit{unit_begin, input.end()};
803+
input.resize(std::distance(input.begin(), unit_begin));
804+
detail::trim(input);
805+
806+
if(opts & UNIT_REQUIRED && unit.empty()) {
807+
throw ValidationError("Missing mandatory unit");
808+
}
809+
if(opts & CASE_INSENSITIVE) {
810+
unit = detail::to_lower(unit);
811+
}
812+
813+
bool converted = detail::lexical_cast(input, num);
814+
if(!converted) {
815+
throw ValidationError("Value " + input + " could not be converted to " + detail::type_name<Number>());
816+
}
817+
818+
if(unit.empty()) {
819+
// No need to modify input if no unit passed
820+
return {};
821+
}
822+
823+
// find corresponding factor
824+
auto it = mapping.find(unit);
825+
if(it == mapping.end()) {
826+
throw ValidationError(unit +
827+
" unit not recognized. "
828+
"Allowed values: " +
829+
detail::generate_map(mapping, true));
830+
}
831+
832+
// perform safe multiplication
833+
bool ok = detail::checked_multiply(num, it->second);
834+
if(!ok) {
835+
throw ValidationError(detail::as_string(num) + " multiplied by " + unit +
836+
" factor would cause number overflow. Use smaller value.");
837+
}
838+
input = detail::as_string(num);
839+
840+
return {};
841+
};
842+
}
843+
844+
private:
845+
/// Check that mapping contains valid units.
846+
/// Update mapping for CASE_INSENSITIVE mode.
847+
template <typename Number> static void validate_mapping(std::map<std::string, Number> &mapping, Options opts) {
848+
for(auto &kv : mapping) {
849+
if(kv.first.empty()) {
850+
throw ValidationError("Unit must not be empty.");
851+
}
852+
if(!detail::isalpha(kv.first)) {
853+
throw ValidationError("Unit must contain only letters.");
854+
}
855+
}
856+
857+
// make all units lowercase if CASE_INSENSITIVE
858+
if(opts & CASE_INSENSITIVE) {
859+
std::map<std::string, Number> lower_mapping;
860+
for(auto &kv : mapping) {
861+
auto s = detail::to_lower(kv.first);
862+
if(lower_mapping.count(s)) {
863+
throw ValidationError("Several matching lowercase unit representations are found: " + s);
864+
}
865+
lower_mapping[detail::to_lower(kv.first)] = kv.second;
866+
}
867+
mapping = std::move(lower_mapping);
868+
}
869+
}
870+
871+
/// Generate description like this: NUMBER [UNIT]
872+
template <typename Number> static std::string generate_description(const std::string &name, Options opts) {
873+
std::stringstream out;
874+
out << detail::type_name<Number>() << ' ';
875+
if(opts & UNIT_REQUIRED) {
876+
out << name;
877+
} else {
878+
out << '[' << name << ']';
879+
}
880+
return out.str();
881+
}
882+
};
883+
884+
/// Converts a human-readable size string (with unit literal) to uin64_t size.
885+
/// Example:
886+
/// "100" => 100
887+
/// "1 b" => 100
888+
/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024)
889+
/// "10 KB" => 10240
890+
/// "10 kb" => 10240
891+
/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024)
892+
/// "10kb" => 10240
893+
/// "2 MB" => 2097152
894+
/// "2 EiB" => 2^61 // Units up to exibyte are supported
895+
class AsSizeValue : public AsNumberWithUnit {
896+
public:
897+
using result_t = uint64_t;
898+
899+
/// If kb_is_1000 is true,
900+
/// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024
901+
/// (same applies to higher order units as well).
902+
/// Otherwise, interpret all literals as factors of 1024.
903+
/// The first option is formally correct, but
904+
/// the second interpretation is more wide-spread
905+
/// (see https://en.wikipedia.org/wiki/Binary_prefix).
906+
explicit AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) {
907+
if(kb_is_1000) {
908+
description("SIZE [b, kb(=1000b), kib(=1024b), ...]");
909+
} else {
910+
description("SIZE [b, kb(=1024b), ...]");
911+
}
912+
}
913+
914+
private:
915+
/// Get <size unit, factor> mapping
916+
static std::map<std::string, result_t> init_mapping(bool kb_is_1000) {
917+
std::map<std::string, result_t> m;
918+
result_t k_factor = kb_is_1000 ? 1000 : 1024;
919+
result_t ki_factor = 1024;
920+
result_t k = 1;
921+
result_t ki = 1;
922+
m["b"] = 1;
923+
for(std::string p : {"k", "m", "g", "t", "p", "e"}) {
924+
k *= k_factor;
925+
ki *= ki_factor;
926+
m[p] = k;
927+
m[p + "b"] = k;
928+
m[p + "i"] = ki;
929+
m[p + "ib"] = ki;
930+
}
931+
return m;
932+
}
933+
934+
/// Cache calculated mapping
935+
static std::map<std::string, result_t> get_mapping(bool kb_is_1000) {
936+
if(kb_is_1000) {
937+
static auto m = init_mapping(true);
938+
return m;
939+
} else {
940+
static auto m = init_mapping(false);
941+
return m;
942+
}
943+
}
944+
};
945+
725946
namespace detail {
726947
/// Split a string into a program name and command line arguments
727948
/// the string is assumed to contain a file name followed by other arguments

0 commit comments

Comments
 (0)