Skip to main content

ttf_view/types/
longdatetime.rs

1use crate::util::DisplayBuffer;
2use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc};
3use std::fmt::Write;
4
5/// The [OpenType LongDateTime][spec] type.
6///
7/// `LongDateTime` is stored as a number of seconds since `1904-01-01T00:00:00Z`, and covers a
8/// riduculously large range of dates, extending to 278 billions of years before the formation of
9/// the universe (~13.8 billion BCE), and into the far-far future.
10///
11/// ```text
12/// chrono crate:         -262_143-01-01 00:00:00 UTC ..=         +262_142-12-31 23:59:59 UTC
13/// LongDateTime: -292_277_022_723-01-25 08:29:52 UTC ..= +292_277_026_530-12-04 15:30:07 UTC
14/// ```
15///
16/// Needless to say, `LongDateTime`'s formatting as civil time probably won't be accurate for really
17/// far away dates, since the Earth did not spin before the universe came into existence, and also
18/// because this implementation does not account for variable length of day, which will likely grow
19/// more than tenfold before an overflow occurs (if the Earth is still somehow not destroyed).
20///
21/// # Formatting
22///
23/// `LongDateTime`'s formatting impls mirror those of [`chrono::DateTime`].
24///
25/// [`Display`][std::fmt::Display] formats `LongDateTime` as a human-readable timestamp.
26///
27/// ```
28/// use ttf_view::types::LongDateTime;
29///
30/// // You can also use .to_string()
31/// let dt = LongDateTime::from_epoch_seconds(-1535200423);
32/// assert_eq!(format!("{}", dt), "1855-05-08 11:26:17 UTC");
33/// let dt = LongDateTime::from_epoch_seconds(3478852446);
34/// assert_eq!(format!("{}", dt), "2014-03-28 11:54:06 UTC");
35/// ```
36///
37/// [`Debug`][std::fmt::Debug] formats `LongDateTime` as an RFC 3339 and ISO 8601 timestamp.
38///
39/// ```
40/// use ttf_view::types::LongDateTime;
41///
42/// let dt = LongDateTime::from_epoch_seconds(-1535200423);
43/// assert_eq!(format!("{:?}", dt), "1855-05-08T11:26:17Z");
44/// let dt = LongDateTime::from_epoch_seconds(3478852446);
45/// assert_eq!(format!("{:?}", dt), "2014-03-28T11:54:06Z");
46/// ```
47///
48/// [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#data-types
49#[derive(Copy, Hash)]
50#[derive_const(Clone, PartialEq, Eq)]
51#[repr(transparent)]
52pub struct LongDateTime([u8; 8]);
53
54const EPOCH_NAIVE: NaiveDateTime =
55    NaiveDate::from_ymd_opt(1904, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
56
57impl LongDateTime {
58    /// The point of reference from which `LongDateTime` counts seconds.
59    ///
60    /// ```
61    /// # use ttf_view::types::LongDateTime;
62    /// assert_eq!(LongDateTime::EPOCH.to_string(), "1904-01-01 00:00:00 UTC");
63    /// ```
64    pub const EPOCH: DateTime<Utc> = EPOCH_NAIVE.and_utc();
65    /// The smallest representable [`LongDateTime`].
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// # use ttf_view::types::LongDateTime;
71    /// assert_eq!(LongDateTime::MIN.to_string(), "-292277022723-01-25 08:29:52 UTC");
72    /// ```
73    pub const MIN: Self = Self::from_epoch_seconds(i64::MIN);
74    /// The largest representable [`LongDateTime`].
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// # use ttf_view::types::LongDateTime;
80    /// assert_eq!(LongDateTime::MAX.to_string(), "+292277026530-12-04 15:30:07 UTC");
81    /// ```
82    pub const MAX: Self = Self::from_epoch_seconds(i64::MAX);
83
84    /// Creates a [`LongDateTime`] from [`chrono::DateTime<Utc>`]. Truncates sub-seconds.
85    ///
86    /// Always succeeds, since `chrono::DateTime`'s range is a subset of `LongDateTime`'s.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use chrono::{DateTime, Utc};
92    /// use ttf_view::types::LongDateTime;
93    ///
94    /// let dt: DateTime<Utc> = "2026-09-06 11:11:36.562 UTC".parse().unwrap();
95    /// assert_eq!(LongDateTime::new(dt).to_string(), "2026-09-06 11:11:36 UTC");
96    /// ```
97    pub const fn new(datetime: DateTime<Utc>) -> Self {
98        let delta = datetime.naive_utc().signed_duration_since(EPOCH_NAIVE);
99        Self::from_epoch_seconds(delta.num_seconds())
100    }
101    /// Returns a [`chrono::DateTime<Utc>`] representing this [`LongDateTime`] value.
102    ///
103    /// Returns `None` if it's out of `chrono::DateTime`'s range.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use chrono::{DateTime, Utc};
109    /// use ttf_view::types::LongDateTime;
110    ///
111    /// let long = LongDateTime::from_epoch_seconds(0xFFFFFFFF);
112    /// assert_eq!(long.to_string(), "2040-02-06 06:28:15 UTC");
113    /// assert_eq!(long.datetime().unwrap().to_string(), "2040-02-06 06:28:15 UTC");
114    ///
115    /// // chrono::DateTime is limited to ±262000 years.
116    /// let long = LongDateTime::from_epoch_seconds(0xFFFFFFFFFFFFFF);
117    /// assert_eq!(long.to_string(), "+2283416158-11-23 12:52:15 UTC");
118    /// assert_eq!(long.datetime(), None);
119    /// ```
120    pub const fn datetime(&self) -> Option<DateTime<Utc>> {
121        let delta = TimeDelta::try_seconds(self.epoch_seconds())?;
122        Some(EPOCH_NAIVE.checked_add_signed(delta)?.and_utc())
123    }
124
125    /// Creates a [`LongDateTime`] from the number of seconds since [`EPOCH`][Self::EPOCH]
126    /// (`1904-01-01 00:00:00 UTC`).
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use ttf_view::types::LongDateTime;
132    ///
133    /// assert_eq!(LongDateTime::from_epoch_seconds(-1535200423).to_string(), "1855-05-08 11:26:17 UTC");
134    /// assert_eq!(LongDateTime::from_epoch_seconds(3478852446).to_string(), "2014-03-28 11:54:06 UTC");
135    /// ```
136    pub const fn from_epoch_seconds(secs: i64) -> Self {
137        Self(i64::to_be_bytes(secs))
138    }
139    /// Returns the number of seconds from [`EPOCH`][Self::EPOCH] (`1904-01-01 00:00:00 UTC`)
140    /// to this [`LongDateTime`]'s value.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use chrono::{DateTime, Utc};
146    /// use ttf_view::types::LongDateTime;
147    ///
148    /// let dt: DateTime<Utc> = "1855-05-08 11:26:17 UTC".parse().unwrap();
149    /// assert_eq!(LongDateTime::new(dt).epoch_seconds(), -1535200423);
150    /// let dt: DateTime<Utc> = "2014-03-28 11:54:06 UTC".parse().unwrap();
151    /// assert_eq!(LongDateTime::new(dt).epoch_seconds(), 3478852446);
152    /// ```
153    pub const fn epoch_seconds(&self) -> i64 {
154        i64::from_be_bytes(self.0)
155    }
156
157    /// Creates a [`LongDateTime`] from big-endian bytes.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use ttf_view::types::LongDateTime;
163    ///
164    /// let raw = [0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x7E, 0xB3, 0x59];
165    /// assert_eq!(LongDateTime::from_be_bytes(raw).to_string(), "1855-05-08 11:26:17 UTC");
166    /// let raw = [0x00, 0x00, 0x00, 0x00, 0xCF, 0x5B, 0x13, 0x5E];
167    /// assert_eq!(LongDateTime::from_be_bytes(raw).to_string(), "2014-03-28 11:54:06 UTC");
168    /// ```
169    pub const fn from_be_bytes(bytes: [u8; 8]) -> Self {
170        Self(bytes)
171    }
172    /// Returns this [`LongDateTime`]'s big-endian bytes.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use chrono::{DateTime, Utc};
178    /// use ttf_view::types::LongDateTime;
179    ///
180    /// let dt: DateTime<Utc> = "1855-05-08 11:26:17 UTC".parse().unwrap();
181    /// let raw = [0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x7E, 0xB3, 0x59];
182    /// assert_eq!(LongDateTime::new(dt).to_be_bytes(), raw);
183    ///
184    /// let dt: DateTime<Utc> = "2014-03-28 11:54:06 UTC".parse().unwrap();
185    /// let raw = [0x00, 0x00, 0x00, 0x00, 0xCF, 0x5B, 0x13, 0x5E];
186    /// assert_eq!(LongDateTime::new(dt).to_be_bytes(), raw);
187    /// ```
188    pub const fn to_be_bytes(self) -> [u8; 8] {
189        self.0
190    }
191}
192
193// TODO: When [u8; 8]'s Default is constified, replace this impl with #[derive_const]
194#[allow(clippy::derivable_impls)]
195const impl Default for LongDateTime {
196    fn default() -> Self {
197        Self([0; 8])
198    }
199}
200// Note: PartialEq + Eq impls need to be explicit, because [u8; 8] is compared unsignedly.
201const impl PartialOrd for LongDateTime {
202    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
203        Some(self.cmp(other))
204    }
205}
206const impl Ord for LongDateTime {
207    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
208        self.epoch_seconds().cmp(&other.epoch_seconds())
209    }
210}
211
212const impl From<DateTime<Utc>> for LongDateTime {
213    fn from(value: DateTime<Utc>) -> Self {
214        Self::new(value)
215    }
216}
217const impl TryFrom<LongDateTime> for DateTime<Utc> {
218    type Error = ();
219    fn try_from(value: LongDateTime) -> Result<Self, Self::Error> {
220        value.datetime().ok_or(())
221    }
222}
223
224/// Formats [`LongDateTime`] as a human-readable timestamp. [See more above](#formatting)
225impl std::fmt::Display for LongDateTime {
226    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
227        fmt_longdatetime(*self, false, f)
228    }
229}
230/// Formats [`LongDateTime`] as an RFC 3339 and ISO 8601 timestamp. [See more above](#formatting)
231impl std::fmt::Debug for LongDateTime {
232    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
233        fmt_longdatetime(*self, true, f)
234    }
235}
236
237fn fmt_longdatetime(ldt: LongDateTime, iso: bool, f: &mut std::fmt::Formatter) -> std::fmt::Result {
238    let epoch_secs = ldt.epoch_seconds();
239
240    const SECS_PER_DAY: i64 = 24 * 60 * 60;
241    let epoch_days = epoch_secs.div_euclid(SECS_PER_DAY);
242    let epoch_secs = epoch_secs.rem_euclid(SECS_PER_DAY);
243
244    // Convert "days since 1904" to "days since 1970"
245    let (year, month, day) = ymd_from_days(epoch_days - 24107);
246
247    // Max length is 32: "-292277022723-01-25 08:29:52 UTC"
248    let mut buf = DisplayBuffer::<32>::new();
249
250    if year > 9999 {
251        // ISO 8601 requires the explicit sign for out-of-range years
252        buf.write_byte_unchecked(b'+');
253    } else if year < 0 {
254        // Write minus separately to avoid counting it towards min width in "{:04}"
255        buf.write_byte_unchecked(b'-');
256    }
257    let year = year.unsigned_abs();
258
259    // Write "{:04}" with year (fast path for 0..=9999)
260    if matches!(year, 0..=9999) {
261        buf.write_two_digits_unchecked((year / 100) as u8);
262        buf.write_two_digits_unchecked((year % 100) as u8);
263    } else {
264        write!(buf, "{:04}", year)?;
265    }
266
267    // Write "-{:02}-{:02}" with month and day
268    buf.write_byte_unchecked(b'-');
269    buf.write_two_digits_unchecked(month);
270    buf.write_byte_unchecked(b'-');
271    buf.write_two_digits_unchecked(day);
272
273    // In Debug use 'T' as separator, and in Display - ' '
274    buf.write_byte_unchecked(if iso { b'T' } else { b' ' });
275
276    // Write "{:02}:{:02}:{:02}" with hour, min, sec
277    buf.write_two_digits_unchecked((epoch_secs / 3600) as u8);
278    buf.write_byte_unchecked(b':');
279    buf.write_two_digits_unchecked(((epoch_secs % 3600) / 60) as u8);
280    buf.write_byte_unchecked(b':');
281    buf.write_two_digits_unchecked((epoch_secs % 60) as u8);
282
283    // In Debug use 'Z' for UTC, and in Display - " UTC"
284    if iso {
285        buf.write_byte_unchecked(b'Z');
286    } else {
287        buf.write_str_unchecked(" UTC");
288    }
289
290    f.write_str(buf.as_str())
291}
292
293fn ymd_from_days(unix_days: i64) -> (i64, u8, u8) {
294    // See https://howardhinnant.github.io/date_algorithms.html#civil_from_days
295    let z = unix_days + 719468;
296    let era = if z >= 0 { z } else { z - 146096 } / 146097;
297    let doe = (z - era * 146097) as u64;
298    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
299    let y = yoe as i64 + era * 400;
300    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
301    let mp = (5 * doy + 2) / 153;
302    let d = doy - (153 * mp + 2) / 5 + 1;
303    let m = if mp < 10 { mp + 3 } else { mp - 9 };
304    (y + (m <= 2) as i64, m as u8, d as u8)
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn longdatetimes() {
313        let dates: [(u64, &'static str); _] = [
314            (0x8000000000000000, "-292277022723-01-25T08:29:52Z"),
315            (0xFFFFF00000000000, "-555571-10-24T14:19:44Z"),
316            (0xFFFFFF0000000000, "-32939-11-10T23:23:44Z"),
317            (0xFFFFFFF000000000, "-0274-05-13T16:27:44Z"),
318            (0xFFFFFFF200D00100, "-0001-01-01T00:00:00Z"),
319            (0xFFFFFFF202B13480, "0000-01-01T00:00:00Z"),
320            (0xFFFFFFF20493B980, "0001-01-01T00:00:00Z"),
321            (0x0000000000000000, "1904-01-01T00:00:00Z"),
322            (0x00000000E3D1B1DE, "2025-02-12T02:03:10Z"),
323            (0x00000000E6B686E6, "2026-08-28T00:29:26Z"),
324            (0x00000000FFFFFFFF, "2040-02-06T06:28:15Z"),
325            (0x000000FFFFFFFFFF, "+36746-02-19T00:36:15Z"),
326            (0x0000FFFFFFFFFFFF, "+8921490-12-06T10:44:15Z"),
327            (0x00FFFFFFFFFFFFFF, "+2283416158-11-23T12:52:15Z"),
328            (0x7FFFFFFFFFFFFFFF, "+292277026530-12-04T15:30:07Z"),
329        ];
330
331        for (stamp, iso_date) in dates {
332            let stamp = LongDateTime::from_epoch_seconds(stamp as i64);
333            assert_eq!(format!("{:?}", stamp), iso_date);
334
335            if let Some(chrono_date) = stamp.datetime() {
336                assert_eq!(format!("{:?}", chrono_date), iso_date);
337                assert_eq!(stamp, LongDateTime::new(chrono_date));
338            }
339        }
340    }
341}