use crate::error::DateTimeError as Error;
use crate::fields::{self, Field, FieldLength, FieldSymbol, Second, Week, Year};
use crate::input::{
DateTimeInput, DateTimeInputWithWeekConfig, ExtractedDateTimeInput, LocalizedDateTimeInput,
};
use crate::pattern::{
runtime::{Pattern, PatternPlurals},
PatternItem,
};
use crate::provider;
use crate::provider::calendar::patterns::PatternPluralsFromPatternsV1Marker;
use crate::provider::date_time::{DateSymbols, TimeSymbols};
use core::fmt;
use fixed_decimal::FixedDecimal;
use icu_calendar::week::WeekCalculator;
use icu_calendar::AnyCalendarKind;
use icu_decimal::FixedDecimalFormatter;
use icu_plurals::PluralRules;
use icu_provider::DataPayload;
use writeable::Writeable;
#[derive(Debug)]
pub struct FormattedDateTime<'l> {
pub(crate) patterns: &'l DataPayload<PatternPluralsFromPatternsV1Marker>,
pub(crate) date_symbols: Option<&'l provider::calendar::DateSymbolsV1<'l>>,
pub(crate) time_symbols: Option<&'l provider::calendar::TimeSymbolsV1<'l>>,
pub(crate) datetime: ExtractedDateTimeInput,
pub(crate) week_data: Option<&'l WeekCalculator>,
pub(crate) ordinal_rules: Option<&'l PluralRules>,
pub(crate) fixed_decimal_format: &'l FixedDecimalFormatter,
}
impl<'l> Writeable for FormattedDateTime<'l> {
fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
write_pattern_plurals(
&self.patterns.get().0,
self.date_symbols,
self.time_symbols,
&self.datetime,
self.week_data,
self.ordinal_rules,
self.fixed_decimal_format,
sink,
)
.map_err(|_| core::fmt::Error)
}
}
impl<'l> fmt::Display for FormattedDateTime<'l> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_to(f)
}
}
fn format_number<W>(
result: &mut W,
fixed_decimal_format: &FixedDecimalFormatter,
mut num: FixedDecimal,
length: FieldLength,
) -> fmt::Result
where
W: fmt::Write + ?Sized,
{
match length {
FieldLength::One | FieldLength::NumericOverride(_) => {}
FieldLength::TwoDigit => {
num.pad_start(2);
num.set_max_position(2);
}
FieldLength::Abbreviated => {
num.pad_start(3);
}
FieldLength::Wide => {
num.pad_start(4);
}
FieldLength::Narrow => {
num.pad_start(5);
}
FieldLength::Six => {
num.pad_start(6);
}
FieldLength::Fixed(p) => {
num.pad_start(p as i16);
num.set_max_position(p as i16);
}
}
let formatted = fixed_decimal_format.format(&num);
formatted.write_to(result)
}
fn write_pattern<T, W>(
pattern: &crate::pattern::runtime::Pattern,
date_symbols: Option<&provider::calendar::DateSymbolsV1>,
time_symbols: Option<&provider::calendar::TimeSymbolsV1>,
loc_datetime: &impl LocalizedDateTimeInput<T>,
fixed_decimal_format: &FixedDecimalFormatter,
w: &mut W,
) -> Result<(), Error>
where
T: DateTimeInput,
W: fmt::Write + ?Sized,
{
let mut iter = pattern.items.iter().peekable();
loop {
match iter.next() {
Some(PatternItem::Field(field)) => write_field(
pattern,
field,
iter.peek(),
date_symbols,
time_symbols,
loc_datetime,
fixed_decimal_format,
w,
)?,
Some(PatternItem::Literal(ch)) => w.write_char(ch)?,
None => break,
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn write_pattern_plurals<T, W>(
patterns: &PatternPlurals,
date_symbols: Option<&provider::calendar::DateSymbolsV1>,
time_symbols: Option<&provider::calendar::TimeSymbolsV1>,
datetime: &T,
week_data: Option<&WeekCalculator>,
ordinal_rules: Option<&PluralRules>,
fixed_decimal_format: &FixedDecimalFormatter,
w: &mut W,
) -> Result<(), Error>
where
T: DateTimeInput,
W: fmt::Write + ?Sized,
{
let loc_datetime = DateTimeInputWithWeekConfig::new(datetime, week_data);
let pattern = patterns.select(&loc_datetime, ordinal_rules)?;
write_pattern(
pattern,
date_symbols,
time_symbols,
&loc_datetime,
fixed_decimal_format,
w,
)
}
const CHINESE_CYCLIC_YEARS: [&str; 60] = [
"甲子", "乙丑", "丙寅", "丁卯", "戊辰", "己巳", "庚午", "辛未", "壬申", "癸酉", "甲戌", "乙亥",
"丙子", "丁丑", "戊寅", "己卯", "庚辰", "辛巳", "壬午", "癸未", "甲申", "乙酉", "丙戌", "丁亥",
"戊子", "己丑", "庚寅", "辛卯", "壬辰", "癸巳", "甲午", "乙未", "丙申", "丁酉", "戊戌", "己亥",
"庚子", "辛丑", "壬寅", "癸卯", "甲辰", "乙巳", "丙午", "丁未", "戊申", "己酉", "庚戌", "辛亥",
"壬子", "癸丑", "甲寅", "乙卯", "丙辰", "丁巳", "戊午", "己未", "庚申", "辛酉", "壬戌", "癸亥",
];
const DANGI_CYCLIC_YEARS: [&str; 60] = [
"갑자", "을축", "병인", "정묘", "무진", "기사", "경오", "신미", "임신", "계유", "갑술", "을해",
"병자", "정축", "무인", "기묘", "경진", "신사", "임오", "계미", "갑신", "을유", "병술", "정해",
"무자", "기축", "경인", "신묘", "임진", "계사", "갑오", "을미", "병신", "정유", "무술", "기해",
"경자", "신축", "임인", "계묘", "갑진", "을사", "병오", "정미", "무신", "기유", "경술", "신해",
"임자", "계축", "갑인", "을묘", "병진", "정사", "무오", "기미", "경신", "신유", "임술", "계해",
];
const CHINESE_LEAP_PREFIX: &str = "閏";
const DANGI_LEAP_PREFIX: &str = "윤";
const PLACEHOLDER_LEAP_PREFIX: &str = "(leap)";
#[allow(clippy::too_many_arguments)]
pub(super) fn write_field<T, W>(
pattern: &crate::pattern::runtime::Pattern,
field: fields::Field,
next_item: Option<&PatternItem>,
date_symbols: Option<&crate::provider::calendar::DateSymbolsV1>,
time_symbols: Option<&crate::provider::calendar::TimeSymbolsV1>,
datetime: &impl LocalizedDateTimeInput<T>,
fixed_decimal_format: &FixedDecimalFormatter,
w: &mut W,
) -> Result<(), Error>
where
T: DateTimeInput,
W: fmt::Write + ?Sized,
{
match field.symbol {
FieldSymbol::Era => {
let era = datetime
.datetime()
.year()
.ok_or(Error::MissingInputField(Some("year")))?
.era;
let symbol = date_symbols
.ok_or(Error::MissingDateSymbols)?
.get_symbol_for_era(field.length, &era);
w.write_str(symbol)?
}
FieldSymbol::Year(year) => match year {
Year::Calendar => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(
datetime
.datetime()
.year()
.ok_or(Error::MissingInputField(Some("year")))?
.number,
),
field.length,
)?,
Year::WeekOf => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(datetime.week_of_year()?.0.number),
field.length,
)?,
Year::Cyclic => {
let datetime = datetime.datetime();
let cyclic = datetime
.year()
.ok_or(Error::MissingInputField(Some("year")))?
.cyclic
.ok_or(Error::MissingInputField(Some("cyclic")))?;
let cyclics = match datetime.any_calendar_kind() {
Some(AnyCalendarKind::Dangi) => &DANGI_CYCLIC_YEARS,
_ => &CHINESE_CYCLIC_YEARS, };
let cyclic_str = cyclics.get(usize::from(cyclic.get()) - 1).ok_or(
icu_calendar::CalendarError::Overflow {
field: "cyclic",
max: 60,
},
)?;
w.write_str(cyclic_str)?;
}
Year::RelatedIso => {
format_number(
w,
fixed_decimal_format,
FixedDecimal::from(
datetime
.datetime()
.year()
.ok_or(Error::MissingInputField(Some("year")))?
.related_iso
.ok_or(Error::MissingInputField(Some("related_iso")))?,
),
field.length,
)?;
}
},
FieldSymbol::Month(month) => match field.length {
FieldLength::One | FieldLength::TwoDigit => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(
datetime
.datetime()
.month()
.ok_or(Error::MissingInputField(Some("month")))?
.ordinal,
),
field.length,
)?,
length => {
let datetime = datetime.datetime();
let code = datetime
.month()
.ok_or(Error::MissingInputField(Some("month")))?
.code;
let (symbol, is_leap) = date_symbols
.ok_or(Error::MissingDateSymbols)?
.get_symbol_for_month(month, length, code)?;
if is_leap {
let leap_str = match datetime.any_calendar_kind() {
Some(AnyCalendarKind::Chinese) => CHINESE_LEAP_PREFIX,
Some(AnyCalendarKind::Dangi) => DANGI_LEAP_PREFIX,
_ => PLACEHOLDER_LEAP_PREFIX,
};
w.write_str(leap_str)?;
}
w.write_str(symbol)?;
}
},
FieldSymbol::Week(week) => match week {
Week::WeekOfYear => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(datetime.week_of_year()?.1 .0),
field.length,
)?,
Week::WeekOfMonth => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(datetime.week_of_month()?.0),
field.length,
)?,
},
FieldSymbol::Weekday(weekday) => {
let dow = datetime
.datetime()
.iso_weekday()
.ok_or(Error::MissingInputField(Some("iso_weekday")))?;
let symbol = date_symbols
.ok_or(Error::MissingDateSymbols)?
.get_symbol_for_weekday(weekday, field.length, dow)?;
w.write_str(symbol)?
}
symbol @ FieldSymbol::Day(day) => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(match day {
fields::Day::DayOfMonth => {
datetime
.datetime()
.day_of_month()
.ok_or(Error::MissingInputField(Some("day_of_month")))?
.0
}
fields::Day::DayOfWeekInMonth => datetime.day_of_week_in_month()?.0,
_ => return Err(Error::UnsupportedField(symbol)),
}),
field.length,
)?,
FieldSymbol::Hour(hour) => {
let h = usize::from(
datetime
.datetime()
.hour()
.ok_or(Error::MissingInputField(Some("hour")))?,
) as isize;
let value = match hour {
fields::Hour::H11 => h % 12,
fields::Hour::H12 => {
let v = h % 12;
if v == 0 {
12
} else {
v
}
}
fields::Hour::H23 => h,
fields::Hour::H24 => {
if h == 0 {
24
} else {
h
}
}
};
format_number(
w,
fixed_decimal_format,
FixedDecimal::from(value),
field.length,
)?
}
FieldSymbol::Minute => format_number(
w,
fixed_decimal_format,
FixedDecimal::from(usize::from(
datetime
.datetime()
.minute()
.ok_or(Error::MissingInputField(Some("minute")))?,
)),
field.length,
)?,
FieldSymbol::Second(Second::Second) => {
let mut seconds = FixedDecimal::from(usize::from(
datetime
.datetime()
.second()
.ok_or(Error::MissingInputField(Some("second")))?,
));
if let Some(PatternItem::Field(next_field)) = next_item {
if let FieldSymbol::Second(Second::FractionalSecond) = next_field.symbol {
let mut fraction = FixedDecimal::from(usize::from(
datetime
.datetime()
.nanosecond()
.ok_or(Error::MissingInputField(Some("nanosecond")))?,
));
let precision = match next_field.length {
FieldLength::Fixed(p) => p,
_ => {
return Err(Error::Pattern(
crate::pattern::PatternError::FieldLengthInvalid(
FieldSymbol::Second(Second::FractionalSecond),
),
));
}
};
fraction.multiply_pow10(-9);
seconds
.concatenate_end(fraction)
.map_err(|_| Error::FixedDecimal)?;
seconds.pad_end(-(precision as i16));
}
}
format_number(w, fixed_decimal_format, seconds, field.length)?
}
FieldSymbol::Second(Second::FractionalSecond) => {
}
field @ FieldSymbol::Second(Second::Millisecond) => {
return Err(Error::UnsupportedField(field))
}
FieldSymbol::DayPeriod(period) => {
let symbol = time_symbols
.ok_or(Error::MissingTimeSymbols)?
.get_symbol_for_day_period(
period,
field.length,
datetime
.datetime()
.hour()
.ok_or(Error::MissingInputField(Some("hour")))?,
pattern.time_granularity.is_top_of_hour(
datetime.datetime().minute().map(u8::from).unwrap_or(0),
datetime.datetime().second().map(u8::from).unwrap_or(0),
datetime.datetime().nanosecond().map(u32::from).unwrap_or(0),
),
)?;
w.write_str(symbol)?
}
field @ FieldSymbol::TimeZone(_) => return Err(Error::UnsupportedField(field)),
};
Ok(())
}
#[derive(Default)]
pub struct RequiredData {
pub date_symbols_data: bool,
pub time_symbols_data: bool,
pub week_data: bool,
}
impl RequiredData {
fn add_requirements_from_pattern(
&mut self,
pattern: &Pattern,
supports_time_zones: bool,
) -> Result<bool, Field> {
let fields = pattern.items.iter().filter_map(|p| match p {
PatternItem::Field(field) => Some(field),
_ => None,
});
for field in fields {
if !self.date_symbols_data {
self.date_symbols_data = match field.symbol {
FieldSymbol::Era => true,
FieldSymbol::Month(_) => {
!matches!(field.length, FieldLength::One | FieldLength::TwoDigit)
}
FieldSymbol::Weekday(_) => true,
_ => false,
}
}
if !self.time_symbols_data {
self.time_symbols_data = matches!(field.symbol, FieldSymbol::DayPeriod(_));
}
if !self.week_data {
self.week_data = matches!(
field.symbol,
FieldSymbol::Year(Year::WeekOf) | FieldSymbol::Week(_)
)
}
if supports_time_zones {
if self.date_symbols_data && self.time_symbols_data && self.week_data {
return Ok(true);
}
} else if matches!(field.symbol, FieldSymbol::TimeZone(_)) {
return Err(field);
}
}
Ok(false)
}
}
pub fn analyze_patterns(
patterns: &PatternPlurals,
supports_time_zones: bool,
) -> Result<RequiredData, Field> {
let mut required = RequiredData::default();
for pattern in patterns.patterns_iter() {
if required.add_requirements_from_pattern(pattern, supports_time_zones)? {
break;
}
}
Ok(required)
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
use icu_decimal::options::{FixedDecimalFormatterOptions, GroupingStrategy};
use icu_locid::Locale;
#[test]
fn test_mixed_calendar_eras() {
use icu::calendar::japanese::JapaneseExtended;
use icu::calendar::Date;
use icu::datetime::options::length;
use icu::datetime::DateFormatter;
let locale: Locale = "en-u-ca-japanese".parse().unwrap();
let dtf = DateFormatter::try_new_with_length(&locale.into(), length::Date::Medium)
.expect("DateTimeFormat construction succeeds");
let date = Date::try_new_gregorian_date(1800, 9, 1).expect("Failed to construct Date.");
let date = date
.to_calendar(JapaneseExtended::new())
.into_japanese_date()
.to_any();
writeable::assert_writeable_eq!(dtf.format(&date).unwrap(), "Sep 1, 12 kansei-1789")
}
#[test]
#[cfg(feature = "serde")]
fn test_basic() {
use crate::provider::calendar::{GregorianDateSymbolsV1Marker, TimeSymbolsV1Marker};
use icu_calendar::DateTime;
use icu_provider::prelude::*;
let locale = "en-u-ca-gregory".parse::<Locale>().unwrap().into();
let req = DataRequest {
locale: &locale,
metadata: Default::default(),
};
let date_data: DataPayload<GregorianDateSymbolsV1Marker> = crate::provider::Baked
.load(req)
.unwrap()
.take_payload()
.unwrap();
let time_data: DataPayload<TimeSymbolsV1Marker> = crate::provider::Baked
.load(req)
.unwrap()
.take_payload()
.unwrap();
let pattern = "MMM".parse().unwrap();
let datetime = DateTime::try_new_gregorian_datetime(2020, 8, 1, 12, 34, 28).unwrap();
let fixed_decimal_format =
FixedDecimalFormatter::try_new(&locale, Default::default()).unwrap();
let mut sink = String::new();
let loc_datetime = DateTimeInputWithWeekConfig::new(&datetime, None);
write_pattern(
&pattern,
Some(date_data.get()),
Some(time_data.get()),
&loc_datetime,
&fixed_decimal_format,
&mut sink,
)
.unwrap();
println!("{sink}");
}
#[test]
fn test_format_number() {
let values = &[2, 20, 201, 2017, 20173];
let samples = &[
(FieldLength::One, ["2", "20", "201", "2017", "20173"]),
(FieldLength::TwoDigit, ["02", "20", "01", "17", "73"]),
(
FieldLength::Abbreviated,
["002", "020", "201", "2017", "20173"],
),
(FieldLength::Wide, ["0002", "0020", "0201", "2017", "20173"]),
];
let mut fixed_decimal_format_options = FixedDecimalFormatterOptions::default();
fixed_decimal_format_options.grouping_strategy = GroupingStrategy::Never;
let fixed_decimal_format = FixedDecimalFormatter::try_new(
&icu_locid::locale!("en").into(),
fixed_decimal_format_options,
)
.unwrap();
for (length, expected) in samples {
for (value, expected) in values.iter().zip(expected) {
let mut s = String::new();
format_number(
&mut s,
&fixed_decimal_format,
FixedDecimal::from(*value),
*length,
)
.unwrap();
assert_eq!(s, *expected);
}
}
}
}