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