diff --git a/AUTHORS.txt b/AUTHORS.txt index 9e738193f8..83395d0d6f 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -8,6 +8,7 @@ Ben Boeckel Ben Eills Brandon W Maister Brandon W Maister +Cecile Tonglet Colin Ray Corey Farwell Dan diff --git a/CHANGELOG.md b/CHANGELOG.md index 360703c62e..5930185d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versions with only mechanical changes will be omitted from the following list. * Add day and week iterators for `NaiveDate` (@gnzlbg & @robyoung) * Add a `Month` enum (@hhamana) +* Add `locales`. All format functions can now use locales. ### Improvements diff --git a/Cargo.toml b/Cargo.toml index 1f6d2b83bf..dd9dbeea97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ alloc = [] std = [] clock = ["time", "std"] wasmbind = ["wasm-bindgen", "js-sys"] +unstable-locales = ["pure-rust-locales", "alloc"] __internal_bench = [] __doctest = [] @@ -38,6 +39,7 @@ num-integer = { version = "0.1.36", default-features = false } num-traits = { version = "0.2", default-features = false } rustc-serialize = { version = "0.3.20", optional = true } serde = { version = "1.0.99", default-features = false, optional = true } +pure-rust-locales = { version = "0.5.2", optional = true } [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies] wasm-bindgen = { version = "0.2", optional = true } diff --git a/README.md b/README.md index b5444f16af..2e363f98bb 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,10 @@ Default features: Optional features: - `wasmbind`: Enable integration with [wasm-bindgen][] and its `js-sys` project -- [`serde`][]: Enable +- [`serde`][]: Enable serialization/deserialization via serde. +- `unstable-locales`: Enable localization. This adds various methods with a + `_localized` suffix. The implementation and API may change or even be + removed in a patch release. Feedback welcome. [`serde`]: https://github.com/serde-rs/serde [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen @@ -225,12 +228,23 @@ Chrono also provides [`to_rfc2822`](https://docs.rs/chrono/0.4/chrono/struct.Dat [`to_rfc3339`](https://docs.rs/chrono/0.4/chrono/struct.DateTime.html#method.to_rfc3339) methods for well-known formats. +Chrono now also provides date formatting in almost any language without the +help of an additional C library. This functionality is under the feature +`unstable-locales`: + +```text +chrono { version = "0.4", features = ["unstable-locales"] +``` + +The `unstable-locales` feature requires and implies at least the `alloc` feature. + ```rust use chrono::prelude::*; let dt = Utc.ymd(2014, 11, 28).and_hms(12, 0, 9); assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09"); assert_eq!(dt.format("%a %b %e %T %Y").to_string(), "Fri Nov 28 12:00:09 2014"); +assert_eq!(dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(), "vendredi 28 novembre 2014, 12:00:09"); assert_eq!(dt.format("%a %b %e %T %Y").to_string(), dt.format("%c").to_string()); assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC"); diff --git a/ci/github.sh b/ci/github.sh index 3c1c873850..229bd6ee54 100755 --- a/ci/github.sh +++ b/ci/github.sh @@ -6,7 +6,7 @@ set -euo pipefail source "${BASH_SOURCE[0]%/*}/_shlib.sh" TEST_TZS=(ACST-9:30 EST4 UTC0 Asia/Katmandu) -FEATURES=(std serde clock "alloc serde") +FEATURES=(std serde clock "alloc serde" unstable-locales) RUST_113_FEATURES=(rustc-serialize serde) main() { @@ -56,7 +56,7 @@ test_all_tzs() { test_regular() { tz="$1" && shift - runt env TZ="$tz" cargo test --features __doctest --color=always -- --color=always + runt env TZ="$tz" cargo test --features __doctest,unstable-locales --color=always -- --color=always for feature in "${FEATURES[@]}"; do runt env TZ="$tz" cargo test --no-default-features --features "$feature" --lib --color=always -- --color=always done diff --git a/src/date.rs b/src/date.rs index 4c573fd9b4..0012d36049 100644 --- a/src/date.rs +++ b/src/date.rs @@ -10,6 +10,8 @@ use core::ops::{Add, Sub}; use core::{fmt, hash}; use oldtime::Duration as OldDuration; +#[cfg(feature = "unstable-locales")] +use format::Locale; #[cfg(any(feature = "alloc", feature = "std", test))] use format::{DelayedFormat, Item, StrftimeItems}; use naive::{self, IsoWeek, NaiveDate, NaiveTime}; @@ -295,6 +297,40 @@ where pub fn format<'a>(&self, fmt: &'a str) -> DelayedFormat> { self.format_with_items(StrftimeItems::new(fmt)) } + + /// Formats the date with the specified formatting items and locale. + #[cfg(feature = "unstable-locales")] + #[inline] + pub fn format_localized_with_items<'a, I, B>( + &self, + items: I, + locale: Locale, + ) -> DelayedFormat + where + I: Iterator + Clone, + B: Borrow>, + { + DelayedFormat::new_with_offset_and_locale( + Some(self.naive_local()), + None, + &self.offset, + items, + locale, + ) + } + + /// Formats the date with the specified format string and locale. + /// See the [`format::strftime` module](./format/strftime/index.html) + /// on the supported escape sequences. + #[cfg(feature = "unstable-locales")] + #[inline] + pub fn format_localized<'a>( + &self, + fmt: &'a str, + locale: Locale, + ) -> DelayedFormat> { + self.format_localized_with_items(StrftimeItems::new_with_locale(fmt, locale), locale) + } } impl Datelike for Date { diff --git a/src/datetime.rs b/src/datetime.rs index c7e6061c47..f4a0cd13e6 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -19,6 +19,8 @@ use std::string::ToString; use core::borrow::Borrow; #[cfg(any(feature = "alloc", feature = "std", test))] use format::DelayedFormat; +#[cfg(feature = "unstable-locales")] +use format::Locale; use format::{parse, ParseError, ParseResult, Parsed, StrftimeItems}; use format::{Fixed, Item}; use naive::{self, IsoWeek, NaiveDateTime, NaiveTime}; @@ -493,6 +495,41 @@ where pub fn format<'a>(&self, fmt: &'a str) -> DelayedFormat> { self.format_with_items(StrftimeItems::new(fmt)) } + + /// Formats the combined date and time with the specified formatting items and locale. + #[cfg(feature = "unstable-locales")] + #[inline] + pub fn format_localized_with_items<'a, I, B>( + &self, + items: I, + locale: Locale, + ) -> DelayedFormat + where + I: Iterator + Clone, + B: Borrow>, + { + let local = self.naive_local(); + DelayedFormat::new_with_offset_and_locale( + Some(local.date()), + Some(local.time()), + &self.offset, + items, + locale, + ) + } + + /// Formats the combined date and time with the specified format string and locale. + /// See the [`format::strftime` module](./format/strftime/index.html) + /// on the supported escape sequences. + #[cfg(feature = "unstable-locales")] + #[inline] + pub fn format_localized<'a>( + &self, + fmt: &'a str, + locale: Locale, + ) -> DelayedFormat> { + self.format_localized_with_items(StrftimeItems::new_with_locale(fmt, locale), locale) + } } impl Datelike for DateTime { diff --git a/src/format/locales.rs b/src/format/locales.rs new file mode 100644 index 0000000000..f7b4bbde5b --- /dev/null +++ b/src/format/locales.rs @@ -0,0 +1,33 @@ +use pure_rust_locales::{locale_match, Locale}; + +pub(crate) fn short_months(locale: Locale) -> &'static [&'static str] { + locale_match!(locale => LC_TIME::ABMON) +} + +pub(crate) fn long_months(locale: Locale) -> &'static [&'static str] { + locale_match!(locale => LC_TIME::MON) +} + +pub(crate) fn short_weekdays(locale: Locale) -> &'static [&'static str] { + locale_match!(locale => LC_TIME::ABDAY) +} + +pub(crate) fn long_weekdays(locale: Locale) -> &'static [&'static str] { + locale_match!(locale => LC_TIME::DAY) +} + +pub(crate) fn am_pm(locale: Locale) -> &'static [&'static str] { + locale_match!(locale => LC_TIME::AM_PM) +} + +pub(crate) fn d_fmt(locale: Locale) -> &'static str { + locale_match!(locale => LC_TIME::D_FMT) +} + +pub(crate) fn d_t_fmt(locale: Locale) -> &'static str { + locale_match!(locale => LC_TIME::D_T_FMT) +} + +pub(crate) fn t_fmt(locale: Locale) -> &'static str { + locale_match!(locale => LC_TIME::T_FMT) +} diff --git a/src/format/mod.rs b/src/format/mod.rs index 459c201ae2..3fbc2dae34 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -36,9 +36,19 @@ use offset::{FixedOffset, Offset}; use {Datelike, Timelike}; use {Month, ParseMonthError, ParseWeekdayError, Weekday}; +#[cfg(feature = "unstable-locales")] +pub(crate) mod locales; + pub use self::parse::parse; pub use self::parsed::Parsed; pub use self::strftime::StrftimeItems; +/// L10n locales. +#[cfg(feature = "unstable-locales")] +pub use pure_rust_locales::Locale; + +#[cfg(not(feature = "unstable-locales"))] +#[derive(Debug)] +struct Locale; /// An uninhabited type used for `InternalNumeric` and `InternalFixed` below. #[derive(Clone, PartialEq, Eq)] @@ -386,7 +396,7 @@ pub fn format_item<'a>( item: &Item<'a>, ) -> fmt::Result { let mut result = String::new(); - format_inner(&mut result, date, time, off, item)?; + format_inner(&mut result, date, time, off, item, None)?; w.pad(&result) } @@ -397,11 +407,27 @@ fn format_inner<'a>( time: Option<&NaiveTime>, off: Option<&(String, FixedOffset)>, item: &Item<'a>, + _locale: Option, ) -> fmt::Result { - // full and abbreviated month and weekday names - static SHORT_MONTHS: [&'static str; 12] = - ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - static LONG_MONTHS: [&'static str; 12] = [ + #[cfg(feature = "unstable-locales")] + let locale = _locale.unwrap_or(Locale::POSIX); + + #[cfg(feature = "unstable-locales")] + let short_months = locales::short_months(locale); + #[cfg(feature = "unstable-locales")] + let long_months = locales::long_months(locale); + #[cfg(feature = "unstable-locales")] + let short_weekdays = locales::short_weekdays(locale); + #[cfg(feature = "unstable-locales")] + let long_weekdays = locales::long_weekdays(locale); + #[cfg(feature = "unstable-locales")] + let am_pm = locales::am_pm(locale); + + #[cfg(not(feature = "unstable-locales"))] + let short_months = + &["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + #[cfg(not(feature = "unstable-locales"))] + let long_months = &[ "January", "February", "March", @@ -415,9 +441,16 @@ fn format_inner<'a>( "November", "December", ]; - static SHORT_WEEKDAYS: [&'static str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; - static LONG_WEEKDAYS: [&'static str; 7] = - ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; + #[cfg(not(feature = "unstable-locales"))] + let short_weekdays = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + #[cfg(not(feature = "unstable-locales"))] + let long_weekdays = + &["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + #[cfg(not(feature = "unstable-locales"))] + let am_pm = &["AM", "PM"]; + + let am_pm_lowercase: Vec<_> = am_pm.iter().map(|x| x.to_lowercase()).collect(); + let am_pm_lowercase = &[am_pm_lowercase[0].as_str(), am_pm_lowercase[1].as_str()]; use core::fmt::Write; use div::{div_floor, mod_floor}; @@ -520,28 +553,32 @@ fn format_inner<'a>( let ret = match *spec { ShortMonthName => date.map(|d| { - result.push_str(SHORT_MONTHS[d.month0() as usize]); + result.push_str(short_months[d.month0() as usize]); Ok(()) }), LongMonthName => date.map(|d| { - result.push_str(LONG_MONTHS[d.month0() as usize]); + result.push_str(long_months[d.month0() as usize]); Ok(()) }), ShortWeekdayName => date.map(|d| { result - .push_str(SHORT_WEEKDAYS[d.weekday().num_days_from_monday() as usize]); + .push_str(short_weekdays[d.weekday().num_days_from_sunday() as usize]); Ok(()) }), LongWeekdayName => date.map(|d| { - result.push_str(LONG_WEEKDAYS[d.weekday().num_days_from_monday() as usize]); + result.push_str(long_weekdays[d.weekday().num_days_from_sunday() as usize]); Ok(()) }), LowerAmPm => time.map(|t| { - result.push_str(if t.hour12().0 { "pm" } else { "am" }); + result.push_str(if t.hour12().0 { + am_pm_lowercase[1] + } else { + am_pm_lowercase[0] + }); Ok(()) }), UpperAmPm => time.map(|t| { - result.push_str(if t.hour12().0 { "PM" } else { "AM" }); + result.push_str(if t.hour12().0 { am_pm[1] } else { am_pm[0] }); Ok(()) }), Nanosecond => time.map(|t| { @@ -610,9 +647,9 @@ fn format_inner<'a>( write!( result, "{}, {:02} {} {:04} {:02}:{:02}:{:02} ", - SHORT_WEEKDAYS[d.weekday().num_days_from_monday() as usize], + short_weekdays[d.weekday().num_days_from_sunday() as usize], d.day(), - SHORT_MONTHS[d.month0() as usize], + short_months[d.month0() as usize], d.year(), t.hour(), t.minute(), @@ -664,7 +701,7 @@ where { let mut result = String::new(); for item in items { - format_inner(&mut result, date, time, off, item.borrow())?; + format_inner(&mut result, date, time, off, item.borrow(), None)?; } w.pad(&result) } @@ -690,13 +727,15 @@ pub struct DelayedFormat { off: Option<(String, FixedOffset)>, /// An iterator returning formatting items. items: I, + /// Locale used for text. + locale: Option, } #[cfg(any(feature = "alloc", feature = "std", test))] impl<'a, I: Iterator + Clone, B: Borrow>> DelayedFormat { /// Makes a new `DelayedFormat` value out of local date and time. pub fn new(date: Option, time: Option, items: I) -> DelayedFormat { - DelayedFormat { date: date, time: time, off: None, items: items } + DelayedFormat { date: date, time: time, off: None, items: items, locale: None } } /// Makes a new `DelayedFormat` value out of local date and time and UTC offset. @@ -710,13 +749,66 @@ impl<'a, I: Iterator + Clone, B: Borrow>> DelayedFormat { Off: Offset + fmt::Display, { let name_and_diff = (offset.to_string(), offset.fix()); - DelayedFormat { date: date, time: time, off: Some(name_and_diff), items: items } + DelayedFormat { + date: date, + time: time, + off: Some(name_and_diff), + items: items, + locale: None, + } + } + + /// Makes a new `DelayedFormat` value out of local date and time and locale. + #[cfg(feature = "unstable-locales")] + pub fn new_with_locale( + date: Option, + time: Option, + items: I, + locale: Locale, + ) -> DelayedFormat { + DelayedFormat { date: date, time: time, off: None, items: items, locale: Some(locale) } + } + + /// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale. + #[cfg(feature = "unstable-locales")] + pub fn new_with_offset_and_locale( + date: Option, + time: Option, + offset: &Off, + items: I, + locale: Locale, + ) -> DelayedFormat + where + Off: Offset + fmt::Display, + { + let name_and_diff = (offset.to_string(), offset.fix()); + DelayedFormat { + date: date, + time: time, + off: Some(name_and_diff), + items: items, + locale: Some(locale), + } } } #[cfg(any(feature = "alloc", feature = "std", test))] impl<'a, I: Iterator + Clone, B: Borrow>> fmt::Display for DelayedFormat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[cfg(feature = "unstable-locales")] + { + if let Some(locale) = self.locale { + return format_localized( + f, + self.date.as_ref(), + self.time.as_ref(), + self.off.as_ref(), + self.items.clone(), + locale, + ); + } + } + format(f, self.date.as_ref(), self.time.as_ref(), self.off.as_ref(), self.items.clone()) } } @@ -759,6 +851,43 @@ impl FromStr for Weekday { } } +/// Formats single formatting item +#[cfg(feature = "unstable-locales")] +pub fn format_item_localized<'a>( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + item: &Item<'a>, + locale: Locale, +) -> fmt::Result { + let mut result = String::new(); + format_inner(&mut result, date, time, off, item, Some(locale))?; + w.pad(&result) +} + +/// Tries to format given arguments with given formatting items. +/// Internally used by `DelayedFormat`. +#[cfg(feature = "unstable-locales")] +pub fn format_localized<'a, I, B>( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + items: I, + locale: Locale, +) -> fmt::Result +where + I: Iterator + Clone, + B: Borrow>, +{ + let mut result = String::new(); + for item in items { + format_inner(&mut result, date, time, off, item.borrow(), Some(locale))?; + } + w.pad(&result) +} + /// Parsing a `str` into a `Month` uses the format [`%W`](./format/strftime/index.html). /// /// # Example diff --git a/src/format/strftime.rs b/src/format/strftime.rs index e4918204a7..0ca2ca9204 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -38,7 +38,7 @@ The following specifiers are available both to formatting and parsing. | `%j` | `189` | Day of the year (001--366), zero-padded to 3 digits. | | | | | | `%D` | `07/08/01` | Month-day-year format. Same as `%m/%d/%y`. | -| `%x` | `07/08/01` | Same as `%D`. | +| `%x` | `07/08/01` | Locale's date representation (e.g., 12/31/99). | | `%F` | `2001-07-08` | Year-month-day format (ISO 8601). Same as `%Y-%m-%d`. | | `%v` | ` 8-Jul-2001` | Day-month-year format. Same as `%e-%b-%Y`. | | | | | @@ -64,7 +64,7 @@ The following specifiers are available both to formatting and parsing. | | | | | `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. | | `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. | -| `%X` | `00:34:60` | Same as `%T`. | +| `%X` | `00:34:60` | Locale's time representation (e.g., 23:13:48). | | `%r` | `12:34:60 AM` | Hour-minute-second format in 12-hour clocks. Same as `%I:%M:%S %p`. | | | | | | | | **TIME ZONE SPECIFIERS:** | @@ -74,7 +74,7 @@ The following specifiers are available both to formatting and parsing. | `%#z` | `+09` | *Parsing only:* Same as `%z` but allows minutes to be missing or present. | | | | | | | | **DATE & TIME SPECIFIERS:** | -|`%c`|`Sun Jul 8 00:34:60 2001`|`ctime` date & time format. Same as `%a %b %e %T %Y` sans `\n`.| +|`%c`|`Sun Jul 8 00:34:60 2001`|Locale's date and time (e.g., Thu Mar 3 23:05:25 2005). | | `%+` | `2001-07-08T00:34:60.026490+09:30` | ISO 8601 / RFC 3339 date & time format. [^6] | | | | | | `%s` | `994518299` | UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC. [^7]| @@ -159,8 +159,35 @@ Notes: */ +#[cfg(feature = "unstable-locales")] +use super::{locales, Locale}; use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad}; +#[cfg(feature = "unstable-locales")] +type Fmt<'a> = Vec>; +#[cfg(not(feature = "unstable-locales"))] +type Fmt<'a> = &'static [Item<'static>]; + +static D_FMT: &'static [Item<'static>] = + &[num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)]; +static D_T_FMT: &'static [Item<'static>] = &[ + fix!(ShortWeekdayName), + sp!(" "), + fix!(ShortMonthName), + sp!(" "), + nums!(Day), + sp!(" "), + num0!(Hour), + lit!(":"), + num0!(Minute), + lit!(":"), + num0!(Second), + sp!(" "), + num0!(Year), +]; +static T_FMT: &'static [Item<'static>] = + &[num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)]; + /// Parsing iterator for `strftime`-like format strings. #[derive(Clone, Debug)] pub struct StrftimeItems<'a> { @@ -169,14 +196,59 @@ pub struct StrftimeItems<'a> { /// If the current specifier is composed of multiple formatting items (e.g. `%+`), /// parser refers to the statically reconstructed slice of them. /// If `recons` is not empty they have to be returned earlier than the `remainder`. - recons: &'static [Item<'static>], + recons: Fmt<'a>, + /// Date format + d_fmt: Fmt<'a>, + /// Date and time format + d_t_fmt: Fmt<'a>, + /// Time format + t_fmt: Fmt<'a>, } impl<'a> StrftimeItems<'a> { /// Creates a new parsing iterator from the `strftime`-like format string. pub fn new(s: &'a str) -> StrftimeItems<'a> { - static FMT_NONE: [Item<'static>; 0] = []; - StrftimeItems { remainder: s, recons: &FMT_NONE } + Self::with_remainer(s) + } + + /// Creates a new parsing iterator from the `strftime`-like format string. + #[cfg(feature = "unstable-locales")] + pub fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> { + let d_fmt = StrftimeItems::new(locales::d_fmt(locale)).collect(); + let d_t_fmt = StrftimeItems::new(locales::d_t_fmt(locale)).collect(); + let t_fmt = StrftimeItems::new(locales::t_fmt(locale)).collect(); + + StrftimeItems { + remainder: s, + recons: Vec::new(), + d_fmt: d_fmt, + d_t_fmt: d_t_fmt, + t_fmt: t_fmt, + } + } + + #[cfg(not(feature = "unstable-locales"))] + fn with_remainer(s: &'a str) -> StrftimeItems<'a> { + static FMT_NONE: &'static [Item<'static>; 0] = &[]; + + StrftimeItems { + remainder: s, + recons: FMT_NONE, + d_fmt: D_FMT, + d_t_fmt: D_T_FMT, + t_fmt: T_FMT, + } + } + + #[cfg(feature = "unstable-locales")] + fn with_remainer(s: &'a str) -> StrftimeItems<'a> { + StrftimeItems { + remainder: s, + recons: Vec::new(), + d_fmt: D_FMT.to_vec(), + d_t_fmt: D_T_FMT.to_vec(), + t_fmt: T_FMT.to_vec(), + } } } @@ -188,8 +260,16 @@ impl<'a> Iterator for StrftimeItems<'a> { fn next(&mut self) -> Option> { // we have some reconstructed items to return if !self.recons.is_empty() { - let item = self.recons[0].clone(); - self.recons = &self.recons[1..]; + let item; + #[cfg(feature = "unstable-locales")] + { + item = self.recons.remove(0); + } + #[cfg(not(feature = "unstable-locales"))] + { + item = self.recons[0].clone(); + self.recons = &self.recons[1..]; + } return Some(item); } @@ -227,13 +307,36 @@ impl<'a> Iterator for StrftimeItems<'a> { } macro_rules! recons { - [$head:expr, $($tail:expr),+] => ({ - const RECONS: &'static [Item<'static>] = &[$($tail),+]; - self.recons = RECONS; + [$head:expr, $($tail:expr),+ $(,)*] => ({ + #[cfg(feature = "unstable-locales")] + { + self.recons.clear(); + $(self.recons.push($tail);)+ + } + #[cfg(not(feature = "unstable-locales"))] + { + const RECONS: &'static [Item<'static>] = &[$($tail),+]; + self.recons = RECONS; + } $head }) } + macro_rules! recons_from_slice { + ($slice:expr) => {{ + #[cfg(feature = "unstable-locales")] + { + self.recons.clear(); + self.recons.extend_from_slice(&$slice[1..]); + } + #[cfg(not(feature = "unstable-locales"))] + { + self.recons = &$slice[1..]; + } + $slice[0].clone() + }}; + } + let item = match spec { 'A' => fix!(LongWeekdayName), 'B' => fix!(LongMonthName), @@ -253,26 +356,12 @@ impl<'a> Iterator for StrftimeItems<'a> { 'U' => num0!(WeekFromSun), 'V' => num0!(IsoWeek), 'W' => num0!(WeekFromMon), - 'X' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)], + 'X' => recons_from_slice!(self.t_fmt), 'Y' => num0!(Year), 'Z' => fix!(TimezoneName), 'a' => fix!(ShortWeekdayName), 'b' | 'h' => fix!(ShortMonthName), - 'c' => recons![ - fix!(ShortWeekdayName), - sp!(" "), - fix!(ShortMonthName), - sp!(" "), - nums!(Day), - sp!(" "), - num0!(Hour), - lit!(":"), - num0!(Minute), - lit!(":"), - num0!(Second), - sp!(" "), - num0!(Year) - ], + 'c' => recons_from_slice!(self.d_t_fmt), 'd' => num0!(Day), 'e' => nums!(Day), 'f' => num0!(Nanosecond), @@ -299,9 +388,7 @@ impl<'a> Iterator for StrftimeItems<'a> { recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"), num0!(Year)] } 'w' => num!(NumDaysFromSun), - 'x' => { - recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)] - } + 'x' => recons_from_slice!(self.d_fmt), 'y' => num0!(YearMod100), 'z' => { if is_alternate { @@ -518,3 +605,36 @@ fn test_strftime_docs() { assert_eq!(dt.format("%n").to_string(), "\n"); assert_eq!(dt.format("%%").to_string(), "%"); } + +#[cfg(feature = "unstable-locales")] +#[test] +fn test_strftime_docs_localized() { + use {FixedOffset, TimeZone}; + + let dt = FixedOffset::east(34200).ymd(2001, 7, 8).and_hms_nano(0, 34, 59, 1_026_490_708); + + // date specifiers + assert_eq!(dt.format_localized("%b", Locale::fr_BE).to_string(), "jui"); + assert_eq!(dt.format_localized("%B", Locale::fr_BE).to_string(), "juillet"); + assert_eq!(dt.format_localized("%h", Locale::fr_BE).to_string(), "jui"); + assert_eq!(dt.format_localized("%a", Locale::fr_BE).to_string(), "dim"); + assert_eq!(dt.format_localized("%A", Locale::fr_BE).to_string(), "dimanche"); + assert_eq!(dt.format_localized("%D", Locale::fr_BE).to_string(), "07/08/01"); + assert_eq!(dt.format_localized("%x", Locale::fr_BE).to_string(), "08/07/01"); + assert_eq!(dt.format_localized("%F", Locale::fr_BE).to_string(), "2001-07-08"); + assert_eq!(dt.format_localized("%v", Locale::fr_BE).to_string(), " 8-jui-2001"); + + // time specifiers + assert_eq!(dt.format_localized("%P", Locale::fr_BE).to_string(), ""); + assert_eq!(dt.format_localized("%p", Locale::fr_BE).to_string(), ""); + assert_eq!(dt.format_localized("%R", Locale::fr_BE).to_string(), "00:34"); + assert_eq!(dt.format_localized("%T", Locale::fr_BE).to_string(), "00:34:60"); + assert_eq!(dt.format_localized("%X", Locale::fr_BE).to_string(), "00:34:60"); + assert_eq!(dt.format_localized("%r", Locale::fr_BE).to_string(), "12:34:60 "); + + // date & time specifiers + assert_eq!( + dt.format_localized("%c", Locale::fr_BE).to_string(), + "dim 08 jui 2001 00:34:60 +09:30" + ); +} diff --git a/src/lib.rs b/src/lib.rs index 7cebccfaf5..89ffd87acd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,10 @@ //! Optional features: //! //! - `wasmbind`: Enable integration with [wasm-bindgen][] and its `js-sys` project -//! - [`serde`][]: Enable +//! - [`serde`][]: Enable serialization/deserialization via serde. +//! - `unstable-locales`: Enable localization. This adds various methods with a +//! `_localized` suffix. The implementation and API may change or even be +//! removed in a patch release. Feedback welcome. //! //! [`serde`]: https://github.com/serde-rs/serde //! [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen @@ -217,12 +220,23 @@ //! [`to_rfc3339`](./struct.DateTime.html#method.to_rfc3339) methods //! for well-known formats. //! +//! Chrono now also provides date formatting in almost any language without the +//! help of an additional C library. This functionality is under the feature +//! `unstable-locales`: +//! +//! ```text +//! chrono { version = "0.4", features = ["unstable-locales"] +//! ``` +//! +//! The `unstable-locales` feature requires and implies at least the `alloc` feature. +//! //! ```rust //! use chrono::prelude::*; //! //! let dt = Utc.ymd(2014, 11, 28).and_hms(12, 0, 9); //! assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09"); //! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), "Fri Nov 28 12:00:09 2014"); +//! assert_eq!(dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(), "vendredi 28 novembre 2014, 12:00:09"); //! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), dt.format("%c").to_string()); //! //! assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC"); @@ -440,6 +454,8 @@ extern crate serde as serdelib; extern crate doc_comment; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] extern crate js_sys; +#[cfg(feature = "unstable-locales")] +extern crate pure_rust_locales; #[cfg(feature = "bench")] extern crate test; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))] @@ -456,6 +472,9 @@ pub use date::{Date, MAX_DATE, MIN_DATE}; #[cfg(feature = "rustc-serialize")] pub use datetime::rustc_serialize::TsSeconds; pub use datetime::{DateTime, SecondsFormat, MAX_DATETIME, MIN_DATETIME}; +/// L10n locales. +#[cfg(feature = "unstable-locales")] +pub use format::Locale; pub use format::{ParseError, ParseResult}; #[doc(no_inline)] pub use naive::{IsoWeek, NaiveDate, NaiveDateTime, NaiveTime}; @@ -473,6 +492,9 @@ pub mod prelude { #[cfg(feature = "clock")] #[doc(no_inline)] pub use Local; + #[cfg(feature = "unstable-locales")] + #[doc(no_inline)] + pub use Locale; #[doc(no_inline)] pub use SubsecRound; #[doc(no_inline)]