Skip to main content

logicaffeine_system/
temporal.rs

1//! Temporal types for Logicaffeine.
2//!
3//! Provides Date and Moment types that complement std::time::Duration.
4
5use std::fmt::{self, Display};
6
7/// Date stored as days since Unix epoch (1970-01-01).
8///
9/// Range: ±5.8 million years from epoch.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct LogosDate(pub i32);
12
13impl LogosDate {
14    /// Create a new date from days since Unix epoch.
15    #[inline]
16    pub fn new(days: i32) -> Self {
17        Self(days)
18    }
19
20    /// Get the raw days value.
21    #[inline]
22    pub fn days(&self) -> i32 {
23        self.0
24    }
25
26    /// UTC calendar components — AOT mirrors of `year_of`/`month_of`/`day_of`/`weekday_of` on a Date.
27    #[inline]
28    pub fn year(&self) -> i64 {
29        self.to_ymd().0
30    }
31    #[inline]
32    pub fn month(&self) -> i64 {
33        self.to_ymd().1
34    }
35    #[inline]
36    pub fn day(&self) -> i64 {
37        self.to_ymd().2
38    }
39    #[inline]
40    pub fn weekday(&self) -> i64 {
41        logicaffeine_base::temporal::weekday_from_days(self.0 as i64) as i64
42    }
43    /// The ISO-8601 week number (1..=53) — the AOT mirror of `week_of` on a Date.
44    #[inline]
45    pub fn iso_week(&self) -> i64 {
46        logicaffeine_base::temporal::iso_week_from_days(self.0 as i64).1 as i64
47    }
48    /// The calendar quarter (1..=4) — the AOT mirror of `quarter_of` on a Date.
49    #[inline]
50    pub fn quarter(&self) -> i64 {
51        (self.month() - 1) / 3 + 1
52    }
53
54    /// Convert to year, month, day using Howard Hinnant's algorithm.
55    pub fn to_ymd(&self) -> (i64, i64, i64) {
56        let z = self.0 as i64 + 719468; // shift epoch
57        let era = if z >= 0 { z } else { z - 146096 } / 146097;
58        let doe = z - era * 146097;
59        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
60        let y = yoe + era * 400;
61        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
62        let mp = (5 * doy + 2) / 153;
63        let d = doy - (153 * mp + 2) / 5 + 1;
64        let m = mp + if mp < 10 { 3 } else { -9 };
65        let year = y + if m <= 2 { 1 } else { 0 };
66        (year, m, d)
67    }
68}
69
70impl Display for LogosDate {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        let (year, month, day) = self.to_ymd();
73        write!(f, "{:04}-{:02}-{:02}", year, month, day)
74    }
75}
76
77impl crate::io::Showable for LogosDate {
78    fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        Display::fmt(self, f)
80    }
81}
82
83/// Moment stored as nanoseconds since Unix epoch.
84///
85/// Provides nanosecond precision for timestamps.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
87pub struct LogosMoment(pub i64);
88
89impl LogosMoment {
90    /// Create a new moment from nanoseconds since epoch.
91    #[inline]
92    pub fn new(nanos: i64) -> Self {
93        Self(nanos)
94    }
95
96    /// Get the raw nanoseconds value.
97    #[inline]
98    pub fn nanos(&self) -> i64 {
99        self.0
100    }
101
102    /// Parse an RFC 3339 / ISO 8601 timestamp into a moment — the AOT mirror of the interpreter's
103    /// `parse_timestamp`. Panics on malformed input (the front-end has validated it before codegen).
104    #[inline]
105    pub fn parse_rfc3339(s: &str) -> Self {
106        Self(
107            logicaffeine_base::temporal::parse_rfc3339(s)
108                .expect("LOGOS runtime error: malformed RFC 3339 timestamp"),
109        )
110    }
111
112    /// Render this moment as an RFC 3339 / ISO 8601 UTC string — the AOT mirror of `format_timestamp`.
113    #[inline]
114    pub fn format_rfc3339(&self) -> String {
115        logicaffeine_base::temporal::format_rfc3339(self.0)
116    }
117
118    /// UTC calendar components — the AOT mirrors of `year_of`/`month_of`/`day_of`/`weekday_of`.
119    #[inline]
120    pub fn year(&self) -> i64 {
121        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).year
122    }
123    #[inline]
124    pub fn month(&self) -> i64 {
125        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).month as i64
126    }
127    #[inline]
128    pub fn day(&self) -> i64 {
129        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).day as i64
130    }
131    #[inline]
132    pub fn weekday(&self) -> i64 {
133        logicaffeine_base::temporal::weekday_from_days(
134            self.0.div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY),
135        ) as i64
136    }
137    #[inline]
138    pub fn hour(&self) -> i64 {
139        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).hour as i64
140    }
141    #[inline]
142    pub fn minute(&self) -> i64 {
143        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).minute as i64
144    }
145    #[inline]
146    pub fn second(&self) -> i64 {
147        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).second as i64
148    }
149
150    /// The calendar day this moment falls on (UTC) — the AOT mirror of `date_of`.
151    #[inline]
152    pub fn date(&self) -> LogosDate {
153        LogosDate(self.0.div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY) as i32)
154    }
155
156    /// The wall-clock time-of-day (UTC) — the AOT mirror of `time_of`.
157    #[inline]
158    pub fn time_of_day(&self) -> LogosTime {
159        LogosTime(self.0.rem_euclid(logicaffeine_base::temporal::NANOS_PER_DAY))
160    }
161
162    /// The ISO-8601 week number (1..=53) — the AOT mirror of `week_of`.
163    #[inline]
164    pub fn iso_week(&self) -> i64 {
165        let days = self.0.div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY);
166        logicaffeine_base::temporal::iso_week_from_days(days).1 as i64
167    }
168
169    /// The calendar quarter (1..=4) — the AOT mirror of `quarter_of`.
170    #[inline]
171    pub fn quarter(&self) -> i64 {
172        (self.month() - 1) / 3 + 1
173    }
174
175    /// The moment `seconds` later (AOT mirror of `add_seconds`).
176    #[inline]
177    pub fn add_seconds(&self, seconds: i64) -> Self {
178        Self(self.0 + seconds * 1_000_000_000)
179    }
180
181    /// Whole seconds from `self` to `other` (AOT mirror of `seconds_between`).
182    #[inline]
183    pub fn seconds_until(&self, other: &LogosMoment) -> i64 {
184        (other.0 - self.0) / 1_000_000_000
185    }
186
187    /// Complete calendar months / years from `self` to `other` — AOT mirrors of
188    /// `months_between` / `years_between` (signed, end-of-month-clamping correct).
189    #[inline]
190    pub fn months_until(&self, other: &LogosMoment) -> i64 {
191        logicaffeine_base::temporal::months_between(self.0, other.0)
192    }
193    #[inline]
194    pub fn years_until(&self, other: &LogosMoment) -> i64 {
195        logicaffeine_base::temporal::years_between(self.0, other.0)
196    }
197
198    /// The local wall-clock time (with offset) in a named zone — AOT mirror of `in_zone`. Panics on
199    /// an unknown zone, which the front-end has validated (it errors cleanly in the interpreter).
200    pub fn in_zone(&self, zone_name: &str) -> String {
201        logicaffeine_base::temporal::format_zoned(self.0, zone_name)
202            .unwrap_or_else(|| panic!("LOGOS runtime error: unknown time zone '{zone_name}'"))
203    }
204
205    /// The local-as-UTC instant in a named zone — AOT mirror of `local_instant`. Composing a UTC
206    /// component accessor onto the result reads the zone's LOCAL field (`the hour of m in "<zone>"`).
207    pub fn local_instant(&self, zone_name: &str) -> LogosMoment {
208        LogosMoment(
209            logicaffeine_base::temporal::local_instant_nanos(self.0, zone_name)
210                .unwrap_or_else(|| panic!("LOGOS runtime error: unknown time zone '{zone_name}'")),
211        )
212    }
213
214    /// Get current moment (now).
215    #[cfg(not(target_arch = "wasm32"))]
216    pub fn now() -> Self {
217        use std::time::{SystemTime, UNIX_EPOCH};
218        let duration = SystemTime::now()
219            .duration_since(UNIX_EPOCH)
220            .unwrap_or_default();
221        Self(duration.as_nanos() as i64)
222    }
223}
224
225// `Moment + Duration` / `Moment − Duration` → a shifted Moment (the AOT mirror of the tree-walker's
226// arith). Duration is `std::time::Duration` on this path; the nanos add/subtract on the i64 instant.
227impl std::ops::Add<std::time::Duration> for LogosMoment {
228    type Output = LogosMoment;
229    #[inline]
230    fn add(self, rhs: std::time::Duration) -> LogosMoment {
231        LogosMoment(self.0.wrapping_add(rhs.as_nanos() as i64))
232    }
233}
234
235impl std::ops::Sub<std::time::Duration> for LogosMoment {
236    type Output = LogosMoment;
237    #[inline]
238    fn sub(self, rhs: std::time::Duration) -> LogosMoment {
239        LogosMoment(self.0.wrapping_sub(rhs.as_nanos() as i64))
240    }
241}
242
243// `Moment + Span` / `Moment − Span` is CIVIL calendar arithmetic — months clamp at end-of-month,
244// leap years are respected, the wall time is preserved — distinct from the physical Duration path
245// above. Delegates to the same `base::temporal::add_span` the interpreter's `moment_add_span` uses,
246// so the AOT tier stays byte-identical to the tree-walker / VM.
247impl LogosMoment {
248    #[inline]
249    fn add_span(self, months: i32, days: i32) -> LogosMoment {
250        let dt = logicaffeine_base::temporal::civil_from_unix_nanos(self.0);
251        let shifted = logicaffeine_base::temporal::add_span(dt, months as i64, days as i64);
252        LogosMoment(logicaffeine_base::temporal::unix_nanos_from_civil(shifted))
253    }
254}
255
256impl std::ops::Add<LogosSpan> for LogosMoment {
257    type Output = LogosMoment;
258    #[inline]
259    fn add(self, rhs: LogosSpan) -> LogosMoment {
260        self.add_span(rhs.months, rhs.days)
261    }
262}
263
264impl std::ops::Sub<LogosSpan> for LogosMoment {
265    type Output = LogosMoment;
266    #[inline]
267    fn sub(self, rhs: LogosSpan) -> LogosMoment {
268        self.add_span(-rhs.months, -rhs.days)
269    }
270}
271
272impl Display for LogosMoment {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        // For now, just show as ISO-ish format with nanosecond precision
275        let nanos = self.0;
276        let seconds = nanos / 1_000_000_000;
277        let remainder = nanos % 1_000_000_000;
278        write!(f, "Moment({}s + {}ns)", seconds, remainder)
279    }
280}
281
282impl crate::io::Showable for LogosMoment {
283    fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
284        Display::fmt(self, f)
285    }
286}
287
288/// Calendar span with separate month and day components.
289///
290/// Months and days are kept separate because they're **incommensurable**:
291/// - "1 month" is 28-31 days depending on the month
292/// - You can't convert months to days without knowing the reference date
293///
294/// Years fold into months (1 year = 12 months).
295/// Weeks fold into days (1 week = 7 days).
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub struct LogosSpan {
298    /// Total months (years * 12 + months)
299    pub months: i32,
300    /// Total days (weeks * 7 + days)
301    pub days: i32,
302}
303
304impl LogosSpan {
305    /// Create a new span from months and days.
306    pub fn new(months: i32, days: i32) -> Self {
307        Self { months, days }
308    }
309
310    /// Create a span from years, months, and days.
311    /// Years are folded into months (1 year = 12 months).
312    pub fn from_years_months_days(years: i32, months: i32, days: i32) -> Self {
313        Self {
314            months: years * 12 + months,
315            days,
316        }
317    }
318
319    /// Create a span from weeks and days.
320    /// Weeks are folded into days (1 week = 7 days).
321    pub fn from_weeks_days(weeks: i32, days: i32) -> Self {
322        Self {
323            months: 0,
324            days: weeks * 7 + days,
325        }
326    }
327
328    /// Negate the span (for "ago" operator).
329    pub fn negate(&self) -> Self {
330        Self {
331            months: -self.months,
332            days: -self.days,
333        }
334    }
335}
336
337impl Display for LogosSpan {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        let mut parts = Vec::new();
340
341        // Extract years from months
342        let years = self.months / 12;
343        let remaining_months = self.months % 12;
344
345        if years != 0 {
346            parts.push(if years.abs() == 1 {
347                format!("{} year", years)
348            } else {
349                format!("{} years", years)
350            });
351        }
352
353        if remaining_months != 0 {
354            parts.push(if remaining_months.abs() == 1 {
355                format!("{} month", remaining_months)
356            } else {
357                format!("{} months", remaining_months)
358            });
359        }
360
361        if self.days != 0 || parts.is_empty() {
362            parts.push(if self.days.abs() == 1 {
363                format!("{} day", self.days)
364            } else {
365                format!("{} days", self.days)
366            });
367        }
368
369        write!(f, "{}", parts.join(" and "))
370    }
371}
372
373impl crate::io::Showable for LogosSpan {
374    fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
375        Display::fmt(self, f)
376    }
377}
378
379/// Time-of-day stored as nanoseconds from midnight — the wall-clock face of a `Moment`, with no
380/// date or zone. Renders `HH:MM:SS[.frac]`, lossless to the nanosecond (the AOT mirror of the
381/// interpreter's `Time`).
382#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
383pub struct LogosTime(pub i64);
384
385impl LogosTime {
386    #[inline]
387    pub fn new(nanos_from_midnight: i64) -> Self {
388        Self(nanos_from_midnight)
389    }
390
391    /// The raw nanoseconds-from-midnight value.
392    #[inline]
393    pub fn nanos(&self) -> i64 {
394        self.0
395    }
396}
397
398impl Display for LogosTime {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        write!(f, "{}", logicaffeine_base::temporal::format_time_of_day(self.0))
401    }
402}
403
404impl crate::io::Showable for LogosTime {
405    fn format_show(&self, f: &mut fmt::Formatter) -> fmt::Result {
406        Display::fmt(self, f)
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn date_to_ymd_epoch() {
416        let date = LogosDate(0);
417        let (y, m, d) = date.to_ymd();
418        assert_eq!((y, m, d), (1970, 1, 1));
419    }
420
421    #[test]
422    fn date_display() {
423        let date = LogosDate(20593); // 2026-05-20
424        assert_eq!(date.to_string(), "2026-05-20");
425    }
426}