1mod ops;
20
21use std::{
22 fmt::{self, Display, Formatter},
23 ops::Neg,
24};
25
26#[derive(Debug, Copy, Clone, Default)]
28pub struct CalendarDuration {
29 pub negative: bool,
30 pub years: u16,
31 pub months: u8,
32 pub weeks: u32,
33 pub days: u32,
34 pub hours: u32,
35 pub minutes: u32,
36 pub seconds: u32,
37}
38
39impl CalendarDuration {
40 fn set_unit_value(&mut self, value_str: &str, unit_str: &str) {
41 match unit_str {
42 "y" => self.years = value_str.parse().unwrap_or(0),
43 "mon" => self.months = value_str.parse().unwrap_or(0),
44 "w" => self.weeks = value_str.parse().unwrap_or(0),
45 "d" => self.days = value_str.parse().unwrap_or(0),
46 "h" => self.hours = value_str.parse().unwrap_or(0),
47 "m" => self.minutes = value_str.parse().unwrap_or(0),
48 "s" => self.seconds = value_str.parse().unwrap_or(0),
49 _ => (),
50 }
51 }
52
53 fn actual_unit_val<T: Neg<Output = T>>(&self, value: T) -> T {
54 match self.negative {
55 true => -value,
56 false => value,
57 }
58 }
59
60 pub fn is_zero(&self) -> bool {
62 self.years == 0
63 && self.months == 0
64 && self.weeks == 0
65 && self.days == 0
66 && self.hours == 0
67 && self.minutes == 0
68 && self.seconds == 0
69 }
70}
71
72fn format_unit_segment(segments: &mut Vec<String>, value: u32, unit: &str) {
73 if value > 0 {
74 segments.push(format!(
75 "{} {}{}",
76 value,
77 unit,
78 if value > 1 { "s" } else { "" }
79 ));
80 }
81}
82
83impl Display for CalendarDuration {
84 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
85 let mut segments = Vec::new();
86 format_unit_segment(&mut segments, self.years.into(), "year");
87 format_unit_segment(&mut segments, self.months.into(), "month");
88 format_unit_segment(&mut segments, self.weeks, "week");
89 format_unit_segment(&mut segments, self.days, "day");
90 format_unit_segment(&mut segments, self.hours, "hour");
91 format_unit_segment(&mut segments, self.minutes, "minute");
92 format_unit_segment(&mut segments, self.seconds, "second");
93 if segments.len() >= 3 {
94 let combined_comma_segments = segments[0..(segments.len() - 1)].join(", ");
96 write!(
97 f,
98 "{} and {}",
99 combined_comma_segments,
100 segments.last().unwrap()
101 )
102 } else {
103 write!(f, "{}", segments.join(" and "))
105 }
106 }
107}
108
109impl From<&str> for CalendarDuration {
110 fn from(s: &str) -> Self {
111 let mut result = Self::default();
112 let mut value_str = String::new();
113 let mut unit_str = String::new();
114 for (i, ch) in s.chars().enumerate() {
115 if i == 0 && ch == '-' {
116 result.negative = true;
117 }
118 if ch.is_alphabetic() {
119 unit_str.push(ch);
121 } else if ch.is_numeric() {
122 if !unit_str.is_empty() {
127 result.set_unit_value(&value_str, &unit_str);
128 value_str.clear();
129 unit_str.clear();
130 }
131 value_str.push(ch);
132 }
133 }
134 result.set_unit_value(&value_str, &unit_str);
135 result
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::CalendarDuration;
142
143 fn assert_parse_and_display_eq(formatted: &str, displayed: &str) {
144 assert_eq!(CalendarDuration::from(formatted).to_string(), displayed);
145 }
146
147 #[test]
148 fn parse_and_display() {
149 assert_parse_and_display_eq("10s", "10 seconds");
150 assert_parse_and_display_eq("40m1s", "40 minutes and 1 second");
151 assert_parse_and_display_eq("1h20m41s", "1 hour, 20 minutes and 41 seconds");
152 assert_parse_and_display_eq("5y2mon3w6h", "5 years, 2 months, 3 weeks and 6 hours");
153 }
154
155 #[test]
156 fn parse_pos_neg() {
157 let duration = CalendarDuration::from("15s");
158 assert_eq!(duration.seconds, 15);
159 assert!(!duration.negative);
160
161 let duration = CalendarDuration::from("-10s");
162 assert_eq!(duration.seconds, 10);
163 assert!(duration.negative);
164 }
165
166 #[test]
167 fn is_zero() {
168 assert!(CalendarDuration::default().is_zero());
169 assert!(!CalendarDuration {
170 seconds: 1,
171 ..Default::default()
172 }
173 .is_zero());
174 }
175}