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}