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 18 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
2 changes: 2 additions & 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
310 changes: 310 additions & 0 deletions components/calendar/src/ixdtf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// 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::{DateRecord, IxdtfParseRecord, TimeRecord};
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 FromIxdtfError {
/// Syntax error in the IXDTF string.
Ixdtf(ParserError),
Copy link
Member

Choose a reason for hiding this comment

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

Thinking about it, I'd call this Syntax as well.

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

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

impl From<ParserError> for FromIxdtfError {
fn from(value: ParserError) -> Self {
Self::Ixdtf(value)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

these are also public API, might want to do map_err(FromIxdtfError::Ixdtf) instead

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about that but they are unavoidably in the public API as being a variant of FromIxdtfError (now FromStrError)

Copy link
Member Author

Choose a reason for hiding this comment

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

A few choices here:

  1. Ignore it and hope that ixdtf::ParserError stays semver compatible
  2. Log the error and return Syntax with no details in the variant
  3. Make sure the ixdtf crate reaches stable by the time we release the next icu_calendar crate version

CC @Manishearth @nekevss

Copy link
Member

Choose a reason for hiding this comment

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

Define "stable"? The crate works and has a reasonable API right now, no?

These impls simplify some code in this file (barely), which I don't think is worth the cost of them showing up in docs, regardless of how stable ixdtf is.

Copy link
Member Author

Choose a reason for hiding this comment

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

"stable" meaning version 1.0 of the ixdtf crate.

I don't quite understand the harm in having From impls when they are truly trivial, wrapping the error with a specific enum variant.

From impls barely show up in docs because they are a standard library trait implementation.

Copy link
Member

Choose a reason for hiding this comment

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

I don't understand that requirement, fixed_decimal, litemap, tinystr, writeable, are all crates that appear on our APIs and are pre-1.0.

Copy link
Member Author

@sffc sffc Jul 19, 2024

Choose a reason for hiding this comment

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

I realized I don't really like option 2, either, because I'd want to add the data when we can, and adding a value to the Syntax variant is also itself a semver change.


impl TryFrom<DateRecord> for Date<Iso> {
type Error = RangeError;
fn try_from(value: DateRecord) -> Result<Self, Self::Error> {
Self::try_new_iso_date(value.year, value.month, value.day)
}
}

impl TryFrom<TimeRecord> for Time {
type Error = RangeError;
fn try_from(value: TimeRecord) -> Result<Self, Self::Error> {
Self::try_new(value.hour, value.minute, value.second, value.nanosecond)
}
}

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

Choose a reason for hiding this comment

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

hmm the records feel like ixdtf parser implementation types, so I'd rather not expose conversions from them. Date and Time are trivial enough to be done manually, and for AnyCalendar I don't really see the use case

Copy link
Member Author

Choose a reason for hiding this comment

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

I figured since DateRecord and TimeRecord are public types, we should have public conversions. But, actually, it puts ixdtf into icu_datetime's API, so we should probably at least wait until ixdtf has a 1.0 release.


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, FromIxdtfError> {
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, FromIxdtfError> {
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, FromIxdtfError> {
let date_record = ixdtf_record.date.ok_or(FromIxdtfError::Missing)?;
let date = Self::try_from(date_record)?;
Ok(date)
}
}

impl FromStr for Date<Iso> {
type Err = FromIxdtfError;
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, FromIxdtfError> {
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, FromIxdtfError> {
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 = FromIxdtfError;
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, FromIxdtfError> {
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, FromIxdtfError> {
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, FromIxdtfError> {
let time_record = ixdtf_record.time.ok_or(FromIxdtfError::Missing)?;
let date = Self::try_from(time_record)?;
Ok(date)
}
}

impl FromStr for Time {
type Err = FromIxdtfError;
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, FromIxdtfError> {
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, FromIxdtfError> {
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, FromIxdtfError> {
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 = FromIxdtfError;
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, FromIxdtfError> {
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, FromIxdtfError> {
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 = FromIxdtfError;
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::FromIxdtfError;
#[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
Loading
Loading