Skip to main content

dataprof_core/
serde_helpers.rs

1//! Custom serde serialization helpers for formatting numeric values with appropriate precision.
2
3use serde::Serializer;
4
5/// Round f64 to 2 decimal places (for percentages and simple ratios).
6/// Returns null for NaN or infinite values.
7pub fn round_2<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
8where
9    S: Serializer,
10{
11    if !value.is_finite() {
12        return serializer.serialize_none();
13    }
14    serializer.serialize_f64((value * 100.0).round() / 100.0)
15}
16
17/// Round f64 to 4 decimal places (for statistical metrics like mean, std_dev).
18/// Returns null for NaN or infinite values.
19pub fn round_4<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
20where
21    S: Serializer,
22{
23    if !value.is_finite() {
24        return serializer.serialize_none();
25    }
26    serializer.serialize_f64((value * 10000.0).round() / 10000.0)
27}
28
29/// Round `Option<f64>` to 2 decimal places.
30/// Returns null for None or non-finite values.
31pub fn round_2_opt<S>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
32where
33    S: Serializer,
34{
35    match value {
36        Some(v) if v.is_finite() => {
37            let rounded = (v * 100.0).round() / 100.0;
38            serializer.serialize_some(&rounded)
39        }
40        _ => serializer.serialize_none(),
41    }
42}
43
44/// Round `Option<f64>` to 4 decimal places.
45/// Returns null for None or non-finite values.
46pub fn round_4_opt<S>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
47where
48    S: Serializer,
49{
50    match value {
51        Some(v) if v.is_finite() => {
52            let rounded = (v * 10000.0).round() / 10000.0;
53            serializer.serialize_some(&rounded)
54        }
55        _ => serializer.serialize_none(),
56    }
57}
58
59/// Round Quartiles fields to 2 decimal places.
60pub mod quartiles {
61    use super::*;
62    use crate::profile::Quartiles;
63
64    pub fn serialize<S>(value: &Option<Quartiles>, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: Serializer,
67    {
68        match value {
69            Some(q) => {
70                use serde::Serialize;
71
72                #[derive(Serialize)]
73                struct RoundedQuartiles {
74                    #[serde(serialize_with = "round_2")]
75                    q1: f64,
76                    #[serde(serialize_with = "round_2")]
77                    q2: f64,
78                    #[serde(serialize_with = "round_2")]
79                    q3: f64,
80                    #[serde(serialize_with = "round_2")]
81                    iqr: f64,
82                }
83
84                let rounded = RoundedQuartiles {
85                    q1: q.q1,
86                    q2: q.q2,
87                    q3: q.q3,
88                    iqr: q.iqr,
89                };
90                serializer.serialize_some(&rounded)
91            }
92            None => serializer.serialize_none(),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::profile::Quartiles;
101    use serde::Serialize;
102    use serde_json::json;
103
104    #[derive(Serialize)]
105    struct Round2Value {
106        #[serde(serialize_with = "round_2")]
107        value: f64,
108    }
109
110    #[derive(Serialize)]
111    struct Round4Value {
112        #[serde(serialize_with = "round_4")]
113        value: f64,
114    }
115
116    #[derive(Serialize)]
117    struct Round2OptValue {
118        #[serde(serialize_with = "round_2_opt")]
119        value: Option<f64>,
120    }
121
122    #[derive(Serialize)]
123    struct Round4OptValue {
124        #[serde(serialize_with = "round_4_opt")]
125        value: Option<f64>,
126    }
127
128    #[derive(Serialize)]
129    struct QuartilesValue {
130        #[serde(serialize_with = "quartiles::serialize")]
131        value: Option<Quartiles>,
132    }
133
134    #[test]
135    fn test_round_2_serializes_rounded_value() {
136        let value = Round2Value { value: 12.345 };
137        let json = serde_json::to_value(value).unwrap();
138        assert_eq!(json, json!({ "value": 12.35 }));
139    }
140
141    #[test]
142    fn test_round_4_serializes_rounded_value() {
143        let value = Round4Value { value: 12.34567 };
144        let json = serde_json::to_value(value).unwrap();
145        assert_eq!(json, json!({ "value": 12.3457 }));
146    }
147
148    #[test]
149    fn test_non_finite_values_serialize_as_null() {
150        let round_2_json = serde_json::to_value(Round2Value { value: f64::NAN }).unwrap();
151        let round_4_json = serde_json::to_value(Round4Value {
152            value: f64::INFINITY,
153        })
154        .unwrap();
155
156        assert_eq!(round_2_json, json!({ "value": null }));
157        assert_eq!(round_4_json, json!({ "value": null }));
158    }
159
160    #[test]
161    fn test_optional_rounders_handle_some_none_and_non_finite() {
162        let rounded_2 = serde_json::to_value(Round2OptValue { value: Some(9.876) }).unwrap();
163        let rounded_4 = serde_json::to_value(Round4OptValue {
164            value: Some(9.87654),
165        })
166        .unwrap();
167        let none_2 = serde_json::to_value(Round2OptValue { value: None }).unwrap();
168        let nan_4 = serde_json::to_value(Round4OptValue {
169            value: Some(f64::NAN),
170        })
171        .unwrap();
172
173        assert_eq!(rounded_2, json!({ "value": 9.88 }));
174        assert_eq!(rounded_4, json!({ "value": 9.8765 }));
175        assert_eq!(none_2, json!({ "value": null }));
176        assert_eq!(nan_4, json!({ "value": null }));
177    }
178
179    #[test]
180    fn test_quartiles_serializer_rounds_each_field() {
181        let value = QuartilesValue {
182            value: Some(Quartiles {
183                q1: 1.234,
184                q2: 2.345,
185                q3: 3.456,
186                iqr: 2.222,
187            }),
188        };
189        let json = serde_json::to_value(value).unwrap();
190
191        assert_eq!(
192            json,
193            json!({
194                "value": {
195                    "q1": 1.23,
196                    "q2": 2.35,
197                    "q3": 3.46,
198                    "iqr": 2.22
199                }
200            })
201        );
202    }
203
204    #[test]
205    fn test_quartiles_serializer_handles_none() {
206        let value = QuartilesValue { value: None };
207        let json = serde_json::to_value(value).unwrap();
208        assert_eq!(json, json!({ "value": null }));
209    }
210}