1#![doc = include_str!("../README.md")]
3#![warn(missing_docs)]
4
5use std::fmt::{self, Debug};
6
7use icu_calendar::types::{IsoSecond, NanoSecond};
8use kine_core::{Calendar, CalendarTime, OffsetTime, TimeResult, TimeZone};
9
10pub mod cal {
12 pub use icu_calendar;
13 pub use icu_calendar::{
14 buddhist::Buddhist,
15 coptic::Coptic,
16 ethiopian::Ethiopian,
17 indian::Indian,
18 japanese::{Japanese, JapaneseExtended},
19 julian::Julian,
20 Gregorian, Iso,
21 };
22}
23
24const NANOS_IN_SECS: i128 = 1_000_000_000;
25const NANOS_IN_SECS_U64: u64 = 1_000_000_000;
26const NANOS_IN_MINS: i128 = 60 * NANOS_IN_SECS;
27
28pub struct Cal<Ca: icu_calendar::AsCalendar, Tz: TimeZone> {
30 cal: Ca,
31 tz: Tz,
32}
33
34#[derive(Clone, Eq, PartialEq)]
40pub struct Time<Ca: icu_calendar::AsCalendar, Tz: TimeZone> {
41 tz: Tz::Sigil,
42 time: icu_calendar::DateTime<Ca>,
43}
44
45impl<Ca: icu_calendar::AsCalendar, Tz: TimeZone> Cal<Ca, Tz> {
46 pub fn new(cal: Ca, tz: Tz) -> Self {
51 Self { cal, tz }
52 }
53}
54
55impl<Ca: icu_calendar::AsCalendar, Tz: TimeZone> Time<Ca, Tz> {
56 pub fn new(tz: Tz::Sigil, time: icu_calendar::DateTime<Ca>) -> Self {
60 Self { tz, time }
61 }
62
63 pub fn tz(&self) -> &Tz::Sigil {
65 &self.tz
66 }
67
68 pub fn icu(&self) -> &icu_calendar::DateTime<Ca> {
70 &self.time
71 }
72}
73
74impl<Ca, Tz> Calendar for Cal<Ca, Tz>
75where
76 Ca: Clone + icu_calendar::AsCalendar,
77 Tz: Clone + TimeZone,
78 <Tz as TimeZone>::Sigil: Clone,
79{
80 type Time = Time<Ca, Tz>;
81
82 fn write(&self, t: &kine_core::Time) -> kine_core::Result<Self::Time> {
83 let offset_time = t.write(self.tz.clone())?;
85
86 let pseudo_nanos = offset_time.as_pseudo_nanos_since_posix_epoch();
88 let extra_nanos = i128::from(offset_time.extra_nanos());
89 let (minutes, submin_pseudo_nanos) =
90 num_integer::div_mod_floor(pseudo_nanos, NANOS_IN_MINS);
91 let (seconds, nanos) =
92 num_integer::div_mod_floor(submin_pseudo_nanos + extra_nanos, NANOS_IN_SECS);
93
94 let minutes = i32::try_from(minutes).map_err(|_| kine_core::Error::OutOfRange)?;
96 let mut res = icu_calendar::DateTime::from_minutes_since_local_unix_epoch(minutes);
97 res.time.second = IsoSecond::try_from(u8::try_from(seconds).unwrap()).unwrap();
98 res.time.nanosecond = NanoSecond::try_from(u32::try_from(nanos).unwrap()).unwrap();
99
100 Ok(Time {
101 tz: offset_time.sigil().clone(),
102 time: res.to_calendar(self.cal.clone()),
103 })
104 }
105}
106
107impl<Ca, Tz> CalendarTime for Time<Ca, Tz>
108where
109 Ca: icu_calendar::AsCalendar,
110 Tz: TimeZone,
111 <Tz as TimeZone>::Sigil: Clone,
112{
113 fn read(&self) -> kine_core::Result<TimeResult> {
114 let time = self.time.to_calendar(icu_calendar::Iso);
115 let local_mins = i128::from(time.minutes_since_local_unix_epoch());
116 let seconds_with_leap = time.time.second.number();
118 let seconds_without_leap = std::cmp::min(seconds_with_leap, 59);
119 let nanos_outside_leap = match seconds_with_leap > 59 {
120 true => NANOS_IN_SECS - 1,
121 false => i128::from(time.time.nanosecond.number()),
122 };
123 let extra_nanos = match seconds_with_leap > 59 {
124 true => {
125 u64::from(time.time.nanosecond.number())
126 + u64::from(seconds_with_leap - 60) * NANOS_IN_SECS_U64
127 + 1
128 }
129 false => 0,
130 };
131 let local_nanos = local_mins * NANOS_IN_MINS
132 + i128::from(seconds_without_leap) * NANOS_IN_SECS
133 + nanos_outside_leap;
134 let offset_time = OffsetTime::from_pseudo_nanos_since_posix_epoch(
135 self.tz.clone(),
136 local_nanos,
137 extra_nanos,
138 );
139 offset_time.read()
140 }
141}
142
143impl<Tz: TimeZone> Debug for Time<icu_calendar::Iso, Tz> {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 let date = &self.time.date;
146 let time = &self.time.time;
147 write!(
148 f,
149 "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}{}",
150 date.year().number,
151 date.month().ordinal,
152 date.day_of_month().0,
153 time.hour.number(),
154 time.minute.number(),
155 time.second.number(),
156 time.nanosecond.number(),
157 self.tz,
158 )
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use icu_calendar::{
165 types::{IsoSecond, NanoSecond},
166 Iso,
167 };
168 use kine_core::{
169 tz::{Utc, UtcSigil},
170 Calendar, CalendarTime, Duration, TimeResult,
171 };
172
173 use crate::{Cal, Time, NANOS_IN_MINS, NANOS_IN_SECS};
174
175 const MIN_NANOS: i128 = -(i32::MIN as i128 * NANOS_IN_MINS);
177 const MAX_NANOS: i128 = -(i32::MAX as i128 * NANOS_IN_MINS);
178
179 const NANOS_IN_SECS_U32: u32 = 1_000_000_000;
180
181 fn mktime(nanos: i128) -> kine_core::Time {
182 kine_core::Time::POSIX_EPOCH + Duration::from_nanos(nanos)
183 }
184
185 #[test]
186 fn negative_time_writes_correctly() {
187 let time = mktime(-NANOS_IN_MINS);
188 let written = time.write(Cal::new(Iso, Utc.clone()));
189 let expected =
190 icu_calendar::DateTime::try_new_iso_datetime(1969, 12, 31, 23, 59, 10).unwrap();
191 assert_eq!(written, Ok(Time::new(UtcSigil, expected)));
192 }
193
194 #[test]
195 fn leap_second_reads_correctly() {
196 let mut time: Time<Iso, Utc> = Time::new(
198 UtcSigil,
199 icu_calendar::DateTime::try_new_iso_datetime(1969, 12, 31, 23, 59, 60).unwrap(),
200 );
201 assert_eq!(
202 time.read(),
203 Ok(TimeResult::One(mktime(-10 * NANOS_IN_SECS)))
204 );
205 time.time.time.nanosecond = NanoSecond::try_from(NANOS_IN_SECS_U32 / 2).unwrap();
206 assert_eq!(
207 time.read(),
208 Ok(TimeResult::One(mktime(-19 * NANOS_IN_SECS / 2)))
209 );
210
211 time.time.time.second = IsoSecond::try_from(61_u8).unwrap();
213 assert_eq!(
214 time.read(),
215 Ok(TimeResult::One(mktime(-17 * NANOS_IN_SECS / 2)))
216 );
217 time.time.time.second = IsoSecond::try_from(65_u8).unwrap();
218 assert_eq!(
219 time.read(),
220 Ok(TimeResult::One(mktime(-9 * NANOS_IN_SECS / 2)))
221 );
222 time.time.time.second = IsoSecond::try_from(69_u8).unwrap();
223 assert_eq!(time.read(), Ok(TimeResult::One(mktime(-NANOS_IN_SECS / 2))));
224 }
225
226 #[test]
227 fn negative_time_reads_correctly() {
228 let time: Time<Iso, Utc> = Time::new(
229 UtcSigil,
230 icu_calendar::DateTime::try_new_iso_datetime(1969, 12, 31, 23, 59, 10).unwrap(),
231 );
232 let read = time.read();
233 let expected = mktime(-NANOS_IN_MINS);
234 assert_eq!(read, Ok(TimeResult::One(expected)));
235 }
236
237 #[test]
238 fn iso_conversion_round_trip() {
239 bolero::check!().with_type::<i128>().for_each(|&t| {
240 let assert_out_of_range = |t| {
241 assert!(
242 t < MIN_NANOS || t > MAX_NANOS,
243 "Returned out of range for time {t} that is not close to the ends of the range"
244 )
245 };
246 let time = kine_core::Time::POSIX_EPOCH + Duration::from_nanos(t);
247 let cal = Cal::new(Iso, Utc.clone());
248 let formatted = match cal.write(&time) {
249 Err(kine_core::Error::OutOfRange) => {
250 assert_out_of_range(t);
251 return;
252 }
253 Ok(t) => t,
254 };
255 let time_bis = match formatted.read() {
256 Err(kine_core::Error::OutOfRange) => {
257 assert_out_of_range(t);
258 return;
259 }
260 Ok(TimeResult::One(t)) => t,
261 Ok(t) => panic!(
262 "Converting formatted ISO time to time did not return exactly one result: {t:?}"
263 ),
264 };
265 assert_eq!(
266 time, time_bis,
267 "Round-tripping through formatted ISO time lost information"
268 );
269 })
270 }
271}