Skip to main content

boa_engine/builtins/temporal/
mod.rs

1//! The ECMAScript `Temporal` stage 3 built-in implementation.
2//!
3//! Boa's Temporal implementation uses the `temporal_rs` crate
4//! for the core functionality of the implementation.
5//!
6//! More information:
7//!
8//! [spec]: https://tc39.es/proposal-temporal/
9
10mod calendar;
11mod duration;
12mod error;
13mod instant;
14mod now;
15mod options;
16mod plain_date;
17mod plain_date_time;
18mod plain_month_day;
19mod plain_time;
20mod plain_year_month;
21mod zoneddatetime;
22
23#[cfg(test)]
24mod tests;
25
26pub use self::{
27    duration::*, instant::*, now::*, plain_date::*, plain_date_time::*, plain_month_day::*,
28    plain_time::*, plain_year_month::*, zoneddatetime::*,
29};
30
31use crate::{
32    Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
33    builtins::{
34        BuiltInBuilder, BuiltInObject, IntrinsicObject,
35        temporal::calendar::get_temporal_calendar_slot_value_with_default,
36    },
37    context::intrinsics::Intrinsics,
38    js_string,
39    property::Attribute,
40    realm::Realm,
41    string::StaticJsStrings,
42};
43use temporal_rs::{
44    PlainDate as TemporalDate, ZonedDateTime as TemporalZonedDateTime,
45    partial::PartialZonedDateTime, primitive::FiniteF64,
46};
47use temporal_rs::{options::RelativeTo, partial::PartialDate};
48
49// An enum representing common fields across `Temporal` objects.
50pub(crate) enum DateTimeValues {
51    Year,
52    Month,
53    MonthCode,
54    Week,
55    Day,
56    Hour,
57    Minute,
58    Second,
59    Millisecond,
60    Microsecond,
61    Nanosecond,
62}
63
64/// `Temporal` built-in implementation
65///
66/// The Temporal implementation in Boa uses `temporal_rs` for the
67/// core implementation.
68///
69/// More information:
70///  - [ECMAScript Temporal proposal][spec]
71///  - [MDN Reference][mdn]
72///  - [`temporal_rs` docs][temporal_rs-docs]
73///
74/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects
75/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
76/// [temporal_rs-docs]: https://docs.rs/temporal_rs/latest/temporal_rs/
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub(crate) struct Temporal;
79
80impl BuiltInObject for Temporal {
81    const NAME: JsString = StaticJsStrings::TEMPORAL;
82}
83
84impl IntrinsicObject for Temporal {
85    fn init(realm: &Realm) {
86        BuiltInBuilder::with_intrinsic::<Self>(realm)
87            .static_property(
88                JsSymbol::to_string_tag(),
89                Self::NAME,
90                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
91            )
92            .static_property(
93                js_string!("Now"),
94                realm.intrinsics().objects().now(),
95                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
96            )
97            .static_property(
98                js_string!("Duration"),
99                realm.intrinsics().constructors().duration().constructor(),
100                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
101            )
102            .static_property(
103                js_string!("Instant"),
104                realm.intrinsics().constructors().instant().constructor(),
105                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
106            )
107            .static_property(
108                js_string!("PlainDate"),
109                realm.intrinsics().constructors().plain_date().constructor(),
110                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
111            )
112            .static_property(
113                js_string!("PlainDateTime"),
114                realm
115                    .intrinsics()
116                    .constructors()
117                    .plain_date_time()
118                    .constructor(),
119                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
120            )
121            .static_property(
122                js_string!("PlainMonthDay"),
123                realm
124                    .intrinsics()
125                    .constructors()
126                    .plain_month_day()
127                    .constructor(),
128                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
129            )
130            .static_property(
131                js_string!("PlainTime"),
132                realm.intrinsics().constructors().plain_time().constructor(),
133                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
134            )
135            .static_property(
136                js_string!("PlainYearMonth"),
137                realm
138                    .intrinsics()
139                    .constructors()
140                    .plain_year_month()
141                    .constructor(),
142                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
143            )
144            .static_property(
145                js_string!("ZonedDateTime"),
146                realm
147                    .intrinsics()
148                    .constructors()
149                    .zoned_date_time()
150                    .constructor(),
151                Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
152            )
153            .build();
154    }
155
156    fn get(intrinsics: &Intrinsics) -> JsObject {
157        intrinsics.objects().temporal()
158    }
159}
160
161// -- Temporal Abstract Operations --
162
163pub(crate) fn get_relative_to_option(
164    options: &JsObject,
165    context: &mut Context,
166) -> JsResult<Option<RelativeTo>> {
167    // Let value be ? Get(options, "relativeTo").
168    let value = options.get(js_string!("relativeTo"), context)?;
169    // 2. If value is undefined, return the Record { [[PlainRelativeTo]]: undefined, [[ZonedRelativeTo]]: undefined }.
170    if value.is_undefined() {
171        return Ok(None);
172    }
173    // 3. Let offsetBehaviour be option.
174    // 4. Let matchBehaviour be match-exactly.
175    // 5. If value is an Object, then
176    if let Some(object) = value.as_object() {
177        // a. If value has an [[InitializedTemporalZonedDateTime]] internal slot, then
178        if let Some(zdt) = object.downcast_ref::<ZonedDateTime>() {
179            // i. Return the Record { [[PlainRelativeTo]]: undefined, [[ZonedRelativeTo]]: value }.
180            return Ok(Some(RelativeTo::ZonedDateTime(zdt.inner.as_ref().clone())));
181        // b. If value has an [[InitializedTemporalDate]] internal slot, then
182        } else if let Some(date) = object.downcast_ref::<PlainDate>() {
183            // i. Return the Record { [[PlainRelativeTo]]: value, [[ZonedRelativeTo]]: undefined }.
184            return Ok(Some(RelativeTo::PlainDate(date.inner.clone())));
185        // c. If value has an [[InitializedTemporalDateTime]] internal slot, then
186        } else if let Some(dt) = object.downcast_ref::<PlainDateTime>() {
187            // i. Let plainDate be ! CreateTemporalDate(value.[[ISODateTime]].[[ISODate]], value.[[Calendar]]).
188            // ii. Return the Record { [[PlainRelativeTo]]: plainDate, [[ZonedRelativeTo]]: undefined }.
189            return Ok(Some(RelativeTo::PlainDate(dt.inner.clone().into())));
190        }
191        // d. Let calendar be ? GetTemporalCalendarIdentifierWithISODefault(value).
192        let calendar = get_temporal_calendar_slot_value_with_default(&object, context)?;
193        // TODO: Check behavior around Partial here.
194        // e. Let fields be ? PrepareCalendarFields(calendar, value, « year, month, month-code, day », « hour, minute, second, millisecond, microsecond, nanosecond, offset, time-zone », «»).
195        let (fields, timezone) = to_zoned_date_time_fields(
196            &object,
197            &calendar,
198            ZdtFieldsType::TimeZoneNotRequired,
199            context,
200        )?;
201        // f. Let result be ? InterpretTemporalDateTimeFields(calendar, fields, constrain).
202        // g. Let timeZone be fields.[[TimeZone]].
203        // h. Let offsetString be fields.[[OffsetString]].
204        // i. If offsetString is unset, then
205        // i. Set offsetBehaviour to wall.
206        // j. Let isoDate be result.[[ISODate]].
207        // TODO: Update in temporal_rs
208        if timezone.is_none() {
209            return Ok(Some(RelativeTo::PlainDate(TemporalDate::from_partial(
210                PartialDate {
211                    calendar_fields: fields.calendar_fields,
212                    calendar,
213                },
214                None,
215            )?)));
216        }
217        // k. Let time be result.[[Time]].
218        let zdt = TemporalZonedDateTime::from_partial_with_provider(
219            PartialZonedDateTime {
220                fields,
221                timezone,
222                calendar,
223            },
224            None,
225            None,
226            None,
227            context.tz_provider(),
228        )?;
229        return Ok(Some(RelativeTo::ZonedDateTime(zdt)));
230    }
231    // 6. Else,
232    // a. If value is not a String, throw a TypeError exception.
233    let Some(relative_to_str) = value.as_string() else {
234        return Err(JsNativeError::typ()
235            .with_message("relativeTo must be an object or string.")
236            .into());
237    };
238    // Steps 7-12 are handled by temporal_rs
239    Ok(Some(RelativeTo::try_from_str_with_provider(
240        &relative_to_str.to_std_string_escaped(),
241        context.tz_provider(),
242    )?))
243}
244
245// 13.26 IsPartialTemporalObject ( object )
246pub(crate) fn is_partial_temporal_object(
247    value: &JsValue,
248    context: &mut Context,
249) -> JsResult<Option<JsObject>> {
250    // 1. If value is not an Object, return false.
251    let Some(obj) = value.as_object() else {
252        return Ok(None);
253    };
254
255    // 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]],
256    // [[InitializedTemporalMonthDay]], [[InitializedTemporalTime]],
257    // [[InitializedTemporalYearMonth]], or
258    // [[InitializedTemporalZonedDateTime]] internal slot, return false.
259    if obj.is::<PlainDate>()
260        || obj.is::<PlainDateTime>()
261        || obj.is::<PlainMonthDay>()
262        || obj.is::<PlainYearMonth>()
263        || obj.is::<PlainTime>()
264        || obj.is::<ZonedDateTime>()
265    {
266        return Ok(None);
267    }
268
269    // 3. Let calendarProperty be ? Get(value, "calendar").
270    let calendar_property = obj.get(js_string!("calendar"), context)?;
271    // 4. If calendarProperty is not undefined, return false.
272    if !calendar_property.is_undefined() {
273        return Ok(None);
274    }
275    // 5. Let timeZoneProperty be ? Get(value, "timeZone").
276    let time_zone_property = obj.get(js_string!("timeZone"), context)?;
277    // 6. If timeZoneProperty is not undefined, return false.
278    if !time_zone_property.is_undefined() {
279        return Ok(None);
280    }
281    // 7. Return true.
282    Ok(Some(obj))
283}
284
285impl JsValue {
286    pub(crate) fn to_finitef64(&self, context: &mut Context) -> JsResult<FiniteF64> {
287        let number = self.to_number(context)?;
288        let result = FiniteF64::try_from(number)?;
289        Ok(result)
290    }
291}
292
293fn extract_from_temporal_type<DF, DTF, YMF, MDF, ZDTF, Ret>(
294    object: &JsObject,
295    date_f: DF,
296    datetime_f: DTF,
297    year_month_f: YMF,
298    month_day_f: MDF,
299    zoned_datetime_f: ZDTF,
300) -> JsResult<Option<Ret>>
301where
302    DF: FnOnce(&PlainDate) -> JsResult<Option<Ret>>,
303    DTF: FnOnce(&PlainDateTime) -> JsResult<Option<Ret>>,
304    YMF: FnOnce(&PlainYearMonth) -> JsResult<Option<Ret>>,
305    MDF: FnOnce(&PlainMonthDay) -> JsResult<Option<Ret>>,
306    ZDTF: FnOnce(&ZonedDateTime) -> JsResult<Option<Ret>>,
307{
308    if let Some(date) = object.downcast_ref::<PlainDate>() {
309        return date_f(&date);
310    } else if let Some(dt) = object.downcast_ref::<PlainDateTime>() {
311        return datetime_f(&dt);
312    } else if let Some(ym) = object.downcast_ref::<PlainYearMonth>() {
313        return year_month_f(&ym);
314    } else if let Some(md) = object.downcast_ref::<PlainMonthDay>() {
315        return month_day_f(&md);
316    } else if let Some(dt) = object.downcast_ref::<ZonedDateTime>() {
317        return zoned_datetime_f(&dt);
318    }
319
320    Ok(None)
321}