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, Millisecond, 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 if chars.peek() == Some(&'3') && chars.peek().is_some() && {
82 let mut peek_chars = chars.clone();
83 peek_chars.next(); peek_chars.next() == Some('f')
85 } =>
86 {
87 chars.next(); chars.next(); FormatSpecifier::Millisecond
90 }
91 Some('f') => FormatSpecifier::Microsecond,
92 Some('p') => FormatSpecifier::AmPm,
93 Some('z') => FormatSpecifier::Timezone,
94 Some(':') if chars.peek() == Some(&'z') => {
95 chars.next(); FormatSpecifier::TimezoneColon
97 }
98 Some('D') => FormatSpecifier::Date,
99 Some('T') => FormatSpecifier::Time,
100 Some('F') => FormatSpecifier::DateTime,
101 Some('+') => FormatSpecifier::Iso8601,
102 Some('%') => {
103 literal.push('%');
104 continue;
105 }
106 Some(_) => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
107 None => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
108 };
109 parts.push(FormatPart::Specifier(spec));
110 } else {
111 literal.push(ch);
112 }
113 }
114
115 if !literal.is_empty() {
116 parts.push(FormatPart::Literal(literal));
117 }
118
119 Ok(FormatString { parts })
120 }
121
122 pub fn format_datetime(&self, dt: &DateTime) -> String {
124 let mut result = String::new();
125 for part in &self.parts {
126 match part {
127 FormatPart::Literal(s) => result.push_str(s),
128 FormatPart::Specifier(spec) => {
129 result.push_str(&self.format_specifier(spec, dt));
130 }
131 }
132 }
133 result
134 }
135
136 pub fn format_date(&self, date: &Date) -> String {
138 let mut result = String::new();
139 for part in &self.parts {
140 match part {
141 FormatPart::Literal(s) => result.push_str(s),
142 FormatPart::Specifier(spec) => {
143 result.push_str(&self.format_specifier_date(spec, date));
144 }
145 }
146 }
147 result
148 }
149
150 pub fn format_time(&self, time: &Time) -> String {
152 let mut result = String::new();
153 for part in &self.parts {
154 match part {
155 FormatPart::Literal(s) => result.push_str(s),
156 FormatPart::Specifier(spec) => {
157 result.push_str(&self.format_specifier_time(spec, time));
158 }
159 }
160 }
161 result
162 }
163
164 fn format_specifier(&self, spec: &FormatSpecifier, dt: &DateTime) -> String {
165 match spec {
166 FormatSpecifier::Year => format!("{:04}", dt.date.year),
167 FormatSpecifier::YearShort => format!("{:02}", dt.date.year % 100),
168 FormatSpecifier::Month => format!("{:02}", dt.date.month),
169 FormatSpecifier::MonthName => self.month_name(dt.date.month),
170 FormatSpecifier::MonthNameShort => self.month_name_short(dt.date.month),
171 FormatSpecifier::Day => format!("{:02}", dt.date.day),
172 FormatSpecifier::DayOfYear => format!("{:03}", dt.date.year_day()),
173 FormatSpecifier::Weekday => self.weekday_name(&dt.date),
174 FormatSpecifier::WeekdayShort => self.weekday_name_short(&dt.date),
175 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(&dt.date)),
176 FormatSpecifier::Hour24 => format!("{:02}", dt.time.hour),
177 FormatSpecifier::Hour12 => {
178 let hour = if dt.time.hour == 0 {
179 12
180 } else if dt.time.hour > 12 {
181 dt.time.hour - 12
182 } else {
183 dt.time.hour
184 };
185 format!("{:02}", hour)
186 }
187 FormatSpecifier::Minute => format!("{:02}", dt.time.minute),
188 FormatSpecifier::Second => format!("{:02}", dt.time.second),
189 FormatSpecifier::Microsecond => format!("{:06}", dt.time.microsecond),
190 FormatSpecifier::Millisecond => format!("{:03}", dt.time.microsecond / 1000),
191 FormatSpecifier::AmPm => if dt.time.hour < 12 { "AM" } else { "PM" }.to_string(),
192 FormatSpecifier::Timezone => self.format_timezone(dt.shift_minutes, false),
193 FormatSpecifier::TimezoneColon => self.format_timezone(dt.shift_minutes, true),
194 FormatSpecifier::Date => format!(
195 "{:02}/{:02}/{:02}",
196 dt.date.month,
197 dt.date.day,
198 dt.date.year % 100
199 ),
200 FormatSpecifier::Time => format!(
201 "{:02}:{:02}:{:02}",
202 dt.time.hour, dt.time.minute, dt.time.second
203 ),
204 FormatSpecifier::DateTime => format!(
205 "{:04}-{:02}-{:02}",
206 dt.date.year, dt.date.month, dt.date.day
207 ),
208 FormatSpecifier::Iso8601 => dt.to_iso_8061(),
209 }
210 }
211
212 fn format_specifier_date(&self, spec: &FormatSpecifier, date: &Date) -> String {
213 match spec {
214 FormatSpecifier::Year => format!("{:04}", date.year),
215 FormatSpecifier::YearShort => format!("{:02}", date.year % 100),
216 FormatSpecifier::Month => format!("{:02}", date.month),
217 FormatSpecifier::MonthName => self.month_name(date.month),
218 FormatSpecifier::MonthNameShort => self.month_name_short(date.month),
219 FormatSpecifier::Day => format!("{:02}", date.day),
220 FormatSpecifier::DayOfYear => format!("{:03}", date.year_day()),
221 FormatSpecifier::Weekday => self.weekday_name(date),
222 FormatSpecifier::WeekdayShort => self.weekday_name_short(date),
223 FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(date)),
224 FormatSpecifier::DateTime => {
225 format!("{:04}-{:02}-{:02}", date.year, date.month, date.day)
226 }
227 FormatSpecifier::Date => {
228 format!("{:02}/{:02}/{:02}", date.month, date.day, date.year % 100)
229 }
230 _ => String::new(), }
232 }
233
234 fn format_specifier_time(&self, spec: &FormatSpecifier, time: &Time) -> String {
235 match spec {
236 FormatSpecifier::Hour24 => format!("{:02}", time.hour),
237 FormatSpecifier::Hour12 => {
238 let hour = if time.hour == 0 {
239 12
240 } else if time.hour > 12 {
241 time.hour - 12
242 } else {
243 time.hour
244 };
245 format!("{:02}", hour)
246 }
247 FormatSpecifier::Minute => format!("{:02}", time.minute),
248 FormatSpecifier::Second => format!("{:02}", time.second),
249 FormatSpecifier::Microsecond => format!("{:06}", time.microsecond),
250 FormatSpecifier::Millisecond => format!("{:03}", time.microsecond / 1000),
251 FormatSpecifier::AmPm => if time.hour < 12 { "AM" } else { "PM" }.to_string(),
252 FormatSpecifier::Time => {
253 format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second)
254 }
255 _ => String::new(), }
257 }
258
259 fn month_name(&self, month: u64) -> String {
260 match month {
261 1 => "January".to_string(),
262 2 => "February".to_string(),
263 3 => "March".to_string(),
264 4 => "April".to_string(),
265 5 => "May".to_string(),
266 6 => "June".to_string(),
267 7 => "July".to_string(),
268 8 => "August".to_string(),
269 9 => "September".to_string(),
270 10 => "October".to_string(),
271 11 => "November".to_string(),
272 12 => "December".to_string(),
273 _ => "Unknown".to_string(),
274 }
275 }
276
277 fn month_name_short(&self, month: u64) -> String {
278 match month {
279 1 => "Jan".to_string(),
280 2 => "Feb".to_string(),
281 3 => "Mar".to_string(),
282 4 => "Apr".to_string(),
283 5 => "May".to_string(),
284 6 => "Jun".to_string(),
285 7 => "Jul".to_string(),
286 8 => "Aug".to_string(),
287 9 => "Sep".to_string(),
288 10 => "Oct".to_string(),
289 11 => "Nov".to_string(),
290 12 => "Dec".to_string(),
291 _ => "Unknown".to_string(),
292 }
293 }
294
295 fn weekday_name(&self, date: &Date) -> String {
296 if date.is_sunday() {
297 "Sunday".to_string()
298 } else if date.is_monday() {
299 "Monday".to_string()
300 } else if date.is_tuesday() {
301 "Tuesday".to_string()
302 } else if date.is_wednesday() {
303 "Wednesday".to_string()
304 } else if date.is_thursday() {
305 "Thursday".to_string()
306 } else if date.is_friday() {
307 "Friday".to_string()
308 } else if date.is_saturday() {
309 "Saturday".to_string()
310 } else {
311 "Unknown".to_string()
312 }
313 }
314
315 fn weekday_name_short(&self, date: &Date) -> String {
316 if date.is_sunday() {
317 "Sun".to_string()
318 } else if date.is_monday() {
319 "Mon".to_string()
320 } else if date.is_tuesday() {
321 "Tue".to_string()
322 } else if date.is_wednesday() {
323 "Wed".to_string()
324 } else if date.is_thursday() {
325 "Thu".to_string()
326 } else if date.is_friday() {
327 "Fri".to_string()
328 } else if date.is_saturday() {
329 "Sat".to_string()
330 } else {
331 "Unknown".to_string()
332 }
333 }
334
335 fn weekday_number(&self, date: &Date) -> u64 {
336 if date.is_sunday() {
337 0
338 } else if date.is_monday() {
339 1
340 } else if date.is_tuesday() {
341 2
342 } else if date.is_wednesday() {
343 3
344 } else if date.is_thursday() {
345 4
346 } else if date.is_friday() {
347 5
348 } else if date.is_saturday() {
349 6
350 } else {
351 0
352 }
353 }
354
355 fn format_timezone(&self, shift_minutes: isize, with_colon: bool) -> String {
356 if shift_minutes == 0 {
357 return "Z".to_string();
358 }
359
360 let abs_minutes = shift_minutes.abs() as u64;
361 let hours = abs_minutes / 60;
362 let minutes = abs_minutes % 60;
363
364 let sign = if shift_minutes > 0 { "+" } else { "-" };
365
366 if with_colon {
367 format!("{}{:02}:{:02}", sign, hours, minutes)
368 } else {
369 format!("{}{:02}{:02}", sign, hours, minutes)
370 }
371 }
372}
373
374pub trait Format {
376 fn format(&self, format: &str) -> Result<String, DateError>;
378}
379
380impl Format for DateTime {
381 fn format(&self, format: &str) -> Result<String, DateError> {
382 let format_string = FormatString::new(format)?;
383 Ok(format_string.format_datetime(self))
384 }
385}
386
387impl Format for Date {
388 fn format(&self, format: &str) -> Result<String, DateError> {
389 let format_string = FormatString::new(format)?;
390 Ok(format_string.format_date(self))
391 }
392}
393
394impl Format for Time {
395 fn format(&self, format: &str) -> Result<String, DateError> {
396 let format_string = FormatString::new(format)?;
397 Ok(format_string.format_time(self))
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_format_string_parsing() {
407 let format = FormatString::new("%Y-%m-%d %H:%M:%S").unwrap();
408 assert_eq!(format.parts.len(), 11); let format = FormatString::new("Date: %Y-%m-%d").unwrap();
411 assert_eq!(format.parts.len(), 6); }
413
414 #[test]
415 fn test_datetime_formatting() {
416 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
417
418 assert_eq!(dt.format("%Y-%m-%d").unwrap(), "2023-06-15");
419 assert_eq!(dt.format("%H:%M:%S").unwrap(), "14:30:45");
420 assert_eq!(
421 dt.format("%Y-%m-%d %H:%M:%S").unwrap(),
422 "2023-06-15 14:30:45"
423 );
424 assert_eq!(dt.format("%B %d, %Y").unwrap(), "June 15, 2023");
425 assert_eq!(
426 dt.format("%A, %B %d, %Y").unwrap(),
427 "Thursday, June 15, 2023"
428 );
429 assert_eq!(dt.format("%I:%M %p").unwrap(), "02:30 PM");
430 }
431
432 #[test]
433 fn test_datetime_millisecond_formatting() {
434 let time = Time::new_with_microseconds(14, 30, 45, 123456);
435 let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
436
437 assert_eq!(
438 dt.format("%Y-%m-%d %H:%M:%S.%.3f").unwrap(),
439 "2023-06-15 14:30:45.123"
440 );
441 }
442
443 #[test]
444 fn test_date_formatting() {
445 let date = Date::new(2023, 6, 15);
446
447 assert_eq!(date.format("%Y-%m-%d").unwrap(), "2023-06-15");
448 assert_eq!(date.format("%B %d, %Y").unwrap(), "June 15, 2023");
449 assert_eq!(
450 date.format("%A, %B %d, %Y").unwrap(),
451 "Thursday, June 15, 2023"
452 );
453 assert_eq!(date.format("%j").unwrap(), "166"); }
455
456 #[test]
457 fn test_time_formatting() {
458 let time = Time::new(14, 30, 45);
459
460 assert_eq!(time.format("%H:%M:%S").unwrap(), "14:30:45");
461 assert_eq!(time.format("%I:%M %p").unwrap(), "02:30 PM");
462 assert_eq!(time.format("%T").unwrap(), "14:30:45");
463 }
464
465 #[test]
466 fn test_timezone_formatting() {
467 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 120);
468
469 assert_eq!(dt.format("%z").unwrap(), "+0200");
470 assert_eq!(dt.format("%:z").unwrap(), "+02:00");
471
472 let dt_utc = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
473 assert_eq!(dt_utc.format("%z").unwrap(), "Z");
474 }
475
476 #[test]
477 fn test_microsecond_formatting() {
478 let time = Time::new_with_microseconds(14, 30, 45, 123456);
479
480 assert_eq!(time.format("%H:%M:%S.%f").unwrap(), "14:30:45.123456");
481 assert_eq!(time.format("%T.%f").unwrap(), "14:30:45.123456");
482 }
483
484 #[test]
485 fn test_millisecond_formatting() {
486 let time = Time::new_with_microseconds(14, 30, 45, 123456);
487
488 assert_eq!(time.format("%H:%M:%S.%.3f").unwrap(), "14:30:45.123");
489 assert_eq!(time.format("%T.%.3f").unwrap(), "14:30:45.123");
490
491 let time2 = Time::new_with_microseconds(9, 15, 30, 50000);
493 assert_eq!(time2.format("%H:%M:%S.%.3f").unwrap(), "09:15:30.050");
494
495 let time3 = Time::new_with_microseconds(23, 59, 59, 999000);
496 assert_eq!(time3.format("%H:%M:%S.%.3f").unwrap(), "23:59:59.999");
497 }
498
499 #[test]
500 fn test_iso8601_formatting() {
501 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
502
503 assert_eq!(dt.format("%+").unwrap(), "2023-06-15T14:30:45Z");
504 }
505
506 #[test]
507 fn test_escaped_percent() {
508 let format = FormatString::new("100%% complete").unwrap();
509 assert_eq!(format.parts.len(), 2);
510
511 let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
512 assert_eq!(dt.format("100%% complete").unwrap(), "100% complete");
513 }
514
515 #[test]
516 fn test_invalid_format() {
517 assert!(FormatString::new("%").is_err());
518 assert!(FormatString::new("%X").is_err());
519 }
520}