dactyl/traits/
btoi.rs

1/*!
2# Dactyl — Bytes to Signed.
3*/
4
5#![expect(clippy::unreadable_literal, reason = "Macros made me do it.")]
6
7use crate::{
8	int,
9	traits::BytesToUnsigned,
10};
11use std::num::{
12	NonZeroI8,
13	NonZeroI16,
14	NonZeroI32,
15	NonZeroI64,
16	NonZeroI128,
17	NonZeroIsize,
18};
19
20
21
22/// # (ASCII) Bytes to Signed.
23///
24/// This is the signed equivalent of [`BytesToUnsigned`](crate::traits::BytesToUnsigned).
25/// It works exactly the same way and for the same reasons, except the first
26/// byte can optionally be a `+` or `-`.
27///
28/// ## Examples
29///
30/// ```
31/// use dactyl::traits::BytesToSigned;
32///
33/// assert_eq!(
34///     i8::btoi(b"-120"),
35///     Some(-120_i8),
36/// );
37/// ```
38pub trait BytesToSigned: Sized {
39	/// # (ASCII) Bytes to Signed.
40	fn btoi(src: &[u8]) -> Option<Self>;
41}
42
43
44
45/// # Helper: Documentation.
46macro_rules! docs {
47	($ty:ident) => (concat!(
48"# (ASCII) Bytes to Signed.
49
50Parse a `", stringify!($ty), "` from an ASCII byte slice.
51
52## Examples
53
54```
55use dactyl::traits::BytesToSigned;
56
57assert_eq!(
58    ", stringify!($ty), "::btoi(b\"", int!(@min $ty), "\"),
59    Some(", stringify!($ty), "::MIN),
60);
61assert_eq!(
62    ", stringify!($ty), "::btoi(b\"", int!(@max $ty), "\"),
63    Some(", stringify!($ty), "::MAX),
64);
65
66// Leading zeroes are fine.
67assert_eq!(
68    ", stringify!($ty), "::btoi(b\"00000123\"),
69    Some(123),
70);
71
72// These are all bad.
73assert!(", stringify!($ty), "::btoi(&[]).is_none()); // Empty.
74assert!(", stringify!($ty), "::btoi(b\" 2223231  \").is_none()); // Whitespace.
75assert!(", stringify!($ty), "::btoi(b\"nope\").is_none()); // Not a number.
76assert!(", stringify!($ty), "::btoi(
77    b\"4402823669209384634633746074317682114550\").is_none()
78); // Too big.
79```
80"
81	));
82}
83
84
85
86/// # Helper: Generate Impls.
87macro_rules! signed {
88	($ty:ident $min:literal) => (
89		impl BytesToSigned for $ty {
90			#[expect(clippy::cast_possible_wrap, reason = "False positive.")]
91			#[inline]
92			#[doc = docs!($ty)]
93			fn btoi(src: &[u8]) -> Option<Self> {
94				let (neg, src) = strip_sign(src)?;
95				let val = <int!(@flip $ty)>::btou(src)?;
96
97				// Negative.
98				if neg {
99					if val == $min { Some(<$ty>::MIN) }
100					else if val < $min { Some(0 - val as $ty) }
101					else { None }
102				}
103				// Positive.
104				else if val <= int!(@max $ty) { Some(val as $ty) }
105				else { None }
106			}
107		}
108	);
109}
110
111signed!(i8   128);
112signed!(i16  32768);
113signed!(i32  2147483648);
114signed!(i64  9223372036854775808);
115signed!(i128 170141183460469231731687303715884105728);
116
117#[cfg(target_pointer_width = "16")]
118signed!(isize 32768);
119
120#[cfg(target_pointer_width = "32")]
121signed!(isize 2147483648);
122
123#[cfg(target_pointer_width = "64")]
124signed!(isize 9223372036854775808);
125
126/// # Helper: Non-Zero.
127macro_rules! nz {
128	($($ty:ident)+) => ($(
129		impl BytesToSigned for $ty {
130			#[inline]
131			/// # (ASCII) Bytes to Signed.
132			fn btoi(src: &[u8]) -> Option<Self> {
133				<int!(@alias $ty)>::btoi(src).and_then(Self::new)
134			}
135		}
136	)+);
137}
138
139nz! { NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize }
140
141
142
143/// # Strip Sign.
144///
145/// If the slice starts with a plus or minus, strip it off, returning the
146/// remainder and a bool indicating negativity.
147///
148/// If empty, `None` is returned. Everything else is passed through as-is and
149/// assumed to be positive.
150const fn strip_sign(src: &[u8]) -> Option<(bool, &[u8])> {
151	match src {
152		[] => None,
153		[b'-', rest @ ..] => Some((true, rest)),
154		[b'+', rest @ ..] => Some((false, rest)),
155		_ => Some((false, src)),
156	}
157}
158
159
160
161#[cfg(test)]
162mod tests {
163	use super::*;
164
165	#[cfg(not(miri))]
166	const SAMPLE_SIZE: usize = 1_000_000;
167
168	#[cfg(miri)]
169	const SAMPLE_SIZE: usize = 500; // Miri runs way too slow for a million tests.
170
171	macro_rules! test {
172		(@eq $ty:ident, $raw:expr, $expected:expr $(,)?) => (
173			assert_eq!(
174				<$ty>::btoi($raw),
175				$expected,
176				concat!(stringify!($ty), "::btoi({:?})"),
177				$raw,
178			);
179		);
180
181		($($fn:ident $ty:ident),+ $(,)?) => ($(
182			#[test]
183			fn $fn() {
184				use std::num::NonZero;
185
186				// Common sanity checks.
187				test!(@eq $ty, b"", None);
188				test!(@eq $ty, b" 1", None);
189				test!(@eq $ty, b"1.0", None);
190				test!(@eq $ty, b"apples", None);
191
192				// Plus is fine for signed types.
193				test!(@eq $ty, b"+123", Some(123));
194
195				// Zero is zero no matter how many.
196				test!(@eq $ty, b"0", Some(0));
197				test!(@eq $ty, b"00", Some(0));
198				test!(@eq $ty, b"0000", Some(0));
199				test!(@eq $ty, b"00000000", Some(0));
200				test!(@eq $ty, b"0000000000000000", Some(0));
201				test!(@eq $ty, b"000000000000000000000000000000000000000000000000", Some(0));
202
203				// Maximum should work with or without a zero.
204				test!(@eq $ty, concat!(int!(@max $ty)).as_bytes(), Some(<$ty>::MAX));
205				test!(@eq $ty, concat!("0", int!(@max $ty)).as_bytes(), Some(<$ty>::MAX));
206
207				// But not if bigger.
208				test!(@eq $ty, concat!(int!(@max $ty), "0").as_bytes(), None);
209
210				// i8 can go all the way.
211				if size_of::<$ty>() == 1 {
212					for i in <$ty>::MIN..<$ty>::MAX {
213						let s = i.to_string();
214						test!(@eq $ty, s.as_bytes(), Some(i));
215						assert_eq!(
216							<NonZero<$ty>>::btoi(s.as_bytes()),
217							NonZero::<$ty>::new(i),
218						);
219					}
220					return;
221				}
222
223				// Test a random sample.
224				let mut rng = fastrand::Rng::new();
225				for i in std::iter::repeat_with(|| rng.$ty(..)).take(SAMPLE_SIZE) {
226					let s = i.to_string();
227					test!(@eq $ty, s.as_bytes(), Some(i));
228					assert_eq!(
229						<NonZero<$ty>>::btoi(s.as_bytes()),
230						NonZero::<$ty>::new(i),
231					);
232				}
233			}
234		)+);
235	}
236
237	test!(
238		t_i8  i8,
239		t_i16 i16,
240		t_i32 i32,
241		t_i64 i64,
242		t_i128 i128,
243		t_isize isize,
244	);
245}