1use std::{
19 cmp::Ordering,
20 fmt::{Debug, Display},
21 hash::{Hash, Hasher},
22 ops::{Add, AddAssign, Deref, Mul, MulAssign, Sub, SubAssign},
23 str::FromStr,
24};
25
26use nautilus_core::{
27 correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
28 parsing::precision_from_str,
29};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Deserializer, Serialize};
32use thousands::Separable;
33
34use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision};
35#[cfg(not(feature = "high-precision"))]
36use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
37#[cfg(feature = "high-precision")]
38use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
39
40#[cfg(feature = "high-precision")]
41pub type QuantityRaw = u128;
42#[cfg(not(feature = "high-precision"))]
43pub type QuantityRaw = u64;
44
45#[unsafe(no_mangle)]
47#[allow(unsafe_code)]
48pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
49
50pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
52
53#[cfg(feature = "high-precision")]
55pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
56#[cfg(not(feature = "high-precision"))]
57pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
58
59pub const QUANTITY_MIN: f64 = 0.0;
61
62#[repr(C)]
73#[derive(Clone, Copy, Default, Eq)]
74#[cfg_attr(
75 feature = "python",
76 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
77)]
78pub struct Quantity {
79 pub raw: QuantityRaw,
81 pub precision: u8,
83}
84
85impl Quantity {
86 pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
98 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
99 check_fixed_precision(precision)?;
100
101 #[cfg(feature = "high-precision")]
102 let raw = f64_to_fixed_u128(value, precision);
103 #[cfg(not(feature = "high-precision"))]
104 let raw = f64_to_fixed_u64(value, precision);
105
106 Ok(Self { raw, precision })
107 }
108
109 pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
123 check_predicate_true(value != 0.0, "value was zero")?;
124 check_fixed_precision(precision)?;
125 let rounded_value =
126 (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
127 check_predicate_true(
128 rounded_value != 0.0,
129 &format!("value {value} was zero after rounding to precision {precision}"),
130 )?;
131
132 Self::new_checked(value, precision)
133 }
134
135 pub fn new(value: f64, precision: u8) -> Self {
141 Self::new_checked(value, precision).expect(FAILED)
142 }
143
144 pub fn non_zero(value: f64, precision: u8) -> Self {
150 Self::non_zero_checked(value, precision).expect(FAILED)
151 }
152
153 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
159 if raw == QUANTITY_UNDEF {
160 check_predicate_true(
161 precision == 0,
162 "`precision` must be 0 when `raw` is QUANTITY_UNDEF",
163 )
164 .expect(FAILED);
165 }
166 check_predicate_true(
167 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
168 &format!("raw outside valid range, was {raw}"),
169 )
170 .expect(FAILED);
171 check_fixed_precision(precision).expect(FAILED);
172 Self { raw, precision }
173 }
174
175 #[must_use]
181 pub fn zero(precision: u8) -> Self {
182 check_fixed_precision(precision).expect(FAILED);
183 Self::new(0.0, precision)
184 }
185
186 #[must_use]
188 pub fn is_undefined(&self) -> bool {
189 self.raw == QUANTITY_UNDEF
190 }
191
192 #[must_use]
194 pub fn is_zero(&self) -> bool {
195 self.raw == 0
196 }
197
198 #[must_use]
200 pub fn is_positive(&self) -> bool {
201 self.raw != QUANTITY_UNDEF && self.raw > 0
202 }
203
204 #[must_use]
206 #[cfg(feature = "high-precision")]
207 pub fn as_f64(&self) -> f64 {
208 fixed_u128_to_f64(self.raw)
209 }
210
211 #[cfg(not(feature = "high-precision"))]
212 pub fn as_f64(&self) -> f64 {
213 fixed_u64_to_f64(self.raw)
214 }
215
216 #[must_use]
218 pub fn as_decimal(&self) -> Decimal {
219 let rescaled_raw =
221 self.raw / QuantityRaw::pow(10, u32::from(FIXED_PRECISION - self.precision));
222 #[allow(clippy::useless_conversion)] Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
227 }
228
229 #[must_use]
231 pub fn to_formatted_string(&self) -> String {
232 format!("{self}").separate_with_underscores()
233 }
234}
235
236impl From<Quantity> for f64 {
237 fn from(qty: Quantity) -> Self {
238 qty.as_f64()
239 }
240}
241
242impl From<&Quantity> for f64 {
243 fn from(qty: &Quantity) -> Self {
244 qty.as_f64()
245 }
246}
247
248impl From<i32> for Quantity {
249 fn from(value: i32) -> Self {
250 Self::new(value as f64, 0)
251 }
252}
253
254impl From<i64> for Quantity {
255 fn from(value: i64) -> Self {
256 Self::new(value as f64, 0)
257 }
258}
259
260impl From<u32> for Quantity {
261 fn from(value: u32) -> Self {
262 Self::new(value as f64, 0)
263 }
264}
265
266impl From<u64> for Quantity {
267 fn from(value: u64) -> Self {
268 Self::new(value as f64, 0)
269 }
270}
271
272impl Hash for Quantity {
273 fn hash<H: Hasher>(&self, state: &mut H) {
274 self.raw.hash(state);
275 }
276}
277
278impl PartialEq for Quantity {
279 fn eq(&self, other: &Self) -> bool {
280 self.raw == other.raw
281 }
282}
283
284impl PartialOrd for Quantity {
285 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
286 Some(self.cmp(other))
287 }
288
289 fn lt(&self, other: &Self) -> bool {
290 self.raw.lt(&other.raw)
291 }
292
293 fn le(&self, other: &Self) -> bool {
294 self.raw.le(&other.raw)
295 }
296
297 fn gt(&self, other: &Self) -> bool {
298 self.raw.gt(&other.raw)
299 }
300
301 fn ge(&self, other: &Self) -> bool {
302 self.raw.ge(&other.raw)
303 }
304}
305
306impl Ord for Quantity {
307 fn cmp(&self, other: &Self) -> Ordering {
308 self.raw.cmp(&other.raw)
309 }
310}
311
312impl Deref for Quantity {
313 type Target = QuantityRaw;
314
315 fn deref(&self) -> &Self::Target {
316 &self.raw
317 }
318}
319
320impl Add for Quantity {
321 type Output = Self;
322 fn add(self, rhs: Self) -> Self::Output {
323 let precision = match self.precision {
324 0 => rhs.precision,
325 _ => self.precision,
326 };
327 assert!(
328 self.precision >= rhs.precision,
329 "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
330 rhs.precision,
331 self.precision,
332 );
333 Self {
334 raw: self
335 .raw
336 .checked_add(rhs.raw)
337 .expect("Overflow occurred when adding `Quantity`"),
338 precision,
339 }
340 }
341}
342
343impl Sub for Quantity {
344 type Output = Self;
345 fn sub(self, rhs: Self) -> Self::Output {
346 let precision = match self.precision {
347 0 => rhs.precision,
348 _ => self.precision,
349 };
350 assert!(
351 self.precision >= rhs.precision,
352 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
353 rhs.precision,
354 self.precision,
355 );
356 Self {
357 raw: self
358 .raw
359 .checked_sub(rhs.raw)
360 .expect("Underflow occurred when subtracting `Quantity`"),
361 precision,
362 }
363 }
364}
365
366#[allow(clippy::suspicious_arithmetic_impl)] impl Mul for Quantity {
368 type Output = Self;
369 fn mul(self, rhs: Self) -> Self::Output {
370 let precision = match self.precision {
371 0 => rhs.precision,
372 _ => self.precision,
373 };
374 assert!(
375 self.precision >= rhs.precision,
376 "Precision mismatch: cannot multiply precision {} with precision {} (precision loss)",
377 rhs.precision,
378 self.precision,
379 );
380
381 let result_raw = self
382 .raw
383 .checked_mul(rhs.raw)
384 .expect("Overflow occurred when multiplying `Quantity`");
385
386 Self {
387 raw: result_raw / (FIXED_SCALAR as QuantityRaw),
388 precision,
389 }
390 }
391}
392
393impl Mul<f64> for Quantity {
394 type Output = f64;
395 fn mul(self, rhs: f64) -> Self::Output {
396 self.as_f64() * rhs
397 }
398}
399
400impl From<Quantity> for QuantityRaw {
401 fn from(value: Quantity) -> Self {
402 value.raw
403 }
404}
405
406impl From<&Quantity> for QuantityRaw {
407 fn from(value: &Quantity) -> Self {
408 value.raw
409 }
410}
411
412impl FromStr for Quantity {
413 type Err = String;
414
415 fn from_str(value: &str) -> Result<Self, Self::Err> {
416 let float_from_input = value
417 .replace('_', "")
418 .parse::<f64>()
419 .map_err(|e| format!("Error parsing `input` string '{value}' as f64: {e}"))?;
420
421 Self::new_checked(float_from_input, precision_from_str(value)).map_err(|e| e.to_string())
422 }
423}
424
425impl From<&str> for Quantity {
427 fn from(value: &str) -> Self {
428 Self::from_str(value).expect("Valid string input for `Quantity`")
429 }
430}
431
432impl From<String> for Quantity {
433 fn from(value: String) -> Self {
434 Self::from_str(&value).expect("Valid string input for `Quantity`")
435 }
436}
437
438impl From<&String> for Quantity {
439 fn from(value: &String) -> Self {
440 Self::from_str(value).expect("Valid string input for `Quantity`")
441 }
442}
443
444impl<T: Into<QuantityRaw>> AddAssign<T> for Quantity {
445 fn add_assign(&mut self, other: T) {
446 self.raw = self
447 .raw
448 .checked_add(other.into())
449 .expect("Overflow occurred when adding `Quantity`");
450 }
451}
452
453impl<T: Into<QuantityRaw>> SubAssign<T> for Quantity {
454 fn sub_assign(&mut self, other: T) {
455 self.raw = self
456 .raw
457 .checked_sub(other.into())
458 .expect("Underflow occurred when subtracting `Quantity`");
459 }
460}
461
462impl<T: Into<QuantityRaw>> MulAssign<T> for Quantity {
463 fn mul_assign(&mut self, other: T) {
464 self.raw = self
465 .raw
466 .checked_mul(other.into())
467 .expect("Overflow occurred when multiplying `Quantity`");
468 }
469}
470
471impl Debug for Quantity {
472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 write!(
474 f,
475 "{}({:.*})",
476 stringify!(Quantity),
477 self.precision as usize,
478 self.as_f64(),
479 )
480 }
481}
482
483impl Display for Quantity {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 write!(f, "{:.*}", self.precision as usize, self.as_f64())
486 }
487}
488
489impl Serialize for Quantity {
490 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
491 where
492 S: serde::Serializer,
493 {
494 serializer.serialize_str(&self.to_string())
495 }
496}
497
498impl<'de> Deserialize<'de> for Quantity {
499 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
500 where
501 D: Deserializer<'de>,
502 {
503 let qty_str: &str = Deserialize::deserialize(_deserializer)?;
504 let qty: Self = qty_str.into();
505 Ok(qty)
506 }
507}
508
509pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
515 if !value.is_positive() {
516 anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
517 }
518 Ok(())
519}
520
521#[cfg(test)]
525mod tests {
526 use std::str::FromStr;
527
528 use float_cmp::approx_eq;
529 use rstest::rstest;
530 use rust_decimal_macros::dec;
531
532 use super::*;
533
534 #[rstest]
535 #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
536 fn test_check_quantity_positive() {
537 let qty = Quantity::new(0.0, 0);
538 check_positive_quantity(qty, "qty").unwrap();
539 }
540
541 #[rstest]
542 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
543 fn test_invalid_precision_new() {
544 let _ = Quantity::new(1.0, FIXED_PRECISION + 1);
546 }
547
548 #[rstest]
549 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
550 fn test_invalid_precision_from_raw() {
551 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
553 }
554
555 #[rstest]
556 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
557 fn test_invalid_precision_zero() {
558 let _ = Quantity::zero(FIXED_PRECISION + 1);
560 }
561
562 #[rstest]
563 #[should_panic(
564 expected = "Precision mismatch: cannot add precision 2 to precision 1 (precision loss)"
565 )]
566 fn test_precision_mismatch_add() {
567 let q1 = Quantity::new(1.0, 1);
568 let q2 = Quantity::new(1.0, 2);
569 let _ = q1 + q2;
570 }
571
572 #[rstest]
573 #[should_panic(
574 expected = "Precision mismatch: cannot subtract precision 2 from precision 1 (precision loss)"
575 )]
576 fn test_precision_mismatch_sub() {
577 let q1 = Quantity::new(1.0, 1);
578 let q2 = Quantity::new(1.0, 2);
579 let _ = q1 - q2;
580 }
581
582 #[rstest]
583 #[should_panic(
584 expected = "Precision mismatch: cannot multiply precision 2 with precision 1 (precision loss)"
585 )]
586 fn test_precision_mismatch_mul() {
587 let q1 = Quantity::new(2.0, 1);
588 let q2 = Quantity::new(3.0, 2);
589 let _ = q1 * q2;
590 }
591
592 #[rstest]
593 fn test_new_non_zero_ok() {
594 let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
595 assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
596 assert!(qty.is_positive());
597 }
598
599 #[rstest]
600 fn test_new_non_zero_zero_input() {
601 assert!(Quantity::non_zero_checked(0.0, 0).is_err());
602 }
603
604 #[rstest]
605 fn test_new_non_zero_rounds_to_zero() {
606 assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
608 }
609
610 #[rstest]
611 fn test_new_non_zero_negative() {
612 assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
613 }
614
615 #[rstest]
616 fn test_new_non_zero_exceeds_max() {
617 assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
618 }
619
620 #[rstest]
621 fn test_new_non_zero_invalid_precision() {
622 assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
623 }
624
625 #[rstest]
626 fn test_new() {
627 let value = 0.00812;
628 let qty = Quantity::new(value, 8);
629 assert_eq!(qty, qty);
630 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
631 assert_eq!(qty.precision, 8);
632 assert_eq!(qty.as_f64(), 0.00812);
633 assert_eq!(qty.as_decimal(), dec!(0.00812000));
634 assert_eq!(qty.to_string(), "0.00812000");
635 assert!(!qty.is_zero());
636 assert!(qty.is_positive());
637 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
638 }
639
640 #[rstest]
641 fn test_check_quantity_positive_ok() {
642 let qty = Quantity::new(10.0, 0);
643 check_positive_quantity(qty, "qty").unwrap();
644 }
645
646 #[rstest]
647 fn test_negative_quantity_validation() {
648 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
649 }
650
651 #[rstest]
652 fn test_undefined() {
653 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
654 assert_eq!(qty.raw, QUANTITY_UNDEF);
655 assert!(qty.is_undefined());
656 }
657
658 #[rstest]
659 fn test_zero() {
660 let qty = Quantity::zero(8);
661 assert_eq!(qty.raw, 0);
662 assert_eq!(qty.precision, 8);
663 assert!(qty.is_zero());
664 assert!(!qty.is_positive());
665 }
666
667 #[rstest]
668 fn test_from_i32() {
669 let value = 100_000i32;
670 let qty = Quantity::from(value);
671 assert_eq!(qty, qty);
672 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
673 assert_eq!(qty.precision, 0);
674 }
675
676 #[rstest]
677 fn test_from_u32() {
678 let value: u32 = 5000;
679 let qty = Quantity::from(value);
680 assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
681 assert_eq!(qty.precision, 0);
682 }
683
684 #[rstest]
685 fn test_from_i64() {
686 let value = 100_000i64;
687 let qty = Quantity::from(value);
688 assert_eq!(qty, qty);
689 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
690 assert_eq!(qty.precision, 0);
691 }
692
693 #[rstest]
694 fn test_from_u64() {
695 let value = 100_000u64;
696 let qty = Quantity::from(value);
697 assert_eq!(qty, qty);
698 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
699 assert_eq!(qty.precision, 0);
700 }
701
702 #[rstest] fn test_with_maximum_value() {
704 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
705 assert!(qty.is_ok());
706 }
707
708 #[rstest]
709 fn test_with_minimum_positive_value() {
710 let value = 0.000_000_001;
711 let qty = Quantity::new(value, 9);
712 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
713 assert_eq!(qty.as_decimal(), dec!(0.000000001));
714 assert_eq!(qty.to_string(), "0.000000001");
715 }
716
717 #[rstest]
718 fn test_with_minimum_value() {
719 let qty = Quantity::new(QUANTITY_MIN, 9);
720 assert_eq!(qty.raw, 0);
721 assert_eq!(qty.as_decimal(), dec!(0));
722 assert_eq!(qty.to_string(), "0.000000000");
723 }
724
725 #[rstest]
726 fn test_is_zero() {
727 let qty = Quantity::zero(8);
728 assert_eq!(qty, qty);
729 assert_eq!(qty.raw, 0);
730 assert_eq!(qty.precision, 8);
731 assert_eq!(qty.as_f64(), 0.0);
732 assert_eq!(qty.as_decimal(), dec!(0));
733 assert_eq!(qty.to_string(), "0.00000000");
734 assert!(qty.is_zero());
735 }
736
737 #[rstest]
738 fn test_precision() {
739 let value = 1.001;
740 let qty = Quantity::new(value, 2);
741 assert_eq!(qty.to_string(), "1.00");
742 }
743
744 #[rstest]
745 fn test_new_from_str() {
746 let qty = Quantity::new(0.00812000, 8);
747 assert_eq!(qty, qty);
748 assert_eq!(qty.precision, 8);
749 assert_eq!(qty.as_f64(), 0.00812);
750 assert_eq!(qty.to_string(), "0.00812000");
751 }
752
753 #[rstest]
754 #[case("0", 0)]
755 #[case("1.1", 1)]
756 #[case("1.123456789", 9)]
757 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
758 let qty = Quantity::from(input);
759 assert_eq!(qty.precision, expected_prec);
760 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
761 }
762
763 #[rstest]
764 #[should_panic]
765 fn test_from_str_invalid_input() {
766 let input = "invalid";
767 Quantity::new(f64::from_str(input).unwrap(), 8);
768 }
769
770 #[rstest]
771 fn test_add() {
772 let a = 1.0;
773 let b = 2.0;
774 let quantity1 = Quantity::new(1.0, 0);
775 let quantity2 = Quantity::new(2.0, 0);
776 let quantity3 = quantity1 + quantity2;
777 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
778 }
779
780 #[rstest]
781 fn test_sub() {
782 let a = 3.0;
783 let b = 2.0;
784 let quantity1 = Quantity::new(a, 0);
785 let quantity2 = Quantity::new(b, 0);
786 let quantity3 = quantity1 - quantity2;
787 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
788 }
789
790 #[rstest]
791 fn test_add_assign() {
792 let a = 1.0;
793 let b = 2.0;
794 let mut quantity1 = Quantity::new(a, 0);
795 let quantity2 = Quantity::new(b, 0);
796 quantity1 += quantity2;
797 assert_eq!(quantity1.raw, Quantity::new(a + b, 0).raw);
798 }
799
800 #[rstest]
801 fn test_sub_assign() {
802 let a = 3.0;
803 let b = 2.0;
804 let mut quantity1 = Quantity::new(a, 0);
805 let quantity2 = Quantity::new(b, 0);
806 quantity1 -= quantity2;
807 assert_eq!(quantity1.raw, Quantity::new(a - b, 0).raw);
808 }
809
810 #[rstest]
811 fn test_mul() {
812 let value = 2.0;
813 let quantity1 = Quantity::new(value, 1);
814 let quantity2 = Quantity::new(value, 1);
815 let quantity3 = quantity1 * quantity2;
816 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
817 }
818
819 #[rstest]
820 fn test_mul_assign() {
821 let mut quantity = Quantity::new(2.0, 0);
822 quantity *= 3u64; assert_eq!(quantity.raw, Quantity::new(6.0, 0).raw);
824
825 let mut fraction = Quantity::new(1.5, 2);
826 fraction *= 2u64; assert_eq!(fraction.raw, Quantity::new(3.0, 2).raw);
828 }
829
830 #[rstest]
831 fn test_comparisons() {
832 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
833 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
834 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
835 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
836 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
837 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
838 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
839 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
840 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
841 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
842 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
843 }
844
845 #[rstest]
846 fn test_debug() {
847 let quantity = Quantity::from_str("44.12").unwrap();
848 let result = format!("{quantity:?}");
849 assert_eq!(result, "Quantity(44.12)");
850 }
851
852 #[rstest]
853 fn test_display() {
854 let quantity = Quantity::from_str("44.12").unwrap();
855 let result = format!("{quantity}");
856 assert_eq!(result, "44.12");
857 }
858
859 #[rstest]
860 fn test_to_formatted_string() {
861 let qty = Quantity::new(1234.5678, 4);
862 let formatted = qty.to_formatted_string();
863 assert_eq!(formatted, "1_234.5678");
864 assert_eq!(qty.to_string(), "1234.5678");
865 }
866
867 #[rstest]
868 fn test_hash() {
869 use std::{
870 collections::hash_map::DefaultHasher,
871 hash::{Hash, Hasher},
872 };
873
874 let q1 = Quantity::new(100.0, 1);
875 let q2 = Quantity::new(100.0, 1);
876 let q3 = Quantity::new(200.0, 1);
877
878 let mut s1 = DefaultHasher::new();
879 let mut s2 = DefaultHasher::new();
880 let mut s3 = DefaultHasher::new();
881
882 q1.hash(&mut s1);
883 q2.hash(&mut s2);
884 q3.hash(&mut s3);
885
886 assert_eq!(
887 s1.finish(),
888 s2.finish(),
889 "Equal quantities must hash equally"
890 );
891 assert_ne!(
892 s1.finish(),
893 s3.finish(),
894 "Different quantities must hash differently"
895 );
896 }
897
898 #[rstest]
899 fn test_quantity_serde_json_round_trip() {
900 let original = Quantity::new(123.456, 3);
901 let json_str = serde_json::to_string(&original).unwrap();
902 assert_eq!(json_str, "\"123.456\"");
903
904 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
905 assert_eq!(deserialized, original);
906 assert_eq!(deserialized.precision, 3);
907 }
908}