1use std::fmt::{self, Display};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct LogosDate(pub i32);
13
14impl LogosDate {
15 #[inline]
17 pub fn new(days: i32) -> Self {
18 Self(days)
19 }
20
21 #[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 #[inline]
30 pub fn days(&self) -> i32 {
31 self.0
32 }
33
34 #[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 #[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 #[inline]
58 pub fn quarter(&self) -> i64 {
59 (self.month() - 1) / 3 + 1
60 }
61
62 pub fn to_ymd(&self) -> (i64, i64, i64) {
64 let z = self.0 as i64 + 719468; 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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
117pub struct LogosMoment(pub i64);
118
119impl LogosMoment {
120 #[inline]
122 pub fn new(nanos: i64) -> Self {
123 Self(nanos)
124 }
125
126 #[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 #[inline]
141 pub fn nanos(&self) -> i64 {
142 self.0
143 }
144
145 #[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 #[inline]
157 pub fn format_rfc3339(&self) -> String {
158 logicaffeine_base::temporal::format_rfc3339(self.0)
159 }
160
161 #[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 #[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 #[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 #[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 #[inline]
223 pub fn quarter(&self) -> i64 {
224 (self.month() - 1) / 3 + 1
225 }
226
227 #[inline]
229 pub fn add_seconds(&self, seconds: i64) -> Self {
230 Self(self.0 + seconds * 1_000_000_000)
231 }
232
233 #[inline]
235 pub fn seconds_until(&self, other: &LogosMoment) -> i64 {
236 (other.0 - self.0) / 1_000_000_000
237 }
238
239 #[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 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 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
278impl 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
296impl 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353pub struct LogosSpan {
354 pub months: i32,
356 pub days: i32,
358}
359
360impl LogosSpan {
361 pub fn new(months: i32, days: i32) -> Self {
363 Self { months, days }
364 }
365
366 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 pub fn from_weeks_days(weeks: i32, days: i32) -> Self {
378 Self {
379 months: 0,
380 days: weeks * 7 + days,
381 }
382 }
383
384 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 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#[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 #[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
460use 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;