date_utilities/
lib.rs

1mod utilities;
2
3use utilities::Template;
4
5/// represents times after 1970
6pub 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    // TODO take format string
25    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; // days are one indexed
57        let month = month + 1;
58
59        // {
60        //     let first_day_of_year = (total_days - day_of_year + 3) % 7;
61        //     let weeks = (day_of_year + first_day_of_year) / 7 + 1; // weeks are one indexed
62        //     let first_day_of_year = DAYS[first_day_of_year as usize];
63        // }
64
65        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                // hours are one indexed
72                "hour" | "hour24" => {
73                    Cow::Owned(format!("{hour:02}", hour = (secs / HOUR) % 24 + 1))
74                }
75                // hours are one indexed
76                "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    /// `month` and `day` are one indexed
98    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    // *nth month year*
118    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    /// Returns the [`Duration`] between dates. If other
171    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;
205// const LEAP_YEAR: u64 = 364 * DAY;
206
207const BASE_YEAR: u64 = 1970;
208
209/*
2101	January	  31
2112	February  28 (29 in leap years)
2123	March	  31
2134	April	  30
2145	May	      31
2156	June	  30
2167	July      31
2178	August    31
2189	September 30
21910	October	  31
22011	November  30
22112	December  31
222*/
223const 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    /// πŸ‡¬πŸ‡§
279    pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
280    /// πŸ‡ΊπŸ‡Έ
281    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}