diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0637aef6da..8472827ebd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,8 +93,7 @@ jobs: # run using `bash` on all platforms for consistent # line-continuation marks shell: bash - - run: cargo test --lib --no-default-features - - run: cargo test --doc --no-default-features + - run: cargo test --no-default-features no_std: strategy: diff --git a/Cargo.toml b/Cargo.toml index 97e0de7971..91d1208b3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ __doctest = [] [dependencies] serde = { version = "1.0.99", default-features = false, optional = true } -pure-rust-locales = { version = "0.5.2", optional = true } +pure-rust-locales = { version = "0.6", optional = true } criterion = { version = "0.4.0", optional = true } rkyv = { version = "0.7", optional = true } arbitrary = { version = "1.0.0", features = ["derive"], optional = true } diff --git a/benches/chrono.rs b/benches/chrono.rs index 0f692ff7bf..74c4781ebc 100644 --- a/benches/chrono.rs +++ b/benches/chrono.rs @@ -3,7 +3,10 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use chrono::format::StrftimeItems; use chrono::prelude::*; +#[cfg(feature = "unstable-locales")] +use chrono::Locale; use chrono::{DateTime, FixedOffset, Local, Utc, __BenchYearFlags}; fn bench_datetime_parse_from_rfc2822(c: &mut Criterion) { @@ -122,6 +125,57 @@ fn bench_num_days_from_ce(c: &mut Criterion) { } } +fn bench_parse_strftime(c: &mut Criterion) { + c.bench_function("bench_parse_strftime", |b| { + b.iter(|| { + let str = black_box("%a, %d %b %Y %H:%M:%S GMT"); + let items = StrftimeItems::new(str); + black_box(items.collect::>()); + }) + }); +} + +#[cfg(feature = "unstable-locales")] +fn bench_parse_strftime_localized(c: &mut Criterion) { + c.bench_function("bench_parse_strftime_localized", |b| { + b.iter(|| { + let str = black_box("%a, %d %b %Y %H:%M:%S GMT"); + let items = StrftimeItems::new_with_locale(str, Locale::nl_NL); + black_box(items.collect::>()); + }) + }); +} + +fn bench_format(c: &mut Criterion) { + let dt = Local::now(); + c.bench_function("bench_format", |b| b.iter(|| format!("{}", dt.format("%Y-%m-%d %H-%M-%S")))); +} + +fn bench_format_with_items(c: &mut Criterion) { + let dt = Local::now(); + let items: Vec<_> = StrftimeItems::new("%Y-%m-%d %H-%M-%S").collect(); + c.bench_function("bench_format_with_items", |b| { + b.iter(|| format!("{}", dt.format_with_items(items.iter()))) + }); +} + +fn bench_format_manual(c: &mut Criterion) { + let dt = Local::now(); + c.bench_function("bench_format_manual", |b| { + b.iter(|| { + format!( + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second() + ) + }) + }); +} + criterion_group!( benches, bench_datetime_parse_from_rfc2822, @@ -132,6 +186,16 @@ criterion_group!( bench_year_flags_from_year, bench_num_days_from_ce, bench_get_local_time, + bench_parse_strftime, + bench_format, + bench_format_with_items, + bench_format_manual, ); +#[cfg(feature = "unstable-locales")] +criterion_group!(unstable_locales, bench_parse_strftime_localized,); + +#[cfg(not(feature = "unstable-locales"))] criterion_main!(benches); +#[cfg(feature = "unstable-locales")] +criterion_main!(benches, unstable_locales); diff --git a/src/date.rs b/src/date.rs index 22af92e362..32cb2068c5 100644 --- a/src/date.rs +++ b/src/date.rs @@ -4,7 +4,7 @@ //! ISO 8601 calendar date with time zone. #![allow(deprecated)] -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use core::borrow::Borrow; use core::cmp::Ordering; use core::ops::{Add, AddAssign, Sub, SubAssign}; @@ -15,7 +15,7 @@ use rkyv::{Archive, Deserialize, Serialize}; #[cfg(feature = "unstable-locales")] use crate::format::Locale; -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use crate::format::{DelayedFormat, Item, StrftimeItems}; use crate::naive::{IsoWeek, NaiveDate, NaiveTime}; use crate::offset::{TimeZone, Utc}; @@ -333,7 +333,7 @@ where Tz::Offset: fmt::Display, { /// Formats the date with the specified formatting items. - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -348,7 +348,7 @@ where /// Formats the date with the specified format string. /// See the [`crate::format::strftime`] module /// on the supported escape sequences. - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 691fd5b27f..bc30dae7b4 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -15,13 +15,10 @@ use core::ops::{Add, AddAssign, Sub, SubAssign}; use core::{fmt, hash, str}; #[cfg(feature = "std")] use std::string::ToString; -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(feature = "rkyv")] -use rkyv::{Archive, Deserialize, Serialize}; - -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use crate::format::DelayedFormat; #[cfg(feature = "unstable-locales")] use crate::format::Locale; @@ -35,6 +32,9 @@ use crate::offset::{FixedOffset, Offset, TimeZone, Utc}; use crate::Date; use crate::{Datelike, Months, TimeDelta, Timelike, Weekday}; +#[cfg(feature = "rkyv")] +use rkyv::{Archive, Deserialize, Serialize}; + /// documented at re-export site #[cfg(feature = "serde")] pub(super) mod serde; @@ -480,6 +480,100 @@ impl DateTime { } } + /// Returns an RFC 2822 date and time string such as `Tue, 1 Jul 2003 10:52:37 +0200`. + /// + /// # Panics + /// + /// Panics if the date can not be represented in this format: the year may not be negative and + /// can not have more than 4 digits. + #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] + #[must_use] + pub fn to_rfc2822(&self) -> String { + let mut result = String::with_capacity(32); + crate::format::write_rfc2822(&mut result, self.naive_local(), self.offset.fix()) + .expect("writing rfc2822 datetime to string should never fail"); + result + } + + /// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`. + #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] + #[must_use] + pub fn to_rfc3339(&self) -> String { + let mut result = String::with_capacity(32); + crate::format::write_rfc3339(&mut result, self.naive_local(), self.offset.fix()) + .expect("writing rfc3339 datetime to string should never fail"); + result + } + + /// Return an RFC 3339 and ISO 8601 date and time string with subseconds + /// formatted as per `SecondsFormat`. + /// + /// If `use_z` is true and the timezone is UTC (offset 0), uses `Z` as + /// per [`Fixed::TimezoneOffsetColonZ`]. If `use_z` is false, uses + /// [`Fixed::TimezoneOffsetColon`] + /// + /// # Examples + /// + /// ```rust + /// # use chrono::{FixedOffset, SecondsFormat, TimeZone, Utc, NaiveDate}; + /// let dt = NaiveDate::from_ymd_opt(2018, 1, 26).unwrap().and_hms_micro_opt(18, 30, 9, 453_829).unwrap().and_local_timezone(Utc).unwrap(); + /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Millis, false), + /// "2018-01-26T18:30:09.453+00:00"); + /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Millis, true), + /// "2018-01-26T18:30:09.453Z"); + /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), + /// "2018-01-26T18:30:09Z"); + /// + /// let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); + /// let dt = pst.from_local_datetime(&NaiveDate::from_ymd_opt(2018, 1, 26).unwrap().and_hms_micro_opt(10, 30, 9, 453_829).unwrap()).unwrap(); + /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), + /// "2018-01-26T10:30:09+08:00"); + /// ``` + #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] + #[must_use] + pub fn to_rfc3339_opts(&self, secform: SecondsFormat, use_z: bool) -> String { + use crate::format::Numeric::*; + use crate::format::Pad::Zero; + use crate::SecondsFormat::*; + + const PREFIX: &[Item<'static>] = &[ + Item::Numeric(Year, Zero), + Item::Literal("-"), + Item::Numeric(Month, Zero), + Item::Literal("-"), + Item::Numeric(Day, Zero), + Item::Literal("T"), + Item::Numeric(Hour, Zero), + Item::Literal(":"), + Item::Numeric(Minute, Zero), + Item::Literal(":"), + Item::Numeric(Second, Zero), + ]; + + let ssitem = match secform { + Secs => None, + Millis => Some(Item::Fixed(Fixed::Nanosecond3)), + Micros => Some(Item::Fixed(Fixed::Nanosecond6)), + Nanos => Some(Item::Fixed(Fixed::Nanosecond9)), + AutoSi => Some(Item::Fixed(Fixed::Nanosecond)), + }; + + let tzitem = Item::Fixed(if use_z { + Fixed::TimezoneOffsetColonZ + } else { + Fixed::TimezoneOffsetColon + }); + + let dt = self.fixed_offset(); + match ssitem { + None => dt.format_with_items(PREFIX.iter().chain([tzitem].iter())).to_string(), + Some(s) => dt.format_with_items(PREFIX.iter().chain([s, tzitem].iter())).to_string(), + } + } + /// The minimum possible `DateTime`. pub const MIN_UTC: DateTime = DateTime { datetime: NaiveDateTime::MIN, offset: Utc }; /// The maximum possible `DateTime`. @@ -757,101 +851,8 @@ impl DateTime where Tz::Offset: fmt::Display, { - /// Returns an RFC 2822 date and time string such as `Tue, 1 Jul 2003 10:52:37 +0200`. - /// - /// # Panics - /// - /// Panics if the date can not be represented in this format: the year may not be negative and - /// can not have more than 4 digits. - #[cfg(any(feature = "alloc", feature = "std", test))] - #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] - #[must_use] - pub fn to_rfc2822(&self) -> String { - let mut result = String::with_capacity(32); - crate::format::write_rfc2822(&mut result, self.naive_local(), self.offset.fix()) - .expect("writing rfc2822 datetime to string should never fail"); - result - } - - /// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`. - #[cfg(any(feature = "alloc", feature = "std", test))] - #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] - #[must_use] - pub fn to_rfc3339(&self) -> String { - let mut result = String::with_capacity(32); - crate::format::write_rfc3339(&mut result, self.naive_local(), self.offset.fix()) - .expect("writing rfc3339 datetime to string should never fail"); - result - } - - /// Return an RFC 3339 and ISO 8601 date and time string with subseconds - /// formatted as per `SecondsFormat`. - /// - /// If `use_z` is true and the timezone is UTC (offset 0), uses `Z` as - /// per [`Fixed::TimezoneOffsetColonZ`]. If `use_z` is false, uses - /// [`Fixed::TimezoneOffsetColon`] - /// - /// # Examples - /// - /// ```rust - /// # use chrono::{FixedOffset, SecondsFormat, TimeZone, Utc, NaiveDate}; - /// let dt = NaiveDate::from_ymd_opt(2018, 1, 26).unwrap().and_hms_micro_opt(18, 30, 9, 453_829).unwrap().and_local_timezone(Utc).unwrap(); - /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Millis, false), - /// "2018-01-26T18:30:09.453+00:00"); - /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Millis, true), - /// "2018-01-26T18:30:09.453Z"); - /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), - /// "2018-01-26T18:30:09Z"); - /// - /// let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); - /// let dt = pst.from_local_datetime(&NaiveDate::from_ymd_opt(2018, 1, 26).unwrap().and_hms_micro_opt(10, 30, 9, 453_829).unwrap()).unwrap(); - /// assert_eq!(dt.to_rfc3339_opts(SecondsFormat::Secs, true), - /// "2018-01-26T10:30:09+08:00"); - /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] - #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] - #[must_use] - pub fn to_rfc3339_opts(&self, secform: SecondsFormat, use_z: bool) -> String { - use crate::format::Numeric::*; - use crate::format::Pad::Zero; - use crate::SecondsFormat::*; - - const PREFIX: &[Item<'static>] = &[ - Item::Numeric(Year, Zero), - Item::Literal("-"), - Item::Numeric(Month, Zero), - Item::Literal("-"), - Item::Numeric(Day, Zero), - Item::Literal("T"), - Item::Numeric(Hour, Zero), - Item::Literal(":"), - Item::Numeric(Minute, Zero), - Item::Literal(":"), - Item::Numeric(Second, Zero), - ]; - - let ssitem = match secform { - Secs => None, - Millis => Some(Item::Fixed(Fixed::Nanosecond3)), - Micros => Some(Item::Fixed(Fixed::Nanosecond6)), - Nanos => Some(Item::Fixed(Fixed::Nanosecond9)), - AutoSi => Some(Item::Fixed(Fixed::Nanosecond)), - }; - - let tzitem = Item::Fixed(if use_z { - Fixed::TimezoneOffsetColonZ - } else { - Fixed::TimezoneOffsetColon - }); - - match ssitem { - None => self.format_with_items(PREFIX.iter().chain([tzitem].iter())).to_string(), - Some(s) => self.format_with_items(PREFIX.iter().chain([s, tzitem].iter())).to_string(), - } - } - /// Formats the combined date and time with the specified formatting items. - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -876,7 +877,7 @@ where /// let formatted = format!("{}", date_time.format("%d/%m/%Y %H:%M")); /// assert_eq!(formatted, "02/04/2017 12:50"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -1354,7 +1355,7 @@ impl str::FromStr for DateTime { } } -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for DateTime { fn from(t: SystemTime) -> DateTime { @@ -1383,7 +1384,7 @@ impl From for DateTime { } } -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From> for SystemTime { fn from(dt: DateTime) -> SystemTime { @@ -1548,7 +1549,7 @@ fn test_decodable_json( // we don't know the exact local offset but we can check that // the conversion didn't change the instant itself assert_eq!( - local_from_str(r#""2014-07-24T12:34:06Z""#).expect("local shouuld parse"), + local_from_str(r#""2014-07-24T12:34:06Z""#).expect("local should parse"), Utc.with_ymd_and_hms(2014, 7, 24, 12, 34, 6).unwrap() ); assert_eq!( diff --git a/src/datetime/serde.rs b/src/datetime/serde.rs index 90c1b4ff5d..6a244f59f6 100644 --- a/src/datetime/serde.rs +++ b/src/datetime/serde.rs @@ -1122,7 +1122,9 @@ pub mod ts_seconds_option { #[cfg(test)] mod tests { - use crate::datetime::{test_decodable_json, test_encodable_json}; + #[cfg(feature = "clock")] + use crate::datetime::test_decodable_json; + use crate::datetime::test_encodable_json; use crate::{DateTime, TimeZone, Utc}; #[test] diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 4517bb9bb9..e6440a685f 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1,5 +1,3 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - use super::DateTime; use crate::naive::{NaiveDate, NaiveTime}; use crate::offset::{FixedOffset, TimeZone, Utc}; @@ -274,6 +272,7 @@ fn ymdhms_milli( // local helper function to easily create a DateTime #[allow(clippy::too_many_arguments)] +#[cfg(any(feature = "alloc", feature = "std"))] fn ymdhms_micro( fixedoffset: &FixedOffset, year: i32, @@ -293,6 +292,7 @@ fn ymdhms_micro( // local helper function to easily create a DateTime #[allow(clippy::too_many_arguments)] +#[cfg(any(feature = "alloc", feature = "std"))] fn ymdhms_nano( fixedoffset: &FixedOffset, year: i32, @@ -449,6 +449,7 @@ fn test_datetime_with_timezone() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_datetime_rfc2822() { let edt = FixedOffset::east_opt(5 * 60 * 60).unwrap(); @@ -553,6 +554,7 @@ fn test_datetime_rfc2822() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_datetime_rfc3339() { let edt5 = FixedOffset::east_opt(5 * 60 * 60).unwrap(); let edt0 = FixedOffset::east_opt(0).unwrap(); @@ -678,6 +680,7 @@ fn test_datetime_rfc3339() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_rfc3339_opts() { use crate::SecondsFormat::*; let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); @@ -1221,79 +1224,9 @@ fn test_subsecond_part() { } #[test] -#[cfg(not(target_os = "windows"))] -fn test_from_system_time() { - use std::time::Duration; - - let epoch = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(); - let nanos = 999_999_999; - - // SystemTime -> DateTime - assert_eq!(DateTime::::from(UNIX_EPOCH), epoch); - assert_eq!( - DateTime::::from(UNIX_EPOCH + Duration::new(999_999_999, nanos)), - Utc.from_local_datetime( - &NaiveDate::from_ymd_opt(2001, 9, 9) - .unwrap() - .and_hms_nano_opt(1, 46, 39, nanos) - .unwrap() - ) - .unwrap() - ); - assert_eq!( - DateTime::::from(UNIX_EPOCH - Duration::new(999_999_999, nanos)), - Utc.from_local_datetime( - &NaiveDate::from_ymd_opt(1938, 4, 24).unwrap().and_hms_nano_opt(22, 13, 20, 1).unwrap() - ) - .unwrap() - ); - - // DateTime -> SystemTime - assert_eq!(SystemTime::from(epoch), UNIX_EPOCH); - assert_eq!( - SystemTime::from( - Utc.from_local_datetime( - &NaiveDate::from_ymd_opt(2001, 9, 9) - .unwrap() - .and_hms_nano_opt(1, 46, 39, nanos) - .unwrap() - ) - .unwrap() - ), - UNIX_EPOCH + Duration::new(999_999_999, nanos) - ); - assert_eq!( - SystemTime::from( - Utc.from_local_datetime( - &NaiveDate::from_ymd_opt(1938, 4, 24) - .unwrap() - .and_hms_nano_opt(22, 13, 20, 1) - .unwrap() - ) - .unwrap() - ), - UNIX_EPOCH - Duration::new(999_999_999, 999_999_999) - ); - - // DateTime -> SystemTime (via `with_timezone`) - #[cfg(feature = "clock")] - { - assert_eq!(SystemTime::from(epoch.with_timezone(&Local)), UNIX_EPOCH); - } - assert_eq!( - SystemTime::from(epoch.with_timezone(&FixedOffset::east_opt(32400).unwrap())), - UNIX_EPOCH - ); - assert_eq!( - SystemTime::from(epoch.with_timezone(&FixedOffset::west_opt(28800).unwrap())), - UNIX_EPOCH - ); -} - -#[test] -#[cfg(target_os = "windows")] +#[cfg(feature = "std")] fn test_from_system_time() { - use std::time::Duration; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; let nanos = 999_999_000; @@ -1365,6 +1298,7 @@ fn test_from_system_time() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_datetime_format_alignment() { let datetime = Utc.with_ymd_and_hms(2007, 1, 2, 0, 0, 0).unwrap(); diff --git a/src/format/formatting.rs b/src/format/formatting.rs new file mode 100644 index 0000000000..61cf4dc5e6 --- /dev/null +++ b/src/format/formatting.rs @@ -0,0 +1,821 @@ +// This is a part of Chrono. +// See README.md and LICENSE.txt for details. + +//! Date and time formatting routines. + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "alloc")] +use alloc::string::{String, ToString}; +#[cfg(any(feature = "alloc", feature = "std"))] +use core::borrow::Borrow; +use core::fmt; +use core::fmt::Write; + +#[cfg(any(feature = "alloc", feature = "std"))] +use crate::naive::{NaiveDate, NaiveTime}; +#[cfg(any(feature = "alloc", feature = "std"))] +use crate::offset::{FixedOffset, Offset}; +#[cfg(any(feature = "alloc", feature = "std"))] +use crate::{Datelike, Timelike, Weekday}; + +#[cfg(feature = "unstable-locales")] +use super::locales; +#[cfg(any(feature = "alloc", feature = "std"))] +use super::{ + Colons, Fixed, InternalFixed, InternalInternal, Item, Locale, Numeric, OffsetFormat, + OffsetPrecision, Pad, +}; + +#[cfg(any(feature = "alloc", feature = "std"))] +struct Locales { + short_months: &'static [&'static str], + long_months: &'static [&'static str], + short_weekdays: &'static [&'static str], + long_weekdays: &'static [&'static str], + am_pm: &'static [&'static str], +} + +#[cfg(any(feature = "alloc", feature = "std"))] +impl Locales { + fn new(_locale: Option) -> Self { + #[cfg(feature = "unstable-locales")] + { + let locale = _locale.unwrap_or(Locale::POSIX); + Self { + short_months: locales::short_months(locale), + long_months: locales::long_months(locale), + short_weekdays: locales::short_weekdays(locale), + long_weekdays: locales::long_weekdays(locale), + am_pm: locales::am_pm(locale), + } + } + #[cfg(not(feature = "unstable-locales"))] + Self { + short_months: &[ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ], + long_months: &[ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + short_weekdays: &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + long_weekdays: &[ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ], + am_pm: &["AM", "PM"], + } + } +} + +/// A *temporary* object which can be used as an argument to `format!` or others. +/// This is normally constructed via `format` methods of each date and time type. +#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] +#[derive(Debug)] +pub struct DelayedFormat { + /// The date view, if any. + date: Option, + /// The time view, if any. + time: Option, + /// The name and local-to-UTC difference for the offset (timezone), if any. + off: Option<(String, FixedOffset)>, + /// An iterator returning formatting items. + items: I, + /// Locale used for text. + // TODO: Only used with the locale feature. We should make this property + // only present when the feature is enabled. + #[cfg(feature = "unstable-locales")] + locale: Option, +} + +#[cfg(any(feature = "alloc", feature = "std"))] +impl<'a, I: Iterator + Clone, B: Borrow>> DelayedFormat { + /// Makes a new `DelayedFormat` value out of local date and time. + #[must_use] + pub fn new(date: Option, time: Option, items: I) -> DelayedFormat { + DelayedFormat { + date, + time, + off: None, + items, + #[cfg(feature = "unstable-locales")] + locale: None, + } + } + + /// Makes a new `DelayedFormat` value out of local date and time and UTC offset. + #[must_use] + pub fn new_with_offset( + date: Option, + time: Option, + offset: &Off, + items: I, + ) -> DelayedFormat + where + Off: Offset + fmt::Display, + { + let name_and_diff = (offset.to_string(), offset.fix()); + DelayedFormat { + date, + time, + off: Some(name_and_diff), + items, + #[cfg(feature = "unstable-locales")] + locale: None, + } + } + + /// Makes a new `DelayedFormat` value out of local date and time and locale. + #[cfg(feature = "unstable-locales")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] + #[must_use] + pub fn new_with_locale( + date: Option, + time: Option, + items: I, + locale: Locale, + ) -> DelayedFormat { + DelayedFormat { date, time, off: None, items, locale: Some(locale) } + } + + /// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale. + #[cfg(feature = "unstable-locales")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] + #[must_use] + 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, time, off: Some(name_and_diff), items, locale: Some(locale) } + } +} + +#[cfg(any(feature = "alloc", feature = "std"))] +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()) + } +} + +/// Tries to format given arguments with given formatting items. +/// Internally used by `DelayedFormat`. +#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] +pub fn format<'a, I, B>( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + items: I, +) -> 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(), None)?; + } + w.pad(&result) +} +/// Formats single formatting item +#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] +pub fn format_item( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + item: &Item<'_>, +) -> fmt::Result { + let mut result = String::new(); + format_inner(&mut result, date, time, off, item, None)?; + w.pad(&result) +} + +/// Tries to format given arguments with given formatting items. +/// Internally used by `DelayedFormat`. +#[cfg(feature = "unstable-locales")] +#[cfg_attr(docsrs, doc(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) +} + +/// Formats single formatting item +#[cfg(feature = "unstable-locales")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] +pub fn format_item_localized( + w: &mut fmt::Formatter, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + item: &Item<'_>, + locale: Locale, +) -> fmt::Result { + let mut result = String::new(); + format_inner(&mut result, date, time, off, item, Some(locale))?; + w.pad(&result) +} + +#[cfg(any(feature = "alloc", feature = "std"))] +fn format_inner( + result: &mut String, + date: Option<&NaiveDate>, + time: Option<&NaiveTime>, + off: Option<&(String, FixedOffset)>, + item: &Item<'_>, + locale: Option, +) -> fmt::Result { + let locale = Locales::new(locale); + + match *item { + Item::Literal(s) | Item::Space(s) => result.push_str(s), + #[cfg(any(feature = "alloc", feature = "std"))] + Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => result.push_str(s), + + Item::Numeric(ref spec, ref pad) => { + use self::Numeric::*; + + let week_from_sun = |d: &NaiveDate| d.weeks_from(Weekday::Sun); + let week_from_mon = |d: &NaiveDate| d.weeks_from(Weekday::Mon); + + let (width, v) = match *spec { + Year => (4, date.map(|d| i64::from(d.year()))), + YearDiv100 => (2, date.map(|d| i64::from(d.year()).div_euclid(100))), + YearMod100 => (2, date.map(|d| i64::from(d.year()).rem_euclid(100))), + IsoYear => (4, date.map(|d| i64::from(d.iso_week().year()))), + IsoYearDiv100 => (2, date.map(|d| i64::from(d.iso_week().year()).div_euclid(100))), + IsoYearMod100 => (2, date.map(|d| i64::from(d.iso_week().year()).rem_euclid(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).timestamp()), + (Some(d), Some(t), Some(&(_, off))) => { + Some((d.and_time(*t) - off).timestamp()) + } + (_, _, _) => None, + }, + ), + + // for the future expansion + Internal(ref int) => match int._dummy {}, + }; + + 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!(result, "{:+}", v), + Pad::Zero => write!(result, "{:+01$}", v, width + 1), + Pad::Space => write!(result, "{:+1$}", v, width + 1), + } + } else { + match *pad { + Pad::None => write!(result, "{}", v), + Pad::Zero => write!(result, "{:01$}", v, width), + Pad::Space => write!(result, "{:1$}", v, width), + } + }? + } else { + return Err(fmt::Error); // insufficient arguments for given format + } + } + + Item::Fixed(ref spec) => { + use self::Fixed::*; + + let ret = match *spec { + ShortMonthName => date.map(|d| { + result.push_str(locale.short_months[d.month0() as usize]); + Ok(()) + }), + LongMonthName => date.map(|d| { + result.push_str(locale.long_months[d.month0() as usize]); + Ok(()) + }), + ShortWeekdayName => date.map(|d| { + result.push_str( + locale.short_weekdays[d.weekday().num_days_from_sunday() as usize], + ); + Ok(()) + }), + LongWeekdayName => date.map(|d| { + result.push_str( + locale.long_weekdays[d.weekday().num_days_from_sunday() as usize], + ); + Ok(()) + }), + LowerAmPm => time.map(|t| { + let ampm = if t.hour12().0 { locale.am_pm[1] } else { locale.am_pm[0] }; + for char in ampm.chars() { + result.extend(char.to_lowercase()) + } + Ok(()) + }), + UpperAmPm => time.map(|t| { + result.push_str(if t.hour12().0 { locale.am_pm[1] } else { locale.am_pm[0] }); + Ok(()) + }), + Nanosecond => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + if nano == 0 { + Ok(()) + } else if nano % 1_000_000 == 0 { + write!(result, ".{:03}", nano / 1_000_000) + } else if nano % 1_000 == 0 { + write!(result, ".{:06}", nano / 1_000) + } else { + write!(result, ".{:09}", nano) + } + }), + Nanosecond3 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, ".{:03}", nano / 1_000_000) + }), + Nanosecond6 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, ".{:06}", nano / 1_000) + }), + Nanosecond9 => time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, ".{:09}", nano) + }), + Internal(InternalFixed { val: InternalInternal::Nanosecond3NoDot }) => { + time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, "{:03}", nano / 1_000_000) + }) + } + Internal(InternalFixed { val: InternalInternal::Nanosecond6NoDot }) => { + time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, "{:06}", nano / 1_000) + }) + } + Internal(InternalFixed { val: InternalInternal::Nanosecond9NoDot }) => { + time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + write!(result, "{:09}", nano) + }) + } + TimezoneName => off.map(|(name, _)| { + result.push_str(name); + Ok(()) + }), + TimezoneOffset | TimezoneOffsetZ => off.map(|&(_, off)| { + OffsetFormat { + precision: OffsetPrecision::Minutes, + colons: Colons::Maybe, + allow_zulu: *spec == TimezoneOffsetZ, + padding: Pad::Zero, + } + .format(result, off) + }), + TimezoneOffsetColon | TimezoneOffsetColonZ => off.map(|&(_, off)| { + OffsetFormat { + precision: OffsetPrecision::Minutes, + colons: Colons::Colon, + allow_zulu: *spec == TimezoneOffsetColonZ, + padding: Pad::Zero, + } + .format(result, off) + }), + TimezoneOffsetDoubleColon => off.map(|&(_, off)| { + OffsetFormat { + precision: OffsetPrecision::Seconds, + colons: Colons::Colon, + allow_zulu: false, + padding: Pad::Zero, + } + .format(result, off) + }), + TimezoneOffsetTripleColon => off.map(|&(_, off)| { + OffsetFormat { + precision: OffsetPrecision::Hours, + colons: Colons::None, + allow_zulu: false, + padding: Pad::Zero, + } + .format(result, off) + }), + Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { + return Err(fmt::Error); + } + RFC2822 => + // same as `%a, %d %b %Y %H:%M:%S %z` + { + if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { + Some(write_rfc2822_inner(result, d, t, off, locale)) + } else { + None + } + } + RFC3339 => + // same as `%Y-%m-%dT%H:%M:%S%.f%:z` + { + if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { + Some(write_rfc3339(result, crate::NaiveDateTime::new(*d, *t), off)) + } else { + None + } + } + }; + + match ret { + Some(ret) => ret?, + None => return Err(fmt::Error), // insufficient arguments for given format + } + } + + Item::Error => return Err(fmt::Error), + } + Ok(()) +} + +#[cfg(any(feature = "alloc", feature = "std"))] +impl OffsetFormat { + /// Writes an offset from UTC to `result` with the format defined by `self`. + fn format(&self, result: &mut String, off: FixedOffset) -> fmt::Result { + let off = off.local_minus_utc(); + if self.allow_zulu && off == 0 { + result.push('Z'); + return Ok(()); + } + let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; + + let hours; + let mut mins = 0; + let mut secs = 0; + let precision = match self.precision { + OffsetPrecision::Hours => { + // Minutes and seconds are simply truncated + hours = (off / 3600) as u8; + OffsetPrecision::Hours + } + OffsetPrecision::Minutes | OffsetPrecision::OptionalMinutes => { + // Round seconds to the nearest minute. + let minutes = (off + 30) / 60; + mins = (minutes % 60) as u8; + hours = (minutes / 60) as u8; + if self.precision == OffsetPrecision::OptionalMinutes && mins == 0 { + OffsetPrecision::Hours + } else { + OffsetPrecision::Minutes + } + } + OffsetPrecision::Seconds + | OffsetPrecision::OptionalSeconds + | OffsetPrecision::OptionalMinutesAndSeconds => { + let minutes = off / 60; + secs = (off % 60) as u8; + mins = (minutes % 60) as u8; + hours = (minutes / 60) as u8; + if self.precision != OffsetPrecision::Seconds && secs == 0 { + if self.precision == OffsetPrecision::OptionalMinutesAndSeconds && mins == 0 { + OffsetPrecision::Hours + } else { + OffsetPrecision::Minutes + } + } else { + OffsetPrecision::Seconds + } + } + }; + let colons = self.colons == Colons::Colon; + + if hours < 10 { + if self.padding == Pad::Space { + result.push(' '); + } + result.push(sign); + if self.padding == Pad::Zero { + result.push('0'); + } + result.push((b'0' + hours) as char); + } else { + result.push(sign); + write_hundreds(result, hours)?; + } + if let OffsetPrecision::Minutes | OffsetPrecision::Seconds = precision { + if colons { + result.push(':'); + } + write_hundreds(result, mins)?; + } + if let OffsetPrecision::Seconds = precision { + if colons { + result.push(':'); + } + write_hundreds(result, secs)?; + } + Ok(()) + } +} + +/// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z` +#[cfg(any(feature = "alloc", feature = "std"))] +pub(crate) fn write_rfc3339( + result: &mut String, + dt: crate::NaiveDateTime, + off: FixedOffset, +) -> fmt::Result { + // reuse `Debug` impls which already print ISO 8601 format. + // this is faster in this way. + write!(result, "{:?}", dt)?; + OffsetFormat { + precision: OffsetPrecision::Minutes, + colons: Colons::Colon, + allow_zulu: false, + padding: Pad::Zero, + } + .format(result, off) +} + +#[cfg(any(feature = "alloc", feature = "std"))] +/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` +pub(crate) fn write_rfc2822( + result: &mut String, + dt: crate::NaiveDateTime, + off: FixedOffset, +) -> fmt::Result { + write_rfc2822_inner(result, &dt.date(), &dt.time(), off, Locales::new(None)) +} + +#[cfg(any(feature = "alloc", feature = "std"))] +/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` +fn write_rfc2822_inner( + result: &mut String, + d: &NaiveDate, + t: &NaiveTime, + off: FixedOffset, + locale: Locales, +) -> fmt::Result { + let year = d.year(); + // RFC2822 is only defined on years 0 through 9999 + if !(0..=9999).contains(&year) { + return Err(fmt::Error); + } + + result.push_str(locale.short_weekdays[d.weekday().num_days_from_sunday() as usize]); + result.push_str(", "); + write_hundreds(result, d.day() as u8)?; + result.push(' '); + result.push_str(locale.short_months[d.month0() as usize]); + result.push(' '); + write_hundreds(result, (year / 100) as u8)?; + write_hundreds(result, (year % 100) as u8)?; + result.push(' '); + write_hundreds(result, t.hour() as u8)?; + result.push(':'); + write_hundreds(result, t.minute() as u8)?; + result.push(':'); + let sec = t.second() + t.nanosecond() / 1_000_000_000; + write_hundreds(result, sec as u8)?; + result.push(' '); + OffsetFormat { + precision: OffsetPrecision::Minutes, + colons: Colons::None, + allow_zulu: false, + padding: Pad::Zero, + } + .format(result, off) +} + +/// Equivalent to `{:02}` formatting for n < 100. +pub(crate) fn write_hundreds(w: &mut impl Write, n: u8) -> fmt::Result { + if n >= 100 { + return Err(fmt::Error); + } + + let tens = b'0' + n / 10; + let ones = b'0' + n % 10; + w.write_char(tens as char)?; + w.write_char(ones as char) +} + +#[cfg(test)] +#[cfg(any(feature = "alloc", feature = "std"))] +mod tests { + use super::{Colons, OffsetFormat, OffsetPrecision, Pad}; + use crate::FixedOffset; + + #[test] + fn test_offset_formatting() { + fn check_all(precision: OffsetPrecision, expected: [[&str; 7]; 12]) { + fn check( + precision: OffsetPrecision, + colons: Colons, + padding: Pad, + allow_zulu: bool, + offsets: [FixedOffset; 7], + expected: [&str; 7], + ) { + let offset_format = OffsetFormat { precision, colons, allow_zulu, padding }; + for (offset, expected) in offsets.iter().zip(expected.iter()) { + let mut output = String::new(); + offset_format.format(&mut output, *offset).unwrap(); + assert_eq!(&output, expected); + } + } + // +03:45, -03:30, +11:00, -11:00:22, +02:34:26, -12:34:30, +00:00 + let offsets = [ + FixedOffset::east_opt(13_500).unwrap(), + FixedOffset::east_opt(-12_600).unwrap(), + FixedOffset::east_opt(39_600).unwrap(), + FixedOffset::east_opt(-39_622).unwrap(), + FixedOffset::east_opt(9266).unwrap(), + FixedOffset::east_opt(-45270).unwrap(), + FixedOffset::east_opt(0).unwrap(), + ]; + check(precision, Colons::Colon, Pad::Zero, false, offsets, expected[0]); + check(precision, Colons::Colon, Pad::Zero, true, offsets, expected[1]); + check(precision, Colons::Colon, Pad::Space, false, offsets, expected[2]); + check(precision, Colons::Colon, Pad::Space, true, offsets, expected[3]); + check(precision, Colons::Colon, Pad::None, false, offsets, expected[4]); + check(precision, Colons::Colon, Pad::None, true, offsets, expected[5]); + check(precision, Colons::None, Pad::Zero, false, offsets, expected[6]); + check(precision, Colons::None, Pad::Zero, true, offsets, expected[7]); + check(precision, Colons::None, Pad::Space, false, offsets, expected[8]); + check(precision, Colons::None, Pad::Space, true, offsets, expected[9]); + check(precision, Colons::None, Pad::None, false, offsets, expected[10]); + check(precision, Colons::None, Pad::None, true, offsets, expected[11]); + // `Colons::Maybe` should format the same as `Colons::None` + check(precision, Colons::Maybe, Pad::Zero, false, offsets, expected[6]); + check(precision, Colons::Maybe, Pad::Zero, true, offsets, expected[7]); + check(precision, Colons::Maybe, Pad::Space, false, offsets, expected[8]); + check(precision, Colons::Maybe, Pad::Space, true, offsets, expected[9]); + check(precision, Colons::Maybe, Pad::None, false, offsets, expected[10]); + check(precision, Colons::Maybe, Pad::None, true, offsets, expected[11]); + } + check_all( + OffsetPrecision::Hours, + [ + ["+03", "-03", "+11", "-11", "+02", "-12", "+00"], + ["+03", "-03", "+11", "-11", "+02", "-12", "Z"], + [" +3", " -3", "+11", "-11", " +2", "-12", " +0"], + [" +3", " -3", "+11", "-11", " +2", "-12", "Z"], + ["+3", "-3", "+11", "-11", "+2", "-12", "+0"], + ["+3", "-3", "+11", "-11", "+2", "-12", "Z"], + ["+03", "-03", "+11", "-11", "+02", "-12", "+00"], + ["+03", "-03", "+11", "-11", "+02", "-12", "Z"], + [" +3", " -3", "+11", "-11", " +2", "-12", " +0"], + [" +3", " -3", "+11", "-11", " +2", "-12", "Z"], + ["+3", "-3", "+11", "-11", "+2", "-12", "+0"], + ["+3", "-3", "+11", "-11", "+2", "-12", "Z"], + ], + ); + check_all( + OffsetPrecision::Minutes, + [ + ["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "+00:00"], + ["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "Z"], + [" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", " +0:00"], + [" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", "Z"], + ["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "+0:00"], + ["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "Z"], + ["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "+0000"], + ["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "Z"], + [" +345", " -330", "+1100", "-1100", " +234", "-1235", " +000"], + [" +345", " -330", "+1100", "-1100", " +234", "-1235", "Z"], + ["+345", "-330", "+1100", "-1100", "+234", "-1235", "+000"], + ["+345", "-330", "+1100", "-1100", "+234", "-1235", "Z"], + ], + ); + #[rustfmt::skip] + check_all( + OffsetPrecision::Seconds, + [ + ["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00:00"], + ["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"], + [" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00:00"], + [" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"], + ["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00:00"], + ["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"], + ["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "+000000"], + ["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "Z"], + [" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", " +00000"], + [" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", "Z"], + ["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "+00000"], + ["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "Z"], + ], + ); + check_all( + OffsetPrecision::OptionalMinutes, + [ + ["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "+00"], + ["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "Z"], + [" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", " +0"], + [" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", "Z"], + ["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "+0"], + ["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "Z"], + ["+0345", "-0330", "+11", "-11", "+0234", "-1235", "+00"], + ["+0345", "-0330", "+11", "-11", "+0234", "-1235", "Z"], + [" +345", " -330", "+11", "-11", " +234", "-1235", " +0"], + [" +345", " -330", "+11", "-11", " +234", "-1235", "Z"], + ["+345", "-330", "+11", "-11", "+234", "-1235", "+0"], + ["+345", "-330", "+11", "-11", "+234", "-1235", "Z"], + ], + ); + check_all( + OffsetPrecision::OptionalSeconds, + [ + ["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00"], + ["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"], + [" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00"], + [" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"], + ["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00"], + ["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"], + ["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "+0000"], + ["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "Z"], + [" +345", " -330", "+1100", "-110022", " +23426", "-123430", " +000"], + [" +345", " -330", "+1100", "-110022", " +23426", "-123430", "Z"], + ["+345", "-330", "+1100", "-110022", "+23426", "-123430", "+000"], + ["+345", "-330", "+1100", "-110022", "+23426", "-123430", "Z"], + ], + ); + check_all( + OffsetPrecision::OptionalMinutesAndSeconds, + [ + ["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "+00"], + ["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "Z"], + [" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", " +0"], + [" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", "Z"], + ["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "+0"], + ["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "Z"], + ["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "+00"], + ["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "Z"], + [" +345", " -330", "+11", "-110022", " +23426", "-123430", " +0"], + [" +345", " -330", "+11", "-110022", " +23426", "-123430", "Z"], + ["+345", "-330", "+11", "-110022", "+23426", "-123430", "+0"], + ["+345", "-330", "+11", "-110022", "+23426", "-123430", "Z"], + ], + ); + } +} diff --git a/src/format/locales.rs b/src/format/locales.rs index d6fecc978b..7311d661b1 100644 --- a/src/format/locales.rs +++ b/src/format/locales.rs @@ -31,3 +31,7 @@ pub(crate) const fn d_t_fmt(locale: Locale) -> &'static str { pub(crate) const fn t_fmt(locale: Locale) -> &'static str { locale_match!(locale => LC_TIME::T_FMT) } + +pub(crate) const fn t_fmt_ampm(locale: Locale) -> &'static str { + locale_match!(locale => LC_TIME::T_FMT_AMPM) +} diff --git a/src/format/mod.rs b/src/format/mod.rs index d7f46c6744..f0830a315c 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -35,27 +35,32 @@ extern crate alloc; #[cfg(feature = "alloc")] use alloc::boxed::Box; -#[cfg(feature = "alloc")] -use alloc::string::{String, ToString}; -#[cfg(any(feature = "alloc", feature = "std", test))] -use core::borrow::Borrow; use core::fmt; -use core::fmt::Write; use core::str::FromStr; -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] use std::error::Error; -#[cfg(any(feature = "alloc", feature = "std", test))] -use crate::naive::{NaiveDate, NaiveTime}; -#[cfg(any(feature = "alloc", feature = "std", test))] -use crate::offset::{FixedOffset, Offset}; -#[cfg(any(feature = "alloc", feature = "std", test))] -use crate::{Datelike, Timelike}; use crate::{Month, ParseMonthError, ParseWeekdayError, Weekday}; +mod formatting; +mod parsed; + +// due to the size of parsing routines, they are in separate modules. +mod parse; +pub(crate) mod scan; + +pub mod strftime; + #[cfg(feature = "unstable-locales")] pub(crate) mod locales; +pub(crate) use formatting::write_hundreds; +#[cfg(any(feature = "alloc", feature = "std"))] +pub use formatting::{format, format_item, DelayedFormat}; +#[cfg(feature = "unstable-locales")] +pub use formatting::{format_item_localized, format_localized}; +#[cfg(any(feature = "alloc", feature = "std"))] +pub(crate) use formatting::{write_rfc2822, write_rfc3339}; pub use parse::{parse, parse_and_remainder}; pub use parsed::Parsed; /// L10n locales. @@ -275,13 +280,49 @@ enum InternalInternal { Nanosecond9NoDot, } -#[cfg(any(feature = "alloc", feature = "std", test))] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum Colons { +/// Type for specifying the format of UTC offsets. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct OffsetFormat { + /// See `OffsetPrecision`. + pub precision: OffsetPrecision, + /// Separator between hours, minutes and seconds. + pub colons: Colons, + /// Represent `+00:00` as `Z`. + pub allow_zulu: bool, + /// Pad the hour value to two digits. + pub padding: Pad, +} + +/// The precision of an offset from UTC formatting item. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OffsetPrecision { + /// Format offset from UTC as only hours. Not recommended, it is not uncommon for timezones to + /// have an offset of 30 minutes, 15 minutes, etc. + /// Any minutes and seconds get truncated. + Hours, + /// Format offset from UTC as hours and minutes. + /// Any seconds will be rounded to the nearest minute. + Minutes, + /// Format offset from UTC as hours, minutes and seconds. + Seconds, + /// Format offset from UTC as hours, and optionally with minutes. + /// Any seconds will be rounded to the nearest minute. + OptionalMinutes, + /// Format offset from UTC as hours and minutes, and optionally seconds. + OptionalSeconds, + /// Format offset from UTC as hours and optionally minutes and seconds. + OptionalMinutesAndSeconds, +} + +/// The separator between hours and minutes in an offset. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Colons { + /// No separator None, - Single, - Double, - Triple, + /// Colon (`:`) as separator + Colon, + /// No separator when formatting, colon allowed when parsing. + Maybe, } /// A single formatting item. This is used for both formatting and parsing. @@ -290,13 +331,13 @@ pub enum Item<'a> { /// A literally printed and parsed text. Literal(&'a str), /// Same as `Literal` but with the string owned by the item. - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] OwnedLiteral(Box), /// Whitespace. Prints literally but reads zero or more whitespace. Space(&'a str), /// Same as `Space` but with the string owned by the item. - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] OwnedSpace(Box), /// Numeric item. Can be optionally padded to the maximal length (if any) when formatting; @@ -308,40 +349,24 @@ pub enum Item<'a> { Error, } -macro_rules! lit { - ($x:expr) => { - Item::Literal($x) - }; +const fn num(numeric: Numeric) -> Item<'static> { + Item::Numeric(numeric, Pad::None) } -macro_rules! sp { - ($x:expr) => { - Item::Space($x) - }; -} -macro_rules! num { - ($x:ident) => { - Item::Numeric(Numeric::$x, Pad::None) - }; -} -macro_rules! num0 { - ($x:ident) => { - Item::Numeric(Numeric::$x, Pad::Zero) - }; + +const fn num0(numeric: Numeric) -> Item<'static> { + Item::Numeric(numeric, Pad::Zero) } -macro_rules! nums { - ($x:ident) => { - Item::Numeric(Numeric::$x, Pad::Space) - }; + +const fn nums(numeric: Numeric) -> Item<'static> { + Item::Numeric(numeric, Pad::Space) } -macro_rules! fix { - ($x:ident) => { - Item::Fixed(Fixed::$x) - }; + +const fn fixed(fixed: Fixed) -> Item<'static> { + Item::Fixed(fixed) } -macro_rules! internal_fix { - ($x:ident) => { - Item::Fixed(Fixed::Internal(InternalFixed { val: InternalInternal::$x })) - }; + +const fn internal_fixed(val: InternalInternal) -> Item<'static> { + Item::Fixed(Fixed::Internal(InternalFixed { val })) } /// An error from the `parse` function. @@ -405,7 +430,7 @@ impl fmt::Display for ParseError { } } -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl Error for ParseError { #[allow(deprecated)] @@ -415,7 +440,7 @@ impl Error for ParseError { } // to be used in this module and submodules -const OUT_OF_RANGE: ParseError = ParseError(ParseErrorKind::OutOfRange); +pub(crate) const OUT_OF_RANGE: ParseError = ParseError(ParseErrorKind::OutOfRange); const IMPOSSIBLE: ParseError = ParseError(ParseErrorKind::Impossible); const NOT_ENOUGH: ParseError = ParseError(ParseErrorKind::NotEnough); const INVALID: ParseError = ParseError(ParseErrorKind::Invalid); @@ -423,531 +448,6 @@ const TOO_SHORT: ParseError = ParseError(ParseErrorKind::TooShort); const TOO_LONG: ParseError = ParseError(ParseErrorKind::TooLong); const BAD_FORMAT: ParseError = ParseError(ParseErrorKind::BadFormat); -#[cfg(any(feature = "alloc", feature = "std", test))] -struct Locales { - short_months: &'static [&'static str], - long_months: &'static [&'static str], - short_weekdays: &'static [&'static str], - long_weekdays: &'static [&'static str], - am_pm: &'static [&'static str], -} - -#[cfg(any(feature = "alloc", feature = "std", test))] -impl Locales { - fn new(_locale: Option) -> Self { - #[cfg(feature = "unstable-locales")] - { - let locale = _locale.unwrap_or(Locale::POSIX); - Self { - short_months: locales::short_months(locale), - long_months: locales::long_months(locale), - short_weekdays: locales::short_weekdays(locale), - long_weekdays: locales::long_weekdays(locale), - am_pm: locales::am_pm(locale), - } - } - #[cfg(not(feature = "unstable-locales"))] - Self { - short_months: &[ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", - ], - long_months: &[ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ], - short_weekdays: &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], - long_weekdays: &[ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ], - am_pm: &["AM", "PM"], - } - } -} - -/// Formats single formatting item -#[cfg(any(feature = "alloc", feature = "std", test))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] -pub fn format_item( - w: &mut fmt::Formatter, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - item: &Item<'_>, -) -> fmt::Result { - let mut result = String::new(); - format_inner(&mut result, date, time, off, item, None)?; - w.pad(&result) -} - -#[cfg(any(feature = "alloc", feature = "std", test))] -fn format_inner( - result: &mut String, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - item: &Item<'_>, - locale: Option, -) -> fmt::Result { - let locale = Locales::new(locale); - - match *item { - Item::Literal(s) | Item::Space(s) => result.push_str(s), - #[cfg(any(feature = "alloc", feature = "std", test))] - Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => result.push_str(s), - - Item::Numeric(ref spec, ref pad) => { - use self::Numeric::*; - - let week_from_sun = |d: &NaiveDate| d.weeks_from(Weekday::Sun); - let week_from_mon = |d: &NaiveDate| d.weeks_from(Weekday::Mon); - - let (width, v) = match *spec { - Year => (4, date.map(|d| i64::from(d.year()))), - YearDiv100 => (2, date.map(|d| i64::from(d.year()).div_euclid(100))), - YearMod100 => (2, date.map(|d| i64::from(d.year()).rem_euclid(100))), - IsoYear => (4, date.map(|d| i64::from(d.iso_week().year()))), - IsoYearDiv100 => (2, date.map(|d| i64::from(d.iso_week().year()).div_euclid(100))), - IsoYearMod100 => (2, date.map(|d| i64::from(d.iso_week().year()).rem_euclid(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).timestamp()), - (Some(d), Some(t), Some(&(_, off))) => { - Some((d.and_time(*t) - off).timestamp()) - } - (_, _, _) => None, - }, - ), - - // for the future expansion - Internal(ref int) => match int._dummy {}, - }; - - 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!(result, "{:+}", v), - Pad::Zero => write!(result, "{:+01$}", v, width + 1), - Pad::Space => write!(result, "{:+1$}", v, width + 1), - } - } else { - match *pad { - Pad::None => write!(result, "{}", v), - Pad::Zero => write!(result, "{:01$}", v, width), - Pad::Space => write!(result, "{:1$}", v, width), - } - }? - } else { - return Err(fmt::Error); // insufficient arguments for given format - } - } - - Item::Fixed(ref spec) => { - use self::Fixed::*; - - let ret = - match *spec { - ShortMonthName => date.map(|d| { - result.push_str(locale.short_months[d.month0() as usize]); - Ok(()) - }), - LongMonthName => date.map(|d| { - result.push_str(locale.long_months[d.month0() as usize]); - Ok(()) - }), - ShortWeekdayName => date.map(|d| { - result.push_str( - locale.short_weekdays[d.weekday().num_days_from_sunday() as usize], - ); - Ok(()) - }), - LongWeekdayName => date.map(|d| { - result.push_str( - locale.long_weekdays[d.weekday().num_days_from_sunday() as usize], - ); - Ok(()) - }), - LowerAmPm => time.map(|t| { - let ampm = if t.hour12().0 { locale.am_pm[1] } else { locale.am_pm[0] }; - for char in ampm.chars() { - result.extend(char.to_lowercase()) - } - Ok(()) - }), - UpperAmPm => time.map(|t| { - result.push_str(if t.hour12().0 { - locale.am_pm[1] - } else { - locale.am_pm[0] - }); - Ok(()) - }), - Nanosecond => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - if nano == 0 { - Ok(()) - } else if nano % 1_000_000 == 0 { - write!(result, ".{:03}", nano / 1_000_000) - } else if nano % 1_000 == 0 { - write!(result, ".{:06}", nano / 1_000) - } else { - write!(result, ".{:09}", nano) - } - }), - Nanosecond3 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, ".{:03}", nano / 1_000_000) - }), - Nanosecond6 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, ".{:06}", nano / 1_000) - }), - Nanosecond9 => time.map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, ".{:09}", nano) - }), - Internal(InternalFixed { val: InternalInternal::Nanosecond3NoDot }) => time - .map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, "{:03}", nano / 1_000_000) - }), - Internal(InternalFixed { val: InternalInternal::Nanosecond6NoDot }) => time - .map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, "{:06}", nano / 1_000) - }), - Internal(InternalFixed { val: InternalInternal::Nanosecond9NoDot }) => time - .map(|t| { - let nano = t.nanosecond() % 1_000_000_000; - write!(result, "{:09}", nano) - }), - TimezoneName => off.map(|(name, _)| { - result.push_str(name); - Ok(()) - }), - TimezoneOffsetColon => off - .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Single)), - TimezoneOffsetDoubleColon => off - .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Double)), - TimezoneOffsetTripleColon => off - .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Triple)), - TimezoneOffsetColonZ => off - .map(|&(_, off)| write_local_minus_utc(result, off, true, Colons::Single)), - TimezoneOffset => { - off.map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::None)) - } - TimezoneOffsetZ => { - off.map(|&(_, off)| write_local_minus_utc(result, off, true, Colons::None)) - } - Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { - panic!("Do not try to write %#z it is undefined") - } - RFC2822 => - // same as `%a, %d %b %Y %H:%M:%S %z` - { - if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { - Some(write_rfc2822_inner(result, d, t, off, locale)) - } else { - None - } - } - RFC3339 => - // same as `%Y-%m-%dT%H:%M:%S%.f%:z` - { - if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { - Some(write_rfc3339(result, crate::NaiveDateTime::new(*d, *t), off)) - } else { - None - } - } - }; - - match ret { - Some(ret) => ret?, - None => return Err(fmt::Error), // insufficient arguments for given format - } - } - - Item::Error => return Err(fmt::Error), - } - Ok(()) -} - -/// 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. -#[cfg(any(feature = "alloc", feature = "std", test))] -fn write_local_minus_utc( - result: &mut String, - off: FixedOffset, - allow_zulu: bool, - colon_type: Colons, -) -> fmt::Result { - let off = off.local_minus_utc(); - if allow_zulu && off == 0 { - result.push('Z'); - return Ok(()); - } - let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; - result.push(sign); - - write_hundreds(result, (off / 3600) as u8)?; - - match colon_type { - Colons::None => write_hundreds(result, (off / 60 % 60) as u8), - Colons::Single => { - result.push(':'); - write_hundreds(result, (off / 60 % 60) as u8) - } - Colons::Double => { - result.push(':'); - write_hundreds(result, (off / 60 % 60) as u8)?; - result.push(':'); - write_hundreds(result, (off % 60) as u8) - } - Colons::Triple => Ok(()), - } -} - -/// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z` -#[cfg(any(feature = "alloc", feature = "std", test))] -pub(crate) fn write_rfc3339( - result: &mut String, - dt: crate::NaiveDateTime, - off: FixedOffset, -) -> fmt::Result { - // reuse `Debug` impls which already print ISO 8601 format. - // this is faster in this way. - write!(result, "{:?}", dt)?; - write_local_minus_utc(result, off, false, Colons::Single) -} - -#[cfg(any(feature = "alloc", feature = "std", test))] -/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` -pub(crate) fn write_rfc2822( - result: &mut String, - dt: crate::NaiveDateTime, - off: FixedOffset, -) -> fmt::Result { - write_rfc2822_inner(result, &dt.date(), &dt.time(), off, Locales::new(None)) -} - -#[cfg(any(feature = "alloc", feature = "std", test))] -/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` -fn write_rfc2822_inner( - result: &mut String, - d: &NaiveDate, - t: &NaiveTime, - off: FixedOffset, - locale: Locales, -) -> fmt::Result { - let year = d.year(); - // RFC2822 is only defined on years 0 through 9999 - if !(0..=9999).contains(&year) { - return Err(fmt::Error); - } - - result.push_str(locale.short_weekdays[d.weekday().num_days_from_sunday() as usize]); - result.push_str(", "); - write_hundreds(result, d.day() as u8)?; - result.push(' '); - result.push_str(locale.short_months[d.month0() as usize]); - result.push(' '); - write_hundreds(result, (year / 100) as u8)?; - write_hundreds(result, (year % 100) as u8)?; - result.push(' '); - write_hundreds(result, t.hour() as u8)?; - result.push(':'); - write_hundreds(result, t.minute() as u8)?; - result.push(':'); - let sec = t.second() + t.nanosecond() / 1_000_000_000; - write_hundreds(result, sec as u8)?; - result.push(' '); - write_local_minus_utc(result, off, false, Colons::None) -} - -/// Equivalent to `{:02}` formatting for n < 100. -pub(crate) fn write_hundreds(w: &mut impl Write, n: u8) -> fmt::Result { - if n >= 100 { - return Err(fmt::Error); - } - - let tens = b'0' + n / 10; - let ones = b'0' + n % 10; - w.write_char(tens as char)?; - w.write_char(ones as char) -} - -/// Tries to format given arguments with given formatting items. -/// Internally used by `DelayedFormat`. -#[cfg(any(feature = "alloc", feature = "std", test))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] -pub fn format<'a, I, B>( - w: &mut fmt::Formatter, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - items: I, -) -> 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(), None)?; - } - w.pad(&result) -} - -mod parsed; - -// due to the size of parsing routines, they are in separate modules. -mod parse; -mod scan; - -pub mod strftime; - -/// A *temporary* object which can be used as an argument to `format!` or others. -/// This is normally constructed via `format` methods of each date and time type. -#[cfg(any(feature = "alloc", feature = "std", test))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] -#[derive(Debug)] -pub struct DelayedFormat { - /// The date view, if any. - date: Option, - /// The time view, if any. - time: Option, - /// The name and local-to-UTC difference for the offset (timezone), if any. - off: Option<(String, FixedOffset)>, - /// An iterator returning formatting items. - items: I, - /// Locale used for text. - // TODO: Only used with the locale feature. We should make this property - // only present when the feature is enabled. - #[cfg(feature = "unstable-locales")] - 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. - #[must_use] - pub fn new(date: Option, time: Option, items: I) -> DelayedFormat { - DelayedFormat { - date, - time, - off: None, - items, - #[cfg(feature = "unstable-locales")] - locale: None, - } - } - - /// Makes a new `DelayedFormat` value out of local date and time and UTC offset. - #[must_use] - pub fn new_with_offset( - date: Option, - time: Option, - offset: &Off, - items: I, - ) -> DelayedFormat - where - Off: Offset + fmt::Display, - { - let name_and_diff = (offset.to_string(), offset.fix()); - DelayedFormat { - date, - time, - off: Some(name_and_diff), - items, - #[cfg(feature = "unstable-locales")] - locale: None, - } - } - - /// Makes a new `DelayedFormat` value out of local date and time and locale. - #[cfg(feature = "unstable-locales")] - #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] - #[must_use] - pub fn new_with_locale( - date: Option, - time: Option, - items: I, - locale: Locale, - ) -> DelayedFormat { - DelayedFormat { date, time, off: None, items, locale: Some(locale) } - } - - /// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale. - #[cfg(feature = "unstable-locales")] - #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] - #[must_use] - 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, time, off: Some(name_and_diff), 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()) - } -} - // this implementation is here only because we need some private code from `scan` /// Parsing a `str` into a `Weekday` uses the format [`%W`](./format/strftime/index.html). @@ -986,45 +486,6 @@ impl FromStr for Weekday { } } -/// Formats single formatting item -#[cfg(feature = "unstable-locales")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] -pub fn format_item_localized( - w: &mut fmt::Formatter, - date: Option<&NaiveDate>, - time: Option<&NaiveTime>, - off: Option<&(String, FixedOffset)>, - item: &Item<'_>, - 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")] -#[cfg_attr(docsrs, doc(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/parse.rs b/src/format/parse.rs index 48ed4d70cb..90688ce677 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -4,8 +4,6 @@ //! Date and time parsing routines. -#![allow(deprecated)] - use core::borrow::Borrow; use core::str; use core::usize; @@ -104,7 +102,7 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st // since we do not directly go to a `DateTime` so one can recover // the offset information from `Parsed` anyway. - s = s.trim_left(); + s = s.trim_start(); if let Ok((s_, weekday)) = scan::short_weekday(s) { if !s_.starts_with(',') { @@ -114,7 +112,7 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st parsed.set_weekday(weekday)?; } - s = s.trim_left(); + s = s.trim_start(); parsed.set_day(try_consume!(scan::number(s, 1, 2)))?; s = scan::space(s)?; // mandatory parsed.set_month(1 + i64::from(try_consume!(scan::short_month0(s))))?; @@ -140,9 +138,9 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st s = scan::space(s)?; // mandatory parsed.set_hour(try_consume!(scan::number(s, 2, 2)))?; - s = scan::char(s.trim_left(), b':')?.trim_left(); // *S ":" *S + s = scan::char(s.trim_start(), b':')?.trim_start(); // *S ":" *S parsed.set_minute(try_consume!(scan::number(s, 2, 2)))?; - if let Ok(s_) = scan::char(s.trim_left(), b':') { + if let Ok(s_) = scan::char(s.trim_start(), b':') { // [ ":" *S 2DIGIT ] parsed.set_second(try_consume!(scan::number(s_, 2, 2)))?; } @@ -219,7 +217,7 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st parsed.set_nanosecond(nanosecond)?; } - let offset = try_consume!(scan::timezone_offset_zulu(s, |s| scan::char(s, b':'))); + let offset = try_consume!(scan::timezone_offset(s, |s| scan::char(s, b':'), true, false, true)); if offset <= -86_400 || offset >= 86_400 { return Err(OUT_OF_RANGE); } @@ -315,7 +313,7 @@ where s = &s[prefix.len()..]; } - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] Item::OwnedLiteral(ref prefix) => { if s.len() < prefix.len() { return Err((s, TOO_SHORT)); @@ -342,7 +340,7 @@ where } } - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] Item::OwnedSpace(ref item_space) => { for expect in item_space.chars() { let actual = match s.chars().next() { @@ -474,7 +472,7 @@ where } &TimezoneName => { - try_consume!(scan::timezone_name_skip(s)); + try_consume!(Ok((s.trim_start_matches(|c: char| !c.is_whitespace()), ()))); } &TimezoneOffsetColon @@ -482,25 +480,37 @@ where | &TimezoneOffsetTripleColon | &TimezoneOffset => { s = scan::trim1(s); - let offset = - try_consume!(scan::timezone_offset(s, scan::consume_colon_maybe)); + let offset = try_consume!(scan::timezone_offset( + s, + scan::consume_colon_maybe, + false, + false, + true, + )); parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } &TimezoneOffsetColonZ | &TimezoneOffsetZ => { s = scan::trim1(s); - let offset = - try_consume!(scan::timezone_offset_zulu(s, scan::consume_colon_maybe)); + let offset = try_consume!(scan::timezone_offset( + s, + scan::consume_colon_maybe, + true, + false, + true, + )); parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } - &Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive, }) => { s = scan::trim1(s); - let offset = try_consume!(scan::timezone_offset_permissive( + let offset = try_consume!(scan::timezone_offset( s, - scan::consume_colon_maybe + scan::consume_colon_maybe, + true, + true, + true, )); parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } @@ -572,1010 +582,1265 @@ impl str::FromStr for DateTime { } #[cfg(test)] -#[test] -fn test_parse() { - use super::*; +mod tests { + use crate::format::*; + use crate::{DateTime, FixedOffset, TimeZone, Timelike, Utc}; + + macro_rules! parsed { + ($($k:ident: $v:expr),*) => (#[allow(unused_mut)] { + let mut expected = Parsed::new(); + $(expected.$k = Some($v);)* + Ok(expected) + }); + } - // workaround for Rust issue #22255 - fn parse_all(s: &str, items: &[Item]) -> ParseResult { - let mut parsed = Parsed::new(); - parse(&mut parsed, s, items.iter())?; - Ok(parsed) + #[test] + fn test_parse_whitespace_and_literal() { + use crate::format::Item::{Literal, Space}; + + // empty string + parses("", &[]); + check(" ", &[], Err(TOO_LONG)); + check("a", &[], Err(TOO_LONG)); + check("abc", &[], Err(TOO_LONG)); + check("🀠", &[], Err(TOO_LONG)); + + // whitespaces + parses("", &[Space("")]); + parses(" ", &[Space(" ")]); + parses(" ", &[Space(" ")]); + parses(" ", &[Space(" ")]); + check(" ", &[Space("")], Err(TOO_LONG)); + check(" ", &[Space(" ")], Err(TOO_LONG)); + check(" ", &[Space(" ")], Err(TOO_LONG)); + check(" ", &[Space(" ")], Err(TOO_LONG)); + check("", &[Space(" ")], Err(TOO_SHORT)); + check(" ", &[Space(" ")], Err(TOO_SHORT)); + check(" ", &[Space(" ")], Err(TOO_SHORT)); + check(" ", &[Space(" "), Space(" ")], Err(TOO_SHORT)); + check(" ", &[Space(" "), Space(" ")], Err(TOO_SHORT)); + parses(" ", &[Space(" "), Space(" ")]); + parses(" ", &[Space(" "), Space(" ")]); + parses(" ", &[Space(" "), Space(" ")]); + parses(" ", &[Space(" "), Space(" "), Space(" ")]); + check("\t", &[Space("")], Err(TOO_LONG)); + check(" \n\r \n", &[Space("")], Err(TOO_LONG)); + parses("\t", &[Space("\t")]); + check("\t", &[Space(" ")], Err(INVALID)); + check(" ", &[Space("\t")], Err(INVALID)); + parses("\t\r", &[Space("\t\r")]); + parses("\t\r ", &[Space("\t\r ")]); + parses("\t \r", &[Space("\t \r")]); + parses(" \t\r", &[Space(" \t\r")]); + parses(" \n\r \n", &[Space(" \n\r \n")]); + check(" \t\n", &[Space(" \t")], Err(TOO_LONG)); + check(" \n\t", &[Space(" \t\n")], Err(INVALID)); + parses("\u{2002}", &[Space("\u{2002}")]); + // most unicode whitespace characters + parses( + "\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}", + &[Space("\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}")] + ); + // most unicode whitespace characters + parses( + "\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}", + &[ + Space("\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}"), + Space("\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}") + ] + ); + check("a", &[Space("")], Err(TOO_LONG)); + check("a", &[Space(" ")], Err(INVALID)); + // a Space containing a literal can match a literal, but this should not be done + parses("a", &[Space("a")]); + check("abc", &[Space("")], Err(TOO_LONG)); + check("abc", &[Space(" ")], Err(INVALID)); + check(" abc", &[Space("")], Err(TOO_LONG)); + check(" abc", &[Space(" ")], Err(TOO_LONG)); + + // `\u{0363}` is combining diacritic mark "COMBINING LATIN SMALL LETTER A" + + // literal + parses("", &[Literal("")]); + check("", &[Literal("a")], Err(TOO_SHORT)); + check(" ", &[Literal("a")], Err(INVALID)); + parses("a", &[Literal("a")]); + parses("+", &[Literal("+")]); + parses("-", &[Literal("-")]); + parses("βˆ’", &[Literal("βˆ’")]); // MINUS SIGN (U+2212) + parses(" ", &[Literal(" ")]); // a Literal may contain whitespace and match whitespace + check("aa", &[Literal("a")], Err(TOO_LONG)); + check("🀠", &[Literal("a")], Err(INVALID)); + check("A", &[Literal("a")], Err(INVALID)); + check("a", &[Literal("z")], Err(INVALID)); + check("a", &[Literal("🀠")], Err(TOO_SHORT)); + check("a", &[Literal("\u{0363}a")], Err(TOO_SHORT)); + check("\u{0363}a", &[Literal("a")], Err(INVALID)); + parses("\u{0363}a", &[Literal("\u{0363}a")]); + check("a", &[Literal("ab")], Err(TOO_SHORT)); + parses("xy", &[Literal("xy")]); + parses("xy", &[Literal("x"), Literal("y")]); + parses("1", &[Literal("1")]); + parses("1234", &[Literal("1234")]); + parses("+1234", &[Literal("+1234")]); + parses("-1234", &[Literal("-1234")]); + parses("βˆ’1234", &[Literal("βˆ’1234")]); // MINUS SIGN (U+2212) + parses("PST", &[Literal("PST")]); + parses("🀠", &[Literal("🀠")]); + parses("🀠a", &[Literal("🀠"), Literal("a")]); + parses("🀠a🀠", &[Literal("🀠"), Literal("a🀠")]); + parses("a🀠b", &[Literal("a"), Literal("🀠"), Literal("b")]); + // literals can be together + parses("xy", &[Literal("xy")]); + parses("xyz", &[Literal("xyz")]); + // or literals can be apart + parses("xy", &[Literal("x"), Literal("y")]); + parses("xyz", &[Literal("x"), Literal("yz")]); + parses("xyz", &[Literal("xy"), Literal("z")]); + parses("xyz", &[Literal("x"), Literal("y"), Literal("z")]); + // + check("x y", &[Literal("x"), Literal("y")], Err(INVALID)); + parses("xy", &[Literal("x"), Space(""), Literal("y")]); + check("x y", &[Literal("x"), Space(""), Literal("y")], Err(INVALID)); + parses("x y", &[Literal("x"), Space(" "), Literal("y")]); + + // whitespaces + literals + parses("a\n", &[Literal("a"), Space("\n")]); + parses("\tab\n", &[Space("\t"), Literal("ab"), Space("\n")]); + parses( + "ab\tcd\ne", + &[Literal("ab"), Space("\t"), Literal("cd"), Space("\n"), Literal("e")], + ); + parses( + "+1ab\tcd\r\n+,.", + &[Literal("+1ab"), Space("\t"), Literal("cd"), Space("\r\n"), Literal("+,.")], + ); + // whitespace and literals can be intermixed + parses("a\tb", &[Literal("a\tb")]); + parses("a\tb", &[Literal("a"), Space("\t"), Literal("b")]); } - macro_rules! check { - ($fmt:expr, $items:expr; $err:tt) => ( - eprintln!("test_parse: format {:?}", $fmt); - assert_eq!(parse_all($fmt, &$items), Err($err)) + #[test] + fn test_parse_numeric() { + use crate::format::Item::{Literal, Space}; + use crate::format::Numeric::*; + + // numeric + check("1987", &[num(Year)], parsed!(year: 1987)); + check("1987 ", &[num(Year)], Err(TOO_LONG)); + check("0x12", &[num(Year)], Err(TOO_LONG)); // `0` is parsed + check("x123", &[num(Year)], Err(INVALID)); + check("o123", &[num(Year)], Err(INVALID)); + check("2015", &[num(Year)], parsed!(year: 2015)); + check("0000", &[num(Year)], parsed!(year: 0)); + check("9999", &[num(Year)], parsed!(year: 9999)); + check(" \t987", &[num(Year)], Err(INVALID)); + check(" \t987", &[Space(" \t"), num(Year)], parsed!(year: 987)); + check(" \t987🀠", &[Space(" \t"), num(Year), Literal("🀠")], parsed!(year: 987)); + check("987🀠", &[num(Year), Literal("🀠")], parsed!(year: 987)); + check("5", &[num(Year)], parsed!(year: 5)); + check("5\0", &[num(Year)], Err(TOO_LONG)); + check("\x005", &[num(Year)], Err(INVALID)); + check("", &[num(Year)], Err(TOO_SHORT)); + check("12345", &[num(Year), Literal("5")], parsed!(year: 1234)); + check("12345", &[nums(Year), Literal("5")], parsed!(year: 1234)); + check("12345", &[num0(Year), Literal("5")], parsed!(year: 1234)); + check("12341234", &[num(Year), num(Year)], parsed!(year: 1234)); + check("1234 1234", &[num(Year), num(Year)], Err(INVALID)); + check("1234 1234", &[num(Year), Space(" "), num(Year)], parsed!(year: 1234)); + check("1234 1235", &[num(Year), num(Year)], Err(INVALID)); + check("1234 1234", &[num(Year), Literal("x"), num(Year)], Err(INVALID)); + check("1234x1234", &[num(Year), Literal("x"), num(Year)], parsed!(year: 1234)); + check("1234 x 1234", &[num(Year), Literal("x"), num(Year)], Err(INVALID)); + check("1234xx1234", &[num(Year), Literal("x"), num(Year)], Err(INVALID)); + check("1234xx1234", &[num(Year), Literal("xx"), num(Year)], parsed!(year: 1234)); + check( + "1234 x 1234", + &[num(Year), Space(" "), Literal("x"), Space(" "), num(Year)], + parsed!(year: 1234), + ); + check( + "1234 x 1235", + &[num(Year), Space(" "), Literal("x"), Space(" "), Literal("1235")], + parsed!(year: 1234), + ); + + // signed numeric + check("-42", &[num(Year)], parsed!(year: -42)); + check("+42", &[num(Year)], parsed!(year: 42)); + check("-0042", &[num(Year)], parsed!(year: -42)); + check("+0042", &[num(Year)], parsed!(year: 42)); + check("-42195", &[num(Year)], parsed!(year: -42195)); + check("βˆ’42195", &[num(Year)], Err(INVALID)); // MINUS SIGN (U+2212) + check("+42195", &[num(Year)], parsed!(year: 42195)); + check(" -42195", &[num(Year)], Err(INVALID)); + check(" +42195", &[num(Year)], Err(INVALID)); + check(" -42195", &[num(Year)], Err(INVALID)); + check(" +42195", &[num(Year)], Err(INVALID)); + check("-42195 ", &[num(Year)], Err(TOO_LONG)); + check("+42195 ", &[num(Year)], Err(TOO_LONG)); + check(" - 42", &[num(Year)], Err(INVALID)); + check(" + 42", &[num(Year)], Err(INVALID)); + check(" -42195", &[Space(" "), num(Year)], parsed!(year: -42195)); + check(" βˆ’42195", &[Space(" "), num(Year)], Err(INVALID)); // MINUS SIGN (U+2212) + check(" +42195", &[Space(" "), num(Year)], parsed!(year: 42195)); + check(" - 42", &[Space(" "), num(Year)], Err(INVALID)); + check(" + 42", &[Space(" "), num(Year)], Err(INVALID)); + check("-", &[num(Year)], Err(TOO_SHORT)); + check("+", &[num(Year)], Err(TOO_SHORT)); + + // unsigned numeric + check("345", &[num(Ordinal)], parsed!(ordinal: 345)); + check("+345", &[num(Ordinal)], Err(INVALID)); + check("-345", &[num(Ordinal)], Err(INVALID)); + check(" 345", &[num(Ordinal)], Err(INVALID)); + check("βˆ’345", &[num(Ordinal)], Err(INVALID)); // MINUS SIGN (U+2212) + check("345 ", &[num(Ordinal)], Err(TOO_LONG)); + check(" 345", &[Space(" "), num(Ordinal)], parsed!(ordinal: 345)); + check("345 ", &[num(Ordinal), Space(" ")], parsed!(ordinal: 345)); + check("345🀠 ", &[num(Ordinal), Literal("🀠"), Space(" ")], parsed!(ordinal: 345)); + check("345🀠", &[num(Ordinal)], Err(TOO_LONG)); + check("\u{0363}345", &[num(Ordinal)], Err(INVALID)); + check(" +345", &[num(Ordinal)], Err(INVALID)); + check(" -345", &[num(Ordinal)], Err(INVALID)); + check("\t345", &[Space("\t"), num(Ordinal)], parsed!(ordinal: 345)); + check(" +345", &[Space(" "), num(Ordinal)], Err(INVALID)); + check(" -345", &[Space(" "), num(Ordinal)], Err(INVALID)); + + const S: Item = Space(" "); + // various numeric fields + check("1234 5678", &[num(Year), S, num(IsoYear)], parsed!(year: 1234, isoyear: 5678)); + check("1234 5678", &[num(Year), S, num(IsoYear)], parsed!(year: 1234, isoyear: 5678)); + check( + "12 34 56 78", + &[num(YearDiv100), S, num(YearMod100), S, num(IsoYearDiv100), S, num(IsoYearMod100)], + parsed!(year_div_100: 12, year_mod_100: 34, isoyear_div_100: 56, isoyear_mod_100: 78), + ); + check( + "1 2 3 45", + &[num(Month), S, num(Day), S, num(WeekFromSun), S, num(NumDaysFromSun), num(IsoWeek)], + parsed!(month: 1, day: 2, week_from_sun: 3, weekday: Weekday::Thu, isoweek: 5), + ); + check( + "6 7 89 01", + &[num(WeekFromMon), S, num(WeekdayFromMon), S, num(Ordinal), S, num(Hour12)], + parsed!(week_from_mon: 6, weekday: Weekday::Sun, ordinal: 89, hour_mod_12: 1), + ); + check( + "23 45 6 78901234 567890123", + &[num(Hour), S, num(Minute), S, num(Second), S, num(Nanosecond), S, num(Timestamp)], + parsed!(hour_div_12: 1, hour_mod_12: 11, minute: 45, second: 6, nanosecond: 78_901_234, timestamp: 567_890_123), ); - ($fmt:expr, $items:expr; $($k:ident: $v:expr),*) => ({ - eprintln!("test_parse: format {:?}", $fmt); - let expected = Parsed { - $($k: Some($v),)* - ..Default::default() - }; - assert_eq!(parse_all($fmt, &$items), Ok(expected)) - }); } - // empty string - check!("", []; ); - check!(" ", []; TOO_LONG); - check!("a", []; TOO_LONG); - check!("abc", []; TOO_LONG); - check!("🀠", []; TOO_LONG); - - // whitespaces - check!("", [sp!("")]; ); - check!(" ", [sp!(" ")]; ); - check!(" ", [sp!(" ")]; ); - check!(" ", [sp!(" ")]; ); - check!(" ", [sp!("")]; TOO_LONG); - check!(" ", [sp!(" ")]; TOO_LONG); - check!(" ", [sp!(" ")]; TOO_LONG); - check!(" ", [sp!(" ")]; TOO_LONG); - check!("", [sp!(" ")]; TOO_SHORT); - check!(" ", [sp!(" ")]; TOO_SHORT); - check!(" ", [sp!(" ")]; TOO_SHORT); - check!(" ", [sp!(" "), sp!(" ")]; TOO_SHORT); - check!(" ", [sp!(" "), sp!(" ")]; TOO_SHORT); - check!(" ", [sp!(" "), sp!(" ")]; ); - check!(" ", [sp!(" "), sp!(" ")]; ); - check!(" ", [sp!(" "), sp!(" ")]; ); - check!(" ", [sp!(" "), sp!(" "), sp!(" ")]; ); - check!("\t", [sp!("")]; TOO_LONG); - check!(" \n\r \n", [sp!("")]; TOO_LONG); - check!("\t", [sp!("\t")]; ); - check!("\t", [sp!(" ")]; INVALID); - check!(" ", [sp!("\t")]; INVALID); - check!("\t\r", [sp!("\t\r")]; ); - check!("\t\r ", [sp!("\t\r ")]; ); - check!("\t \r", [sp!("\t \r")]; ); - check!(" \t\r", [sp!(" \t\r")]; ); - check!(" \n\r \n", [sp!(" \n\r \n")]; ); - check!(" \t\n", [sp!(" \t")]; TOO_LONG); - check!(" \n\t", [sp!(" \t\n")]; INVALID); - check!("\u{2002}", [sp!("\u{2002}")]; ); - // most unicode whitespace characters - check!( - "\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}", - [sp!("\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}")]; - ); - // most unicode whitespace characters - check!( - "\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}", - [ - sp!("\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}"), - sp!("\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{3000}") - ]; - ); - check!("a", [sp!("")]; TOO_LONG); - check!("a", [sp!(" ")]; INVALID); - // a Space containing a literal can match a literal, but this should not be done - check!("a", [sp!("a")]; ); - check!("abc", [sp!("")]; TOO_LONG); - check!("abc", [sp!(" ")]; INVALID); - check!(" abc", [sp!("")]; TOO_LONG); - check!(" abc", [sp!(" ")]; TOO_LONG); - - // `\u{0363}` is combining diacritic mark "COMBINING LATIN SMALL LETTER A" - - // literal - check!("", [lit!("")]; ); - check!("", [lit!("a")]; TOO_SHORT); - check!(" ", [lit!("a")]; INVALID); - check!("a", [lit!("a")]; ); - // a Literal may contain whitespace and match whitespace, but this should not be done - check!(" ", [lit!(" ")]; ); - check!("aa", [lit!("a")]; TOO_LONG); - check!("🀠", [lit!("a")]; INVALID); - check!("A", [lit!("a")]; INVALID); - check!("a", [lit!("z")]; INVALID); - check!("a", [lit!("🀠")]; TOO_SHORT); - check!("a", [lit!("\u{0363}a")]; TOO_SHORT); - check!("\u{0363}a", [lit!("a")]; INVALID); - check!("\u{0363}a", [lit!("\u{0363}a")]; ); - check!("a", [lit!("ab")]; TOO_SHORT); - check!("xy", [lit!("xy")]; ); - check!("xy", [lit!("x"), lit!("y")]; ); - check!("1", [lit!("1")]; ); - check!("1234", [lit!("1234")]; ); - check!("+1234", [lit!("+1234")]; ); - check!("PST", [lit!("PST")]; ); - check!("🀠", [lit!("🀠")]; ); - check!("🀠a", [lit!("🀠"), lit!("a")]; ); - check!("🀠a🀠", [lit!("🀠"), lit!("a🀠")]; ); - check!("a🀠b", [lit!("a"), lit!("🀠"), lit!("b")]; ); - // literals can be together - check!("xy", [lit!("xy")]; ); - check!("xyz", [lit!("xyz")]; ); - // or literals can be apart - check!("xy", [lit!("x"), lit!("y")]; ); - check!("xyz", [lit!("x"), lit!("yz")]; ); - check!("xyz", [lit!("xy"), lit!("z")]; ); - check!("xyz", [lit!("x"), lit!("y"), lit!("z")]; ); - // - check!("x y", [lit!("x"), lit!("y")]; INVALID); - check!("xy", [lit!("x"), sp!(""), lit!("y")]; ); - check!("x y", [lit!("x"), sp!(""), lit!("y")]; INVALID); - check!("x y", [lit!("x"), sp!(" "), lit!("y")]; ); - - // whitespaces + literals - check!("a\n", [lit!("a"), sp!("\n")]; ); - check!("\tab\n", [sp!("\t"), lit!("ab"), sp!("\n")]; ); - check!("ab\tcd\ne", [lit!("ab"), sp!("\t"), lit!("cd"), sp!("\n"), lit!("e")]; ); - check!("+1ab\tcd\r\n+,.", [lit!("+1ab"), sp!("\t"), lit!("cd"), sp!("\r\n"), lit!("+,.")]; ); - // whitespace and literals can be intermixed - check!("a\tb", [lit!("a\tb")]; ); - check!("a\tb", [lit!("a"), sp!("\t"), lit!("b")]; ); - - // numeric - check!("1987", [num!(Year)]; year: 1987); - check!("1987 ", [num!(Year)]; TOO_LONG); - check!("0x12", [num!(Year)]; TOO_LONG); // `0` is parsed - check!("x123", [num!(Year)]; INVALID); - check!("o123", [num!(Year)]; INVALID); - check!("2015", [num!(Year)]; year: 2015); - check!("0000", [num!(Year)]; year: 0); - check!("9999", [num!(Year)]; year: 9999); - check!(" \t987", [num!(Year)]; INVALID); - check!(" \t987", [sp!(" \t"), num!(Year)]; year: 987); - check!(" \t987🀠", [sp!(" \t"), num!(Year), lit!("🀠")]; year: 987); - check!("987🀠", [num!(Year), lit!("🀠")]; year: 987); - check!("5", [num!(Year)]; year: 5); - check!("5\0", [num!(Year)]; TOO_LONG); - check!("\x005", [num!(Year)]; INVALID); - check!("", [num!(Year)]; TOO_SHORT); - check!("12345", [num!(Year), lit!("5")]; year: 1234); - check!("12345", [nums!(Year), lit!("5")]; year: 1234); - check!("12345", [num0!(Year), lit!("5")]; year: 1234); - check!("12341234", [num!(Year), num!(Year)]; year: 1234); - check!("1234 1234", [num!(Year), num!(Year)]; INVALID); - check!("1234 1234", [num!(Year), sp!(" "), num!(Year)]; year: 1234); - check!("1234 1235", [num!(Year), num!(Year)]; INVALID); - check!("1234 1234", [num!(Year), lit!("x"), num!(Year)]; INVALID); - check!("1234x1234", [num!(Year), lit!("x"), num!(Year)]; year: 1234); - check!("1234 x 1234", [num!(Year), lit!("x"), num!(Year)]; INVALID); - check!("1234xx1234", [num!(Year), lit!("x"), num!(Year)]; INVALID); - check!("1234xx1234", [num!(Year), lit!("xx"), num!(Year)]; year: 1234); - check!("1234 x 1234", [num!(Year), sp!(" "), lit!("x"), sp!(" "), num!(Year)]; year: 1234); - check!("1234 x 1235", [num!(Year), sp!(" "), lit!("x"), sp!(" "), lit!("1235")]; year: 1234); - - // signed numeric - check!("-42", [num!(Year)]; year: -42); - check!("+42", [num!(Year)]; year: 42); - check!("-0042", [num!(Year)]; year: -42); - check!("+0042", [num!(Year)]; year: 42); - check!("-42195", [num!(Year)]; year: -42195); - check!("+42195", [num!(Year)]; year: 42195); - check!(" -42195", [num!(Year)]; INVALID); - check!(" +42195", [num!(Year)]; INVALID); - check!(" -42195", [num!(Year)]; INVALID); - check!(" +42195", [num!(Year)]; INVALID); - check!("-42195 ", [num!(Year)]; TOO_LONG); - check!("+42195 ", [num!(Year)]; TOO_LONG); - check!(" - 42", [num!(Year)]; INVALID); - check!(" + 42", [num!(Year)]; INVALID); - check!(" -42195", [sp!(" "), num!(Year)]; year: -42195); - check!(" +42195", [sp!(" "), num!(Year)]; year: 42195); - check!(" - 42", [sp!(" "), num!(Year)]; INVALID); - check!(" + 42", [sp!(" "), num!(Year)]; INVALID); - check!("-", [num!(Year)]; TOO_SHORT); - check!("+", [num!(Year)]; TOO_SHORT); - - // unsigned numeric - check!("345", [num!(Ordinal)]; ordinal: 345); - check!("+345", [num!(Ordinal)]; INVALID); - check!("-345", [num!(Ordinal)]; INVALID); - check!(" 345", [num!(Ordinal)]; INVALID); - check!("345 ", [num!(Ordinal)]; TOO_LONG); - check!(" 345", [sp!(" "), num!(Ordinal)]; ordinal: 345); - check!("345 ", [num!(Ordinal), sp!(" ")]; ordinal: 345); - check!("345🀠 ", [num!(Ordinal), lit!("🀠"), sp!(" ")]; ordinal: 345); - check!("345🀠", [num!(Ordinal)]; TOO_LONG); - check!("\u{0363}345", [num!(Ordinal)]; INVALID); - check!(" +345", [num!(Ordinal)]; INVALID); - check!(" -345", [num!(Ordinal)]; INVALID); - check!("\t345", [sp!("\t"), num!(Ordinal)]; ordinal: 345); - check!(" +345", [sp!(" "), num!(Ordinal)]; INVALID); - check!(" -345", [sp!(" "), num!(Ordinal)]; INVALID); - - // various numeric fields - check!("1234 5678", [num!(Year), num!(IsoYear)]; INVALID); - check!("1234 5678", - [num!(Year), sp!(" "), num!(IsoYear)]; - year: 1234, isoyear: 5678); - check!("12 34 56 78", - [num!(YearDiv100), num!(YearMod100), num!(IsoYearDiv100), num!(IsoYearMod100)]; - INVALID); - check!("12 34🀠56 78", - [num!(YearDiv100), sp!(" "), num!(YearMod100), - lit!("🀠"), num!(IsoYearDiv100), sp!(" "), num!(IsoYearMod100)]; - year_div_100: 12, year_mod_100: 34, isoyear_div_100: 56, isoyear_mod_100: 78); - check!("1 2 3 4 5 6", - [num!(Month), sp!(" "), num!(Day), sp!(" "), num!(WeekFromSun), sp!(" "), - num!(WeekFromMon), sp!(" "), num!(IsoWeek), sp!(" "), num!(NumDaysFromSun)]; - month: 1, day: 2, week_from_sun: 3, week_from_mon: 4, isoweek: 5, weekday: Weekday::Sat); - check!("7 89 01", - [num!(WeekdayFromMon), sp!(" "), num!(Ordinal), sp!(" "), num!(Hour12)]; - weekday: Weekday::Sun, ordinal: 89, hour_mod_12: 1); - check!("23 45 6 78901234 567890123", - [num!(Hour), sp!(" "), num!(Minute), sp!(" "), num!(Second), sp!(" "), - num!(Nanosecond), sp!(" "), num!(Timestamp)]; - hour_div_12: 1, hour_mod_12: 11, minute: 45, second: 6, nanosecond: 78_901_234, - timestamp: 567_890_123); - - // fixed: month and weekday names - check!("apr", [fix!(ShortMonthName)]; month: 4); - check!("Apr", [fix!(ShortMonthName)]; month: 4); - check!("APR", [fix!(ShortMonthName)]; month: 4); - check!("ApR", [fix!(ShortMonthName)]; month: 4); - check!("\u{0363}APR", [fix!(ShortMonthName)]; INVALID); - check!("April", [fix!(ShortMonthName)]; TOO_LONG); // `Apr` is parsed - check!("A", [fix!(ShortMonthName)]; TOO_SHORT); - check!("Sol", [fix!(ShortMonthName)]; INVALID); - check!("Apr", [fix!(LongMonthName)]; month: 4); - check!("Apri", [fix!(LongMonthName)]; TOO_LONG); // `Apr` is parsed - check!("April", [fix!(LongMonthName)]; month: 4); - check!("Aprill", [fix!(LongMonthName)]; TOO_LONG); - check!("Aprill", [fix!(LongMonthName), lit!("l")]; month: 4); - check!("Aprl", [fix!(LongMonthName), lit!("l")]; month: 4); - check!("April", [fix!(LongMonthName), lit!("il")]; TOO_SHORT); // do not backtrack - check!("thu", [fix!(ShortWeekdayName)]; weekday: Weekday::Thu); - check!("Thu", [fix!(ShortWeekdayName)]; weekday: Weekday::Thu); - check!("THU", [fix!(ShortWeekdayName)]; weekday: Weekday::Thu); - check!("tHu", [fix!(ShortWeekdayName)]; weekday: Weekday::Thu); - check!("Thursday", [fix!(ShortWeekdayName)]; TOO_LONG); // `Thu` is parsed - check!("T", [fix!(ShortWeekdayName)]; TOO_SHORT); - check!("The", [fix!(ShortWeekdayName)]; INVALID); - check!("Nop", [fix!(ShortWeekdayName)]; INVALID); - check!("Thu", [fix!(LongWeekdayName)]; weekday: Weekday::Thu); - check!("Thur", [fix!(LongWeekdayName)]; TOO_LONG); // `Thu` is parsed - check!("Thurs", [fix!(LongWeekdayName)]; TOO_LONG); // ditto - check!("Thursday", [fix!(LongWeekdayName)]; weekday: Weekday::Thu); - check!("Thursdays", [fix!(LongWeekdayName)]; TOO_LONG); - check!("Thursdays", [fix!(LongWeekdayName), lit!("s")]; weekday: Weekday::Thu); - check!("Thus", [fix!(LongWeekdayName), lit!("s")]; weekday: Weekday::Thu); - check!("Thursday", [fix!(LongWeekdayName), lit!("rsday")]; TOO_SHORT); // do not backtrack - - // fixed: am/pm - check!("am", [fix!(LowerAmPm)]; hour_div_12: 0); - check!("pm", [fix!(LowerAmPm)]; hour_div_12: 1); - check!("AM", [fix!(LowerAmPm)]; hour_div_12: 0); - check!("PM", [fix!(LowerAmPm)]; hour_div_12: 1); - check!("am", [fix!(UpperAmPm)]; hour_div_12: 0); - check!("pm", [fix!(UpperAmPm)]; hour_div_12: 1); - check!("AM", [fix!(UpperAmPm)]; hour_div_12: 0); - check!("PM", [fix!(UpperAmPm)]; hour_div_12: 1); - check!("Am", [fix!(LowerAmPm)]; hour_div_12: 0); - check!(" Am", [sp!(" "), fix!(LowerAmPm)]; hour_div_12: 0); - check!("Am🀠", [fix!(LowerAmPm), lit!("🀠")]; hour_div_12: 0); - check!("🀠Am", [lit!("🀠"), fix!(LowerAmPm)]; hour_div_12: 0); - check!("\u{0363}am", [fix!(LowerAmPm)]; INVALID); - check!("\u{0360}am", [fix!(LowerAmPm)]; INVALID); - check!(" Am", [fix!(LowerAmPm)]; INVALID); - check!("Am ", [fix!(LowerAmPm)]; TOO_LONG); - check!("a.m.", [fix!(LowerAmPm)]; INVALID); - check!("A.M.", [fix!(LowerAmPm)]; INVALID); - check!("ame", [fix!(LowerAmPm)]; TOO_LONG); // `am` is parsed - check!("a", [fix!(LowerAmPm)]; TOO_SHORT); - check!("p", [fix!(LowerAmPm)]; TOO_SHORT); - check!("x", [fix!(LowerAmPm)]; TOO_SHORT); - check!("xx", [fix!(LowerAmPm)]; INVALID); - check!("", [fix!(LowerAmPm)]; TOO_SHORT); - - // fixed: dot plus nanoseconds - check!("", [fix!(Nanosecond)]; ); // no field set, but not an error - check!(".", [fix!(Nanosecond)]; TOO_SHORT); - check!("4", [fix!(Nanosecond)]; TOO_LONG); // never consumes `4` - check!("4", [fix!(Nanosecond), num!(Second)]; second: 4); - check!(".0", [fix!(Nanosecond)]; nanosecond: 0); - check!(".4", [fix!(Nanosecond)]; nanosecond: 400_000_000); - check!(".42", [fix!(Nanosecond)]; nanosecond: 420_000_000); - check!(".421", [fix!(Nanosecond)]; nanosecond: 421_000_000); - check!(".42195", [fix!(Nanosecond)]; nanosecond: 421_950_000); - check!(".421951", [fix!(Nanosecond)]; nanosecond: 421_951_000); - check!(".4219512", [fix!(Nanosecond)]; nanosecond: 421_951_200); - check!(".42195123", [fix!(Nanosecond)]; nanosecond: 421_951_230); - check!(".421950803", [fix!(Nanosecond)]; nanosecond: 421_950_803); - check!(".4219508035", [fix!(Nanosecond)]; nanosecond: 421_950_803); - check!(".42195080354", [fix!(Nanosecond)]; nanosecond: 421_950_803); - check!(".421950803547", [fix!(Nanosecond)]; nanosecond: 421_950_803); - check!(".000000003", [fix!(Nanosecond)]; nanosecond: 3); - check!(".0000000031", [fix!(Nanosecond)]; nanosecond: 3); - check!(".0000000035", [fix!(Nanosecond)]; nanosecond: 3); - check!(".000000003547", [fix!(Nanosecond)]; nanosecond: 3); - check!(".0000000009", [fix!(Nanosecond)]; nanosecond: 0); - check!(".000000000547", [fix!(Nanosecond)]; nanosecond: 0); - check!(".0000000009999999999999999999999999", [fix!(Nanosecond)]; nanosecond: 0); - check!(".4🀠", [fix!(Nanosecond), lit!("🀠")]; nanosecond: 400_000_000); - check!(".4x", [fix!(Nanosecond)]; TOO_LONG); - check!(". 4", [fix!(Nanosecond)]; INVALID); - check!(" .4", [fix!(Nanosecond)]; TOO_LONG); // no automatic trimming - - // fixed: nanoseconds without the dot - check!("", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!(".", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!("0", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!("4", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!("42", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!("421", [internal_fix!(Nanosecond3NoDot)]; nanosecond: 421_000_000); - check!("4210", [internal_fix!(Nanosecond3NoDot)]; TOO_LONG); - check!("42143", [internal_fix!(Nanosecond3NoDot), num!(Second)]; nanosecond: 421_000_000, second: 43); - check!("421🀠", [internal_fix!(Nanosecond3NoDot), lit!("🀠")]; nanosecond: 421_000_000); - check!("🀠421", [lit!("🀠"), internal_fix!(Nanosecond3NoDot)]; nanosecond: 421_000_000); - check!("42195", [internal_fix!(Nanosecond3NoDot)]; TOO_LONG); - check!("123456789", [internal_fix!(Nanosecond3NoDot)]; TOO_LONG); - check!("4x", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT); - check!(" 4", [internal_fix!(Nanosecond3NoDot)]; INVALID); - check!(".421", [internal_fix!(Nanosecond3NoDot)]; INVALID); - - check!("", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!(".", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!("0", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!("1234", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!("12345", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!("421950", [internal_fix!(Nanosecond6NoDot)]; nanosecond: 421_950_000); - check!("000003", [internal_fix!(Nanosecond6NoDot)]; nanosecond: 3000); - check!("000000", [internal_fix!(Nanosecond6NoDot)]; nanosecond: 0); - check!("1234567", [internal_fix!(Nanosecond6NoDot)]; TOO_LONG); - check!("123456789", [internal_fix!(Nanosecond6NoDot)]; TOO_LONG); - check!("4x", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT); - check!(" 4", [internal_fix!(Nanosecond6NoDot)]; INVALID); - check!(".42100", [internal_fix!(Nanosecond6NoDot)]; INVALID); - - check!("", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT); - check!(".", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT); - check!("42195", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT); - check!("12345678", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT); - check!("421950803", [internal_fix!(Nanosecond9NoDot)]; nanosecond: 421_950_803); - check!("000000003", [internal_fix!(Nanosecond9NoDot)]; nanosecond: 3); - check!("42195080354", [internal_fix!(Nanosecond9NoDot), num!(Second)]; nanosecond: 421_950_803, second: 54); // don't skip digits that come after the 9 - check!("1234567890", [internal_fix!(Nanosecond9NoDot)]; TOO_LONG); - check!("000000000", [internal_fix!(Nanosecond9NoDot)]; nanosecond: 0); - check!("00000000x", [internal_fix!(Nanosecond9NoDot)]; INVALID); - check!(" 4", [internal_fix!(Nanosecond9NoDot)]; INVALID); - check!(".42100000", [internal_fix!(Nanosecond9NoDot)]; INVALID); - - // fixed: timezone offsets - - // TimezoneOffset - check!("1", [fix!(TimezoneOffset)]; INVALID); - check!("12", [fix!(TimezoneOffset)]; INVALID); - check!("123", [fix!(TimezoneOffset)]; INVALID); - check!("1234", [fix!(TimezoneOffset)]; INVALID); - check!("12345", [fix!(TimezoneOffset)]; INVALID); - check!("123456", [fix!(TimezoneOffset)]; INVALID); - check!("1234567", [fix!(TimezoneOffset)]; INVALID); - check!("+1", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+12", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+123", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+1234", [fix!(TimezoneOffset)]; offset: 45_240); - check!("+12345", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+123456", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+1234567", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12345678", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+12:3", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+12:34", [fix!(TimezoneOffset)]; offset: 45_240); - check!("-12:34", [fix!(TimezoneOffset)]; offset: -45_240); - check!("+12:34:", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:34:5", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:34:56", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:34:56:", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12 34", [fix!(TimezoneOffset)]; INVALID); - check!("+12 34", [fix!(TimezoneOffset)]; INVALID); - check!("12:34", [fix!(TimezoneOffset)]; INVALID); - check!("12:34:56", [fix!(TimezoneOffset)]; INVALID); - check!("+12::34", [fix!(TimezoneOffset)]; INVALID); - check!("+12: :34", [fix!(TimezoneOffset)]; INVALID); - check!("+12:::34", [fix!(TimezoneOffset)]; INVALID); - check!("+12::::34", [fix!(TimezoneOffset)]; INVALID); - check!("+12::34", [fix!(TimezoneOffset)]; INVALID); - check!("+12:34:56", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:3456", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+1234:56", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+1234:567", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+00:00", [fix!(TimezoneOffset)]; offset: 0); - check!("-00:00", [fix!(TimezoneOffset)]; offset: 0); - check!("+00:01", [fix!(TimezoneOffset)]; offset: 60); - check!("-00:01", [fix!(TimezoneOffset)]; offset: -60); - check!("+00:30", [fix!(TimezoneOffset)]; offset: 1_800); - check!("-00:30", [fix!(TimezoneOffset)]; offset: -1_800); - check!("+24:00", [fix!(TimezoneOffset)]; offset: 86_400); - check!("-24:00", [fix!(TimezoneOffset)]; offset: -86_400); - check!("+99:59", [fix!(TimezoneOffset)]; offset: 359_940); - check!("-99:59", [fix!(TimezoneOffset)]; offset: -359_940); - check!("+00:60", [fix!(TimezoneOffset)]; OUT_OF_RANGE); - check!("+00:99", [fix!(TimezoneOffset)]; OUT_OF_RANGE); - check!("#12:34", [fix!(TimezoneOffset)]; INVALID); - check!("+12:34 ", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12 34 ", [fix!(TimezoneOffset)]; INVALID); - check!(" +12:34", [fix!(TimezoneOffset)]; offset: 45_240); - check!(" -12:34", [fix!(TimezoneOffset)]; offset: -45_240); - check!(" +12:34", [fix!(TimezoneOffset)]; INVALID); - check!(" -12:34", [fix!(TimezoneOffset)]; INVALID); - check!("\t -12:34", [fix!(TimezoneOffset)]; INVALID); - check!("-12: 34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 :34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 : 34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 : 34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 : 34", [fix!(TimezoneOffset)]; INVALID); - check!("-12: 34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 :34", [fix!(TimezoneOffset)]; INVALID); - check!("-12 : 34", [fix!(TimezoneOffset)]; INVALID); - check!("12:34 ", [fix!(TimezoneOffset)]; INVALID); - check!(" 12:34", [fix!(TimezoneOffset)]; INVALID); - check!("", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+12345", [fix!(TimezoneOffset), num!(Day)]; offset: 45_240, day: 5); - check!("+12:345", [fix!(TimezoneOffset), num!(Day)]; offset: 45_240, day: 5); - check!("+12:34:", [fix!(TimezoneOffset), lit!(":")]; offset: 45_240); - check!("Z12:34", [fix!(TimezoneOffset)]; INVALID); - check!("X12:34", [fix!(TimezoneOffset)]; INVALID); - check!("Z+12:34", [fix!(TimezoneOffset)]; INVALID); - check!("X+12:34", [fix!(TimezoneOffset)]; INVALID); - check!("🀠+12:34", [fix!(TimezoneOffset)]; INVALID); - check!("+12:34🀠", [fix!(TimezoneOffset)]; TOO_LONG); - check!("+12:🀠34", [fix!(TimezoneOffset)]; INVALID); - check!("+12:34🀠", [fix!(TimezoneOffset), lit!("🀠")]; offset: 45_240); - check!("🀠+12:34", [lit!("🀠"), fix!(TimezoneOffset)]; offset: 45_240); - check!("Z", [fix!(TimezoneOffset)]; INVALID); - check!("A", [fix!(TimezoneOffset)]; INVALID); - check!("PST", [fix!(TimezoneOffset)]; INVALID); - check!("#Z", [fix!(TimezoneOffset)]; INVALID); - check!(":Z", [fix!(TimezoneOffset)]; INVALID); - check!("+Z", [fix!(TimezoneOffset)]; TOO_SHORT); - check!("+:Z", [fix!(TimezoneOffset)]; INVALID); - check!("+Z:", [fix!(TimezoneOffset)]; INVALID); - check!("z", [fix!(TimezoneOffset)]; INVALID); - check!(" :Z", [fix!(TimezoneOffset)]; INVALID); - check!(" Z", [fix!(TimezoneOffset)]; INVALID); - check!(" z", [fix!(TimezoneOffset)]; INVALID); - - // TimezoneOffsetColon - check!("1", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12", [fix!(TimezoneOffsetColon)]; INVALID); - check!("123", [fix!(TimezoneOffsetColon)]; INVALID); - check!("1234", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12345", [fix!(TimezoneOffsetColon)]; INVALID); - check!("123456", [fix!(TimezoneOffsetColon)]; INVALID); - check!("1234567", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12345678", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+1", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+12", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+123", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+1234", [fix!(TimezoneOffsetColon)]; offset: 45_240); - check!("-1234", [fix!(TimezoneOffsetColon)]; offset: -45_240); - check!("+12345", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+123456", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+1234567", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12345678", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("1:", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:3", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:34:", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:34:5", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:34:56", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+1:", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12:", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+12:3", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+12:34", [fix!(TimezoneOffsetColon)]; offset: 45_240); - check!("-12:34", [fix!(TimezoneOffsetColon)]; offset: -45_240); - check!("+12:34:", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:34:5", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:34:56", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:34:56:", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:34:56:7", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:34:56:78", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12:3456", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+1234:56", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!("+12 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12: 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12 :34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12::34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12: :34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12:::34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12::::34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12::34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("#1234", [fix!(TimezoneOffsetColon)]; INVALID); - check!("#12:34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12:34 ", [fix!(TimezoneOffsetColon)]; TOO_LONG); - check!(" +12:34", [fix!(TimezoneOffsetColon)]; offset: 45_240); - check!("\t+12:34", [fix!(TimezoneOffsetColon)]; offset: 45_240); - check!("\t\t+12:34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("12:34 ", [fix!(TimezoneOffsetColon)]; INVALID); - check!(" 12:34", [fix!(TimezoneOffsetColon)]; INVALID); - check!("", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!(":", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+12345", [fix!(TimezoneOffsetColon), num!(Day)]; offset: 45_240, day: 5); - check!("+12:345", [fix!(TimezoneOffsetColon), num!(Day)]; offset: 45_240, day: 5); - check!("+12:34:", [fix!(TimezoneOffsetColon), lit!(":")]; offset: 45_240); - check!("Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!("A", [fix!(TimezoneOffsetColon)]; INVALID); - check!("PST", [fix!(TimezoneOffsetColon)]; INVALID); - check!("#Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!(":Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+Z", [fix!(TimezoneOffsetColon)]; TOO_SHORT); - check!("+:Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!("+Z:", [fix!(TimezoneOffsetColon)]; INVALID); - check!("z", [fix!(TimezoneOffsetColon)]; INVALID); - check!(" :Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!(" Z", [fix!(TimezoneOffsetColon)]; INVALID); - check!(" z", [fix!(TimezoneOffsetColon)]; INVALID); - // testing `TimezoneOffsetColon` also tests same path as `TimezoneOffsetDoubleColon` - // and `TimezoneOffsetTripleColon` for function `parse_internal`. - // No need for separate tests for `TimezoneOffsetDoubleColon` and - // `TimezoneOffsetTripleColon`. - - // TimezoneOffsetZ - check!("1", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12", [fix!(TimezoneOffsetZ)]; INVALID); - check!("123", [fix!(TimezoneOffsetZ)]; INVALID); - check!("1234", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12345", [fix!(TimezoneOffsetZ)]; INVALID); - check!("123456", [fix!(TimezoneOffsetZ)]; INVALID); - check!("1234567", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12345678", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+1", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+12", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+123", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+1234", [fix!(TimezoneOffsetZ)]; offset: 45_240); - check!("-1234", [fix!(TimezoneOffsetZ)]; offset: -45_240); - check!("+12345", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+123456", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+1234567", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12345678", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("1:", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:3", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:34:", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:34:5", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:34:56", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+1:", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12:", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+12:3", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+12:34", [fix!(TimezoneOffsetZ)]; offset: 45_240); - check!("-12:34", [fix!(TimezoneOffsetZ)]; offset: -45_240); - check!("+12:34:", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12:34:5", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12:34:56", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12:34:56:", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12:34:56:7", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12:34:56:78", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12::34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12:3456", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+1234:56", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12: 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 :34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12 : 34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("12:34 ", [fix!(TimezoneOffsetZ)]; INVALID); - check!(" 12:34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+12:34 ", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("+12 34 ", [fix!(TimezoneOffsetZ)]; INVALID); - check!(" +12:34", [fix!(TimezoneOffsetZ)]; offset: 45_240); - check!("+12345", [fix!(TimezoneOffsetZ), num!(Day)]; offset: 45_240, day: 5); - check!("+12:345", [fix!(TimezoneOffsetZ), num!(Day)]; offset: 45_240, day: 5); - check!("+12:34:", [fix!(TimezoneOffsetZ), lit!(":")]; offset: 45_240); - check!("Z12:34", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("X12:34", [fix!(TimezoneOffsetZ)]; INVALID); - check!("Z", [fix!(TimezoneOffsetZ)]; offset: 0); - check!("z", [fix!(TimezoneOffsetZ)]; offset: 0); - check!(" Z", [fix!(TimezoneOffsetZ)]; offset: 0); - check!(" z", [fix!(TimezoneOffsetZ)]; offset: 0); - check!("\u{0363}Z", [fix!(TimezoneOffsetZ)]; INVALID); - check!("Z ", [fix!(TimezoneOffsetZ)]; TOO_LONG); - check!("A", [fix!(TimezoneOffsetZ)]; INVALID); - check!("PST", [fix!(TimezoneOffsetZ)]; INVALID); - check!("#Z", [fix!(TimezoneOffsetZ)]; INVALID); - check!(":Z", [fix!(TimezoneOffsetZ)]; INVALID); - check!(":z", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+Z", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("-Z", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+A", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+πŸ™ƒ", [fix!(TimezoneOffsetZ)]; INVALID); - check!("+Z:", [fix!(TimezoneOffsetZ)]; INVALID); - check!(" :Z", [fix!(TimezoneOffsetZ)]; INVALID); - check!(" +Z", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!(" -Z", [fix!(TimezoneOffsetZ)]; TOO_SHORT); - check!("+:Z", [fix!(TimezoneOffsetZ)]; INVALID); - check!("Y", [fix!(TimezoneOffsetZ)]; INVALID); - check!("Zulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 0); - check!("zulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 0); - check!("+1234ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 45_240); - check!("+12:34ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 45_240); - // Testing `TimezoneOffsetZ` also tests same path as `TimezoneOffsetColonZ` - // in function `parse_internal`. - // No need for separate tests for `TimezoneOffsetColonZ`. - - // TimezoneOffsetPermissive - check!("1", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("123", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("1234", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12345", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("123456", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("1234567", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12345678", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+1", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+12", [internal_fix!(TimezoneOffsetPermissive)]; offset: 43_200); - check!("+123", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+1234", [internal_fix!(TimezoneOffsetPermissive)]; offset: 45_240); - check!("-1234", [internal_fix!(TimezoneOffsetPermissive)]; offset: -45_240); - check!("+12345", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+123456", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+1234567", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12345678", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("1:", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:3", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:34:", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:34:5", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:34:56", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+1:", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:", [internal_fix!(TimezoneOffsetPermissive)]; offset: 43_200); - check!("+12:3", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+12:34", [internal_fix!(TimezoneOffsetPermissive)]; offset: 45_240); - check!("-12:34", [internal_fix!(TimezoneOffsetPermissive)]; offset: -45_240); - check!("+12:34:", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:34:5", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:34:56", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:34:56:", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:34:56:7", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:34:56:78", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 :34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12: 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 : 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 :34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12: 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 : 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12::34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 ::34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12: :34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:: 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12 ::34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12: :34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:: 34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:::34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12::::34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("12:34 ", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!(" 12:34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:34 ", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!(" +12:34", [internal_fix!(TimezoneOffsetPermissive)]; offset: 45_240); - check!("+12345", [internal_fix!(TimezoneOffsetPermissive), num!(Day)]; offset: 45_240, day: 5); - check!("+12:345", [internal_fix!(TimezoneOffsetPermissive), num!(Day)]; offset: 45_240, day: 5); - check!("+12:34:", [internal_fix!(TimezoneOffsetPermissive), lit!(":")]; offset: 45_240); - check!("🀠+12:34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:34🀠", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("+12:🀠34", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+12:34🀠", [internal_fix!(TimezoneOffsetPermissive), lit!("🀠")]; offset: 45_240); - check!("🀠+12:34", [lit!("🀠"), internal_fix!(TimezoneOffsetPermissive)]; offset: 45_240); - check!("Z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); - check!("A", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("PST", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); - check!(" Z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); - check!(" z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); - check!("Z ", [internal_fix!(TimezoneOffsetPermissive)]; TOO_LONG); - check!("#Z", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!(":Z", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!(":z", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+Z", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("-Z", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+A", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+PST", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+πŸ™ƒ", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("+Z:", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!(" :Z", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!(" +Z", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!(" -Z", [internal_fix!(TimezoneOffsetPermissive)]; TOO_SHORT); - check!("+:Z", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - check!("Y", [internal_fix!(TimezoneOffsetPermissive)]; INVALID); - - // TimezoneName - check!("CEST", [fix!(TimezoneName)]; ); - check!("cest", [fix!(TimezoneName)]; ); // lowercase - check!("XXXXXXXX", [fix!(TimezoneName)]; ); // not a real timezone name - check!("!!!!", [fix!(TimezoneName)]; ); // not a real timezone name! - check!("CEST 5", [fix!(TimezoneName), lit!(" "), num!(Day)]; day: 5); - check!("CEST ", [fix!(TimezoneName)]; TOO_LONG); - check!(" CEST", [fix!(TimezoneName)]; TOO_LONG); - check!("CE ST", [fix!(TimezoneName)]; TOO_LONG); - - // some practical examples - check!("2015-02-04T14:37:05+09:00", - [num!(Year), lit!("-"), num!(Month), lit!("-"), num!(Day), lit!("T"), - num!(Hour), lit!(":"), num!(Minute), lit!(":"), num!(Second), fix!(TimezoneOffset)]; - year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, - minute: 37, second: 5, offset: 32400); - check!("20150204143705567", - [num!(Year), num!(Month), num!(Day), - num!(Hour), num!(Minute), num!(Second), internal_fix!(Nanosecond3NoDot)]; - year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, - minute: 37, second: 5, nanosecond: 567000000); - check!("20150204143705.567", - [num!(Year), num!(Month), num!(Day), - num!(Hour), num!(Minute), num!(Second), fix!(Nanosecond)]; - year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, - minute: 37, second: 5, nanosecond: 567000000); - check!("20150204143705.567891", - [num!(Year), num!(Month), num!(Day), - num!(Hour), num!(Minute), num!(Second), fix!(Nanosecond)]; - year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, - minute: 37, second: 5, nanosecond: 567891000); - check!("20150204143705.567891023", - [num!(Year), num!(Month), num!(Day), - num!(Hour), num!(Minute), num!(Second), fix!(Nanosecond)]; - year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, - minute: 37, second: 5, nanosecond: 567891023); - check!("Mon, 10 Jun 2013 09:32:37 GMT", - [fix!(ShortWeekdayName), lit!(","), sp!(" "), num!(Day), sp!(" "), - fix!(ShortMonthName), sp!(" "), num!(Year), sp!(" "), num!(Hour), lit!(":"), - num!(Minute), lit!(":"), num!(Second), sp!(" "), lit!("GMT")]; - year: 2013, month: 6, day: 10, weekday: Weekday::Mon, - hour_div_12: 0, hour_mod_12: 9, minute: 32, second: 37); - check!("🀠Mon, 10 Jun🀠2013 09:32:37 GMT🀠", - [lit!("🀠"), fix!(ShortWeekdayName), lit!(","), sp!(" "), num!(Day), sp!(" "), - fix!(ShortMonthName), lit!("🀠"), num!(Year), sp!(" "), num!(Hour), lit!(":"), - num!(Minute), lit!(":"), num!(Second), sp!(" "), lit!("GMT"), lit!("🀠")]; - year: 2013, month: 6, day: 10, weekday: Weekday::Mon, - hour_div_12: 0, hour_mod_12: 9, minute: 32, second: 37); - check!("Sun Aug 02 13:39:15 CEST 2020", - [fix!(ShortWeekdayName), sp!(" "), fix!(ShortMonthName), sp!(" "), - num!(Day), sp!(" "), num!(Hour), lit!(":"), num!(Minute), lit!(":"), - num!(Second), sp!(" "), fix!(TimezoneName), sp!(" "), num!(Year)]; - year: 2020, month: 8, day: 2, weekday: Weekday::Sun, - hour_div_12: 1, hour_mod_12: 1, minute: 39, second: 15); - check!("20060102150405", - [num!(Year), num!(Month), num!(Day), num!(Hour), num!(Minute), num!(Second)]; - year: 2006, month: 1, day: 2, hour_div_12: 1, hour_mod_12: 3, minute: 4, second: 5); - check!("3:14PM", - [num!(Hour12), lit!(":"), num!(Minute), fix!(LowerAmPm)]; - hour_div_12: 1, hour_mod_12: 3, minute: 14); - check!("12345678901234.56789", - [num!(Timestamp), lit!("."), num!(Nanosecond)]; - nanosecond: 56_789, timestamp: 12_345_678_901_234); - check!("12345678901234.56789", - [num!(Timestamp), fix!(Nanosecond)]; - nanosecond: 567_890_000, timestamp: 12_345_678_901_234); - - // docstring examples from `impl str::FromStr` - check!("2000-01-02T03:04:05Z", - [num!(Year), lit!("-"), num!(Month), lit!("-"), num!(Day), lit!("T"), - num!(Hour), lit!(":"), num!(Minute), lit!(":"), num!(Second), - internal_fix!(TimezoneOffsetPermissive)]; - year: 2000, month: 1, day: 2, - hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, - offset: 0); - check!("2000-01-02 03:04:05Z", - [num!(Year), lit!("-"), num!(Month), lit!("-"), num!(Day), sp!(" "), - num!(Hour), lit!(":"), num!(Minute), lit!(":"), num!(Second), - internal_fix!(TimezoneOffsetPermissive)]; - year: 2000, month: 1, day: 2, - hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, - offset: 0); -} + #[test] + fn test_parse_fixed() { + use crate::format::Fixed::*; + use crate::format::Item::{Literal, Space}; + + // fixed: month and weekday names + check("apr", &[fixed(ShortMonthName)], parsed!(month: 4)); + check("Apr", &[fixed(ShortMonthName)], parsed!(month: 4)); + check("APR", &[fixed(ShortMonthName)], parsed!(month: 4)); + check("ApR", &[fixed(ShortMonthName)], parsed!(month: 4)); + check("\u{0363}APR", &[fixed(ShortMonthName)], Err(INVALID)); + check("April", &[fixed(ShortMonthName)], Err(TOO_LONG)); // `Apr` is parsed + check("A", &[fixed(ShortMonthName)], Err(TOO_SHORT)); + check("Sol", &[fixed(ShortMonthName)], Err(INVALID)); + check("Apr", &[fixed(LongMonthName)], parsed!(month: 4)); + check("Apri", &[fixed(LongMonthName)], Err(TOO_LONG)); // `Apr` is parsed + check("April", &[fixed(LongMonthName)], parsed!(month: 4)); + check("Aprill", &[fixed(LongMonthName)], Err(TOO_LONG)); + check("Aprill", &[fixed(LongMonthName), Literal("l")], parsed!(month: 4)); + check("Aprl", &[fixed(LongMonthName), Literal("l")], parsed!(month: 4)); + check("April", &[fixed(LongMonthName), Literal("il")], Err(TOO_SHORT)); // do not backtrack + check("thu", &[fixed(ShortWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("Thu", &[fixed(ShortWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("THU", &[fixed(ShortWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("tHu", &[fixed(ShortWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("Thursday", &[fixed(ShortWeekdayName)], Err(TOO_LONG)); // `Thu` is parsed + check("T", &[fixed(ShortWeekdayName)], Err(TOO_SHORT)); + check("The", &[fixed(ShortWeekdayName)], Err(INVALID)); + check("Nop", &[fixed(ShortWeekdayName)], Err(INVALID)); + check("Thu", &[fixed(LongWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("Thur", &[fixed(LongWeekdayName)], Err(TOO_LONG)); // `Thu` is parsed + check("Thurs", &[fixed(LongWeekdayName)], Err(TOO_LONG)); // `Thu` is parsed + check("Thursday", &[fixed(LongWeekdayName)], parsed!(weekday: Weekday::Thu)); + check("Thursdays", &[fixed(LongWeekdayName)], Err(TOO_LONG)); + check("Thursdays", &[fixed(LongWeekdayName), Literal("s")], parsed!(weekday: Weekday::Thu)); + check("Thus", &[fixed(LongWeekdayName), Literal("s")], parsed!(weekday: Weekday::Thu)); + check("Thursday", &[fixed(LongWeekdayName), Literal("rsday")], Err(TOO_SHORT)); // do not backtrack + + // fixed: am/pm + check("am", &[fixed(LowerAmPm)], parsed!(hour_div_12: 0)); + check("pm", &[fixed(LowerAmPm)], parsed!(hour_div_12: 1)); + check("AM", &[fixed(LowerAmPm)], parsed!(hour_div_12: 0)); + check("PM", &[fixed(LowerAmPm)], parsed!(hour_div_12: 1)); + check("am", &[fixed(UpperAmPm)], parsed!(hour_div_12: 0)); + check("pm", &[fixed(UpperAmPm)], parsed!(hour_div_12: 1)); + check("AM", &[fixed(UpperAmPm)], parsed!(hour_div_12: 0)); + check("PM", &[fixed(UpperAmPm)], parsed!(hour_div_12: 1)); + check("Am", &[fixed(LowerAmPm)], parsed!(hour_div_12: 0)); + check(" Am", &[Space(" "), fixed(LowerAmPm)], parsed!(hour_div_12: 0)); + check("Am🀠", &[fixed(LowerAmPm), Literal("🀠")], parsed!(hour_div_12: 0)); + check("🀠Am", &[Literal("🀠"), fixed(LowerAmPm)], parsed!(hour_div_12: 0)); + check("\u{0363}am", &[fixed(LowerAmPm)], Err(INVALID)); + check("\u{0360}am", &[fixed(LowerAmPm)], Err(INVALID)); + check(" Am", &[fixed(LowerAmPm)], Err(INVALID)); + check("Am ", &[fixed(LowerAmPm)], Err(TOO_LONG)); + check("a.m.", &[fixed(LowerAmPm)], Err(INVALID)); + check("A.M.", &[fixed(LowerAmPm)], Err(INVALID)); + check("ame", &[fixed(LowerAmPm)], Err(TOO_LONG)); // `am` is parsed + check("a", &[fixed(LowerAmPm)], Err(TOO_SHORT)); + check("p", &[fixed(LowerAmPm)], Err(TOO_SHORT)); + check("x", &[fixed(LowerAmPm)], Err(TOO_SHORT)); + check("xx", &[fixed(LowerAmPm)], Err(INVALID)); + check("", &[fixed(LowerAmPm)], Err(TOO_SHORT)); + } -#[cfg(test)] -#[test] -fn test_rfc2822() { - use super::NOT_ENOUGH; - use super::*; - use crate::offset::FixedOffset; - use crate::DateTime; - - // Test data - (input, Ok(expected result after parse and format) or Err(error code)) - let testdates = [ - ("Tue, 20 Jan 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // normal case - ("Fri, 2 Jan 2015 17:35:20 -0800", Ok("Fri, 02 Jan 2015 17:35:20 -0800")), // folding whitespace - ("Fri, 02 Jan 2015 17:35:20 -0800", Ok("Fri, 02 Jan 2015 17:35:20 -0800")), // leading zero - ("Tue, 20 Jan 2015 17:35:20 -0800 (UTC)", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // trailing comment - ("Tue, 20 Jan 2015 17:35:20 -0800 (UTC)", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // intermixed arbitrary whitespace - ("Tue, 20 Jan 2015\t17:35:20\t-0800\t\t(UTC)", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // intermixed arbitrary whitespace - ( - r"Tue, 20 Jan 2015 17:35:20 -0800 ( (UTC ) (\( (a)\(( \t ) ) \\( \) ))", - Ok("Tue, 20 Jan 2015 17:35:20 -0800"), - ), // complex trailing comment - (r"Tue, 20 Jan 2015 17:35:20 -0800 (UTC\)", Err(TOO_LONG)), // incorrect comment, not enough closing parentheses - ( - "Tue, 20 Jan 2015 17:35:20 -0800 (UTC)\t \r\n(Anothercomment)", - Ok("Tue, 20 Jan 2015 17:35:20 -0800"), - ), // multiple comments - ("Tue, 20 Jan 2015 17:35:20 -0800 (UTC) ", Err(TOO_LONG)), // trailing whitespace after comment - ("20 Jan 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // no day of week - ("20 JAN 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // upper case month - ("Tue, 20 Jan 2015 17:35 -0800", Ok("Tue, 20 Jan 2015 17:35:00 -0800")), // no second - ("11 Sep 2001 09:45:00 EST", Ok("Tue, 11 Sep 2001 09:45:00 -0500")), - ("30 Feb 2015 17:35:20 -0800", Err(OUT_OF_RANGE)), // bad day of month - ("Tue, 20 Jan 2015", Err(TOO_SHORT)), // omitted fields - ("Tue, 20 Avr 2015 17:35:20 -0800", Err(INVALID)), // bad month name - ("Tue, 20 Jan 2015 25:35:20 -0800", Err(OUT_OF_RANGE)), // bad hour - ("Tue, 20 Jan 2015 7:35:20 -0800", Err(INVALID)), // bad # of digits in hour - ("Tue, 20 Jan 2015 17:65:20 -0800", Err(OUT_OF_RANGE)), // bad minute - ("Tue, 20 Jan 2015 17:35:90 -0800", Err(OUT_OF_RANGE)), // bad second - ("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset - ("6 Jun 1944 04:00:00Z", Err(INVALID)), // bad offset (zulu not allowed) - ("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named time zone - // named timezones that have specific timezone offsets - // see https://www.rfc-editor.org/rfc/rfc2822#section-4.3 - ("Tue, 20 Jan 2015 17:35:20 GMT", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 UT", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 ut", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 EDT", Ok("Tue, 20 Jan 2015 17:35:20 -0400")), - ("Tue, 20 Jan 2015 17:35:20 EST", Ok("Tue, 20 Jan 2015 17:35:20 -0500")), - ("Tue, 20 Jan 2015 17:35:20 CDT", Ok("Tue, 20 Jan 2015 17:35:20 -0500")), - ("Tue, 20 Jan 2015 17:35:20 CST", Ok("Tue, 20 Jan 2015 17:35:20 -0600")), - ("Tue, 20 Jan 2015 17:35:20 MDT", Ok("Tue, 20 Jan 2015 17:35:20 -0600")), - ("Tue, 20 Jan 2015 17:35:20 MST", Ok("Tue, 20 Jan 2015 17:35:20 -0700")), - ("Tue, 20 Jan 2015 17:35:20 PDT", Ok("Tue, 20 Jan 2015 17:35:20 -0700")), - ("Tue, 20 Jan 2015 17:35:20 PST", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), - ("Tue, 20 Jan 2015 17:35:20 pst", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), - // named single-letter military timezones must fallback to +0000 - ("Tue, 20 Jan 2015 17:35:20 Z", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 A", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 a", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 K", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - ("Tue, 20 Jan 2015 17:35:20 k", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), - // named single-letter timezone "J" is specifically not valid - ("Tue, 20 Jan 2015 17:35:20 J", Err(NOT_ENOUGH)), - ("Tue, 20 Jan 2015😈17:35:20 -0800", Err(INVALID)), // bad character! - ]; - - fn rfc2822_to_datetime(date: &str) -> ParseResult> { - let mut parsed = Parsed::new(); - parse(&mut parsed, date, [Item::Fixed(Fixed::RFC2822)].iter())?; - parsed.to_datetime() + #[test] + fn test_parse_fixed_nanosecond() { + use crate::format::Fixed::Nanosecond; + use crate::format::InternalInternal::*; + use crate::format::Item::Literal; + use crate::format::Numeric::Second; + + // fixed: dot plus nanoseconds + check("", &[fixed(Nanosecond)], parsed!()); // no field set, but not an error + check(".", &[fixed(Nanosecond)], Err(TOO_SHORT)); + check("4", &[fixed(Nanosecond)], Err(TOO_LONG)); // never consumes `4` + check("4", &[fixed(Nanosecond), num(Second)], parsed!(second: 4)); + check(".0", &[fixed(Nanosecond)], parsed!(nanosecond: 0)); + check(".4", &[fixed(Nanosecond)], parsed!(nanosecond: 400_000_000)); + check(".42", &[fixed(Nanosecond)], parsed!(nanosecond: 420_000_000)); + check(".421", &[fixed(Nanosecond)], parsed!(nanosecond: 421_000_000)); + check(".42195", &[fixed(Nanosecond)], parsed!(nanosecond: 421_950_000)); + check(".421951", &[fixed(Nanosecond)], parsed!(nanosecond: 421_951_000)); + check(".4219512", &[fixed(Nanosecond)], parsed!(nanosecond: 421_951_200)); + check(".42195123", &[fixed(Nanosecond)], parsed!(nanosecond: 421_951_230)); + check(".421950803", &[fixed(Nanosecond)], parsed!(nanosecond: 421_950_803)); + check(".4219508035", &[fixed(Nanosecond)], parsed!(nanosecond: 421_950_803)); + check(".42195080354", &[fixed(Nanosecond)], parsed!(nanosecond: 421_950_803)); + check(".421950803547", &[fixed(Nanosecond)], parsed!(nanosecond: 421_950_803)); + check(".000000003", &[fixed(Nanosecond)], parsed!(nanosecond: 3)); + check(".0000000031", &[fixed(Nanosecond)], parsed!(nanosecond: 3)); + check(".0000000035", &[fixed(Nanosecond)], parsed!(nanosecond: 3)); + check(".000000003547", &[fixed(Nanosecond)], parsed!(nanosecond: 3)); + check(".0000000009", &[fixed(Nanosecond)], parsed!(nanosecond: 0)); + check(".000000000547", &[fixed(Nanosecond)], parsed!(nanosecond: 0)); + check(".0000000009999999999999999999999999", &[fixed(Nanosecond)], parsed!(nanosecond: 0)); + check(".4🀠", &[fixed(Nanosecond), Literal("🀠")], parsed!(nanosecond: 400_000_000)); + check(".4x", &[fixed(Nanosecond)], Err(TOO_LONG)); + check(". 4", &[fixed(Nanosecond)], Err(INVALID)); + check(" .4", &[fixed(Nanosecond)], Err(TOO_LONG)); // no automatic trimming + + // fixed: nanoseconds without the dot + check("", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check(".", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check("0", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check("4", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check("42", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check("421", &[internal_fixed(Nanosecond3NoDot)], parsed!(nanosecond: 421_000_000)); + check("4210", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_LONG)); + check( + "42143", + &[internal_fixed(Nanosecond3NoDot), num(Second)], + parsed!(nanosecond: 421_000_000, second: 43), + ); + check( + "421🀠", + &[internal_fixed(Nanosecond3NoDot), Literal("🀠")], + parsed!(nanosecond: 421_000_000), + ); + check( + "🀠421", + &[Literal("🀠"), internal_fixed(Nanosecond3NoDot)], + parsed!(nanosecond: 421_000_000), + ); + check("42195", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_LONG)); + check("123456789", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_LONG)); + check("4x", &[internal_fixed(Nanosecond3NoDot)], Err(TOO_SHORT)); + check(" 4", &[internal_fixed(Nanosecond3NoDot)], Err(INVALID)); + check(".421", &[internal_fixed(Nanosecond3NoDot)], Err(INVALID)); + + check("", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check(".", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check("0", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check("1234", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check("12345", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check("421950", &[internal_fixed(Nanosecond6NoDot)], parsed!(nanosecond: 421_950_000)); + check("000003", &[internal_fixed(Nanosecond6NoDot)], parsed!(nanosecond: 3000)); + check("000000", &[internal_fixed(Nanosecond6NoDot)], parsed!(nanosecond: 0)); + check("1234567", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_LONG)); + check("123456789", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_LONG)); + check("4x", &[internal_fixed(Nanosecond6NoDot)], Err(TOO_SHORT)); + check(" 4", &[internal_fixed(Nanosecond6NoDot)], Err(INVALID)); + check(".42100", &[internal_fixed(Nanosecond6NoDot)], Err(INVALID)); + + check("", &[internal_fixed(Nanosecond9NoDot)], Err(TOO_SHORT)); + check(".", &[internal_fixed(Nanosecond9NoDot)], Err(TOO_SHORT)); + check("42195", &[internal_fixed(Nanosecond9NoDot)], Err(TOO_SHORT)); + check("12345678", &[internal_fixed(Nanosecond9NoDot)], Err(TOO_SHORT)); + check("421950803", &[internal_fixed(Nanosecond9NoDot)], parsed!(nanosecond: 421_950_803)); + check("000000003", &[internal_fixed(Nanosecond9NoDot)], parsed!(nanosecond: 3)); + check( + "42195080354", + &[internal_fixed(Nanosecond9NoDot), num(Second)], + parsed!(nanosecond: 421_950_803, second: 54), + ); // don't skip digits that come after the 9 + check("1234567890", &[internal_fixed(Nanosecond9NoDot)], Err(TOO_LONG)); + check("000000000", &[internal_fixed(Nanosecond9NoDot)], parsed!(nanosecond: 0)); + check("00000000x", &[internal_fixed(Nanosecond9NoDot)], Err(INVALID)); + check(" 4", &[internal_fixed(Nanosecond9NoDot)], Err(INVALID)); + check(".42100000", &[internal_fixed(Nanosecond9NoDot)], Err(INVALID)); } - fn fmt_rfc2822_datetime(dt: DateTime) -> String { - dt.format_with_items([Item::Fixed(Fixed::RFC2822)].iter()).to_string() + #[test] + fn test_parse_fixed_timezone_offset() { + use crate::format::Fixed::*; + use crate::format::InternalInternal::*; + use crate::format::Item::Literal; + + // TimezoneOffset + check("1", &[fixed(TimezoneOffset)], Err(INVALID)); + check("12", &[fixed(TimezoneOffset)], Err(INVALID)); + check("123", &[fixed(TimezoneOffset)], Err(INVALID)); + check("1234", &[fixed(TimezoneOffset)], Err(INVALID)); + check("12345", &[fixed(TimezoneOffset)], Err(INVALID)); + check("123456", &[fixed(TimezoneOffset)], Err(INVALID)); + check("1234567", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+1", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+12", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+123", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+1234", &[fixed(TimezoneOffset)], parsed!(offset: 45_240)); + check("+12345", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+123456", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+1234567", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12345678", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+12:3", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+12:34", &[fixed(TimezoneOffset)], parsed!(offset: 45_240)); + check("-12:34", &[fixed(TimezoneOffset)], parsed!(offset: -45_240)); + check("βˆ’12:34", &[fixed(TimezoneOffset)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12:34:", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:34:5", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:34:56", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:34:56:", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("12:34:56", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12::34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12: :34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12:::34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12::::34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12::34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12:34:56", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:3456", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+1234:56", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+1234:567", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+00:00", &[fixed(TimezoneOffset)], parsed!(offset: 0)); + check("-00:00", &[fixed(TimezoneOffset)], parsed!(offset: 0)); + check("βˆ’00:00", &[fixed(TimezoneOffset)], parsed!(offset: 0)); // MINUS SIGN (U+2212) + check("+00:01", &[fixed(TimezoneOffset)], parsed!(offset: 60)); + check("-00:01", &[fixed(TimezoneOffset)], parsed!(offset: -60)); + check("+00:30", &[fixed(TimezoneOffset)], parsed!(offset: 1_800)); + check("-00:30", &[fixed(TimezoneOffset)], parsed!(offset: -1_800)); + check("+24:00", &[fixed(TimezoneOffset)], parsed!(offset: 86_400)); + check("-24:00", &[fixed(TimezoneOffset)], parsed!(offset: -86_400)); + check("βˆ’24:00", &[fixed(TimezoneOffset)], parsed!(offset: -86_400)); // MINUS SIGN (U+2212) + check("+99:59", &[fixed(TimezoneOffset)], parsed!(offset: 359_940)); + check("-99:59", &[fixed(TimezoneOffset)], parsed!(offset: -359_940)); + check("+00:60", &[fixed(TimezoneOffset)], Err(OUT_OF_RANGE)); + check("+00:99", &[fixed(TimezoneOffset)], Err(OUT_OF_RANGE)); + check("#12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12:34 ", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12 34 ", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" +12:34", &[fixed(TimezoneOffset)], parsed!(offset: 45_240)); + check(" -12:34", &[fixed(TimezoneOffset)], parsed!(offset: -45_240)); + check(" βˆ’12:34", &[fixed(TimezoneOffset)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check(" +12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" -12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("\t -12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12: 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 :34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 : 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 : 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 : 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12: 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 :34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("-12 : 34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("12:34 ", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" 12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check( + "+12345", + &[fixed(TimezoneOffset), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check( + "+12:345", + &[fixed(TimezoneOffset), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check("+12:34:", &[fixed(TimezoneOffset), Literal(":")], parsed!(offset: 45_240)); + check("Z12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("X12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("Z+12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("X+12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("Xβˆ’12:34", &[fixed(TimezoneOffset)], Err(INVALID)); // MINUS SIGN (U+2212) + check("🀠+12:34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+12:34🀠", &[fixed(TimezoneOffset)], Err(TOO_LONG)); + check("+12:🀠34", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+1234🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: 45_240)); + check("-1234🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: -45_240)); + check("βˆ’1234🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12:34🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: 45_240)); + check("-12:34🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: -45_240)); + check("βˆ’12:34🀠", &[fixed(TimezoneOffset), Literal("🀠")], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("🀠+12:34", &[Literal("🀠"), fixed(TimezoneOffset)], parsed!(offset: 45_240)); + check("Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check("A", &[fixed(TimezoneOffset)], Err(INVALID)); + check("PST", &[fixed(TimezoneOffset)], Err(INVALID)); + check("#Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check(":Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+Z", &[fixed(TimezoneOffset)], Err(TOO_SHORT)); + check("+:Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check("+Z:", &[fixed(TimezoneOffset)], Err(INVALID)); + check("z", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" :Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" Z", &[fixed(TimezoneOffset)], Err(INVALID)); + check(" z", &[fixed(TimezoneOffset)], Err(INVALID)); + + // TimezoneOffsetColon + check("1", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("123", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("1234", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12345", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("123456", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("1234567", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12345678", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+1", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+12", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+123", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+1234", &[fixed(TimezoneOffsetColon)], parsed!(offset: 45_240)); + check("-1234", &[fixed(TimezoneOffsetColon)], parsed!(offset: -45_240)); + check("βˆ’1234", &[fixed(TimezoneOffsetColon)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12345", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+123456", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+1234567", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12345678", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("1:", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:3", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:34:", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:34:5", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:34:56", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+1:", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12:", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+12:3", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: 45_240)); + check("-12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: -45_240)); + check("βˆ’12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12:34:", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:34:5", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:34:56", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:34:56:", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:34:56:7", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:34:56:78", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+12:3456", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("+1234:56", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check("βˆ’12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("βˆ’12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); // MINUS SIGN (U+2212) + check("+12 :34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12: 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12: 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 :34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("-12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12::34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12: :34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12:::34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12::::34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12::34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("#1234", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("#12:34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+12:34 ", &[fixed(TimezoneOffsetColon)], Err(TOO_LONG)); + check(" +12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: 45_240)); + check("\t+12:34", &[fixed(TimezoneOffsetColon)], parsed!(offset: 45_240)); + check("\t\t+12:34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("12:34 ", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check(" 12:34", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check(":", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check( + "+12345", + &[fixed(TimezoneOffsetColon), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check( + "+12:345", + &[fixed(TimezoneOffsetColon), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check("+12:34:", &[fixed(TimezoneOffsetColon), Literal(":")], parsed!(offset: 45_240)); + check("Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("A", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("PST", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("#Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check(":Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+Z", &[fixed(TimezoneOffsetColon)], Err(TOO_SHORT)); + check("+:Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("+Z:", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check("z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check(" :Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check(" Z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + check(" z", &[fixed(TimezoneOffsetColon)], Err(INVALID)); + // testing `TimezoneOffsetColon` also tests same path as `TimezoneOffsetDoubleColon` + // and `TimezoneOffsetTripleColon` for function `parse_internal`. + // No need for separate tests for `TimezoneOffsetDoubleColon` and + // `TimezoneOffsetTripleColon`. + + // TimezoneOffsetZ + check("1", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("123", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("1234", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12345", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("123456", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("1234567", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12345678", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+1", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+12", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+123", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+1234", &[fixed(TimezoneOffsetZ)], parsed!(offset: 45_240)); + check("-1234", &[fixed(TimezoneOffsetZ)], parsed!(offset: -45_240)); + check("βˆ’1234", &[fixed(TimezoneOffsetZ)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12345", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+123456", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+1234567", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12345678", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("1:", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:3", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:34:", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:34:5", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:34:56", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+1:", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12:", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+12:3", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+12:34", &[fixed(TimezoneOffsetZ)], parsed!(offset: 45_240)); + check("-12:34", &[fixed(TimezoneOffsetZ)], parsed!(offset: -45_240)); + check("βˆ’12:34", &[fixed(TimezoneOffsetZ)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12:34:", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12:34:5", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12:34:56", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12:34:56:", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12:34:56:7", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12:34:56:78", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12::34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12:3456", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+1234:56", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12: 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 :34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12 : 34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("12:34 ", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(" 12:34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+12:34 ", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("+12 34 ", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(" +12:34", &[fixed(TimezoneOffsetZ)], parsed!(offset: 45_240)); + check( + "+12345", + &[fixed(TimezoneOffsetZ), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check( + "+12:345", + &[fixed(TimezoneOffsetZ), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check("+12:34:", &[fixed(TimezoneOffsetZ), Literal(":")], parsed!(offset: 45_240)); + check("Z12:34", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("X12:34", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("Z", &[fixed(TimezoneOffsetZ)], parsed!(offset: 0)); + check("z", &[fixed(TimezoneOffsetZ)], parsed!(offset: 0)); + check(" Z", &[fixed(TimezoneOffsetZ)], parsed!(offset: 0)); + check(" z", &[fixed(TimezoneOffsetZ)], parsed!(offset: 0)); + check("\u{0363}Z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("Z ", &[fixed(TimezoneOffsetZ)], Err(TOO_LONG)); + check("A", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("PST", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("#Z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(":Z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(":z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+Z", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("-Z", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+A", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+πŸ™ƒ", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("+Z:", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(" :Z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check(" +Z", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check(" -Z", &[fixed(TimezoneOffsetZ)], Err(TOO_SHORT)); + check("+:Z", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("Y", &[fixed(TimezoneOffsetZ)], Err(INVALID)); + check("Zulu", &[fixed(TimezoneOffsetZ), Literal("ulu")], parsed!(offset: 0)); + check("zulu", &[fixed(TimezoneOffsetZ), Literal("ulu")], parsed!(offset: 0)); + check("+1234ulu", &[fixed(TimezoneOffsetZ), Literal("ulu")], parsed!(offset: 45_240)); + check("+12:34ulu", &[fixed(TimezoneOffsetZ), Literal("ulu")], parsed!(offset: 45_240)); + // Testing `TimezoneOffsetZ` also tests same path as `TimezoneOffsetColonZ` + // in function `parse_internal`. + // No need for separate tests for `TimezoneOffsetColonZ`. + + // TimezoneOffsetPermissive + check("1", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("123", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("1234", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12345", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("123456", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("1234567", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12345678", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+1", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+12", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 43_200)); + check("+123", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+1234", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 45_240)); + check("-1234", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); + check("βˆ’1234", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12345", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+123456", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+1234567", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12345678", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("1:", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:3", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:34:", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:34:5", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:34:56", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+1:", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 43_200)); + check("+12:3", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 45_240)); + check("-12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); + check("βˆ’12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check("+12:34:", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:34:5", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:34:56", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:34:56:", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:34:56:7", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:34:56:78", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 :34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12: 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 : 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 :34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12: 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 : 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12::34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 ::34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12: :34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:: 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12 ::34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12: :34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:: 34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:::34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12::::34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("12:34 ", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check(" 12:34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:34 ", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check(" +12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 45_240)); + check(" -12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); + check(" βˆ’12:34", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: -45_240)); // MINUS SIGN (U+2212) + check( + "+12345", + &[internal_fixed(TimezoneOffsetPermissive), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check( + "+12:345", + &[internal_fixed(TimezoneOffsetPermissive), num(Numeric::Day)], + parsed!(offset: 45_240, day: 5), + ); + check( + "+12:34:", + &[internal_fixed(TimezoneOffsetPermissive), Literal(":")], + parsed!(offset: 45_240), + ); + check("🀠+12:34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+12:34🀠", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("+12:🀠34", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check( + "+12:34🀠", + &[internal_fixed(TimezoneOffsetPermissive), Literal("🀠")], + parsed!(offset: 45_240), + ); + check( + "🀠+12:34", + &[Literal("🀠"), internal_fixed(TimezoneOffsetPermissive)], + parsed!(offset: 45_240), + ); + check("Z", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 0)); + check("A", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("PST", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("z", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 0)); + check(" Z", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 0)); + check(" z", &[internal_fixed(TimezoneOffsetPermissive)], parsed!(offset: 0)); + check("Z ", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_LONG)); + check("#Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check(":Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check(":z", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("-Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+A", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+PST", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+πŸ™ƒ", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("+Z:", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check(" :Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check(" +Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check(" -Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(TOO_SHORT)); + check("+:Z", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + check("Y", &[internal_fixed(TimezoneOffsetPermissive)], Err(INVALID)); + + // TimezoneName + check("CEST", &[fixed(TimezoneName)], parsed!()); + check("cest", &[fixed(TimezoneName)], parsed!()); // lowercase + check("XXXXXXXX", &[fixed(TimezoneName)], parsed!()); // not a real timezone name + check("!!!!", &[fixed(TimezoneName)], parsed!()); // not a real timezone name! + check("CEST 5", &[fixed(TimezoneName), Literal(" "), num(Numeric::Day)], parsed!(day: 5)); + check("CEST ", &[fixed(TimezoneName)], Err(TOO_LONG)); + check(" CEST", &[fixed(TimezoneName)], Err(TOO_LONG)); + check("CE ST", &[fixed(TimezoneName)], Err(TOO_LONG)); } - // Test against test data above - for &(date, checkdate) in testdates.iter() { - let d = rfc2822_to_datetime(date); // parse a date - let dt = match d { - // did we get a value? - Ok(dt) => Ok(fmt_rfc2822_datetime(dt)), // yes, go on - Err(e) => Err(e), // otherwise keep an error for the comparison - }; - if dt != checkdate.map(|s| s.to_string()) { - // check for expected result - panic!( - "Date conversion failed for {}\nReceived: {:?}\nExpected: {:?}", - date, dt, checkdate - ); - } + #[test] + #[rustfmt::skip] + fn test_parse_practical_examples() { + use crate::format::InternalInternal::*; + use crate::format::Item::{Literal, Space}; + use crate::format::Numeric::*; + + // some practical examples + check( + "2015-02-04T14:37:05+09:00", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), num(Day), Literal("T"), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + fixed(Fixed::TimezoneOffset), + ], + parsed!( + year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, minute: 37, + second: 5, offset: 32400 + ), + ); + check( + "2015-02-04T14:37:05-09:00", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), num(Day), Literal("T"), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + fixed(Fixed::TimezoneOffset), + ], + parsed!( + year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, minute: 37, + second: 5, offset: -32400 + ), + ); + check( + "2015-02-04T14:37:05βˆ’09:00", // timezone offset using MINUS SIGN (U+2212) + &[ + num(Year), Literal("-"), num(Month), Literal("-"), num(Day), Literal("T"), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + fixed(Fixed::TimezoneOffset) + ], + parsed!( + year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, minute: 37, + second: 5, offset: -32400 + ), + ); + check( + "20150204143705567", + &[ + num(Year), num(Month), num(Day), num(Hour), num(Minute), num(Second), + internal_fixed(Nanosecond3NoDot) + ], + parsed!( + year: 2015, month: 2, day: 4, hour_div_12: 1, hour_mod_12: 2, minute: 37, + second: 5, nanosecond: 567000000 + ), + ); + check( + "Mon, 10 Jun 2013 09:32:37 GMT", + &[ + fixed(Fixed::ShortWeekdayName), Literal(","), Space(" "), num(Day), Space(" "), + fixed(Fixed::ShortMonthName), Space(" "), num(Year), Space(" "), num(Hour), + Literal(":"), num(Minute), Literal(":"), num(Second), Space(" "), Literal("GMT") + ], + parsed!( + year: 2013, month: 6, day: 10, weekday: Weekday::Mon, + hour_div_12: 0, hour_mod_12: 9, minute: 32, second: 37 + ), + ); + check( + "🀠Mon, 10 Jun🀠2013 09:32:37 GMT🀠", + &[ + Literal("🀠"), fixed(Fixed::ShortWeekdayName), Literal(","), Space(" "), num(Day), + Space(" "), fixed(Fixed::ShortMonthName), Literal("🀠"), num(Year), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), Space(" "), + Literal("GMT"), Literal("🀠") + ], + parsed!( + year: 2013, month: 6, day: 10, weekday: Weekday::Mon, + hour_div_12: 0, hour_mod_12: 9, minute: 32, second: 37 + ), + ); + check( + "Sun Aug 02 13:39:15 CEST 2020", + &[ + fixed(Fixed::ShortWeekdayName), Space(" "), fixed(Fixed::ShortMonthName), + Space(" "), num(Day), Space(" "), num(Hour), Literal(":"), num(Minute), + Literal(":"), num(Second), Space(" "), fixed(Fixed::TimezoneName), Space(" "), + num(Year) + ], + parsed!( + year: 2020, month: 8, day: 2, weekday: Weekday::Sun, + hour_div_12: 1, hour_mod_12: 1, minute: 39, second: 15 + ), + ); + check( + "20060102150405", + &[num(Year), num(Month), num(Day), num(Hour), num(Minute), num(Second)], + parsed!( + year: 2006, month: 1, day: 2, hour_div_12: 1, hour_mod_12: 3, minute: 4, second: 5 + ), + ); + check( + "3:14PM", + &[num(Hour12), Literal(":"), num(Minute), fixed(Fixed::LowerAmPm)], + parsed!(hour_div_12: 1, hour_mod_12: 3, minute: 14), + ); + check( + "12345678901234.56789", + &[num(Timestamp), Literal("."), num(Nanosecond)], + parsed!(nanosecond: 56_789, timestamp: 12_345_678_901_234), + ); + check( + "12345678901234.56789", + &[num(Timestamp), fixed(Fixed::Nanosecond)], + parsed!(nanosecond: 567_890_000, timestamp: 12_345_678_901_234), + ); + + // docstring examples from `impl str::FromStr` + check( + "2000-01-02T03:04:05Z", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), num(Day), Literal("T"), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + parsed!( + year: 2000, month: 1, day: 2, hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, + offset: 0 + ), + ); + check( + "2000-01-02 03:04:05Z", + &[ + num(Year), Literal("-"), num(Month), Literal("-"), num(Day), Space(" "), + num(Hour), Literal(":"), num(Minute), Literal(":"), num(Second), + internal_fixed(TimezoneOffsetPermissive) + ], + parsed!( + year: 2000, month: 1, day: 2, hour_div_12: 0, hour_mod_12: 3, minute: 4, second: 5, + offset: 0 + ), + ); } -} -#[cfg(test)] -#[test] -fn parse_rfc850() { - use crate::{TimeZone, Utc}; - - static RFC850_FMT: &str = "%A, %d-%b-%y %T GMT"; - - let dt_str = "Sunday, 06-Nov-94 08:49:37 GMT"; - let dt = Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap(); - - // Check that the format is what we expect - assert_eq!(dt.format(RFC850_FMT).to_string(), dt_str); - - // Check that it parses correctly - assert_eq!(Ok(dt), Utc.datetime_from_str("Sunday, 06-Nov-94 08:49:37 GMT", RFC850_FMT)); - - // Check that the rest of the weekdays parse correctly (this test originally failed because - // Sunday parsed incorrectly). - let testdates = [ - (Utc.with_ymd_and_hms(1994, 11, 7, 8, 49, 37).unwrap(), "Monday, 07-Nov-94 08:49:37 GMT"), - (Utc.with_ymd_and_hms(1994, 11, 8, 8, 49, 37).unwrap(), "Tuesday, 08-Nov-94 08:49:37 GMT"), - ( - Utc.with_ymd_and_hms(1994, 11, 9, 8, 49, 37).unwrap(), - "Wednesday, 09-Nov-94 08:49:37 GMT", - ), - ( - Utc.with_ymd_and_hms(1994, 11, 10, 8, 49, 37).unwrap(), - "Thursday, 10-Nov-94 08:49:37 GMT", - ), - (Utc.with_ymd_and_hms(1994, 11, 11, 8, 49, 37).unwrap(), "Friday, 11-Nov-94 08:49:37 GMT"), - ( - Utc.with_ymd_and_hms(1994, 11, 12, 8, 49, 37).unwrap(), - "Saturday, 12-Nov-94 08:49:37 GMT", - ), - ]; - - for val in &testdates { - assert_eq!(Ok(val.0), Utc.datetime_from_str(val.1, RFC850_FMT)); + #[track_caller] + fn parses(s: &str, items: &[Item]) { + let mut parsed = Parsed::new(); + assert!(parse(&mut parsed, s, items.iter()).is_ok()); } -} -#[cfg(test)] -#[test] -fn test_rfc3339() { - use super::*; - use crate::offset::FixedOffset; - use crate::DateTime; - - // Test data - (input, Ok(expected result after parse and format) or Err(error code)) - let testdates = [ - ("2015-01-20T17:35:20-08:00", Ok("2015-01-20T17:35:20-08:00")), // normal case - ("1944-06-06T04:04:00Z", Ok("1944-06-06T04:04:00+00:00")), // D-day - ("2001-09-11T09:45:00-08:00", Ok("2001-09-11T09:45:00-08:00")), - ("2015-01-20T17:35:20.001-08:00", Ok("2015-01-20T17:35:20.001-08:00")), - ("2015-01-20T17:35:20.000031-08:00", Ok("2015-01-20T17:35:20.000031-08:00")), - ("2015-01-20T17:35:20.000000004-08:00", Ok("2015-01-20T17:35:20.000000004-08:00")), - ("2015-01-20T17:35:20.000000000452-08:00", Ok("2015-01-20T17:35:20-08:00")), // too small - ("2015-01-20 17:35:20.001-08:00", Err(INVALID)), // missing separator 'T' - ("2015/01/20T17:35:20.001-08:00", Err(INVALID)), // wrong separator char YMD - ("2015-01-20T17-35-20.001-08:00", Err(INVALID)), // wrong separator char HMS - ("99999-01-20T17:35:20-08:00", Err(INVALID)), // bad year value - ("-2000-01-20T17:35:20-08:00", Err(INVALID)), // bad year value - ("2015-02-30T17:35:20-08:00", Err(OUT_OF_RANGE)), // bad day of month value - ("2015-01-20T25:35:20-08:00", Err(OUT_OF_RANGE)), // bad hour value - ("2015-01-20T17:65:20-08:00", Err(OUT_OF_RANGE)), // bad minute value - ("2015-01-20T17:35:90-08:00", Err(OUT_OF_RANGE)), // bad second value - ("2015-01-20T17:35:20-24:00", Err(OUT_OF_RANGE)), // bad offset value - ("15-01-20T17:35:20-08:00", Err(INVALID)), // bad year format - ("15-01-20T17:35:20-08:00:00", Err(INVALID)), // bad year format, bad offset format - ("2015-01-20T17:35:20-0800", Err(INVALID)), // bad offset format - ("2015-01-20T17:35:20.001-08 : 00", Err(INVALID)), // bad offset format - ("2015-01-20T17:35:20-08:00:00", Err(TOO_LONG)), // bad offset format - ("2015-01-20T17:35:20-08:", Err(TOO_SHORT)), // bad offset format - ("2015-01-20T17:35:20-08", Err(TOO_SHORT)), // bad offset format - ("2015-01-20T", Err(TOO_SHORT)), // missing HMS - ("2015-01-20T00:00:1", Err(TOO_SHORT)), // missing complete S - ("2015-01-20T00:00:1-08:00", Err(INVALID)), // missing complete S - ]; - - fn rfc3339_to_datetime(date: &str) -> ParseResult> { + #[track_caller] + fn check(s: &str, items: &[Item], expected: ParseResult) { let mut parsed = Parsed::new(); - parse(&mut parsed, date, [Item::Fixed(Fixed::RFC3339)].iter())?; - parsed.to_datetime() + let result = parse(&mut parsed, s, items.iter()); + let parsed = result.map(|_| parsed); + assert_eq!(parsed, expected); + } + + #[test] + fn test_rfc2822() { + let ymd_hmsn = |y, m, d, h, n, s, nano, off| { + FixedOffset::east_opt(off * 60 * 60) + .unwrap() + .with_ymd_and_hms(y, m, d, h, n, s) + .unwrap() + .with_nanosecond(nano) + .unwrap() + }; + + // Test data - (input, Ok(expected result) or Err(error code)) + let testdates = [ + ("Tue, 20 Jan 2015 17:35:20 -0800", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // normal case + ("Fri, 2 Jan 2015 17:35:20 -0800", Ok(ymd_hmsn(2015, 1, 2, 17, 35, 20, 0, -8))), // folding whitespace + ("Fri, 02 Jan 2015 17:35:20 -0800", Ok(ymd_hmsn(2015, 1, 2, 17, 35, 20, 0, -8))), // leading zero + ("Tue, 20 Jan 2015 17:35:20 -0800 (UTC)", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // trailing comment + ( + r"Tue, 20 Jan 2015 17:35:20 -0800 ( (UTC ) (\( (a)\(( \t ) ) \\( \) ))", + Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8)), + ), // complex trailing comment + (r"Tue, 20 Jan 2015 17:35:20 -0800 (UTC\)", Err(TOO_LONG)), // incorrect comment, not enough closing parentheses + ( + "Tue, 20 Jan 2015 17:35:20 -0800 (UTC)\t \r\n(Anothercomment)", + Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8)), + ), // multiple comments + ("Tue, 20 Jan 2015 17:35:20 -0800 (UTC) ", Err(TOO_LONG)), // trailing whitespace after comment + ("20 Jan 2015 17:35:20 -0800", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // no day of week + ("20 JAN 2015 17:35:20 -0800", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // upper case month + ("Tue, 20 Jan 2015 17:35 -0800", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 0, 0, -8))), // no second + ("11 Sep 2001 09:45:00 +0000", Ok(ymd_hmsn(2001, 9, 11, 9, 45, 0, 0, 0))), + ("11 Sep 2001 09:45:00 EST", Ok(ymd_hmsn(2001, 9, 11, 9, 45, 0, 0, -5))), + ("11 Sep 2001 09:45:00 GMT", Ok(ymd_hmsn(2001, 9, 11, 9, 45, 0, 0, 0))), + ("30 Feb 2015 17:35:20 -0800", Err(OUT_OF_RANGE)), // bad day of month + ("Tue, 20 Jan 2015", Err(TOO_SHORT)), // omitted fields + ("Tue, 20 Avr 2015 17:35:20 -0800", Err(INVALID)), // bad month name + ("Tue, 20 Jan 2015 25:35:20 -0800", Err(OUT_OF_RANGE)), // bad hour + ("Tue, 20 Jan 2015 7:35:20 -0800", Err(INVALID)), // bad # of digits in hour + ("Tue, 20 Jan 2015 17:65:20 -0800", Err(OUT_OF_RANGE)), // bad minute + ("Tue, 20 Jan 2015 17:35:90 -0800", Err(OUT_OF_RANGE)), // bad second + ("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset + ("6 Jun 1944 04:00:00Z", Err(INVALID)), // bad offset (zulu not allowed) + ("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named time zone + // named timezones that have specific timezone offsets + // see https://www.rfc-editor.org/rfc/rfc2822#section-4.3 + ("Tue, 20 Jan 2015 17:35:20 GMT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 UT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 ut", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 EDT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -4))), + ("Tue, 20 Jan 2015 17:35:20 EST", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -5))), + ("Tue, 20 Jan 2015 17:35:20 CDT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -5))), + ("Tue, 20 Jan 2015 17:35:20 CST", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -6))), + ("Tue, 20 Jan 2015 17:35:20 MDT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -6))), + ("Tue, 20 Jan 2015 17:35:20 MST", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -7))), + ("Tue, 20 Jan 2015 17:35:20 PDT", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -7))), + ("Tue, 20 Jan 2015 17:35:20 PST", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), + ("Tue, 20 Jan 2015 17:35:20 pst", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), + // named single-letter military timezones must fallback to +0000 + ("Tue, 20 Jan 2015 17:35:20 Z", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 A", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 a", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 K", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + ("Tue, 20 Jan 2015 17:35:20 k", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, 0))), + // named single-letter timezone "J" is specifically not valid + ("Tue, 20 Jan 2015 17:35:20 J", Err(NOT_ENOUGH)), + ("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset minutes + ("Tue, 20 Jan 2015 17:35:20Z", Err(INVALID)), // bad offset: zulu not allowed + ("Tue, 20 Jan 2015 17:35:20 Zulu", Err(NOT_ENOUGH)), // bad offset: zulu not allowed + ("Tue, 20 Jan 2015 17:35:20 ZULU", Err(NOT_ENOUGH)), // bad offset: zulu not allowed + ("Tue, 20 Jan 2015 17:35:20 βˆ’0800", Err(INVALID)), // bad offset: timezone offset using MINUS SIGN (U+2212), not specified for RFC 2822 + ("Tue, 20 Jan 2015 17:35:20 0800", Err(INVALID)), // missing offset sign + ("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named timezone + ("Tue, 20 Jan 2015😈17:35:20 -0800", Err(INVALID)), // bad character! + ]; + + fn rfc2822_to_datetime(date: &str) -> ParseResult> { + let mut parsed = Parsed::new(); + parse(&mut parsed, date, [Item::Fixed(Fixed::RFC2822)].iter())?; + parsed.to_datetime() + } + + // Test against test data above + for &(date, checkdate) in testdates.iter() { + eprintln!("Test input: {:?}", date); + eprintln!(" Expect: {:?}", checkdate); + let dt = rfc2822_to_datetime(date); // parse a date + if dt != checkdate { + // check for expected result + panic!( + "Date conversion failed for {}\nReceived: {:?}\nExpected: {:?}", + date, dt, checkdate + ); + } + } } - fn fmt_rfc3339_datetime(dt: DateTime) -> String { - dt.format_with_items([Item::Fixed(Fixed::RFC3339)].iter()).to_string() + #[test] + fn parse_rfc850() { + static RFC850_FMT: &str = "%A, %d-%b-%y %T GMT"; + + let dt = Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap(); + + // Check that the format is what we expect + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!(dt.format(RFC850_FMT).to_string(), "Sunday, 06-Nov-94 08:49:37 GMT"); + + // Check that it parses correctly + assert_eq!(Ok(dt), Utc.datetime_from_str("Sunday, 06-Nov-94 08:49:37 GMT", RFC850_FMT)); + + // Check that the rest of the weekdays parse correctly (this test originally failed because + // Sunday parsed incorrectly). + let testdates = [ + ( + Utc.with_ymd_and_hms(1994, 11, 7, 8, 49, 37).unwrap(), + "Monday, 07-Nov-94 08:49:37 GMT", + ), + ( + Utc.with_ymd_and_hms(1994, 11, 8, 8, 49, 37).unwrap(), + "Tuesday, 08-Nov-94 08:49:37 GMT", + ), + ( + Utc.with_ymd_and_hms(1994, 11, 9, 8, 49, 37).unwrap(), + "Wednesday, 09-Nov-94 08:49:37 GMT", + ), + ( + Utc.with_ymd_and_hms(1994, 11, 10, 8, 49, 37).unwrap(), + "Thursday, 10-Nov-94 08:49:37 GMT", + ), + ( + Utc.with_ymd_and_hms(1994, 11, 11, 8, 49, 37).unwrap(), + "Friday, 11-Nov-94 08:49:37 GMT", + ), + ( + Utc.with_ymd_and_hms(1994, 11, 12, 8, 49, 37).unwrap(), + "Saturday, 12-Nov-94 08:49:37 GMT", + ), + ]; + + for val in &testdates { + assert_eq!(Ok(val.0), Utc.datetime_from_str(val.1, RFC850_FMT)); + } + + let test_dates_fail = [ + "Saturday, 12-Nov-94 08:49:37", + "Saturday, 12-Nov-94 08:49:37 Z", + "Saturday, 12-Nov-94 08:49:37 GMTTTT", + "Saturday, 12-Nov-94 08:49:37 gmt", + "Saturday, 12-Nov-94 08:49:37 +08:00", + "Caturday, 12-Nov-94 08:49:37 GMT", + "Saturday, 99-Nov-94 08:49:37 GMT", + "Saturday, 12-Nov-2000 08:49:37 GMT", + "Saturday, 12-Mop-94 08:49:37 GMT", + "Saturday, 12-Nov-94 28:49:37 GMT", + "Saturday, 12-Nov-94 08:99:37 GMT", + "Saturday, 12-Nov-94 08:49:99 GMT", + ]; + + for val in &test_dates_fail { + assert!(Utc.datetime_from_str(val, RFC850_FMT).is_err()); + } } - // Test against test data above - for &(date, checkdate) in testdates.iter() { - eprintln!("test_rfc3339: date {:?}, expect {:?}", date, checkdate); - let d = rfc3339_to_datetime(date); // parse a date - let dt = match d { - // did we get a value? - Ok(dt) => Ok(fmt_rfc3339_datetime(dt)), // yes, go on - Err(e) => Err(e), // otherwise keep an error for the comparison + #[test] + fn test_rfc3339() { + let ymd_hmsn = |y, m, d, h, n, s, nano, off| { + FixedOffset::east_opt(off * 60 * 60) + .unwrap() + .with_ymd_and_hms(y, m, d, h, n, s) + .unwrap() + .with_nanosecond(nano) + .unwrap() }; - if dt != checkdate.map(|s| s.to_string()) { - // check for expected result - panic!( - "Date conversion failed for {}\nReceived: {:?}\nExpected: {:?}", - date, dt, checkdate - ); + + // Test data - (input, Ok(expected result) or Err(error code)) + let testdates = [ + ("2015-01-20T17:35:20-08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // normal case + ("2015-01-20T17:35:20βˆ’08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8))), // normal case with MINUS SIGN (U+2212) + ("1944-06-06T04:04:00Z", Ok(ymd_hmsn(1944, 6, 6, 4, 4, 0, 0, 0))), // D-day + ("2001-09-11T09:45:00-08:00", Ok(ymd_hmsn(2001, 9, 11, 9, 45, 0, 0, -8))), + ("2015-01-20T17:35:20.001-08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 1_000_000, -8))), + ("2015-01-20T17:35:20.001βˆ’08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 1_000_000, -8))), // with MINUS SIGN (U+2212) + ("2015-01-20T17:35:20.000031-08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 31_000, -8))), + ("2015-01-20T17:35:20.000000004-08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 4, -8))), + ("2015-01-20T17:35:20.000000004βˆ’08:00", Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 4, -8))), // with MINUS SIGN (U+2212) + ( + "2015-01-20T17:35:20.000000000452-08:00", + Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8)), + ), // too small + ( + "2015-01-20T17:35:20.000000000452βˆ’08:00", + Ok(ymd_hmsn(2015, 1, 20, 17, 35, 20, 0, -8)), + ), // too small with MINUS SIGN (U+2212) + ("2015-01-20 17:35:20.001-08:00", Err(INVALID)), // missing separator 'T' + ("2015/01/20T17:35:20.001-08:00", Err(INVALID)), // wrong separator char YMD + ("2015-01-20T17-35-20.001-08:00", Err(INVALID)), // wrong separator char HMS + ("-01-20T17:35:20-08:00", Err(INVALID)), // missing year + ("99-01-20T17:35:20-08:00", Err(INVALID)), // bad year format + ("99999-01-20T17:35:20-08:00", Err(INVALID)), // bad year value + ("-2000-01-20T17:35:20-08:00", Err(INVALID)), // bad year value + ("2015-02-30T17:35:20-08:00", Err(OUT_OF_RANGE)), // bad day of month value + ("2015-01-20T25:35:20-08:00", Err(OUT_OF_RANGE)), // bad hour value + ("2015-01-20T17:65:20-08:00", Err(OUT_OF_RANGE)), // bad minute value + ("2015-01-20T17:35:90-08:00", Err(OUT_OF_RANGE)), // bad second value + ("2015-01-20T17:35:20-24:00", Err(OUT_OF_RANGE)), // bad offset value + ("15-01-20T17:35:20-08:00", Err(INVALID)), // bad year format + ("15-01-20T17:35:20-08:00:00", Err(INVALID)), // bad year format, bad offset format + ("2015-01-20T17:35:2008:00", Err(INVALID)), // missing offset sign + ("2015-01-20T17:35:20 08:00", Err(INVALID)), // missing offset sign + ("2015-01-20T17:35:20Zulu", Err(TOO_LONG)), // bad offset format + ("2015-01-20T17:35:20 Zulu", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20GMT", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20 GMT", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20+GMT", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20++08:00", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20--08:00", Err(INVALID)), // bad offset format + ("2015-01-20T17:35:20βˆ’βˆ’08:00", Err(INVALID)), // bad offset format with MINUS SIGN (U+2212) + ("2015-01-20T17:35:20Β±08:00", Err(INVALID)), // bad offset sign + ("2015-01-20T17:35:20-08-00", Err(INVALID)), // bad offset separator + ("2015-01-20T17:35:20-08;00", Err(INVALID)), // bad offset separator + ("2015-01-20T17:35:20-0800", Err(INVALID)), // bad offset separator + ("2015-01-20T17:35:20-08:0", Err(TOO_SHORT)), // bad offset minutes + ("2015-01-20T17:35:20-08:AA", Err(INVALID)), // bad offset minutes + ("2015-01-20T17:35:20-08:ZZ", Err(INVALID)), // bad offset minutes + ("2015-01-20T17:35:20.001-08 : 00", Err(INVALID)), // bad offset separator + ("2015-01-20T17:35:20-08:00:00", Err(TOO_LONG)), // bad offset format + ("2015-01-20T17:35:20+08:", Err(TOO_SHORT)), // bad offset format + ("2015-01-20T17:35:20-08:", Err(TOO_SHORT)), // bad offset format + ("2015-01-20T17:35:20βˆ’08:", Err(TOO_SHORT)), // bad offset format with MINUS SIGN (U+2212) + ("2015-01-20T17:35:20-08", Err(TOO_SHORT)), // bad offset format + ("2015-01-20T", Err(TOO_SHORT)), // missing HMS + ("2015-01-20T00:00:1", Err(TOO_SHORT)), // missing complete S + ("2015-01-20T00:00:1-08:00", Err(INVALID)), // missing complete S + ]; + + fn rfc3339_to_datetime(date: &str) -> ParseResult> { + let mut parsed = Parsed::new(); + parse(&mut parsed, date, [Item::Fixed(Fixed::RFC3339)].iter())?; + parsed.to_datetime() + } + + // Test against test data above + for &(date, checkdate) in testdates.iter() { + let dt = rfc3339_to_datetime(date); // parse a date + if dt != checkdate { + // check for expected result + panic!( + "Date conversion failed for {}\nReceived: {:?}\nExpected: {:?}", + date, dt, checkdate + ); + } } } -} -#[cfg(test)] -#[test] -fn test_issue_1010() { - let dt = crate::NaiveDateTime::parse_from_str("\u{c}SUN\u{e}\u{3000}\0m@J\u{3000}\0\u{3000}\0m\u{c}!\u{c}\u{b}\u{c}\u{c}\u{c}\u{c}%A\u{c}\u{b}\0SU\u{c}\u{c}", - "\u{c}\u{c}%A\u{c}\u{b}\0SUN\u{c}\u{c}\u{c}SUNN\u{c}\u{c}\u{c}SUN\u{c}\u{c}!\u{c}\u{b}\u{c}\u{c}\u{c}\u{c}%A\u{c}\u{b}%a"); - assert_eq!(dt, Err(ParseError(ParseErrorKind::Invalid))); + #[test] + fn test_issue_1010() { + let dt = crate::NaiveDateTime::parse_from_str("\u{c}SUN\u{e}\u{3000}\0m@J\u{3000}\0\u{3000}\0m\u{c}!\u{c}\u{b}\u{c}\u{c}\u{c}\u{c}%A\u{c}\u{b}\0SU\u{c}\u{c}", + "\u{c}\u{c}%A\u{c}\u{b}\0SUN\u{c}\u{c}\u{c}SUNN\u{c}\u{c}\u{c}SUN\u{c}\u{c}!\u{c}\u{b}\u{c}\u{c}\u{c}\u{c}%A\u{c}\u{b}%a"); + assert_eq!(dt, Err(ParseError(ParseErrorKind::Invalid))); + } } diff --git a/src/format/scan.rs b/src/format/scan.rs index 29fa70d4d7..4b293db1f9 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -5,29 +5,9 @@ * Various scanning routines for the parser. */ -#![allow(deprecated)] - use super::{ParseResult, INVALID, OUT_OF_RANGE, TOO_SHORT}; use crate::Weekday; -/// Returns true when two slices are equal case-insensitively (in ASCII). -/// Assumes that the `pattern` is already converted to lower case. -fn equals(s: &[u8], pattern: &str) -> bool { - let mut xs = s.iter().map(|&c| match c { - b'A'..=b'Z' => c + 32, - _ => c, - }); - let mut ys = pattern.as_bytes().iter().cloned(); - loop { - match (xs.next(), ys.next()) { - (None, None) => return true, - (None, _) | (_, None) => return false, - (Some(x), Some(y)) if x != y => return false, - _ => (), - } - } -} - /// Tries to parse the non-negative number from `min` to `max` digits. /// /// The absence of digits at all is an unconditional error. @@ -79,7 +59,7 @@ pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> { let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?; // if there are more than 9 digits, skip next digits. - let s = s.trim_left_matches(|c: char| c.is_ascii_digit()); + let s = s.trim_start_matches(|c: char| c.is_ascii_digit()); Ok((s, v)) } @@ -145,14 +125,16 @@ pub(super) fn short_weekday(s: &str) -> ParseResult<(&str, Weekday)> { /// It prefers long month names to short month names when both are possible. pub(super) fn short_or_long_month0(s: &str) -> ParseResult<(&str, u8)> { // lowercased month names, minus first three chars - static LONG_MONTH_SUFFIXES: [&str; 12] = - ["uary", "ruary", "ch", "il", "", "e", "y", "ust", "tember", "ober", "ember", "ember"]; + static LONG_MONTH_SUFFIXES: [&[u8]; 12] = [ + b"uary", b"ruary", b"ch", b"il", b"", b"e", b"y", b"ust", b"tember", b"ober", b"ember", + b"ember", + ]; let (mut s, month0) = short_month0(s)?; // tries to consume the suffix if possible let suffix = LONG_MONTH_SUFFIXES[month0 as usize]; - if s.len() >= suffix.len() && equals(&s.as_bytes()[..suffix.len()], suffix) { + if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) { s = &s[suffix.len()..]; } @@ -163,14 +145,14 @@ pub(super) fn short_or_long_month0(s: &str) -> ParseResult<(&str, u8)> { /// It prefers long weekday names to short weekday names when both are possible. pub(super) fn short_or_long_weekday(s: &str) -> ParseResult<(&str, Weekday)> { // lowercased weekday names, minus first three chars - static LONG_WEEKDAY_SUFFIXES: [&str; 7] = - ["day", "sday", "nesday", "rsday", "day", "urday", "day"]; + static LONG_WEEKDAY_SUFFIXES: [&[u8]; 7] = + [b"day", b"sday", b"nesday", b"rsday", b"day", b"urday", b"day"]; let (mut s, weekday) = short_weekday(s)?; // tries to consume the suffix if possible let suffix = LONG_WEEKDAY_SUFFIXES[weekday.num_days_from_monday() as usize]; - if s.len() >= suffix.len() && equals(&s.as_bytes()[..suffix.len()], suffix) { + if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) { s = &s[suffix.len()..]; } @@ -188,7 +170,7 @@ pub(super) fn char(s: &str, c1: u8) -> ParseResult<&str> { /// Tries to consume one or more whitespace. pub(super) fn space(s: &str) -> ParseResult<&str> { - let s_ = s.trim_left(); + let s_ = s.trim_start(); if s_.len() < s.len() { Ok(s_) } else if s.is_empty() { @@ -221,7 +203,7 @@ pub(super) fn trim1(s: &str) -> &str { /// Consumes one colon char `:` if it is at the front of `s`. /// Always returns `Ok(s)`. -pub(super) fn consume_colon_maybe(mut s: &str) -> ParseResult<&str> { +pub(crate) fn consume_colon_maybe(mut s: &str) -> ParseResult<&str> { if s.is_empty() { // nothing consumed return Ok(s); @@ -235,25 +217,49 @@ pub(super) fn consume_colon_maybe(mut s: &str) -> ParseResult<&str> { Ok(s) } -/// Tries to parse `[-+]\d\d` continued by `\d\d`. Return an offset in seconds if possible. +/// Parse a timezone from `s` and return the offset in seconds. /// -/// The additional `colon` may be used to parse a mandatory or optional `:` -/// between hours and minutes, and should return either a new suffix or `Err` when parsing fails. -pub(super) fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, i32)> -where - F: FnMut(&str) -> ParseResult<&str>, -{ - timezone_offset_internal(s, consume_colon, false) -} - -fn timezone_offset_internal( +/// The `consume_colon` function is used to parse a mandatory or optional `:` +/// separator between hours offset and minutes offset. +/// +/// The `allow_missing_minutes` flag allows the timezone minutes offset to be +/// missing from `s`. +/// +/// The `allow_tz_minus_sign` flag allows the timezone offset negative character +/// to also be `βˆ’` MINUS SIGN (U+2212) in addition to the typical +/// ASCII-compatible `-` HYPHEN-MINUS (U+2D). +/// This is part of [RFC 3339 & ISO 8601]. +/// +/// [RFC 3339 & ISO 8601]: https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=1114309368#Time_offsets_from_UTC +pub(crate) fn timezone_offset( mut s: &str, mut consume_colon: F, + allow_zulu: bool, allow_missing_minutes: bool, + allow_tz_minus_sign: bool, ) -> ParseResult<(&str, i32)> where F: FnMut(&str) -> ParseResult<&str>, { + if allow_zulu { + let bytes = s.as_bytes(); + match bytes.first() { + Some(&b'z') | Some(&b'Z') => return Ok((&s[1..], 0)), + Some(&b'u') | Some(&b'U') => { + if bytes.len() >= 3 { + let (b, c) = (bytes[1], bytes[2]); + match (b | 32, c | 32) { + (b't', b'c') => return Ok((&s[3..], 0)), + _ => return Err(INVALID), + } + } else { + return Err(INVALID); + } + } + _ => {} + } + } + const fn digits(s: &str) -> ParseResult<(u8, u8)> { let b = s.as_bytes(); if b.len() < 2 { @@ -262,13 +268,31 @@ where Ok((b[0], b[1])) } } - let negative = match s.as_bytes().first() { - Some(&b'+') => false, - Some(&b'-') => true, + let negative = match s.chars().next() { + Some('+') => { + // PLUS SIGN (U+2B) + s = &s['+'.len_utf8()..]; + + false + } + Some('-') => { + // HYPHEN-MINUS (U+2D) + s = &s['-'.len_utf8()..]; + + true + } + Some('βˆ’') => { + // MINUS SIGN (U+2212) + if !allow_tz_minus_sign { + return Err(INVALID); + } + s = &s['βˆ’'.len_utf8()..]; + + true + } Some(_) => return Err(INVALID), None => return Err(TOO_SHORT), }; - s = &s[1..]; // hours (00--99) let hours = match digits(s)? { @@ -303,41 +327,6 @@ where Ok((s, if negative { -seconds } else { seconds })) } -/// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as `+00:00`. -pub(super) fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, i32)> -where - F: FnMut(&str) -> ParseResult<&str>, -{ - let bytes = s.as_bytes(); - match bytes.first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), - Some(&b'u') | Some(&b'U') => { - if bytes.len() >= 3 { - let (b, c) = (bytes[1], bytes[2]); - match (b | 32, c | 32) { - (b't', b'c') => Ok((&s[3..], 0)), - _ => Err(INVALID), - } - } else { - Err(INVALID) - } - } - _ => timezone_offset(s, colon), - } -} - -/// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as -/// `+00:00`, and allows missing minutes entirely. -pub(super) fn timezone_offset_permissive(s: &str, colon: F) -> ParseResult<(&str, i32)> -where - F: FnMut(&str) -> ParseResult<&str>, -{ - match s.as_bytes().first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), - _ => timezone_offset_internal(s, colon, true), - } -} - /// Same as `timezone_offset` but also allows for RFC 2822 legacy timezones. /// May return `None` which indicates an insufficient offset data (i.e. `-0000`). /// See [RFC 2822 Section 4.3]. @@ -350,17 +339,17 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option)> let name = &s.as_bytes()[..upto]; let s = &s[upto..]; let offset_hours = |o| Ok((s, Some(o * 3600))); - if equals(name, "gmt") || equals(name, "ut") { + if name.eq_ignore_ascii_case(b"gmt") || name.eq_ignore_ascii_case(b"ut") { offset_hours(0) - } else if equals(name, "edt") { + } else if name.eq_ignore_ascii_case(b"edt") { offset_hours(-4) - } else if equals(name, "est") || equals(name, "cdt") { + } else if name.eq_ignore_ascii_case(b"est") || name.eq_ignore_ascii_case(b"cdt") { offset_hours(-5) - } else if equals(name, "cst") || equals(name, "mdt") { + } else if name.eq_ignore_ascii_case(b"cst") || name.eq_ignore_ascii_case(b"mdt") { offset_hours(-6) - } else if equals(name, "mst") || equals(name, "pdt") { + } else if name.eq_ignore_ascii_case(b"mst") || name.eq_ignore_ascii_case(b"pdt") { offset_hours(-7) - } else if equals(name, "pst") { + } else if name.eq_ignore_ascii_case(b"pst") { offset_hours(-8) } else if name.len() == 1 { match name[0] { @@ -372,17 +361,11 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option)> Ok((s, None)) } } else { - let (s_, offset) = timezone_offset(s, |s| Ok(s))?; + let (s_, offset) = timezone_offset(s, |s| Ok(s), false, false, false)?; Ok((s_, Some(offset))) } } -/// Tries to consume everything until next whitespace-like symbol. -/// Does not provide any offset information from the consumed data. -pub(super) fn timezone_name_skip(s: &str) -> ParseResult<(&str, ())> { - Ok((s.trim_left_matches(|c: char| !c.is_whitespace()), ())) -} - /// Tries to consume an RFC2822 comment including preceding ` `. /// /// Returns the remaining string after the closing parenthesis. diff --git a/src/format/strftime.rs b/src/format/strftime.rs index d08d4e43c3..63c0427c96 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -53,19 +53,19 @@ The following specifiers are available both to formatting and parsing. | | | | | `%M` | `34` | Minute number (00--59), zero-padded to 2 digits. | | `%S` | `60` | Second number (00--60), zero-padded to 2 digits. [^4] | -| `%f` | `026490000` | The fractional seconds (in nanoseconds) since last whole second. [^7] | -| `%.f` | `.026490`| Similar to `.%f` but left-aligned. These all consume the leading dot. [^7] | -| `%.3f`| `.026` | Similar to `.%f` but left-aligned but fixed to a length of 3. [^7] | -| `%.6f`| `.026490` | Similar to `.%f` but left-aligned but fixed to a length of 6. [^7] | -| `%.9f`| `.026490000` | Similar to `.%f` but left-aligned but fixed to a length of 9. [^7] | -| `%3f` | `026` | Similar to `%.3f` but without the leading dot. [^7] | -| `%6f` | `026490` | Similar to `%.6f` but without the leading dot. [^7] | -| `%9f` | `026490000` | Similar to `%.9f` but without the leading dot. [^7] | +| `%f` | `26490000` | Number of nanoseconds since last whole second. [^7] | +| `%.f` | `.026490`| Decimal fraction of a second. Consumes the leading dot. [^7] | +| `%.3f`| `.026` | Decimal fraction of a second with a fixed length of 3. | +| `%.6f`| `.026490` | Decimal fraction of a second with a fixed length of 6. | +| `%.9f`| `.026490000` | Decimal fraction of a second with a fixed length of 9. | +| `%3f` | `026` | Decimal fraction of a second like `%.3f` but without the leading dot. | +| `%6f` | `026490` | Decimal fraction of a second like `%.6f` but without the leading dot. | +| `%9f` | `026490000` | Decimal fraction of a second like `%.9f` but without the leading dot. | | | | | | `%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` | 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`. | +| `%r` | `12:34:60 AM` | Locale's 12 hour clock time. (e.g., 11:11:04 PM). Falls back to `%X` if the locale does not have a 12 hour clock format. | | | | | | | | **TIME ZONE SPECIFIERS:** | | `%Z` | `ACST` | Local time zone name. Skips all non-whitespace characters during parsing. Identical to `%:z` when formatting. [^8] | @@ -132,36 +132,12 @@ Notes: For the purpose of Chrono, it only accounts for non-leap seconds so it slightly differs from ISO C `strftime` behavior. -[^7]: `%f`, `%.f`, `%.3f`, `%.6f`, `%.9f`, `%3f`, `%6f`, `%9f`: +[^7]: `%f`, `%.f`:
- The default `%f` is right-aligned and always zero-padded to 9 digits - for the compatibility with glibc and others, - so it always counts the number of nanoseconds since the last whole second. - E.g. 7ms after the last second will print `007000000`, - and parsing `7000000` will yield the same. -
-
- The variant `%.f` is left-aligned and print 0, 3, 6 or 9 fractional digits - according to the precision. - E.g. 70ms after the last second under `%.f` will print `.070` (note: not `.07`), - and parsing `.07`, `.070000` etc. will yield the same. - Note that they can print or read nothing if the fractional part is zero or - the next character is not `.`. -
-
- The variant `%.3f`, `%.6f` and `%.9f` are left-aligned and print 3, 6 or 9 fractional digits - according to the number preceding `f`. - E.g. 70ms after the last second under `%.3f` will print `.070` (note: not `.07`), - and parsing `.07`, `.070000` etc. will yield the same. - Note that they can read nothing if the fractional part is zero or - the next character is not `.` however will print with the specified length. -
-
- The variant `%3f`, `%6f` and `%9f` are left-aligned and print 3, 6 or 9 fractional digits - according to the number preceding `f`, but without the leading dot. - E.g. 70ms after the last second under `%3f` will print `070` (note: not `07`), - and parsing `07`, `070000` etc. will yield the same. - Note that they can read nothing if the fractional part is zero. + `%f` and `%.f` are notably different formatting specifiers.
+ `%f` counts the number of nanoseconds since the last whole second, while `%.f` is a fraction of a + second.
+ Example: 7ΞΌs is formatted as `7000` with `%f`, and formatted as `.000007` with `%.f`. [^8]: `%Z`: Since `chrono` is not aware of timezones beyond their offsets, this specifier @@ -180,39 +156,10 @@ Notes: China Daylight Time. */ -#[cfg(feature = "unstable-locales")] -extern crate alloc; - -#[cfg(feature = "unstable-locales")] -use alloc::vec::Vec; - +use super::{fixed, internal_fixed, num, num0, nums}; #[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: &[Item<'static>] = - &[num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)]; -static D_T_FMT: &[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: &[Item<'static>] = &[num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)]; +use super::{Fixed, InternalInternal, Item, Numeric, Pad}; /// Parsing iterator for `strftime`-like format strings. #[derive(Clone, Debug)] @@ -220,58 +167,34 @@ pub struct StrftimeItems<'a> { /// Remaining portion of the string. remainder: &'a str, /// 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: Fmt<'a>, - /// Date format - d_fmt: Fmt<'a>, - /// Date and time format - d_t_fmt: Fmt<'a>, - /// Time format - t_fmt: Fmt<'a>, + /// `queue` stores a slice of `Item`s that have to be returned one by one. + queue: &'static [Item<'static>], + #[cfg(feature = "unstable-locales")] + locale_str: &'a str, + #[cfg(feature = "unstable-locales")] + locale: Option, } impl<'a> StrftimeItems<'a> { /// Creates a new parsing iterator from the `strftime`-like format string. #[must_use] - pub fn new(s: &'a str) -> StrftimeItems<'a> { - Self::with_remainer(s) + pub const fn new(s: &'a str) -> StrftimeItems<'a> { + #[cfg(not(feature = "unstable-locales"))] + { + StrftimeItems { remainder: s, queue: &[] } + } + #[cfg(feature = "unstable-locales")] + { + StrftimeItems { remainder: s, queue: &[], locale_str: "", locale: None } + } } /// Creates a new parsing iterator from the `strftime`-like format string. #[cfg(feature = "unstable-locales")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))] #[must_use] - 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_t_fmt, t_fmt } - } - - #[cfg(not(feature = "unstable-locales"))] - fn with_remainer(s: &'a str) -> StrftimeItems<'a> { - static FMT_NONE: &[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(), - } + pub const fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> { + StrftimeItems { remainder: s, queue: &[], locale_str: "", locale: Some(locale) } } } @@ -281,37 +204,78 @@ impl<'a> Iterator for StrftimeItems<'a> { type Item = Item<'a>; fn next(&mut self) -> Option> { - // we have some reconstructed items to return - if !self.recons.is_empty() { - 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..]; - } + // We have items queued to return from a specifier composed of multiple formatting items. + if let Some((item, remainder)) = self.queue.split_first() { + self.queue = remainder; + return Some(item.clone()); + } + + // We are in the middle of parsing the localized formatting string of a specifier. + #[cfg(feature = "unstable-locales")] + if !self.locale_str.is_empty() { + let (remainder, item) = self.parse_next_item(self.locale_str)?; + self.locale_str = remainder; return Some(item); } - match self.remainder.chars().next() { + // Normal: we are parsing the formatting string. + let (remainder, item) = self.parse_next_item(self.remainder)?; + self.remainder = remainder; + Some(item) + } +} + +impl<'a> StrftimeItems<'a> { + fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> { + use InternalInternal::*; + use Item::{Literal, Space}; + use Numeric::*; + + static D_FMT: &[Item<'static>] = + &[num0(Month), Literal("/"), num0(Day), Literal("/"), num0(YearMod100)]; + static D_T_FMT: &[Item<'static>] = &[ + fixed(Fixed::ShortWeekdayName), + Space(" "), + fixed(Fixed::ShortMonthName), + Space(" "), + nums(Day), + Space(" "), + num0(Hour), + Literal(":"), + num0(Minute), + Literal(":"), + num0(Second), + Space(" "), + num0(Year), + ]; + static T_FMT: &[Item<'static>] = + &[num0(Hour), Literal(":"), num0(Minute), Literal(":"), num0(Second)]; + static T_FMT_AMPM: &[Item<'static>] = &[ + num0(Hour12), + Literal(":"), + num0(Minute), + Literal(":"), + num0(Second), + Space(" "), + fixed(Fixed::UpperAmPm), + ]; + + match remainder.chars().next() { // we are done None => None, // the next item is a specifier Some('%') => { - self.remainder = &self.remainder[1..]; + remainder = &remainder[1..]; macro_rules! next { () => { - match self.remainder.chars().next() { + match remainder.chars().next() { Some(x) => { - self.remainder = &self.remainder[x.len_utf8()..]; + remainder = &remainder[x.len_utf8()..]; x } - None => return Some(Item::Error), // premature end of string + None => return Some((remainder, Item::Error)), // premature end of string } }; } @@ -326,193 +290,218 @@ impl<'a> Iterator for StrftimeItems<'a> { let is_alternate = spec == '#'; let spec = if pad_override.is_some() || is_alternate { next!() } else { spec }; if is_alternate && !HAVE_ALTERNATES.contains(spec) { - return Some(Item::Error); + return Some((remainder, Item::Error)); } - macro_rules! recons { + macro_rules! queue { [$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; - } + const QUEUE: &'static [Item<'static>] = &[$($tail),+]; + self.queue = QUEUE; $head }) } - - macro_rules! recons_from_slice { + #[cfg(not(feature = "unstable-locales"))] + macro_rules! queue_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..]; - } + self.queue = &$slice[1..]; $slice[0].clone() }}; } let item = match spec { - 'A' => fix!(LongWeekdayName), - 'B' => fix!(LongMonthName), - 'C' => num0!(YearDiv100), + 'A' => fixed(Fixed::LongWeekdayName), + 'B' => fixed(Fixed::LongMonthName), + 'C' => num0(YearDiv100), 'D' => { - recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)] + queue![num0(Month), Literal("/"), num0(Day), Literal("/"), num0(YearMod100)] + } + 'F' => queue![num0(Year), Literal("-"), num0(Month), Literal("-"), num0(Day)], + 'G' => num0(IsoYear), + 'H' => num0(Hour), + 'I' => num0(Hour12), + 'M' => num0(Minute), + 'P' => fixed(Fixed::LowerAmPm), + 'R' => queue![num0(Hour), Literal(":"), num0(Minute)], + 'S' => num0(Second), + 'T' => { + queue![num0(Hour), Literal(":"), num0(Minute), Literal(":"), num0(Second)] } - 'F' => recons![num0!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day)], - 'G' => num0!(IsoYear), - 'H' => num0!(Hour), - 'I' => num0!(Hour12), - 'M' => num0!(Minute), - 'P' => fix!(LowerAmPm), - 'R' => recons![num0!(Hour), lit!(":"), num0!(Minute)], - 'S' => num0!(Second), - 'T' => recons![num0!(Hour), lit!(":"), num0!(Minute), lit!(":"), num0!(Second)], - 'U' => num0!(WeekFromSun), - 'V' => num0!(IsoWeek), - 'W' => num0!(WeekFromMon), - 'X' => recons_from_slice!(self.t_fmt), - 'Y' => num0!(Year), - 'Z' => fix!(TimezoneName), - 'a' => fix!(ShortWeekdayName), - 'b' | 'h' => fix!(ShortMonthName), - 'c' => recons_from_slice!(self.d_t_fmt), - 'd' => num0!(Day), - 'e' => nums!(Day), - 'f' => num0!(Nanosecond), - 'g' => num0!(IsoYearMod100), - 'j' => num0!(Ordinal), - 'k' => nums!(Hour), - 'l' => nums!(Hour12), - 'm' => num0!(Month), - 'n' => sp!("\n"), - 'p' => fix!(UpperAmPm), - 'r' => recons![ - num0!(Hour12), - lit!(":"), - num0!(Minute), - lit!(":"), - num0!(Second), - sp!(" "), - fix!(UpperAmPm) - ], - 's' => num!(Timestamp), - 't' => sp!("\t"), - 'u' => num!(WeekdayFromMon), + 'U' => num0(WeekFromSun), + 'V' => num0(IsoWeek), + 'W' => num0(WeekFromMon), + #[cfg(not(feature = "unstable-locales"))] + 'X' => queue_from_slice!(T_FMT), + #[cfg(feature = "unstable-locales")] + 'X' => self.switch_to_locale_str(locales::t_fmt, T_FMT), + 'Y' => num0(Year), + 'Z' => fixed(Fixed::TimezoneName), + 'a' => fixed(Fixed::ShortWeekdayName), + 'b' | 'h' => fixed(Fixed::ShortMonthName), + #[cfg(not(feature = "unstable-locales"))] + 'c' => queue_from_slice!(D_T_FMT), + #[cfg(feature = "unstable-locales")] + 'c' => self.switch_to_locale_str(locales::d_t_fmt, D_T_FMT), + 'd' => num0(Day), + 'e' => nums(Day), + 'f' => num0(Nanosecond), + 'g' => num0(IsoYearMod100), + 'j' => num0(Ordinal), + 'k' => nums(Hour), + 'l' => nums(Hour12), + 'm' => num0(Month), + 'n' => Space("\n"), + 'p' => fixed(Fixed::UpperAmPm), + #[cfg(not(feature = "unstable-locales"))] + 'r' => queue_from_slice!(T_FMT_AMPM), + #[cfg(feature = "unstable-locales")] + 'r' => { + if self.locale.is_some() + && locales::t_fmt_ampm(self.locale.unwrap()).is_empty() + { + // 12-hour clock not supported by this locale. Switch to 24-hour format. + self.switch_to_locale_str(locales::t_fmt, T_FMT) + } else { + self.switch_to_locale_str(locales::t_fmt_ampm, T_FMT_AMPM) + } + } + 's' => num(Timestamp), + 't' => Space("\t"), + 'u' => num(WeekdayFromMon), 'v' => { - recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"), num0!(Year)] + queue![ + nums(Day), + Literal("-"), + fixed(Fixed::ShortMonthName), + Literal("-"), + num0(Year) + ] } - 'w' => num!(NumDaysFromSun), - 'x' => recons_from_slice!(self.d_fmt), - 'y' => num0!(YearMod100), + 'w' => num(NumDaysFromSun), + #[cfg(not(feature = "unstable-locales"))] + 'x' => queue_from_slice!(D_FMT), + #[cfg(feature = "unstable-locales")] + 'x' => self.switch_to_locale_str(locales::d_fmt, D_FMT), + 'y' => num0(YearMod100), 'z' => { if is_alternate { - internal_fix!(TimezoneOffsetPermissive) + internal_fixed(TimezoneOffsetPermissive) } else { - fix!(TimezoneOffset) + fixed(Fixed::TimezoneOffset) } } - '+' => fix!(RFC3339), + '+' => fixed(Fixed::RFC3339), ':' => { - if self.remainder.starts_with("::z") { - self.remainder = &self.remainder[3..]; - fix!(TimezoneOffsetTripleColon) - } else if self.remainder.starts_with(":z") { - self.remainder = &self.remainder[2..]; - fix!(TimezoneOffsetDoubleColon) - } else if self.remainder.starts_with('z') { - self.remainder = &self.remainder[1..]; - fix!(TimezoneOffsetColon) + if remainder.starts_with("::z") { + remainder = &remainder[3..]; + fixed(Fixed::TimezoneOffsetTripleColon) + } else if remainder.starts_with(":z") { + remainder = &remainder[2..]; + fixed(Fixed::TimezoneOffsetDoubleColon) + } else if remainder.starts_with('z') { + remainder = &remainder[1..]; + fixed(Fixed::TimezoneOffsetColon) } else { Item::Error } } '.' => match next!() { '3' => match next!() { - 'f' => fix!(Nanosecond3), + 'f' => fixed(Fixed::Nanosecond3), _ => Item::Error, }, '6' => match next!() { - 'f' => fix!(Nanosecond6), + 'f' => fixed(Fixed::Nanosecond6), _ => Item::Error, }, '9' => match next!() { - 'f' => fix!(Nanosecond9), + 'f' => fixed(Fixed::Nanosecond9), _ => Item::Error, }, - 'f' => fix!(Nanosecond), + 'f' => fixed(Fixed::Nanosecond), _ => Item::Error, }, '3' => match next!() { - 'f' => internal_fix!(Nanosecond3NoDot), + 'f' => internal_fixed(Nanosecond3NoDot), _ => Item::Error, }, '6' => match next!() { - 'f' => internal_fix!(Nanosecond6NoDot), + 'f' => internal_fixed(Nanosecond6NoDot), _ => Item::Error, }, '9' => match next!() { - 'f' => internal_fix!(Nanosecond9NoDot), + 'f' => internal_fixed(Nanosecond9NoDot), _ => Item::Error, }, - '%' => lit!("%"), + '%' => Literal("%"), _ => Item::Error, // no such specifier }; - // adjust `item` if we have any padding modifier + // Adjust `item` if we have any padding modifier. + // Not allowed on non-numeric items or on specifiers composed out of multiple + // formatting items. if let Some(new_pad) = pad_override { match item { - Item::Numeric(ref kind, _pad) if self.recons.is_empty() => { - Some(Item::Numeric(kind.clone(), new_pad)) + Item::Numeric(ref kind, _pad) if self.queue.is_empty() => { + Some((remainder, Item::Numeric(kind.clone(), new_pad))) } - _ => Some(Item::Error), // no reconstructed or non-numeric item allowed + _ => Some((remainder, Item::Error)), } } else { - Some(item) + Some((remainder, item)) } } // the next item is space Some(c) if c.is_whitespace() => { // `%` is not a whitespace, so `c != '%'` is redundant - let nextspec = self - .remainder - .find(|c: char| !c.is_whitespace()) - .unwrap_or(self.remainder.len()); + let nextspec = + remainder.find(|c: char| !c.is_whitespace()).unwrap_or(remainder.len()); assert!(nextspec > 0); - let item = sp!(&self.remainder[..nextspec]); - self.remainder = &self.remainder[nextspec..]; - Some(item) + let item = Space(&remainder[..nextspec]); + remainder = &remainder[nextspec..]; + Some((remainder, item)) } // the next item is literal _ => { - let nextspec = self - .remainder + let nextspec = remainder .find(|c: char| c.is_whitespace() || c == '%') - .unwrap_or(self.remainder.len()); + .unwrap_or(remainder.len()); assert!(nextspec > 0); - let item = lit!(&self.remainder[..nextspec]); - self.remainder = &self.remainder[nextspec..]; - Some(item) + let item = Literal(&remainder[..nextspec]); + remainder = &remainder[nextspec..]; + Some((remainder, item)) } } } + + #[cfg(feature = "unstable-locales")] + fn switch_to_locale_str( + &mut self, + localized_fmt_str: impl Fn(Locale) -> &'static str, + fallback: &'static [Item<'static>], + ) -> Item<'a> { + if let Some(locale) = self.locale { + assert!(self.locale_str.is_empty()); + let (fmt_str, item) = self.parse_next_item(localized_fmt_str(locale)).unwrap(); + self.locale_str = fmt_str; + item + } else { + self.queue = &fallback[1..]; + fallback[0].clone() + } + } } #[cfg(test)] mod tests { + use super::StrftimeItems; + use crate::format::Item::{self, Literal, Space}; #[cfg(feature = "unstable-locales")] - use super::Locale; - use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad, StrftimeItems}; + use crate::format::Locale; + use crate::format::{fixed, internal_fixed, num, num0, nums}; + use crate::format::{Fixed, InternalInternal, Numeric::*}; + #[cfg(any(feature = "alloc", feature = "std"))] use crate::{DateTime, FixedOffset, NaiveDate, TimeZone, Timelike, Utc}; #[test] @@ -526,97 +515,122 @@ mod tests { } assert_eq!(parse_and_collect(""), []); - assert_eq!(parse_and_collect(" "), [sp!(" ")]); - assert_eq!(parse_and_collect(" "), [sp!(" ")]); + assert_eq!(parse_and_collect(" "), [Space(" ")]); + assert_eq!(parse_and_collect(" "), [Space(" ")]); // ne! - assert_ne!(parse_and_collect(" "), [sp!(" "), sp!(" ")]); + assert_ne!(parse_and_collect(" "), [Space(" "), Space(" ")]); // eq! - assert_eq!(parse_and_collect(" "), [sp!(" ")]); - assert_eq!(parse_and_collect("a"), [lit!("a")]); - assert_eq!(parse_and_collect("ab"), [lit!("ab")]); - assert_eq!(parse_and_collect("😽"), [lit!("😽")]); - assert_eq!(parse_and_collect("a😽"), [lit!("a😽")]); - assert_eq!(parse_and_collect("😽a"), [lit!("😽a")]); - assert_eq!(parse_and_collect(" 😽"), [sp!(" "), lit!("😽")]); - assert_eq!(parse_and_collect("😽 "), [lit!("😽"), sp!(" ")]); + assert_eq!(parse_and_collect(" "), [Space(" ")]); + assert_eq!(parse_and_collect("a"), [Literal("a")]); + assert_eq!(parse_and_collect("ab"), [Literal("ab")]); + assert_eq!(parse_and_collect("😽"), [Literal("😽")]); + assert_eq!(parse_and_collect("a😽"), [Literal("a😽")]); + assert_eq!(parse_and_collect("😽a"), [Literal("😽a")]); + assert_eq!(parse_and_collect(" 😽"), [Space(" "), Literal("😽")]); + assert_eq!(parse_and_collect("😽 "), [Literal("😽"), Space(" ")]); // ne! - assert_ne!(parse_and_collect("😽😽"), [lit!("😽")]); - assert_ne!(parse_and_collect("😽"), [lit!("😽😽")]); - assert_ne!(parse_and_collect("😽😽"), [lit!("😽😽"), lit!("😽")]); + assert_ne!(parse_and_collect("😽😽"), [Literal("😽")]); + assert_ne!(parse_and_collect("😽"), [Literal("😽😽")]); + assert_ne!(parse_and_collect("😽😽"), [Literal("😽😽"), Literal("😽")]); // eq! - assert_eq!(parse_and_collect("😽😽"), [lit!("😽😽")]); - assert_eq!(parse_and_collect(" \t\n\r "), [sp!(" \t\n\r ")]); - assert_eq!(parse_and_collect("hello?"), [lit!("hello?")]); + assert_eq!(parse_and_collect("😽😽"), [Literal("😽😽")]); + assert_eq!(parse_and_collect(" \t\n\r "), [Space(" \t\n\r ")]); + assert_eq!(parse_and_collect("hello?"), [Literal("hello?")]); assert_eq!( parse_and_collect("a b\t\nc"), - [lit!("a"), sp!(" "), lit!("b"), sp!("\t\n"), lit!("c")] + [Literal("a"), Space(" "), Literal("b"), Space("\t\n"), Literal("c")] + ); + assert_eq!(parse_and_collect("100%%"), [Literal("100"), Literal("%")]); + assert_eq!( + parse_and_collect("100%% ok"), + [Literal("100"), Literal("%"), Space(" "), Literal("ok")] ); - assert_eq!(parse_and_collect("100%%"), [lit!("100"), lit!("%")]); - assert_eq!(parse_and_collect("100%% ok"), [lit!("100"), lit!("%"), sp!(" "), lit!("ok")]); - assert_eq!(parse_and_collect("%%PDF-1.0"), [lit!("%"), lit!("PDF-1.0")]); + assert_eq!(parse_and_collect("%%PDF-1.0"), [Literal("%"), Literal("PDF-1.0")]); assert_eq!( parse_and_collect("%Y-%m-%d"), - [num0!(Year), lit!("-"), num0!(Month), lit!("-"), num0!(Day)] + [num0(Year), Literal("-"), num0(Month), Literal("-"), num0(Day)] + ); + assert_eq!(parse_and_collect("😽 "), [Literal("😽"), Space(" ")]); + assert_eq!(parse_and_collect("😽😽"), [Literal("😽😽")]); + assert_eq!(parse_and_collect("😽😽😽"), [Literal("😽😽😽")]); + assert_eq!(parse_and_collect("😽😽 😽"), [Literal("😽😽"), Space(" "), Literal("😽")]); + assert_eq!(parse_and_collect("😽😽a 😽"), [Literal("😽😽a"), Space(" "), Literal("😽")]); + assert_eq!(parse_and_collect("😽😽a b😽"), [Literal("😽😽a"), Space(" "), Literal("b😽")]); + assert_eq!( + parse_and_collect("😽😽a b😽c"), + [Literal("😽😽a"), Space(" "), Literal("b😽c")] + ); + assert_eq!(parse_and_collect("😽😽 "), [Literal("😽😽"), Space(" ")]); + assert_eq!(parse_and_collect("😽😽 😽"), [Literal("😽😽"), Space(" "), Literal("😽")]); + assert_eq!(parse_and_collect(" 😽"), [Space(" "), Literal("😽")]); + assert_eq!(parse_and_collect(" 😽 "), [Space(" "), Literal("😽"), Space(" ")]); + assert_eq!( + parse_and_collect(" 😽 😽"), + [Space(" "), Literal("😽"), Space(" "), Literal("😽")] ); - assert_eq!(parse_and_collect("😽 "), [lit!("😽"), sp!(" ")]); - assert_eq!(parse_and_collect("😽😽"), [lit!("😽😽")]); - assert_eq!(parse_and_collect("😽😽😽"), [lit!("😽😽😽")]); - assert_eq!(parse_and_collect("😽😽 😽"), [lit!("😽😽"), sp!(" "), lit!("😽")]); - assert_eq!(parse_and_collect("😽😽a 😽"), [lit!("😽😽a"), sp!(" "), lit!("😽")]); - assert_eq!(parse_and_collect("😽😽a b😽"), [lit!("😽😽a"), sp!(" "), lit!("b😽")]); - assert_eq!(parse_and_collect("😽😽a b😽c"), [lit!("😽😽a"), sp!(" "), lit!("b😽c")]); - assert_eq!(parse_and_collect("😽😽 "), [lit!("😽😽"), sp!(" ")]); - assert_eq!(parse_and_collect("😽😽 😽"), [lit!("😽😽"), sp!(" "), lit!("😽")]); - assert_eq!(parse_and_collect(" 😽"), [sp!(" "), lit!("😽")]); - assert_eq!(parse_and_collect(" 😽 "), [sp!(" "), lit!("😽"), sp!(" ")]); - assert_eq!(parse_and_collect(" 😽 😽"), [sp!(" "), lit!("😽"), sp!(" "), lit!("😽")]); assert_eq!( parse_and_collect(" 😽 😽 "), - [sp!(" "), lit!("😽"), sp!(" "), lit!("😽"), sp!(" ")] + [Space(" "), Literal("😽"), Space(" "), Literal("😽"), Space(" ")] ); assert_eq!( parse_and_collect(" 😽 😽 "), - [sp!(" "), lit!("😽"), sp!(" "), lit!("😽"), sp!(" ")] + [Space(" "), Literal("😽"), Space(" "), Literal("😽"), Space(" ")] ); assert_eq!( parse_and_collect(" 😽 😽😽 "), - [sp!(" "), lit!("😽"), sp!(" "), lit!("😽😽"), sp!(" ")] + [Space(" "), Literal("😽"), Space(" "), Literal("😽😽"), Space(" ")] ); - assert_eq!(parse_and_collect(" 😽😽"), [sp!(" "), lit!("😽😽")]); - assert_eq!(parse_and_collect(" 😽😽 "), [sp!(" "), lit!("😽😽"), sp!(" ")]); - assert_eq!(parse_and_collect(" 😽😽 "), [sp!(" "), lit!("😽😽"), sp!(" ")]); - assert_eq!(parse_and_collect(" 😽😽 "), [sp!(" "), lit!("😽😽"), sp!(" ")]); - assert_eq!(parse_and_collect(" 😽😽 "), [sp!(" "), lit!("😽😽"), sp!(" ")]); + assert_eq!(parse_and_collect(" 😽😽"), [Space(" "), Literal("😽😽")]); + assert_eq!(parse_and_collect(" 😽😽 "), [Space(" "), Literal("😽😽"), Space(" ")]); + assert_eq!( + parse_and_collect(" 😽😽 "), + [Space(" "), Literal("😽😽"), Space(" ")] + ); + assert_eq!( + parse_and_collect(" 😽😽 "), + [Space(" "), Literal("😽😽"), Space(" ")] + ); + assert_eq!(parse_and_collect(" 😽😽 "), [Space(" "), Literal("😽😽"), Space(" ")]); assert_eq!( parse_and_collect(" 😽 😽😽 "), - [sp!(" "), lit!("😽"), sp!(" "), lit!("😽😽"), sp!(" ")] + [Space(" "), Literal("😽"), Space(" "), Literal("😽😽"), Space(" ")] ); assert_eq!( parse_and_collect(" 😽 πŸ˜½γ―γ„πŸ˜½ ハンバーガー"), - [sp!(" "), lit!("😽"), sp!(" "), lit!("πŸ˜½γ―γ„πŸ˜½"), sp!(" "), lit!("ハンバーガー")] + [ + Space(" "), + Literal("😽"), + Space(" "), + Literal("πŸ˜½γ―γ„πŸ˜½"), + Space(" "), + Literal("ハンバーガー") + ] ); - assert_eq!(parse_and_collect("%%😽%%😽"), [lit!("%"), lit!("😽"), lit!("%"), lit!("😽")]); - assert_eq!(parse_and_collect("%Y--%m"), [num0!(Year), lit!("--"), num0!(Month)]); + assert_eq!( + parse_and_collect("%%😽%%😽"), + [Literal("%"), Literal("😽"), Literal("%"), Literal("😽")] + ); + assert_eq!(parse_and_collect("%Y--%m"), [num0(Year), Literal("--"), num0(Month)]); assert_eq!(parse_and_collect("[%F]"), parse_and_collect("[%Y-%m-%d]")); - assert_eq!(parse_and_collect("100%%😽"), [lit!("100"), lit!("%"), lit!("😽")]); + assert_eq!(parse_and_collect("100%%😽"), [Literal("100"), Literal("%"), Literal("😽")]); assert_eq!( parse_and_collect("100%%😽%%a"), - [lit!("100"), lit!("%"), lit!("😽"), lit!("%"), lit!("a")] + [Literal("100"), Literal("%"), Literal("😽"), Literal("%"), Literal("a")] ); - assert_eq!(parse_and_collect("😽100%%"), [lit!("😽100"), lit!("%")]); - assert_eq!(parse_and_collect("%m %d"), [num0!(Month), sp!(" "), num0!(Day)]); + assert_eq!(parse_and_collect("😽100%%"), [Literal("😽100"), Literal("%")]); + assert_eq!(parse_and_collect("%m %d"), [num0(Month), Space(" "), num0(Day)]); assert_eq!(parse_and_collect("%"), [Item::Error]); - assert_eq!(parse_and_collect("%%"), [lit!("%")]); + assert_eq!(parse_and_collect("%%"), [Literal("%")]); assert_eq!(parse_and_collect("%%%"), [Item::Error]); - assert_eq!(parse_and_collect("%a"), [fix!(ShortWeekdayName)]); - assert_eq!(parse_and_collect("%aa"), [fix!(ShortWeekdayName), lit!("a")]); + assert_eq!(parse_and_collect("%a"), [fixed(Fixed::ShortWeekdayName)]); + assert_eq!(parse_and_collect("%aa"), [fixed(Fixed::ShortWeekdayName), Literal("a")]); assert_eq!(parse_and_collect("%%a%"), [Item::Error]); assert_eq!(parse_and_collect("%😽"), [Item::Error]); assert_eq!(parse_and_collect("%😽😽"), [Item::Error]); - assert_eq!(parse_and_collect("%%%%"), [lit!("%"), lit!("%")]); + assert_eq!(parse_and_collect("%%%%"), [Literal("%"), Literal("%")]); assert_eq!( parse_and_collect("%%%%ハンバーガー"), - [lit!("%"), lit!("%"), lit!("ハンバーガー")] + [Literal("%"), Literal("%"), Literal("ハンバーガー")] ); assert_eq!(parse_and_collect("foo%?"), [Item::Error]); assert_eq!(parse_and_collect("bar%42"), [Item::Error]); @@ -628,24 +642,28 @@ mod tests { assert_eq!(parse_and_collect("%_Z"), [Item::Error]); assert_eq!(parse_and_collect("%.j"), [Item::Error]); assert_eq!(parse_and_collect("%:j"), [Item::Error]); - assert_eq!(parse_and_collect("%-j"), [num!(Ordinal)]); - assert_eq!(parse_and_collect("%0j"), [num0!(Ordinal)]); - assert_eq!(parse_and_collect("%_j"), [nums!(Ordinal)]); + assert_eq!(parse_and_collect("%-j"), [num(Ordinal)]); + assert_eq!(parse_and_collect("%0j"), [num0(Ordinal)]); + assert_eq!(parse_and_collect("%_j"), [nums(Ordinal)]); assert_eq!(parse_and_collect("%.e"), [Item::Error]); assert_eq!(parse_and_collect("%:e"), [Item::Error]); - assert_eq!(parse_and_collect("%-e"), [num!(Day)]); - assert_eq!(parse_and_collect("%0e"), [num0!(Day)]); - assert_eq!(parse_and_collect("%_e"), [nums!(Day)]); - assert_eq!(parse_and_collect("%z"), [fix!(TimezoneOffset)]); - assert_eq!(parse_and_collect("%:z"), [fix!(TimezoneOffsetColon)]); - assert_eq!(parse_and_collect("%Z"), [fix!(TimezoneName)]); - assert_eq!(parse_and_collect("%ZZZZ"), [fix!(TimezoneName), lit!("ZZZ")]); - assert_eq!(parse_and_collect("%Z😽"), [fix!(TimezoneName), lit!("😽")]); - assert_eq!(parse_and_collect("%#z"), [internal_fix!(TimezoneOffsetPermissive)]); + assert_eq!(parse_and_collect("%-e"), [num(Day)]); + assert_eq!(parse_and_collect("%0e"), [num0(Day)]); + assert_eq!(parse_and_collect("%_e"), [nums(Day)]); + assert_eq!(parse_and_collect("%z"), [fixed(Fixed::TimezoneOffset)]); + assert_eq!(parse_and_collect("%:z"), [fixed(Fixed::TimezoneOffsetColon)]); + assert_eq!(parse_and_collect("%Z"), [fixed(Fixed::TimezoneName)]); + assert_eq!(parse_and_collect("%ZZZZ"), [fixed(Fixed::TimezoneName), Literal("ZZZ")]); + assert_eq!(parse_and_collect("%Z😽"), [fixed(Fixed::TimezoneName), Literal("😽")]); + assert_eq!( + parse_and_collect("%#z"), + [internal_fixed(InternalInternal::TimezoneOffsetPermissive)] + ); assert_eq!(parse_and_collect("%#m"), [Item::Error]); } #[test] + #[cfg(any(feature = "alloc", feature = "std"))] fn test_strftime_docs() { let dt = FixedOffset::east_opt(34200) .unwrap() @@ -758,8 +776,8 @@ mod tests { ); } - #[cfg(feature = "unstable-locales")] #[test] + #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))] fn test_strftime_docs_localized() { let dt = FixedOffset::east_opt(34200) .unwrap() @@ -785,7 +803,7 @@ mod tests { 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 "); + assert_eq!(dt.format_localized("%r", Locale::fr_BE).to_string(), "00:34:60"); // date & time specifiers assert_eq!( @@ -806,4 +824,96 @@ mod tests { assert_eq!(nd.format_localized("%F", Locale::de_DE).to_string(), "2001-07-08"); assert_eq!(nd.format_localized("%v", Locale::de_DE).to_string(), " 8-Jul-2001"); } + + /// Ensure parsing a timestamp with the parse-only stftime formatter "%#z" does + /// not cause a panic. + /// + /// See . + #[test] + #[cfg(any(feature = "alloc", feature = "std"))] + fn test_parse_only_timezone_offset_permissive_no_panic() { + use crate::NaiveDate; + use crate::{FixedOffset, TimeZone}; + use std::fmt::Write; + + let dt = FixedOffset::east_opt(34200) + .unwrap() + .from_local_datetime( + &NaiveDate::from_ymd_opt(2001, 7, 8) + .unwrap() + .and_hms_nano_opt(0, 34, 59, 1_026_490_708) + .unwrap(), + ) + .unwrap(); + + let mut buf = String::new(); + let _ = write!(buf, "{}", dt.format("%#z")).expect_err("parse-only formatter should fail"); + } + + #[test] + #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))] + fn test_strftime_localized_korean() { + let dt = FixedOffset::east_opt(34200) + .unwrap() + .with_ymd_and_hms(2001, 7, 8, 0, 34, 59) + .unwrap() + .with_nanosecond(1_026_490_708) + .unwrap(); + + // date specifiers + assert_eq!(dt.format_localized("%b", Locale::ko_KR).to_string(), " 7μ›”"); + assert_eq!(dt.format_localized("%B", Locale::ko_KR).to_string(), "7μ›”"); + assert_eq!(dt.format_localized("%h", Locale::ko_KR).to_string(), " 7μ›”"); + assert_eq!(dt.format_localized("%a", Locale::ko_KR).to_string(), "일"); + assert_eq!(dt.format_localized("%A", Locale::ko_KR).to_string(), "μΌμš”μΌ"); + assert_eq!(dt.format_localized("%D", Locale::ko_KR).to_string(), "07/08/01"); + assert_eq!(dt.format_localized("%x", Locale::ko_KR).to_string(), "2001λ…„ 07μ›” 08일"); + assert_eq!(dt.format_localized("%F", Locale::ko_KR).to_string(), "2001-07-08"); + assert_eq!(dt.format_localized("%v", Locale::ko_KR).to_string(), " 8- 7μ›”-2001"); + assert_eq!(dt.format_localized("%r", Locale::ko_KR).to_string(), "μ˜€μ „ 12μ‹œ 34λΆ„ 60초"); + + // date & time specifiers + assert_eq!( + dt.format_localized("%c", Locale::ko_KR).to_string(), + "2001λ…„ 07μ›” 08일 (일) μ˜€μ „ 12μ‹œ 34λΆ„ 60초" + ); + } + + #[test] + #[cfg(all(feature = "unstable-locales", any(feature = "alloc", feature = "std")))] + fn test_strftime_localized_japanese() { + let dt = FixedOffset::east_opt(34200) + .unwrap() + .with_ymd_and_hms(2001, 7, 8, 0, 34, 59) + .unwrap() + .with_nanosecond(1_026_490_708) + .unwrap(); + + // date specifiers + assert_eq!(dt.format_localized("%b", Locale::ja_JP).to_string(), " 7月"); + assert_eq!(dt.format_localized("%B", Locale::ja_JP).to_string(), "7月"); + assert_eq!(dt.format_localized("%h", Locale::ja_JP).to_string(), " 7月"); + assert_eq!(dt.format_localized("%a", Locale::ja_JP).to_string(), "ζ—₯"); + assert_eq!(dt.format_localized("%A", Locale::ja_JP).to_string(), "ζ—₯ζ›œζ—₯"); + assert_eq!(dt.format_localized("%D", Locale::ja_JP).to_string(), "07/08/01"); + assert_eq!(dt.format_localized("%x", Locale::ja_JP).to_string(), "2001εΉ΄07月08ζ—₯"); + assert_eq!(dt.format_localized("%F", Locale::ja_JP).to_string(), "2001-07-08"); + assert_eq!(dt.format_localized("%v", Locale::ja_JP).to_string(), " 8- 7月-2001"); + assert_eq!(dt.format_localized("%r", Locale::ja_JP).to_string(), "εˆε‰12ζ™‚34εˆ†60η§’"); + + // date & time specifiers + assert_eq!( + dt.format_localized("%c", Locale::ja_JP).to_string(), + "2001εΉ΄07月08ζ—₯ 00ζ™‚34εˆ†60η§’" + ); + } + + #[test] + #[cfg(feature = "unstable-locales")] + fn test_type_sizes() { + use core::mem::size_of; + assert_eq!(size_of::(), 24); + assert_eq!(size_of::(), 56); + assert_eq!(size_of::(), 2); + } } diff --git a/src/lib.rs b/src/lib.rs index 5429316d4a..2cf1b196ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -373,7 +373,6 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![warn(unreachable_pub)] -#![deny(dead_code)] #![deny(clippy::tests_outside_test_module)] #![cfg_attr(not(any(feature = "std", test)), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] diff --git a/src/naive/date.rs b/src/naive/date.rs index e98615b458..c7040c8729 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -3,7 +3,7 @@ //! ISO 8601 calendar date without timezone. -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use core::borrow::Borrow; use core::iter::FusedIterator; use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign}; @@ -16,7 +16,7 @@ use rkyv::{Archive, Deserialize, Serialize}; #[cfg(feature = "unstable-locales")] use pure_rust_locales::Locale; -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use crate::format::DelayedFormat; use crate::format::{ parse, parse_and_remainder, write_hundreds, Item, Numeric, Pad, ParseError, ParseResult, @@ -284,9 +284,14 @@ impl NaiveDate { /// assert!(from_ymd_opt(-400000, 1, 1).is_none()); /// ``` #[must_use] - pub fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option { + pub const fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option { let flags = YearFlags::from_year(year); - NaiveDate::from_mdf(year, Mdf::new(month, day, flags)?) + + if let Some(mdf) = Mdf::new(month, day, flags) { + NaiveDate::from_mdf(year, mdf) + } else { + None + } } /// Makes a new `NaiveDate` from the [ordinal date](#ordinal-date) @@ -1245,7 +1250,7 @@ impl NaiveDate { /// # let d = NaiveDate::from_ymd_opt(2015, 9, 5).unwrap(); /// assert_eq!(format!("{}", d.format_with_items(fmt)), "2015-09-05"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -1289,7 +1294,7 @@ impl NaiveDate { /// assert_eq!(format!("{}", d.format("%Y-%m-%d")), "2015-09-05"); /// assert_eq!(format!("{}", d.format("%A, %-d %B, %C%y")), "Saturday, 5 September, 2015"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -2250,15 +2255,6 @@ mod serde { formatter.write_str("a formatted date string") } - #[cfg(any(feature = "std", test))] - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - value.parse().map_err(E::custom) - } - - #[cfg(not(any(feature = "std", test)))] fn visit_str(self, value: &str) -> Result where E: de::Error, @@ -3028,6 +3024,7 @@ mod tests { } #[test] + #[cfg(any(feature = "alloc", feature = "std"))] fn test_date_format() { let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap(); assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12"); diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index d10ea89d63..94cbf14fc7 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -3,7 +3,7 @@ //! ISO 8601 date and time without timezone. -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use core::borrow::Borrow; use core::fmt::Write; use core::ops::{Add, AddAssign, Sub, SubAssign}; @@ -12,7 +12,7 @@ use core::{fmt, str}; #[cfg(feature = "rkyv")] use rkyv::{Archive, Deserialize, Serialize}; -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use crate::format::DelayedFormat; use crate::format::{parse, parse_and_remainder, ParseError, ParseResult, Parsed, StrftimeItems}; use crate::format::{Fixed, Item, Numeric, Pad}; @@ -851,7 +851,7 @@ impl NaiveDateTime { /// # let dt = NaiveDate::from_ymd_opt(2015, 9, 5).unwrap().and_hms_opt(23, 56, 4).unwrap(); /// assert_eq!(format!("{}", dt.format_with_items(fmt)), "2015-09-05 23:56:04"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -895,7 +895,7 @@ impl NaiveDateTime { /// assert_eq!(format!("{}", dt.format("%Y-%m-%d %H:%M:%S")), "2015-09-05 23:56:04"); /// assert_eq!(format!("{}", dt.format("around %l %p on %b %-d")), "around 11 PM on Sep 5"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] diff --git a/src/naive/datetime/tests.rs b/src/naive/datetime/tests.rs index 5d9f8e9fc1..4f98678357 100644 --- a/src/naive/datetime/tests.rs +++ b/src/naive/datetime/tests.rs @@ -17,10 +17,11 @@ fn test_datetime_from_timestamp_millis() { (2034061609000, "2034-06-16 09:06:49.000000000"), ]; - for (timestamp_millis, formatted) in valid_map.iter().copied() { + for (timestamp_millis, _formatted) in valid_map.iter().copied() { let naive_datetime = NaiveDateTime::from_timestamp_millis(timestamp_millis); assert_eq!(timestamp_millis, naive_datetime.unwrap().timestamp_millis()); - assert_eq!(naive_datetime.unwrap().format("%F %T%.9f").to_string(), formatted); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!(naive_datetime.unwrap().format("%F %T%.9f").to_string(), _formatted); } let invalid = [i64::MAX, i64::MIN]; @@ -54,10 +55,11 @@ fn test_datetime_from_timestamp_micros() { (2034061609000000, "2034-06-16 09:06:49.000000000"), ]; - for (timestamp_micros, formatted) in valid_map.iter().copied() { + for (timestamp_micros, _formatted) in valid_map.iter().copied() { let naive_datetime = NaiveDateTime::from_timestamp_micros(timestamp_micros); assert_eq!(timestamp_micros, naive_datetime.unwrap().timestamp_micros()); - assert_eq!(naive_datetime.unwrap().format("%F %T%.9f").to_string(), formatted); + #[cfg(any(feature = "alloc", feature = "std"))] + assert_eq!(naive_datetime.unwrap().format("%F %T%.9f").to_string(), _formatted); } let invalid = [i64::MAX, i64::MIN]; @@ -327,6 +329,7 @@ fn test_datetime_parse_from_str() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_datetime_format() { let dt = NaiveDate::from_ymd_opt(2010, 9, 8).unwrap().and_hms_milli_opt(7, 6, 54, 321).unwrap(); assert_eq!(dt.format("%c").to_string(), "Wed Sep 8 07:06:54 2010"); diff --git a/src/naive/isoweek.rs b/src/naive/isoweek.rs index df5556a120..8edf738ba0 100644 --- a/src/naive/isoweek.rs +++ b/src/naive/isoweek.rs @@ -158,11 +158,13 @@ mod tests { assert_eq!(minweek.year(), internals::MIN_YEAR); assert_eq!(minweek.week(), 1); assert_eq!(minweek.week0(), 0); + #[cfg(any(feature = "alloc", feature = "std"))] assert_eq!(format!("{:?}", minweek), NaiveDate::MIN.format("%G-W%V").to_string()); assert_eq!(maxweek.year(), internals::MAX_YEAR + 1); assert_eq!(maxweek.week(), 1); assert_eq!(maxweek.week0(), 0); + #[cfg(any(feature = "alloc", feature = "std"))] assert_eq!(format!("{:?}", maxweek), NaiveDate::MAX.format("%G-W%V").to_string()); } diff --git a/src/naive/time/mod.rs b/src/naive/time/mod.rs index 856444f6fd..5481a18214 100644 --- a/src/naive/time/mod.rs +++ b/src/naive/time/mod.rs @@ -3,7 +3,7 @@ //! ISO 8601 time without timezone. -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use core::borrow::Borrow; use core::ops::{Add, AddAssign, Sub, SubAssign}; use core::{fmt, str}; @@ -11,7 +11,7 @@ use core::{fmt, str}; #[cfg(feature = "rkyv")] use rkyv::{Archive, Deserialize, Serialize}; -#[cfg(any(feature = "alloc", feature = "std", test))] +#[cfg(any(feature = "alloc", feature = "std"))] use crate::format::DelayedFormat; use crate::format::{ parse, parse_and_remainder, write_hundreds, Fixed, Item, Numeric, Pad, ParseError, ParseResult, @@ -96,7 +96,8 @@ mod tests; /// (short for [`NaiveTime::overflowing_add_signed`](#method.overflowing_add_signed)): /// /// - `03:00:00 + 1s = 03:00:01`. -/// - `03:00:59 + 60s = 03:02:00`. +/// - `03:00:59 + 60s = 03:01:59`. +/// - `03:00:59 + 61s = 03:02:00`. /// - `03:00:59 + 1s = 03:01:00`. /// - `03:00:60 + 1s = 03:01:00`. /// Note that the sum is identical to the previous. @@ -741,7 +742,7 @@ impl NaiveTime { /// # let t = NaiveTime::from_hms_opt(23, 56, 4).unwrap(); /// assert_eq!(format!("{}", t.format_with_items(fmt)), "23:56:04"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] @@ -787,7 +788,7 @@ impl NaiveTime { /// assert_eq!(format!("{}", t.format("%H:%M:%S%.6f")), "23:56:04.012345"); /// assert_eq!(format!("{}", t.format("%-I:%M %p")), "11:56 PM"); /// ``` - #[cfg(any(feature = "alloc", feature = "std", test))] + #[cfg(any(feature = "alloc", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] #[inline] #[must_use] diff --git a/src/naive/time/tests.rs b/src/naive/time/tests.rs index 4aba43434b..c8759b06e9 100644 --- a/src/naive/time/tests.rs +++ b/src/naive/time/tests.rs @@ -346,6 +346,7 @@ fn test_time_parse_from_str() { } #[test] +#[cfg(any(feature = "alloc", feature = "std"))] fn test_time_format() { let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap(); assert_eq!(t.format("%H,%k,%I,%l,%P,%p").to_string(), "03, 3,03, 3,am,AM"); diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index f0b77479ea..13c1b72ca8 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -5,15 +5,16 @@ use core::fmt; use core::ops::{Add, Sub}; +use core::str::FromStr; #[cfg(feature = "rkyv")] use rkyv::{Archive, Deserialize, Serialize}; use super::{LocalResult, Offset, TimeZone}; +use crate::format::{scan, OUT_OF_RANGE}; use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime}; use crate::time_delta::TimeDelta; -use crate::DateTime; -use crate::Timelike; +use crate::{DateTime, ParseError, Timelike}; /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. /// @@ -113,6 +114,15 @@ impl FixedOffset { } } +/// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime). +impl FromStr for FixedOffset { + type Err = ParseError; + fn from_str(s: &str) -> Result { + let (_, offset) = scan::timezone_offset(s, scan::consume_colon_maybe, false, false, true)?; + Self::east_opt(offset).ok_or(OUT_OF_RANGE) + } +} + impl TimeZone for FixedOffset { type Offset = FixedOffset; @@ -246,6 +256,7 @@ impl Sub for DateTime { mod tests { use super::FixedOffset; use crate::offset::TimeZone; + use std::str::FromStr; #[test] fn test_date_extreme_offset() { @@ -292,4 +303,14 @@ mod tests { "2012-03-04T05:06:07-23:59:59".to_string() ); } + + #[test] + fn test_parse_offset() { + let offset = FixedOffset::from_str("-0500").unwrap(); + assert_eq!(offset.local_minus_utc, -5 * 3600); + let offset = FixedOffset::from_str("-08:00").unwrap(); + assert_eq!(offset.local_minus_utc, -8 * 3600); + let offset = FixedOffset::from_str("+06:30").unwrap(); + assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800); + } } diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index d2de060237..36b2808105 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -486,6 +486,11 @@ struct TimeZoneName { impl TimeZoneName { /// Construct a time zone name + /// + /// man tzfile(5): + /// Time zone designations should consist of at least three (3) and no more than six (6) ASCII + /// characters from the set of alphanumerics, β€œ-”, and β€œ+”. This is for compatibility with + /// POSIX requirements for time zone abbreviations. fn new(input: &[u8]) -> Result { let len = input.len(); @@ -743,18 +748,17 @@ mod tests { #[test] fn test_tz_ascii_str() -> Result<(), Error> { assert!(matches!(TimeZoneName::new(b""), Err(Error::LocalTimeType(_)))); - assert!(matches!(TimeZoneName::new(b"1"), Err(Error::LocalTimeType(_)))); - assert!(matches!(TimeZoneName::new(b"12"), Err(Error::LocalTimeType(_)))); - assert_eq!(TimeZoneName::new(b"123")?.as_bytes(), b"123"); - assert_eq!(TimeZoneName::new(b"1234")?.as_bytes(), b"1234"); - assert_eq!(TimeZoneName::new(b"12345")?.as_bytes(), b"12345"); - assert_eq!(TimeZoneName::new(b"123456")?.as_bytes(), b"123456"); - assert_eq!(TimeZoneName::new(b"1234567")?.as_bytes(), b"1234567"); + assert!(matches!(TimeZoneName::new(b"A"), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"AB"), Err(Error::LocalTimeType(_)))); + assert_eq!(TimeZoneName::new(b"CET")?.as_bytes(), b"CET"); + assert_eq!(TimeZoneName::new(b"CHADT")?.as_bytes(), b"CHADT"); + assert_eq!(TimeZoneName::new(b"abcdefg")?.as_bytes(), b"abcdefg"); + assert_eq!(TimeZoneName::new(b"UTC+02")?.as_bytes(), b"UTC+02"); + assert_eq!(TimeZoneName::new(b"-1230")?.as_bytes(), b"-1230"); + assert!(matches!(TimeZoneName::new("βˆ’0330".as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212) + assert!(matches!(TimeZoneName::new(b"\x00123"), Err(Error::LocalTimeType(_)))); assert!(matches!(TimeZoneName::new(b"12345678"), Err(Error::LocalTimeType(_)))); - assert!(matches!(TimeZoneName::new(b"123456789"), Err(Error::LocalTimeType(_)))); - assert!(matches!(TimeZoneName::new(b"1234567890"), Err(Error::LocalTimeType(_)))); - - assert!(matches!(TimeZoneName::new(b"123\0\0\0"), Err(Error::LocalTimeType(_)))); + assert!(matches!(TimeZoneName::new(b"GMT\0\0\0"), Err(Error::LocalTimeType(_)))); Ok(()) } diff --git a/src/round.rs b/src/round.rs index 435810ec21..a3489bc2dd 100644 --- a/src/round.rs +++ b/src/round.rs @@ -100,11 +100,11 @@ const fn span_for_digits(digits: u16) -> u32 { /// will also fail if the `TimeDelta` is bigger than the timestamp. pub trait DurationRound: Sized { /// Error that can occur in rounding or truncating - #[cfg(any(feature = "std", test))] + #[cfg(feature = "std")] type Err: std::error::Error; /// Error that can occur in rounding or truncating - #[cfg(not(any(feature = "std", test)))] + #[cfg(not(feature = "std"))] type Err: fmt::Debug + fmt::Display; /// Return a copy rounded by TimeDelta. @@ -299,7 +299,7 @@ impl fmt::Display for RoundingError { } } -#[cfg(any(feature = "std", test))] +#[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for RoundingError { #[allow(deprecated)] diff --git a/tests/dateutils.rs b/tests/dateutils.rs index 21ed1f1e79..a7c4c64bb6 100644 --- a/tests/dateutils.rs +++ b/tests/dateutils.rs @@ -1,14 +1,8 @@ -#[cfg(unix)] -use chrono::offset::TimeZone; -#[cfg(unix)] -use chrono::Local; -#[cfg(unix)] -use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike}; +#![cfg(all(unix, feature = "clock", feature = "std"))] -#[cfg(unix)] -use std::{path, process}; +use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike}; +use std::{path, process, thread}; -#[cfg(unix)] fn verify_against_date_command_local(path: &'static str, dt: NaiveDateTime) { let output = process::Command::new(path) .arg("-d") @@ -64,7 +58,6 @@ const DATE_PATH: &str = "/usr/bin/date"; const DATE_PATH: &str = "/opt/freeware/bin/date"; #[cfg(test)] -#[cfg(unix)] /// test helper to sanity check the date command behaves as expected /// asserts the command succeeded fn assert_run_date_version() { @@ -82,7 +75,6 @@ fn assert_run_date_version() { } #[test] -#[cfg(unix)] fn try_verify_against_date_command() { if !path::Path::new(DATE_PATH).exists() { eprintln!("date command {:?} not found, skipping", DATE_PATH); @@ -90,31 +82,26 @@ fn try_verify_against_date_command() { } assert_run_date_version(); - let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(); - eprintln!( - "Run command {:?} for every hour from {} to 2077, skipping some years...", + "Run command {:?} for every hour from 1975 to 2077, skipping some years...", DATE_PATH, - date.year() ); - let mut count: u64 = 0; - let mut year_at = date.year(); - while date.year() < 2078 { - if (1975..=1977).contains(&date.year()) - || (2020..=2022).contains(&date.year()) - || (2073..=2077).contains(&date.year()) - { - if date.year() != year_at { - eprintln!("at year {}...", date.year()); - year_at = date.year(); - } - verify_against_date_command_local(DATE_PATH, date); - count += 1; - } - date += chrono::TimeDelta::hours(1); + let mut children = vec![]; + for year in [1975, 1976, 1977, 2020, 2021, 2022, 2073, 2074, 2075, 2076, 2077].iter() { + children.push(thread::spawn(|| { + let mut date = NaiveDate::from_ymd_opt(*year, 1, 1).unwrap().and_time(NaiveTime::MIN); + let end = NaiveDate::from_ymd_opt(*year + 1, 1, 1).unwrap().and_time(NaiveTime::MIN); + while date <= end { + verify_against_date_command_local(DATE_PATH, date); + date += chrono::TimeDelta::hours(1); + } + })); + } + for child in children { + // Wait for the thread to finish. Returns a result. + let _ = child.join(); } - eprintln!("Command {:?} was run {} times", DATE_PATH, count); } #[cfg(target_os = "linux")] diff --git a/tests/wasm.rs b/tests/wasm.rs index 6863502bae..53d418feaf 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -8,6 +8,7 @@ #![cfg(all( target_arch = "wasm32", feature = "wasmbind", + feature = "clock", not(any(target_os = "emscripten", target_os = "wasi")) ))]