jomini/common/
date.rs

1use crate::scalar::to_i64_t;
2use crate::util::{fast_digit_parse, le_u64};
3use std::cmp::Ordering;
4use std::convert::TryFrom;
5use std::fmt::{self, Debug, Display};
6use std::str::FromStr;
7
8/// A date error.
9#[derive(Debug, PartialEq, Eq)]
10pub struct DateError;
11
12impl std::error::Error for DateError {
13    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
14        None
15    }
16}
17
18impl std::fmt::Display for DateError {
19    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20        write!(f, "unable to decode date")
21    }
22}
23
24const DAYS_PER_MONTH: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
25
26/// Common set of methods between all the date components
27pub trait PdsDate {
28    /// Return the year
29    fn year(&self) -> i16;
30
31    /// Returns the month. Range: [1, 12]
32    fn month(&self) -> u8;
33
34    /// Return the day
35    fn day(&self) -> u8;
36
37    /// Formats the date in the game format
38    fn game_fmt(&self) -> PdsDateFormatter;
39
40    /// Formats the date in an iso8601 format
41    fn iso_8601(&self) -> PdsDateFormatter;
42}
43
44/// Controls the output format of a date
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum DateFormat {
47    /// ISO-8601 format
48    Iso8601,
49
50    /// Y.M.D[.H] where month, day, and hour don't have zero padding
51    DotShort,
52
53    /// Y.M.D[.H] where month, day, and hour are zero padded to two digits
54    DotWide,
55}
56
57/// A temporary object which can be used as an argument to `format!`.
58///
59/// Used to avoid a needless intermediate allocation
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct PdsDateFormatter {
62    raw: RawDate,
63    format: DateFormat,
64}
65
66impl PdsDateFormatter {
67    /// Creates new formatter with a given date and desired format
68    pub fn new(raw: RawDate, format: DateFormat) -> Self {
69        Self { raw, format }
70    }
71}
72
73impl Display for PdsDateFormatter {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        if self.format == DateFormat::Iso8601 {
76            write!(
77                f,
78                "{:04}-{:02}-{:02}",
79                self.raw.year(),
80                self.raw.month(),
81                self.raw.day(),
82            )?;
83
84            if self.raw.has_hour() {
85                write!(f, "T{:02}", self.raw.hour() - 1)
86            } else {
87                Ok(())
88            }
89        } else {
90            let fmt = self.format;
91            let width = if fmt == DateFormat::DotWide { 2 } else { 0 };
92            write!(
93                f,
94                "{}.{:03$}.{:03$}",
95                self.raw.year(),
96                self.raw.month(),
97                self.raw.day(),
98                width,
99            )?;
100
101            if self.raw.has_hour() {
102                write!(f, ".{:01$}", self.raw.hour(), width)
103            } else {
104                Ok(())
105            }
106        }
107    }
108}
109
110/// A [RawDate] where each component is a full data type
111#[derive(Debug)]
112struct ExpandedRawDate {
113    year: i16,
114    month: u8,
115    day: u8,
116    hour: u8,
117}
118
119impl ExpandedRawDate {
120    #[inline]
121    fn from_binary(mut s: i32) -> Option<Self> {
122        // quite annoying that the binary format uses a 24 hour clock
123        // indexed at 0 so it is up to a higher level API to determine
124        // to map the hour to 1 based.
125        let hour = s % 24;
126        s /= 24;
127
128        let days_since_jan1 = s % 365;
129        if hour < 0 || days_since_jan1 < 0 {
130            return None;
131        }
132
133        s /= 365;
134        let year = s.checked_sub(5000).and_then(|x| i16::try_from(x).ok())?;
135        let (month, day) = month_day_from_julian(days_since_jan1);
136        Some(ExpandedRawDate {
137            year,
138            month,
139            day,
140            hour: hour as u8,
141        })
142    }
143
144    #[inline]
145    fn parse<T: AsRef<[u8]>>(s: T) -> Option<Self> {
146        Self::_parse(s.as_ref())
147    }
148
149    #[inline]
150    fn _parse(data: &[u8]) -> Option<Self> {
151        let (year, data) = to_i64_t(data).ok()?;
152        if data.is_empty() {
153            return i32::try_from(year).ok().and_then(Self::from_binary);
154        }
155
156        let year = i16::try_from(year).ok()?;
157        if *data.first()? != b'.' {
158            return None;
159        }
160
161        let n = data.get(1)?;
162        let month1 = if !n.is_ascii_digit() {
163            return None;
164        } else {
165            n - b'0'
166        };
167
168        let n = data.get(2)?;
169        let (month, offset) = if *n == b'.' {
170            (month1, 2)
171        } else if n.is_ascii_digit() {
172            (month1 * 10 + (n - b'0'), 3)
173        } else {
174            return None;
175        };
176
177        if *data.get(offset)? != b'.' {
178            return None;
179        }
180
181        let n = data.get(offset + 1)?;
182        let day1 = if !n.is_ascii_digit() {
183            return None;
184        } else {
185            n - b'0'
186        };
187
188        let (day, offset) = match data.get(offset + 2) {
189            None => {
190                return Some(ExpandedRawDate {
191                    year,
192                    month,
193                    day: day1,
194                    hour: 0,
195                })
196            }
197            Some(b'.') => (day1, offset + 2),
198            Some(n) if n.is_ascii_digit() => {
199                let result = day1 * 10 + (n - b'0');
200                if data.len() != offset + 3 {
201                    (result, offset + 3)
202                } else {
203                    return Some(ExpandedRawDate {
204                        year,
205                        month,
206                        day: result,
207                        hour: 0,
208                    });
209                }
210            }
211            _ => return None,
212        };
213
214        if *data.get(offset)? != b'.' {
215            return None;
216        }
217
218        let n = data.get(offset + 1)?;
219        let hour1 = if !n.is_ascii_digit() || *n == b'0' {
220            return None;
221        } else {
222            n - b'0'
223        };
224
225        match data.get(offset + 2) {
226            None => Some(ExpandedRawDate {
227                year,
228                month,
229                day,
230                hour: hour1,
231            }),
232            Some(n) if n.is_ascii_digit() => {
233                let result = hour1 * 10 + (n - b'0');
234                if data.len() != offset + 3 {
235                    None
236                } else {
237                    Some(ExpandedRawDate {
238                        year,
239                        month,
240                        day,
241                        hour: result,
242                    })
243                }
244            }
245            _ => None,
246        }
247    }
248}
249
250/// Common implementation between the different date and time formats.
251///
252/// Space optimized to only need 4 bytes
253///
254/// It may or may not have an hour component.
255///
256/// Paradox games do not follow any traditional calendar and instead view the
257/// world on simpler terms: that every year should be treated as a non-leap
258/// year.
259///
260/// Years can be negative but can't be zero.
261///
262/// An hour component is considered present if it is non-zero. This means that
263/// games with hours run on a non-traditional clock from 1-24 instead of the
264/// traditional 24 hour clock (0-23). An exception is Victoria 3, which has day
265/// cycle of (0, 6, 12, 18) hours, but remains consistent in omitting the hour
266/// when it is zero.
267///
268/// A raw date has very minimal validation and can support any calendar system
269/// as it holds abitrary values for year, month, day, and hours
270///
271/// It is typically recommended to use one of the specialized types: [Date],
272/// [DateHour], or [UniformDate] as date formats aren't variable within a game
273/// and have less pitfalls.
274#[derive(Clone, Copy, PartialEq, Eq, Hash)]
275pub struct RawDate {
276    year: i16,
277
278    // month: 4 bits
279    // day: 5 bits
280    // hour: 5 bits
281    // empty: 2 bits
282    data: u16,
283}
284
285impl Debug for RawDate {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        write!(
288            f,
289            "RawDate {{ year: {} month: {} day: {} hour: {} }}",
290            self.year(),
291            self.month(),
292            self.day(),
293            self.hour()
294        )
295    }
296}
297
298impl PartialOrd for RawDate {
299    fn partial_cmp(&self, other: &RawDate) -> Option<Ordering> {
300        Some(self.cmp(other))
301    }
302}
303
304impl Ord for RawDate {
305    fn cmp(&self, other: &RawDate) -> Ordering {
306        self.year()
307            .cmp(&other.year())
308            .then_with(|| self.data.cmp(&other.data))
309    }
310}
311
312impl RawDate {
313    #[inline]
314    fn from_expanded(data: ExpandedRawDate) -> Option<Self> {
315        Self::from_ymdh_opt(data.year, data.month, data.day, data.hour)
316    }
317
318    /// Creates a raw date from a binary integer
319    ///
320    /// ```
321    /// use jomini::common::{RawDate, PdsDate};
322    /// let date = RawDate::from_binary(56379360).unwrap();
323    /// assert_eq!(date.iso_8601().to_string(), String::from("1436-01-01"));
324    ///
325    /// let date2 = RawDate::from_binary(60759371).unwrap();
326    /// assert_eq!(date2.iso_8601().to_string(), String::from("1936-01-01T10"));
327    /// ```
328    #[inline]
329    pub fn from_binary(s: i32) -> Option<Self> {
330        ExpandedRawDate::from_binary(s).and_then(Self::from_expanded)
331    }
332
333    /// Create a raw date from individual components.
334    ///
335    /// Will return none for an invalid date
336    #[inline]
337    pub fn from_ymdh_opt(year: i16, month: u8, day: u8, hour: u8) -> Option<Self> {
338        if month != 0 && month < 13 && day != 0 && day < 32 && hour < 25 {
339            let data = (u16::from(month) << 12) + (u16::from(day) << 7) + (u16::from(hour) << 2);
340            Some(RawDate { year, data })
341        } else {
342            None
343        }
344    }
345
346    /// Create a raw date from individual components.
347    ///
348    /// Will panic on invalid dates
349    #[inline]
350    pub fn from_ymdh(year: i16, month: u8, day: u8, hour: u8) -> Self {
351        Self::from_ymdh_opt(year, month, day, hour).unwrap()
352    }
353
354    /// Return the hour component. Range [1, 24]. If zero, then there is no hour
355    #[inline]
356    pub fn hour(&self) -> u8 {
357        ((self.data >> 2) & 0x1f) as u8
358    }
359
360    /// Return if this date has an hour component
361    #[inline]
362    pub fn has_hour(&self) -> bool {
363        self.data & 0x7c != 0
364    }
365
366    /// Parses date components from the following formatted text:
367    ///
368    /// - `Y.M.D`
369    /// - `Y.M.D.H`
370    /// - `YYYY.MM.DD.HH`
371    /// - or any variation of the above
372    ///
373    /// A zero component for the hour is disallowed, so the hour
374    /// must be omitted when parsing to only a date without a time component.
375    ///
376    /// Unlike [`Date::parse`], this will not parse the textual form of the
377    /// date's binary representation.
378    #[inline]
379    pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
380        Self::_parse(s.as_ref())
381    }
382
383    #[inline]
384    fn _parse(s: &[u8]) -> Result<Self, DateError> {
385        ExpandedRawDate::parse(s)
386            .and_then(Self::from_expanded)
387            .and_then(|x| {
388                if to_i64_t(s).ok()?.1.is_empty() {
389                    None
390                } else {
391                    Some(x)
392                }
393            })
394            .ok_or(DateError)
395    }
396}
397
398impl PdsDate for RawDate {
399    /// Return year of date
400    #[inline]
401    fn year(&self) -> i16 {
402        self.year
403    }
404
405    /// Return month of date
406    #[inline]
407    fn month(&self) -> u8 {
408        (self.data >> 12) as u8
409    }
410
411    /// Return day of date
412    #[inline]
413    fn day(&self) -> u8 {
414        ((self.data >> 7) & 0x1f) as u8
415    }
416
417    fn game_fmt(&self) -> PdsDateFormatter {
418        PdsDateFormatter::new(*self, DateFormat::DotShort)
419    }
420
421    fn iso_8601(&self) -> PdsDateFormatter {
422        PdsDateFormatter::new(*self, DateFormat::Iso8601)
423    }
424}
425
426impl FromStr for RawDate {
427    type Err = DateError;
428
429    #[inline]
430    fn from_str(s: &str) -> Result<Self, Self::Err> {
431        Self::parse(s.as_bytes())
432    }
433}
434
435/// A date without a time component
436///
437/// See [RawDate] for additional date / time commentary
438#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
439pub struct Date {
440    raw: RawDate,
441}
442
443impl Debug for Date {
444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445        write!(f, "Date {}", self.game_fmt())
446    }
447}
448
449impl Date {
450    #[inline]
451    fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
452        if date.hour != 0 {
453            None
454        } else {
455            Self::from_ymd_opt(date.year, date.month, date.day)
456        }
457    }
458
459    #[inline]
460    fn days(&self) -> i32 {
461        let month_days = julian_ordinal_day(self.month());
462        let year_day = i32::from(self.year()) * 365;
463        if year_day < 0 {
464            year_day - month_days - i32::from(self.day())
465        } else {
466            year_day + month_days + i32::from(self.day())
467        }
468    }
469
470    /// Create a new date from year, month, and day parts
471    ///
472    /// Will return `None` if the date does not exist
473    ///
474    /// ```
475    /// use jomini::common::Date;
476    /// assert_eq!(Date::from_ymd_opt(1444, 11, 11), Some(Date::from_ymd(1444, 11, 11)));
477    /// assert_eq!(Date::from_ymd_opt(800, 5, 3), Some(Date::from_ymd(800, 5, 3)));
478    /// assert!(Date::from_ymd_opt(800, 0, 3).is_none());
479    /// assert!(Date::from_ymd_opt(800, 1, 0).is_none());
480    /// assert!(Date::from_ymd_opt(800, 13, 1).is_none());
481    /// assert!(Date::from_ymd_opt(800, 12, 32).is_none());
482    /// assert!(Date::from_ymd_opt(2020, 2, 29).is_none());
483    /// ```
484    #[inline]
485    pub fn from_ymd_opt(year: i16, month: u8, day: u8) -> Option<Self> {
486        RawDate::from_ymdh_opt(year, month, day, 0).and_then(|raw| {
487            let days = DAYS_PER_MONTH[usize::from(month)];
488            if day <= days {
489                Some(Date { raw })
490            } else {
491                None
492            }
493        })
494    }
495
496    /// Create a new date from year, month, and day parts
497    ///
498    /// Will panic if the date does not exist.
499    #[inline]
500    pub fn from_ymd(year: i16, month: u8, day: u8) -> Self {
501        Self::from_ymd_opt(year, month, day).unwrap()
502    }
503
504    /// Parses a string and returns a new [`Date`] if valid. The expected
505    /// format is either YYYY.MM.DD or a number representing of the equivalent
506    /// binary representation.
507    ///
508    /// ```
509    /// use jomini::common::{Date, PdsDate};
510    /// let date = Date::parse("1444.11.11").expect("to parse date");
511    /// assert_eq!(date.year(), 1444);
512    /// assert_eq!(date.month(), 11);
513    /// assert_eq!(date.day(), 11);
514    /// ```
515    #[inline]
516    pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
517        Self::_parse(s.as_ref())
518    }
519
520    #[inline]
521    fn fast_parse(r: [u8; 8]) -> Option<Result<Self, DateError>> {
522        Self::fast_parse_u64(u64::from_le_bytes(r))
523    }
524
525    #[inline]
526    fn fast_parse_u64(r: u64) -> Option<Result<Self, DateError>> {
527        let val = fast_digit_parse(r)?;
528        let day = val % 100;
529        let month = (val / 100) % 100;
530        let val = val / 10_000;
531
532        let result = Self::from_expanded(ExpandedRawDate {
533            year: val as i16,
534            month: month as u8,
535            day: day as u8,
536            hour: 0,
537        })
538        .ok_or(DateError);
539
540        Some(result)
541    }
542
543    #[cold]
544    fn fallback(s: &[u8]) -> Result<Self, DateError> {
545        ExpandedRawDate::parse(s)
546            .and_then(Self::from_expanded)
547            .ok_or(DateError)
548    }
549
550    #[inline]
551    fn _parse(s: &[u8]) -> Result<Self, DateError> {
552        match s {
553            [y1, y2, y3, y4, b'.', m1, m2, b'.', d1, d2] => {
554                let r = [*y1, *y2, *y3, *y4, *m1, *m2, *d1, *d2];
555                Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
556            }
557            [y1, y2, y3, y4, b'.', m1, m2, b'.', d1] => {
558                let r = [*y1, *y2, *y3, *y4, *m1, *m2, b'0', *d1];
559                Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
560            }
561            [y1, y2, y3, y4, b'.', m1, b'.', d1, d2] => {
562                let r = [*y1, *y2, *y3, *y4, b'0', *m1, *d1, *d2];
563                Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
564            }
565            _ => {
566                if s.len() == 8 {
567                    // YYYY.M.D
568                    let d = le_u64(s);
569                    let one_digit_month = d & 0x00FF_00FF_0000_0000 == 0x002E_002E_0000_0000;
570                    let e = (d & 0xFF30_FF30_FFFF_FFFF) | 0x0030_0030_0000_0000;
571                    if one_digit_month {
572                        if let Some(x) = Self::fast_parse_u64(e) {
573                            return x;
574                        }
575                    }
576
577                    Self::fallback(s)
578                } else if s.len() < 5 || s.len() > 12 || !matches!(s[0], b'-' | b'0'..=b'9') {
579                    Err(DateError)
580                } else {
581                    Self::fallback(s)
582                }
583            }
584        }
585    }
586
587    /// Returns the number of days between two dates
588    ///
589    /// ```
590    /// use jomini::common::Date;
591    /// let date = Date::parse("1400.1.2").unwrap();
592    /// let date2 = Date::parse("1400.1.3").unwrap();
593    /// let date3 = Date::parse("1401.1.2").unwrap();
594    /// let date4 = Date::parse("1401.12.31").unwrap();
595    /// assert_eq!(1, date.days_until(&date2));
596    /// assert_eq!(365, date.days_until(&date3));
597    /// assert_eq!(728, date.days_until(&date4));
598    /// assert_eq!(-728, date4.days_until(&date));
599    /// ```
600    #[inline]
601    pub fn days_until(self, other: &Date) -> i32 {
602        other.days() - self.days()
603    }
604
605    /// Return a new date that is the given number of days in the future
606    /// from the current date
607    ///
608    /// ```
609    /// use jomini::common::Date;
610    ///
611    /// let date = Date::parse("1400.1.2").unwrap();
612    /// let expected = Date::parse("1400.1.3").unwrap();
613    /// let expected2 = Date::parse("1400.1.1").unwrap();
614    /// assert_eq!(expected, date.add_days(1));
615    /// assert_eq!(expected2, date.add_days(-1));
616    /// ```
617    ///
618    /// Will panic on overflow or underflow.
619    #[inline]
620    pub fn add_days(self, days: i32) -> Date {
621        let new_days = self
622            .days()
623            .checked_add(days)
624            .expect("adding days overflowed");
625
626        let days_since_jan1 = (new_days % 365).abs();
627        let year = new_days / 365;
628        let (month, day) = month_day_from_julian(days_since_jan1);
629
630        let year = i16::try_from(year).expect("year to fit inside signed 16bits");
631        Date {
632            raw: RawDate::from_ymdh(year, month, day, self.raw.hour()),
633        }
634    }
635
636    /// Decodes a date from a number that had been parsed from binary data
637    ///
638    /// The hour component, if present, will be ignored
639    #[inline]
640    pub fn from_binary(s: i32) -> Option<Self> {
641        // I've not yet found a binary date that has an hour component but shouldn't
642        // but for consistency sake we zero out the hour so that we maintain the
643        // invariant that a Date does not have an hour component
644        ExpandedRawDate::from_binary(s)
645            .map(|x| ExpandedRawDate { hour: 0, ..x })
646            .and_then(Self::from_expanded)
647    }
648
649    /// Decodes a date from a number that had been parsed from binary data with the
650    /// added check that the date is not too far fetched. This function is useful
651    /// when working with binary data and it's not clear with an encountered integer
652    /// is supposed to represent a date or a number.
653    ///
654    /// We use -100 as a cut off dates for years. Antonio I (EU4) holds the
655    /// record with a birth date of `-58.1.1`. The exception is monuments,
656    /// which date back to -2500 or even farther back (mods), but this
657    /// function is just a heuristic so direct any extreme dates towards
658    /// [`Date::from_binary`].
659    #[inline]
660    pub fn from_binary_heuristic(s: i32) -> Option<Self> {
661        ExpandedRawDate::from_binary(s).and_then(|x| {
662            if x.year > -100 {
663                Self::from_expanded(x)
664            } else {
665                None
666            }
667        })
668    }
669
670    /// Converts a date into the binary representation
671    ///
672    /// ```rust
673    /// use jomini::common::Date;
674    /// let date = Date::from_ymd(1, 1, 1);
675    /// assert_eq!(43808760, date.to_binary());
676    /// ```
677    #[inline]
678    pub fn to_binary(self) -> i32 {
679        let ordinal_day = julian_ordinal_day(self.month()) + i32::from(self.day());
680        to_binary(self.year(), ordinal_day, 0)
681    }
682}
683
684impl PdsDate for Date {
685    /// Year of the date
686    ///
687    /// ```
688    /// use jomini::common::{Date, PdsDate};
689    /// let date = Date::from_ymd(1444, 2, 3);
690    /// assert_eq!(date.year(), 1444);
691    /// ```
692    #[inline]
693    fn year(&self) -> i16 {
694        self.raw.year()
695    }
696
697    /// Month of the date
698    ///
699    /// ```
700    /// use jomini::common::{Date, PdsDate};
701    /// let date = Date::from_ymd(1444, 2, 3);
702    /// assert_eq!(date.month(), 2);
703    /// ```
704    #[inline]
705    fn month(&self) -> u8 {
706        self.raw.month()
707    }
708
709    /// Day of the date
710    ///
711    /// ```
712    /// use jomini::common::{Date, PdsDate};
713    /// let date = Date::from_ymd(1444, 2, 3);
714    /// assert_eq!(date.day(), 3);
715    /// ```
716    #[inline]
717    fn day(&self) -> u8 {
718        self.raw.day()
719    }
720
721    /// Formats a date in the ISO 8601 format: YYYY-MM-DD
722    ///
723    /// ```
724    /// use jomini::common::{Date, PdsDate};
725    /// let date = Date::from_ymd(1400, 1, 2);
726    /// assert_eq!(date.iso_8601().to_string(), String::from("1400-01-02"));
727    /// ```
728    fn iso_8601(&self) -> PdsDateFormatter {
729        PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
730    }
731
732    /// Formats a date in the game format: Y.M.D
733    ///
734    /// ```
735    /// use jomini::common::{Date, PdsDate};
736    /// let date = Date::from_ymd(1400, 1, 2);
737    /// assert_eq!(date.game_fmt().to_string(), String::from("1400.1.2"));
738    /// ```
739    fn game_fmt(&self) -> PdsDateFormatter {
740        PdsDateFormatter::new(self.raw, DateFormat::DotShort)
741    }
742}
743
744impl FromStr for Date {
745    type Err = DateError;
746
747    #[inline]
748    fn from_str(s: &str) -> Result<Self, Self::Err> {
749        Self::parse(s.as_bytes())
750    }
751}
752
753/// A date with an hour component
754///
755/// Geared towards the hearts of iron games.
756///
757/// See [RawDate] for additional date / time commentary
758///
759/// ```rust
760/// use jomini::common::{DateHour, PdsDate};
761/// let date = DateHour::from_ymdh(1936, 1, 1, 24);
762/// let iso = date.iso_8601().to_string();
763/// assert_eq!(iso, String::from("1936-01-01T23"));
764/// ```
765#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
766pub struct DateHour {
767    raw: RawDate,
768}
769
770impl Debug for DateHour {
771    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
772        write!(f, "DateHour {}", self.game_fmt())
773    }
774}
775
776impl DateHour {
777    #[inline]
778    fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
779        Self::from_ymdh_opt(date.year, date.month, date.day, date.hour)
780    }
781
782    /// Create a new [DateHour] from individual components
783    ///
784    /// The hour is expected to be non-zero in addition to the validation
785    /// that a regular [Date] object undergoes.
786    ///
787    /// ```rust
788    /// use jomini::common::DateHour;
789    /// assert!(DateHour::from_ymdh_opt(1936, 1, 1, 24).is_some());
790    /// assert!(DateHour::from_ymdh_opt(1936, 1, 1, 0).is_none());
791    /// ```
792    #[inline]
793    pub fn from_ymdh_opt(year: i16, month: u8, day: u8, hour: u8) -> Option<Self> {
794        RawDate::from_ymdh_opt(year, month, day, hour).and_then(|raw| {
795            let days = DAYS_PER_MONTH[usize::from(month)];
796            if hour > 0 && day <= days {
797                Some(Self { raw })
798            } else {
799                None
800            }
801        })
802    }
803
804    /// ```rust
805    /// use jomini::common::{DateHour, PdsDate};
806    /// let date = DateHour::from_ymdh(1936, 1, 3, 12);
807    /// assert_eq!(date.day(), 3);
808    /// ```
809    #[inline]
810    pub fn from_ymdh(year: i16, month: u8, day: u8, hour: u8) -> Self {
811        Self::from_ymdh_opt(year, month, day, hour).unwrap()
812    }
813
814    /// hour of the date. Range: [1, 24]
815    ///
816    /// ```
817    /// use jomini::common::DateHour;
818    /// let date = DateHour::from_ymdh(1936, 1, 2, 12);
819    /// assert_eq!(date.hour(), 12);
820    /// ```
821    pub fn hour(&self) -> u8 {
822        // we know that this is > 0, per DateHour invariant
823        self.raw.hour()
824    }
825
826    /// Parse a [DateHour] from text. Follows the same logic as [RawDate::parse]
827    /// except an hour component is enforced.
828    ///
829    /// ```rust
830    /// use jomini::common::DateHour;
831    /// assert_eq!(DateHour::parse("1936.1.1.24"), Ok(DateHour::from_ymdh(1936, 1, 1, 24)));
832    /// ```
833    #[inline]
834    pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
835        ExpandedRawDate::parse(s)
836            .and_then(Self::from_expanded)
837            .ok_or(DateError)
838    }
839
840    /// Decode a number extracted from the binary format into a date.
841    #[inline]
842    pub fn from_binary(s: i32) -> Option<Self> {
843        ExpandedRawDate::from_binary(s).and_then(|mut raw| {
844            // Shift hour from 0 based 24 hour clock to 1 based 24 hour clock
845            raw.hour += 1;
846            Self::from_expanded(raw)
847        })
848    }
849
850    /// Decode a number extracted from the binary format into a date, but
851    /// ensure that the date is at least in the 1800's (an arbitrary chosen
852    /// value to support HOI4 mods that move the start date up). There is a
853    /// special exception made for 1.1.1.1 and -1.1.1.1 which represents an
854    /// event that has not occurred yet.
855    #[inline]
856    pub fn from_binary_heuristic(s: i32) -> Option<Self> {
857        Self::from_binary(s).and_then(|x| {
858            let is_min_year = x.year() == 1 || x.year() == -1;
859            let is_min_date = is_min_year && x.month() == 1 && x.day() == 1 && x.hour() == 1;
860            if x.year() < 1800 && !is_min_date {
861                None
862            } else {
863                Some(x)
864            }
865        })
866    }
867
868    /// Converts a date into the binary representation
869    ///
870    /// ```rust
871    /// use jomini::common::DateHour;
872    /// let date = DateHour::from_ymdh(1, 1, 1, 1);
873    /// assert_eq!(43808760, date.to_binary());
874    /// ```
875    #[inline]
876    pub fn to_binary(self) -> i32 {
877        let ordinal_day = julian_ordinal_day(self.month()) + i32::from(self.day());
878        to_binary(self.year(), ordinal_day, self.hour())
879    }
880}
881
882impl PdsDate for DateHour {
883    /// Year of the date
884    ///
885    /// ```
886    /// use jomini::common::{DateHour, PdsDate};
887    /// let date = DateHour::from_ymdh(1936, 1, 2, 24);
888    /// assert_eq!(date.year(), 1936);
889    /// ```
890    #[inline]
891    fn year(&self) -> i16 {
892        self.raw.year()
893    }
894
895    /// Month of the date
896    ///
897    /// ```
898    /// use jomini::common::{DateHour, PdsDate};
899    /// let date = DateHour::from_ymdh(1936, 1, 2, 24);
900    /// assert_eq!(date.month(), 1);
901    /// ```
902    #[inline]
903    fn month(&self) -> u8 {
904        self.raw.month()
905    }
906
907    /// Day of the date
908    ///
909    /// ```
910    /// use jomini::common::{DateHour, PdsDate};
911    /// let date = DateHour::from_ymdh(1936, 1, 2, 24);
912    /// assert_eq!(date.day(), 2);
913    /// ```
914    #[inline]
915    fn day(&self) -> u8 {
916        self.raw.day()
917    }
918
919    /// Return the date as an iso8601 compatible string
920    ///
921    /// The hour component is converted to a range of [0, 23] per the spec
922    ///
923    /// ```rust
924    /// use jomini::common::{DateHour, PdsDate};
925    /// let date = DateHour::from_ymdh(1936, 1, 2, 12);
926    /// assert_eq!(String::from("1936-01-02T11"), date.iso_8601().to_string());
927    /// ```
928    fn iso_8601(&self) -> PdsDateFormatter {
929        PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
930    }
931
932    /// Return the date in the game format
933    ///
934    /// ```rust
935    /// use jomini::common::{DateHour, PdsDate};
936    /// let date = DateHour::from_ymdh(1936, 1, 2, 12);
937    /// assert_eq!(String::from("1936.1.2.12"), date.game_fmt().to_string());
938    /// ```
939    fn game_fmt(&self) -> PdsDateFormatter {
940        PdsDateFormatter::new(self.raw, DateFormat::DotShort)
941    }
942}
943
944impl FromStr for DateHour {
945    type Err = DateError;
946
947    #[inline]
948    fn from_str(s: &str) -> Result<Self, Self::Err> {
949        Self::parse(s.as_bytes())
950    }
951}
952
953/// A date without a time component where each month has 30 days
954///
955/// Useful for Stellaris
956///
957/// See [RawDate] for additional date / time commentary
958#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
959pub struct UniformDate {
960    raw: RawDate,
961}
962
963impl Debug for UniformDate {
964    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965        write!(f, "UniformDate {}", self.game_fmt())
966    }
967}
968
969impl UniformDate {
970    #[inline]
971    fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
972        if date.hour != 0 {
973            None
974        } else {
975            Self::from_ymd_opt(date.year, date.month, date.day)
976        }
977    }
978
979    /// Create a new date from year, month, and day parts
980    ///
981    /// Will return `None` if the date does not exist
982    ///
983    /// ```
984    /// use jomini::common::{PdsDate, UniformDate};
985    /// assert_eq!(UniformDate::from_ymd_opt(1444, 11, 11), Some(UniformDate::from_ymd(1444, 11, 11)));
986    /// assert_eq!(UniformDate::from_ymd_opt(800, 5, 3), Some(UniformDate::from_ymd(800, 5, 3)));
987    /// assert!(UniformDate::from_ymd_opt(800, 0, 3).is_none());
988    /// assert!(UniformDate::from_ymd_opt(800, 1, 0).is_none());
989    /// assert!(UniformDate::from_ymd_opt(800, 13, 1).is_none());
990    /// assert!(UniformDate::from_ymd_opt(800, 12, 32).is_none());
991    /// assert!(UniformDate::from_ymd_opt(2020, 2, 29).is_some());
992    /// ```
993    #[inline]
994    pub fn from_ymd_opt(year: i16, month: u8, day: u8) -> Option<Self> {
995        if day > 30 {
996            None
997        } else {
998            RawDate::from_ymdh_opt(year, month, day, 0).map(|raw| Self { raw })
999        }
1000    }
1001
1002    /// Create a new date from year, month, and day parts
1003    ///
1004    /// Will panic if the date does not exist.
1005    #[inline]
1006    pub fn from_ymd(year: i16, month: u8, day: u8) -> Self {
1007        Self::from_ymd_opt(year, month, day).unwrap()
1008    }
1009
1010    /// Parse a [DateHour] from text. Follows the same logic as [RawDate::parse]
1011    /// except that a 12 month calendar of 30 days is enforced.
1012    ///
1013    /// ```rust
1014    /// use jomini::common::UniformDate;
1015    /// assert_eq!(UniformDate::parse("2200.02.30"), Ok(UniformDate::from_ymd(2200, 2, 30)));
1016    /// ```
1017    #[inline]
1018    pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
1019        Self::_parse(s.as_ref())
1020    }
1021
1022    #[inline]
1023    fn _parse(s: &[u8]) -> Result<Self, DateError> {
1024        ExpandedRawDate::parse(s)
1025            .and_then(Self::from_expanded)
1026            .ok_or(DateError)
1027    }
1028}
1029
1030impl PdsDate for UniformDate {
1031    /// Year of the date
1032    ///
1033    /// ```
1034    /// use jomini::common::{UniformDate, PdsDate};
1035    /// let date = UniformDate::from_ymd(1444, 2, 3);
1036    /// assert_eq!(date.year(), 1444);
1037    /// ```
1038    #[inline]
1039    fn year(&self) -> i16 {
1040        self.raw.year()
1041    }
1042
1043    /// Month of the date
1044    ///
1045    /// ```
1046    /// use jomini::common::{UniformDate, PdsDate};
1047    /// let date = UniformDate::from_ymd(1444, 2, 3);
1048    /// assert_eq!(date.month(), 2);
1049    /// ```
1050    #[inline]
1051    fn month(&self) -> u8 {
1052        self.raw.month()
1053    }
1054
1055    /// Day of the date
1056    ///
1057    /// ```
1058    /// use jomini::common::{UniformDate, PdsDate};
1059    /// let date = UniformDate::from_ymd(1444, 2, 3);
1060    /// assert_eq!(date.day(), 3);
1061    /// ```
1062    #[inline]
1063    fn day(&self) -> u8 {
1064        self.raw.day()
1065    }
1066
1067    /// Formats a date in the ISO 8601 format: YYYY-MM-DD
1068    ///
1069    /// ```
1070    /// use jomini::common::{UniformDate, PdsDate};
1071    /// let date = UniformDate::from_ymd(1400, 1, 2);
1072    /// assert_eq!(date.iso_8601().to_string(), String::from("1400-01-02"));
1073    /// ```
1074    fn iso_8601(&self) -> PdsDateFormatter {
1075        PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
1076    }
1077
1078    /// Formats a date in the game format: Y.MM.DD
1079    ///
1080    /// ```
1081    /// use jomini::common::{UniformDate, PdsDate};
1082    /// let date = UniformDate::from_ymd(1400, 1, 2);
1083    /// assert_eq!(date.game_fmt().to_string(), String::from("1400.01.02"));
1084    /// ```
1085    fn game_fmt(&self) -> PdsDateFormatter {
1086        PdsDateFormatter::new(self.raw, DateFormat::DotWide)
1087    }
1088}
1089
1090impl FromStr for UniformDate {
1091    type Err = DateError;
1092
1093    #[inline]
1094    fn from_str(s: &str) -> Result<Self, Self::Err> {
1095        Self::parse(s.as_bytes())
1096    }
1097}
1098
1099#[inline]
1100fn month_day_from_julian(days_since_jan1: i32) -> (u8, u8) {
1101    // https://landweb.modaps.eosdis.nasa.gov/browse/calendar.html
1102    // except we start at 0 instead of 1
1103    let (month, day) = match days_since_jan1 {
1104        0..=30 => (1, days_since_jan1 + 1),
1105        31..=58 => (2, days_since_jan1 - 30),
1106        59..=89 => (3, days_since_jan1 - 58),
1107        90..=119 => (4, days_since_jan1 - 89),
1108        120..=150 => (5, days_since_jan1 - 119),
1109        151..=180 => (6, days_since_jan1 - 150),
1110        181..=211 => (7, days_since_jan1 - 180),
1111        212..=242 => (8, days_since_jan1 - 211),
1112        243..=272 => (9, days_since_jan1 - 242),
1113        273..=303 => (10, days_since_jan1 - 272),
1114        304..=333 => (11, days_since_jan1 - 303),
1115        334..=364 => (12, days_since_jan1 - 333),
1116        _ => unreachable!(),
1117    };
1118
1119    debug_assert!(day < 255);
1120    (month, day as u8)
1121}
1122
1123#[inline]
1124fn julian_ordinal_day(month: u8) -> i32 {
1125    match month {
1126        1 => -1,
1127        2 => 30,
1128        3 => 58,
1129        4 => 89,
1130        5 => 119,
1131        6 => 150,
1132        7 => 180,
1133        8 => 211,
1134        9 => 242,
1135        10 => 272,
1136        11 => 303,
1137        12 => 333,
1138        _ => unreachable!(),
1139    }
1140}
1141
1142#[inline]
1143fn to_binary(year: i16, ordinal_day: i32, hour: u8) -> i32 {
1144    let year_part = (i32::from(year) + 5000) * 365;
1145    let hour = i32::from(hour.saturating_sub(1));
1146    (year_part + ordinal_day) * 24 + hour
1147}
1148
1149#[cfg(feature = "derive")]
1150mod datederive {
1151    use super::{Date, DateHour, PdsDate, UniformDate};
1152    use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
1153    use std::fmt;
1154
1155    impl Serialize for Date {
1156        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1157        where
1158            S: Serializer,
1159        {
1160            serializer.serialize_str(self.iso_8601().to_string().as_str())
1161        }
1162    }
1163
1164    struct DateVisitor;
1165
1166    impl Visitor<'_> for DateVisitor {
1167        type Value = Date;
1168
1169        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1170            formatter.write_str("a date")
1171        }
1172
1173        fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
1174        where
1175            E: de::Error,
1176        {
1177            Date::from_binary(v)
1178                .ok_or_else(|| de::Error::custom(format!("invalid binary date: {}", v)))
1179        }
1180
1181        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1182        where
1183            E: de::Error,
1184        {
1185            Date::parse(v).map_err(|_e| de::Error::custom(format!("invalid date: {}", v)))
1186        }
1187
1188        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
1189        where
1190            E: de::Error,
1191        {
1192            self.visit_str(v.as_str())
1193        }
1194    }
1195
1196    impl<'de> Deserialize<'de> for Date {
1197        fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
1198        where
1199            D: Deserializer<'de>,
1200        {
1201            deserializer.deserialize_any(DateVisitor)
1202        }
1203    }
1204
1205    impl Serialize for DateHour {
1206        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1207        where
1208            S: Serializer,
1209        {
1210            serializer.serialize_str(self.iso_8601().to_string().as_str())
1211        }
1212    }
1213
1214    struct DateHourVisitor;
1215
1216    impl Visitor<'_> for DateHourVisitor {
1217        type Value = DateHour;
1218
1219        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1220            formatter.write_str("a date hour")
1221        }
1222
1223        fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
1224        where
1225            E: de::Error,
1226        {
1227            DateHour::from_binary(v)
1228                .ok_or_else(|| de::Error::custom(format!("invalid binary date hour: {}", v)))
1229        }
1230
1231        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1232        where
1233            E: de::Error,
1234        {
1235            DateHour::parse(v).map_err(|_e| de::Error::custom(format!("invalid date hour: {}", v)))
1236        }
1237
1238        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
1239        where
1240            E: de::Error,
1241        {
1242            self.visit_str(v.as_str())
1243        }
1244    }
1245
1246    impl<'de> Deserialize<'de> for DateHour {
1247        fn deserialize<D>(deserializer: D) -> Result<DateHour, D::Error>
1248        where
1249            D: Deserializer<'de>,
1250        {
1251            deserializer.deserialize_any(DateHourVisitor)
1252        }
1253    }
1254
1255    struct UniformDateVisitor;
1256
1257    impl Visitor<'_> for UniformDateVisitor {
1258        type Value = UniformDate;
1259
1260        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1261            formatter.write_str("a uniform date")
1262        }
1263
1264        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1265        where
1266            E: de::Error,
1267        {
1268            UniformDate::parse(v)
1269                .map_err(|_e| de::Error::custom(format!("invalid uniform date: {}", v)))
1270        }
1271
1272        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
1273        where
1274            E: de::Error,
1275        {
1276            self.visit_str(v.as_str())
1277        }
1278    }
1279
1280    impl<'de> Deserialize<'de> for UniformDate {
1281        fn deserialize<D>(deserializer: D) -> Result<UniformDate, D::Error>
1282        where
1283            D: Deserializer<'de>,
1284        {
1285            deserializer.deserialize_any(UniformDateVisitor)
1286        }
1287    }
1288}
1289
1290#[cfg(not(feature = "derive"))]
1291mod datederive {}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::*;
1296    use quickcheck_macros::quickcheck;
1297    use rstest::*;
1298
1299    #[test]
1300    fn test_date_iso() {
1301        let date = Date::parse("1400.1.2").unwrap();
1302        assert_eq!(date.iso_8601().to_string(), String::from("1400-01-02"));
1303    }
1304
1305    #[test]
1306    fn test_date_parse() {
1307        assert_eq!(Date::parse("1.01.01").unwrap(), Date::from_ymd(1, 1, 1));
1308    }
1309
1310    #[test]
1311    fn test_first_bin_date() {
1312        let date = Date::from_binary(56379360).unwrap();
1313        assert_eq!(date.iso_8601().to_string(), String::from("1436-01-01"));
1314    }
1315
1316    #[test]
1317    fn test_text_date_overflow() {
1318        assert!(Date::parse("1444.257.1").is_err());
1319        assert!(Date::parse("1444.1.257").is_err());
1320        assert!(Date::parse("60000.1.1").is_err());
1321        assert!(Date::parse("-60000.1.1").is_err());
1322    }
1323
1324    #[test]
1325    fn test_binary_date_overflow() {
1326        assert_eq!(Date::from_binary(999379360), None);
1327    }
1328
1329    #[test]
1330    #[should_panic]
1331    fn test_add_adds_year_overflow() {
1332        let date = Date::parse("1400.1.2").unwrap();
1333        let _ = date.add_days(100000000);
1334    }
1335
1336    #[test]
1337    #[should_panic]
1338    fn test_add_adds_day_overflow() {
1339        let date = Date::parse("1400.1.2").unwrap();
1340        let _ = date.add_days(i32::MAX);
1341    }
1342
1343    #[test]
1344    fn test_ignore_bin_dates() {
1345        // These are numbers from a savefile that shouldn't be interpreted as dates
1346        assert_eq!(Date::from_binary_heuristic(0), None);
1347        assert_eq!(Date::from_binary_heuristic(380947), None);
1348        assert_eq!(Date::from_binary_heuristic(21282204), None);
1349        assert_eq!(Date::from_binary_heuristic(33370842), None);
1350        assert_eq!(Date::from_binary_heuristic(42267422), None);
1351        assert_eq!(Date::from_binary_heuristic(693362154), None);
1352    }
1353
1354    #[test]
1355    fn test_negative_date() {
1356        // EU4 Monarch birth dates can be negative, no idea what those mean
1357        let date = Date::parse("-17.1.1").unwrap();
1358        assert_eq!(date.game_fmt().to_string(), String::from("-17.1.1"));
1359
1360        let date2 = Date::from_binary(43651080).unwrap();
1361        assert_eq!(date.game_fmt().to_string(), String::from("-17.1.1"));
1362
1363        assert_eq!(date, date2);
1364    }
1365
1366    #[rstest]
1367    #[case("-1.1.1")]
1368    #[case("-1.1.12")]
1369    #[case("-1.11.1")]
1370    #[case("-1.11.12")]
1371    #[case("-10.1.1")]
1372    #[case("-10.1.12")]
1373    #[case("-10.11.1")]
1374    #[case("-10.11.12")]
1375    #[case("-100.1.1")]
1376    #[case("-100.1.12")]
1377    #[case("-100.11.1")]
1378    #[case("-100.11.12")]
1379    #[case("-1000.1.1")]
1380    #[case("-1000.1.12")]
1381    #[case("-1000.11.1")]
1382    #[case("-1000.11.12")]
1383    #[case("1.1.1")]
1384    #[case("1.1.12")]
1385    #[case("1.11.1")]
1386    #[case("1.11.12")]
1387    #[case("10.1.1")]
1388    #[case("10.1.12")]
1389    #[case("10.11.1")]
1390    #[case("10.11.12")]
1391    #[case("100.1.1")]
1392    #[case("100.1.12")]
1393    #[case("100.11.1")]
1394    #[case("100.11.12")]
1395    #[case("1000.1.1")]
1396    #[case("1000.1.12")]
1397    #[case("1000.11.1")]
1398    #[case("1000.11.12")]
1399    #[case("1400.1.2")]
1400    #[case("1457.3.5")]
1401    #[case("1.1.1")]
1402    #[case("1444.11.11")]
1403    #[case("1444.11.30")]
1404    #[case("1444.2.19")]
1405    #[case("1444.12.3")]
1406    fn test_date_game_fmt_roundtrip(#[case] input: &str) {
1407        let s = Date::parse(input).unwrap().game_fmt().to_string();
1408        assert_eq!(&s, input);
1409    }
1410
1411    #[test]
1412    fn test_zero_date() {
1413        let date = Date::from_binary(43800000).unwrap();
1414        assert_eq!(date.iso_8601().to_string(), String::from("0000-01-01"));
1415    }
1416
1417    #[test]
1418    fn test_negative_datehour_binary() {
1419        let date = DateHour::from_binary(43791240).unwrap();
1420        assert_eq!(date.game_fmt().to_string(), String::from("-1.1.1.1"));
1421        assert_eq!(Some(date), DateHour::from_binary_heuristic(43791240));
1422    }
1423
1424    #[test]
1425    fn test_very_negative_date() {
1426        // EU4 stonehenge and pyramids
1427        let date = Date::parse("-2500.1.1").unwrap();
1428        assert_eq!(date.game_fmt().to_string(), String::from("-2500.1.1"));
1429
1430        let date2 = Date::from_binary(21900000).unwrap();
1431        assert_eq!(date2.game_fmt().to_string(), String::from("-2500.1.1"));
1432        assert_eq!(date, date2);
1433    }
1434
1435    #[test]
1436    fn test_very_negative_date2() {
1437        // EU4 monuments expanded
1438        let date = Date::parse("-10000.1.1").unwrap();
1439        assert_eq!(date.game_fmt().to_string(), String::from("-10000.1.1"));
1440
1441        let date2 = Date::from_binary(-43800000).unwrap();
1442        assert_eq!(date2.game_fmt().to_string(), String::from("-10000.1.1"));
1443        assert_eq!(date, date2);
1444    }
1445
1446    #[test]
1447    fn test_november_date_regression() {
1448        let date = Date::from_binary(56379360).unwrap().add_days(303);
1449        assert_eq!(date.iso_8601().to_string(), String::from("1436-10-31"));
1450        let date = Date::from_binary(56379360).unwrap().add_days(304);
1451        assert_eq!(date.iso_8601().to_string(), String::from("1436-11-01"));
1452        let date = Date::from_binary(56379360).unwrap().add_days(303 - 30);
1453        assert_eq!(date.iso_8601().to_string(), String::from("1436-10-01"));
1454        let date = Date::from_binary(56379360).unwrap().add_days(303 - 31);
1455        assert_eq!(date.iso_8601().to_string(), String::from("1436-09-30"));
1456        let date = Date::from_binary(56379360).unwrap().add_days(303 - 31 - 29);
1457        assert_eq!(date.iso_8601().to_string(), String::from("1436-09-01"));
1458        let date = Date::from_binary(56379360).unwrap().add_days(303 - 31 - 30);
1459        assert_eq!(date.iso_8601().to_string(), String::from("1436-08-31"));
1460    }
1461
1462    #[test]
1463    fn test_past_leap_year_bin_date() {
1464        let date = Date::from_binary(59611248).unwrap();
1465        assert_eq!(date.iso_8601().to_string(), String::from("1804-12-09"));
1466    }
1467
1468    #[test]
1469    fn test_early_leap_year_bin_date() {
1470        let date = Date::from_binary(57781584).unwrap();
1471        assert_eq!(date.iso_8601().to_string(), String::from("1596-01-27"));
1472    }
1473
1474    #[test]
1475    fn test_non_leap_year_bin_date() {
1476        let date = Date::from_binary(57775944).unwrap();
1477        assert_eq!(date.iso_8601().to_string(), String::from("1595-06-06"));
1478    }
1479
1480    #[test]
1481    fn test_early_date() {
1482        let date = Date::from_binary(43808760).unwrap();
1483        assert_eq!(date.iso_8601().to_string(), String::from("0001-01-01"));
1484    }
1485
1486    #[test]
1487    fn test_days_until() {
1488        let date = Date::parse("1400.1.2").unwrap();
1489        let date2 = Date::parse("1400.1.3").unwrap();
1490        assert_eq!(1, date.days_until(&date2));
1491    }
1492
1493    #[test]
1494    fn test_days_until2() {
1495        let date = Date::parse("1400.1.2").unwrap();
1496        let date2 = Date::parse("1401.1.2").unwrap();
1497        assert_eq!(365, date.days_until(&date2));
1498    }
1499
1500    #[test]
1501    fn test_days_until3() {
1502        let date = Date::parse("1400.1.1").unwrap();
1503        let date2 = Date::parse("1401.12.31").unwrap();
1504        assert_eq!(729, date.days_until(&date2));
1505    }
1506
1507    #[test]
1508    fn test_days_until4() {
1509        let date = Date::parse("1400.1.2").unwrap();
1510        let date2 = Date::parse("1400.1.2").unwrap();
1511        assert_eq!(0, date.days_until(&date2));
1512    }
1513
1514    #[test]
1515    fn test_days_until5() {
1516        let date = Date::parse("1400.1.1").unwrap();
1517        let date2 = Date::parse("1401.12.31").unwrap();
1518        assert_eq!(-729, date2.days_until(&date));
1519    }
1520
1521    #[test]
1522    fn test_add_days() {
1523        let date = Date::parse("1400.1.2").unwrap();
1524        let actual = date.add_days(1);
1525        let expected = Date::parse("1400.1.3").unwrap();
1526        assert_eq!(actual, expected);
1527    }
1528
1529    #[test]
1530    fn test_add_days2() {
1531        let date = Date::parse("1400.1.2").unwrap();
1532        let actual = date.add_days(365);
1533        let expected = Date::parse("1401.1.2").unwrap();
1534        assert_eq!(actual, expected);
1535    }
1536
1537    #[test]
1538    fn test_add_days3() {
1539        let date = Date::parse("1400.1.1").unwrap();
1540        let actual = date.add_days(729);
1541        let expected = Date::parse("1401.12.31").unwrap();
1542        assert_eq!(actual, expected);
1543    }
1544
1545    #[test]
1546    fn test_add_days4() {
1547        let date = Date::parse("1400.1.2").unwrap();
1548        let actual = date.add_days(0);
1549        let expected = Date::parse("1400.1.2").unwrap();
1550        assert_eq!(actual, expected);
1551    }
1552
1553    #[test]
1554    fn test_all_days() {
1555        let start = Date::parse("1400.1.1").unwrap();
1556        for i in 0..364 {
1557            let (month, day) = month_day_from_julian(i);
1558            let next = Date::parse(format!("1400.{}.{}", month, day)).unwrap();
1559            assert_eq!(start.add_days(i), next);
1560            assert_eq!(start.days_until(&next), i);
1561        }
1562    }
1563
1564    #[test]
1565    fn test_cmp() {
1566        let date = Date::parse("1457.3.5").unwrap();
1567        let date2 = Date::parse("1457.3.4").unwrap();
1568        assert!(date2 < date);
1569    }
1570
1571    #[test]
1572    fn test_binary_date_regression() {
1573        let input = i32::from_le_bytes([14, 54, 43, 253]);
1574        let _ = Date::from_binary(input);
1575    }
1576
1577    #[test]
1578    fn test_day_overflow_regression() {
1579        let _ = Date::from_ymd_opt(1222, 12, 222);
1580    }
1581
1582    #[test]
1583    fn test_date_days() {
1584        let date = Date::parse("1.1.1").unwrap();
1585        assert_eq!(date.days(), 365);
1586
1587        let date = Date::parse("-1.1.1").unwrap();
1588        assert_eq!(date.days(), -365);
1589
1590        let date = Date::parse("-1.1.2").unwrap();
1591        assert_eq!(date.days(), -366);
1592
1593        let date = Date::parse("-1.2.2").unwrap();
1594        assert_eq!(date.days(), -397);
1595    }
1596
1597    #[test]
1598    fn test_negative_date_math() {
1599        let date = Date::parse("-1.1.2").unwrap();
1600        let d1 = date.add_days(1);
1601        assert_eq!(d1.game_fmt().to_string(), "-1.1.1");
1602        assert_eq!(date.days_until(&d1), 1);
1603
1604        let date = Date::parse("-3.6.3").unwrap();
1605        let d1 = date.add_days(1);
1606        assert_eq!(d1.game_fmt().to_string(), "-3.6.2");
1607        assert_eq!(date.days_until(&d1), 1);
1608    }
1609
1610    #[test]
1611    fn test_datehour_roundtrip() {
1612        let date = DateHour::parse("1936.1.1.24").unwrap();
1613        assert_eq!(date.iso_8601().to_string(), String::from("1936-01-01T23"));
1614    }
1615
1616    #[test]
1617    fn test_date_zeros_hour() {
1618        let data = i32::from_le_bytes([0x4b, 0x1d, 0x9f, 0x03]);
1619        let date = Date::from_binary(data).unwrap();
1620        let date_hour = DateHour::from_binary(data).unwrap();
1621        assert_eq!(date.iso_8601().to_string(), String::from("1936-01-01"));
1622        assert_eq!(
1623            date_hour.iso_8601().to_string(),
1624            String::from("1936-01-01T11")
1625        );
1626    }
1627
1628    #[test]
1629    fn test_non_zero_binary_hours_are_not_heuristic_dates() {
1630        let data = i32::from_le_bytes([0x4b, 0x1d, 0x9f, 0x03]);
1631        assert_eq!(Date::from_binary_heuristic(data), None);
1632    }
1633
1634    #[test]
1635    fn test_date_disallow_hour_parse_str() {
1636        assert!(Date::parse("1936.1.1.0").is_err())
1637    }
1638
1639    #[test]
1640    fn test_date_state_of_wide_number() {
1641        assert_eq!(Date::parse("1936.01.01"), Ok(Date::from_ymd(1936, 1, 1)));
1642        assert_eq!(
1643            DateHour::parse("1936.01.01.12"),
1644            Ok(DateHour::from_ymdh(1936, 1, 1, 12))
1645        );
1646    }
1647
1648    #[test]
1649    fn test_date_to_binary() {
1650        let date = Date::from_ymd(1, 1, 1);
1651        let bin = date.to_binary();
1652        assert_eq!(Date::from_binary(bin).unwrap(), date);
1653        assert_eq!(Date::from_binary_heuristic(bin).unwrap(), date);
1654    }
1655
1656    #[test]
1657    fn test_date_hour_to_binary() {
1658        let date = DateHour::from_ymdh(1, 1, 1, 1);
1659        let bin = date.to_binary();
1660        assert_eq!(DateHour::from_binary(bin).unwrap(), date);
1661        assert_eq!(DateHour::from_binary_heuristic(bin).unwrap(), date);
1662
1663        assert_eq!(DateHour::from_binary_heuristic(1), None);
1664    }
1665
1666    #[test]
1667    fn test_uniform_date() {
1668        let date = UniformDate::from_ymd(2205, 2, 30);
1669        assert_eq!(date.iso_8601().to_string(), String::from("2205-02-30"));
1670        assert_eq!(date.game_fmt().to_string(), String::from("2205.02.30"));
1671
1672        let date2 = UniformDate::parse("2205.02.30").unwrap();
1673        assert_eq!(date, date2);
1674
1675        let date3 = UniformDate::parse("1.01.01").unwrap();
1676        assert_eq!(date3.game_fmt().to_string(), String::from("1.01.01"));
1677    }
1678
1679    #[test]
1680    fn test_date_converted_into_number() {
1681        // So that we can decode paperman melted saves where a date is
1682        // detected as a number
1683        assert!(RawDate::parse(b"43808760").is_err());
1684        assert_eq!(Date::parse(b"43808760").unwrap(), Date::from_ymd(1, 1, 1));
1685    }
1686
1687    #[test]
1688    fn test_from_str_impl() {
1689        let _date: RawDate = "1444.11.11".parse().unwrap();
1690        let _date: Date = "1444.11.11".parse().unwrap();
1691        let _date: DateHour = "1936.1.1.1".parse().unwrap();
1692        let _date: UniformDate = "2200.01.01".parse().unwrap();
1693    }
1694
1695    #[test]
1696    fn test_date_hour_negative_regression() {
1697        assert!(Date::from_binary(-1).is_none());
1698        assert!(DateHour::from_binary(-1).is_none());
1699        assert!(Date::from_binary(-24).is_none());
1700    }
1701
1702    #[test]
1703    fn test_date_parse_edge_cases() {
1704        assert!(Date::parse("05.5.3`.3").is_err());
1705    }
1706
1707    #[test]
1708    fn test_memory_size() {
1709        // https://users.rust-lang.org/t/guidelines-for-self-ownership-on-copy-types/61262/2
1710        assert!(std::mem::size_of::<Date>() <= 2 * std::mem::size_of::<usize>());
1711    }
1712
1713    #[quickcheck]
1714    fn test_binary_date_equality(data: i32) -> bool {
1715        Date::from_binary(data)
1716            .map(|x| x == Date::from_binary(x.to_binary()).unwrap())
1717            .unwrap_or(true)
1718    }
1719
1720    #[quickcheck]
1721    fn test_binary_date_hour_equality(data: i32) -> bool {
1722        DateHour::from_binary(data)
1723            .map(|x| x == DateHour::from_binary(x.to_binary()).unwrap())
1724            .unwrap_or(true)
1725    }
1726}