btoi/
lib.rs

1// Copyright 2017-2023 Niklas Fiekas <niklas.fiekas@backscattering.de>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Parse integers from ASCII byte slices.
10//!
11//! Provides functions similar to [`from_str_radix`], but is faster when
12//! parsing directly from byte slices instead of strings.
13//!
14//! Supports `#![no_std]`.
15//!
16//! # Examples
17//!
18//! ```
19//! use btoi::btoi;
20//!
21//! assert_eq!(Ok(42), btoi(b"42"));
22//! assert_eq!(Ok(-1000), btoi(b"-1000"));
23//! ```
24//!
25//! All functions are [generic](https://docs.rs/num-traits/) over integer
26//! types. Use the turbofish syntax if a type cannot be inferred from the
27//! context.
28//!
29//! ```
30//! # use btoi::btoi;
31//! // overflows the selected target type
32//! assert!(btoi::<u32>(b"9876543210").is_err());
33//!
34//! // underflows the selected target type (an unsigned integer)
35//! assert!(btoi::<u32>(b"-1").is_err());
36//! ```
37//!
38//! It is possible to use saturating arithmetic for overflow handling.
39//!
40//! ```
41//! use btoi::btoi_saturating;
42//!
43//! assert_eq!(Ok(0xffff_ffff), btoi_saturating::<u32>(b"9876543210"));
44//! assert_eq!(Ok(0), btoi_saturating::<u32>(b"-1"));
45//! ```
46//!
47//! # Errors
48//!
49//! All functions return [`ParseIntegerError`] for these error conditions:
50//!
51//! * The byte slice does not contain any digits.
52//! * Not all characters are `0-9`, `a-z`, `A-Z`. Leading or trailing
53//!   whitespace is not allowed. The `btoi*` functions accept an optional
54//!   leading `+` or `-` sign. The `btou*` functions respectively do not
55//!   allow signs.
56//! * Not all digits are valid in the given radix.
57//! * The number overflows or underflows the target type, but saturating
58//!   arithmetic is not used.
59//!
60//! # Panics
61//!
62//! Just like `from_str_radix` functions will panic if the given radix is
63//! not in the range `2..=36` (or in the pathological case that there is
64//! no representation of the radix in the target integer type).
65//!
66//! [`ParseIntegerError`]: struct.ParseIntegerError.html
67//! [`from_str_radix`]: https://doc.rust-lang.org/std/primitive.u32.html#method.from_str_radix
68
69#![doc(html_root_url = "https://docs.rs/btoi/0.4.3")]
70#![forbid(unsafe_code)]
71#![deny(missing_docs)]
72#![deny(missing_debug_implementations)]
73#![cfg_attr(not(feature = "std"), no_std)]
74
75extern crate num_traits;
76
77#[cfg(test)]
78#[macro_use]
79extern crate quickcheck;
80#[cfg(feature = "std")]
81extern crate std as core;
82
83use core::fmt;
84#[cfg(feature = "std")]
85use std::error::Error;
86
87use num_traits::{Bounded, CheckedAdd, CheckedMul, CheckedSub, FromPrimitive, Saturating, Zero};
88
89/// An error that can occur when parsing an integer.
90///
91/// * No digits
92/// * Invalid digit
93/// * Overflow
94/// * Underflow
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ParseIntegerError {
97    kind: ErrorKind,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101enum ErrorKind {
102    Empty,
103    InvalidDigit,
104    Overflow,
105    Underflow,
106}
107
108impl ParseIntegerError {
109    fn desc(&self) -> &str {
110        match self.kind {
111            ErrorKind::Empty => "cannot parse integer without digits",
112            ErrorKind::InvalidDigit => "invalid digit found in slice",
113            ErrorKind::Overflow => "number too large to fit in target type",
114            ErrorKind::Underflow => "number too small to fit in target type",
115        }
116    }
117}
118
119impl fmt::Display for ParseIntegerError {
120    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121        self.desc().fmt(f)
122    }
123}
124
125#[cfg(feature = "std")]
126impl Error for ParseIntegerError {
127    fn description(&self) -> &str {
128        self.desc()
129    }
130}
131
132/// Converts a byte slice in a given base to an integer. Signs are not allowed.
133///
134/// # Errors
135///
136/// Returns [`ParseIntegerError`] for any of the following conditions:
137///
138/// * `bytes` is empty
139/// * not all characters of `bytes` are `0-9`, `a-z` or `A-Z`
140/// * not all characters refer to digits in the given `radix`
141/// * the number overflows `I`
142///
143/// # Panics
144///
145/// Panics if `radix` is not in the range `2..=36` (or in the pathological
146/// case that there is no representation of `radix` in `I`).
147///
148/// # Examples
149///
150/// ```
151/// # use btoi::btou_radix;
152/// assert_eq!(Ok(255), btou_radix(b"ff", 16));
153/// assert_eq!(Ok(42), btou_radix(b"101010", 2));
154/// ```
155///
156/// [`ParseIntegerError`]: struct.ParseIntegerError.html
157#[track_caller]
158pub fn btou_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
159where
160    I: FromPrimitive + Zero + CheckedAdd + CheckedMul,
161{
162    assert!(
163        (2..=36).contains(&radix),
164        "radix must lie in the range 2..=36, found {}",
165        radix
166    );
167
168    let base = I::from_u32(radix).expect("radix can be represented as integer");
169
170    if bytes.is_empty() {
171        return Err(ParseIntegerError {
172            kind: ErrorKind::Empty,
173        });
174    }
175
176    let mut result = I::zero();
177
178    for &digit in bytes {
179        let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
180            Some(x) => x,
181            None => {
182                return Err(ParseIntegerError {
183                    kind: ErrorKind::InvalidDigit,
184                })
185            }
186        };
187        result = match result.checked_mul(&base) {
188            Some(result) => result,
189            None => {
190                return Err(ParseIntegerError {
191                    kind: ErrorKind::Overflow,
192                })
193            }
194        };
195        result = match result.checked_add(&x) {
196            Some(result) => result,
197            None => {
198                return Err(ParseIntegerError {
199                    kind: ErrorKind::Overflow,
200                })
201            }
202        };
203    }
204
205    Ok(result)
206}
207
208/// Converts a byte slice to an integer. Signs are not allowed.
209///
210/// # Errors
211///
212/// Returns [`ParseIntegerError`] for any of the following conditions:
213///
214/// * `bytes` is empty
215/// * not all characters of `bytes` are `0-9`
216/// * the number overflows `I`
217///
218/// # Panics
219///
220/// Panics in the pathological case that there is no representation of `10`
221/// in `I`.
222///
223/// # Examples
224///
225/// ```
226/// # use btoi::btou;
227/// assert_eq!(Ok(12345), btou(b"12345"));
228/// assert!(btou::<u8>(b"+1").is_err()); // only btoi allows signs
229/// assert!(btou::<u8>(b"256").is_err()); // overflow
230/// ```
231///
232/// [`ParseIntegerError`]: struct.ParseIntegerError.html
233#[track_caller]
234pub fn btou<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
235where
236    I: FromPrimitive + Zero + CheckedAdd + CheckedMul,
237{
238    btou_radix(bytes, 10)
239}
240
241/// Converts a byte slice in a given base to an integer.
242///
243/// Like [`btou_radix`], but numbers may optionally start with a sign
244/// (`-` or `+`).
245///
246/// # Errors
247///
248/// Returns [`ParseIntegerError`] for any of the following conditions:
249///
250/// * `bytes` has no digits
251/// * not all characters of `bytes` are `0-9`, `a-z`, `A-Z`, exluding an
252///   optional leading sign
253/// * not all characters refer to digits in the given `radix`, exluding an
254///   optional leading sign
255/// * the number overflows or underflows `I`
256///
257/// # Panics
258///
259/// Panics if `radix` is not in the range `2..=36` (or in the pathological
260/// case that there is no representation of `radix` in `I`).
261///
262/// # Examples
263///
264/// ```
265/// # use btoi::btoi_radix;
266/// assert_eq!(Ok(10), btoi_radix(b"a", 16));
267/// assert_eq!(Ok(10), btoi_radix(b"+a", 16));
268/// assert_eq!(Ok(-42), btoi_radix(b"-101010", 2));
269/// ```
270///
271/// [`btou_radix`]: fn.btou_radix.html
272/// [`ParseIntegerError`]: struct.ParseIntegerError.html
273#[track_caller]
274pub fn btoi_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
275where
276    I: FromPrimitive + Zero + CheckedAdd + CheckedSub + CheckedMul,
277{
278    assert!(
279        (2..=36).contains(&radix),
280        "radix must lie in the range 2..=36, found {}",
281        radix
282    );
283
284    let base = I::from_u32(radix).expect("radix can be represented as integer");
285
286    if bytes.is_empty() {
287        return Err(ParseIntegerError {
288            kind: ErrorKind::Empty,
289        });
290    }
291
292    let digits = match bytes[0] {
293        b'+' => return btou_radix(&bytes[1..], radix),
294        b'-' => &bytes[1..],
295        _ => return btou_radix(bytes, radix),
296    };
297
298    if digits.is_empty() {
299        return Err(ParseIntegerError {
300            kind: ErrorKind::Empty,
301        });
302    }
303
304    let mut result = I::zero();
305
306    for &digit in digits {
307        let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
308            Some(x) => x,
309            None => {
310                return Err(ParseIntegerError {
311                    kind: ErrorKind::InvalidDigit,
312                })
313            }
314        };
315        result = match result.checked_mul(&base) {
316            Some(result) => result,
317            None => {
318                return Err(ParseIntegerError {
319                    kind: ErrorKind::Underflow,
320                })
321            }
322        };
323        result = match result.checked_sub(&x) {
324            Some(result) => result,
325            None => {
326                return Err(ParseIntegerError {
327                    kind: ErrorKind::Underflow,
328                })
329            }
330        };
331    }
332
333    Ok(result)
334}
335
336/// Converts a byte slice to an integer.
337///
338/// Like [`btou`], but numbers may optionally start with a sign (`-` or `+`).
339///
340/// # Errors
341///
342/// Returns [`ParseIntegerError`] for any of the following conditions:
343///
344/// * `bytes` has no digits
345/// * not all characters of `bytes` are `0-9`, excluding an optional leading
346///   sign
347/// * the number overflows or underflows `I`
348///
349/// # Panics
350///
351/// Panics in the pathological case that there is no representation of `10`
352/// in `I`.
353///
354/// # Examples
355///
356/// ```
357/// # use btoi::btoi;
358/// assert_eq!(Ok(123), btoi(b"123"));
359/// assert_eq!(Ok(123), btoi(b"+123"));
360/// assert_eq!(Ok(-123), btoi(b"-123"));
361///
362/// assert!(btoi::<i16>(b"123456789").is_err()); // overflow
363/// assert!(btoi::<u32>(b"-1").is_err()); // underflow
364///
365/// assert!(btoi::<i32>(b" 42").is_err()); // leading space
366/// ```
367///
368/// [`btou`]: fn.btou.html
369/// [`ParseIntegerError`]: struct.ParseIntegerError.html
370#[track_caller]
371pub fn btoi<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
372where
373    I: FromPrimitive + Zero + CheckedAdd + CheckedSub + CheckedMul,
374{
375    btoi_radix(bytes, 10)
376}
377
378/// Converts a byte slice in a given base to the closest possible integer.
379/// Signs are not allowed.
380///
381/// # Errors
382///
383/// Returns [`ParseIntegerError`] for any of the following conditions:
384///
385/// * `bytes` is empty
386/// * not all characters of `bytes` are `0-9`, `a-z`, `A-Z`
387/// * not all characters refer to digits in the given `radix`
388///
389/// # Panics
390///
391/// Panics if `radix` is not in the range `2..=36` (or in the pathological
392/// case that there is no representation of `radix` in `I`).
393///
394/// # Examples
395///
396/// ```
397/// # use btoi::btou_saturating_radix;
398/// assert_eq!(Ok(255), btou_saturating_radix::<u8>(b"00ff", 16));
399/// assert_eq!(Ok(255), btou_saturating_radix::<u8>(b"0100", 16)); // u8 saturated
400/// assert_eq!(Ok(255), btou_saturating_radix::<u8>(b"0101", 16)); // u8 saturated
401/// ```
402///
403/// [`ParseIntegerError`]: struct.ParseIntegerError.html
404#[track_caller]
405pub fn btou_saturating_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
406where
407    I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
408{
409    assert!(
410        (2..=36).contains(&radix),
411        "radix must lie in the range 2..=36, found {}",
412        radix
413    );
414
415    let base = I::from_u32(radix).expect("radix can be represented as integer");
416
417    if bytes.is_empty() {
418        return Err(ParseIntegerError {
419            kind: ErrorKind::Empty,
420        });
421    }
422
423    let mut result = I::zero();
424
425    for &digit in bytes {
426        let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
427            Some(x) => x,
428            None => {
429                return Err(ParseIntegerError {
430                    kind: ErrorKind::InvalidDigit,
431                })
432            }
433        };
434        result = match result.checked_mul(&base) {
435            Some(result) => result,
436            None => return Ok(I::max_value()),
437        };
438        result = result.saturating_add(x);
439    }
440
441    Ok(result)
442}
443
444/// Converts a byte slice to the closest possible integer.
445/// Signs are not allowed.
446///
447/// # Errors
448///
449/// Returns [`ParseIntegerError`] for any of the following conditions:
450///
451/// * `bytes` is empty
452/// * not all characters of `bytes` are `0-9`
453///
454/// # Panics
455///
456/// Panics in the pathological case that there is no representation of `10`
457/// in `I`.
458///
459/// # Examples
460///
461/// ```
462/// # use btoi::btou_saturating;
463/// assert_eq!(Ok(65535), btou_saturating::<u16>(b"65535"));
464/// assert_eq!(Ok(65535), btou_saturating::<u16>(b"65536")); // u16 saturated
465/// assert_eq!(Ok(65535), btou_saturating::<u16>(b"65537")); // u16 saturated
466/// ```
467///
468/// [`ParseIntegerError`]: struct.ParseIntegerError.html
469#[track_caller]
470pub fn btou_saturating<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
471where
472    I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
473{
474    btou_saturating_radix(bytes, 10)
475}
476
477/// Converts a byte slice in a given base to the closest possible integer.
478///
479/// Like [`btou_saturating_radix`], but numbers may optionally start with a
480/// sign (`-` or `+`).
481///
482/// # Errors
483///
484/// Returns [`ParseIntegerError`] for any of the following conditions:
485///
486/// * `bytes` has no digits
487/// * not all characters of `bytes` are `0-9`, `a-z`, `A-Z`, excluding an
488///   optional leading sign
489/// * not all characters refer to digits in the given `radix`, excluding an
490///   optional leading sign
491///
492/// # Panics
493///
494/// Panics if `radix` is not in the range `2..=36` (or in the pathological
495/// case that there is no representation of `radix` in `I`).
496///
497/// # Examples
498///
499/// ```
500/// # use btoi::btoi_saturating_radix;
501/// assert_eq!(Ok(127), btoi_saturating_radix::<i8>(b"7f", 16));
502/// assert_eq!(Ok(127), btoi_saturating_radix::<i8>(b"ff", 16)); // no overflow
503/// assert_eq!(Ok(-128), btoi_saturating_radix::<i8>(b"-ff", 16)); // no underflow
504/// ```
505///
506/// [`btou_saturating_radix`]: fn.btou_saturating_radix.html
507/// [`ParseIntegerError`]: struct.ParseIntegerError.html
508#[track_caller]
509pub fn btoi_saturating_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
510where
511    I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
512{
513    assert!(
514        (2..=36).contains(&radix),
515        "radix must lie in the range 2..=36, found {}",
516        radix
517    );
518
519    let base = I::from_u32(radix).expect("radix can be represented as integer");
520
521    if bytes.is_empty() {
522        return Err(ParseIntegerError {
523            kind: ErrorKind::Empty,
524        });
525    }
526
527    let digits = match bytes[0] {
528        b'+' => return btou_saturating_radix(&bytes[1..], radix),
529        b'-' => &bytes[1..],
530        _ => return btou_saturating_radix(bytes, radix),
531    };
532
533    if digits.is_empty() {
534        return Err(ParseIntegerError {
535            kind: ErrorKind::Empty,
536        });
537    }
538
539    let mut result = I::zero();
540
541    for &digit in digits {
542        let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
543            Some(x) => x,
544            None => {
545                return Err(ParseIntegerError {
546                    kind: ErrorKind::InvalidDigit,
547                })
548            }
549        };
550        result = match result.checked_mul(&base) {
551            Some(result) => result,
552            None => return Ok(I::min_value()),
553        };
554        result = result.saturating_sub(x);
555    }
556
557    Ok(result)
558}
559
560/// Converts a byte slice to the closest possible integer.
561///
562/// Like [`btou_saturating`], but numbers may optionally start with a sign
563/// (`-` or `+`).
564///
565/// # Errors
566///
567/// Returns [`ParseIntegerError`] for any of the following conditions:
568///
569/// * `bytes` has no digits
570/// * not all characters of `bytes` are `0-9`, excluding an optional leading
571///   sign
572///
573/// # Panics
574///
575/// Panics in the pathological case that there is no representation of `10`
576/// in `I`.
577///
578/// # Examples
579///
580/// ```
581/// # use btoi::btoi_saturating;
582/// assert_eq!(Ok(127), btoi_saturating::<i8>(b"127"));
583/// assert_eq!(Ok(127), btoi_saturating::<i8>(b"128")); // i8 saturated
584/// assert_eq!(Ok(127), btoi_saturating::<i8>(b"+1024")); // i8 saturated
585/// assert_eq!(Ok(-128), btoi_saturating::<i8>(b"-128"));
586/// assert_eq!(Ok(-128), btoi_saturating::<i8>(b"-129")); // i8 saturated
587///
588/// assert_eq!(Ok(0), btoi_saturating::<u32>(b"-123")); // unsigned integer saturated
589/// ```
590///
591/// [`btou_saturating`]: fn.btou_saturating.html
592/// [`ParseIntegerError`]: struct.ParseIntegerError.html
593#[track_caller]
594pub fn btoi_saturating<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
595where
596    I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
597{
598    btoi_saturating_radix(bytes, 10)
599}
600
601#[cfg(test)]
602mod tests {
603    use std::str;
604
605    use super::*;
606
607    quickcheck! {
608        fn btou_identity(n: u32) -> bool {
609            Ok(n) == btou(n.to_string().as_bytes())
610        }
611
612        fn btou_binary_identity(n: u64) -> bool {
613            Ok(n) == btou_radix(format!("{:b}", n).as_bytes(), 2)
614        }
615
616        fn btou_octal_identity(n: u64) -> bool {
617            Ok(n) == btou_radix(format!("{:o}", n).as_bytes(), 8)
618        }
619
620        fn btou_lower_hex_identity(n: u64) -> bool {
621            Ok(n) == btou_radix(format!("{:x}", n).as_bytes(), 16)
622        }
623
624        fn btou_upper_hex_identity(n: u64) -> bool {
625            Ok(n) == btou_radix(format!("{:X}", n).as_bytes(), 16)
626        }
627
628        fn btoi_identity(n: i32) -> bool {
629            Ok(n) == btoi(n.to_string().as_bytes())
630        }
631
632        fn btou_saturating_identity(n: u32) -> bool {
633            Ok(n) == btou_saturating(n.to_string().as_bytes())
634        }
635
636        fn btoi_saturating_identity(n: i32) -> bool {
637            Ok(n) == btoi_saturating(n.to_string().as_bytes())
638        }
639
640        fn btoi_radix_std(bytes: Vec<u8>, radix: u32) -> bool {
641            let radix = radix % 35 + 2; // panic unless 2 <= radix <= 36
642            str::from_utf8(&bytes).ok().and_then(|src| i32::from_str_radix(src, radix).ok()) ==
643                btoi_radix(&bytes, radix).ok()
644        }
645    }
646
647    #[test]
648    fn test_lone_minus() {
649        assert!(btoi::<isize>(b"-").is_err());
650        assert!(btoi_radix::<isize>(b"-", 16).is_err());
651        assert!(btoi_saturating::<isize>(b"-").is_err());
652        assert!(btoi_saturating_radix::<isize>(b"-", 8).is_err());
653
654        assert!(btou::<isize>(b"-").is_err());
655        assert!(btou_radix::<isize>(b"-", 16).is_err());
656        assert!(btou_saturating::<isize>(b"-").is_err());
657        assert!(btou_saturating_radix::<isize>(b"-", 8).is_err());
658    }
659}