diff --git a/Cargo.toml b/Cargo.toml index 4854a4fc7de..cec02ee56c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ members = [ "components/locale", "components/num-util", "components/pluralrules", + "components/datetime", ] diff --git a/components/datetime/Cargo.toml b/components/datetime/Cargo.toml new file mode 100644 index 00000000000..cb8586f628d --- /dev/null +++ b/components/datetime/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "icu-datetime" +description = "API for managing Unicode Language and Locale Identifiers" +version = "0.0.1" +authors = ["The ICU4X Project Developers"] +edition = "2018" +readme = "README.md" +repository = "https://github.com/unicode-org/icu4x" +license-file = "../../LICENSE" +categories = ["internationalization"] +include = [ + "src/**/*", + "Cargo.toml", + "README.md" +] + +[dependencies] + +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "datetime" +harness = false diff --git a/components/datetime/benches/datetime.rs b/components/datetime/benches/datetime.rs new file mode 100644 index 00000000000..d76a8cf5e39 --- /dev/null +++ b/components/datetime/benches/datetime.rs @@ -0,0 +1,196 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::fmt::Write; + +use icu_datetime::date::DateTime; +use icu_datetime::options::{self, DateTimeFormatOptions}; +use icu_datetime::provider::DummyDataProvider; +use icu_datetime::DateTimeFormat; + +fn datetime_benches(c: &mut Criterion) { + let datetimes = vec![ + DateTime::new(2001, 9, 8, 18, 46, 40), + DateTime::new(2017, 7, 13, 19, 40, 0), + DateTime::new(2020, 9, 13, 5, 26, 40), + DateTime::new(2021, 1, 6, 22, 13, 20), + DateTime::new(2021, 5, 2, 17, 0, 0), + DateTime::new(2021, 8, 26, 10, 46, 40), + DateTime::new(2021, 12, 20, 3, 33, 20), + DateTime::new(2022, 4, 14, 22, 20, 0), + DateTime::new(2022, 8, 8, 16, 6, 40), + DateTime::new(2033, 5, 17, 20, 33, 20), + ]; + let values = &[ + ("pl", options::style::Date::Full, options::style::Time::None), + ("pl", options::style::Date::Long, options::style::Time::None), + ( + "pl", + options::style::Date::Medium, + options::style::Time::None, + ), + ( + "pl", + options::style::Date::Short, + options::style::Time::None, + ), + ("pl", options::style::Date::None, options::style::Time::Full), + ("pl", options::style::Date::None, options::style::Time::Long), + ( + "pl", + options::style::Date::None, + options::style::Time::Medium, + ), + ( + "pl", + options::style::Date::None, + options::style::Time::Short, + ), + ("pl", options::style::Date::Full, options::style::Time::Full), + ("pl", options::style::Date::Long, options::style::Time::Long), + ( + "pl", + options::style::Date::Medium, + options::style::Time::Medium, + ), + ( + "pl", + options::style::Date::Short, + options::style::Time::Short, + ), + ]; + let components = vec![ + options::components::Bag { + year: options::components::Numeric::TwoDigit, + month: options::components::Month::Long, + day: options::components::Numeric::TwoDigit, + ..Default::default() + }, + options::components::Bag { + hour: options::components::Numeric::TwoDigit, + minute: options::components::Numeric::Numeric, + second: options::components::Numeric::Numeric, + hour_cycle: options::components::HourCycle::H23, + ..Default::default() + }, + options::components::Bag { + year: options::components::Numeric::Numeric, + month: options::components::Month::Short, + day: options::components::Numeric::Numeric, + weekday: options::components::Text::Long, + era: options::components::Text::Long, + hour: options::components::Numeric::Numeric, + minute: options::components::Numeric::TwoDigit, + second: options::components::Numeric::TwoDigit, + hour_cycle: options::components::HourCycle::H12, + time_zone_name: options::components::TimeZoneName::Long, + ..Default::default() + }, + options::components::Bag { + hour: options::components::Numeric::Numeric, + minute: options::components::Numeric::TwoDigit, + ..Default::default() + }, + options::components::Bag { + minute: options::components::Numeric::TwoDigit, + second: options::components::Numeric::TwoDigit, + ..Default::default() + }, + ]; + + let mut results = vec![]; + + for _ in 0..datetimes.len() { + results.push(String::new()); + } + + let dp = DummyDataProvider::default(); + + { + let mut group = c.benchmark_group("datetime"); + + group.bench_function("DateTimeFormat/format_to_write", |b| { + b.iter(|| { + for value in values { + let options = DateTimeFormatOptions::Style(options::style::Bag { + date: value.1, + time: value.2, + ..Default::default() + }); + let dtf = DateTimeFormat::try_new(&dp, &options); + + for (dt, result) in datetimes.iter().zip(results.iter_mut()) { + result.clear(); + let _ = dtf.format_to_write(&dt, result); + } + } + }) + }); + + group.bench_function("DateTimeFormat/format_to_string", |b| { + b.iter(|| { + for value in values { + let options = DateTimeFormatOptions::Style(options::style::Bag { + date: value.1, + time: value.2, + ..Default::default() + }); + let dtf = DateTimeFormat::try_new(&dp, &options); + + for dt in &datetimes { + let _ = dtf.format_to_string(&dt); + } + } + }) + }); + + group.bench_function("FormattedDateTime/format", |b| { + b.iter(|| { + for value in values { + let options = DateTimeFormatOptions::Style(options::style::Bag { + date: value.1, + time: value.2, + ..Default::default() + }); + let dtf = DateTimeFormat::try_new(&dp, &options); + + for (dt, result) in datetimes.iter().zip(results.iter_mut()) { + result.clear(); + let fdt = dtf.format(&dt); + write!(result, "{}", fdt).unwrap(); + } + } + }) + }); + + group.bench_function("FormattedDateTime/to_string", |b| { + b.iter(|| { + for value in values { + let options = DateTimeFormatOptions::Style(options::style::Bag { + date: value.1, + time: value.2, + ..Default::default() + }); + let dtf = DateTimeFormat::try_new(&dp, &options); + + for dt in &datetimes { + let fdt = dtf.format(&dt); + let _ = fdt.to_string(); + } + } + }) + }); + + group.bench_function("options/write_skeleton", |b| { + b.iter(|| { + for component in &components { + let mut s = String::new(); + component.write_skeleton(&mut s).unwrap(); + } + }) + }); + + group.finish(); + } +} + +criterion_group!(benches, datetime_benches,); +criterion_main!(benches); diff --git a/components/datetime/src/date.rs b/components/datetime/src/date.rs new file mode 100644 index 00000000000..ee86dee955d --- /dev/null +++ b/components/datetime/src/date.rs @@ -0,0 +1,29 @@ +#[derive(Default)] +pub struct DateTime { + pub year: usize, + pub month: usize, + pub day: usize, + pub hour: usize, + pub minute: usize, + pub second: usize, +} + +impl DateTime { + pub fn new( + year: usize, + month: usize, + day: usize, + hour: usize, + minute: usize, + second: usize, + ) -> Self { + Self { + year, + month, + day, + hour, + minute, + second, + } + } +} diff --git a/components/datetime/src/fields.rs b/components/datetime/src/fields.rs new file mode 100644 index 00000000000..b4c134a9f72 --- /dev/null +++ b/components/datetime/src/fields.rs @@ -0,0 +1,141 @@ +use std::fmt; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum FieldLength { + Short = 1, + TwoDigit = 2, + Abbreviated = 3, + Wide = 4, + Narrow = 5, +} + +#[derive(Debug, PartialEq)] +pub enum FieldSymbol { + Era, + Year(Year), + Month(Month), + Day(Day), + Hour(Hour), +} + +impl FieldSymbol { + pub fn write(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::Era => w.write_char('G'), + Self::Year(year) => year.write(w), + Self::Month(month) => month.write(w), + Self::Day(day) => day.write(w), + Self::Hour(hour) => hour.write(w), + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum FieldType { + Era, + Year, + Month, + Day, + Hour, +} + +impl FieldType { + pub fn iter() -> impl Iterator { + [Self::Era, Self::Year, Self::Month, Self::Day].iter() + } +} + +#[derive(Debug, PartialEq)] +pub enum Year { + Calendar, + WeekOf, + Extended, + Cyclic, + RelatedGregorian, +} + +impl Year { + pub fn write(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_char(match self { + Self::Calendar => 'y', + Self::WeekOf => 'Y', + Self::Extended => 'u', + Self::Cyclic => 'U', + Self::RelatedGregorian => 'r', + }) + } +} + +#[derive(Debug, PartialEq)] +pub enum Month { + Format, + StandAlone, +} + +impl Month { + pub fn write(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_char(match self { + Self::Format => 'M', + Self::StandAlone => 'L', + }) + } +} + +#[derive(Debug, PartialEq)] +pub enum Day { + DayOfMonth, + DayOfYear, + DayOfWeekInMonth, + ModifiedJulianDay, +} + +impl Day { + pub fn write(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_char(match self { + Self::DayOfMonth => 'd', + Self::DayOfYear => 'D', + Self::DayOfWeekInMonth => 'F', + Self::ModifiedJulianDay => 'g', + }) + } +} + +#[derive(Debug, PartialEq)] +pub enum Hour { + H11, + H12, + H23, + H24, + Preferred, + PreferredNoDayPeriod, + PreferredFlexibleDayPeriod, +} + +impl Hour { + pub fn write(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_char(match self { + Self::H11 => 'K', + Self::H12 => 'h', + Self::H23 => 'H', + Self::H24 => 'k', + Self::Preferred => 'j', + Self::PreferredNoDayPeriod => 'J', + Self::PreferredFlexibleDayPeriod => 'C', + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct Field { + pub symbol: FieldSymbol, + pub length: FieldLength, +} + +impl Field { + pub fn write_pattern(&self, w: &mut impl fmt::Write) -> fmt::Result { + for _ in 0..(self.length as u8) { + self.symbol.write(w)?; + } + Ok(()) + } +} diff --git a/components/datetime/src/format.rs b/components/datetime/src/format.rs new file mode 100644 index 00000000000..465072689c7 --- /dev/null +++ b/components/datetime/src/format.rs @@ -0,0 +1,46 @@ +use super::date::DateTime; +// use super::pattern::{parse_pattern, FieldType}; +use crate::fields::{FieldLength, FieldSymbol}; +use crate::pattern::PatternItem; +use std::fmt; + +pub struct FormattedDateTime<'s> { + pub(crate) pattern: &'s [PatternItem], + pub(crate) date_time: &'s DateTime, +} + +impl<'s> fmt::Display for FormattedDateTime<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write_pattern(self.pattern.iter(), &self.date_time, f) + } +} + +fn format_number( + result: &mut impl fmt::Write, + num: usize, + length: &FieldLength, +) -> Result<(), std::fmt::Error> { + match length { + FieldLength::TwoDigit => write!(result, "{:0>2}", num), + _ => write!(result, "{}", num), + } +} + +pub fn write_pattern>( + pattern: impl Iterator, + date_time: &DateTime, + w: &mut impl fmt::Write, +) -> std::fmt::Result { + for item in pattern { + match item.borrow() { + PatternItem::Field(field) => match field.symbol { + FieldSymbol::Year(..) => format_number(w, date_time.year, &field.length)?, + FieldSymbol::Month(..) => format_number(w, date_time.month, &field.length)?, + FieldSymbol::Day(..) => format_number(w, date_time.day, &field.length)?, + _ => unimplemented!(), + }, + PatternItem::Literal(l) => w.write_str(l)?, + } + } + Ok(()) +} diff --git a/components/datetime/src/lib.rs b/components/datetime/src/lib.rs new file mode 100644 index 00000000000..22f0d35b874 --- /dev/null +++ b/components/datetime/src/lib.rs @@ -0,0 +1,46 @@ +pub mod date; +pub mod fields; +mod format; +pub mod options; +pub mod pattern; +pub mod provider; + +use date::DateTime; +use format::write_pattern; +pub use format::FormattedDateTime; +use options::DateTimeFormatOptions; +use pattern::PatternItem; +use provider::DummyDataProvider; + +pub struct DateTimeFormat { + pattern: &'static [PatternItem], +} + +impl DateTimeFormat { + pub fn try_new(data_provider: &DummyDataProvider, options: &DateTimeFormatOptions) -> Self { + Self { + pattern: data_provider.get_pattern(options), + } + } + + pub fn format<'l>(&'l self, value: &'l DateTime) -> FormattedDateTime<'l> { + FormattedDateTime { + pattern: &self.pattern, + date_time: value, + } + } + + pub fn format_to_write( + &self, + value: &DateTime, + w: &mut impl std::fmt::Write, + ) -> std::fmt::Result { + write_pattern(self.pattern.iter(), value, w) + } + + pub fn format_to_string(&self, value: &DateTime) -> String { + let mut s = String::new(); + self.format_to_write(value, &mut s).unwrap(); + s + } +} diff --git a/components/datetime/src/options/components.rs b/components/datetime/src/options/components.rs new file mode 100644 index 00000000000..c70b40511fc --- /dev/null +++ b/components/datetime/src/options/components.rs @@ -0,0 +1,209 @@ +use crate::fields::{self, Field, FieldLength, FieldSymbol, FieldType}; +use std::fmt; + +#[derive(Debug)] +pub struct Bag { + pub era: Text, + pub year: Numeric, + pub month: Month, + pub day: Numeric, + pub weekday: Text, + + pub hour: Numeric, + pub minute: Numeric, + pub second: Numeric, + pub hour_cycle: HourCycle, + + pub time_zone_name: TimeZoneName, +} + +impl Default for Bag { + fn default() -> Self { + Self { + era: Text::default(), + year: Numeric::Numeric, + month: Month::Long, + day: Numeric::Numeric, + weekday: Text::default(), + + hour: Numeric::Numeric, + minute: Numeric::Numeric, + second: Numeric::Numeric, + hour_cycle: HourCycle::default(), + + time_zone_name: TimeZoneName::default(), + } + } +} + +impl Bag { + pub fn write_skeleton(&self, w: &mut impl fmt::Write) -> fmt::Result { + for field in self.skeleton() { + field.write_pattern(w)?; + } + Ok(()) + } + + fn get(&self, ft: &FieldType) -> Option { + match ft { + FieldType::Era => self.era.get_field(FieldSymbol::Era), + FieldType::Year => self + .year + .get_field(FieldSymbol::Year(fields::Year::Calendar)), + FieldType::Month => self + .month + .get_field(FieldSymbol::Month(fields::Month::Format)), + FieldType::Day => self + .day + .get_field(FieldSymbol::Day(fields::Day::DayOfMonth)), + FieldType::Hour => self + .hour + .get_field(FieldSymbol::Hour(self.hour_cycle.field())), + } + } + + pub fn skeleton(&self) -> impl Iterator + '_ { + FieldType::iter().filter_map(move |field_type| self.get(field_type)) + } +} + +impl fmt::Display for Bag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.write_skeleton(f) + } +} + +pub trait ComponentType { + fn get_length(&self) -> Option; + + fn get_field(&self, symbol: FieldSymbol) -> Option { + self.get_length().map(|length| Field { symbol, length }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HourCycle { + H24, + H23, + H12, + H11, + None, +} + +impl Default for HourCycle { + fn default() -> Self { + Self::None + } +} + +impl HourCycle { + pub fn field(&self) -> fields::Hour { + match self { + Self::H11 => fields::Hour::H11, + Self::H12 => fields::Hour::H12, + Self::H23 => fields::Hour::H23, + Self::H24 => fields::Hour::H24, + Self::None => fields::Hour::Preferred, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Numeric { + Numeric, + TwoDigit, + None, +} + +impl Default for Numeric { + fn default() -> Self { + Self::None + } +} + +impl ComponentType for Numeric { + fn get_length(&self) -> Option { + match self { + Self::Numeric => Some(FieldLength::Short), + Self::TwoDigit => Some(FieldLength::TwoDigit), + Self::None => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Text { + Long, + Short, + Narrow, + None, +} + +impl Default for Text { + fn default() -> Self { + Self::None + } +} + +impl ComponentType for Text { + fn get_length(&self) -> Option { + match self { + Self::Short => Some(FieldLength::Short), + Self::Long => Some(FieldLength::Wide), + Self::Narrow => Some(FieldLength::Narrow), + Self::None => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Month { + Numeric, + TwoDigit, + Long, + Short, + Narrow, + None, +} + +impl Default for Month { + fn default() -> Self { + Self::None + } +} + +impl ComponentType for Month { + fn get_length(&self) -> Option { + match self { + Self::Numeric => Some(FieldLength::Short), + Self::TwoDigit => Some(FieldLength::TwoDigit), + Self::Long => Some(FieldLength::Abbreviated), + Self::Short => Some(FieldLength::Wide), + Self::Narrow => Some(FieldLength::Narrow), + Self::None => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TimeZoneName { + Long, + Short, + None, +} + +impl Default for TimeZoneName { + fn default() -> Self { + Self::None + } +} + +impl ComponentType for TimeZoneName { + fn get_length(&self) -> Option { + match self { + Self::Short => Some(FieldLength::Short), + Self::Long => Some(FieldLength::Wide), + Self::None => None, + } + } +} diff --git a/components/datetime/src/options/mod.rs b/components/datetime/src/options/mod.rs new file mode 100644 index 00000000000..2f8553a3be5 --- /dev/null +++ b/components/datetime/src/options/mod.rs @@ -0,0 +1,14 @@ +pub mod components; +pub mod style; + +#[derive(Debug)] +pub enum DateTimeFormatOptions { + Style(style::Bag), + Components(components::Bag), +} + +impl Default for DateTimeFormatOptions { + fn default() -> Self { + Self::Style(style::Bag::default()) + } +} diff --git a/components/datetime/src/options/style.rs b/components/datetime/src/options/style.rs new file mode 100644 index 00000000000..2608ffec923 --- /dev/null +++ b/components/datetime/src/options/style.rs @@ -0,0 +1,48 @@ +use super::components; + +#[derive(Debug)] +pub struct Bag { + pub date: Date, + pub time: Time, + pub hour_cycle: components::HourCycle, +} + +impl Default for Bag { + fn default() -> Self { + Self { + date: Date::Long, + time: Time::Long, + hour_cycle: components::HourCycle::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Date { + Full, + Long, + Medium, + Short, + None, +} + +impl Default for Date { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Time { + Full, + Long, + Medium, + Short, + None, +} + +impl Default for Time { + fn default() -> Self { + Self::None + } +} diff --git a/components/datetime/src/pattern.rs b/components/datetime/src/pattern.rs new file mode 100644 index 00000000000..413a6d06f7e --- /dev/null +++ b/components/datetime/src/pattern.rs @@ -0,0 +1,7 @@ +use crate::fields::Field; + +#[derive(Debug, PartialEq)] +pub enum PatternItem { + Field(Field), + Literal(&'static str), +} diff --git a/components/datetime/src/provider.rs b/components/datetime/src/provider.rs new file mode 100644 index 00000000000..82a4c5b7312 --- /dev/null +++ b/components/datetime/src/provider.rs @@ -0,0 +1,35 @@ +use crate::fields::{self, Field, FieldLength, FieldSymbol}; +use crate::options::DateTimeFormatOptions; +use crate::pattern::PatternItem; + +#[derive(Default)] +pub struct DummyDataProvider {} + +impl DummyDataProvider { + pub fn get_pattern(&self, _options: &DateTimeFormatOptions) -> &'static [PatternItem] { + &[ + PatternItem::Field(Field { + symbol: FieldSymbol::Year(fields::Year::Calendar), + length: FieldLength::Wide, + }), + PatternItem::Literal("-"), + PatternItem::Field(Field { + symbol: FieldSymbol::Month(fields::Month::Format), + length: FieldLength::TwoDigit, + }), + PatternItem::Literal("-"), + PatternItem::Field(Field { + symbol: FieldSymbol::Day(fields::Day::DayOfMonth), + length: FieldLength::TwoDigit, + }), + ] + // match options { + // DateTimeFormatOptions::Style(_style) => "YYYY-mm-dd".to_string(), + // DateTimeFormatOptions::Components(components) => { + // let mut skeleton = String::new(); + // components.write_skeleton(&mut skeleton).unwrap(); + // "YYYY-mm-dd".to_string() + // } + // } + } +} diff --git a/components/datetime/tests/date.rs b/components/datetime/tests/date.rs new file mode 100644 index 00000000000..d24fbff0e88 --- /dev/null +++ b/components/datetime/tests/date.rs @@ -0,0 +1,40 @@ +use icu_datetime::date::DateTime; +use icu_datetime::options; +use icu_datetime::provider; +use icu_datetime::DateTimeFormat; +use std::fmt::Write; + +#[test] +fn it_works() { + let data_provider = provider::DummyDataProvider::default(); + + let dt = DateTime { + year: 2020, + month: 8, + day: 5, + ..Default::default() + }; + + let dtf = DateTimeFormat::try_new(&data_provider, &Default::default()); + + let num = dtf.format(&dt); + + let s = num.to_string(); + assert_eq!(s, "2020-08-05"); + + let mut s = String::new(); + write!(s, "{}", num).unwrap(); + assert_eq!(s, "2020-08-05"); + + let mut s = String::new(); + dtf.format_to_write(&dt, &mut s).unwrap(); + assert_eq!(s, "2020-08-05"); + + let s = dtf.format_to_string(&dt); + assert_eq!(s, "2020-08-05"); + + let bag = options::components::Bag { + ..Default::default() + }; + assert_eq!(bag.to_string(), "yMMMd"); +}