1use std::fmt::{self, Display};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct LogosDate(pub i32);
12
13impl LogosDate {
14 #[inline]
16 pub fn new(days: i32) -> Self {
17 Self(days)
18 }
19
20 #[inline]
22 pub fn days(&self) -> i32 {
23 self.0
24 }
25
26 #[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 #[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 #[inline]
50 pub fn quarter(&self) -> i64 {
51 (self.month() - 1) / 3 + 1
52 }
53
54 pub fn to_ymd(&self) -> (i64, i64, i64) {
56 let z = self.0 as i64 + 719468; 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
87pub struct LogosMoment(pub i64);
88
89impl LogosMoment {
90 #[inline]
92 pub fn new(nanos: i64) -> Self {
93 Self(nanos)
94 }
95
96 #[inline]
98 pub fn nanos(&self) -> i64 {
99 self.0
100 }
101
102 #[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 #[inline]
114 pub fn format_rfc3339(&self) -> String {
115 logicaffeine_base::temporal::format_rfc3339(self.0)
116 }
117
118 #[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 #[inline]
152 pub fn date(&self) -> LogosDate {
153 LogosDate(self.0.div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY) as i32)
154 }
155
156 #[inline]
158 pub fn time_of_day(&self) -> LogosTime {
159 LogosTime(self.0.rem_euclid(logicaffeine_base::temporal::NANOS_PER_DAY))
160 }
161
162 #[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 #[inline]
171 pub fn quarter(&self) -> i64 {
172 (self.month() - 1) / 3 + 1
173 }
174
175 #[inline]
177 pub fn add_seconds(&self, seconds: i64) -> Self {
178 Self(self.0 + seconds * 1_000_000_000)
179 }
180
181 #[inline]
183 pub fn seconds_until(&self, other: &LogosMoment) -> i64 {
184 (other.0 - self.0) / 1_000_000_000
185 }
186
187 #[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 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 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 #[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
225impl 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
243impl 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub struct LogosSpan {
298 pub months: i32,
300 pub days: i32,
302}
303
304impl LogosSpan {
305 pub fn new(months: i32, days: i32) -> Self {
307 Self { months, days }
308 }
309
310 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 pub fn from_weeks_days(weeks: i32, days: i32) -> Self {
322 Self {
323 months: 0,
324 days: weeks * 7 + days,
325 }
326 }
327
328 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 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#[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 #[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); assert_eq!(date.to_string(), "2026-05-20");
425 }
426}