Skip to main content

logicaffeine_data/
temporal.rs

1//! The temporal values of a compiled program: a calendar date, an instant, a time of day and a calendar span,
2//! with the arithmetic and the printing every tier shares (base's `temporal`), and each as an [`OtherValue`] a
3//! [`LogosValue`](crate::exact::LogosValue) holds, so the exact extension's values meet them as the
4//! interpreter's do. Reading the clock is the system's (`logicaffeine_system::temporal::now`).
5
6use std::fmt::{self, Display};
7
8/// Date stored as days since Unix epoch (1970-01-01).
9///
10/// Range: ±5.8 million years from epoch.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct LogosDate(pub i32);
13
14impl LogosDate {
15    /// Create a new date from days since Unix epoch.
16    #[inline]
17    pub fn new(days: i32) -> Self {
18        Self(days)
19    }
20
21    /// Construct a date from civil `(year, month, day)` via base `days_from_civil` — the AOT mirror
22    /// of the `makeDate` builtin.
23    #[inline]
24    pub fn from_ymd(year: i64, month: i64, day: i64) -> Self {
25        Self(logicaffeine_base::temporal::days_from_civil(year, month as u32, day as u32) as i32)
26    }
27
28    /// Get the raw days value.
29    #[inline]
30    pub fn days(&self) -> i32 {
31        self.0
32    }
33
34    /// UTC calendar components — AOT mirrors of `year_of`/`month_of`/`day_of`/`weekday_of` on a Date.
35    #[inline]
36    pub fn year(&self) -> i64 {
37        self.to_ymd().0
38    }
39    #[inline]
40    pub fn month(&self) -> i64 {
41        self.to_ymd().1
42    }
43    #[inline]
44    pub fn day(&self) -> i64 {
45        self.to_ymd().2
46    }
47    #[inline]
48    pub fn weekday(&self) -> i64 {
49        logicaffeine_base::temporal::iso_weekday_from_days(self.0 as i64) as i64
50    }
51    /// The ISO-8601 week number (1..=53) — the AOT mirror of `week_of` on a Date.
52    #[inline]
53    pub fn iso_week(&self) -> i64 {
54        logicaffeine_base::temporal::iso_week_from_days(self.0 as i64).1 as i64
55    }
56    /// The calendar quarter (1..=4) — the AOT mirror of `quarter_of` on a Date.
57    #[inline]
58    pub fn quarter(&self) -> i64 {
59        (self.month() - 1) / 3 + 1
60    }
61
62    /// Convert to year, month, day using Howard Hinnant's algorithm.
63    pub fn to_ymd(&self) -> (i64, i64, i64) {
64        let z = self.0 as i64 + 719468; // shift epoch
65        let era = if z >= 0 { z } else { z - 146096 } / 146097;
66        let doe = z - era * 146097;
67        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
68        let y = yoe + era * 400;
69        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
70        let mp = (5 * doy + 2) / 153;
71        let d = doy - (153 * mp + 2) / 5 + 1;
72        let m = mp + if mp < 10 { 3 } else { -9 };
73        let year = y + if m <= 2 { 1 } else { 0 };
74        (year, m, d)
75    }
76}
77
78impl LogosDate {
79    /// This date moved by a calendar span: months first (clamped to the end of the month), then
80    /// days — base `date_add_span`, the arithmetic every tier's `Date + Span` shares.
81    #[inline]
82    pub fn add_span(self, months: i32, days: i32) -> LogosDate {
83        LogosDate(logicaffeine_base::temporal::date_add_span(
84            self.0, months, days,
85        ))
86    }
87}
88
89impl std::ops::Add<LogosSpan> for LogosDate {
90    type Output = LogosDate;
91    #[inline]
92    fn add(self, rhs: LogosSpan) -> LogosDate {
93        self.add_span(rhs.months, rhs.days)
94    }
95}
96
97impl std::ops::Sub<LogosSpan> for LogosDate {
98    type Output = LogosDate;
99    #[inline]
100    fn sub(self, rhs: LogosSpan) -> LogosDate {
101        self.add_span(rhs.months.wrapping_neg(), rhs.days.wrapping_neg())
102    }
103}
104
105impl Display for LogosDate {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        let (year, month, day) = self.to_ymd();
108        write!(f, "{:04}-{:02}-{:02}", year, month, day)
109    }
110}
111
112
113/// Moment stored as nanoseconds since Unix epoch.
114///
115/// Provides nanosecond precision for timestamps.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
117pub struct LogosMoment(pub i64);
118
119impl LogosMoment {
120    /// Create a new moment from nanoseconds since epoch.
121    #[inline]
122    pub fn new(nanos: i64) -> Self {
123        Self(nanos)
124    }
125
126    /// Construct a UTC moment from civil `(year, month, day, hour, minute, second)` — the AOT mirror
127    /// of the `makeMoment` builtin. Uses the same `days_from_civil` core as `calendar.timegm`, so the
128    /// two agree to the second.
129    #[inline]
130    pub fn from_ymd(year: i64, month: i64, day: i64, hour: i64, minute: i64, second: i64) -> Self {
131        let days = logicaffeine_base::temporal::days_from_civil(year, month as u32, day as u32);
132        Self(
133            days * logicaffeine_base::temporal::NANOS_PER_DAY
134                + (hour * 3600 + minute * 60 + second)
135                    * logicaffeine_base::temporal::NANOS_PER_SECOND,
136        )
137    }
138
139    /// Get the raw nanoseconds value.
140    #[inline]
141    pub fn nanos(&self) -> i64 {
142        self.0
143    }
144
145    /// Parse an RFC 3339 / ISO 8601 timestamp into a moment — the AOT mirror of the interpreter's
146    /// `parse_timestamp`, stopping the program in the same words on text that is not one.
147    #[inline]
148    pub fn parse_rfc3339(s: &str) -> Self {
149        Self(
150            logicaffeine_base::temporal::parse_rfc3339(s)
151                .unwrap_or_else(|| panic!("{}", crate::failure::not_a_timestamp(s))),
152        )
153    }
154
155    /// Render this moment as an RFC 3339 / ISO 8601 UTC string — the AOT mirror of `format_timestamp`.
156    #[inline]
157    pub fn format_rfc3339(&self) -> String {
158        logicaffeine_base::temporal::format_rfc3339(self.0)
159    }
160
161    /// UTC calendar components — the AOT mirrors of `year_of`/`month_of`/`day_of`/`weekday_of`.
162    #[inline]
163    pub fn year(&self) -> i64 {
164        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).year
165    }
166    #[inline]
167    pub fn month(&self) -> i64 {
168        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).month as i64
169    }
170    #[inline]
171    pub fn day(&self) -> i64 {
172        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).day as i64
173    }
174    #[inline]
175    pub fn weekday(&self) -> i64 {
176        logicaffeine_base::temporal::iso_weekday_from_days(
177            self.0
178                .div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY),
179        ) as i64
180    }
181    #[inline]
182    pub fn hour(&self) -> i64 {
183        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).hour as i64
184    }
185    #[inline]
186    pub fn minute(&self) -> i64 {
187        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).minute as i64
188    }
189    #[inline]
190    pub fn second(&self) -> i64 {
191        logicaffeine_base::temporal::civil_from_unix_nanos(self.0).second as i64
192    }
193
194    /// The calendar day this moment falls on (UTC) — the AOT mirror of `date_of`.
195    #[inline]
196    pub fn date(&self) -> LogosDate {
197        LogosDate(
198            self.0
199                .div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY) as i32,
200        )
201    }
202
203    /// The wall-clock time-of-day (UTC) — the AOT mirror of `time_of`.
204    #[inline]
205    pub fn time_of_day(&self) -> LogosTime {
206        LogosTime(
207            self.0
208                .rem_euclid(logicaffeine_base::temporal::NANOS_PER_DAY),
209        )
210    }
211
212    /// The ISO-8601 week number (1..=53) — the AOT mirror of `week_of`.
213    #[inline]
214    pub fn iso_week(&self) -> i64 {
215        let days = self
216            .0
217            .div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY);
218        logicaffeine_base::temporal::iso_week_from_days(days).1 as i64
219    }
220
221    /// The calendar quarter (1..=4) — the AOT mirror of `quarter_of`.
222    #[inline]
223    pub fn quarter(&self) -> i64 {
224        (self.month() - 1) / 3 + 1
225    }
226
227    /// The moment `seconds` later (AOT mirror of `add_seconds`).
228    #[inline]
229    pub fn add_seconds(&self, seconds: i64) -> Self {
230        Self(self.0 + seconds * 1_000_000_000)
231    }
232
233    /// Whole seconds from `self` to `other` (AOT mirror of `seconds_between`).
234    #[inline]
235    pub fn seconds_until(&self, other: &LogosMoment) -> i64 {
236        (other.0 - self.0) / 1_000_000_000
237    }
238
239    /// Complete calendar months / years from `self` to `other` — AOT mirrors of
240    /// `months_between` / `years_between` (signed, end-of-month-clamping correct).
241    #[inline]
242    pub fn months_until(&self, other: &LogosMoment) -> i64 {
243        logicaffeine_base::temporal::months_between(self.0, other.0)
244    }
245    #[inline]
246    pub fn years_until(&self, other: &LogosMoment) -> i64 {
247        logicaffeine_base::temporal::years_between(self.0, other.0)
248    }
249
250    /// The local wall-clock time (with offset) in a named zone — AOT mirror of `in_zone`. Panics on
251    /// an unknown zone, which the front-end has validated (it errors cleanly in the interpreter).
252    pub fn in_zone(&self, zone_name: &str) -> String {
253        logicaffeine_base::temporal::format_zoned(self.0, zone_name).unwrap_or_else(|| {
254            panic!(
255                "{}",
256                crate::failure::unknown_time_zone(zone_name)
257            )
258        })
259    }
260
261    /// The local-as-UTC instant in a named zone — AOT mirror of `local_instant`. Composing a UTC
262    /// component accessor onto the result reads the zone's LOCAL field (`the hour of m in "<zone>"`).
263    pub fn local_instant(&self, zone_name: &str) -> LogosMoment {
264        LogosMoment(
265            logicaffeine_base::temporal::local_instant_nanos(self.0, zone_name).unwrap_or_else(
266                || {
267                    panic!(
268                        "{}",
269                        crate::failure::unknown_time_zone(zone_name)
270                    )
271                },
272            ),
273        )
274    }
275
276}
277
278// `Moment + Duration` / `Moment − Duration` → a shifted Moment (the AOT mirror of the tree-walker's
279// arith). Duration is `std::time::Duration` on this path; the nanos add/subtract on the i64 instant.
280impl std::ops::Add<std::time::Duration> for LogosMoment {
281    type Output = LogosMoment;
282    #[inline]
283    fn add(self, rhs: std::time::Duration) -> LogosMoment {
284        LogosMoment(self.0.wrapping_add(rhs.as_nanos() as i64))
285    }
286}
287
288impl std::ops::Sub<std::time::Duration> for LogosMoment {
289    type Output = LogosMoment;
290    #[inline]
291    fn sub(self, rhs: std::time::Duration) -> LogosMoment {
292        LogosMoment(self.0.wrapping_sub(rhs.as_nanos() as i64))
293    }
294}
295
296// `Moment + Span` / `Moment − Span` is CIVIL calendar arithmetic — months clamp at end-of-month,
297// leap years are respected, the wall time is preserved — distinct from the physical Duration path
298// above. Delegates to the same `base::temporal::add_span` the interpreter's `moment_add_span` uses,
299// so the AOT tier stays byte-identical to the tree-walker / VM.
300impl LogosMoment {
301    #[inline]
302    fn add_span(self, months: i32, days: i32) -> LogosMoment {
303        let dt = logicaffeine_base::temporal::civil_from_unix_nanos(self.0);
304        let shifted = logicaffeine_base::temporal::add_span(dt, months as i64, days as i64);
305        LogosMoment(logicaffeine_base::temporal::unix_nanos_from_civil(shifted))
306    }
307}
308
309impl std::ops::Add<LogosSpan> for LogosMoment {
310    type Output = LogosMoment;
311    #[inline]
312    fn add(self, rhs: LogosSpan) -> LogosMoment {
313        self.add_span(rhs.months, rhs.days)
314    }
315}
316
317impl std::ops::Sub<LogosSpan> for LogosMoment {
318    type Output = LogosMoment;
319    #[inline]
320    fn sub(self, rhs: LogosSpan) -> LogosMoment {
321        self.add_span(-rhs.months, -rhs.days)
322    }
323}
324
325impl Display for LogosMoment {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        // Civil UTC "YYYY-MM-DD HH:MM" — byte-identical to the interpreter's Moment Show. Euclidean
328        // division so a pre-epoch (negative) Moment yields the correct date and a 0..86399 time-of-day.
329        let total_seconds = self.0.div_euclid(1_000_000_000);
330        let days = total_seconds.div_euclid(86_400);
331        let day_seconds = total_seconds.rem_euclid(86_400);
332        let hours = day_seconds / 3600;
333        let minutes = (day_seconds % 3600) / 60;
334        let (year, month, day) = LogosDate(days as i32).to_ymd();
335        write!(
336            f,
337            "{:04}-{:02}-{:02} {:02}:{:02}",
338            year, month, day, hours, minutes
339        )
340    }
341}
342
343
344/// Calendar span with separate month and day components.
345///
346/// Months and days are kept separate because they're **incommensurable**:
347/// - "1 month" is 28-31 days depending on the month
348/// - You can't convert months to days without knowing the reference date
349///
350/// Years fold into months (1 year = 12 months).
351/// Weeks fold into days (1 week = 7 days).
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353pub struct LogosSpan {
354    /// Total months (years * 12 + months)
355    pub months: i32,
356    /// Total days (weeks * 7 + days)
357    pub days: i32,
358}
359
360impl LogosSpan {
361    /// Create a new span from months and days.
362    pub fn new(months: i32, days: i32) -> Self {
363        Self { months, days }
364    }
365
366    /// Create a span from years, months, and days.
367    /// Years are folded into months (1 year = 12 months).
368    pub fn from_years_months_days(years: i32, months: i32, days: i32) -> Self {
369        Self {
370            months: years * 12 + months,
371            days,
372        }
373    }
374
375    /// Create a span from weeks and days.
376    /// Weeks are folded into days (1 week = 7 days).
377    pub fn from_weeks_days(weeks: i32, days: i32) -> Self {
378        Self {
379            months: 0,
380            days: weeks * 7 + days,
381        }
382    }
383
384    /// Negate the span (for "ago" operator).
385    pub fn negate(&self) -> Self {
386        Self {
387            months: -self.months,
388            days: -self.days,
389        }
390    }
391}
392
393impl Display for LogosSpan {
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        let mut parts = Vec::new();
396
397        // Extract years from months
398        let years = self.months / 12;
399        let remaining_months = self.months % 12;
400
401        if years != 0 {
402            parts.push(if years.abs() == 1 {
403                format!("{} year", years)
404            } else {
405                format!("{} years", years)
406            });
407        }
408
409        if remaining_months != 0 {
410            parts.push(if remaining_months.abs() == 1 {
411                format!("{} month", remaining_months)
412            } else {
413                format!("{} months", remaining_months)
414            });
415        }
416
417        if self.days != 0 || parts.is_empty() {
418            parts.push(if self.days.abs() == 1 {
419                format!("{} day", self.days)
420            } else {
421                format!("{} days", self.days)
422            });
423        }
424
425        write!(f, "{}", parts.join(" and "))
426    }
427}
428
429
430/// Time-of-day stored as nanoseconds from midnight — the wall-clock face of a `Moment`, with no
431/// date or zone. Renders `HH:MM:SS[.frac]`, lossless to the nanosecond (the AOT mirror of the
432/// interpreter's `Time`).
433#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
434pub struct LogosTime(pub i64);
435
436impl LogosTime {
437    #[inline]
438    pub fn new(nanos_from_midnight: i64) -> Self {
439        Self(nanos_from_midnight)
440    }
441
442    /// The raw nanoseconds-from-midnight value.
443    #[inline]
444    pub fn nanos(&self) -> i64 {
445        self.0
446    }
447}
448
449impl Display for LogosTime {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        write!(
452            f,
453            "{}",
454            logicaffeine_base::temporal::format_time_of_day(self.0)
455        )
456    }
457}
458
459
460// ── as values of the exact extension ─────────────────────────────────────────────────────────
461//
462// A date, a moment, a time of day or a span can reach a binding the exact extension also writes —
463// the clock of a loop that runs beside an infinitesimal. There it rides in a `LogosValue` and still
464// answers as itself: the interpreter's equalities, orders and calendar arithmetic, word for word.
465
466use crate::exact::{LogosValue, OtherValue};
467use logicaffeine_base::tower::Op;
468
469const NANOS_PER_DAY: i64 = 86_400_000_000_000;
470
471impl From<LogosDate> for LogosValue {
472    fn from(d: LogosDate) -> Self {
473        LogosValue::other(d)
474    }
475}
476
477impl From<LogosMoment> for LogosValue {
478    fn from(m: LogosMoment) -> Self {
479        LogosValue::other(m)
480    }
481}
482
483impl From<LogosTime> for LogosValue {
484    fn from(t: LogosTime) -> Self {
485        LogosValue::other(t)
486    }
487}
488
489impl From<LogosSpan> for LogosValue {
490    fn from(s: LogosSpan) -> Self {
491        LogosValue::other(s)
492    }
493}
494
495impl OtherValue for LogosDate {
496    fn type_name(&self) -> &str {
497        "Date"
498    }
499
500    fn display_string(&self) -> String {
501        self.to_string()
502    }
503
504    fn as_any(&self) -> &dyn std::any::Any {
505        self
506    }
507
508    fn equals(&self, o: &dyn OtherValue) -> bool {
509        o.as_any().downcast_ref::<LogosDate>() == Some(self)
510    }
511
512    fn order(&self, o: &LogosValue) -> Option<Option<std::cmp::Ordering>> {
513        o.other_as::<LogosDate>().map(|d| Some(self.0.cmp(&d.0)))
514    }
515
516    fn combine(&self, op: Op, o: &LogosValue, reversed: bool) -> Option<Result<LogosValue, String>> {
517        let span = *o.other_as::<LogosSpan>()?;
518        match (op, reversed) {
519            (Op::Add, false) => Some(Ok(LogosValue::other(*self + span))),
520            (Op::Sub, false) => Some(Ok(LogosValue::other(
521                self.add_span(-span.months, -span.days),
522            ))),
523            _ => None,
524        }
525    }
526}
527
528impl OtherValue for LogosMoment {
529    fn type_name(&self) -> &str {
530        "Moment"
531    }
532
533    fn display_string(&self) -> String {
534        self.to_string()
535    }
536
537    fn as_any(&self) -> &dyn std::any::Any {
538        self
539    }
540
541    fn equals(&self, o: &dyn OtherValue) -> bool {
542        o.as_any().downcast_ref::<LogosMoment>() == Some(self)
543    }
544
545    fn order(&self, o: &LogosValue) -> Option<Option<std::cmp::Ordering>> {
546        if let Some(m) = o.other_as::<LogosMoment>() {
547            return Some(Some(self.0.cmp(&m.0)));
548        }
549        o.other_as::<LogosTime>()
550            .map(|t| Some(self.0.rem_euclid(NANOS_PER_DAY).cmp(&t.0)))
551    }
552
553    fn combine(&self, op: Op, o: &LogosValue, reversed: bool) -> Option<Result<LogosValue, String>> {
554        let span = *o.other_as::<LogosSpan>()?;
555        match (op, reversed) {
556            (Op::Add, _) => Some(Ok(LogosValue::other(*self + span))),
557            (Op::Sub, false) => Some(Ok(LogosValue::other(self.add_span(-span.months, -span.days)))),
558            _ => None,
559        }
560    }
561}
562
563impl OtherValue for LogosTime {
564    fn type_name(&self) -> &str {
565        "Time"
566    }
567
568    fn display_string(&self) -> String {
569        self.to_string()
570    }
571
572    fn as_any(&self) -> &dyn std::any::Any {
573        self
574    }
575
576    fn equals(&self, o: &dyn OtherValue) -> bool {
577        o.as_any().downcast_ref::<LogosTime>() == Some(self)
578    }
579
580    fn order(&self, o: &LogosValue) -> Option<Option<std::cmp::Ordering>> {
581        if let Some(t) = o.other_as::<LogosTime>() {
582            return Some(Some(self.0.cmp(&t.0)));
583        }
584        o.other_as::<LogosMoment>()
585            .map(|m| Some(self.0.cmp(&m.0.rem_euclid(NANOS_PER_DAY))))
586    }
587
588    fn combine(&self, _op: Op, _o: &LogosValue, _reversed: bool) -> Option<Result<LogosValue, String>> {
589        None
590    }
591}
592
593impl OtherValue for LogosSpan {
594    fn type_name(&self) -> &str {
595        "Span"
596    }
597
598    fn display_string(&self) -> String {
599        self.to_string()
600    }
601
602    fn as_any(&self) -> &dyn std::any::Any {
603        self
604    }
605
606    fn equals(&self, o: &dyn OtherValue) -> bool {
607        o.as_any().downcast_ref::<LogosSpan>() == Some(self)
608    }
609
610    fn order(&self, _o: &LogosValue) -> Option<Option<std::cmp::Ordering>> {
611        None
612    }
613
614    fn combine(&self, _op: Op, _o: &LogosValue, _reversed: bool) -> Option<Result<LogosValue, String>> {
615        None
616    }
617}
618
619#[cfg(test)]
620#[path = "../../logicaffeine_tests/unit/logicaffeine_data/temporal/tests.rs"]
621mod tests;