diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..3b81be9 --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,194 @@ +use std::fmt; + +use crate::util; +use chrono::{ + format::{Fixed, Item, Numeric, Pad}, + Datelike, FixedOffset, NaiveDate, NaiveTime, Timelike, +}; +use num_integer::{div_floor, mod_floor}; + +/// Parses a [Fixed] value and formats it. +pub fn parse_fixed( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + spec: &Fixed, + locale: &str, +) -> Option { + use self::Fixed::*; + + match spec { + ShortMonthName => date.map(|d| write!(w, "{}", util::short_month(d.month0() as usize, locale))), + LongMonthName => date.map(|d| write!(w, "{}", util::long_month(d.month0() as usize, locale))), + ShortWeekdayName => date.map(|d| write!(w, "{}", util::short_weekday(d.weekday().num_days_from_monday() as usize, locale))), + LongWeekdayName => date.map(|d| write!(w, "{}", util::long_weekday(d.weekday().num_days_from_monday() as usize, locale))), + LowerAmPm => time.map(|t| write!(w, "{}", util::ampm(t.hour12().0 as usize, locale))), + UpperAmPm => time.map(|t| write!(w, "{}", util::ampm(t.hour12().0 as usize + 2, locale))), + Nanosecond => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + if nano == 0 { + Ok(()) + } else if nano % 1_000_000 == 0 { + write!(w, ".{:03}", nano / 1_000_000) + } else if nano % 1_000 == 0 { + write!(w, ".{:06}", nano / 1_000) + } else { + write!(w, ".{:09}", nano) + } + }), + Nanosecond3 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(w, ".{:03}", nano / 1_000_000) + }), + Nanosecond6 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(w, ".{:06}", nano / 1_000) + }), + Nanosecond9 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(w, ".{:09}", nano) + }), + Internal(_) => panic!("Internal is not supported"), + TimezoneName => off.map(|(name, _)| write!(w, "{}", *name)), + TimezoneOffsetColon => off.map(|&(_, off)| util::write_local_minus_utc(w, off, false, true)), + TimezoneOffsetColonZ => off.map(|&(_, off)| util::write_local_minus_utc(w, off, true, true)), + TimezoneOffsetDoubleColon => off.map(|&(_, off)| util::write_local_minus_utc(w, off, false, true)), + TimezoneOffsetTripleColon => off.map(|&(_, off)| util::write_local_minus_utc(w, off, false, true)), + TimezoneOffset => off.map(|&(_, off)| util::write_local_minus_utc(w, off, false, false)), + TimezoneOffsetZ => off.map(|&(_, off)| util::write_local_minus_utc(w, off, true, false)), + RFC2822 => + // same to `%a, %e %b %Y %H:%M:%S %z` + { + if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { + let sec = t.second() + t.nanosecond() / 1_000_000_000; + write!( + w, + "{}, {:2} {} {:04} {:02}:{:02}:{:02} ", + util::short_weekday(d.weekday().num_days_from_monday() as usize, locale), + d.day(), + util::short_month(d.month0() as usize, locale), + d.year(), + t.hour(), + t.minute(), + sec + ) + .ok()?; + Some(util::write_local_minus_utc(w, off, false, false)) + } else { + None + } + } + RFC3339 => + // same to `%Y-%m-%dT%H:%M:%S%.f%:z` + { + if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { + // reuse `Debug` impls which already print ISO 8601 format. + // this is faster in this way. + write!(w, "{:?}T{:?}", d, t).ok()?; + Some(util::write_local_minus_utc(w, off, false, true)) + } else { + None + } + } + + spec => todo!("Support for formatting fixed format {:?} is yet to be implemented!", spec), + } +} + +/// Parses a [Numeric] value and returns its width and its formattable component. +pub fn parse_numeric( + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + spec: &Numeric, +) -> (usize, Option) { + use self::Numeric::*; + + let week_from_sun = |d: &NaiveDate| (d.ordinal() as i32 - d.weekday().num_days_from_sunday() as i32 + 7) / 7; + let week_from_mon = |d: &NaiveDate| (d.ordinal() as i32 - d.weekday().num_days_from_monday() as i32 + 7) / 7; + + match spec { + Year => (4, date.map(|d| i64::from(d.year()))), + YearDiv100 => (2, date.map(|d| div_floor(i64::from(d.year()), 100))), + YearMod100 => (2, date.map(|d| mod_floor(i64::from(d.year()), 100))), + IsoYear => (4, date.map(|d| i64::from(d.iso_week().year()))), + IsoYearDiv100 => (2, date.map(|d| div_floor(i64::from(d.iso_week().year()), 100))), + IsoYearMod100 => (2, date.map(|d| mod_floor(i64::from(d.iso_week().year()), 100))), + Month => (2, date.map(|d| i64::from(d.month()))), + Day => (2, date.map(|d| i64::from(d.day()))), + WeekFromSun => (2, date.map(|d| i64::from(week_from_sun(d)))), + WeekFromMon => (2, date.map(|d| i64::from(week_from_mon(d)))), + IsoWeek => (2, date.map(|d| i64::from(d.iso_week().week()))), + NumDaysFromSun => (1, date.map(|d| i64::from(d.weekday().num_days_from_sunday()))), + WeekdayFromMon => (1, date.map(|d| i64::from(d.weekday().number_from_monday()))), + Ordinal => (3, date.map(|d| i64::from(d.ordinal()))), + Hour => (2, time.map(|t| i64::from(t.hour()))), + Hour12 => (2, time.map(|t| i64::from(t.hour12().1))), + Minute => (2, time.map(|t| i64::from(t.minute()))), + Second => (2, time.map(|t| i64::from(t.second() + t.nanosecond() / 1_000_000_000))), + Nanosecond => (9, time.map(|t| i64::from(t.nanosecond() % 1_000_000_000))), + Timestamp => ( + 1, + match (date, time, off) { + (Some(d), Some(t), None) => Some(d.and_time(*t).and_utc().timestamp()), + (Some(d), Some(t), Some(&(_, off))) => Some((d.and_time(*t) - off).and_utc().timestamp()), + (_, _, _) => None, + }, + ), + // for the future expansion + Internal(_) => (1, None), + spec => todo!("Support for formatting numeric format {:?} is yet to be implemented!", spec), + } +} + +/// This function is nearly entirely copied from chrono's format() +/// internal formats (3, 6 and 9-digits nanoseconds) have been disabled due to lack of access to chrono internals +pub fn format_l10n<'a, I>( + w: &mut std::fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + items: I, + locale: &str, +) -> std::fmt::Result +where + I: Iterator>, +{ + let locale = locale.to_lowercase().replace('_', "-"); + for item in items { + match item { + Item::Literal(s) | Item::Space(s) => write!(w, "{}", s)?, + Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => write!(w, "{}", s)?, + + Item::Numeric(spec, pad) => { + use self::Numeric::{IsoYear, Year}; + let (width, v) = parse_numeric(date, time, off, &spec); + + if let Some(v) = v { + if (spec == Year || spec == IsoYear) && !(0..10_000).contains(&v) { + // non-four-digit years require an explicit sign as per ISO 8601 + match pad { + Pad::None => write!(w, "{:+}", v)?, + Pad::Zero => write!(w, "{:+01$}", v, width + 1)?, + Pad::Space => write!(w, "{:+1$}", v, width + 1)?, + } + } else { + match pad { + Pad::None => write!(w, "{}", v)?, + Pad::Zero => write!(w, "{:01$}", v, width)?, + Pad::Space => write!(w, "{:1$}", v, width)?, + } + } + } else { + return Err(std::fmt::Error); // insufficient arguments for given format + } + } + + Item::Fixed(spec) => parse_fixed(w, date, time, off, &spec, &locale).ok_or(std::fmt::Error)??, + Item::Error => return Err(std::fmt::Error), + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 63bbf88..b922939 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,14 +58,16 @@ #[allow(unused_imports)] pub(crate) use lazy_static::lazy_static; -use std::collections::HashMap; -use std::fmt::{self, Error}; +pub(crate) mod fmt; +pub(crate) mod locales; +pub(crate) mod util; -use chrono::format::{Fixed, Item, Numeric, Pad, StrftimeItems}; -use chrono::{Datelike, FixedOffset, NaiveDate, NaiveTime, Offset, TimeZone, Timelike}; -use num_integer::{div_floor, mod_floor}; +pub use crate::fmt::format_l10n; -mod locales; +use chrono::{ + format::{Item, StrftimeItems}, + FixedOffset, NaiveDate, NaiveTime, Offset, TimeZone, +}; pub trait LocaleDate { fn formatl<'a>(&self, fmt: &'a str, locale: &str) -> DelayedFormatL10n>; @@ -132,8 +134,8 @@ impl<'a, I: Iterator> + Clone> DelayedFormatL10n { } } -impl<'a, I: Iterator> + Clone> fmt::Display for DelayedFormatL10n { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl<'a, I: Iterator> + Clone> std::fmt::Display for DelayedFormatL10n { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { format_l10n( f, self.date.as_ref(), @@ -144,238 +146,3 @@ impl<'a, I: Iterator> + Clone> fmt::Display for DelayedFormatL10 ) } } - -/// Prints an offset from UTC in the format of `+HHMM` or `+HH:MM`. -/// `Z` instead of `+00[:]00` is allowed when `allow_zulu` is true. -fn write_local_minus_utc(w: &mut fmt::Formatter, off: FixedOffset, allow_zulu: bool, use_colon: bool) -> fmt::Result { - let off = off.local_minus_utc(); - if !allow_zulu || off != 0 { - let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; - if use_colon { - write!(w, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) - } else { - write!(w, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) - } - } else { - write!(w, "Z") - } -} - -/// Parses a [Fixed] value and formats it. -fn parse_fixed( - w: &mut fmt::Formatter, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - spec: &Fixed, - locale: &str, -) -> Option> { - use self::Fixed::*; - - match spec { - ShortMonthName => date.map(|d| write!(w, "{}", short_month(d.month0() as usize, locale))), - LongMonthName => date.map(|d| write!(w, "{}", long_month(d.month0() as usize, locale))), - ShortWeekdayName => date.map(|d| write!(w, "{}", short_weekday(d.weekday().num_days_from_monday() as usize, locale))), - LongWeekdayName => date.map(|d| write!(w, "{}", long_weekday(d.weekday().num_days_from_monday() as usize, locale))), - LowerAmPm => time.map(|t| write!(w, "{}", ampm(t.hour12().0 as usize, locale))), - UpperAmPm => time.map(|t| write!(w, "{}", ampm(t.hour12().0 as usize + 2, locale))), - Nanosecond => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - if nano == 0 { - Ok(()) - } else if nano % 1_000_000 == 0 { - write!(w, ".{:03}", nano / 1_000_000) - } else if nano % 1_000 == 0 { - write!(w, ".{:06}", nano / 1_000) - } else { - write!(w, ".{:09}", nano) - } - }), - Nanosecond3 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(w, ".{:03}", nano / 1_000_000) - }), - Nanosecond6 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(w, ".{:06}", nano / 1_000) - }), - Nanosecond9 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(w, ".{:09}", nano) - }), - Internal(_) => panic!("Internal is not supported"), - TimezoneName => off.map(|(name, _)| write!(w, "{}", *name)), - TimezoneOffsetColon => off.map(|&(_, off)| write_local_minus_utc(w, off, false, true)), - TimezoneOffsetColonZ => off.map(|&(_, off)| write_local_minus_utc(w, off, true, true)), - TimezoneOffsetDoubleColon => off.map(|&(_, off)| write_local_minus_utc(w, off, false, true)), - TimezoneOffsetTripleColon => off.map(|&(_, off)| write_local_minus_utc(w, off, false, true)), - TimezoneOffset => off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)), - TimezoneOffsetZ => off.map(|&(_, off)| write_local_minus_utc(w, off, true, false)), - RFC2822 => - // same to `%a, %e %b %Y %H:%M:%S %z` - { - if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { - let sec = t.second() + t.nanosecond() / 1_000_000_000; - write!( - w, - "{}, {:2} {} {:04} {:02}:{:02}:{:02} ", - short_weekday(d.weekday().num_days_from_monday() as usize, locale), - d.day(), - short_month(d.month0() as usize, locale), - d.year(), - t.hour(), - t.minute(), - sec - ) - .ok()?; - Some(write_local_minus_utc(w, off, false, false)) - } else { - None - } - } - RFC3339 => - // same to `%Y-%m-%dT%H:%M:%S%.f%:z` - { - if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { - // reuse `Debug` impls which already print ISO 8601 format. - // this is faster in this way. - write!(w, "{:?}T{:?}", d, t).ok()?; - Some(write_local_minus_utc(w, off, false, true)) - } else { - None - } - } - - spec => todo!("Support for formatting fixed format {:?} is yet to be implemented!", spec), - } -} - -/// Parses a [Numeric] value and returns its width and its formattable component. -fn parse_numeric(date: Option<&NaiveDate>, time: Option<&NaiveTime>, off: Option<&(String, FixedOffset)>, spec: &Numeric) -> (usize, Option) { - use self::Numeric::*; - - let week_from_sun = |d: &NaiveDate| (d.ordinal() as i32 - d.weekday().num_days_from_sunday() as i32 + 7) / 7; - let week_from_mon = |d: &NaiveDate| (d.ordinal() as i32 - d.weekday().num_days_from_monday() as i32 + 7) / 7; - - match spec { - Year => (4, date.map(|d| i64::from(d.year()))), - YearDiv100 => (2, date.map(|d| div_floor(i64::from(d.year()), 100))), - YearMod100 => (2, date.map(|d| mod_floor(i64::from(d.year()), 100))), - IsoYear => (4, date.map(|d| i64::from(d.iso_week().year()))), - IsoYearDiv100 => (2, date.map(|d| div_floor(i64::from(d.iso_week().year()), 100))), - IsoYearMod100 => (2, date.map(|d| mod_floor(i64::from(d.iso_week().year()), 100))), - Month => (2, date.map(|d| i64::from(d.month()))), - Day => (2, date.map(|d| i64::from(d.day()))), - WeekFromSun => (2, date.map(|d| i64::from(week_from_sun(d)))), - WeekFromMon => (2, date.map(|d| i64::from(week_from_mon(d)))), - IsoWeek => (2, date.map(|d| i64::from(d.iso_week().week()))), - NumDaysFromSun => (1, date.map(|d| i64::from(d.weekday().num_days_from_sunday()))), - WeekdayFromMon => (1, date.map(|d| i64::from(d.weekday().number_from_monday()))), - Ordinal => (3, date.map(|d| i64::from(d.ordinal()))), - Hour => (2, time.map(|t| i64::from(t.hour()))), - Hour12 => (2, time.map(|t| i64::from(t.hour12().1))), - Minute => (2, time.map(|t| i64::from(t.minute()))), - Second => (2, time.map(|t| i64::from(t.second() + t.nanosecond() / 1_000_000_000))), - Nanosecond => (9, time.map(|t| i64::from(t.nanosecond() % 1_000_000_000))), - Timestamp => ( - 1, - match (date, time, off) { - (Some(d), Some(t), None) => Some(d.and_time(*t).and_utc().timestamp()), - (Some(d), Some(t), Some(&(_, off))) => Some((d.and_time(*t) - off).and_utc().timestamp()), - (_, _, _) => None, - }, - ), - // for the future expansion - Internal(_) => (1, None), - spec => todo!("Support for formatting numeric format {:?} is yet to be implemented!", spec), - } -} - -/// This function is nearly entirely copied from chrono's format() -/// internal formats (3, 6 and 9-digits nanoseconds) have been disabled due to lack of access to chrono internals -pub fn format_l10n<'a, I>( - w: &mut fmt::Formatter, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - items: I, - locale: &str, -) -> fmt::Result -where - I: Iterator>, -{ - let locale = locale.to_lowercase().replace('_', "-"); - for item in items { - match item { - Item::Literal(s) | Item::Space(s) => write!(w, "{}", s)?, - Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => write!(w, "{}", s)?, - - Item::Numeric(spec, pad) => { - use self::Numeric::{IsoYear, Year}; - let (width, v) = parse_numeric(date, time, off, &spec); - - if let Some(v) = v { - if (spec == Year || spec == IsoYear) && !(0..10_000).contains(&v) { - // non-four-digit years require an explicit sign as per ISO 8601 - match pad { - Pad::None => write!(w, "{:+}", v)?, - Pad::Zero => write!(w, "{:+01$}", v, width + 1)?, - Pad::Space => write!(w, "{:+1$}", v, width + 1)?, - } - } else { - match pad { - Pad::None => write!(w, "{}", v)?, - Pad::Zero => write!(w, "{:01$}", v, width)?, - Pad::Space => write!(w, "{:1$}", v, width)?, - } - } - } else { - return Err(fmt::Error); // insufficient arguments for given format - } - } - - Item::Fixed(spec) => parse_fixed(w, date, time, off, &spec, &locale).ok_or(fmt::Error)??, - Item::Error => return Err(fmt::Error), - } - } - - Ok(()) -} - -fn short_month(key: usize, locale: &str) -> &'static str { - find_key(key, &locales::LOCALES.short_months, locale).expect("Internal error: missing short months in the C locale") -} - -fn long_month(key: usize, locale: &str) -> &'static str { - find_key(key, &locales::LOCALES.long_months, locale).expect("Internal error: missing long months in the C locale") -} - -fn short_weekday(key: usize, locale: &str) -> &'static str { - find_key(key, &locales::LOCALES.short_weekdays, locale).expect("Internal error: missing short weekdays in the C locale") -} - -fn long_weekday(key: usize, locale: &str) -> &'static str { - find_key(key, &locales::LOCALES.long_weekdays, locale).expect("Internal error: missing long weekdays in the C locale") -} - -fn ampm(key: usize, locale: &str) -> &'static str { - find_key(key, &locales::LOCALES.ampm, locale).expect("Internal error: missing AM/PM in the C locale") -} - -fn find_key(key: usize, data: &'static HashMap>, locale: &str) -> Option<&'static &'static str> { - data.get(locale) - .and_then(|res| res.get(key)) - .or_else(|| { - if locale.contains('-') { - locale - .split('-') - .collect::>() - .first() - .cloned() - .and_then(|locale| data.get(locale).and_then(|res| res.get(key))) - } else { - None - } - }) - .or_else(|| data.get("C").and_then(|res| res.get(key))) -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..4803b71 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,59 @@ +use std::{collections::HashMap, fmt}; + +use crate::locales::LOCALES; + +use chrono::FixedOffset; + +/// Prints an offset from UTC in the format of `+HHMM` or `+HH:MM`. +/// `Z` instead of `+00[:]00` is allowed when `allow_zulu` is true. +pub fn write_local_minus_utc(w: &mut fmt::Formatter, off: FixedOffset, allow_zulu: bool, use_colon: bool) -> fmt::Result { + let off = off.local_minus_utc(); + if !allow_zulu || off != 0 { + let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; + if use_colon { + write!(w, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) + } else { + write!(w, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) + } + } else { + write!(w, "Z") + } +} + +pub fn short_month(key: usize, locale: &str) -> &'static str { + find_key(key, &LOCALES.short_months, locale).expect("Internal error: missing short months in the C locale") +} + +pub fn long_month(key: usize, locale: &str) -> &'static str { + find_key(key, &LOCALES.long_months, locale).expect("Internal error: missing long months in the C locale") +} + +pub fn short_weekday(key: usize, locale: &str) -> &'static str { + find_key(key, &LOCALES.short_weekdays, locale).expect("Internal error: missing short weekdays in the C locale") +} + +pub fn long_weekday(key: usize, locale: &str) -> &'static str { + find_key(key, &LOCALES.long_weekdays, locale).expect("Internal error: missing long weekdays in the C locale") +} + +pub fn ampm(key: usize, locale: &str) -> &'static str { + find_key(key, &LOCALES.ampm, locale).expect("Internal error: missing AM/PM in the C locale") +} + +pub fn find_key(key: usize, data: &'static HashMap>, locale: &str) -> Option<&'static &'static str> { + data.get(locale) + .and_then(|res| res.get(key)) + .or_else(|| { + if locale.contains('-') { + locale + .split('-') + .collect::>() + .first() + .cloned() + .and_then(|locale| data.get(locale).and_then(|res| res.get(key))) + } else { + None + } + }) + .or_else(|| data.get("C").and_then(|res| res.get(key))) +}