Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Date, Time, and DateTime from str impls via IXDTF #5260

Merged
merged 46 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e6c0e2d
Add initial ixdtf integration to icu::calendar
sffc Jul 17, 2024
59b40a7
Add AnyCalendar support
sffc Jul 17, 2024
e2a012e
Refactor
sffc Jul 17, 2024
70047f5
fmt
sffc Jul 17, 2024
4b361d1
Add Time and DateTime
sffc Jul 17, 2024
05f5fa9
Add utf8 functions
sffc Jul 17, 2024
de60458
Use it in datetime test
sffc Jul 17, 2024
97663dd
Add impl FromStr
sffc Jul 17, 2024
de0a56e
features
sffc Jul 17, 2024
5594033
Add diplomat wrappers
sffc Jul 18, 2024
444c064
Remove js-specific rename
sffc Jul 18, 2024
b937e49
diplomat-gen
sffc Jul 18, 2024
2d9a022
Add ixdtf dep
sffc Jul 18, 2024
e8b63bb
fmt
sffc Jul 18, 2024
ce2ba13
features
sffc Jul 18, 2024
e3c9962
msrv?
sffc Jul 18, 2024
15543e8
rust_link
sffc Jul 18, 2024
504d026
fmt, diplomat-gen
sffc Jul 18, 2024
1f5b4c9
Update components/calendar/src/ixdtf.rs
sffc Jul 18, 2024
87299ff
MissingFields
sffc Jul 18, 2024
9d7e12a
remove public From impls
sffc Jul 18, 2024
32eac8f
Fix diplomat link and regen
sffc Jul 18, 2024
8800afd
Merge remote-tracking branch 'upstream/main' into ixdtf-calendar-inte…
sffc Jul 18, 2024
e47fe2a
diplomat-gen
sffc Jul 18, 2024
a84b333
More rust links
sffc Jul 19, 2024
d51d245
Remove icu_capi ixdtf dep
sffc Jul 19, 2024
bb88ea6
Merge branch 'main' into ixdtf-calendar-integration
sffc Jul 19, 2024
78a6947
Rust renames
sffc Jul 19, 2024
49c4152
icu_capi error rename
sffc Jul 19, 2024
0eb45b1
from_string
sffc Jul 19, 2024
3f4c0ac
Fix rust_link
sffc Jul 19, 2024
d54fa6e
diplomat-gen
sffc Jul 19, 2024
78df9bf
Add missing files from diplomat-gen
sffc Jul 19, 2024
c3d8f08
Merge remote-tracking branch 'upstream/main' into ixdtf-calendar-inte…
sffc Jul 19, 2024
752151f
Fix rust_link again
sffc Jul 19, 2024
6ae5f22
diplomat-gen
sffc Jul 19, 2024
67f6e9c
cargo fmt
sffc Jul 19, 2024
f87d82e
Update components/calendar/src/ixdtf.rs
sffc Jul 19, 2024
cebaca9
ParseError
sffc Jul 19, 2024
8bf9e89
diplomat-gen
sffc Jul 19, 2024
9303143
oops, CalendarParseError
sffc Jul 19, 2024
b607aca
diplomat-gen
sffc Jul 19, 2024
cfc0b1a
Merge remote-tracking branch 'upstream/main' into ixdtf-calendar-inte…
sffc Jul 19, 2024
b851b04
Update attrs according to what @Manishearth says I should do
sffc Jul 19, 2024
db13889
diplomat-gen
sffc Jul 19, 2024
f9c04f8
Remove redundant all()
sffc Jul 19, 2024
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion components/calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ calendrical_calculations = { workspace = true }
displaydoc = { workspace = true }
icu_provider = { workspace = true, features = ["macros"] }
icu_locale_core = { workspace = true }
ixdtf = { workspace = true, optional = true }
tinystr = { workspace = true, features = ["alloc", "zerovec"] }
zerovec = { workspace = true, features = ["derive"] }
writeable = { workspace = true }
Expand All @@ -46,7 +47,8 @@ criterion = { workspace = true }


[features]
default = ["compiled_data"]
default = ["compiled_data", "ixdtf"]
ixdtf = ["dep:ixdtf"]
logging = ["calendrical_calculations/logging"]
std = ["icu_provider/std", "icu_locale_core/std", "calendrical_calculations/std"]
serde = ["dep:serde", "zerovec/serde", "tinystr/serde", "icu_provider/serde"]
Expand Down
301 changes: 301 additions & 0 deletions components/calendar/src/ixdtf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use core::str::FromStr;

use crate::{AnyCalendar, Date, DateTime, Iso, RangeError, Time};
use ixdtf::parsers::records::IxdtfParseRecord;
use ixdtf::parsers::IxdtfParser;
use ixdtf::ParserError;

/// An error returned from parsing an IXDTF string to an `icu_calendar` type.
#[derive(Debug)]
#[non_exhaustive]
pub enum ParseError {
/// Syntax error in the IXDTF string.
Syntax(ParserError),
Copy link
Member

Choose a reason for hiding this comment

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

follow-up: this should also be ParseError instead of ParserError.

Copy link
Member Author

Choose a reason for hiding this comment

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

/// Value is out of range.
Range(RangeError),
/// The IXDTF is missing fields required for parsing into the chosen type.
MissingFields,
/// The IXDTF specifies an unknown calendar.
UnknownCalendar,
}

impl From<RangeError> for ParseError {
fn from(value: RangeError) -> Self {
Self::Range(value)
}
}

impl From<ParserError> for ParseError {
fn from(value: ParserError) -> Self {
Self::Syntax(value)
}
}

impl AnyCalendar {
#[cfg(feature = "compiled_data")]
fn try_from_ixdtf_record(ixdtf_record: &IxdtfParseRecord) -> Result<Self, ParseError> {
let calendar_id = ixdtf_record.calendar.unwrap_or(b"iso");
let calendar_kind = crate::AnyCalendarKind::get_for_bcp47_bytes(calendar_id)
.ok_or(ParseError::UnknownCalendar)?;
let calendar = AnyCalendar::new(calendar_kind);
Ok(calendar)
}
}

impl Date<Iso> {
/// Creates a [`Date`] in the ISO-8601 calendar from an IXDTF syntax string.
///
/// Ignores any calendar annotations in the string.
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
///
/// # Examples
///
/// ```
/// use icu::calendar::Date;
///
/// let date = Date::try_iso_from_str("2024-07-17").unwrap();
///
/// assert_eq!(date.year().number, 2024);
/// assert_eq!(
/// date.month().code,
/// icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M07"))
/// );
/// assert_eq!(date.day_of_month().0, 17);
/// ```
pub fn try_iso_from_str(ixdtf_str: &str) -> Result<Self, ParseError> {
Self::try_iso_from_utf8(ixdtf_str.as_bytes())
}

/// Creates a [`Date`] in the ISO-8601 calendar from an IXDTF syntax string.
///
/// See [`Self::try_iso_from_str()`].
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
pub fn try_iso_from_utf8(ixdtf_str: &[u8]) -> Result<Self, ParseError> {
let ixdtf_record = IxdtfParser::from_utf8(ixdtf_str).parse()?;
Self::try_from_ixdtf_record(&ixdtf_record)
}

fn try_from_ixdtf_record(ixdtf_record: &IxdtfParseRecord) -> Result<Self, ParseError> {
let date_record = ixdtf_record.date.ok_or(ParseError::MissingFields)?;
let date = Self::try_new_iso_date(date_record.year, date_record.month, date_record.day)?;
Ok(date)
}
}

impl FromStr for Date<Iso> {
type Err = ParseError;
fn from_str(ixdtf_str: &str) -> Result<Self, Self::Err> {
Self::try_iso_from_str(ixdtf_str)
}
}

impl Date<AnyCalendar> {
/// Creates a [`Date`] in any calendar from an IXDTF syntax string with compiled data.
///
/// ✨ *Enabled with the `compiled_data` and `ixdtf` Cargo features.*
///
/// # Examples
///
/// ```
/// use icu::calendar::Date;
///
/// let date = Date::try_from_str("2024-07-17[u-ca=hebrew]").unwrap();
///
/// assert_eq!(date.year().number, 5784);
/// assert_eq!(
/// date.month().code,
/// icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M10"))
/// );
/// assert_eq!(date.day_of_month().0, 11);
/// ```
#[cfg(feature = "compiled_data")]
pub fn try_from_str(ixdtf_str: &str) -> Result<Self, ParseError> {
Self::try_from_utf8(ixdtf_str.as_bytes())
}

/// Creates a [`Date`] in any calendar from an IXDTF syntax string with compiled data.
///
/// ✨ *Enabled with the `compiled_data` and `ixdtf` Cargo features.*
///
/// See [`Self::try_from_str()`].
#[cfg(feature = "compiled_data")]
pub fn try_from_utf8(ixdtf_str: &[u8]) -> Result<Self, ParseError> {
let ixdtf_record = IxdtfParser::from_utf8(ixdtf_str).parse()?;
let iso_date = Date::<Iso>::try_from_ixdtf_record(&ixdtf_record)?;
let calendar = AnyCalendar::try_from_ixdtf_record(&ixdtf_record)?;
let date = iso_date.to_any().to_calendar(calendar);
Ok(date)
}
}

#[cfg(feature = "compiled_data")]
impl FromStr for Date<AnyCalendar> {
type Err = ParseError;
fn from_str(ixdtf_str: &str) -> Result<Self, Self::Err> {
Self::try_from_str(ixdtf_str)
}
}

impl Time {
/// Creates a [`Time`] from an IXDTF syntax string of a time.
///
/// Does not support parsing an IXDTF string with a date and time; for that, use [`DateTime`].
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
///
/// # Examples
///
/// ```
/// use icu::calendar::Time;
///
/// let time = Time::try_from_str("16:01:17.045").unwrap();
///
/// assert_eq!(time.hour.number(), 16);
/// assert_eq!(time.minute.number(), 1);
/// assert_eq!(time.second.number(), 17);
/// assert_eq!(time.nanosecond.number(), 45000000);
/// ```
pub fn try_from_str(ixdtf_str: &str) -> Result<Self, ParseError> {
Self::try_from_utf8(ixdtf_str.as_bytes())
}

/// Creates a [`Time`] in the ISO-8601 calendar from an IXDTF syntax string.
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
///
/// See [`Self::try_from_str()`].
pub fn try_from_utf8(ixdtf_str: &[u8]) -> Result<Self, ParseError> {
let ixdtf_record = IxdtfParser::from_utf8(ixdtf_str).parse_time()?;
Self::try_from_ixdtf_record(&ixdtf_record)
}

fn try_from_ixdtf_record(ixdtf_record: &IxdtfParseRecord) -> Result<Self, ParseError> {
let time_record = ixdtf_record.time.ok_or(ParseError::MissingFields)?;
let time = Self::try_new(
time_record.hour,
time_record.minute,
time_record.second,
time_record.nanosecond,
)?;
Ok(time)
}
}

impl FromStr for Time {
type Err = ParseError;
fn from_str(ixdtf_str: &str) -> Result<Self, Self::Err> {
Self::try_from_str(ixdtf_str)
}
}

impl DateTime<Iso> {
/// Creates a [`DateTime`] in the ISO-8601 calendar from an IXDTF syntax string.
///
/// Ignores any calendar annotations in the string.
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
///
/// # Examples
///
/// ```
/// use icu::calendar::DateTime;
///
/// let datetime = DateTime::try_iso_from_str("2024-07-17T16:01:17.045").unwrap();
///
/// assert_eq!(datetime.date.year().number, 2024);
/// assert_eq!(
/// datetime.date.month().code,
/// icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M07"))
/// );
/// assert_eq!(datetime.date.day_of_month().0, 17);
///
/// assert_eq!(datetime.time.hour.number(), 16);
/// assert_eq!(datetime.time.minute.number(), 1);
/// assert_eq!(datetime.time.second.number(), 17);
/// assert_eq!(datetime.time.nanosecond.number(), 45000000);
/// ```
pub fn try_iso_from_str(ixdtf_str: &str) -> Result<Self, ParseError> {
Self::try_iso_from_utf8(ixdtf_str.as_bytes())
}

/// Creates a [`DateTime`] in the ISO-8601 calendar from an IXDTF syntax string.
///
/// ✨ *Enabled with the `ixdtf` Cargo feature.*
///
/// See [`Self::try_iso_from_str()`].
pub fn try_iso_from_utf8(ixdtf_str: &[u8]) -> Result<Self, ParseError> {
let ixdtf_record = IxdtfParser::from_utf8(ixdtf_str).parse()?;
Self::try_from_ixdtf_record(&ixdtf_record)
}

fn try_from_ixdtf_record(ixdtf_record: &IxdtfParseRecord) -> Result<Self, ParseError> {
let date = Date::<Iso>::try_from_ixdtf_record(ixdtf_record)?;
let time = Time::try_from_ixdtf_record(ixdtf_record)?;
Ok(Self::new(date, time))
}
}

impl FromStr for DateTime<Iso> {
type Err = ParseError;
fn from_str(ixdtf_str: &str) -> Result<Self, Self::Err> {
Self::try_iso_from_str(ixdtf_str)
}
}

impl DateTime<AnyCalendar> {
/// Creates a [`DateTime`] in any calendar from an IXDTF syntax string with compiled data.
///
/// ✨ *Enabled with the `compiled_data` and `ixdtf` Cargo features.*
///
/// # Examples
///
/// ```
/// use icu::calendar::DateTime;
///
/// let datetime = DateTime::try_from_str("2024-07-17T16:01:17.045[u-ca=hebrew]").unwrap();
///
/// assert_eq!(datetime.date.year().number, 5784);
/// assert_eq!(
/// datetime.date.month().code,
/// icu::calendar::types::MonthCode(tinystr::tinystr!(4, "M10"))
/// );
/// assert_eq!(datetime.date.day_of_month().0, 11);
///
/// assert_eq!(datetime.time.hour.number(), 16);
/// assert_eq!(datetime.time.minute.number(), 1);
/// assert_eq!(datetime.time.second.number(), 17);
/// assert_eq!(datetime.time.nanosecond.number(), 45000000);
/// ```
#[cfg(feature = "compiled_data")]
pub fn try_from_str(ixdtf_str: &str) -> Result<Self, ParseError> {
Self::try_from_utf8(ixdtf_str.as_bytes())
}

/// Creates a [`DateTime`] in any calendar from an IXDTF syntax string with compiled data.
///
/// See [`Self::try_from_str()`].
///
/// ✨ *Enabled with the `compiled_data` and `ixdtf` Cargo features.*
#[cfg(feature = "compiled_data")]
pub fn try_from_utf8(ixdtf_str: &[u8]) -> Result<Self, ParseError> {
let ixdtf_record = IxdtfParser::from_utf8(ixdtf_str).parse()?;
let iso_datetime = DateTime::<Iso>::try_from_ixdtf_record(&ixdtf_record)?;
let calendar = AnyCalendar::try_from_ixdtf_record(&ixdtf_record)?;
let datetime = iso_datetime.to_any().to_calendar(calendar);
Ok(datetime)
}
}

#[cfg(feature = "compiled_data")]
impl FromStr for DateTime<AnyCalendar> {
type Err = ParseError;
fn from_str(ixdtf_str: &str) -> Result<Self, Self::Err> {
Self::try_from_str(ixdtf_str)
}
}
4 changes: 4 additions & 0 deletions components/calendar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ pub mod hebrew;
pub mod indian;
pub mod islamic;
pub mod iso;
#[cfg(feature = "ixdtf")]
mod ixdtf;
pub mod japanese;
pub mod julian;
pub mod persian;
Expand All @@ -151,6 +153,8 @@ pub mod week {
pub use week_of::MIN_UNIT_DAYS;
}

#[cfg(feature = "ixdtf")]
pub use crate::ixdtf::ParseError;
#[doc(no_inline)]
pub use any_calendar::{AnyCalendar, AnyCalendarKind};
pub use calendar::Calendar;
Expand Down
1 change: 1 addition & 0 deletions components/datetime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ litemap = { workspace = true, optional = true }
[dev-dependencies]
icu = { path = "../../components/icu", default-features = false }
icu_benchmark_macros = { path = "../../tools/benchmark/macros" }
icu_calendar = { path = "../calendar", features = ["ixdtf"] }
icu_provider_adapters = { path = "../../provider/adapters" }
icu_provider_blob = { path = "../../provider/blob" }
litemap = { path = "../../utils/litemap" }
Expand Down
26 changes: 2 additions & 24 deletions components/datetime/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,8 @@ use icu_timezone::CustomTimeZone;
/// assert_eq!(u32::from(date.time.nanosecond), 101_000_000);
/// ```
pub fn parse_gregorian_from_str(input: &str) -> DateTime<Gregorian> {
#![allow(clippy::indexing_slicing)]
assert!(input.len() > 20 || input.len() == 19);
let year: i32 = input[0..4].parse().unwrap();
assert_eq!(input.as_bytes()[4], b'-');
let month: u8 = input[5..7].parse().unwrap();
assert_eq!(input.as_bytes()[7], b'-');
let day: u8 = input[8..10].parse().unwrap();
assert_eq!(input.as_bytes()[10], b'T');
let hour: u8 = input[11..13].parse().unwrap();
assert_eq!(input.as_bytes()[13], b':');
let minute: u8 = input[14..16].parse().unwrap();
assert_eq!(input.as_bytes()[16], b':');
let second: u8 = input[17..19].parse().unwrap();
let mut datetime =
DateTime::try_new_gregorian_datetime(year, month, day, hour, minute, second).unwrap();
if input.len() > 20 {
assert_eq!(input.as_bytes()[19], b'.');
let fraction_str = &input[20..29.min(input.len())];
let fraction = fraction_str.parse::<u32>().unwrap();
let nanoseconds = fraction * (10u32.pow(9 - fraction_str.len() as u32));
datetime.time = icu_calendar::Time::try_new(hour, minute, second, nanoseconds).unwrap();
};

datetime
let datetime_iso = DateTime::try_iso_from_str(input).unwrap();
datetime_iso.to_calendar(Gregorian)
}

/// Parse a [`DateTime`] and [`CustomTimeZone`] from a string.
Expand Down
4 changes: 2 additions & 2 deletions ffi/capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ diplomat = { workspace = true }
diplomat-runtime = { workspace = true }

# Optional ICU4X components and their dependent utils
fixed_decimal = { workspace = true, features = ["ryu"] , optional = true}
icu_calendar = { workspace = true, optional = true }
fixed_decimal = { workspace = true, features = ["ryu"] , optional = true }
icu_calendar = { workspace = true, features = ["ixdtf"], optional = true }
icu_casemap = { workspace = true, optional = true }
icu_collator = { workspace = true, optional = true }
icu_collections = { workspace = true, optional = true }
Expand Down
Loading