1mod utilities;
2
3use utilities::Template;
4
5pub struct Instant {
7    secs: u64,
8}
9
10impl Instant {
11    pub fn now() -> Self {
12        use std::time::SystemTime;
13
14        let time = SystemTime::now();
15        let since = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
16        let secs = since.as_secs();
17        Self { secs }
18    }
19
20    pub fn from_secs(secs: u64) -> Self {
21        Self { secs }
22    }
23
24    pub fn format(&self, template: &str) -> String {
26        let template = Template::new(template);
27        let secs = self.secs;
28
29        let offset_year = secs / NON_LEAP_YEAR;
30        let total_days = secs / DAY;
31        let mut day_of_year = total_days - offset_year * 365;
32
33        let leap_years = (BASE_YEAR..)
34            .take(offset_year as usize)
35            .filter(|year| is_leap_year(*year))
36            .count() as u64;
37        day_of_year -= leap_years;
38
39        let year = BASE_YEAR + offset_year;
40
41        let date_prefixes = if is_leap_year(year) {
42            LEAP_YEAR_MONTHS_PREFIX_SUM
43        } else {
44            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
45        };
46
47        let (month, (month_name, day_sum)) = date_prefixes
48            .iter()
49            .enumerate()
50            .rev()
51            .find(|(_, (_, acc))| *acc < day_of_year)
52            .unwrap();
53
54        let date = day_of_year - day_sum + 1; let month = month + 1;
56
57        template.interpolate(|item| {
64            use std::borrow::Cow;
65
66            match item {
67                "second" => Cow::Owned(format!("{second:02}", second = secs % 60)),
68                "minute" => Cow::Owned(format!("{minute:02}", minute = (secs / MINUTE) % 60)),
69                "hour" | "hour24" => {
70                    let hour = (secs / HOUR) % 24 + 1; Cow::Owned(format!("{hour:02}"))
72                }
73                "hour12" => {
74                    let hour = (secs / HOUR) % 12 + 1; Cow::Owned(format!("{hour:02}"))
76                }
77                "week_day" => Cow::Borrowed(DAYS[(total_days as usize + 3) % 7]),
78                "date_suffix" => Cow::Borrowed(number_index_suffix(date as usize)),
79                "date" => Cow::Owned(format!("{date}")),
80                "month_name" => Cow::Borrowed(month_name),
81                "month" => Cow::Owned(format!("{month:02}")),
82                "year" => Cow::Owned(format!("{year}")),
83                name => {
84                    panic!("unknown interpolation {name}");
85                }
86            }
87        })
88    }
89
90    pub fn seconds(&self) -> u64 {
91        self.secs
92    }
93
94    pub fn new(year: u64, month: u64, day: u64, hour: u64, minute: u64, second: u64) -> Self {
96        let date_prefixes = if is_leap_year(year) {
97            LEAP_YEAR_MONTHS_PREFIX_SUM
98        } else {
99            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
100        };
101
102        let year = year - BASE_YEAR;
103
104        let leap_years = (BASE_YEAR..)
105            .take(year as usize)
106            .filter(|year| is_leap_year(*year))
107            .count() as u64;
108        let year = year * NON_LEAP_YEAR + leap_years * DAY;
109
110        let days = (day - 1 + date_prefixes[month as usize - 1].1) * DAY;
111        Self::from_secs(year + days + hour * HOUR + minute * MINUTE + second)
112    }
113
114    pub fn parse_english(on: &str) -> Result<Self, &str> {
116        let Some((date, rest)) = on.split_once(' ') else {
117            return Err(on);
118        };
119        let Some((month, year)) = rest.split_once(' ') else {
120            return Err(on);
121        };
122        let date = {
123            let suffixed = date.ends_with("st")
124                || date.ends_with("nd")
125                || date.ends_with("rd")
126                || date.ends_with("th");
127            if suffixed {
128                &date[..(date.len() - 2)]
129            } else {
130                date
131            }
132        };
133
134        let Ok(year) = year.parse() else {
135            return Err(on);
136        };
137
138        let months = if is_leap_year(year) {
139            LEAP_YEAR_MONTHS_PREFIX_SUM
140        } else {
141            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
142        };
143
144        let month = if month.len() == 3 {
145            months
146                .iter()
147                .position(|(name, _)| name[..3].eq_ignore_ascii_case(month))
148        } else {
149            months
150                .iter()
151                .position(|(name, _)| name.eq_ignore_ascii_case(month))
152        };
153
154        let month = if let Some(month) = month {
155            month + 1
156        } else {
157            return Err(on);
158        };
159
160        let Ok(day) = date.parse() else {
161            return Err(on);
162        };
163
164        Ok(Self::new(year, month as u64, day, 12, 0, 0))
165    }
166
167    pub fn difference_days(&self, other: &Self) -> u64 {
168        (other.secs - self.secs) / DAY
170    }
171}
172
173pub const MINUTE: u64 = 60;
174pub const HOUR: u64 = 60 * MINUTE;
175pub const DAY: u64 = 24 * HOUR;
176pub const WEEK: u64 = 7 * DAY;
177
178#[allow(non_snake_case)]
179pub mod FORMATS {
180    pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
182    pub const MONTH_DATE_YEAR: &str = "%month/%date/%year";
184
185    pub const DATE_NAME_MONTH_YEAR: &str = "%week_day %date%date_suffix %month_name %year";
186    pub const TIME_DATE_NAME_MONTH_YEAR: &str =
187        "%hour:%minute %week_day %date%date_suffix %month_name %year";
188
189    pub const TIME: &str = "%hour:%minute";
190    pub const TIME_WITH_SECONDS: &str = "%hour:%minute:%second";
191
192    pub const FULL_MINIMAL: &str = "%hour:%minute:%second %date/%month/%year";
193}
194
195const NON_LEAP_YEAR: u64 = 365 * DAY;
196const BASE_YEAR: u64 = 1970;
199
200const NON_LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64)] = &[
215    ("January", 0),
216    ("February", 31),
217    ("March", 59),
218    ("April", 90),
219    ("May", 120),
220    ("June", 151),
221    ("July", 181),
222    ("August", 212),
223    ("September", 243),
224    ("October", 273),
225    ("November", 304),
226    ("December", 334),
227];
228
229const LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64)] = &[
230    ("January", 0),
231    ("February", 31),
232    ("March", 60),
233    ("April", 91),
234    ("May", 121),
235    ("June", 152),
236    ("July", 182),
237    ("August", 213),
238    ("September", 244),
239    ("October", 274),
240    ("November", 305),
241    ("December", 335),
242];
243
244const DAYS: &[&str] = &[
245    "Monday",
246    "Tuesday",
247    "Wednesday",
248    "Thursday",
249    "Friday",
250    "Saturday",
251    "Sunday",
252];
253
254fn is_leap_year(year: u64) -> bool {
255    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
256}
257
258fn number_index_suffix(item: usize) -> &'static str {
259    match item % 10 {
260        1 => "st",
261        2 => "nd",
262        3 => "rd",
263        _ => "th",
264    }
265}