Skip to main content

bids_core/
padded_int.rs

1//! Zero-padded integer type that preserves original formatting.
2//!
3//! BIDS uses zero-padded integers in entity values (e.g., `sub-01`, `run-002`).
4//! [`PaddedInt`] stores both the numeric value and the original string so that
5//! comparisons use the number but display preserves the padding.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Integer type that preserves zero-padding.
11///
12/// Acts like an i64 in comparisons and arithmetic, but string formatting
13/// preserves the original zero-padding.
14///
15/// ```
16/// use bids_core::PaddedInt;
17///
18/// let p = PaddedInt::new("02");
19/// assert_eq!(p.value(), 2);
20/// assert_eq!(p.to_string(), "02");
21/// assert_eq!(p, PaddedInt::from(2));
22/// ```
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct PaddedInt {
25    value: i64,
26    formatted: String,
27}
28
29impl PaddedInt {
30    /// Parse a zero-padded integer string (e.g., `"02"`, `"001"`).
31    pub fn new(s: &str) -> Self {
32        let value = s.parse::<i64>().unwrap_or(0);
33        Self {
34            value,
35            formatted: s.to_string(),
36        }
37    }
38
39    /// The numeric value (ignoring padding).
40    #[must_use]
41    pub fn value(&self) -> i64 {
42        self.value
43    }
44}
45
46impl From<i64> for PaddedInt {
47    fn from(v: i64) -> Self {
48        Self {
49            value: v,
50            formatted: v.to_string(),
51        }
52    }
53}
54
55impl From<i32> for PaddedInt {
56    fn from(v: i32) -> Self {
57        Self::from(v as i64)
58    }
59}
60
61impl fmt::Display for PaddedInt {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "{}", self.formatted)
64    }
65}
66
67impl PartialEq for PaddedInt {
68    fn eq(&self, other: &Self) -> bool {
69        self.value == other.value
70    }
71}
72
73impl Eq for PaddedInt {}
74
75impl PartialOrd for PaddedInt {
76    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81impl Ord for PaddedInt {
82    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
83        self.value.cmp(&other.value)
84    }
85}
86
87impl std::hash::Hash for PaddedInt {
88    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
89        self.value.hash(state);
90    }
91}
92
93impl PartialEq<i64> for PaddedInt {
94    fn eq(&self, other: &i64) -> bool {
95        self.value == *other
96    }
97}
98
99impl PartialEq<PaddedInt> for i64 {
100    fn eq(&self, other: &PaddedInt) -> bool {
101        *self == other.value
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_padded_int() {
111        let p = PaddedInt::new("02");
112        assert_eq!(p.value(), 2);
113        assert_eq!(p.to_string(), "02");
114        assert_eq!(p, PaddedInt::from(2));
115        assert!(p == 2i64);
116
117        let p1 = PaddedInt::new("001");
118        let p2 = PaddedInt::new("01");
119        assert_eq!(p1, p2);
120        assert_eq!(p1.to_string(), "001");
121        assert_eq!(p2.to_string(), "01");
122    }
123}