Skip to main content

reifydb_value/value/decimal/
parse.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::borrow::Cow;
5
6use crate::{
7	error::{Error, TypeError},
8	fragment::Fragment,
9	value::{
10		decimal::{Decimal, parse_error},
11		value_type::ValueType,
12	},
13};
14
15pub fn parse_decimal(fragment: Fragment) -> Result<Decimal, Error> {
16	let fragment_owned = fragment.clone();
17	let raw_value = fragment.text();
18
19	let needs_trimming = raw_value.as_bytes().first().is_some_and(|&b| b.is_ascii_whitespace())
20		|| raw_value.as_bytes().last().is_some_and(|&b| b.is_ascii_whitespace());
21	let has_underscores = raw_value.as_bytes().contains(&b'_');
22
23	let value = match (needs_trimming, has_underscores) {
24		(false, false) => Cow::Borrowed(raw_value),
25		(true, false) => Cow::Borrowed(raw_value.trim()),
26		(false, true) => Cow::Owned(raw_value.replace('_', "")),
27		(true, true) => Cow::Owned(raw_value.trim().replace('_', "")),
28	};
29
30	if value.is_empty() {
31		return Err(TypeError::InvalidNumberFormat {
32			target: ValueType::DECIMAL,
33			fragment: fragment_owned,
34		}
35		.into());
36	}
37
38	Decimal::parse(&value).map_err(|error| parse_error(error, fragment_owned))
39}
40
41#[cfg(test)]
42pub mod tests {
43	use super::*;
44
45	#[test]
46	fn test_parse_decimal_integer() {
47		let decimal = parse_decimal(Fragment::testing("123")).unwrap();
48		assert_eq!(decimal.to_string(), "123");
49	}
50
51	#[test]
52	fn test_parse_decimal_with_fractional() {
53		let decimal = parse_decimal(Fragment::testing("123.45")).unwrap();
54		assert_eq!(decimal.to_string(), "123.45");
55	}
56
57	#[test]
58	fn test_parse_decimal_with_underscores() {
59		let decimal = parse_decimal(Fragment::testing("1_234.56")).unwrap();
60		assert_eq!(decimal.to_string(), "1234.56");
61	}
62
63	#[test]
64	fn test_parse_decimal_negative() {
65		let decimal = parse_decimal(Fragment::testing("-123.45")).unwrap();
66		assert_eq!(decimal.to_string(), "-123.45");
67	}
68
69	#[test]
70	fn test_parse_decimal_empty() {
71		assert!(parse_decimal(Fragment::testing("")).is_err());
72	}
73
74	#[test]
75	fn test_parse_decimal_invalid() {
76		assert!(parse_decimal(Fragment::testing("not_a_number")).is_err());
77	}
78
79	#[test]
80	fn test_parse_decimal_scientific_notation() {
81		let decimal = parse_decimal(Fragment::testing("1.23e2")).unwrap();
82		assert_eq!(decimal.to_string(), "123");
83	}
84
85	#[test]
86	fn parse_keeps_the_literal_scale_and_rejects_seventy_seven_digits() {
87		// The literal scale is what the column type derives from, and 77 digits cannot be stored.
88		let decimal = parse_decimal(Fragment::testing(" 1_000.50 ")).unwrap();
89		assert_eq!((decimal.to_string(), decimal.scale()), ("1000.50".to_string(), 2));
90		let error = parse_decimal(Fragment::testing("9".repeat(77))).unwrap_err();
91		assert_eq!(error.code, "NUMBER_002");
92	}
93}