Merge branch '0.4.x'

This commit is contained in:
Paul Dicker 2023-07-05 12:26:05 +02:00
commit 121f5c7323
24 changed files with 2526 additions and 2013 deletions

View File

@ -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:

View File

@ -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 }

View File

@ -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::<Vec<_>>());
})
});
}
#[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::<Vec<_>>());
})
});
}
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);

View File

@ -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]

View File

@ -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<Tz: TimeZone> DateTime<Tz> {
}
}
/// 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<Utc>`.
pub const MIN_UTC: DateTime<Utc> = DateTime { datetime: NaiveDateTime::MIN, offset: Utc };
/// The maximum possible `DateTime<Utc>`.
@ -757,101 +851,8 @@ impl<Tz: TimeZone> DateTime<Tz>
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<Local> {
}
}
#[cfg(any(feature = "std", test))]
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl From<SystemTime> for DateTime<Utc> {
fn from(t: SystemTime) -> DateTime<Utc> {
@ -1383,7 +1384,7 @@ impl From<SystemTime> for DateTime<Local> {
}
}
#[cfg(any(feature = "std", test))]
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl<Tz: TimeZone> From<DateTime<Tz>> for SystemTime {
fn from(dt: DateTime<Tz>) -> SystemTime {
@ -1548,7 +1549,7 @@ fn test_decodable_json<FUtc, FFixed, FLocal, E>(
// 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!(

View File

@ -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]

View File

@ -1,5 +1,3 @@
use std::time::{SystemTime, UNIX_EPOCH};
use super::DateTime;
use crate::naive::{NaiveDate, NaiveTime};
use crate::offset::{FixedOffset, TimeZone, Utc};
@ -449,6 +447,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 +552,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 +678,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 +1222,9 @@ fn test_subsecond_part() {
}
#[test]
#[cfg(not(target_os = "windows"))]
#[cfg(feature = "std")]
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<Utc>
assert_eq!(DateTime::<Utc>::from(UNIX_EPOCH), epoch);
assert_eq!(
DateTime::<Utc>::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::<Utc>::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<Utc> -> 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<any tz> -> 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")]
fn test_from_system_time() {
use std::time::Duration;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
let nanos = 999_999_000;
@ -1365,6 +1296,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();

821
src/format/formatting.rs Normal file
View File

@ -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<Locale>) -> 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<I> {
/// The date view, if any.
date: Option<NaiveDate>,
/// The time view, if any.
time: Option<NaiveTime>,
/// 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<Locale>,
}
#[cfg(any(feature = "alloc", feature = "std"))]
impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
/// Makes a new `DelayedFormat` value out of local date and time.
#[must_use]
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<I> {
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<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
) -> DelayedFormat<I>
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<NaiveDate>,
time: Option<NaiveTime>,
items: I,
locale: Locale,
) -> DelayedFormat<I> {
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<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
locale: Locale,
) -> DelayedFormat<I>
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<Item = B> + Clone, B: Borrow<Item<'a>>> fmt::Display for DelayedFormat<I> {
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<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
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<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
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<Locale>,
) -> 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"],
],
);
}
}

View File

@ -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)
}

View File

@ -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<str>),
/// 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<str>),
/// 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)
};
const fn num0(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Zero)
}
macro_rules! num {
($x:ident) => {
Item::Numeric(Numeric::$x, Pad::None)
};
const fn nums(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Space)
}
macro_rules! num0 {
($x:ident) => {
Item::Numeric(Numeric::$x, Pad::Zero)
};
const fn fixed(fixed: Fixed) -> Item<'static> {
Item::Fixed(fixed)
}
macro_rules! nums {
($x:ident) => {
Item::Numeric(Numeric::$x, Pad::Space)
};
}
macro_rules! fix {
($x:ident) => {
Item::Fixed(Fixed::$x)
};
}
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<Locale>) -> 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<Locale>,
) -> 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<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
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<I> {
/// The date view, if any.
date: Option<NaiveDate>,
/// The time view, if any.
time: Option<NaiveTime>,
/// 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<Locale>,
}
#[cfg(any(feature = "alloc", feature = "std", test))]
impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
/// Makes a new `DelayedFormat` value out of local date and time.
#[must_use]
pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<I> {
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<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
) -> DelayedFormat<I>
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<NaiveDate>,
time: Option<NaiveTime>,
items: I,
locale: Locale,
) -> DelayedFormat<I> {
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<Off>(
date: Option<NaiveDate>,
time: Option<NaiveTime>,
offset: &Off,
items: I,
locale: Locale,
) -> DelayedFormat<I>
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<Item = B> + Clone, B: Borrow<Item<'a>>> fmt::Display for DelayedFormat<I> {
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<Item = B> + Clone,
B: Borrow<Item<'a>>,
{
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

File diff suppressed because it is too large Load Diff

View File

@ -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<F>(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<F>(
/// 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<F>(
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<F>(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<F>(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<i32>)>
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<i32>)>
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.

View File

@ -65,7 +65,7 @@ The following specifiers are available both to formatting and parsing.
| `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. |
| `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. |
| `%X` | `00:34:60` | 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] |
@ -180,39 +180,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<Item<'a>>;
#[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 +191,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<Locale>,
}
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 +228,78 @@ impl<'a> Iterator for StrftimeItems<'a> {
type Item = Item<'a>;
fn next(&mut self) -> Option<Item<'a>> {
// 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 +314,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' => 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),
'v' => {
recons![nums!(Day), lit!("-"), fix!(ShortMonthName), lit!("-"), num0!(Year)]
'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)]
}
'w' => num!(NumDaysFromSun),
'x' => recons_from_slice!(self.d_fmt),
'y' => num0!(YearMod100),
'z' => {
if is_alternate {
internal_fix!(TimezoneOffsetPermissive)
'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 {
fix!(TimezoneOffset)
self.switch_to_locale_str(locales::t_fmt_ampm, T_FMT_AMPM)
}
}
'+' => fix!(RFC3339),
's' => num(Timestamp),
't' => Space("\t"),
'u' => num(WeekdayFromMon),
'v' => {
queue![
nums(Day),
Literal("-"),
fixed(Fixed::ShortMonthName),
Literal("-"),
num0(Year)
]
}
'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_fixed(TimezoneOffsetPermissive)
} else {
fixed(Fixed::TimezoneOffset)
}
}
'+' => 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 +539,100 @@ 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%%"), [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("100%%"), [Literal("100"), Literal("%")]);
assert_eq!(
parse_and_collect("100%% ok"),
[Literal("100"), Literal("%"), Space(" "), Literal("ok")]
);
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("😽 "), [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("😽 "), [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(" 😽 😽 "),
[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 +644,25 @@ 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(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 +775,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 +802,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 +823,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 <https://github.com/chronotope/chrono/issues/1139>.
#[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::<Item>(), 24);
assert_eq!(size_of::<StrftimeItems>(), 56);
assert_eq!(size_of::<Locale>(), 2);
}
}

View File

@ -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,
@ -1245,7 +1245,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 +1289,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 +2250,6 @@ mod serde {
formatter.write_str("a formatted date string")
}
#[cfg(any(feature = "std", test))]
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value.parse().map_err(E::custom)
}
#[cfg(not(any(feature = "std", test)))]
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
@ -3028,6 +3019,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");

View File

@ -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]

View File

@ -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");

View File

@ -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());
}

View File

@ -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]

View File

@ -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");

View File

@ -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<Self, Self::Err> {
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<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> {
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);
}
}

View File

@ -486,29 +486,49 @@ struct TimeZoneName {
impl TimeZoneName {
/// Construct a time zone name
///
/// Note: Converts `` MINUS SIGN (U+2212) to `-` HYPHEN-MINUS (U+2D).
/// Multi-byte MINUS SIGN is allowed in [ISO 8601 / RFC 3339]. But
/// working with single-byte HYPHEN-MINUS is easier and more common.
///
/// [ISO 8601 / RFC 3339]: https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=1114309368#Time_offsets_from_UTC
fn new(input: &[u8]) -> Result<Self, Error> {
let len = input.len();
let s = match str::from_utf8(input) {
Ok(s) => s,
Err(_err) => return Err(Error::LocalTimeType("invalid UTF-8")),
};
if !(3..=7).contains(&len) {
if !(3..=7).contains(&s.chars().count()) {
return Err(Error::LocalTimeType(
"time zone name must have between 3 and 7 characters",
));
}
let mut bytes = [0; 8];
bytes[0] = input.len() as u8;
let mut i = 0;
while i < len {
let b = input[i];
match b {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' => {}
let mut copied = 0;
for (i, c) in s.chars().enumerate() {
match c {
'0'..='9' | 'A'..='Z' | 'a'..='z'
// ISO 8601 / RFC 3339 proscribes use of `+` PLUS SIGN (U+2B)
// in timezone
| '+'
// ISO 8601 / RFC 3339 allows use of `-` HYPHEN-MINUS (U+2D)
// in timezone
| '-' => {
bytes[i + 1] = c as u8;
}
// ISO 8601 / RFC 3339 recommends the use of
// `` MINUS SIGN (U+2212) in timezone.
// But replace with single-byte `-` HYPHEN-MINUS (U+2D) for
// easier byte <-> char conversions later on.
| '' => {
bytes[i + 1] = b'-';
}
_ => return Err(Error::LocalTimeType("invalid characters in time zone name")),
}
bytes[i + 1] = b;
i += 1;
copied += 1;
}
bytes[0] = copied as u8;
Ok(Self { bytes })
}
@ -741,20 +761,63 @@ 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"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(_))));
fn test_timezonename_new() -> Result<(), Error> {
// expect Error::LocalTimeType()
const INPUT_ERR: &[&str] = &[
"",
"1",
"+",
"-",
"", // MINUS SIGN (U+2212)
"12",
"--",
"", // MINUS SIGN (U+2212)
"AB",
"ab",
"12345678",
"ABCDEFGH",
"123456789",
"1234567890",
"--------",
"123\0\0\0",
"\0\0\0",
"\x00123",
"123\0",
];
for input_ in INPUT_ERR.iter() {
eprintln!("TimeZoneName::new({:?}) (expect Error::LocalTimeType)", input_);
let input_ = input_.as_bytes();
let err = TimeZoneName::new(input_);
eprintln!("err = {:?}", err);
assert!(matches!(err, Err(Error::LocalTimeType(_))));
}
// expect Ok
const INPUT_OK_EXPECT: &[(&str, &str)] = &[
("123", "123"),
("abc", "abc"),
("ABC", "ABC"),
("1234", "1234"),
("12345", "12345"),
("123456", "123456"),
("1234567", "1234567"),
("+1234", "+1234"),
("+1234", "+1234"),
("-1234", "-1234"),
("1234", "-1234"), // MINUS SIGN (U+2212) to HYPHEN-MINUS (U+002D)
// Ok nonsense
("+++", "+++"),
("-----", "-----"),
("", "---"), // MINUS SIGN (U+2212) to HYPHEN-MINUS (U+002D)
("", "-------"), // MINUS SIGN (U+2212) to HYPHEN-MINUS (U+002D)
];
for (input_, expect) in INPUT_OK_EXPECT.iter() {
eprintln!("TimeZoneName::new({:?})", input_);
let output = TimeZoneName::new(input_.as_bytes());
match output {
Ok(output) => assert_eq!(output.as_bytes(), expect.as_bytes()),
Err(error) => panic!("Failed: input {:?}, error {}", input_, error),
}
}
Ok(())
}

View File

@ -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(any(feature = "std"))]
type Err: std::error::Error;
/// Error that can occur in rounding or truncating
#[cfg(not(any(feature = "std", test)))]
#[cfg(not(any(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(any(feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for RoundingError {
#[allow(deprecated)]

View File

@ -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")]

View File

@ -8,6 +8,7 @@
#![cfg(all(
target_arch = "wasm32",
feature = "wasmbind",
feature = "clock",
not(any(target_os = "emscripten", target_os = "wasi"))
))]