scryfall/card/
price.rs

1//! Module defining a price object containing data in various currencies.
2use std::cmp::Ordering;
3
4use serde::{Deserialize, Serialize};
5
6/// Struct defining a price object containing data in various currencies.
7#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, Default)]
8#[cfg_attr(test, serde(deny_unknown_fields))]
9#[allow(missing_docs)]
10#[non_exhaustive]
11pub struct Price {
12    pub usd: Option<String>,
13    pub usd_foil: Option<String>,
14    pub eur: Option<String>,
15    pub eur_foil: Option<String>,
16    pub tix: Option<String>,
17    pub usd_etched: Option<String>,
18}
19
20impl Price {
21    /// Creates an array of component prices that can be iterated over.
22    fn to_array(&self) -> [&Option<String>; 5] {
23        [
24            &self.usd,
25            &self.usd_foil,
26            &self.eur,
27            &self.eur_foil,
28            &self.tix,
29        ]
30    }
31}
32
33/// Compares two prices as floating-point numbers.
34fn compare_prices(a: &Option<String>, b: &Option<String>) -> Option<Ordering> {
35    if let (Some(a), Some(b)) = (a, b) {
36        if let (Ok(a), Ok(b)) = (a.parse::<f32>(), b.parse()) {
37            return a.partial_cmp(&b);
38        }
39    }
40    None
41}
42
43impl PartialOrd for Price {
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        let mut result = None;
46        for (a, b) in self.to_array().iter().zip(other.to_array().iter()) {
47            match (result, compare_prices(a, b)) {
48                // If either ordering is `None`, use the other. Then if either is `Some(Equal)`,
49                // use the other.
50                (None, order)
51                | (order, None)
52                | (Some(Ordering::Equal), order)
53                | (order, Some(Ordering::Equal)) => {
54                    result = order;
55                },
56                // If the two orderings already agree, do nothing.
57                (Some(a), Some(b)) if a == b => {},
58                // Otherwise, they disagree, so these prices cannot be ordered.
59                _ => return None,
60            }
61        }
62        result
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn no_prices() {
72        let a = Price::default();
73        let b = Price::default();
74
75        assert_eq!(a.partial_cmp(&b), None);
76    }
77
78    #[test]
79    fn prices_agree() {
80        let a = Price {
81            usd: Some("5".to_string()),
82            usd_foil: Some("8".to_string()),
83            eur: Some("3".to_string()),
84            ..Default::default()
85        };
86        let b = Price {
87            usd: Some("10".to_string()),
88            usd_foil: Some("14".to_string()),
89            tix: Some("1".to_string()),
90            ..Default::default()
91        };
92
93        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
94    }
95
96    #[test]
97    fn prices_disagree() {
98        let a = Price {
99            usd: Some("0.1".to_string()),
100            tix: Some("15".to_string()),
101            ..Default::default()
102        };
103        let b = Price {
104            usd: Some("2".to_string()),
105            tix: Some(".5".to_string()),
106            ..Default::default()
107        };
108
109        assert_eq!(a.partial_cmp(&b), None);
110    }
111
112    #[test]
113    fn prices_equal() {
114        let a = Price {
115            usd: Some("3.99".to_string()),
116            tix: Some("2.1".to_string()),
117            ..Default::default()
118        };
119        let b = Price {
120            usd: Some("3.99".to_string()),
121            eur: Some("4.20".to_string()),
122            ..Default::default()
123        };
124
125        assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
126    }
127}