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 result = date_prefixes
48 .iter()
49 .enumerate()
50 .rev()
51 .find(|(_, (_, acc))| *acc <= day_of_year);
52 let Some((month, (month_name, day_sum))) = result else {
53 panic!("bad day of year {day_of_year}");
54 };
55
56 let date = day_of_year - day_sum + 1; let month = month + 1;
58
59 template.interpolate(|item| {
66 use std::borrow::Cow;
67
68 match item {
69 "second" => Cow::Owned(format!("{second:02}", second = secs % 60)),
70 "minute" => Cow::Owned(format!("{minute:02}", minute = (secs / MINUTE) % 60)),
71 "hour" | "hour24" => {
73 Cow::Owned(format!("{hour:02}", hour = (secs / HOUR) % 24 + 1))
74 }
75 "hour12" => Cow::Owned(format!("{hour:02}", hour = (secs / HOUR) % 12 + 1)),
77 "week_day" => Cow::Borrowed(DAYS[(total_days as usize + 3) % 7]),
78 "week_day_short" => Cow::Borrowed(&DAYS[(total_days as usize + 3) % 7][..3]),
79 "date_suffix" => Cow::Borrowed(number_index_suffix(date as usize)),
80 "date" => Cow::Owned(format!("{date:02}")),
81 "month_name" => Cow::Borrowed(month_name),
82 "month_name_short" => Cow::Borrowed(&month_name[..3]),
83 "month" => Cow::Owned(format!("{month:02}")),
84 "full_year" => Cow::Owned(format!("{year}", year = year % 100)),
85 "year" => Cow::Owned(format!("{year}")),
86 name => {
87 panic!("unknown interpolation {name}");
88 }
89 }
90 })
91 }
92
93 pub fn seconds(&self) -> u64 {
94 self.secs
95 }
96
97 pub fn new(year: u64, month: u64, day: u64, hour: u64, minute: u64, second: u64) -> Self {
99 let date_prefixes = if is_leap_year(year) {
100 LEAP_YEAR_MONTHS_PREFIX_SUM
101 } else {
102 NON_LEAP_YEAR_MONTHS_PREFIX_SUM
103 };
104
105 let year = year - BASE_YEAR;
106
107 let leap_years = (BASE_YEAR..)
108 .take(year as usize)
109 .filter(|year| is_leap_year(*year))
110 .count() as u64;
111 let year = year * NON_LEAP_YEAR + leap_years * DAY;
112
113 let days = (day - 1 + date_prefixes[month as usize - 1].1) * DAY;
114 Self::from_secs(year + days + hour * HOUR + minute * MINUTE + second)
115 }
116
117 pub fn parse_english(on: &str) -> Result<Self, &str> {
119 let Some((date, rest)) = on.split_once(' ') else {
120 return Err(on);
121 };
122 let Some((month, year)) = rest.split_once(' ') else {
123 return Err(on);
124 };
125 let date = {
126 let suffixed = date.ends_with("st")
127 || date.ends_with("nd")
128 || date.ends_with("rd")
129 || date.ends_with("th");
130 if suffixed {
131 &date[..(date.len() - 2)]
132 } else {
133 date
134 }
135 };
136
137 let Ok(year) = year.parse() else {
138 return Err(on);
139 };
140
141 let months = if is_leap_year(year) {
142 LEAP_YEAR_MONTHS_PREFIX_SUM
143 } else {
144 NON_LEAP_YEAR_MONTHS_PREFIX_SUM
145 };
146
147 let month = if month.len() == 3 {
148 months
149 .iter()
150 .position(|(name, _)| name[..3].eq_ignore_ascii_case(month))
151 } else {
152 months
153 .iter()
154 .position(|(name, _)| name.eq_ignore_ascii_case(month))
155 };
156
157 let month = if let Some(month) = month {
158 month + 1
159 } else {
160 return Err(on);
161 };
162
163 let Ok(day) = date.parse() else {
164 return Err(on);
165 };
166
167 Ok(Self::new(year, month as u64, day, 12, 0, 0))
168 }
169
170 pub fn difference(&self, other: Instant) -> Result<Duration, Duration> {
172 let difference = self.secs - other.secs;
173 Ok(Duration { secs: difference })
174 }
175}
176
177pub struct Duration {
178 secs: u64,
179}
180
181impl Duration {
182 pub fn format(&self) -> String {
183 if self.secs < MINUTE {
184 format!("{secs} seconds ago", secs = self.secs)
185 } else if self.secs < HOUR {
186 format!("{mins} minutes ago", mins = self.secs / MINUTE)
187 } else if self.secs < DAY {
188 format!("{hours} hours ago", hours = self.secs / HOUR)
189 } else if self.secs < WEEK {
190 format!("{days} days ago", days = self.secs / DAY)
191 } else if self.secs < NON_LEAP_YEAR {
192 format!("{weeks} weeks ago", weeks = self.secs / WEEK)
193 } else {
194 format!("{years} years ago", years = self.secs / NON_LEAP_YEAR)
195 }
196 }
197}
198
199pub const MINUTE: u64 = 60;
200pub const HOUR: u64 = 60 * MINUTE;
201pub const DAY: u64 = 24 * HOUR;
202pub const WEEK: u64 = 7 * DAY;
203
204const NON_LEAP_YEAR: u64 = 365 * DAY;
205const BASE_YEAR: u64 = 1970;
208
209const NON_LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64)] = &[
224 ("January", 0),
225 ("February", 31),
226 ("March", 59),
227 ("April", 90),
228 ("May", 120),
229 ("June", 151),
230 ("July", 181),
231 ("August", 212),
232 ("September", 243),
233 ("October", 273),
234 ("November", 304),
235 ("December", 334),
236];
237
238const LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64)] = &[
239 ("January", 0),
240 ("February", 31),
241 ("March", 60),
242 ("April", 91),
243 ("May", 121),
244 ("June", 152),
245 ("July", 182),
246 ("August", 213),
247 ("September", 244),
248 ("October", 274),
249 ("November", 305),
250 ("December", 335),
251];
252
253const DAYS: &[&str] = &[
254 "Monday",
255 "Tuesday",
256 "Wednesday",
257 "Thursday",
258 "Friday",
259 "Saturday",
260 "Sunday",
261];
262
263fn is_leap_year(year: u64) -> bool {
264 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
265}
266
267fn number_index_suffix(item: usize) -> &'static str {
268 match item % 10 {
269 1 => "st",
270 2 => "nd",
271 3 => "rd",
272 _ => "th",
273 }
274}
275
276#[allow(non_snake_case)]
277pub mod FORMATS {
278 pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
280 pub const MONTH_DATE_YEAR: &str = "%month/%date/%year";
282
283 pub const DATE_NAME_MONTH_YEAR: &str = "%week_day %date%date_suffix %month_name %year";
284 pub const TIME_DATE_NAME_MONTH_YEAR: &str =
285 "%hour:%minute %week_day %date%date_suffix %month_name %year";
286 pub const ENGLISH: &str = "%week_day the %date%date_suffix of %month_name %year";
287
288 pub const TIME: &str = "%hour:%minute";
289 pub const TIME_WITH_SECONDS: &str = "%hour:%minute:%second";
290
291 pub const FULL_MINIMAL: &str = "%hour:%minute:%second %date/%month/%year";
292}