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 (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; // days are one indexed
55        let month = month + 1;
56
57        // {
58        //     let first_day_of_year = (total_days - day_of_year + 3) % 7;
59        //     let weeks = (day_of_year + first_day_of_year) / 7 + 1; // weeks are one indexed
60        //     let first_day_of_year = DAYS[first_day_of_year as usize];
61        // }
62
63        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; // hours are one indexed
71                    Cow::Owned(format!("{hour:02}"))
72                }
73                "hour12" => {
74                    let hour = (secs / HOUR) % 12 + 1; // hours are one indexed
75                    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    /// `month` and `day` are one indexed
95    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    // *nth month year*
115    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        // TODO could do something more complex where it counts absolute days
169        (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    /// πŸ‡¬πŸ‡§
181    pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
182    /// πŸ‡ΊπŸ‡Έ
183    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;
196// const LEAP_YEAR: u64 = 364 * DAY;
197
198const BASE_YEAR: u64 = 1970;
199
200/*
2011	January	  31
2022	February  28 (29 in leap years)
2033	March	  31
2044	April	  30
2055	May	      31
2066	June	  30
2077	July      31
2088	August    31
2099	September 30
21010	October	  31
21111	November  30
21212	December  31
213*/
214const 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}