clap_num/lib.rs
1//! clap number parsers.
2//!
3//! This crate contains functions to validate and parse numerical values from
4//! strings provided by [clap].
5//!
6//! * `maybe_hex`
7//! Validates an unsigned integer value that can be base-10 or base-16.
8//! * `maybe_hex_range`
9//! Validates an unsigned integer value that can be base-10 or base-16 within a range.
10//! * `number_range`
11//! Validate a signed or unsigned integer value.
12//! * `si_number`
13//! Validate a signed or unsigned integer value with a metric prefix.
14//! * `si_number_range`
15//! Validate a signed or unsigned integer value with a metric prefix within a range.
16//!
17//! [clap]: https://github.com/clap-rs/clap
18#![deny(missing_docs)]
19
20use core::{iter, str::FromStr};
21use num_traits::identities::Zero;
22use num_traits::{sign, CheckedAdd, CheckedMul, CheckedSub, Num};
23
24fn check_range<T>(val: T, min: T, max: T) -> Result<T, String>
25where
26 T: FromStr,
27 <T as FromStr>::Err: std::fmt::Display,
28 T: Ord,
29 T: std::fmt::Display,
30{
31 if val > max {
32 Err(format!("exceeds maximum of {max}"))
33 } else if val < min {
34 Err(format!("less than minimum of {min}"))
35 } else {
36 Ok(val)
37 }
38}
39
40/// Validate a signed or unsigned integer value.
41///
42/// # Arguments
43///
44/// * `s` - String to parse.
45/// * `min` - Minimum value, inclusive.
46/// * `max` - Maximum value, inclusive.
47///
48/// # Example
49///
50/// This allows for a number of cents to be passed in the range of 0-99
51/// (inclusive).
52///
53/// ```
54/// use clap::Parser;
55/// use clap_num::number_range;
56///
57/// fn less_than_100(s: &str) -> Result<u8, String> {
58/// number_range(s, 0, 99)
59/// }
60///
61/// #[derive(Parser)]
62/// struct Change {
63/// #[clap(long, value_parser=less_than_100)]
64/// cents: u8,
65/// }
66/// # let args = Change::parse_from(&["", "--cents", "99"]);
67/// # assert_eq!(args.cents, 99);
68/// ```
69///
70/// To run this example run `cargo run --example change`, giving arguments to
71/// the program after `--`, for example:
72///
73/// ```text
74/// $ cargo run --example change -- --cents 99
75/// Change: 99 cents
76/// ```
77///
78/// ## Error Messages
79///
80/// Values that are not numbers will show an error message similar to this:
81///
82/// ```text
83/// error: Invalid value for '--cents <cents>': invalid digit found in string
84/// ```
85///
86/// Values resulting in integer overflow will show an error message similar to this:
87///
88/// ```text
89/// error: Invalid value for '--cents <cents>': number too large to fit in target type
90/// ```
91///
92/// Values exceeding the limits will show an error message similar to this:
93///
94/// ```text
95/// error: Invalid value for '--cents <cents>': exceeds maximum of 99
96/// ```
97pub fn number_range<T>(s: &str, min: T, max: T) -> Result<T, String>
98where
99 T: FromStr,
100 <T as FromStr>::Err: std::fmt::Display,
101 T: Ord,
102 T: PartialOrd,
103 T: std::fmt::Display,
104{
105 debug_assert!(min <= max, "minimum of {min} exceeds maximum of {max}");
106 let val = s.parse::<T>().map_err(stringify)?;
107 check_range(val, min, max)
108}
109
110static OVERFLOW_MSG: &str = "number too large to fit in target type";
111
112// helper for mapping errors to strings
113fn stringify<T: std::fmt::Display>(e: T) -> String {
114 format!("{e}")
115}
116
117#[derive(Copy, Clone)]
118enum SiPrefix {
119 Yotta,
120 Zetta,
121 Exa,
122 Peta,
123 Tera,
124 Giga,
125 Mega,
126 Kilo,
127}
128
129impl SiPrefix {
130 fn from_char(symbol: char) -> Option<Self> {
131 match symbol {
132 'Y' => Some(Self::Yotta),
133 'Z' => Some(Self::Zetta),
134 'E' => Some(Self::Exa),
135 'P' => Some(Self::Peta),
136 'T' => Some(Self::Tera),
137 'G' => Some(Self::Giga),
138 'M' => Some(Self::Mega),
139 'k' | 'K' => Some(Self::Kilo),
140 _ => None,
141 }
142 }
143
144 fn multiplier(&self) -> u128 {
145 match self {
146 SiPrefix::Yotta => 1_000_000_000_000_000_000_000_000,
147 SiPrefix::Zetta => 1_000_000_000_000_000_000_000,
148 SiPrefix::Exa => 1_000_000_000_000_000_000,
149 SiPrefix::Peta => 1_000_000_000_000_000,
150 SiPrefix::Tera => 1_000_000_000_000,
151 SiPrefix::Giga => 1_000_000_000,
152 SiPrefix::Mega => 1_000_000,
153 SiPrefix::Kilo => 1_000,
154 }
155 }
156
157 fn digits(&self) -> usize {
158 match self {
159 SiPrefix::Yotta => 24,
160 SiPrefix::Zetta => 21,
161 SiPrefix::Exa => 18,
162 SiPrefix::Peta => 15,
163 SiPrefix::Tera => 12,
164 SiPrefix::Giga => 9,
165 SiPrefix::Mega => 6,
166 SiPrefix::Kilo => 3,
167 }
168 }
169}
170
171fn parse_post<T>(mut post: String, digits: usize) -> Result<T, String>
172where
173 <T as FromStr>::Err: std::fmt::Display,
174 T: PartialOrd + FromStr,
175{
176 if let Some(zeros) = digits.checked_sub(post.len()) {
177 post.extend(iter::repeat('0').take(zeros));
178 post.parse::<T>().map_err(stringify)
179 } else {
180 Err(String::from("not an integer"))
181 }
182}
183
184/// Validate a signed or unsigned integer value with a [metric prefix].
185///
186/// This can accept strings with the (case sensitive) SI symbols.
187///
188/// | Symbol | Name | Value |
189/// |--------|-------|-----------------------------------|
190/// | Y | yotta | 1_000_000_000_000_000_000_000_000 |
191/// | Z | zetta | 1_000_000_000_000_000_000_000 |
192/// | E | exa | 1_000_000_000_000_000_000 |
193/// | P | peta | 1_000_000_000_000_000 |
194/// | T | tera | 1_000_000_000_000 |
195/// | G | giga | 1_000_000_000 |
196/// | M | mega | 1_000_000 |
197/// | k | kilo | 1_000 |
198///
199/// The strings can be provided with a decimal, or using the SI symbol as the
200/// decimal separator.
201///
202/// | String | Value |
203/// |--------|-----------|
204/// | 3k3 | 3300 |
205/// | 3.3k | 3300 |
206/// | 1M | 1_000_000 |
207///
208/// # Example
209///
210/// This allows for resistance value to be passed using SI symbols.
211///
212/// ```
213/// use clap::Parser;
214/// use clap_num::si_number;
215///
216/// #[derive(Parser)]
217/// struct Args {
218/// #[clap(short, long, value_parser=si_number::<u128>)]
219/// resistance: u128,
220/// }
221/// # let args = Args::parse_from(&["", "--resistance", "1M1"]);
222/// # assert_eq!(args.resistance, 1_100_000);
223/// ```
224///
225/// To run this example use `cargo run --example resistance`, giving arguments
226/// to the program after `--`, for example:
227///
228/// ```text
229/// $ cargo run --example resistance -- --resistance 1M1
230/// Resistance: 1100000 ohms
231/// ```
232///
233/// [metric prefix]: https://en.wikipedia.org/wiki/Metric_prefix
234pub fn si_number<T>(s: &str) -> Result<T, String>
235where
236 <T as TryFrom<u128>>::Error: std::fmt::Display,
237 <T as FromStr>::Err: std::fmt::Display,
238 T: CheckedAdd,
239 T: CheckedMul,
240 T: CheckedSub,
241 T: FromStr,
242 T: PartialOrd,
243 T: TryFrom<u128>,
244 T: Zero,
245{
246 // contains SI symbol
247 if let Some(si_prefix_index) = s.find(|c| SiPrefix::from_char(c).is_some()) {
248 let si_prefix = SiPrefix::from_char(s.as_bytes()[si_prefix_index] as char).unwrap();
249 let multiplier: T = T::try_from(si_prefix.multiplier()).map_err(|_| OVERFLOW_MSG)?;
250
251 let (pre_si, post_si) = s.split_at(si_prefix_index);
252 let post_si = &post_si[1..];
253
254 if pre_si.is_empty() {
255 return Err("no value found before SI symbol".to_string());
256 }
257
258 // in the format of "1k234" for 1_234
259 let (pre, post) = if !post_si.is_empty() {
260 (
261 pre_si.parse::<T>().map_err(stringify)?,
262 parse_post(post_si.to_string(), si_prefix.digits())?,
263 )
264
265 // in the format of "1.234k" for 1_234
266 } else if let Some((pre_dec, post_dec)) = s.split_once('.') {
267 let mut post_dec: String = post_dec.to_string();
268 post_dec.pop(); // remove SI symbol
269 let post_dec = parse_post(post_dec, si_prefix.digits())?;
270 (pre_dec.parse::<T>().map_err(stringify)?, post_dec)
271
272 // no decimal
273 } else {
274 (pre_si.parse::<T>().map_err(stringify)?, T::zero())
275 };
276
277 let pre = pre.checked_mul(&multiplier).ok_or(OVERFLOW_MSG)?;
278
279 if pre >= T::zero() {
280 pre.checked_add(&post)
281 } else {
282 pre.checked_sub(&post)
283 }
284 .ok_or_else(|| OVERFLOW_MSG.to_string())
285 } else {
286 // no SI symbol, parse normally
287 s.chars()
288 .filter(|&c| c != '_')
289 .collect::<String>()
290 .parse::<T>()
291 .map_err(stringify)
292 }
293}
294
295/// Validate a signed or unsigned integer value with a [metric prefix] within
296/// a range.
297///
298/// This combines [`si_number`] and [`number_range`], see the
299/// documentation for those functions for details.
300///
301/// # Example
302///
303/// This extends the example in [`si_number`], and only allows a range of
304/// resistances from 1k to 999.999k.
305///
306/// ```
307/// use clap::Parser;
308/// use clap_num::si_number_range;
309///
310/// fn kilo(s: &str) -> Result<u32, String> {
311/// si_number_range(s, 1_000, 999_999)
312/// }
313///
314/// #[derive(Parser)]
315/// struct Args {
316/// #[clap(short, long, value_parser=kilo)]
317/// resistance: u32,
318/// }
319/// # let args = Args::parse_from(&["", "--resistance", "999k999"]);
320/// # assert_eq!(args.resistance, 999_999);
321/// ```
322///
323/// [metric prefix]: https://en.wikipedia.org/wiki/Metric_prefix
324pub fn si_number_range<T>(s: &str, min: T, max: T) -> Result<T, String>
325where
326 <T as TryFrom<u128>>::Error: std::fmt::Display,
327 <T as FromStr>::Err: std::fmt::Display,
328 T: CheckedAdd,
329 T: CheckedMul,
330 T: CheckedSub,
331 T: FromStr,
332 T: PartialOrd,
333 T: TryFrom<u128>,
334 T: Zero,
335 T: Ord,
336 T: PartialOrd,
337 T: std::fmt::Display,
338{
339 let val = si_number(s)?;
340 check_range(val, min, max)
341}
342
343/// Validates an unsigned integer value that can be base-10 or base-16.
344///
345/// The number is assumed to be base-10 by default, it is parsed as hex if the
346/// number is prefixed with `0x`, case insensitive.
347///
348/// # Example
349///
350/// This allows base-10 addresses to be passed normally, or base-16 values to
351/// be passed when prefixed with `0x`.
352///
353/// ```
354/// use clap::Parser;
355/// use clap_num::maybe_hex;
356///
357/// #[derive(Parser)]
358/// struct Args {
359/// #[clap(short, long, value_parser=maybe_hex::<u32>)]
360/// address: u32,
361/// }
362/// # let args = Args::parse_from(&["", "-a", "0x10"]);
363/// # assert_eq!(args.address, 16);
364/// ```
365pub fn maybe_hex<T: Num + sign::Unsigned>(s: &str) -> Result<T, String>
366where
367 <T as Num>::FromStrRadixErr: std::fmt::Display,
368{
369 const HEX_PREFIX: &str = "0x";
370 const HEX_PREFIX_UPPER: &str = "0X";
371 const HEX_PREFIX_LEN: usize = HEX_PREFIX.len();
372
373 let result = if s.starts_with(HEX_PREFIX) || s.starts_with(HEX_PREFIX_UPPER) {
374 T::from_str_radix(&s[HEX_PREFIX_LEN..], 16)
375 } else {
376 T::from_str_radix(s, 10)
377 };
378
379 result.map_err(stringify)
380}
381
382/// Validates an unsigned integer value that can be base-10 or base-16 within
383/// a range.
384///
385/// This combines [`maybe_hex`] and [`number_range`], see the
386/// documentation for those functions for details.
387///
388/// # Example
389///
390/// This extends the example in [`maybe_hex`], and only allows a range of
391/// addresses from `0x100` to `0x200`.
392///
393/// ```
394/// use clap::Parser;
395/// use clap_num::maybe_hex_range;
396///
397/// fn address_in_range(s: &str) -> Result<u32, String> {
398/// maybe_hex_range(s, 0x100, 0x200)
399/// }
400///
401/// #[derive(Parser)]
402/// struct Args {
403/// #[clap(short, long, value_parser=address_in_range)]
404/// address: u32,
405/// }
406/// # let args = Args::parse_from(&["", "-a", "300"]);
407/// # assert_eq!(args.address, 300);
408/// ```
409pub fn maybe_hex_range<T>(s: &str, min: T, max: T) -> Result<T, String>
410where
411 <T as Num>::FromStrRadixErr: std::fmt::Display,
412 <T as FromStr>::Err: std::fmt::Display,
413 T: FromStr,
414 T: std::fmt::Display,
415 T: Ord,
416 T: Num,
417 T: sign::Unsigned,
418{
419 let val = maybe_hex(s)?;
420 check_range(val, min, max)
421}
422
423/// Validates an unsigned integer value that can be base-10 or base-2.
424///
425/// The number is assumed to be base-10 by default, it is parsed as binary if the
426/// number is prefixed with `0b` (case sensitive!).
427///
428/// # Example
429///
430/// This allows base-10 addresses to be passed normally, or base-2 values to
431/// be passed when prefixed with `0b`.
432///
433/// ```
434/// use clap::Parser;
435/// use clap_num::maybe_bin;
436///
437/// #[derive(Parser)]
438/// struct Args {
439/// #[clap(short, long, value_parser=maybe_bin::<u8>)]
440/// address: u8,
441/// }
442/// # let args = Args::parse_from(&["", "-a", "0b1001"]);
443/// # assert_eq!(args.address, 9);
444/// ```
445pub fn maybe_bin<T: Num + sign::Unsigned>(s: &str) -> Result<T, String>
446where
447 <T as Num>::FromStrRadixErr: std::fmt::Display,
448{
449 const BIN_PREFIX: &str = "0b";
450 const BIN_PREFIX_LEN: usize = BIN_PREFIX.len();
451
452 let result = if s.starts_with(BIN_PREFIX) {
453 T::from_str_radix(&s[BIN_PREFIX_LEN..], 2)
454 } else {
455 T::from_str_radix(s, 10)
456 };
457
458 result.map_err(stringify)
459}
460
461/// Validates an unsigned integer value that can be base-10 or base-2 within
462/// a range.
463///
464/// This combines [`maybe_bin`] and [`number_range`], see the
465/// documentation of those functions for details.
466///
467/// # Example
468///
469/// This extends the example in [`maybe_bin`], and only allows a range of
470/// addresses from 2 (`0b10`) to 15 (`0b1111`).
471///
472/// ```
473/// use clap::Parser;
474/// use clap_num::maybe_bin_range;
475///
476/// fn address_in_range(s: &str) -> Result<u8, String> {
477/// maybe_bin_range(s, 0b10, 0b1111)
478/// }
479///
480/// #[derive(Parser)]
481/// struct Args {
482/// #[clap(short, long, value_parser=address_in_range)]
483/// address: u8,
484/// }
485/// # let args = Args::parse_from(&["", "-a", "0b1001"]);
486/// # assert_eq!(args.address, 9);
487/// ```
488pub fn maybe_bin_range<T>(s: &str, min: T, max: T) -> Result<T, String>
489where
490 <T as Num>::FromStrRadixErr: std::fmt::Display,
491 <T as FromStr>::Err: std::fmt::Display,
492 T: FromStr,
493 T: std::fmt::Display,
494 T: Ord,
495 T: Num,
496 T: sign::Unsigned,
497{
498 let val = maybe_bin(s)?;
499 check_range(val, min, max)
500}