1use crate::date::Date;
2use crate::date_error::{DateError, DateErrorKind};
3use crate::date_time::DateTime;
4use crate::time::Time;
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, FractionalSecond(u8), AmPm, Timezone, TimezoneColon, Date, Time, DateTime, Iso8601, }
51
52impl FormatString {
53 pub fn new(format: &str) -> Result<Self, DateError> {
55 let mut parts = Vec::new();
56 let mut chars = format.chars().peekable();
57 let mut literal = String::new();
58
59 while let Some(ch) = chars.next() {
60 if ch == '%' {
61 if !literal.is_empty() {
62 parts.push(FormatPart::Literal(literal.clone()));
63 literal.clear();
64 }
65 let spec = match chars.next() {
66 Some('Y') => FormatSpecifier::Year,
67 Some('y') => FormatSpecifier::YearShort,
68 Some('m') => FormatSpecifier::Month,
69 Some('B') => FormatSpecifier::MonthName,
70 Some('b') => FormatSpecifier::MonthNameShort,
71 Some('d') => FormatSpecifier::Day,
72 Some('j') => FormatSpecifier::DayOfYear,
73 Some('A') => FormatSpecifier::Weekday,
74 Some('a') => FormatSpecifier::WeekdayShort,
75 Some('w') => FormatSpecifier::WeekdayNum,
76 Some('H') => FormatSpecifier::Hour24,
77 Some('I') => FormatSpecifier::Hour12,
78 Some('M') => FormatSpecifier::Minute,
79 Some('S') => FormatSpecifier::Second,
80 Some('.') => {
81 let mut precision = 0u8;
83 let mut temp_chars = chars.clone();
84 let mut digit_count = 0;
85
86 while let Some(&digit) = temp_chars.peek() {
88 if digit.is_ascii_digit() {
89 precision = precision * 10 + (digit as u8 - b'0');
90 temp_chars.next();
91 digit_count += 1;
92 } else {
93 break;
94 }
95 }
96
97 if temp_chars.next() == Some('f') {
99 for _ in 0..digit_count {
101 chars.next();
102 }
103 chars.next(); if precision >= 1 && precision <= 6 {
107 FormatSpecifier::FractionalSecond(precision)
108 } else {
109 FormatSpecifier::Microsecond
110 }
111 } else {
112 literal.push('.');
114 continue;
115 }
116 }
117 Some('f') => FormatSpecifier::Microsecond,
118 Some('p') => FormatSpecifier::AmPm,
119 Some('z') => FormatSpecifier::Timezone,
120 Some(':') if chars.peek() == Some(&'z') => {
121 chars.next(); FormatSpecifier::TimezoneColon
123 }
124 Some('D') => FormatSpecifier::Date,
125 Some('T') => FormatSpecifier::Time,
126 Some('F') => FormatSpecifier::DateTime,
127 Some('+') => FormatSpecifier::Iso8601,
128 Some('%') => {
129 literal.push('%');
130 continue;
131 }
132 Some(_) => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
133 None => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
134 };
135 parts.push(FormatPart::Specifier(spec));
136 } else {
137 literal.push(ch);
138 }
139 }
140
141 if !literal.is_empty() {
142 parts.push(FormatPart::Literal(literal));
143 }
144
145 Ok(FormatString { parts })
146 }
147
148 pub fn format_datetime(&self, dt: &DateTime) -> String {
150 let mut result = String::new();
151 for part in &self.parts {
152 match part {
153 FormatPart::Literal(s) => result.push_str(s),
154 FormatPart::Specifier(spec) => {
155 result.push_str(&self.format_specifier(spec, dt));
156 }
157 }
158 }
159 result
160 }
161
162 pub fn format_date(&self, date: &Date) -> String {
164 let mut result = String::new();
165 for part in &self.parts {
166 match part {
167 FormatPart::Literal(s) => result.push_str(s),
168 FormatPart::Specifier(spec) => {
169 result.push_str(&self.format_specifier_date(spec, date));
170 }
171 }
172 }
173 result
174 }
175
176 pub fn format_time(&self, time: &Time) -> String {
178 let mut result = String::new();
179 for part in &self.parts {
180 match part {
181 FormatPart::Literal(s) => result.push_str(s),
182 FormatPart::Specifier(spec) => {
183 result.push_str(&self.format_specifier_time(spec, time));
184 }
185 }
186 }
187 result
188 }
189
190 fn format_specifier(&self, spec: &FormatSpecifier, dt: &DateTime) -> String {
191 match spec {
192 FormatSpecifier::Year => format!("{:04}", dt.date.year),
193 FormatSpecifier::YearShort => format!("{:02}", dt.date.year % 100),
194 FormatSpecifier::Month => format!("{:02}", dt.date.month),
195 FormatSpecifier::MonthName => self.month_name(dt.date.month),
196 FormatSpecifier::MonthNameShort => self.month_name_short(dt.date.month),
197 FormatSpecifier::Day => format!("{:02}", dt.date.day),
198 FormatSpecifier::DayOfYear => format!("{:03}", dt.date.year_day()),
199 FormatSpecifier::Weekday => self.weekday_name(&dt.date),
200 FormatSpecifier::WeekdayShort => self.weekday_name_short(&dt.date),
201 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(&dt.date)),
202 FormatSpecifier::Hour24 => format!("{:02}", dt.time.hour),
203 FormatSpecifier::Hour12 => {
204 let hour = if dt.time.hour == 0 {
205 12
206 } else if dt.time.hour > 12 {
207 dt.time.hour - 12
208 } else {
209 dt.time.hour
210 };
211 format!("{:02}", hour)
212 }
213 FormatSpecifier::Minute => format!("{:02}", dt.time.minute),
214 FormatSpecifier::Second => format!("{:02}", dt.time.second),
215 FormatSpecifier::Microsecond => format!(".{:06}", dt.time.microsecond),
216 FormatSpecifier::FractionalSecond(precision) => {
217 let divisor = 10u64.pow(6 - *precision as u32);
218 let value = dt.time.microsecond / divisor;
219 format!(".{:0width$}", value, width = *precision as usize)
220 }
221 FormatSpecifier::AmPm => if dt.time.hour < 12 { "AM" } else { "PM" }.to_string(),
222 FormatSpecifier::Timezone => self.format_timezone(dt.shift_minutes, false),
223 FormatSpecifier::TimezoneColon => self.format_timezone(dt.shift_minutes, true),
224 FormatSpecifier::Date => format!(
225 "{:02}/{:02}/{:02}",
226 dt.date.month,
227 dt.date.day,
228 dt.date.year % 100
229 ),
230 FormatSpecifier::Time => format!(
231 "{:02}:{:02}:{:02}",
232 dt.time.hour, dt.time.minute, dt.time.second
233 ),
234 FormatSpecifier::DateTime => format!(
235 "{:04}-{:02}-{:02}",
236 dt.date.year, dt.date.month, dt.date.day
237 ),
238 FormatSpecifier::Iso8601 => dt.to_iso_8061(),
239 }
240 }
241
242 fn format_specifier_date(&self, spec: &FormatSpecifier, date: &Date) -> String {
243 match spec {
244 FormatSpecifier::Year => format!("{:04}", date.year),
245 FormatSpecifier::YearShort => format!("{:02}", date.year % 100),
246 FormatSpecifier::Month => format!("{:02}", date.month),
247 FormatSpecifier::MonthName => self.month_name(date.month),
248 FormatSpecifier::MonthNameShort => self.month_name_short(date.month),
249 FormatSpecifier::Day => format!("{:02}", date.day),
250 FormatSpecifier::DayOfYear => format!("{:03}", date.year_day()),
251 FormatSpecifier::Weekday => self.weekday_name(date),
252 FormatSpecifier::WeekdayShort => self.weekday_name_short(date),
253 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(date)),
254 FormatSpecifier::DateTime => {
255 format!("{:04}-{:02}-{:02}", date.year, date.month, date.day)
256 }
257 FormatSpecifier::Date => {
258 format!("{:02}/{:02}/{:02}", date.month, date.day, date.year % 100)
259 }
260 _ => String::new(), }
262 }
263
264 fn format_specifier_time(&self, spec: &FormatSpecifier, time: &Time) -> String {
265 match spec {
266 FormatSpecifier::Hour24 => format!("{:02}", time.hour),
267 FormatSpecifier::Hour12 => {
268 let hour = if time.hour == 0 {
269 12
270 } else if time.hour > 12 {
271 time.hour - 12
272 } else {
273 time.hour
274 };
275 format!("{:02}", hour)
276 }
277 FormatSpecifier::Minute => format!("{:02}", time.minute),
278 FormatSpecifier::Second => format!("{:02}", time.second),
279 FormatSpecifier::Microsecond => format!(".{:06}", time.microsecond),
280 FormatSpecifier::FractionalSecond(precision) => {
281 let divisor = 10u64.pow(6 - *precision as u32);
282 let value = time.microsecond / divisor;
283 format!(".{:0width$}", value, width = *precision as usize)
284 }
285 FormatSpecifier::AmPm => if time.hour < 12 { "AM" } else { "PM" }.to_string(),
286 FormatSpecifier::Time => {
287 format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second)
288 }
289 _ => String::new(), }
291 }
292
293 fn month_name(&self, month: u64) -> String {
294 match month {
295 1 => "January".to_string(),
296 2 => "February".to_string(),
297 3 => "March".to_string(),
298 4 => "April".to_string(),
299 5 => "May".to_string(),
300 6 => "June".to_string(),
301 7 => "July".to_string(),
302 8 => "August".to_string(),
303 9 => "September".to_string(),
304 10 => "October".to_string(),
305 11 => "November".to_string(),
306 12 => "December".to_string(),
307 _ => "Unknown".to_string(),
308 }
309 }
310
311 fn month_name_short(&self, month: u64) -> String {
312 match month {
313 1 => "Jan".to_string(),
314 2 => "Feb".to_string(),
315 3 => "Mar".to_string(),
316 4 => "Apr".to_string(),
317 5 => "May".to_string(),
318 6 => "Jun".to_string(),
319 7 => "Jul".to_string(),
320 8 => "Aug".to_string(),
321 9 => "Sep".to_string(),
322 10 => "Oct".to_string(),
323 11 => "Nov".to_string(),
324 12 => "Dec".to_string(),
325 _ => "Unknown".to_string(),
326 }
327 }
328
329 fn weekday_name(&self, date: &Date) -> String {
330 if date.is_sunday() {
331 "Sunday".to_string()
332 } else if date.is_monday() {
333 "Monday".to_string()
334 } else if date.is_tuesday() {
335 "Tuesday".to_string()
336 } else if date.is_wednesday() {
337 "Wednesday".to_string()
338 } else if date.is_thursday() {
339 "Thursday".to_string()
340 } else if date.is_friday() {
341 "Friday".to_string()
342 } else if date.is_saturday() {
343 "Saturday".to_string()
344 } else {
345 "Unknown".to_string()
346 }
347 }
348
349 fn weekday_name_short(&self, date: &Date) -> String {
350 if date.is_sunday() {
351 "Sun".to_string()
352 } else if date.is_monday() {
353 "Mon".to_string()
354 } else if date.is_tuesday() {
355 "Tue".to_string()
356 } else if date.is_wednesday() {
357 "Wed".to_string()
358 } else if date.is_thursday() {
359 "Thu".to_string()
360 } else if date.is_friday() {
361 "Fri".to_string()
362 } else if date.is_saturday() {
363 "Sat".to_string()
364 } else {
365 "Unknown".to_string()
366 }
367 }
368
369 fn weekday_number(&self, date: &Date) -> u64 {
370 if date.is_sunday() {
371 0
372 } else if date.is_monday() {
373 1
374 } else if date.is_tuesday() {
375 2
376 } else if date.is_wednesday() {
377 3
378 } else if date.is_thursday() {
379 4
380 } else if date.is_friday() {
381 5
382 } else if date.is_saturday() {
383 6
384 } else {
385 0
386 }
387 }
388
389 fn format_timezone(&self, shift_minutes: isize, with_colon: bool) -> String {
390 if shift_minutes == 0 {
391 return "Z".to_string();
392 }
393
394 let abs_minutes = shift_minutes.abs() as u64;
395 let hours = abs_minutes / 60;
396 let minutes = abs_minutes % 60;
397
398 let sign = if shift_minutes > 0 { "+" } else { "-" };
399
400 if with_colon {
401 format!("{}{:02}:{:02}", sign, hours, minutes)
402 } else {
403 format!("{}{:02}{:02}", sign, hours, minutes)
404 }
405 }
406}
407
408pub trait Format {
410 fn format(&self, format: &str) -> Result<String, DateError>;
412}
413
414impl Format for DateTime {
415 fn format(&self, format: &str) -> Result<String, DateError> {
416 let format_string = FormatString::new(format)?;
417 Ok(format_string.format_datetime(self))
418 }
419}
420
421impl Format for Date {
422 fn format(&self, format: &str) -> Result<String, DateError> {
423 let format_string = FormatString::new(format)?;
424 Ok(format_string.format_date(self))
425 }
426}
427
428impl Format for Time {
429 fn format(&self, format: &str) -> Result<String, DateError> {
430 let format_string = FormatString::new(format)?;
431 Ok(format_string.format_time(self))
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_format_string_parsing() {
441 let format = FormatString::new("%Y-%m-%d %H:%M:%S").unwrap();
442 assert_eq!(format.parts.len(), 11); let format = FormatString::new("Date: %Y-%m-%d").unwrap();
445 assert_eq!(format.parts.len(), 6); }
447
448 #[test]
449 fn test_datetime_formatting() {
450 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
451
452 assert_eq!(dt.format("%Y-%m-%d").unwrap(), "2023-06-15");
453 assert_eq!(dt.format("%H:%M:%S").unwrap(), "14:30:45");
454 assert_eq!(
455 dt.format("%Y-%m-%d %H:%M:%S").unwrap(),
456 "2023-06-15 14:30:45"
457 );
458 assert_eq!(dt.format("%B %d, %Y").unwrap(), "June 15, 2023");
459 assert_eq!(
460 dt.format("%A, %B %d, %Y").unwrap(),
461 "Thursday, June 15, 2023"
462 );
463 assert_eq!(dt.format("%I:%M %p").unwrap(), "02:30 PM");
464 }
465
466 #[test]
467 fn test_datetime_millisecond_formatting() {
468 let time = Time::new_with_microseconds(14, 30, 45, 123456);
469 let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
470
471 assert_eq!(
472 dt.format("%Y-%m-%d %H:%M:%S%.4f").unwrap(),
473 "2023-06-15 14:30:45.1234"
474 );
475 }
476
477 #[test]
478 fn test_fractional_second_formatting() {
479 let time = Time::new_with_microseconds(14, 30, 45, 123456);
480 let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
481
482 assert_eq!(dt.format("%H:%M:%S%.1f").unwrap(), "14:30:45.1");
484 assert_eq!(dt.format("%H:%M:%S%.2f").unwrap(), "14:30:45.12");
485 assert_eq!(dt.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
486 assert_eq!(dt.format("%H:%M:%S%.4f").unwrap(), "14:30:45.1234");
487 assert_eq!(dt.format("%H:%M:%S%.5f").unwrap(), "14:30:45.12345");
488 assert_eq!(dt.format("%H:%M:%S%.6f").unwrap(), "14:30:45.123456");
489
490 assert_eq!(time.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
492 assert_eq!(time.format("%H:%M:%S%.4f").unwrap(), "14:30:45.1234");
493 }
494
495 #[test]
496 fn test_fractional_second_fallback() {
497 let time = Time::new_with_microseconds(14, 30, 45, 123456);
498 let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
499
500 assert_eq!(dt.format("%H:%M:%S%.0f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.7f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.10f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.99f").unwrap(), "14:30:45.123456"); assert_eq!(time.format("%H:%M:%S%.0f").unwrap(), "14:30:45.123456");
508 assert_eq!(time.format("%H:%M:%S%.7f").unwrap(), "14:30:45.123456");
509 assert_eq!(time.format("%H:%M:%S%f").unwrap(), "14:30:45.123456");
510 }
511
512 #[test]
513 fn test_literal_dot_handling() {
514 let time = Time::new_with_microseconds(14, 30, 45, 123456);
515 let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
516
517 assert_eq!(dt.format("%H:%M:%S.").unwrap(), "14:30:45.");
519 assert_eq!(dt.format("%H:%M:%S.123").unwrap(), "14:30:45.123");
520 assert_eq!(dt.format("%H:%M:%S.abc").unwrap(), "14:30:45.abc");
521 }
522
523 #[test]
524 fn test_date_formatting() {
525 let date = Date::new(2023, 6, 15);
526
527 assert_eq!(date.format("%Y-%m-%d").unwrap(), "2023-06-15");
528 assert_eq!(date.format("%B %d, %Y").unwrap(), "June 15, 2023");
529 assert_eq!(
530 date.format("%A, %B %d, %Y").unwrap(),
531 "Thursday, June 15, 2023"
532 );
533 assert_eq!(date.format("%j").unwrap(), "166"); }
535
536 #[test]
537 fn test_time_formatting() {
538 let time = Time::new(14, 30, 45);
539
540 assert_eq!(time.format("%H:%M:%S").unwrap(), "14:30:45");
541 assert_eq!(time.format("%I:%M %p").unwrap(), "02:30 PM");
542 assert_eq!(time.format("%T").unwrap(), "14:30:45");
543 }
544
545 #[test]
546 fn test_timezone_formatting() {
547 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 120);
548
549 assert_eq!(dt.format("%z").unwrap(), "+0200");
550 assert_eq!(dt.format("%:z").unwrap(), "+02:00");
551
552 let dt_utc = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
553 assert_eq!(dt_utc.format("%z").unwrap(), "Z");
554 }
555
556 #[test]
557 fn test_microsecond_formatting() {
558 let time = Time::new_with_microseconds(14, 30, 45, 123456);
559
560 assert_eq!(time.format("%H:%M:%S%f").unwrap(), "14:30:45.123456");
561 assert_eq!(time.format("%T%f").unwrap(), "14:30:45.123456");
562 }
563
564 #[test]
565 fn test_millisecond_formatting() {
566 let time = Time::new_with_microseconds(14, 30, 45, 123456);
567
568 assert_eq!(time.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
569 assert_eq!(time.format("%T%.3f").unwrap(), "14:30:45.123");
570
571 let time2 = Time::new_with_microseconds(9, 15, 30, 50000);
573 assert_eq!(time2.format("%H:%M:%S%.3f").unwrap(), "09:15:30.050");
574
575 let time3 = Time::new_with_microseconds(23, 59, 59, 999000);
576 assert_eq!(time3.format("%H:%M:%S%.3f").unwrap(), "23:59:59.999");
577 }
578
579 #[test]
580 fn test_iso8601_formatting() {
581 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
582
583 assert_eq!(dt.format("%+").unwrap(), "2023-06-15T14:30:45Z");
584 }
585
586 #[test]
587 fn test_escaped_percent() {
588 let format = FormatString::new("100%% complete").unwrap();
589 assert_eq!(format.parts.len(), 2);
590
591 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
592 assert_eq!(dt.format("100%% complete").unwrap(), "100% complete");
593 }
594
595 #[test]
596 fn test_invalid_format() {
597 assert!(FormatString::new("%").is_err());
598 assert!(FormatString::new("%X").is_err());
599 }
600}