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