Skip to main content

date_utilities/
lib.rs

1mod utilities;
2
3use utilities::Template;
4
5/// represents times after 1970. second precision
6pub struct Instant {
7    /// without `utc_offset` applied
8    utc_secs: u64,
9    utc_offset: i32,
10}
11
12impl Instant {
13    pub fn now_utc() -> Self {
14        use std::time::SystemTime;
15
16        let time = SystemTime::now();
17        let since = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
18        let utc_secs = since.as_secs();
19        Self::from_utc_secs(utc_secs)
20    }
21
22    pub fn now_local() -> Self {
23        use std::time::SystemTime;
24
25        let time = SystemTime::now();
26        let since = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
27        let utc_secs = since.as_secs();
28        let utc_offset = system_utc_offset_seconds();
29        Self {
30            utc_secs,
31            utc_offset,
32        }
33    }
34
35    pub fn from_utc_secs(utc_secs: u64) -> Self {
36        Self {
37            utc_secs,
38            utc_offset: 0,
39        }
40    }
41
42    // TODO std gate && Result
43    pub fn from_system_time(time: std::time::SystemTime) -> Self {
44        let secs = time
45            .duration_since(std::time::SystemTime::UNIX_EPOCH)
46            .unwrap()
47            .as_secs();
48        Self::from_utc_secs(secs)
49    }
50
51    pub fn format(&self, template: &str) -> String {
52        let template = Template::new(template, '%');
53        let secs = self.seconds();
54
55        let day_of_week_str = self.day_of_week_str();
56
57        let (day_of_year, offset_year) = self.day_of_year_and_offset_year();
58
59        let year = BASE_YEAR + offset_year;
60
61        let date_prefixes = if is_leap_year(year) {
62            LEAP_YEAR_MONTHS_PREFIX_SUM
63        } else {
64            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
65        };
66
67        let result = date_prefixes
68            .iter()
69            .enumerate()
70            .rev()
71            .find(|(_, (_, _, acc))| *acc <= day_of_year);
72
73        let Some((month, (month_name, _, day_sum))) = result else {
74            panic!("bad day of year {day_of_year}");
75        };
76
77        let date = day_of_year - day_sum + 1; // days are one indexed
78        let month = month + 1;
79
80        let week = self.week_of_year();
81
82        // {
83        //     let first_day_of_year = (total_days - day_of_year + 3) % 7;
84        //     let weeks = (day_of_year + first_day_of_year) / 7 + 1; // weeks are one indexed
85        //     let first_day_of_year = DAYS[first_day_of_year as usize];
86        // }
87
88        template.interpolate(|item| {
89            use std::borrow::Cow;
90
91            fn hour_zero_to_twelve(i: u64) -> u64 {
92                if i == 0 { 12 } else { i }
93            }
94
95            match item {
96                "second" => Cow::Owned(format!("{second:02}", second = secs % 60)),
97                "minute" => Cow::Owned(format!("{minute:02}", minute = (secs / MINUTE) % 60)),
98                "hour" | "hour24" => Cow::Owned(format!("{hour:02}", hour = (secs / HOUR) % 24)),
99                "hour12" => Cow::Owned(format!(
100                    "{hour:02}",
101                    hour = hour_zero_to_twelve((secs / HOUR) % 12)
102                )),
103                // TODO
104                "week" => Cow::Owned(week.to_string()),
105                "week_day" => Cow::Borrowed(day_of_week_str),
106                "week_day_short" => Cow::Borrowed(&day_of_week_str[..3]),
107                "date_suffix" => Cow::Borrowed(number_index_suffix(date as usize)),
108                "date" => Cow::Owned(format!("{date:02}")),
109                "month_name" => Cow::Borrowed(month_name),
110                "month_name_short" => Cow::Borrowed(&month_name[..3]),
111                "month" => Cow::Owned(format!("{month:02}")),
112                "full_year" => Cow::Owned(format!("{year}", year = year % 100)),
113                "year" => Cow::Owned(format!("{year}")),
114                "offset" => {
115                    /// I think this is correct
116                    fn offset(prefix: char, utc_offset: u64) -> String {
117                        let hour = utc_offset / HOUR;
118                        let minute = (utc_offset / MINUTE) % 60;
119                        format!("{prefix}{hour:02}{minute:02}")
120                    }
121
122                    if self.utc_offset < 0 {
123                        Cow::Owned(offset('-', -self.utc_offset as u64))
124                    } else {
125                        Cow::Owned(offset('+', self.utc_offset as u64))
126                    }
127                }
128                name => {
129                    panic!("unknown interpolation {name}");
130                }
131            }
132        })
133    }
134
135    /// with `utc_offset` applied
136    pub fn seconds(&self) -> u64 {
137        // we branch to prevent truncation
138        if self.utc_offset > 0 {
139            self.utc_secs + self.utc_offset as u64
140        } else {
141            self.utc_secs - self.utc_offset as u64
142        }
143    }
144
145    /// `month` and `day` are one indexed
146    pub fn new(year: u64, month: u64, day: u64, hour: u64, minute: u64, second: u64) -> Self {
147        let date_prefixes = if is_leap_year(year) {
148            LEAP_YEAR_MONTHS_PREFIX_SUM
149        } else {
150            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
151        };
152
153        let year = year - BASE_YEAR;
154
155        let leap_years = (BASE_YEAR..)
156            .take(year as usize)
157            .filter(|year| is_leap_year(*year))
158            .count() as u64;
159        let year = year * NON_LEAP_YEAR + leap_years * DAY;
160
161        let days = (day - 1 + date_prefixes[month as usize - 1].2) * DAY;
162        Self::from_utc_secs(year + days + hour * HOUR + minute * MINUTE + second)
163    }
164
165    /// *nth month year*
166    /// TODO errors
167    pub fn parse_english<S: AsRef<str>>(
168        mut words: impl Iterator<Item = S>,
169    ) -> Result<Self, &'static str> {
170        let Some(mut date) = words.next() else {
171            return Err("no date");
172        };
173
174        if let Some((day, month_and_year)) = date.as_ref().split_once('/') {
175            let (month, year) = month_and_year.split_once('/').unwrap();
176
177            let day = str::parse::<u64>(day).unwrap();
178            let month = str::parse::<u64>(month).unwrap();
179            let year = str::parse::<u64>(year).unwrap();
180
181            let year = if year < 100 { year + 2000 } else { year };
182
183            let next = words.next();
184            let (hour, minute, second) = if let Some(next) = next
185                && let Some(triple) = parse_time(next.as_ref())
186            {
187                triple
188            } else {
189                (12, 0, 0)
190            };
191
192            Ok(Instant::new(year, month, day, hour, minute, second))
193        } else if let Some((hour, minute, second)) = parse_time(date.as_ref()) {
194            Ok(Instant::now_local().set_time(hour, minute, second))
195        } else {
196            if date.as_ref().eq_ignore_ascii_case("the") {
197                if let Some(next) = words.next() {
198                    date = next;
199                } else {
200                    return Err("nothing after the");
201                }
202            }
203
204            let Some(mut month) = words.next() else {
205                return Err("no month");
206            };
207
208            if month.as_ref().eq_ignore_ascii_case("of") {
209                if let Some(next) = words.next() {
210                    month = next;
211                } else {
212                    return Err("nothing after of");
213                }
214            }
215
216            let year = words.next();
217
218            //
219
220            let year = if let Some(year) = year {
221                let Ok(year) = year.as_ref().parse() else {
222                    return Err("invalid year");
223                };
224                if year < 100 { year + 2000 } else { year }
225            } else {
226                Instant::now_local().year()
227            };
228
229            //
230
231            let month = month.as_ref();
232
233            let months = if is_leap_year(year) {
234                LEAP_YEAR_MONTHS_PREFIX_SUM
235            } else {
236                NON_LEAP_YEAR_MONTHS_PREFIX_SUM
237            };
238
239            let month = if month.len() == 3 {
240                months
241                    .iter()
242                    .position(|(name, ..)| name[..3].eq_ignore_ascii_case(month))
243            } else {
244                months
245                    .iter()
246                    .position(|(name, ..)| name.eq_ignore_ascii_case(month))
247            };
248
249            let month = if let Some(month) = month {
250                month + 1
251            } else {
252                return Err("cannot find month");
253            };
254
255            //
256            let date = {
257                let date = date.as_ref();
258                let suffixed = date.ends_with("st")
259                    || date.ends_with("nd")
260                    || date.ends_with("rd")
261                    || date.ends_with("th");
262                if suffixed {
263                    &date[..(date.len() - 2)]
264                } else {
265                    date
266                }
267            };
268
269            let Ok(day) = date.parse() else {
270                return Err("invalid date");
271            };
272
273            let (hour, minute, seconds) = if let Some(other) = words.next() {
274                if let Some(triple) = parse_time(other.as_ref()) {
275                    triple
276                } else {
277                    return Err("invalid time");
278                }
279            } else {
280                (12, 0, 0)
281            };
282
283            Ok(Self::new(year, month as u64, day, hour, minute, seconds))
284        }
285    }
286
287    /// Returns the [`Duration`] between dates. If other
288    pub fn difference(&self, other: Instant) -> Result<Duration, Duration> {
289        let difference = self.seconds().checked_sub(other.seconds());
290        match difference {
291            Some(secs) => Ok(Duration { secs }),
292            None => Err(Duration {
293                secs: other.seconds() - self.seconds(),
294            }),
295        }
296    }
297
298    #[must_use]
299    pub fn set_time(mut self, hours: u64, minutes: u64, seconds: u64) -> Self {
300        // reset
301        self.utc_secs -= self.utc_secs % DAY;
302        self.utc_secs -= self.utc_secs % HOUR;
303        self.utc_secs -= self.utc_secs % MINUTE;
304
305        // set new
306        self.utc_secs += hours * HOUR;
307        self.utc_secs += minutes * MINUTE;
308        self.utc_secs += seconds;
309        self
310    }
311
312    #[must_use]
313    pub fn year(&self) -> u64 {
314        // This will overcount
315        let non_leap_years = self.seconds() / NON_LEAP_YEAR;
316        let accounting_for_leap_years =
317            (self.seconds() - DAY * leap_years_since(non_leap_years)) / NON_LEAP_YEAR;
318        BASE_YEAR + accounting_for_leap_years
319    }
320
321    #[must_use]
322    pub fn day_of_week(&self) -> u8 {
323        ((self.seconds() / DAY + 3) % 7) as u8
324    }
325
326    #[must_use]
327    pub fn day_of_week_str(&self) -> &str {
328        DAYS[self.day_of_week() as usize]
329    }
330
331    /// 1 indexed
332    #[must_use]
333    pub fn week_of_year(&self) -> u64 {
334        let (day_of_year, _) = self.day_of_year_and_offset_year();
335        let offset = Self::utc_start_of_year(self.year()).day_of_week() as u64;
336        ((day_of_year + offset) / 7) + 1
337    }
338
339    fn day_of_year_and_offset_year(&self) -> (u64, u64) {
340        let secs = self.seconds();
341
342        let offset_year = secs / NON_LEAP_YEAR;
343        let day_of_year = (secs / DAY) % 365;
344
345        let leap_years = leap_years_since(offset_year);
346
347        if leap_years > day_of_year {
348            let year = offset_year - 1;
349            let day = (count_of_days_in_year(BASE_YEAR + year) + day_of_year) - leap_years;
350            (day, year)
351        } else {
352            (day_of_year - leap_years, offset_year)
353        }
354    }
355
356    pub fn month(&self) -> (&'static str, u64, u64) {
357        let (day_of_year, offset_year) = self.day_of_year_and_offset_year();
358
359        let year = BASE_YEAR + offset_year;
360        let date_prefixes = if is_leap_year(year) {
361            LEAP_YEAR_MONTHS_PREFIX_SUM
362        } else {
363            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
364        };
365
366        let result = date_prefixes
367            .iter()
368            .enumerate()
369            .rev()
370            .find(|(_, (_, _, acc))| *acc <= day_of_year);
371
372        let Some((month, (month_name, _, day_sum))) = result else {
373            panic!("bad day of year {day_of_year}");
374        };
375
376        let date = day_of_year - day_sum + 1; // days are one indexed
377        (month_name, month as u64, date)
378    }
379
380    // pub fn month_str() -> &str {}
381
382    /// TODO utc vs local etc
383    #[must_use]
384    pub fn utc_start_of_year(year: u64) -> Self {
385        let year = year - BASE_YEAR;
386        Self {
387            utc_secs: year * NON_LEAP_YEAR + leap_years_since(year) * DAY,
388            utc_offset: 0,
389        }
390    }
391}
392
393fn parse_time(on: &str) -> Option<(u64, u64, u64)> {
394    if let Some((hour, minute_and_seconds)) = on.split_once(':') {
395        let hour = str::parse::<u64>(hour).unwrap();
396        if let Some((minute, second)) = minute_and_seconds.split_once(':') {
397            let minute = str::parse::<u64>(minute).unwrap();
398            let second = str::parse::<u64>(second).unwrap();
399            Some((hour, minute, second))
400        } else {
401            let minute = minute_and_seconds;
402            let minute = str::parse::<u64>(minute).unwrap();
403            Some((hour, minute, 0))
404        }
405    } else {
406        None
407    }
408}
409
410pub struct Month {
411    pub name: &'static str,
412    pub days: u8,
413}
414
415/// From offset year
416pub struct Year(u32);
417
418impl Year {
419    pub fn new(year: u32) -> Self {
420        Self(year)
421    }
422    pub fn months(&self) -> impl Iterator<Item = Month> {
423        let months = if is_leap_year(self.0 as u64) {
424            LEAP_YEAR_MONTHS_PREFIX_SUM
425        } else {
426            NON_LEAP_YEAR_MONTHS_PREFIX_SUM
427        };
428
429        months.iter().map(|(name, days, _)| Month {
430            name,
431            days: *days as u8,
432        })
433    }
434}
435
436#[derive(Clone, Copy)]
437pub struct Duration {
438    pub(crate) secs: u64,
439}
440
441impl Duration {
442    #[must_use]
443    pub const fn from_secs(secs: u64) -> Self {
444        Self { secs }
445    }
446
447    #[must_use]
448    pub const fn from_seconds(secs: u64) -> Self {
449        Self { secs }
450    }
451
452    #[must_use]
453    pub const fn from_minutes(minutes: u64) -> Self {
454        Self {
455            secs: minutes * MINUTE,
456        }
457    }
458
459    #[must_use]
460    pub const fn from_hours(hours: u64) -> Self {
461        Self { secs: hours * HOUR }
462    }
463
464    #[must_use]
465    pub const fn from_days(days: u64) -> Self {
466        Self { secs: days * DAY }
467    }
468
469    #[must_use]
470    pub const fn from_weeks(weeks: u64) -> Self {
471        Self { secs: weeks * WEEK }
472    }
473
474    #[must_use]
475    pub fn format(&self) -> String {
476        fn format_(secs: u64, precision: usize, buf: &mut String) {
477            use std::fmt::Write;
478
479            if secs == 0 {
480                return;
481            }
482
483            if !buf.is_empty() {
484                if precision > 1 {
485                    buf.push_str(", ");
486                } else if precision == 1 {
487                    buf.push_str(" and ");
488                } else if precision == 0 {
489                    return;
490                }
491            }
492
493            if secs < MINUTE {
494                write!(buf, "{secs} seconds").unwrap();
495            } else if secs < HOUR {
496                let mins = secs / MINUTE;
497                write!(buf, "{mins} minutes").unwrap();
498                format_(secs % MINUTE, precision - 1, buf);
499            } else if secs < DAY {
500                let hours = secs / HOUR;
501                write!(buf, "{hours} hours").unwrap();
502                format_(secs % HOUR, precision - 1, buf);
503            } else if secs < WEEK {
504                let days = secs / DAY;
505                write!(buf, "{days} days").unwrap();
506                format_(secs % DAY, precision - 1, buf);
507            } else if secs < NON_LEAP_YEAR {
508                let weeks = secs / WEEK;
509                write!(buf, "{weeks} weeks").unwrap();
510                format_(secs % WEEK, precision - 1, buf);
511            } else {
512                const MONTH: u64 = DAY * 30;
513                let months = secs / MONTH;
514                write!(buf, "{months} months").unwrap();
515                // format!("{years} years", years = self.secs / NON_LEAP_YEAR)
516            }
517        }
518
519        let precision = 2;
520        let mut buf = String::new();
521
522        format_(self.secs, precision, &mut buf);
523
524        buf
525    }
526}
527
528impl std::ops::Add<Duration> for Instant {
529    type Output = Self;
530
531    fn add(mut self, duration: Duration) -> Self::Output {
532        self.utc_secs += duration.secs;
533        self
534    }
535}
536
537impl std::ops::AddAssign<Duration> for Instant {
538    fn add_assign(&mut self, duration: Duration) {
539        self.utc_secs += duration.secs;
540    }
541}
542
543impl std::ops::Sub<Duration> for Instant {
544    type Output = Self;
545
546    fn sub(mut self, duration: Duration) -> Self::Output {
547        self.utc_secs -= duration.secs;
548        self
549    }
550}
551
552impl std::ops::SubAssign<Duration> for Instant {
553    fn sub_assign(&mut self, duration: Duration) {
554        self.utc_secs -= duration.secs;
555    }
556}
557
558pub const MINUTE: u64 = 60;
559pub const HOUR: u64 = 60 * MINUTE;
560pub const DAY: u64 = 24 * HOUR;
561pub const WEEK: u64 = 7 * DAY;
562
563const NON_LEAP_YEAR: u64 = 365 * DAY;
564// const LEAP_YEAR: u64 = 364 * DAY;
565
566const BASE_YEAR: u64 = 1970;
567
568/*
5691	January	  31
5702	February  28 (29 in leap years)
5713	March	  31
5724	April	  30
5735	May	      31
5746	June	  30
5757	July      31
5768	August    31
5779	September 30
57810	October	  31
57911	November  30
58012	December  31
581*/
582const NON_LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64, u64)] = &[
583    ("January", 31, 0),
584    ("February", 28, 31),
585    ("March", 31, 59),
586    ("April", 30, 90),
587    ("May", 31, 120),
588    ("June", 30, 151),
589    ("July", 31, 181),
590    ("August", 31, 212),
591    ("September", 30, 243),
592    ("October", 31, 273),
593    ("November", 30, 304),
594    ("December", 31, 334),
595];
596
597const LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64, u64)] = &[
598    ("January", 31, 0),
599    ("February", 29, 31),
600    ("March", 31, 60),
601    ("April", 30, 91),
602    ("May", 31, 121),
603    ("June", 30, 152),
604    ("July", 31, 182),
605    ("August", 31, 213),
606    ("September", 30, 244),
607    ("October", 31, 274),
608    ("November", 30, 305),
609    ("December", 31, 335),
610];
611
612const DAYS: &[&str] = &[
613    "Monday",
614    "Tuesday",
615    "Wednesday",
616    "Thursday",
617    "Friday",
618    "Saturday",
619    "Sunday",
620];
621
622pub fn count_of_days_in_year(year: u64) -> u64 {
623    365 + is_leap_year(year) as u64
624}
625
626fn is_leap_year(year: u64) -> bool {
627    year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
628}
629
630fn leap_years_since(offset_year: u64) -> u64 {
631    (BASE_YEAR..)
632        .take(offset_year as usize)
633        .filter(|year| is_leap_year(*year))
634        .count() as u64
635}
636
637fn number_index_suffix(item: usize) -> &'static str {
638    match item % 10 {
639        1 => "st",
640        2 => "nd",
641        3 => "rd",
642        _ => "th",
643    }
644}
645
646#[allow(non_snake_case)]
647pub mod FORMATS {
648    /// πŸ‡¬πŸ‡§
649    pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
650    /// πŸ‡ΊπŸ‡Έ
651    pub const MONTH_DATE_YEAR: &str = "%month/%date/%year";
652
653    pub const DATE_NAME_MONTH_YEAR: &str = "%week_day %date%date_suffix %month_name %year";
654    pub const TIME_DATE_NAME_MONTH_YEAR: &str =
655        "%hour:%minute %week_day %date%date_suffix %month_name %year";
656    pub const ENGLISH: &str = "%week_day the %date%date_suffix of %month_name %year";
657
658    pub const TIME: &str = "%hour:%minute";
659    pub const TIME_WITH_SECONDS: &str = "%hour:%minute:%second";
660
661    pub const WEEK: &str = "%week";
662
663    pub const FULL_MINIMAL: &str = "%hour:%minute:%second %date/%month/%year";
664
665    /// TODO
666    pub const FULL: &str = "%hour:%minute:%second %date/%month/%year";
667
668    pub const ISO_8601: &str = "%year-%month-%dateT%hour:%minute:%second";
669    // Thu, 07 Apr 2005 22:13:13 +0200
670    pub const RFC_2822: &str = "%year-%month-%dateT%hour:%minute:%second";
671}
672
673#[cfg(unix)]
674fn system_utc_offset_seconds() -> i32 {
675    use std::mem::MaybeUninit;
676
677    unsafe {
678        let mut now: libc::time_t = 0;
679        libc::time(&raw mut now);
680
681        let mut tm = MaybeUninit::<libc::tm>::uninit();
682        libc::localtime_r(&raw const now, tm.as_mut_ptr());
683
684        let tm = tm.assume_init();
685        i32::try_from(tm.tm_gmtoff).unwrap()
686    }
687}
688
689#[cfg(windows)]
690fn system_utc_offset_seconds() -> i32 {
691    use std::mem::MaybeUninit;
692    use winapi::um::timezoneapi::GetTimeZoneInformation;
693    use winapi::um::timezoneapi::TIME_ZONE_INFORMATION;
694
695    unsafe {
696        let mut tzi = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
697        let state = GetTimeZoneInformation(tzi.as_mut_ptr());
698        let tzi = tzi.assume_init();
699
700        let mut bias = tzi.Bias;
701
702        match state {
703            winapi::um::timezoneapi::TIME_ZONE_ID_DAYLIGHT => {
704                bias += tzi.DaylightBias;
705            }
706            winapi::um::timezoneapi::TIME_ZONE_ID_STANDARD => {
707                bias += tzi.StandardBias;
708            }
709            _ => {}
710        }
711
712        // Windows bias is minutes *west* of UTC
713        -bias * 60
714    }
715}