Skip to main content

temporal_capi/
duration.rs

1use crate::error::ffi::TemporalError;
2
3#[diplomat::bridge]
4#[diplomat::abi_rename = "temporal_rs_{0}"]
5pub mod ffi {
6    use crate::error::ffi::TemporalError;
7    use crate::options::ffi::ToStringRoundingOptions;
8    use crate::options::ffi::{RoundingOptions, Unit};
9    use crate::provider::ffi::Provider;
10    use crate::zoned_date_time::ffi::RelativeTo;
11    use alloc::boxed::Box;
12    use alloc::string::String;
13    use core::str::FromStr;
14    use diplomat_runtime::DiplomatOption;
15    use diplomat_runtime::{DiplomatStr, DiplomatStr16};
16    use num_traits::FromPrimitive;
17
18    #[diplomat::opaque]
19    pub struct Duration(pub(crate) temporal_rs::Duration);
20
21    #[diplomat::opaque]
22    #[diplomat::transparent_convert]
23    pub struct DateDuration(pub(crate) temporal_rs::duration::DateDuration);
24
25    pub struct PartialDuration {
26        pub years: DiplomatOption<i64>,
27        pub months: DiplomatOption<i64>,
28        pub weeks: DiplomatOption<i64>,
29        pub days: DiplomatOption<i64>,
30        pub hours: DiplomatOption<i64>,
31        pub minutes: DiplomatOption<i64>,
32        pub seconds: DiplomatOption<i64>,
33        pub milliseconds: DiplomatOption<i64>,
34        pub microseconds: DiplomatOption<f64>,
35        pub nanoseconds: DiplomatOption<f64>,
36    }
37
38    #[diplomat::enum_convert(temporal_rs::Sign)]
39    pub enum Sign {
40        Positive = 1,
41        Zero = 0,
42        Negative = -1,
43    }
44
45    impl PartialDuration {
46        pub fn is_empty(self) -> bool {
47            temporal_rs::partial::PartialDuration::try_from(self)
48                .map(|p| p.is_empty())
49                .unwrap_or(false)
50        }
51    }
52
53    impl DateDuration {
54        pub fn try_new(
55            years: i64,
56            months: i64,
57            weeks: i64,
58            days: i64,
59        ) -> Result<Box<Self>, TemporalError> {
60            temporal_rs::duration::DateDuration::new(years, months, weeks, days)
61                .map(|x| Box::new(DateDuration(x)))
62                .map_err(Into::into)
63        }
64
65        pub fn abs(&self) -> Box<Self> {
66            Box::new(Self(self.0.abs()))
67        }
68        pub fn negated(&self) -> Box<Self> {
69            Box::new(Self(self.0.negated()))
70        }
71
72        pub fn sign(&self) -> Sign {
73            self.0.sign().into()
74        }
75    }
76    impl Duration {
77        /// Temporary API until v8 can move off of it
78        pub fn create(
79            years: i64,
80            months: i64,
81            weeks: i64,
82            days: i64,
83            hours: i64,
84            minutes: i64,
85            seconds: i64,
86            milliseconds: i64,
87            microseconds: f64,
88            nanoseconds: f64,
89        ) -> Result<Box<Self>, TemporalError> {
90            Self::try_new(
91                years,
92                months,
93                weeks,
94                days,
95                hours,
96                minutes,
97                seconds,
98                milliseconds,
99                microseconds,
100                nanoseconds,
101            )
102        }
103
104        pub fn try_new(
105            years: i64,
106            months: i64,
107            weeks: i64,
108            days: i64,
109            hours: i64,
110            minutes: i64,
111            seconds: i64,
112            milliseconds: i64,
113            microseconds: f64,
114            nanoseconds: f64,
115        ) -> Result<Box<Self>, TemporalError> {
116            temporal_rs::Duration::new(
117                years,
118                months,
119                weeks,
120                days,
121                hours,
122                minutes,
123                seconds,
124                milliseconds,
125                i128::from_f64(microseconds).ok_or(TemporalError::range("μs out of range"))?,
126                i128::from_f64(nanoseconds).ok_or(TemporalError::range("ms out of range"))?,
127            )
128            .map(|x| Box::new(Duration(x)))
129            .map_err(Into::into)
130        }
131
132        pub fn from_partial_duration(partial: PartialDuration) -> Result<Box<Self>, TemporalError> {
133            temporal_rs::Duration::from_partial_duration(partial.try_into()?)
134                .map(|x| Box::new(Duration(x)))
135                .map_err(Into::into)
136        }
137
138        pub fn from_utf8(s: &DiplomatStr) -> Result<Box<Self>, TemporalError> {
139            temporal_rs::Duration::from_utf8(s)
140                .map(|c| Box::new(Self(c)))
141                .map_err(Into::into)
142        }
143
144        pub fn from_utf16(s: &DiplomatStr16) -> Result<Box<Self>, TemporalError> {
145            // TODO(#275) This should not need to convert
146            let s = String::from_utf16(s).map_err(|_| temporal_rs::TemporalError::range())?;
147            temporal_rs::Duration::from_str(&s)
148                .map(|c| Box::new(Self(c)))
149                .map_err(Into::into)
150        }
151
152        pub fn is_time_within_range(&self) -> bool {
153            self.0.is_time_within_range()
154        }
155
156        // set_time_duration is NOT safe to expose over FFI if the date()/time() methods are available
157        // Diplomat plans to make this a hard error.
158
159        pub fn years(&self) -> i64 {
160            self.0.years()
161        }
162        pub fn months(&self) -> i64 {
163            self.0.months()
164        }
165        pub fn weeks(&self) -> i64 {
166            self.0.weeks()
167        }
168        pub fn days(&self) -> i64 {
169            self.0.days()
170        }
171        pub fn hours(&self) -> i64 {
172            self.0.hours()
173        }
174        pub fn minutes(&self) -> i64 {
175            self.0.minutes()
176        }
177        pub fn seconds(&self) -> i64 {
178            self.0.seconds()
179        }
180        pub fn milliseconds(&self) -> i64 {
181            self.0.milliseconds()
182        }
183        pub fn microseconds(&self) -> f64 {
184            // The error case should never occur since
185            // duration values are clamped within range
186            //
187            // https://github.com/boa-dev/temporal/issues/189
188            f64::from_i128(self.0.microseconds()).unwrap_or(0.)
189        }
190        pub fn nanoseconds(&self) -> f64 {
191            // The error case should never occur since
192            // duration values are clamped within range
193            //
194            // https://github.com/boa-dev/temporal/issues/189
195            f64::from_i128(self.0.nanoseconds()).unwrap_or(0.)
196        }
197
198        pub fn sign(&self) -> Sign {
199            self.0.sign().into()
200        }
201
202        pub fn is_zero(&self) -> bool {
203            self.0.is_zero()
204        }
205
206        pub fn abs(&self) -> Box<Self> {
207            Box::new(Self(self.0.abs()))
208        }
209        pub fn negated(&self) -> Box<Self> {
210            Box::new(Self(self.0.negated()))
211        }
212
213        pub fn add(&self, other: &Self) -> Result<Box<Self>, TemporalError> {
214            self.0
215                .add(&other.0)
216                .map(|x| Box::new(Duration(x)))
217                .map_err(Into::into)
218        }
219
220        pub fn subtract(&self, other: &Self) -> Result<Box<Self>, TemporalError> {
221            self.0
222                .subtract(&other.0)
223                .map(|x| Box::new(Duration(x)))
224                .map_err(Into::into)
225        }
226
227        pub fn to_string(
228            &self,
229            options: ToStringRoundingOptions,
230            write: &mut DiplomatWrite,
231        ) -> Result<(), TemporalError> {
232            use core::fmt::Write;
233            let string = self.0.as_temporal_string(options.into())?;
234            // throw away the error, this should always succeed
235            let _ = write.write_str(&string);
236
237            Ok(())
238        }
239
240        #[cfg(feature = "compiled_data")]
241        pub fn round(
242            &self,
243            options: RoundingOptions,
244            relative_to: RelativeTo,
245        ) -> Result<Box<Self>, TemporalError> {
246            self.round_with_provider(options, relative_to, &Provider::compiled())
247        }
248        pub fn round_with_provider<'p>(
249            &self,
250            options: RoundingOptions,
251            relative_to: RelativeTo,
252            p: &Provider<'p>,
253        ) -> Result<Box<Self>, TemporalError> {
254            with_provider!(p, |p| self.0.round_with_provider(
255                options.try_into()?,
256                relative_to.into(),
257                p
258            ))
259            .map(|x| Box::new(Duration(x)))
260            .map_err(Into::into)
261        }
262
263        #[cfg(feature = "compiled_data")]
264        pub fn compare(&self, other: &Self, relative_to: RelativeTo) -> Result<i8, TemporalError> {
265            self.compare_with_provider(other, relative_to, &Provider::compiled())
266        }
267        pub fn compare_with_provider<'p>(
268            &self,
269            other: &Self,
270            relative_to: RelativeTo,
271            p: &Provider<'p>,
272        ) -> Result<i8, TemporalError> {
273            // Ideally we'd return core::cmp::Ordering here but Diplomat
274            // isn't happy about needing to convert the contents of a result
275            with_provider!(p, |p| self.0.compare_with_provider(
276                &other.0,
277                relative_to.into(),
278                p
279            ))
280            .map(|x| x as i8)
281            .map_err(Into::into)
282        }
283
284        #[cfg(feature = "compiled_data")]
285        pub fn total(&self, unit: Unit, relative_to: RelativeTo) -> Result<f64, TemporalError> {
286            self.total_with_provider(unit, relative_to, &Provider::compiled())
287        }
288        pub fn total_with_provider<'p>(
289            &self,
290            unit: Unit,
291            relative_to: RelativeTo,
292            p: &Provider<'p>,
293        ) -> Result<f64, TemporalError> {
294            with_provider!(p, |p| self.0.total_with_provider(
295                unit.into(),
296                relative_to.into(),
297                p
298            ))
299            .map(|x| x.as_inner())
300            .map_err(Into::into)
301        }
302
303        #[allow(clippy::should_implement_trait)]
304        pub fn clone(&self) -> Box<Self> {
305            Box::new(Self(self.0))
306        }
307    }
308}
309
310impl TryFrom<ffi::PartialDuration> for temporal_rs::partial::PartialDuration {
311    type Error = TemporalError;
312    fn try_from(other: ffi::PartialDuration) -> Result<Self, TemporalError> {
313        use num_traits::FromPrimitive;
314        Ok(Self {
315            years: other.years.into_option(),
316            months: other.months.into_option(),
317            weeks: other.weeks.into_option(),
318            days: other.days.into_option(),
319            hours: other.hours.into_option(),
320            minutes: other.minutes.into_option(),
321            seconds: other.seconds.into_option(),
322            milliseconds: other.milliseconds.into_option(),
323            microseconds: other
324                .microseconds
325                .into_option()
326                .map(|v| i128::from_f64(v).ok_or(TemporalError::range("μs out of range")))
327                .transpose()?,
328            nanoseconds: other
329                .nanoseconds
330                .into_option()
331                .map(|v| i128::from_f64(v).ok_or(TemporalError::range("ns out of range")))
332                .transpose()?,
333        })
334    }
335}