Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/builtins/core/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, Disambiguation,
DisplayCalendar, ResolvedRoundingOptions, Unit, UnitGroup,
},
parsers::{parse_date_time, IxdtfStringBuilder},
parsers::{parse_date_time, IxdtfStringBuilder, TemporalParser},
provider::{NeverProvider, TimeZoneProvider},
MonthCode, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
};
Expand Down Expand Up @@ -490,6 +490,25 @@ impl PlainDate {
Self::try_new(date.year, date.month, date.day, calendar)
}

/// Converts a UTF-16 encoded string into a `PlainDate`.
pub fn from_utf16(s: &[u16]) -> TemporalResult<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n.b. temporal_capi has a bunch of internally-converting from_utf16s that probably can be swapped over to these

let parser = TemporalParser::from_utf16(s);
let parsed = parser.parse_date_time()?;

let calendar = if let Some(cal_bytes) = parsed.calendar {
Calendar::try_from_utf8(&cal_bytes)?
} else {
Calendar::default()
};

Self::try_new(
parsed.iso.date.year,
parsed.iso.date.month,
parsed.iso.date.day,
calendar,
)
}

/// Creates a date time with values from a `PartialDate`.
pub fn with(
&self,
Expand Down
182 changes: 157 additions & 25 deletions src/builtins/core/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ use crate::{
DisplayCalendar, ResolvedRoundingOptions, RoundingOptions, ToStringRoundingOptions, Unit,
UnitGroup,
},
parsers::{parse_date_time, IxdtfStringBuilder},
parsers::{IxdtfStringBuilder, TemporalParser},
primitive::FiniteF64,
provider::{NeverProvider, TimeZoneProvider},
temporal_assert, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
temporal_assert, MonthCode, TemporalError, TemporalResult, TimeZone,
};
use alloc::string::String;
use core::{cmp::Ordering, str::FromStr};
Expand Down Expand Up @@ -549,30 +549,30 @@ impl PlainDateTime {

// Converts a UTF-8 encoded string into a `PlainDateTime`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let parse_record = parse_date_time(s)?;

let calendar = parse_record
.calendar
.map(Calendar::try_from_utf8)
.transpose()?
.unwrap_or_default();

let time = parse_record
.time
.map(IsoTime::from_time_record)
.transpose()?
.unwrap_or_default();

let parsed_date = parse_record.date.temporal_unwrap()?;

let date = IsoDate::new_with_overflow(
parsed_date.year,
parsed_date.month,
parsed_date.day,
ArithmeticOverflow::Reject,
)?;
let parser = TemporalParser::from_utf8(s);
let parsed = parser.parse_date_time()?;

let calendar = if let Some(cal_bytes) = parsed.calendar {
Calendar::try_from_utf8(&cal_bytes)?
} else {
Calendar::default()
};

Ok(Self::new_unchecked(parsed.iso, calendar))
}

/// Converts a UTF-16 encoded string into a `PlainDateTime`.
pub fn from_utf16(s: &[u16]) -> TemporalResult<Self> {
let parser = TemporalParser::from_utf16(s);
let parsed = parser.parse_date_time()?;

let calendar = if let Some(cal_bytes) = parsed.calendar {
Calendar::try_from_utf8(&cal_bytes)?
} else {
Calendar::default()
};

Ok(Self::new_unchecked(IsoDateTime::new(date, time)?, calendar))
Ok(Self::new_unchecked(parsed.iso, calendar))
}

/// Creates a new `DateTime` with the fields of a `PartialDateTime`.
Expand Down Expand Up @@ -1530,4 +1530,136 @@ mod tests {
"pads 4 decimal places to 9"
);
}

#[test]
fn test_utf16_datetime_parsing() {
use alloc::vec::Vec;

let datetime_str = "2023-05-15T14:30:45.123";
let datetime_utf16: Vec<u16> = datetime_str.encode_utf16().collect();

// Test UTF-16 parsing
let datetime_utf16_result = PlainDateTime::from_utf16(&datetime_utf16).unwrap();

// Test UTF-8 parsing for comparison
let datetime_utf8_result = PlainDateTime::from_utf8(datetime_str.as_bytes()).unwrap();

// Compare results
assert_eq!(datetime_utf16_result.year(), datetime_utf8_result.year());
assert_eq!(datetime_utf16_result.month(), datetime_utf8_result.month());
assert_eq!(datetime_utf16_result.day(), datetime_utf8_result.day());
assert_eq!(datetime_utf16_result.hour(), datetime_utf8_result.hour());
assert_eq!(
datetime_utf16_result.minute(),
datetime_utf8_result.minute()
);
assert_eq!(
datetime_utf16_result.second(),
datetime_utf8_result.second()
);
assert_eq!(
datetime_utf16_result.millisecond(),
datetime_utf8_result.millisecond()
);

// Test specific values
assert_eq!(datetime_utf16_result.year(), 2023);
assert_eq!(datetime_utf16_result.month(), 5);
assert_eq!(datetime_utf16_result.day(), 15);
assert_eq!(datetime_utf16_result.hour(), 14);
assert_eq!(datetime_utf16_result.minute(), 30);
assert_eq!(datetime_utf16_result.second(), 45);
assert_eq!(datetime_utf16_result.millisecond(), 123);
}

#[test]
fn test_temporal_parser_from_str_as_utf8() {
use crate::parsers::TemporalParser;

let datetime_str = "2023-05-15T14:30:45.123";
let parser = TemporalParser::from_str_as_utf8(datetime_str);

// Test that the parser works correctly with the renamed method
let parsed = parser.parse_date_time().unwrap();

assert_eq!(parsed.iso.date.year, 2023);
assert_eq!(parsed.iso.date.month, 5);
assert_eq!(parsed.iso.date.day, 15);
assert_eq!(parsed.iso.time.hour, 14);
assert_eq!(parsed.iso.time.minute, 30);
assert_eq!(parsed.iso.time.second, 45);
assert_eq!(parsed.iso.time.millisecond, 123);
}

#[test]
fn test_all_temporal_types_utf16_support() {
use crate::{Instant, PlainDate, PlainMonthDay, PlainTime, PlainYearMonth};
use alloc::vec::Vec;

// Test all temporal types have consistent UTF-16 support
let datetime_str = "2023-05-15T14:30:45.123";
let datetime_utf16: Vec<u16> = datetime_str.encode_utf16().collect();

let time_str = "14:30:45.123";
let time_utf16: Vec<u16> = time_str.encode_utf16().collect();

let date_str = "2023-05-15T00:00:00";
let date_utf16: Vec<u16> = date_str.encode_utf16().collect();

let year_month_str = "2023-05";
let year_month_utf16: Vec<u16> = year_month_str.encode_utf16().collect();

let month_day_str = "05-15";
let month_day_utf16: Vec<u16> = month_day_str.encode_utf16().collect();

let instant_str = "2023-05-15T14:30:45.123Z";
let instant_utf16: Vec<u16> = instant_str.encode_utf16().collect();

// Test that all types can parse UTF-16
let datetime = PlainDateTime::from_utf16(&datetime_utf16).unwrap();
assert_eq!(datetime.year(), 2023);
assert_eq!(datetime.month(), 5);
assert_eq!(datetime.day(), 15);
assert_eq!(datetime.hour(), 14);
assert_eq!(datetime.minute(), 30);
assert_eq!(datetime.second(), 45);
assert_eq!(datetime.millisecond(), 123);

let time = PlainTime::from_utf16(&time_utf16).unwrap();
assert_eq!(time.hour(), 14);
assert_eq!(time.minute(), 30);
assert_eq!(time.second(), 45);
assert_eq!(time.millisecond(), 123);

let date = PlainDate::from_utf16(&date_utf16).unwrap();
assert_eq!(date.year(), 2023);
assert_eq!(date.month(), 5);
assert_eq!(date.day(), 15);

let year_month = PlainYearMonth::from_utf16(&year_month_utf16).unwrap();
assert_eq!(year_month.year(), 2023);
assert_eq!(year_month.month(), 5);

let month_day = PlainMonthDay::from_utf16(&month_day_utf16).unwrap();
assert_eq!(month_day.iso_month(), 5);
assert_eq!(month_day.day(), 15);

let instant = Instant::from_utf16(&instant_utf16).unwrap();
assert_eq!(instant.epoch_milliseconds(), 1684161045123);

// Test UTF-16 vs UTF-8 equivalence
let datetime_utf8 = PlainDateTime::from_utf8(datetime_str.as_bytes()).unwrap();
let time_utf8 = PlainTime::from_utf8(time_str.as_bytes()).unwrap();
let date_utf8 = PlainDate::from_utf8(date_str.as_bytes()).unwrap();
let year_month_utf8 = PlainYearMonth::from_utf8(year_month_str.as_bytes()).unwrap();
let month_day_utf8 = PlainMonthDay::from_utf8(month_day_str.as_bytes()).unwrap();
let instant_utf8 = Instant::from_utf8(instant_str.as_bytes()).unwrap();

assert_eq!(datetime, datetime_utf8);
assert_eq!(time, time_utf8);
assert_eq!(date, date_utf8);
assert_eq!(year_month, year_month_utf8);
assert_eq!(month_day, month_day_utf8);
assert_eq!(instant, instant_utf8);
}
}
71 changes: 57 additions & 14 deletions src/builtins/core/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
DifferenceOperation, DifferenceSettings, DisplayOffset, ResolvedRoundingOptions,
RoundingOptions, ToStringRoundingOptions, Unit, UnitGroup,
},
parsers::{parse_instant, IxdtfStringBuilder},
parsers::{IxdtfStringBuilder, TemporalParser},
provider::TimeZoneProvider,
rounding::{IncrementRounder, Round},
unix_time::EpochNanoseconds,
Expand Down Expand Up @@ -278,10 +278,11 @@ impl Instant {

// Converts a UTF-8 encoded string into a `Instant`.
pub fn from_utf8(s: &[u8]) -> TemporalResult<Self> {
let ixdtf_record = parse_instant(s)?;
let parser = TemporalParser::from_utf8(s);
let parsed = parser.parse_instant()?;

// Find the offset
let ns_offset = match ixdtf_record.offset {
let ns_offset = match parsed.offset {
UtcOffsetRecordOrZ::Offset(offset) => {
let ns = offset
.fraction()
Expand All @@ -296,21 +297,63 @@ impl Instant {
UtcOffsetRecordOrZ::Z => 0,
};

let time_nanoseconds = ixdtf_record
.time
.fraction
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
let time_nanoseconds = parsed.iso.time.millisecond as u32 * 1_000_000
+ parsed.iso.time.microsecond as u32 * 1_000
+ parsed.iso.time.nanosecond as u32;
let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000);
let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000);

let balanced = IsoDateTime::balance(
ixdtf_record.date.year,
ixdtf_record.date.month.into(),
ixdtf_record.date.day.into(),
ixdtf_record.time.hour.into(),
ixdtf_record.time.minute.into(),
ixdtf_record.time.second.clamp(0, 59).into(),
parsed.iso.date.year,
parsed.iso.date.month.into(),
parsed.iso.date.day.into(),
parsed.iso.time.hour.into(),
parsed.iso.time.minute.into(),
parsed.iso.time.second.clamp(0, 59).into(),
millisecond.into(),
microsecond.into(),
i128::from(nanosecond) - i128::from(ns_offset),
);

let nanoseconds = balanced.as_nanoseconds()?;

Ok(Self(nanoseconds))
}

/// Converts a UTF-16 encoded string into a `Instant`.
pub fn from_utf16(s: &[u16]) -> TemporalResult<Self> {
let parser = TemporalParser::from_utf16(s);
let parsed = parser.parse_instant()?;

// Find the offset
let ns_offset = match parsed.offset {
UtcOffsetRecordOrZ::Offset(offset) => {
let ns = offset
.fraction()
.and_then(|x| x.to_nanoseconds())
.unwrap_or(0);
(offset.hour() as i64 * NANOSECONDS_PER_HOUR
+ i64::from(offset.minute()) * NANOSECONDS_PER_MINUTE
+ i64::from(offset.second().unwrap_or(0)) * NANOSECONDS_PER_SECOND
+ i64::from(ns))
* offset.sign() as i64
}
UtcOffsetRecordOrZ::Z => 0,
};

let time_nanoseconds = parsed.iso.time.millisecond as u32 * 1_000_000
+ parsed.iso.time.microsecond as u32 * 1_000
+ parsed.iso.time.nanosecond as u32;
let (millisecond, rem) = time_nanoseconds.div_rem_euclid(&1_000_000);
let (microsecond, nanosecond) = rem.div_rem_euclid(&1_000);

let balanced = IsoDateTime::balance(
parsed.iso.date.year,
parsed.iso.date.month.into(),
parsed.iso.date.day.into(),
parsed.iso.time.hour.into(),
parsed.iso.time.minute.into(),
parsed.iso.time.second.clamp(0, 59).into(),
millisecond.into(),
microsecond.into(),
i128::from(nanosecond) - i128::from(ns_offset),
Expand Down
Loading
Loading