Skip to main content

reifydb_type/value/uint/
parse.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025 ReifyDB
3
4use std::borrow::Cow;
5
6use num_bigint::{BigInt, Sign};
7
8use crate::{
9	error::{Error, TypeError},
10	fragment::Fragment,
11	value::{r#type::Type, uint::Uint},
12};
13
14pub fn parse_uint(fragment: Fragment) -> Result<Uint, Error> {
15	// Fragment is already owned, no conversion needed
16	let raw_value = fragment.text();
17
18	// Fast path: check if we need any string processing
19	let needs_trimming = raw_value.as_bytes().first().map_or(false, |&b| b.is_ascii_whitespace())
20		|| raw_value.as_bytes().last().map_or(false, |&b| b.is_ascii_whitespace());
21
22	let has_underscores = raw_value.as_bytes().contains(&b'_');
23
24	let value = match (needs_trimming, has_underscores) {
25		(false, false) => Cow::Borrowed(raw_value), // Fast path -
26		// no processing
27		// needed
28		(true, false) => Cow::Borrowed(raw_value.trim()),
29		(false, true) => Cow::Owned(raw_value.replace('_', "")),
30		(true, true) => Cow::Owned(raw_value.trim().replace('_', "")),
31	};
32
33	if value.is_empty() {
34		return Err(TypeError::InvalidNumberFormat {
35			target: Type::Uint,
36			fragment,
37		}
38		.into());
39	}
40
41	// Check for negative sign early, but allow -0.0 case to be handled by
42	// float parsing
43	if value.starts_with('-') && value != "-0.0" && value != "-0" {
44		// Quick check for other obvious negative values
45		if let Ok(bigint) = value.parse::<BigInt>() {
46			if bigint.sign() == Sign::Minus {
47				return Err(TypeError::NumberOutOfRange {
48					target: Type::Uint,
49					fragment,
50					descriptor: None,
51				}
52				.into());
53			}
54		}
55		// For non-BigInt parseable values, let float parsing handle it
56	}
57
58	// Try parsing as BigInt first
59	match value.parse::<BigInt>() {
60		Ok(v) => {
61			// Double check that the BigInt is non-negative (should
62			// be guaranteed by the prefix check)
63			if v.sign() == Sign::Minus {
64				return Err(TypeError::NumberOutOfRange {
65					target: Type::Uint,
66					fragment,
67					descriptor: None,
68				}
69				.into());
70			}
71			Ok(Uint::from(v))
72		}
73		Err(_) => {
74			// If BigInt parsing fails, try parsing as f64 for
75			// scientific notation and truncation
76			if let Ok(f) = value.parse::<f64>() {
77				if f.is_infinite() {
78					Err(TypeError::NumberOutOfRange {
79						target: Type::Uint,
80						fragment,
81						descriptor: None,
82					}
83					.into())
84				} else {
85					let truncated = f.trunc();
86					// Handle negative zero and other
87					// negative values
88					if truncated < 0.0 && truncated != -0.0 {
89						return Err(TypeError::NumberOutOfRange {
90							target: Type::Uint,
91							fragment,
92							descriptor: None,
93						}
94						.into());
95					}
96					// Convert the truncated float to
97					// BigInt, treating -0.0 as 0.0
98					let abs_truncated = if truncated == -0.0 {
99						0.0
100					} else {
101						truncated
102					};
103					if let Ok(bigint) = format!("{:.0}", abs_truncated).parse::<BigInt>() {
104						Ok(Uint::from(bigint))
105					} else {
106						Err(TypeError::InvalidNumberFormat {
107							target: Type::Uint,
108							fragment,
109						}
110						.into())
111					}
112				}
113			} else {
114				// Check if it contains a minus sign to provide
115				// better error message
116				if value.contains('-') {
117					Err(TypeError::NumberOutOfRange {
118						target: Type::Uint,
119						fragment,
120						descriptor: None,
121					}
122					.into())
123				} else {
124					Err(TypeError::InvalidNumberFormat {
125						target: Type::Uint,
126						fragment,
127					}
128					.into())
129				}
130			}
131		}
132	}
133}
134
135#[cfg(test)]
136pub mod tests {
137	use super::*;
138
139	#[test]
140	fn test_parse_uint_valid_zero() {
141		assert_eq!(parse_uint(Fragment::testing("0")).unwrap(), Uint::zero());
142	}
143
144	#[test]
145	fn test_parse_uint_valid_positive() {
146		let result = parse_uint(Fragment::testing("12345")).unwrap();
147		assert_eq!(format!("{}", result), "12345");
148	}
149
150	#[test]
151	fn test_parse_uint_large_positive() {
152		let large_num = "123456789012345678901234567890";
153		let result = parse_uint(Fragment::testing(large_num)).unwrap();
154		assert_eq!(format!("{}", result), large_num);
155	}
156
157	#[test]
158	fn test_parse_uint_scientific_notation() {
159		let result = parse_uint(Fragment::testing("1e5")).unwrap();
160		assert_eq!(format!("{}", result), "100000");
161	}
162
163	#[test]
164	fn test_parse_uint_scientific_decimal() {
165		let result = parse_uint(Fragment::testing("2.5e3")).unwrap();
166		assert_eq!(format!("{}", result), "2500");
167	}
168
169	#[test]
170	fn test_parse_uint_float_truncation() {
171		let result = parse_uint(Fragment::testing("123.789")).unwrap();
172		assert_eq!(format!("{}", result), "123");
173	}
174
175	#[test]
176	fn test_parse_uint_float_truncation_zero() {
177		let result = parse_uint(Fragment::testing("0.999")).unwrap();
178		assert_eq!(format!("{}", result), "0");
179	}
180
181	#[test]
182	fn test_parse_uint_with_underscores() {
183		let result = parse_uint(Fragment::testing("1_234_567")).unwrap();
184		assert_eq!(format!("{}", result), "1234567");
185	}
186
187	#[test]
188	fn test_parse_uint_with_leading_space() {
189		let result = parse_uint(Fragment::testing(" 12345")).unwrap();
190		assert_eq!(format!("{}", result), "12345");
191	}
192
193	#[test]
194	fn test_parse_uint_with_trailing_space() {
195		let result = parse_uint(Fragment::testing("12345 ")).unwrap();
196		assert_eq!(format!("{}", result), "12345");
197	}
198
199	#[test]
200	fn test_parse_uint_with_both_spaces() {
201		let result = parse_uint(Fragment::testing(" 12345 ")).unwrap();
202		assert_eq!(format!("{}", result), "12345");
203	}
204
205	#[test]
206	fn test_parse_uint_negative_integer() {
207		assert!(parse_uint(Fragment::testing("-12345")).is_err());
208	}
209
210	#[test]
211	fn test_parse_uint_negative_float() {
212		assert!(parse_uint(Fragment::testing("-123.45")).is_err());
213	}
214
215	#[test]
216	fn test_parse_uint_negative_scientific() {
217		assert!(parse_uint(Fragment::testing("-1e5")).is_err());
218	}
219
220	#[test]
221	fn test_parse_uint_negative_zero_float() {
222		// This should be handled gracefully - negative zero should
223		// become positive zero
224		let result = parse_uint(Fragment::testing("-0.0")).unwrap();
225		assert_eq!(format!("{}", result), "0");
226	}
227
228	#[test]
229	fn test_parse_uint_invalid_empty() {
230		assert!(parse_uint(Fragment::testing("")).is_err());
231	}
232
233	#[test]
234	fn test_parse_uint_invalid_whitespace() {
235		assert!(parse_uint(Fragment::testing("   ")).is_err());
236	}
237
238	#[test]
239	fn test_parse_uint_invalid_text() {
240		assert!(parse_uint(Fragment::testing("abc")).is_err());
241	}
242
243	#[test]
244	fn test_parse_uint_invalid_multiple_dots() {
245		assert!(parse_uint(Fragment::testing("1.2.3")).is_err());
246	}
247
248	#[test]
249	fn test_parse_uint_infinity() {
250		assert!(parse_uint(Fragment::testing("inf")).is_err());
251	}
252
253	#[test]
254	fn test_parse_uint_negative_infinity() {
255		assert!(parse_uint(Fragment::testing("-inf")).is_err());
256	}
257}