Skip to main content

reifydb_value/value/decimal/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	cmp::Ordering,
6	fmt,
7	fmt::{Display, Formatter},
8	hash,
9	str::FromStr,
10};
11
12use arrow_buffer::i256;
13use serde::{
14	Deserialize, Deserializer, Serialize, Serializer,
15	de::{self, Visitor},
16};
17
18use crate::{
19	error::{Error, TypeError},
20	fragment::Fragment,
21	value::value_type::ValueType,
22};
23
24pub mod parse;
25pub mod unscaled;
26
27use unscaled::{MAX_DIGITS, ParseError, Parsed};
28
29#[derive(Clone, Debug, Default)]
30pub struct Decimal {
31	unscaled: i256,
32	scale: u8,
33}
34
35impl Decimal {
36	pub const MAX_SCALE: u8 = MAX_DIGITS;
37
38	pub fn from_parts(unscaled: i256, scale: u8) -> Option<Self> {
39		(scale <= MAX_DIGITS && unscaled::in_range(unscaled)).then_some(Self {
40			unscaled,
41			scale,
42		})
43	}
44
45	pub fn parse(text: &str) -> Result<Self, ParseError> {
46		let (unscaled, scale) = Parsed::parse(text)?.to_decimal()?;
47		Ok(Self {
48			unscaled,
49			scale,
50		})
51	}
52
53	pub fn zero() -> Self {
54		Self::default()
55	}
56
57	pub fn one() -> Self {
58		Self::from_i64(1)
59	}
60
61	pub fn from_i64(value: i64) -> Self {
62		Self::integer(i256::from_i128(value as i128))
63	}
64
65	pub fn from_i128(value: i128) -> Self {
66		Self::integer(i256::from_i128(value))
67	}
68
69	pub fn from_u128(value: u128) -> Self {
70		Self::integer(i256::from_parts(value, 0))
71	}
72
73	pub fn from_f64(value: f64) -> Option<Self> {
74		if !value.is_finite() {
75			return None;
76		}
77		Self::from_float_text(&format!("{value:e}"))
78	}
79
80	pub fn from_f32(value: f32) -> Option<Self> {
81		if !value.is_finite() {
82			return None;
83		}
84		Self::from_float_text(&format!("{value:e}"))
85	}
86
87	pub fn saturated(negative: bool, scale: u8) -> Self {
88		Self {
89			unscaled: if negative {
90				unscaled::MIN
91			} else {
92				unscaled::MAX
93			},
94			scale: scale.min(MAX_DIGITS),
95		}
96	}
97
98	pub fn unscaled(&self) -> i256 {
99		self.unscaled
100	}
101
102	pub fn scale(&self) -> u8 {
103		self.scale
104	}
105
106	pub fn digits(&self) -> u8 {
107		unscaled::digits(self.unscaled)
108	}
109
110	pub fn is_zero(&self) -> bool {
111		self.unscaled == i256::ZERO
112	}
113
114	pub fn is_negative(&self) -> bool {
115		self.unscaled.is_negative()
116	}
117
118	pub fn negate(&self) -> Self {
119		Self {
120			unscaled: self.unscaled.wrapping_neg(),
121			scale: self.scale,
122		}
123	}
124
125	pub fn abs(&self) -> Self {
126		Self {
127			unscaled: self.unscaled.wrapping_abs(),
128			scale: self.scale,
129		}
130	}
131
132	pub fn trunc(&self) -> i256 {
133		match unscaled::pow10(self.scale) {
134			Some(divisor) => self.unscaled.wrapping_div(divisor),
135			None => i256::ZERO,
136		}
137	}
138
139	pub fn ceil(&self) -> i256 {
140		match unscaled::pow10(self.scale) {
141			Some(divisor) if self.unscaled.wrapping_rem(divisor) > i256::ZERO => {
142				self.trunc().wrapping_add(i256::ONE)
143			}
144			_ => self.trunc(),
145		}
146	}
147
148	pub fn floor(&self) -> i256 {
149		match unscaled::pow10(self.scale) {
150			Some(divisor) if self.unscaled.wrapping_rem(divisor) < i256::ZERO => {
151				self.trunc().wrapping_sub(i256::ONE)
152			}
153			_ => self.trunc(),
154		}
155	}
156
157	pub fn to_f64(&self) -> f64 {
158		self.exponent_text().parse().expect("a decimal exponent text always parses as f64")
159	}
160
161	pub fn to_f32(&self) -> f32 {
162		self.exponent_text().parse().expect("a decimal exponent text always parses as f32")
163	}
164
165	pub fn rescale(&self, scale: u8) -> Option<Self> {
166		if scale >= self.scale {
167			let unscaled = unscaled::upscale(self.unscaled, scale - self.scale)?;
168			return Self::from_parts(unscaled, scale);
169		}
170		let divisor = unscaled::pow10(self.scale - scale)?;
171		if self.unscaled.wrapping_rem(divisor) != i256::ZERO {
172			return None;
173		}
174		Self::from_parts(self.unscaled.wrapping_div(divisor), scale)
175	}
176
177	pub fn round_to_scale(&self, scale: u8) -> Option<Self> {
178		if scale >= self.scale {
179			return self.rescale(scale);
180		}
181		Self::from_parts(unscaled::round_half_up(self.unscaled, self.scale - scale), scale)
182	}
183
184	pub fn fits(&self, precision: u8, scale: u8) -> Option<Self> {
185		let rescaled = self.rescale(scale)?;
186		(rescaled.digits() <= precision).then_some(rescaled)
187	}
188
189	pub fn checked_add(&self, other: &Self) -> Option<Self> {
190		let scale = self.scale.max(other.scale);
191		let left = unscaled::upscale(self.unscaled, scale - self.scale)?;
192		let right = unscaled::upscale(other.unscaled, scale - other.scale)?;
193		Self::from_parts(left.checked_add(right)?, scale)
194	}
195
196	pub fn checked_sub(&self, other: &Self) -> Option<Self> {
197		self.checked_add(&other.negate())
198	}
199
200	pub fn checked_mul(&self, other: &Self) -> Option<Self> {
201		let scale = self.scale.checked_add(other.scale)?;
202		if let Some(product) =
203			self.unscaled.checked_mul(other.unscaled).and_then(|unscaled| Self::from_parts(unscaled, scale))
204		{
205			return Some(product);
206		}
207		let ((left, left_scale), (right, right_scale)) = (self.normalized(), other.normalized());
208		Self::from_parts(left.checked_mul(right)?, left_scale + right_scale)
209	}
210
211	pub fn checked_pow(&self, exp: u32) -> Option<Self> {
212		let (unscaled, scale) = self.normalized();
213		let scale = u8::try_from(u32::from(scale).checked_mul(exp)?).ok()?;
214		Self::from_parts(unscaled.checked_pow(exp)?, scale)
215	}
216
217	pub fn checked_div(&self, other: &Self) -> Option<Self> {
218		if other.is_zero() {
219			return None;
220		}
221		let scale = self.scale.max(other.scale).max(6);
222		let extra = u32::from(scale - self.scale) + u32::from(other.scale);
223		let magnitude =
224			unscaled::divide_half_up(self.unscaled.wrapping_abs(), other.unscaled.wrapping_abs(), extra)?;
225		let unscaled = if self.is_negative() != other.is_negative() {
226			magnitude.wrapping_neg()
227		} else {
228			magnitude
229		};
230		Self::from_parts(unscaled, scale)
231	}
232
233	pub fn checked_rem(&self, other: &Self) -> Option<Self> {
234		if other.is_zero() {
235			return None;
236		}
237		let scale = self.scale.max(other.scale);
238		let dividend = self.unscaled.wrapping_abs();
239		let magnitude = match unscaled::upscale(other.unscaled.wrapping_abs(), scale - other.scale) {
240			Some(divisor) => {
241				unscaled::shifted_remainder(dividend.wrapping_rem(divisor), divisor, scale - self.scale)
242			}
243			None => unscaled::upscale(dividend, scale - self.scale)?,
244		};
245		let unscaled = if self.is_negative() {
246			magnitude.wrapping_neg()
247		} else {
248			magnitude
249		};
250		Self::from_parts(unscaled, scale)
251	}
252
253	pub fn saturating_add(&self, other: &Self) -> Self {
254		self.checked_add(other)
255			.unwrap_or_else(|| Self::saturated(*self < other.negate(), self.scale.max(other.scale)))
256	}
257
258	pub fn saturating_sub(&self, other: &Self) -> Self {
259		self.checked_sub(other).unwrap_or_else(|| Self::saturated(*self < *other, self.scale.max(other.scale)))
260	}
261
262	pub fn saturating_mul(&self, other: &Self) -> Self {
263		if let Some(product) = self.checked_mul(other) {
264			return product;
265		}
266		let scale = u16::from(self.scale) + u16::from(other.scale);
267		if scale > u16::from(MAX_DIGITS)
268			&& let Some(product) = self.unscaled.checked_mul(other.unscaled)
269		{
270			let drop = u8::try_from(scale - u16::from(MAX_DIGITS)).unwrap_or(u8::MAX);
271			return Self {
272				unscaled: unscaled::round_half_up(product, drop),
273				scale: MAX_DIGITS,
274			};
275		}
276		Self::saturated(self.is_negative() != other.is_negative(), scale.min(u16::from(MAX_DIGITS)) as u8)
277	}
278
279	pub fn saturating_div(&self, other: &Self) -> Self {
280		let scale = self.scale.max(other.scale).max(6);
281		if other.is_zero() {
282			return Self {
283				unscaled: i256::ZERO,
284				scale,
285			};
286		}
287		self.checked_div(other)
288			.unwrap_or_else(|| Self::saturated(self.is_negative() != other.is_negative(), scale))
289	}
290
291	pub fn saturating_rem(&self, other: &Self) -> Self {
292		if other.is_zero() {
293			return Self::zero();
294		}
295		self.checked_rem(other)
296			.unwrap_or_else(|| Self::saturated(self.is_negative(), self.scale.max(other.scale)))
297	}
298
299	fn integer(unscaled: i256) -> Self {
300		Self {
301			unscaled,
302			scale: 0,
303		}
304	}
305
306	fn from_float_text(text: &str) -> Option<Self> {
307		let (unscaled, scale) = Parsed::parse(text).ok()?.to_rounded_decimal().ok()?;
308		Self::from_parts(unscaled, scale)
309	}
310
311	fn exponent_text(&self) -> String {
312		format!("{}e-{}", self.unscaled, self.scale)
313	}
314
315	fn normalized(&self) -> (i256, u8) {
316		let ten = i256::from_i128(10);
317		let mut unscaled = self.unscaled;
318		let mut scale = self.scale;
319		while scale > 0 && unscaled.wrapping_rem(ten) == i256::ZERO {
320			unscaled = unscaled.wrapping_div(ten);
321			scale -= 1;
322		}
323		(unscaled, scale)
324	}
325}
326
327impl PartialEq for Decimal {
328	fn eq(&self, other: &Self) -> bool {
329		self.normalized() == other.normalized()
330	}
331}
332
333impl Eq for Decimal {}
334
335impl PartialOrd for Decimal {
336	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
337		Some(self.cmp(other))
338	}
339}
340
341impl Ord for Decimal {
342	fn cmp(&self, other: &Self) -> Ordering {
343		let scale = self.scale.max(other.scale);
344		let left = unscaled::upscale(self.unscaled, scale - self.scale);
345		let right = unscaled::upscale(other.unscaled, scale - other.scale);
346		match (left, right) {
347			(Some(left), Some(right)) => left.cmp(&right),
348			(None, _) if self.is_negative() => Ordering::Less,
349			(None, _) => Ordering::Greater,
350			(_, None) if other.is_negative() => Ordering::Greater,
351			(_, None) => Ordering::Less,
352		}
353	}
354}
355
356impl hash::Hash for Decimal {
357	fn hash<H: hash::Hasher>(&self, state: &mut H) {
358		self.normalized().hash(state);
359	}
360}
361
362impl Display for Decimal {
363	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
364		let magnitude = self.unscaled.wrapping_abs().to_string();
365		let sign = if self.is_negative() {
366			"-"
367		} else {
368			""
369		};
370		let scale = self.scale as usize;
371		if scale == 0 {
372			return write!(f, "{sign}{magnitude}");
373		}
374		let padded = if magnitude.len() <= scale {
375			format!("{}{}", "0".repeat(scale + 1 - magnitude.len()), magnitude)
376		} else {
377			magnitude
378		};
379		let (integer, fraction) = padded.split_at(padded.len() - scale);
380		write!(f, "{sign}{integer}.{fraction}")
381	}
382}
383
384impl FromStr for Decimal {
385	type Err = Error;
386
387	fn from_str(s: &str) -> Result<Self, Self::Err> {
388		Self::parse(s).map_err(|error| parse_error(error, Fragment::None))
389	}
390}
391
392pub(crate) fn parse_error(error: ParseError, fragment: Fragment) -> Error {
393	match error {
394		ParseError::Invalid => TypeError::InvalidNumberFormat {
395			target: ValueType::DECIMAL,
396			fragment,
397		}
398		.into(),
399		ParseError::OutOfRange => TypeError::NumberOutOfRange {
400			target: ValueType::DECIMAL,
401			fragment,
402			descriptor: None,
403		}
404		.into(),
405	}
406}
407
408impl From<i8> for Decimal {
409	fn from(value: i8) -> Self {
410		Self::from_i128(value as i128)
411	}
412}
413
414impl From<i16> for Decimal {
415	fn from(value: i16) -> Self {
416		Self::from_i128(value as i128)
417	}
418}
419
420impl From<i32> for Decimal {
421	fn from(value: i32) -> Self {
422		Self::from_i128(value as i128)
423	}
424}
425
426impl From<i64> for Decimal {
427	fn from(value: i64) -> Self {
428		Self::from_i128(value as i128)
429	}
430}
431
432impl From<i128> for Decimal {
433	fn from(value: i128) -> Self {
434		Self::from_i128(value)
435	}
436}
437
438impl From<u8> for Decimal {
439	fn from(value: u8) -> Self {
440		Self::from_i128(value as i128)
441	}
442}
443
444impl From<u16> for Decimal {
445	fn from(value: u16) -> Self {
446		Self::from_i128(value as i128)
447	}
448}
449
450impl From<u32> for Decimal {
451	fn from(value: u32) -> Self {
452		Self::from_i128(value as i128)
453	}
454}
455
456impl From<u64> for Decimal {
457	fn from(value: u64) -> Self {
458		Self::from_i128(value as i128)
459	}
460}
461
462impl From<u128> for Decimal {
463	fn from(value: u128) -> Self {
464		Self::from_u128(value)
465	}
466}
467
468impl From<f32> for Decimal {
469	fn from(value: f32) -> Self {
470		if !value.is_finite() {
471			return Self::zero();
472		}
473		Self::from_f32(value).unwrap_or_else(|| Self::saturated(value < 0.0, 0))
474	}
475}
476
477impl From<f64> for Decimal {
478	fn from(value: f64) -> Self {
479		if !value.is_finite() {
480			return Self::zero();
481		}
482		Self::from_f64(value).unwrap_or_else(|| Self::saturated(value < 0.0, 0))
483	}
484}
485
486impl Serialize for Decimal {
487	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488	where
489		S: Serializer,
490	{
491		serializer.collect_str(self)
492	}
493}
494
495struct DecimalVisitor;
496
497impl<'de> Visitor<'de> for DecimalVisitor {
498	type Value = Decimal;
499
500	fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
501		formatter.write_str("a decimal number as a string")
502	}
503
504	fn visit_str<E>(self, value: &str) -> Result<Decimal, E>
505	where
506		E: de::Error,
507	{
508		Decimal::parse(value).map_err(|error| E::custom(format!("invalid decimal {value:?}: {error:?}")))
509	}
510}
511
512impl<'de> Deserialize<'de> for Decimal {
513	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
514	where
515		D: Deserializer<'de>,
516	{
517		deserializer.deserialize_str(DecimalVisitor)
518	}
519}
520
521#[cfg(test)]
522pub mod tests {
523	use std::{
524		collections::HashSet,
525		hash::{DefaultHasher, Hash, Hasher},
526	};
527
528	use postcard::{from_bytes, to_stdvec};
529	use serde_json::{from_str, to_string};
530
531	use super::*;
532
533	fn dec(text: &str) -> Decimal {
534		Decimal::from_str(text).unwrap()
535	}
536
537	fn hash_of(decimal: &Decimal) -> u64 {
538		let mut hasher = DefaultHasher::new();
539		decimal.hash(&mut hasher);
540		hasher.finish()
541	}
542
543	#[test]
544	fn test_new_decimal_valid() {
545		let decimal = Decimal::from_parts(i256::from_i128(12345), 2).unwrap();
546		assert_eq!(decimal.to_string(), "123.45");
547	}
548
549	#[test]
550	fn test_from_str() {
551		let decimal = Decimal::from_str("123.45").unwrap();
552		assert_eq!(decimal.to_string(), "123.45");
553	}
554
555	#[test]
556	fn test_comparison() {
557		let d1 = Decimal::from_str("123.45").unwrap();
558		let d2 = Decimal::from_str("123.46").unwrap();
559		let d3 = Decimal::from_str("123.45").unwrap();
560
561		assert!(d1 < d2);
562		assert_eq!(d1, d3);
563	}
564
565	#[test]
566	fn test_display() {
567		let decimal = Decimal::from_str("123.45").unwrap();
568		assert_eq!(format!("{}", decimal), "123.45");
569	}
570
571	#[test]
572	fn test_serde_json() {
573		let decimal = Decimal::from_str("123.456789").unwrap();
574		let json = to_string(&decimal).unwrap();
575		assert_eq!(json, "\"123.456789\"");
576
577		let deserialized: Decimal = from_str(&json).unwrap();
578		assert_eq!(deserialized, decimal);
579	}
580
581	#[test]
582	fn test_serde_json_negative() {
583		let decimal = Decimal::from_str("-987.654321").unwrap();
584		let json = to_string(&decimal).unwrap();
585		assert_eq!(json, "\"-987.654321\"");
586
587		let deserialized: Decimal = from_str(&json).unwrap();
588		assert_eq!(deserialized, decimal);
589	}
590
591	#[test]
592	fn test_serde_json_zero() {
593		let decimal = Decimal::zero();
594		let json = to_string(&decimal).unwrap();
595		assert_eq!(json, "\"0\"");
596
597		let deserialized: Decimal = from_str(&json).unwrap();
598		assert_eq!(deserialized, decimal);
599	}
600
601	#[test]
602	fn test_serde_json_zero_with_scale() {
603		// Equality ignores scale, so only the text shows 0.00 did not come back as 0.
604		let decimal = Decimal::from_str("0.00").unwrap();
605		let json = to_string(&decimal).unwrap();
606		assert_eq!(json, "\"0.00\"");
607
608		let deserialized: Decimal = from_str(&json).unwrap();
609		assert_eq!(deserialized, decimal);
610		assert_eq!(deserialized.to_string(), "0.00");
611	}
612
613	#[test]
614	fn test_serde_json_high_precision() {
615		let decimal = Decimal::from_str("123456789.123456789123456789").unwrap();
616		let json = to_string(&decimal).unwrap();
617
618		let deserialized: Decimal = from_str(&json).unwrap();
619		assert_eq!(deserialized, decimal);
620	}
621
622	#[test]
623	fn test_serde_postcard() {
624		let decimal = Decimal::from_str("123.456789").unwrap();
625		let encoded = to_stdvec(&decimal).unwrap();
626
627		let decoded: Decimal = from_bytes(&encoded).unwrap();
628		assert_eq!(decoded, decimal);
629	}
630
631	#[test]
632	fn test_serde_postcard_negative() {
633		let decimal = Decimal::from_str("-987.654321").unwrap();
634		let encoded = to_stdvec(&decimal).unwrap();
635
636		let decoded: Decimal = from_bytes(&encoded).unwrap();
637		assert_eq!(decoded, decimal);
638	}
639
640	#[test]
641	fn test_serde_postcard_zero() {
642		let decimal = Decimal::zero();
643		let encoded = to_stdvec(&decimal).unwrap();
644
645		let decoded: Decimal = from_bytes(&encoded).unwrap();
646		assert_eq!(decoded, decimal);
647	}
648
649	#[test]
650	fn test_serde_postcard_high_precision() {
651		let decimal = Decimal::from_str("123456789.123456789123456789").unwrap();
652		let encoded = to_stdvec(&decimal).unwrap();
653
654		let decoded: Decimal = from_bytes(&encoded).unwrap();
655		assert_eq!(decoded, decimal);
656	}
657
658	#[test]
659	fn test_serde_postcard_large_number() {
660		let decimal = Decimal::from_str("999999999999999999999999999999.999999999999999999999999").unwrap();
661		let encoded = to_stdvec(&decimal).unwrap();
662
663		let decoded: Decimal = from_bytes(&encoded).unwrap();
664		assert_eq!(decoded, decimal);
665	}
666
667	#[test]
668	fn serde_keeps_the_scale_not_only_the_value() {
669		// Equality ignores scale, so only the parts show whether a round trip normalised 1.50 to 1.5.
670		for text in ["1.50", "-0.120", "0.00", "7"] {
671			let decimal = dec(text);
672			let back: Decimal = from_bytes(&to_stdvec(&decimal).unwrap()).unwrap();
673			assert_eq!((back.unscaled(), back.scale()), (decimal.unscaled(), decimal.scale()), "{text}");
674			let back: Decimal = from_str(&to_string(&decimal).unwrap()).unwrap();
675			assert_eq!(back.to_string(), text);
676		}
677	}
678
679	#[test]
680	fn display_prints_exactly_scale_fraction_digits() {
681		// Trimming or padding here changes what every decimal column renders.
682		assert_eq!(Decimal::from_parts(i256::ZERO, 2).unwrap().to_string(), "0.00");
683		assert_eq!(Decimal::from_parts(i256::from_i128(5), 3).unwrap().to_string(), "0.005");
684		assert_eq!(Decimal::from_parts(i256::from_i128(-5), 3).unwrap().to_string(), "-0.005");
685		assert_eq!(Decimal::from_parts(i256::from_i128(-12345), 3).unwrap().to_string(), "-12.345");
686		assert_eq!(Decimal::from_parts(i256::from_i128(1000), 0).unwrap().to_string(), "1000");
687		assert_eq!(
688			Decimal::from_parts(unscaled::MIN, 76).unwrap().to_string(),
689			format!("-0.{}", "9".repeat(76))
690		);
691	}
692
693	#[test]
694	fn from_parts_rejects_seventy_seven_digits_and_scales_past_seventy_six() {
695		// Letting either through builds a value no Arrow decimal type can hold.
696		assert!(Decimal::from_parts(unscaled::MAX, 76).is_some());
697		assert_eq!(Decimal::from_parts(unscaled::MAX.wrapping_add(i256::ONE), 0), None);
698		assert_eq!(Decimal::from_parts(unscaled::MIN.wrapping_sub(i256::ONE), 0), None);
699		assert_eq!(Decimal::from_parts(i256::ONE, 77), None);
700	}
701
702	#[test]
703	fn equality_order_and_hash_ignore_the_scale() {
704		// Hash must agree with Eq, otherwise hashed collections split 1.5 and 1.50.
705		assert_eq!(dec("1.5"), dec("1.50"));
706		assert_eq!(hash_of(&dec("1.5")), hash_of(&dec("1.500")));
707		assert_eq!(dec("0"), dec("0.000"));
708		assert_eq!(hash_of(&dec("0")), hash_of(&dec("0.000")));
709		assert_eq!(dec("1.5").cmp(&dec("1.50")), Ordering::Equal);
710		assert!(dec("-1.51") < dec("-1.5"));
711		assert!(dec("0.1") < dec("0.10000000001"));
712		assert_ne!(dec("1.5"), dec("15"));
713		let set: HashSet<Decimal> = ["1.5", "1.50", "1.500"].iter().map(|s| dec(s)).collect();
714		assert_eq!(set.len(), 1);
715	}
716
717	#[test]
718	fn order_holds_when_rescaling_would_overflow() {
719		// Rescaling a 76 digit integer to scale 76 overflows i256; the order must still be exact.
720		let huge = Decimal::from_parts(unscaled::MAX, 0).unwrap();
721		let tiny = Decimal::from_parts(i256::ONE, 76).unwrap();
722		assert!(huge > tiny);
723		assert!(huge.negate() < tiny);
724		assert!(tiny < huge);
725		assert!(tiny > huge.negate());
726		assert_ne!(huge, tiny);
727	}
728
729	#[test]
730	fn from_str_rejects_past_seventy_six_digits_as_out_of_range() {
731		// A 77 digit value must be a range error, never a wrapped or truncated number.
732		assert_eq!(Decimal::from_str(&"1".repeat(77)).unwrap_err().code, "NUMBER_002");
733		assert_eq!(Decimal::from_str("1.2.3").unwrap_err().code, "NUMBER_001");
734		assert!(Decimal::from_str(&"9".repeat(76)).is_ok());
735	}
736
737	#[test]
738	fn checked_add_and_sub_use_the_larger_scale() {
739		// A result at the smaller scale would drop digits from the more precise operand.
740		let sum = dec("1.5").checked_add(&dec("2.25")).unwrap();
741		assert_eq!((sum.to_string(), sum.scale()), ("3.75".to_string(), 2));
742		assert_eq!(dec("1").checked_sub(&dec("0.001")).unwrap().to_string(), "0.999");
743		let max = Decimal::from_parts(unscaled::MAX, 0).unwrap();
744		assert_eq!(max.checked_add(&Decimal::one()), None);
745		assert_eq!(max.negate().checked_sub(&Decimal::one()), None);
746		assert_eq!(max.checked_sub(&Decimal::one()).unwrap().digits(), 76);
747	}
748
749	#[test]
750	fn checked_mul_adds_the_scales() {
751		// Any other result scale disagrees with the type rule multiplication derives.
752		assert_eq!(dec("1.50").checked_mul(&dec("2.0")).unwrap().to_string(), "3.000");
753		assert_eq!(dec("-1.5").checked_mul(&dec("1.5")).unwrap().to_string(), "-2.25");
754		let wide = Decimal::from_parts(unscaled::pow10(40).unwrap(), 0).unwrap();
755		assert_eq!(wide.checked_mul(&wide), None);
756		let deep = Decimal::from_parts(i256::ONE, 40).unwrap();
757		assert_eq!(deep.checked_mul(&deep), None);
758	}
759
760	#[test]
761	fn checked_div_uses_at_least_six_places_and_rounds_half_up() {
762		// The result scale is max(left, right, 6); truncating would give 0.666666 for 2/3.
763		assert_eq!(dec("2").checked_div(&dec("3")).unwrap().to_string(), "0.666667");
764		assert_eq!(dec("-2").checked_div(&dec("3")).unwrap().to_string(), "-0.666667");
765		assert_eq!(dec("3").checked_div(&dec("2")).unwrap().to_string(), "1.500000");
766		assert_eq!(dec("1.00000000").checked_div(&dec("4")).unwrap().to_string(), "0.25000000");
767		assert_eq!(dec("10").checked_div(&dec("0.001")).unwrap().to_string(), "10000.000000");
768		assert_eq!(dec("1").checked_div(&dec("0")), None);
769		let huge = Decimal::from_parts(unscaled::pow10(70).unwrap(), 0).unwrap();
770		assert_eq!(huge.checked_div(&dec("3")).unwrap().digits(), 76);
771		assert_eq!(huge.checked_div(&dec("0.1")), None);
772	}
773
774	#[test]
775	fn checked_rem_keeps_the_dividend_sign_and_the_larger_scale() {
776		// A remainder with the divisor sign or a lost fraction breaks x = q * y + r.
777		assert_eq!(dec("7.5").checked_rem(&dec("2")).unwrap().to_string(), "1.5");
778		assert_eq!(dec("-7.5").checked_rem(&dec("2")).unwrap().to_string(), "-1.5");
779		assert_eq!(dec("7").checked_rem(&dec("0.3")).unwrap().to_string(), "0.1");
780		assert_eq!(dec("7").checked_rem(&dec("0")), None);
781		let huge = Decimal::from_parts(unscaled::MAX, 0).unwrap();
782		let tenths = Decimal::from_parts(i256::from_i128(3), 1).unwrap();
783		assert_eq!(huge.checked_rem(&tenths).unwrap().to_string(), "0.0");
784		let small = dec("0.5");
785		assert_eq!(small.checked_rem(&huge).unwrap().to_string(), "0.5");
786		assert_eq!(dec("-0.5").checked_rem(&huge).unwrap().to_string(), "-0.5");
787	}
788
789	#[test]
790	fn round_to_scale_rounds_half_away_from_zero() {
791		// A cast rounds half up on the magnitude, so -1.235 becomes -1.24, not -1.23.
792		assert_eq!(dec("1.235").round_to_scale(2).unwrap().to_string(), "1.24");
793		assert_eq!(dec("-1.235").round_to_scale(2).unwrap().to_string(), "-1.24");
794		assert_eq!(dec("1.234").round_to_scale(2).unwrap().to_string(), "1.23");
795		assert_eq!(dec("0.125").round_to_scale(2).unwrap().to_string(), "0.13");
796		assert_eq!(dec("0.0000001").round_to_scale(2).unwrap().to_string(), "0.00");
797		assert_eq!(dec("1.5").round_to_scale(3).unwrap().to_string(), "1.500");
798		let max = Decimal::from_parts(unscaled::MAX, 1).unwrap();
799		assert_eq!(max.round_to_scale(0), Decimal::from_parts(unscaled::pow10(75).unwrap(), 0), "a carry fits");
800		let widest = Decimal::from_parts(unscaled::MAX, 0).unwrap();
801		assert_eq!(widest.round_to_scale(1), None, "padding 76 nines with a zero needs a 77th digit");
802	}
803
804	#[test]
805	fn rescale_refuses_to_drop_fraction_digits() {
806		// A write never rounds, so 1.235 cannot become a scale 2 value.
807		assert_eq!(dec("1.235").rescale(2), None);
808		assert_eq!(dec("1.230").rescale(2).unwrap().to_string(), "1.23");
809		assert_eq!(dec("1.2").rescale(2).unwrap().to_string(), "1.20");
810		assert_eq!(Decimal::from_parts(unscaled::MAX, 0).unwrap().rescale(1), None);
811		assert_eq!(dec("1.2").fits(10, 2).unwrap().to_string(), "1.20");
812		assert_eq!(dec("123456789.99").fits(10, 2), None);
813		assert_eq!(dec("12345678.99").fits(10, 2).unwrap().to_string(), "12345678.99");
814		assert_eq!(dec("1.235").fits(10, 2), None);
815	}
816
817	#[test]
818	fn float_conversions_go_through_the_shortest_text() {
819		// The shortest float text keeps 0.1 as 0.1 instead of its binary expansion.
820		assert_eq!(Decimal::from_f64(0.1).unwrap().to_string(), "0.1");
821		assert_eq!(Decimal::from_f64(-2.5e-3).unwrap().to_string(), "-0.0025");
822		assert_eq!(Decimal::from_f64(1e20).unwrap().to_string(), "100000000000000000000");
823		assert_eq!(Decimal::from_f64(1e-300).unwrap(), Decimal::zero());
824		assert_eq!(Decimal::from_f64(1e80), None);
825		assert_eq!(Decimal::from_f64(f64::NAN), None);
826		assert_eq!(Decimal::from_f32(1.5f32).unwrap().to_string(), "1.5");
827		assert_eq!(Decimal::from(f64::INFINITY), Decimal::zero());
828		assert_eq!(Decimal::from(-1e80f64), Decimal::saturated(true, 0));
829		assert_eq!(dec("0.1").to_f64(), 0.1);
830		assert_eq!(dec("-12.5").to_f32(), -12.5f32);
831		assert_eq!(Decimal::from_parts(unscaled::MAX, 0).unwrap().to_f64(), 1e76);
832	}
833
834	#[test]
835	fn trunc_drops_the_fraction_toward_zero() {
836		// Flooring would turn -3.7 into -4 where every integer conversion truncates.
837		assert_eq!(dec("3.7").trunc(), i256::from_i128(3));
838		assert_eq!(dec("-3.7").trunc(), i256::from_i128(-3));
839		assert_eq!(dec("0.999").trunc(), i256::ZERO);
840	}
841
842	#[test]
843	fn ceil_rounds_toward_positive_infinity_exactly() {
844		// A trip through f64 would turn the 29 digit value into 12345678901234568227576610816.
845		assert_eq!(dec("3.2").ceil(), i256::from_i128(4));
846		assert_eq!(dec("-3.7").ceil(), i256::from_i128(-3));
847		assert_eq!(dec("5.000").ceil(), i256::from_i128(5));
848		assert_eq!(dec("0.001").ceil(), i256::ONE);
849		assert_eq!(dec("-0.001").ceil(), i256::ZERO);
850		assert_eq!(
851			dec("12345678901234567890123456789.5").ceil(),
852			dec("12345678901234567890123456790").unscaled()
853		);
854	}
855
856	#[test]
857	fn floor_rounds_toward_negative_infinity_exactly() {
858		// Truncating instead of flooring would leave -3.2 at -3 and -0.001 at 0.
859		assert_eq!(dec("3.7").floor(), i256::from_i128(3));
860		assert_eq!(dec("-3.2").floor(), i256::from_i128(-4));
861		assert_eq!(dec("-5.00").floor(), i256::from_i128(-5));
862		assert_eq!(dec("-0.001").floor(), i256::MINUS_ONE);
863		assert_eq!(dec("0.001").floor(), i256::ZERO);
864		assert_eq!(
865			dec("-12345678901234567890123456789.5").floor(),
866			dec("-12345678901234567890123456790").unscaled()
867		);
868	}
869
870	#[test]
871	fn checked_pow_is_exact_and_refuses_what_does_not_fit() {
872		// f64 would give 1.5241578753238835e36 and drop the last 20 digits of the square.
873		assert_eq!(
874			dec("1234567890123456789.25").checked_pow(2),
875			Some(dec("1524157875323883675636335943811918915.5625"))
876		);
877		assert_eq!(dec("2.50").checked_pow(3), Some(dec("15.625")));
878		assert_eq!(dec("-1.5").checked_pow(3), Some(dec("-3.375")));
879		assert_eq!(dec("7.25").checked_pow(0), Some(Decimal::one()));
880		assert_eq!(dec("10").checked_pow(80), None);
881		assert_eq!(dec("0.5").checked_pow(200), None);
882	}
883
884	#[test]
885	fn saturating_ops_clamp_to_seventy_six_nines_with_the_exact_sign() {
886		// Clamping with the wrong sign, or to a value past 76 digits, stores a number nobody computed.
887		let max = Decimal::from_parts(unscaled::MAX, 0).unwrap();
888		let min = max.negate();
889		assert_eq!(max.saturating_add(&Decimal::one()), max);
890		assert_eq!(min.saturating_add(&Decimal::one().negate()), min);
891		assert_eq!(min.saturating_sub(&Decimal::one()), min);
892		assert_eq!(max.saturating_sub(&Decimal::one().negate()), max);
893		assert_eq!(max.saturating_add(&dec("0.5")), Decimal::saturated(false, 1));
894		assert_eq!(max.saturating_mul(&dec("-2")), min);
895		assert_eq!(min.saturating_mul(&dec("-2")), max);
896		assert_eq!(dec("1").saturating_div(&dec("0")).to_string(), "0.000000");
897		assert_eq!(max.saturating_div(&dec("0.5")), Decimal::saturated(false, 6));
898		assert_eq!(dec("7").saturating_rem(&dec("0")), Decimal::zero());
899		assert_eq!(dec("1.5").saturating_add(&dec("1")).to_string(), "2.5");
900	}
901
902	#[test]
903	fn saturating_mul_rounds_a_scale_past_seventy_six_instead_of_clamping() {
904		// A tiny product must round toward zero, not jump to the largest representable value.
905		let deep = Decimal::from_parts(i256::from_i128(15), 40).unwrap();
906		let product = deep.saturating_mul(&deep);
907		assert_eq!(product.scale(), 76);
908		assert_eq!(product, Decimal::zero());
909		let half = Decimal::from_parts(i256::from_i128(5), 40).unwrap();
910		let unit = Decimal::from_parts(i256::ONE, 37).unwrap();
911		assert_eq!(half.saturating_mul(&unit), Decimal::from_parts(i256::ONE, 76).unwrap());
912		assert_eq!(half.negate().saturating_mul(&unit), Decimal::from_parts(i256::MINUS_ONE, 76).unwrap());
913	}
914}