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..f4ecfd86f48 --- /dev/null +++ b/components/datetime/benches/datetime.rs @@ -0,0 +1,125 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::fmt::Write; + +use icu_datetime::date::DateTime; +use icu_datetime::options::{DateStyle, DateTimeFormatOptions, DateTimeFormatStyle, TimeStyle}; +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", DateStyle::Full, TimeStyle::None), + ("pl", DateStyle::Long, TimeStyle::None), + ("pl", DateStyle::Medium, TimeStyle::None), + ("pl", DateStyle::Short, TimeStyle::None), + ("pl", DateStyle::None, TimeStyle::Full), + ("pl", DateStyle::None, TimeStyle::Long), + ("pl", DateStyle::None, TimeStyle::Medium), + ("pl", DateStyle::None, TimeStyle::Short), + ("pl", DateStyle::Full, TimeStyle::Full), + ("pl", DateStyle::Long, TimeStyle::Long), + ("pl", DateStyle::Medium, TimeStyle::Medium), + ("pl", DateStyle::Short, TimeStyle::Short), + ]; + + 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(DateTimeFormatStyle { + date_style: value.1, + time_style: 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(DateTimeFormatStyle { + date_style: value.1, + time_style: 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(DateTimeFormatStyle { + date_style: value.1, + time_style: 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(DateTimeFormatStyle { + date_style: value.1, + time_style: 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.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/format.rs b/components/datetime/src/format.rs new file mode 100644 index 00000000000..a135ab9221b --- /dev/null +++ b/components/datetime/src/format.rs @@ -0,0 +1,55 @@ +use super::date::DateTime; +use super::pattern::{parse_pattern, Field, FieldType}; +use std::fmt; + +pub struct FormattedDateTime<'s> { + pub(crate) pattern: &'s str, + // fields: Vec, + pub(crate) date_time: &'s DateTime, +} + +impl<'s> fmt::Display for FormattedDateTime<'s> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let fields = parse_pattern(self.pattern.as_bytes()); + write_pattern(self.pattern, fields, &self.date_time, f) + } +} + +fn format_number( + result: &mut impl fmt::Write, + num: usize, + two_digit: bool, +) -> Result<(), std::fmt::Error> { + if two_digit { + write!(result, "{:0>2}", num) + } else { + write!(result, "{}", num) + } +} + +pub fn write_pattern>( + pattern: &str, + fields: impl Iterator, + date_time: &DateTime, + f: &mut impl fmt::Write, +) -> std::fmt::Result { + let mut ptr = 0; + + for field in fields { + let field = field.borrow(); + if field.idx.start > ptr { + f.write_str(&pattern[ptr..field.idx.start])?; + } + match field.field_type { + FieldType::Year => format_number(f, date_time.year, false)?, + FieldType::Month => format_number(f, date_time.month, true)?, + FieldType::Day => format_number(f, date_time.day, true)?, + } + ptr = field.idx.end + 1; + } + + if ptr < pattern.len() { + f.write_str(&pattern[ptr..])?; + } + Ok(()) +} diff --git a/components/datetime/src/lib.rs b/components/datetime/src/lib.rs new file mode 100644 index 00000000000..c7435e3eb5e --- /dev/null +++ b/components/datetime/src/lib.rs @@ -0,0 +1,49 @@ +pub mod date; +mod format; +pub mod options; +pub mod pattern; +pub mod provider; + +use date::DateTime; +pub use format::FormattedDateTime; +use options::DateTimeFormatOptions; +use pattern::parse_pattern; +use provider::DataProviderType; + +pub struct DateTimeFormat { + pattern: String, +} + +impl DateTimeFormat { + pub fn try_new( + data_provider: &D, + 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, + // fields: parse_pattern(self.pattern.as_bytes()).collect(), + date_time: value, + } + } + + pub fn format_to_write( + &self, + value: &DateTime, + w: &mut impl std::fmt::Write, + ) -> std::fmt::Result { + let fields = parse_pattern(self.pattern.as_bytes()); + format::write_pattern(&self.pattern, fields, &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.rs b/components/datetime/src/options.rs new file mode 100644 index 00000000000..3245d56e813 --- /dev/null +++ b/components/datetime/src/options.rs @@ -0,0 +1,213 @@ +#[derive(Debug)] +pub enum DateTimeFormatOptions { + Style(DateTimeFormatStyle), + Components(DateTimeFormatComponents), +} + +impl Default for DateTimeFormatOptions { + fn default() -> Self { + Self::Style(DateTimeFormatStyle::default()) + } +} + +#[derive(Debug)] +pub struct DateTimeFormatStyle { + pub date_style: DateStyle, + pub time_style: TimeStyle, + pub hour_cycle: HourCycle, +} + +impl Default for DateTimeFormatStyle { + fn default() -> Self { + Self { + date_style: DateStyle::Long, + time_style: TimeStyle::Long, + hour_cycle: HourCycle::default(), + } + } +} + +#[derive(Debug, Default)] +pub struct DateTimeFormatComponents { + pub year: NumericComponent, + pub month: MonthComponent, + pub day: NumericComponent, + pub weekday: TextComponent, + pub era: TextComponent, + + pub hour: NumericComponent, + pub minute: NumericComponent, + pub second: NumericComponent, + pub hour_cycle: HourCycle, + + pub time_zone_name: TimeZoneNameComponent, +} + +impl DateTimeFormatComponents { + pub fn skeleton(&self) -> String { + let mut result = String::new(); + + match self.weekday { + TextComponent::Narrow => result.push_str("EEEEE"), + TextComponent::Short => result.push_str("E"), + TextComponent::Long => result.push_str("EEEE"), + TextComponent::None => {} + } + match self.era { + TextComponent::Narrow => result.push_str("GGGGG"), + TextComponent::Short => result.push_str("G"), + TextComponent::Long => result.push_str("GGGG"), + TextComponent::None => {} + } + match self.year { + NumericComponent::Numeric => result.push_str("y"), + NumericComponent::TwoDigit => result.push_str("yy"), + NumericComponent::None => {} + } + match self.month { + MonthComponent::Numeric => result.push_str("M"), + MonthComponent::TwoDigit => result.push_str("MM"), + MonthComponent::Narrow => result.push_str("MMMMM"), + MonthComponent::Short => result.push_str("MMM"), + MonthComponent::Long => result.push_str("MMMM"), + MonthComponent::None => {} + } + match self.day { + NumericComponent::Numeric => result.push_str("d"), + NumericComponent::TwoDigit => result.push_str("dd"), + NumericComponent::None => {} + } + let hour_skeleton_char = match self.hour_cycle { + HourCycle::H24 => 'H', + HourCycle::H23 => 'H', + HourCycle::H12 => 'h', + HourCycle::H11 => 'h', + HourCycle::None => 'j', + }; + match self.hour { + NumericComponent::Numeric => result.push(hour_skeleton_char), + NumericComponent::TwoDigit => { + result.push(hour_skeleton_char); + result.push(hour_skeleton_char); + } + NumericComponent::None => {} + } + match self.minute { + NumericComponent::Numeric => result.push_str("m"), + NumericComponent::TwoDigit => result.push_str("mm"), + NumericComponent::None => {} + } + match self.second { + NumericComponent::Numeric => result.push_str("s"), + NumericComponent::TwoDigit => result.push_str("ss"), + NumericComponent::None => {} + } + match self.time_zone_name { + TimeZoneNameComponent::Short => result.push_str("z"), + TimeZoneNameComponent::Long => result.push_str("zzzz"), + TimeZoneNameComponent::None => {} + } + result + } +} + +#[derive(Debug, Clone, Copy)] +pub enum DateStyle { + Full, + Long, + Medium, + Short, + None, +} + +impl Default for DateStyle { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TimeStyle { + Full, + Long, + Medium, + Short, + None, +} + +impl Default for TimeStyle { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum HourCycle { + H24, + H23, + H12, + H11, + None, +} + +impl Default for HourCycle { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum NumericComponent { + Numeric, + TwoDigit, + None, +} + +impl Default for NumericComponent { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TextComponent { + Long, + Short, + Narrow, + None, +} + +impl Default for TextComponent { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum MonthComponent { + Numeric, + TwoDigit, + Long, + Short, + Narrow, + None, +} + +impl Default for MonthComponent { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TimeZoneNameComponent { + Long, + Short, + None, +} + +impl Default for TimeZoneNameComponent { + 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..0b79cfb2b0d --- /dev/null +++ b/components/datetime/src/pattern.rs @@ -0,0 +1,49 @@ +#[derive(Debug, PartialEq)] +pub enum FieldType { + Year, + Month, + Day, +} + +#[derive(Debug, PartialEq)] +pub struct Field { + pub field_type: FieldType, + pub idx: std::ops::Range, +} + +pub fn parse_pattern(input: &[u8]) -> impl Iterator + '_ { + let mut idx = 0; + + std::iter::from_fn(move || loop { + if let Some(b) = input.get(idx) { + match b { + b'Y' => { + idx += 4; + return Some(Field { + field_type: FieldType::Year, + idx: (idx - 4)..(idx - 1), + }); + } + b'm' => { + idx += 2; + return Some(Field { + field_type: FieldType::Month, + idx: (idx - 2)..(idx - 1), + }); + } + b'd' => { + idx += 2; + return Some(Field { + field_type: FieldType::Day, + idx: (idx - 2)..(idx - 1), + }); + } + _ => { + idx += 1; + } + } + } else { + return None; + } + }) +} diff --git a/components/datetime/src/provider.rs b/components/datetime/src/provider.rs new file mode 100644 index 00000000000..41323e67f6b --- /dev/null +++ b/components/datetime/src/provider.rs @@ -0,0 +1,20 @@ +use crate::options::DateTimeFormatOptions; + +pub trait DataProviderType { + fn get_pattern(&self, _options: &DateTimeFormatOptions) -> String; +} + +#[derive(Default)] +pub struct DummyDataProvider {} + +impl DataProviderType for DummyDataProvider { + fn get_pattern(&self, options: &DateTimeFormatOptions) -> String { + match options { + DateTimeFormatOptions::Style(_style) => "YYYY-mm-dd".to_string(), + DateTimeFormatOptions::Components(components) => { + let _skeleton = components.skeleton(); + "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..811387d8be5 --- /dev/null +++ b/components/datetime/tests/date.rs @@ -0,0 +1,54 @@ +use icu_datetime::date::DateTime; +use icu_datetime::pattern::{parse_pattern, Field, FieldType}; +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 pattern = parse_pattern(b"YYYY-mm-dd").collect::>(); + assert_eq!( + pattern, + vec![ + Field { + field_type: FieldType::Year, + idx: 0..3 + }, + Field { + field_type: FieldType::Month, + idx: 5..6 + }, + Field { + field_type: FieldType::Day, + idx: 8..9 + }, + ] + ); +}