1use crate::error::YukiError;
2
3pub fn parse_period(period: &str) -> Result<(String, String), YukiError> {
10 let invalid = || YukiError::Config(format!("invalid period: {period}"));
11
12 if period.len() == 4 && period.chars().all(|c| c.is_ascii_digit()) {
14 let year: u32 = period.parse().map_err(|_| invalid())?;
15 return Ok((format!("{year:04}-01-01"), format!("{year:04}-12-31")));
16 }
17
18 if period.len() == 7 {
20 let (year_str, rest) = period.split_at(4);
21 if let Some(q) = rest.strip_prefix("-Q") {
22 let year: u32 = year_str.parse().map_err(|_| invalid())?;
23 let quarter: u32 = q.parse().map_err(|_| invalid())?;
24 let (start_month, end_month, end_day) = match quarter {
25 1 => (1u32, 3u32, 31u32),
26 2 => (4, 6, 30),
27 3 => (7, 9, 30),
28 4 => (10, 12, 31),
29 _ => return Err(invalid()),
30 };
31 return Ok((
32 format!("{year:04}-{start_month:02}-01"),
33 format!("{year:04}-{end_month:02}-{end_day:02}"),
34 ));
35 }
36 }
37
38 if period.len() == 7 {
40 let (year_str, rest) = period.split_at(4);
41 if let Some(month_str) = rest.strip_prefix('-') {
42 let year: u32 = year_str.parse().map_err(|_| invalid())?;
43 let month: u32 = month_str.parse().map_err(|_| invalid())?;
44 if month == 0 || month > 12 {
45 return Err(invalid());
46 }
47 let last_day = days_in_month(year, month);
48 return Ok((
49 format!("{year:04}-{month:02}-01"),
50 format!("{year:04}-{month:02}-{last_day:02}"),
51 ));
52 }
53 }
54
55 Err(invalid())
56}
57
58fn days_in_month(year: u32, month: u32) -> u32 {
60 match month {
61 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
62 4 | 6 | 9 | 11 => 30,
63 2 => {
64 if is_leap_year(year) {
65 29
66 } else {
67 28
68 }
69 }
70 _ => unreachable!("month already validated"),
71 }
72}
73
74fn is_leap_year(year: u32) -> bool {
76 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
77}