1use crate::date::Date;
2use crate::time::Time;
3use crate::date_time::DateTime;
4use crate::date_error::{DateError, DateErrorKind};
5
6#[derive(Debug, Clone)]
8pub struct FormatString {
9 parts: Vec<FormatPart>,
10}
11
12#[derive(Debug, Clone)]
13enum FormatPart {
14 Literal(String),
15 Specifier(FormatSpecifier),
16}
17
18#[derive(Debug, Clone)]
19enum FormatSpecifier {
20 Year, YearShort, Month, MonthName, MonthNameShort, Day, DayOfYear, Weekday, WeekdayShort, WeekdayNum, Hour24, Hour12, Minute, Second, Microsecond, AmPm, Timezone, TimezoneColon, Date, Time, DateTime, Iso8601, }
50
51impl FormatString {
52 pub fn new(format: &str) -> Result<Self, DateError> {
54 let mut parts = Vec::new();
55 let mut chars = format.chars().peekable();
56 let mut literal = String::new();
57
58 while let Some(ch) = chars.next() {
59 if ch == '%' {
60 if !literal.is_empty() {
61 parts.push(FormatPart::Literal(literal.clone()));
62 literal.clear();
63 }
64 let spec = match chars.next() {
65 Some('Y') => FormatSpecifier::Year,
66 Some('y') => FormatSpecifier::YearShort,
67 Some('m') => FormatSpecifier::Month,
68 Some('B') => FormatSpecifier::MonthName,
69 Some('b') => FormatSpecifier::MonthNameShort,
70 Some('d') => FormatSpecifier::Day,
71 Some('j') => FormatSpecifier::DayOfYear,
72 Some('A') => FormatSpecifier::Weekday,
73 Some('a') => FormatSpecifier::WeekdayShort,
74 Some('w') => FormatSpecifier::WeekdayNum,
75 Some('H') => FormatSpecifier::Hour24,
76 Some('I') => FormatSpecifier::Hour12,
77 Some('M') => FormatSpecifier::Minute,
78 Some('S') => FormatSpecifier::Second,
79 Some('f') => FormatSpecifier::Microsecond,
80 Some('p') => FormatSpecifier::AmPm,
81 Some('z') => FormatSpecifier::Timezone,
82 Some(':') if chars.peek() == Some(&'z') => {
83 chars.next(); FormatSpecifier::TimezoneColon
85 }
86 Some('D') => FormatSpecifier::Date,
87 Some('T') => FormatSpecifier::Time,
88 Some('F') => FormatSpecifier::DateTime,
89 Some('+') => FormatSpecifier::Iso8601,
90 Some('%') => {
91 literal.push('%');
92 continue;
93 }
94 Some(_) => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
95 None => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
96 };
97 parts.push(FormatPart::Specifier(spec));
98 } else {
99 literal.push(ch);
100 }
101 }
102
103 if !literal.is_empty() {
104 parts.push(FormatPart::Literal(literal));
105 }
106
107 Ok(FormatString { parts })
108 }
109
110 pub fn format_datetime(&self, dt: &DateTime) -> String {
112 let mut result = String::new();
113 for part in &self.parts {
114 match part {
115 FormatPart::Literal(s) => result.push_str(s),
116 FormatPart::Specifier(spec) => {
117 result.push_str(&self.format_specifier(spec, dt));
118 }
119 }
120 }
121 result
122 }
123
124 pub fn format_date(&self, date: &Date) -> String {
126 let mut result = String::new();
127 for part in &self.parts {
128 match part {
129 FormatPart::Literal(s) => result.push_str(s),
130 FormatPart::Specifier(spec) => {
131 result.push_str(&self.format_specifier_date(spec, date));
132 }
133 }
134 }
135 result
136 }
137
138 pub fn format_time(&self, time: &Time) -> String {
140 let mut result = String::new();
141 for part in &self.parts {
142 match part {
143 FormatPart::Literal(s) => result.push_str(s),
144 FormatPart::Specifier(spec) => {
145 result.push_str(&self.format_specifier_time(spec, time));
146 }
147 }
148 }
149 result
150 }
151
152 fn format_specifier(&self, spec: &FormatSpecifier, dt: &DateTime) -> String {
153 match spec {
154 FormatSpecifier::Year => format!("{:04}", dt.date.year),
155 FormatSpecifier::YearShort => format!("{:02}", dt.date.year % 100),
156 FormatSpecifier::Month => format!("{:02}", dt.date.month),
157 FormatSpecifier::MonthName => self.month_name(dt.date.month),
158 FormatSpecifier::MonthNameShort => self.month_name_short(dt.date.month),
159 FormatSpecifier::Day => format!("{:02}", dt.date.day),
160 FormatSpecifier::DayOfYear => format!("{:03}", dt.date.year_day()),
161 FormatSpecifier::Weekday => self.weekday_name(&dt.date),
162 FormatSpecifier::WeekdayShort => self.weekday_name_short(&dt.date),
163 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(&dt.date)),
164 FormatSpecifier::Hour24 => format!("{:02}", dt.time.hour),
165 FormatSpecifier::Hour12 => {
166 let hour = if dt.time.hour == 0 { 12 } else if dt.time.hour > 12 { dt.time.hour - 12 } else { dt.time.hour };
167 format!("{:02}", hour)
168 }
169 FormatSpecifier::Minute => format!("{:02}", dt.time.minute),
170 FormatSpecifier::Second => format!("{:02}", dt.time.second),
171 FormatSpecifier::Microsecond => format!("{:06}", dt.time.microsecond),
172 FormatSpecifier::AmPm => if dt.time.hour < 12 { "AM" } else { "PM" }.to_string(),
173 FormatSpecifier::Timezone => self.format_timezone(dt.shift_minutes, false),
174 FormatSpecifier::TimezoneColon => self.format_timezone(dt.shift_minutes, true),
175 FormatSpecifier::Date => format!("{:02}/{:02}/{:02}", dt.date.month, dt.date.day, dt.date.year % 100),
176 FormatSpecifier::Time => format!("{:02}:{:02}:{:02}", dt.time.hour, dt.time.minute, dt.time.second),
177 FormatSpecifier::DateTime => format!("{:04}-{:02}-{:02}", dt.date.year, dt.date.month, dt.date.day),
178 FormatSpecifier::Iso8601 => dt.to_iso_8061(),
179 }
180 }
181
182 fn format_specifier_date(&self, spec: &FormatSpecifier, date: &Date) -> String {
183 match spec {
184 FormatSpecifier::Year => format!("{:04}", date.year),
185 FormatSpecifier::YearShort => format!("{:02}", date.year % 100),
186 FormatSpecifier::Month => format!("{:02}", date.month),
187 FormatSpecifier::MonthName => self.month_name(date.month),
188 FormatSpecifier::MonthNameShort => self.month_name_short(date.month),
189 FormatSpecifier::Day => format!("{:02}", date.day),
190 FormatSpecifier::DayOfYear => format!("{:03}", date.year_day()),
191 FormatSpecifier::Weekday => self.weekday_name(date),
192 FormatSpecifier::WeekdayShort => self.weekday_name_short(date),
193 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(date)),
194 FormatSpecifier::DateTime => format!("{:04}-{:02}-{:02}", date.year, date.month, date.day),
195 FormatSpecifier::Date => format!("{:02}/{:02}/{:02}", date.month, date.day, date.year % 100),
196 _ => String::new(), }
198 }
199
200 fn format_specifier_time(&self, spec: &FormatSpecifier, time: &Time) -> String {
201 match spec {
202 FormatSpecifier::Hour24 => format!("{:02}", time.hour),
203 FormatSpecifier::Hour12 => {
204 let hour = if time.hour == 0 { 12 } else if time.hour > 12 { time.hour - 12 } else { time.hour };
205 format!("{:02}", hour)
206 }
207 FormatSpecifier::Minute => format!("{:02}", time.minute),
208 FormatSpecifier::Second => format!("{:02}", time.second),
209 FormatSpecifier::Microsecond => format!("{:06}", time.microsecond),
210 FormatSpecifier::AmPm => if time.hour < 12 { "AM" } else { "PM" }.to_string(),
211 FormatSpecifier::Time => format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second),
212 _ => String::new(), }
214 }
215
216 fn month_name(&self, month: u64) -> String {
217 match month {
218 1 => "January".to_string(),
219 2 => "February".to_string(),
220 3 => "March".to_string(),
221 4 => "April".to_string(),
222 5 => "May".to_string(),
223 6 => "June".to_string(),
224 7 => "July".to_string(),
225 8 => "August".to_string(),
226 9 => "September".to_string(),
227 10 => "October".to_string(),
228 11 => "November".to_string(),
229 12 => "December".to_string(),
230 _ => "Unknown".to_string(),
231 }
232 }
233
234 fn month_name_short(&self, month: u64) -> String {
235 match month {
236 1 => "Jan".to_string(),
237 2 => "Feb".to_string(),
238 3 => "Mar".to_string(),
239 4 => "Apr".to_string(),
240 5 => "May".to_string(),
241 6 => "Jun".to_string(),
242 7 => "Jul".to_string(),
243 8 => "Aug".to_string(),
244 9 => "Sep".to_string(),
245 10 => "Oct".to_string(),
246 11 => "Nov".to_string(),
247 12 => "Dec".to_string(),
248 _ => "Unknown".to_string(),
249 }
250 }
251
252 fn weekday_name(&self, date: &Date) -> String {
253 if date.is_sunday() { "Sunday".to_string() }
254 else if date.is_monday() { "Monday".to_string() }
255 else if date.is_tuesday() { "Tuesday".to_string() }
256 else if date.is_wednesday() { "Wednesday".to_string() }
257 else if date.is_thursday() { "Thursday".to_string() }
258 else if date.is_friday() { "Friday".to_string() }
259 else if date.is_saturday() { "Saturday".to_string() }
260 else { "Unknown".to_string() }
261 }
262
263 fn weekday_name_short(&self, date: &Date) -> String {
264 if date.is_sunday() { "Sun".to_string() }
265 else if date.is_monday() { "Mon".to_string() }
266 else if date.is_tuesday() { "Tue".to_string() }
267 else if date.is_wednesday() { "Wed".to_string() }
268 else if date.is_thursday() { "Thu".to_string() }
269 else if date.is_friday() { "Fri".to_string() }
270 else if date.is_saturday() { "Sat".to_string() }
271 else { "Unknown".to_string() }
272 }
273
274 fn weekday_number(&self, date: &Date) -> u64 {
275 if date.is_sunday() { 0 }
276 else if date.is_monday() { 1 }
277 else if date.is_tuesday() { 2 }
278 else if date.is_wednesday() { 3 }
279 else if date.is_thursday() { 4 }
280 else if date.is_friday() { 5 }
281 else if date.is_saturday() { 6 }
282 else { 0 }
283 }
284
285 fn format_timezone(&self, shift_minutes: isize, with_colon: bool) -> String {
286 if shift_minutes == 0 {
287 return "Z".to_string();
288 }
289
290 let abs_minutes = shift_minutes.abs() as u64;
291 let hours = abs_minutes / 60;
292 let minutes = abs_minutes % 60;
293
294 let sign = if shift_minutes > 0 { "+" } else { "-" };
295
296 if with_colon {
297 format!("{}{:02}:{:02}", sign, hours, minutes)
298 } else {
299 format!("{}{:02}{:02}", sign, hours, minutes)
300 }
301 }
302}
303
304pub trait Format {
306 fn format(&self, format: &str) -> Result<String, DateError>;
308}
309
310impl Format for DateTime {
311 fn format(&self, format: &str) -> Result<String, DateError> {
312 let format_string = FormatString::new(format)?;
313 Ok(format_string.format_datetime(self))
314 }
315}
316
317impl Format for Date {
318 fn format(&self, format: &str) -> Result<String, DateError> {
319 let format_string = FormatString::new(format)?;
320 Ok(format_string.format_date(self))
321 }
322}
323
324impl Format for Time {
325 fn format(&self, format: &str) -> Result<String, DateError> {
326 let format_string = FormatString::new(format)?;
327 Ok(format_string.format_time(self))
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_format_string_parsing() {
337 let format = FormatString::new("%Y-%m-%d %H:%M:%S").unwrap();
338 assert_eq!(format.parts.len(), 11); let format = FormatString::new("Date: %Y-%m-%d").unwrap();
341 assert_eq!(format.parts.len(), 6); }
343
344 #[test]
345 fn test_datetime_formatting() {
346 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
347
348 assert_eq!(dt.format("%Y-%m-%d").unwrap(), "2023-06-15");
349 assert_eq!(dt.format("%H:%M:%S").unwrap(), "14:30:45");
350 assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").unwrap(), "2023-06-15 14:30:45");
351 assert_eq!(dt.format("%B %d, %Y").unwrap(), "June 15, 2023");
352 assert_eq!(dt.format("%A, %B %d, %Y").unwrap(), "Thursday, June 15, 2023");
353 assert_eq!(dt.format("%I:%M %p").unwrap(), "02:30 PM");
354 }
355
356 #[test]
357 fn test_date_formatting() {
358 let date = Date::new(2023, 6, 15);
359
360 assert_eq!(date.format("%Y-%m-%d").unwrap(), "2023-06-15");
361 assert_eq!(date.format("%B %d, %Y").unwrap(), "June 15, 2023");
362 assert_eq!(date.format("%A, %B %d, %Y").unwrap(), "Thursday, June 15, 2023");
363 assert_eq!(date.format("%j").unwrap(), "166"); }
365
366 #[test]
367 fn test_time_formatting() {
368 let time = Time::new(14, 30, 45);
369
370 assert_eq!(time.format("%H:%M:%S").unwrap(), "14:30:45");
371 assert_eq!(time.format("%I:%M %p").unwrap(), "02:30 PM");
372 assert_eq!(time.format("%T").unwrap(), "14:30:45");
373 }
374
375 #[test]
376 fn test_timezone_formatting() {
377 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 120);
378
379 assert_eq!(dt.format("%z").unwrap(), "+0200");
380 assert_eq!(dt.format("%:z").unwrap(), "+02:00");
381
382 let dt_utc = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
383 assert_eq!(dt_utc.format("%z").unwrap(), "Z");
384 }
385
386 #[test]
387 fn test_microsecond_formatting() {
388 let time = Time::new_with_microseconds(14, 30, 45, 123456);
389
390 assert_eq!(time.format("%H:%M:%S.%f").unwrap(), "14:30:45.123456");
391 assert_eq!(time.format("%T.%f").unwrap(), "14:30:45.123456");
392 }
393
394 #[test]
395 fn test_iso8601_formatting() {
396 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
397
398 assert_eq!(dt.format("%+").unwrap(), "2023-06-15T14:30:45Z");
399 }
400
401 #[test]
402 fn test_escaped_percent() {
403 let format = FormatString::new("100%% complete").unwrap();
404 assert_eq!(format.parts.len(), 2);
405
406 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
407 assert_eq!(dt.format("100%% complete").unwrap(), "100% complete");
408 }
409
410 #[test]
411 fn test_invalid_format() {
412 assert!(FormatString::new("%").is_err());
413 assert!(FormatString::new("%X").is_err());
414 }
415
416}