diff --git a/Cargo.toml b/Cargo.toml index b07d003c..e24d9515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "strftime-ruby" -version = "0.1.0" # remember to set `html_root_url` in `src/lib.rs`. -authors = ["Ryan Lopopolo "] +# remember to set `html_root_url` in `src/lib.rs`. +version = "0.1.0" +authors = ["Ryan Lopopolo ", "x-hgg-x"] license = "MIT" edition = "2021" rust-version = "1.58.0" @@ -24,6 +25,7 @@ std = ["alloc"] alloc = [] [dependencies] +bitflags = "1.3" [dev-dependencies] # Property testing for interner getters and setters. diff --git a/LICENSE b/LICENSE index 37c09a14..4c8354c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2022 Ryan Lopopolo +Copyright (c) 2022 Ryan Lopopolo and x-hgg-x. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e97af5eb..ba1d5ab7 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,11 @@ All features are enabled by default. - **std** - Enables a dependency on the Rust Standard Library. Activating this feature also activates the **alloc** feature. - **alloc** - Enables a dependency on the Rust [`alloc`] crate. Activating this - feature enables APIs that require [`alloc::string::String`]. + feature enables APIs that require [`alloc::vec::Vec`] or + [`alloc::string::String`]. [`alloc`]: https://doc.rust-lang.org/alloc/ +[`alloc::vec::vec`]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html [`alloc::string::string`]: https://doc.rust-lang.org/alloc/string/struct.String.html @@ -56,4 +58,5 @@ releases. ## License -`strftime-ruby` is licensed under the [MIT License](LICENSE) (c) Ryan Lopopolo. +`strftime-ruby` is licensed under the [MIT License](LICENSE) (c) Ryan Lopopolo +and x-hgg-x. diff --git a/src/format/assert.rs b/src/format/assert.rs new file mode 100644 index 00000000..f2acf718 --- /dev/null +++ b/src/format/assert.rs @@ -0,0 +1,50 @@ +//! Compile-time assert functions. + +/// Helper macro for implementing asserts. +macro_rules! assert_sorted_by_key { + ($s:expr, $f:expr) => {{ + let mut i = 0; + while i + 1 < $s.len() { + assert!(*$f(&$s[i]) < *$f(&$s[i + 1])); + i += 1; + } + $s + }}; +} + +/// Returns the first element of a tuple. +const fn elem_0(x: &(u8, T)) -> &u8 { + &x.0 +} + +/// Asserts that a slice is sorted and has no duplicates. +pub(crate) const fn assert_sorted(s: &[u8]) -> &[u8] { + assert_sorted_by_key!(s, core::convert::identity) +} + +/// Asserts that a slice is sorted by its first element and has no duplicates. +pub(crate) const fn assert_sorted_elem_0(s: &[(u8, T)]) -> &[(u8, T)] { + assert_sorted_by_key!(s, elem_0) +} + +/// Asserts that converting the first input to uppercase yields the second input. +#[allow(dead_code)] +pub(crate) const fn assert_to_ascii_uppercase(table: &[&str], upper_table: &[&str]) { + assert!(table.len() == upper_table.len()); + + let mut index = 0; + while index < table.len() { + let (s, upper_s) = (table[index].as_bytes(), upper_table[index].as_bytes()); + assert!(s.len() == upper_s.len()); + + let mut i = 0; + while i < s.len() { + assert!(s[i].is_ascii()); + assert!(upper_s[i].is_ascii()); + assert!(upper_s[i] == s[i].to_ascii_uppercase()); + i += 1; + } + + index += 1; + } +} diff --git a/src/format/mod.rs b/src/format/mod.rs new file mode 100644 index 00000000..2db15fdb --- /dev/null +++ b/src/format/mod.rs @@ -0,0 +1,892 @@ +//! Module containing the formatting logic. + +mod assert; +mod utils; +mod week; +mod write; + +use core::fmt; +use core::num::IntErrorKind; +use core::str; + +use bitflags::bitflags; + +use crate::{Error, Time}; +use assert::{assert_sorted, assert_sorted_elem_0, assert_to_ascii_uppercase}; +use utils::{Cursor, SizeLimiter}; +use week::{iso_8601_year_and_week_number, week_number, WeekStart}; +use write::Write; + +/// Alias to a `c_int`. +#[cfg(feature = "std")] +type Int = std::os::raw::c_int; +/// Fallback alias to a `c_int`. +#[cfg(not(feature = "std"))] +type Int = i32; + +/// List of weekday names. +const DAYS: [&str; 7] = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +/// List of uppercase weekday names. +const DAYS_UPPER: [&str; 7] = [ + "SUNDAY", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", +]; + +/// List of month names. +const MONTHS: [&str; 12] = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +/// List of uppercase month names. +const MONTHS_UPPER: [&str; 12] = [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER", +]; + +// Check day and month tables +const _: () = { + assert_to_ascii_uppercase(&DAYS, &DAYS_UPPER); + assert_to_ascii_uppercase(&MONTHS, &MONTHS_UPPER); +}; + +bitflags! { + /// Formatting flags. + struct Flags: u32 { + /// Use left padding, removing all other padding options in most cases. + const LEFT_PADDING = 1 << 0; + /// Change case for a string value. + const CHANGE_CASE = 1 << 1; + /// Convert a string value to uppercase. + const UPPER_CASE = 1 << 2; + } +} + +impl Flags { + /// Check if one of the case flags is set. + fn has_change_or_upper_case(self) -> bool { + let flag = Flags::CHANGE_CASE | Flags::UPPER_CASE; + !self.intersection(flag).is_empty() + } +} + +/// Padding method. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Padding { + /// Left padding. + Left, + /// Padding with spaces. + Spaces, + /// Padding with zeros. + Zeros, +} + +/// Formatting specifier. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Spec { + /// `"%Y"`: Year with century if provided, zero-padded to at least 4 digits plus the possible negative sign. + Year4Digits, + /// `"%C"`: `Year / 100` using Euclidian division, zero-padded to at least 2 digits. + YearDiv100, + /// `"%y"`: `Year % 100` in `00..=99`, using Euclidian remainder, zero-padded to 2 digits. + YearRem100, + /// `"%m"`: Month of the year in `01..=12`, zero-padded to 2 digits. + Month, + /// `"%B"`: Locale independent full month name. + MonthName, + /// `"%b"` and `"%h"`: Locale independent abbreviated month name, using the first 3 letters. + MonthNameAbbr, + /// `"%d"`: Day of the month in `01..=31`, zero-padded to 2 digits. + MonthDayZero, + /// `"%e"`: Day of the month in ` 1..=31`, blank-padded to 2 digits. + MonthDaySpace, + /// `"%j"`: Day of the year in `001..=366`, zero-padded to 3 digits. + YearDay, + /// `"%H"`: Hour of the day (24-hour clock) in `00..=23`, zero-padded to 2 digits. + Hour24hZero, + /// `"%k"`: Hour of the day (24-hour clock) in ` 0..=23`, blank-padded to 2 digits. + Hour24hSpace, + /// `"%I"`: Hour of the day (12-hour clock) in `01..=12`, zero-padded to 2 digits. + Hour12hZero, + /// `"%l"`: Hour of the day (12-hour clock) in ` 1..=12`, blank-padded to 2 digits. + Hour12hSpace, + /// `"%P"`: Lowercase meridian indicator (`"am"` or `"pm"`). + MeridianLower, + /// `"%p"`: Uppercase meridian indicator (`"AM"` or `"PM"`). + MeridianUpper, + /// `"%M"`: Minute of the hour in `00..=59`, zero-padded to 2 digits. + Minute, + /// `"%S"`: Second of the minute in `00..=60`, zero-padded to 2 digits. + Second, + /// `"%L"`: Troncated fractional seconds digits, with 3 digits by default. Number of digits is specified by the width field. + MilliSecond, + /// `"%N"`: Troncated fractional seconds digits, with 9 digits by default. Number of digits is specified by the width field. + FractionalSecond, + /// `"%z"`: Zero-padded signed time zone UTC hour and minute offsets (`+hhmm`). + TimeZoneOffsetHourMinute, + /// `"%:z"`: Zero-padded signed time zone UTC hour and minute offsets with colons (`+hh:mm`). + TimeZoneOffsetHourMinuteColon, + /// `"%::z"`: Zero-padded signed time zone UTC hour, minute and second offsets with colons (`+hh:mm:ss`). + TimeZoneOffsetHourMinuteSecondColon, + /// `"%:::z"`: Zero-padded signed time zone UTC hour offset, with optional minute and second offsets with colons (`+hh[:mm[:ss]]`). + TimeZoneOffsetColonMinimal, + /// `"%Z"`: Platform-dependent abbreviated time zone name. + TimeZoneName, + /// `"%A"`: Locale independent full weekday name. + WeekDayName, + /// `"%a"`: Locale independent abbreviated weekday name, using the first 3 letters. + WeekDayNameAbbr, + /// `"%u"`: Day of the week from Monday in `1..=7`, zero-padded to 1 digit. + WeekDayFrom1, + /// `"%w"`: Day of the week from Sunday in `0..=6`, zero-padded to 1 digit. + WeekDayFrom0, + /// `"%G"`: Same as `%Y`, but using the ISO 8601 week-based year. + YearIso8601, + /// `"%g"`: Same as `%y`, but using the ISO 8601 week-based year. + YearIso8601Rem100, + /// `"%V"`: ISO 8601 week number in `01..=53`, zero-padded to 2 digits. + WeekNumberIso8601, + /// `"%U"`: Week number from Sunday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Sunday of the year. + WeekNumberFromSunday, + /// `"%W"`: Week number from Monday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Monday of the year. + WeekNumberFromMonday, + /// `"%s"`: Number of seconds since `1970-01-01 00:00:00 UTC`, zero-padded to at least 1 digit. + SecondsSinceEpoch, + /// `"%n"`: Newline character `'\n'`. + Newline, + /// `"%t"`: Tab character `'\t'`. + Tabulation, + /// `"%%"`: Literal `'%'` character. + Percent, + /// `"%c"`: Date and time, equivalent to `"%a %b %e %H:%M:%S %Y"`. + CombinationDateTime, + /// `"%D"` and `"%x"`: Date, equivalent to `"%m/%d/%y"`. + CombinationDate, + /// `"%F"`: ISO 8601 date, equivalent to `"%Y-%m-%d"`. + CombinationIso8601, + /// `"%v"`: VMS date, equivalent to `"%e-%^b-%4Y"`. + CombinationVmsDate, + /// `"%r"`: 12-hour time, equivalent to `"%I:%M:%S %p"`. + CombinationTime12h, + /// `"%R"`: 24-hour time without seconds, equivalent to `"%H:%M"`. + CombinationHourMinute24h, + /// `"%T"` and `"%X"`: 24-hour time, equivalent to `"%H:%M:%S"`. + CombinationTime24h, +} + +/// UTC offset parts. +#[derive(Debug)] +struct UtcOffset { + /// Signed hour. + hour: f64, + /// Minute. + minute: u32, + /// Second. + second: u32, +} + +impl UtcOffset { + /// Construct a new `UtcOffset`. + fn new(hour: f64, minute: u32, second: u32) -> Self { + Self { + hour, + minute, + second, + } + } +} + +/// Formatting directive. +#[derive(Debug)] +struct Piece { + /// Optional width. + width: Option, + /// Padding method. + padding: Padding, + /// Formatting flags. + flags: Flags, + /// Formatting specifier. + spec: Spec, +} + +impl Piece { + /// Construct a new `Piece`. + fn new(width: Option, padding: Padding, flags: Flags, spec: Spec) -> Self { + Self { + width, + padding, + flags, + spec, + } + } + + /// Format a numerical value, padding with zeros by default. + fn format_num_zeros( + &self, + f: &mut SizeLimiter<'_>, + value: impl fmt::Display, + default_width: usize, + ) -> Result<(), Error> { + if self.flags.contains(Flags::LEFT_PADDING) { + write!(f, "{value}") + } else if self.padding == Padding::Spaces { + let width = self.width.unwrap_or(default_width); + write!(f, "{value: >width$}") + } else { + let width = self.width.unwrap_or(default_width); + write!(f, "{value:0width$}") + } + } + + /// Format a numerical value, padding with spaces by default. + fn format_num_spaces( + &self, + f: &mut SizeLimiter<'_>, + value: impl fmt::Display, + default_width: usize, + ) -> Result<(), Error> { + if self.flags.contains(Flags::LEFT_PADDING) { + write!(f, "{value}") + } else if self.padding == Padding::Zeros { + let width = self.width.unwrap_or(default_width); + write!(f, "{value:0width$}") + } else { + let width = self.width.unwrap_or(default_width); + write!(f, "{value: >width$}") + } + } + + /// Format nanoseconds with the specified precision. + fn format_nanoseconds( + &self, + f: &mut SizeLimiter<'_>, + nanoseconds: u32, + default_width: usize, + ) -> Result<(), Error> { + let width = self.width.unwrap_or(default_width); + + if width <= 9 { + let value = nanoseconds / 10u32.pow(9 - width as u32); + write!(f, "{value:0n$}", n = width) + } else { + write!(f, "{nanoseconds:09}{:0n$}", 0, n = width - 9) + } + } + + /// Format a string value. + fn format_string(&self, f: &mut SizeLimiter<'_>, s: &str) -> Result<(), Error> { + match self.width { + None => write!(f, "{s}"), + Some(width) => { + if self.flags.contains(Flags::LEFT_PADDING) { + write!(f, "{s}") + } else if self.padding == Padding::Zeros { + write!(f, "{s:0>width$}") + } else { + write!(f, "{s: >width$}") + } + } + } + } + + /// Write padding separately. + fn write_padding(&self, f: &mut SizeLimiter<'_>, min_width: usize) -> Result<(), Error> { + if let Some(width) = self.width { + let n = width.saturating_sub(min_width); + + match self.padding { + Padding::Zeros => write!(f, "{:0>n$}", "")?, + _ => write!(f, "{: >n$}", "")?, + }; + } + Ok(()) + } + + /// Compute UTC offset parts for the `%z` specifier. + fn compute_offset_parts(&self, time: &impl Time) -> UtcOffset { + let utc_offset = time.utc_offset(); + let utc_offset_abs = utc_offset.unsigned_abs(); + + // UTC is represented as "-00:00" if the '-' flag is set + let sign = if utc_offset < 0 || time.is_utc() && self.flags.contains(Flags::LEFT_PADDING) { + -1.0 + } else { + 1.0 + }; + + // Convert to f64 to have signed zero + let hour = sign * f64::from(utc_offset_abs / 3600); + let minute = (utc_offset_abs / 60) % 60; + let second = utc_offset_abs % 60; + + UtcOffset::new(hour, minute, second) + } + + /// Compute hour padding for the `%z` specifier. + fn hour_padding(&self, min_width: usize) -> usize { + const MIN_PADDING: usize = "+hh".len(); + + match self.width { + Some(width) => width.saturating_sub(min_width) + MIN_PADDING, + None => MIN_PADDING, + } + } + + /// Write the time zone UTC offset as `"+hh"`. + fn write_offset_hh( + &self, + f: &mut SizeLimiter<'_>, + utc_offset: &UtcOffset, + ) -> Result<(), Error> { + let hour = utc_offset.hour; + let n = self.hour_padding("+hh".len()); + + match self.padding { + Padding::Spaces => write!(f, "{hour: >+n$.0}"), + _ => write!(f, "{hour:+0n$.0}"), + } + } + + /// Write the time zone UTC offset as `"+hhmm"`. + fn write_offset_hhmm( + &self, + f: &mut SizeLimiter<'_>, + utc_offset: &UtcOffset, + ) -> Result<(), Error> { + let UtcOffset { hour, minute, .. } = utc_offset; + let n = self.hour_padding("+hhmm".len()); + + match self.padding { + Padding::Spaces => write!(f, "{hour: >+n$.0}{minute:02}"), + _ => write!(f, "{hour:+0n$.0}{minute:02}"), + } + } + + /// Write the time zone UTC offset as `"+hh:mm"`. + fn write_offset_hh_mm( + &self, + f: &mut SizeLimiter<'_>, + utc_offset: &UtcOffset, + ) -> Result<(), Error> { + let UtcOffset { hour, minute, .. } = utc_offset; + let n = self.hour_padding("+hh:mm".len()); + + match self.padding { + Padding::Spaces => write!(f, "{hour: >+n$.0}:{minute:02}"), + _ => write!(f, "{hour:+0n$.0}:{minute:02}"), + } + } + + /// Write the time zone UTC offset as `"+hh:mm:ss"`. + fn write_offset_hh_mm_ss( + &self, + f: &mut SizeLimiter<'_>, + utc_offset: &UtcOffset, + ) -> Result<(), Error> { + let UtcOffset { + hour, + minute, + second, + } = utc_offset; + + let n = self.hour_padding("+hh:mm:ss".len()); + + match self.padding { + Padding::Spaces => write!(f, "{hour: >+n$.0}:{minute:02}:{second:02}"), + _ => write!(f, "{hour:+0n$.0}:{minute:02}:{second:02}"), + } + } + + /// Format time using the formatting directive. + #[allow(clippy::too_many_lines)] + fn fmt(&self, f: &mut SizeLimiter<'_>, time: &impl Time) -> Result<(), Error> { + match self.spec { + Spec::Year4Digits => { + let year = time.year(); + let default_width = if year < 0 { 5 } else { 4 }; + self.format_num_zeros(f, year, default_width) + } + Spec::YearDiv100 => self.format_num_zeros(f, time.year().div_euclid(100), 2), + Spec::YearRem100 => self.format_num_zeros(f, time.year().rem_euclid(100), 2), + Spec::Month => self.format_num_zeros(f, time.month(), 2), + Spec::MonthName => { + let index = (time.month() - 1) as usize; + if self.flags.has_change_or_upper_case() { + self.format_string(f, MONTHS_UPPER[index]) + } else { + self.format_string(f, MONTHS[index]) + } + } + Spec::MonthNameAbbr => { + let index = (time.month() - 1) as usize; + if self.flags.has_change_or_upper_case() { + self.format_string(f, &MONTHS_UPPER[index][..3]) + } else { + self.format_string(f, &MONTHS[index][..3]) + } + } + Spec::MonthDayZero => self.format_num_zeros(f, time.day(), 2), + Spec::MonthDaySpace => self.format_num_spaces(f, time.day(), 2), + Spec::YearDay => self.format_num_zeros(f, time.day_of_year(), 3), + Spec::Hour24hZero => self.format_num_zeros(f, time.hour(), 2), + Spec::Hour24hSpace => self.format_num_spaces(f, time.hour(), 2), + Spec::Hour12hZero => { + let hour = time.hour() % 12; + let hour = if hour == 0 { 12 } else { hour }; + self.format_num_zeros(f, hour, 2) + } + Spec::Hour12hSpace => { + let hour = time.hour() % 12; + let hour = if hour == 0 { 12 } else { hour }; + self.format_num_spaces(f, hour, 2) + } + Spec::MeridianLower => { + let (am, pm) = if self.flags.has_change_or_upper_case() { + ("AM", "PM") + } else { + ("am", "pm") + }; + let meridian = if time.hour() < 12 { am } else { pm }; + self.format_string(f, meridian) + } + Spec::MeridianUpper => { + let (am, pm) = if self.flags.contains(Flags::CHANGE_CASE) { + ("am", "pm") + } else { + ("AM", "PM") + }; + let meridian = if time.hour() < 12 { am } else { pm }; + self.format_string(f, meridian) + } + Spec::Minute => self.format_num_zeros(f, time.minute(), 2), + Spec::Second => self.format_num_zeros(f, time.second(), 2), + Spec::MilliSecond => self.format_nanoseconds(f, time.nanoseconds(), 3), + Spec::FractionalSecond => self.format_nanoseconds(f, time.nanoseconds(), 9), + Spec::TimeZoneOffsetHourMinute => { + self.write_offset_hhmm(f, &self.compute_offset_parts(time)) + } + Spec::TimeZoneOffsetHourMinuteColon => { + self.write_offset_hh_mm(f, &self.compute_offset_parts(time)) + } + Spec::TimeZoneOffsetHourMinuteSecondColon => { + self.write_offset_hh_mm_ss(f, &self.compute_offset_parts(time)) + } + Spec::TimeZoneOffsetColonMinimal => { + let utc_offset = self.compute_offset_parts(time); + + if utc_offset.second != 0 { + self.write_offset_hh_mm_ss(f, &utc_offset) + } else if utc_offset.minute != 0 { + self.write_offset_hh_mm(f, &utc_offset) + } else { + self.write_offset_hh(f, &utc_offset) + } + } + Spec::TimeZoneName => { + let tz_name = time.time_zone(); + if !tz_name.is_empty() { + assert!(tz_name.is_ascii()); + + if !self.flags.contains(Flags::LEFT_PADDING) { + self.write_padding(f, tz_name.len())?; + } + + let convert: fn(&u8) -> u8 = if self.flags.contains(Flags::CHANGE_CASE) { + u8::to_ascii_lowercase + } else if self.flags.contains(Flags::UPPER_CASE) { + u8::to_ascii_uppercase + } else { + |&x| x + }; + + for x in tz_name.as_bytes() { + f.write(&[convert(x)])?; + } + } + Ok(()) + } + Spec::WeekDayName => { + let index = time.day_of_week() as usize; + if self.flags.has_change_or_upper_case() { + self.format_string(f, DAYS_UPPER[index]) + } else { + self.format_string(f, DAYS[index]) + } + } + Spec::WeekDayNameAbbr => { + let index = time.day_of_week() as usize; + if self.flags.has_change_or_upper_case() { + self.format_string(f, &DAYS_UPPER[index][..3]) + } else { + self.format_string(f, &DAYS[index][..3]) + } + } + Spec::WeekDayFrom1 => { + let day_of_week = time.day_of_week(); + let day_of_week = if day_of_week == 0 { 7 } else { day_of_week }; + self.format_num_zeros(f, day_of_week, 1) + } + Spec::WeekDayFrom0 => self.format_num_zeros(f, time.day_of_week(), 1), + Spec::YearIso8601 => { + let (iso_year, _) = iso_8601_year_and_week_number( + time.year().into(), + time.day_of_week().into(), + time.day_of_year().into(), + ); + let default_width = if iso_year < 0 { 5 } else { 4 }; + self.format_num_zeros(f, iso_year, default_width) + } + Spec::YearIso8601Rem100 => { + let (iso_year, _) = iso_8601_year_and_week_number( + time.year().into(), + time.day_of_week().into(), + time.day_of_year().into(), + ); + self.format_num_zeros(f, iso_year.rem_euclid(100), 2) + } + Spec::WeekNumberIso8601 => { + let (_, iso_week_number) = iso_8601_year_and_week_number( + time.year().into(), + time.day_of_week().into(), + time.day_of_year().into(), + ); + self.format_num_zeros(f, iso_week_number, 2) + } + Spec::WeekNumberFromSunday => { + let week_number = week_number( + time.day_of_week().into(), + time.day_of_year().into(), + WeekStart::Sunday, + ); + self.format_num_zeros(f, week_number, 2) + } + Spec::WeekNumberFromMonday => { + let week_number = week_number( + time.day_of_week().into(), + time.day_of_year().into(), + WeekStart::Monday, + ); + self.format_num_zeros(f, week_number, 2) + } + Spec::SecondsSinceEpoch => self.format_num_zeros(f, time.to_int(), 1), + Spec::Newline => self.format_string(f, "\n"), + Spec::Tabulation => self.format_string(f, "\t"), + Spec::Percent => self.format_string(f, "%"), + Spec::CombinationDateTime => { + const MIN_WIDTH_NO_YEAR: usize = "www mmm dd HH:MM:SS ".len(); + + let year = time.year(); + let default_year_width = if year < 0 { 5 } else { 4 }; + let min_width = MIN_WIDTH_NO_YEAR + year_width(year).max(default_year_width); + self.write_padding(f, min_width)?; + + let (day_names, month_names) = if self.flags.contains(Flags::UPPER_CASE) { + (&DAYS_UPPER, &MONTHS_UPPER) + } else { + (&DAYS, &MONTHS) + }; + + let week_day_name = &day_names[time.day_of_week() as usize][..3]; + let month_name = &month_names[(time.month() - 1) as usize][..3]; + let day = time.day(); + let (hour, minute, second) = (time.hour(), time.minute(), time.second()); + + write!(f, "{week_day_name} {month_name} ")?; + write!(f, "{day: >2} {hour:02}:{minute:02}:{second:02} ")?; + write!(f, "{year:0default_year_width$}") + } + Spec::CombinationDate => { + self.write_padding(f, "mm/dd/yy".len())?; + + let year = time.year().rem_euclid(100); + let month = time.month(); + let day = time.day(); + + write!(f, "{month:02}/{day:02}/{year:02}") + } + Spec::CombinationIso8601 => { + const MIN_WIDTH_NO_YEAR: usize = "-mm-dd".len(); + + let year = time.year(); + let default_year_width = if year < 0 { 5 } else { 4 }; + let min_width = MIN_WIDTH_NO_YEAR + year_width(year).max(default_year_width); + self.write_padding(f, min_width)?; + + let month = time.month(); + let day = time.day(); + + write!(f, "{year:0default_year_width$}-{month:02}-{day:02}") + } + Spec::CombinationVmsDate => { + let year = time.year(); + self.write_padding(f, "dd-mmm-".len() + year_width(year).max(4))?; + + let month_name = &MONTHS_UPPER[(time.month() - 1) as usize][..3]; + let day = time.day(); + + write!(f, "{day: >2}-{month_name}-{year:04}") + } + Spec::CombinationTime12h => { + self.write_padding(f, "HH:MM:SS PM".len())?; + + let hour = time.hour() % 12; + let hour = if hour == 0 { 12 } else { hour }; + + let (minute, second) = (time.minute(), time.second()); + let meridian = if time.hour() < 12 { "AM" } else { "PM" }; + + write!(f, "{hour:02}:{minute:02}:{second:02} {meridian}") + } + Spec::CombinationHourMinute24h => { + self.write_padding(f, "HH:MM".len())?; + let (hour, minute) = (time.hour(), time.minute()); + write!(f, "{hour:02}:{minute:02}") + } + Spec::CombinationTime24h => { + self.write_padding(f, "HH:MM:SS".len())?; + let (hour, minute, second) = (time.hour(), time.minute(), time.second()); + write!(f, "{hour:02}:{minute:02}:{second:02}") + } + } + } +} + +/// Wrapper struct for formatting time with the provided format string. +pub(crate) struct TimeFormatter<'t, 'f, T> { + /// Time implementation + time: &'t T, + /// Format string + format: &'f [u8], +} + +impl<'t, 'f, T: Time> TimeFormatter<'t, 'f, T> { + /// Construct a new `TimeFormatter` wrapper. + pub(crate) fn new + ?Sized>(time: &'t T, format: &'f F) -> Self { + Self { + time, + format: format.as_ref(), + } + } + + /// Format time using the format string. + pub(crate) fn fmt(&self, buf: &mut dyn Write) -> Result<(), Error> { + // Do nothing if the format string is empty + if self.format.is_empty() { + return Ok(()); + } + + // Use a size limiter to limit the maximum size of the resulting formatted string + let size_limit = self.format.len().saturating_mul(512 * 1024); + let mut f = SizeLimiter::new(buf, size_limit); + + let mut cursor = Cursor::new(self.format); + + loop { + f.write_all(cursor.read_until(|&x| x == b'%'))?; + + let remaining_before = cursor.remaining(); + + // Read the '%' character + if cursor.next().is_none() { + break; + } + + match Self::parse_spec(&mut cursor)? { + Some(piece) => piece.fmt(&mut f, self.time)?, + None => { + // No valid format specifier was found + let remaining_after = cursor.remaining(); + let text = &remaining_before[..remaining_before.len() - remaining_after.len()]; + f.write_all(text)?; + } + } + } + + Ok(()) + } + + /// Parse a formatting directive. + fn parse_spec(cursor: &mut Cursor<'_>) -> Result, Error> { + // Parse flags + let mut padding = Padding::Left; + let mut flags = Flags::empty(); + + loop { + // The left padding overrides the other padding options for most cases. + // It is also used for the hour sign in the %z specifier. + // + // Similary, the change case flag overrides the upper case flag, except + // when using combination specifiers (%c, %D, %x, %F, %v, %r, %R, %T, %X). + match cursor.remaining().first() { + Some(&b'-') => { + padding = Padding::Left; + flags.insert(Flags::LEFT_PADDING); + } + Some(&b'_') => padding = Padding::Spaces, + Some(&b'0') => padding = Padding::Zeros, + Some(&b'^') => flags.insert(Flags::UPPER_CASE), + Some(&b'#') => flags.insert(Flags::CHANGE_CASE), + _ => break, + } + cursor.next(); + } + + // Parse width + let width_digits = str::from_utf8(cursor.read_while(u8::is_ascii_digit)) + .expect("reading ASCII digits should yield a valid UTF-8 slice"); + + let width = match width_digits.parse::() { + Ok(width) if Int::try_from(width).is_ok() => Some(width), + Err(err) if *err.kind() == IntErrorKind::Empty => None, + _ => return Ok(None), + }; + + // Ignore POSIX locale extensions (https://github.com/ruby/ruby/blob/4491bb740a9506d76391ac44bb2fe6e483fec952/strftime.c#L713-L722) + if let Some(&[ext, spec]) = cursor.remaining().get(..2) { + const EXT_E_SPECS: &[u8] = assert_sorted(b"CXYcxy"); + const EXT_O_SPECS: &[u8] = assert_sorted(b"HIMSUVWdeklmuwy"); + + match ext { + b'E' if EXT_E_SPECS.binary_search(&spec).is_ok() => cursor.next(), + b'O' if EXT_O_SPECS.binary_search(&spec).is_ok() => cursor.next(), + _ => None, + }; + } + + // Parse spec + let colons = cursor.read_while(|&x| x == b':'); + + let spec = if colons.is_empty() { + const POSSIBLE_SPECS: &[(u8, Spec)] = assert_sorted_elem_0(&[ + (b'%', Spec::Percent), + (b'A', Spec::WeekDayName), + (b'B', Spec::MonthName), + (b'C', Spec::YearDiv100), + (b'D', Spec::CombinationDate), + (b'F', Spec::CombinationIso8601), + (b'G', Spec::YearIso8601), + (b'H', Spec::Hour24hZero), + (b'I', Spec::Hour12hZero), + (b'L', Spec::MilliSecond), + (b'M', Spec::Minute), + (b'N', Spec::FractionalSecond), + (b'P', Spec::MeridianLower), + (b'R', Spec::CombinationHourMinute24h), + (b'S', Spec::Second), + (b'T', Spec::CombinationTime24h), + (b'U', Spec::WeekNumberFromSunday), + (b'V', Spec::WeekNumberIso8601), + (b'W', Spec::WeekNumberFromMonday), + (b'X', Spec::CombinationTime24h), + (b'Y', Spec::Year4Digits), + (b'Z', Spec::TimeZoneName), + (b'a', Spec::WeekDayNameAbbr), + (b'b', Spec::MonthNameAbbr), + (b'c', Spec::CombinationDateTime), + (b'd', Spec::MonthDayZero), + (b'e', Spec::MonthDaySpace), + (b'g', Spec::YearIso8601Rem100), + (b'h', Spec::MonthNameAbbr), + (b'j', Spec::YearDay), + (b'k', Spec::Hour24hSpace), + (b'l', Spec::Hour12hSpace), + (b'm', Spec::Month), + (b'n', Spec::Newline), + (b'p', Spec::MeridianUpper), + (b'r', Spec::CombinationTime12h), + (b's', Spec::SecondsSinceEpoch), + (b't', Spec::Tabulation), + (b'u', Spec::WeekDayFrom1), + (b'v', Spec::CombinationVmsDate), + (b'w', Spec::WeekDayFrom0), + (b'x', Spec::CombinationDate), + (b'y', Spec::YearRem100), + (b'z', Spec::TimeZoneOffsetHourMinute), + ]); + + match cursor.next() { + Some(x) => match POSSIBLE_SPECS.binary_search_by_key(&x, |&(c, _)| c) { + Ok(index) => Some(POSSIBLE_SPECS[index].1), + Err(_) => None, + }, + None => return Err(Error::InvalidFormatString), + } + } else if cursor.read_optional_tag(b"z") { + match colons.len() { + 1 => Some(Spec::TimeZoneOffsetHourMinuteColon), + 2 => Some(Spec::TimeZoneOffsetHourMinuteSecondColon), + 3 => Some(Spec::TimeZoneOffsetColonMinimal), + _ => None, + } + } else { + None + }; + + Ok(spec.map(|spec| Piece::new(width, padding, flags, spec))) + } +} + +/// Compute the width of the string representation of a year. +fn year_width(year: i32) -> usize { + let mut n = if year <= 0 { 1 } else { 0 }; + let mut val = year; + while val != 0 { + val /= 10; + n += 1; + } + n +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_year_width() { + assert_eq!(year_width(-100), 4); + assert_eq!(year_width(-99), 3); + assert_eq!(year_width(-10), 3); + assert_eq!(year_width(-9), 2); + assert_eq!(year_width(-1), 2); + assert_eq!(year_width(0), 1); + assert_eq!(year_width(1), 1); + assert_eq!(year_width(9), 1); + assert_eq!(year_width(10), 2); + assert_eq!(year_width(99), 2); + assert_eq!(year_width(100), 3); + } +} diff --git a/src/format/utils.rs b/src/format/utils.rs new file mode 100644 index 00000000..2804b016 --- /dev/null +++ b/src/format/utils.rs @@ -0,0 +1,97 @@ +//! Some useful types. + +use super::write::Write; +use crate::Error; + +/// A `Cursor` contains a slice of a buffer. +#[derive(Debug, Clone)] +pub(crate) struct Cursor<'a> { + /// Slice representing the remaining data to be read. + remaining: &'a [u8], +} + +impl<'a> Cursor<'a> { + /// Construct a new `Cursor` from remaining data. + pub(crate) fn new(remaining: &'a [u8]) -> Self { + Self { remaining } + } + + /// Returns remaining data. + pub(crate) fn remaining(&self) -> &'a [u8] { + self.remaining + } + + /// Returns the next byte. + pub(crate) fn next(&mut self) -> Option { + let (&first, tail) = self.remaining.split_first()?; + self.remaining = tail; + Some(first) + } + + /// Read bytes if the remaining data is prefixed by the provided tag. + pub(crate) fn read_optional_tag(&mut self, tag: &[u8]) -> bool { + if self.remaining.starts_with(tag) { + self.read_exact(tag.len()); + true + } else { + false + } + } + + /// Read bytes as long as the provided predicate is true. + pub(crate) fn read_while bool>(&mut self, f: F) -> &'a [u8] { + match self.remaining.iter().position(|x| !f(x)) { + None => self.read_exact(self.remaining.len()), + Some(position) => self.read_exact(position), + } + } + + /// Read bytes until the provided predicate is true. + pub(crate) fn read_until bool>(&mut self, f: F) -> &'a [u8] { + match self.remaining.iter().position(f) { + None => self.read_exact(self.remaining.len()), + Some(position) => self.read_exact(position), + } + } + + /// Read exactly `count` bytes. + fn read_exact(&mut self, count: usize) -> &'a [u8] { + let (result, remaining) = self.remaining.split_at(count); + self.remaining = remaining; + result + } +} + +/// A `SizeLimiter` limits the maximum amount a writer can write. +pub(crate) struct SizeLimiter<'a> { + /// Inner writer. + inner: &'a mut dyn Write, + /// Size limit. + size_limit: usize, + /// Current write count. + count: usize, +} + +impl<'a> SizeLimiter<'a> { + /// Construct a new `SizeLimiter`. + pub(crate) fn new(inner: &'a mut dyn Write, max_size: usize) -> Self { + Self { + inner, + size_limit: max_size, + count: 0, + } + } +} + +impl<'a> Write for SizeLimiter<'a> { + fn write(&mut self, buf: &[u8]) -> Result { + if self.count + buf.len() > self.size_limit { + return Err(Error::FormattedStringTooLarge); + } + + let write_limit = buf.len().min(self.size_limit - self.count); + let written = self.inner.write(&buf[..write_limit])?; + self.count += written; + Ok(written) + } +} diff --git a/src/format/week.rs b/src/format/week.rs new file mode 100644 index 00000000..3c89693a --- /dev/null +++ b/src/format/week.rs @@ -0,0 +1,160 @@ +//! Module containing week-related items. + +/// Start day of the week. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum WeekStart { + /// Sunday. + Sunday = 0, + /// Monday. + Monday = 1, +} + +/// Compute the week number, beginning at the provided start day of the week. +/// +/// ## Inputs +/// +/// * `week_day`: Day of the week from Sunday in `0..=6`. +/// * `year_day_1`: Day of the year in `1..=366`. +/// * `week_start`: Start day of the week. +/// +pub(crate) fn week_number(week_day: i64, year_day_1: i64, week_start: WeekStart) -> i64 { + let year_day = year_day_1 - 1; + let start_of_first_week = (year_day - week_day + week_start as i64).rem_euclid(7); + (year_day + 7 - start_of_first_week) / 7 +} + +/// Compute the ISO 8601 week-based year and week number. +/// +/// The first week of `YYYY` starts with a Monday and includes `YYYY-01-04`. +/// The days in the year before the first week are in the last week of the previous year. +/// +/// ## Inputs +/// +/// * `year`: Year. +/// * `week_day`: Day of the week from Sunday in `0..=6`. +/// * `year_day_1`: Day of the year in `1..=366`. +/// +pub(crate) fn iso_8601_year_and_week_number( + year: i64, + week_day: i64, + year_day_1: i64, +) -> (i64, i64) { + let year_day = year_day_1 - 1; + + let mut start_of_first_week = (year_day - week_day + 1).rem_euclid(7); + + if start_of_first_week > 3 { + start_of_first_week -= 7; + } + + if year_day < start_of_first_week { + // Use previous year + let previous_year = year - 1; + + let previous_year_day = if is_leap_year(previous_year) { + 366 + year_day + } else { + 365 + year_day + }; + + return iso_8601_year_and_week_number(previous_year, week_day, previous_year_day + 1); + } + + let week_number = (year_day + 7 - start_of_first_week) / 7; + + if week_number >= 52 { + let last_year_day = if is_leap_year(year) { 365 } else { 364 }; + + let week_day_of_last_year_day = (week_day + last_year_day - year_day) % 7; + + if (1..=3).contains(&week_day_of_last_year_day) { + let last_monday = last_year_day - (week_day_of_last_year_day - 1); + if year_day >= last_monday { + // Use next year + return (year + 1, 1); + } + } + } + + // Use current year + (year, week_number) +} + +/// Check if a year is a leap year. +fn is_leap_year(year: i64) -> bool { + year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_week_number() { + assert_eq!(week_number(1, 0, WeekStart::Sunday), 0); + assert_eq!(week_number(2, 1, WeekStart::Sunday), 0); + assert_eq!(week_number(3, 2, WeekStart::Sunday), 0); + assert_eq!(week_number(4, 3, WeekStart::Sunday), 0); + assert_eq!(week_number(5, 4, WeekStart::Sunday), 0); + assert_eq!(week_number(6, 5, WeekStart::Sunday), 0); + assert_eq!(week_number(0, 6, WeekStart::Sunday), 1); + assert_eq!(week_number(1, 7, WeekStart::Sunday), 1); + assert_eq!(week_number(2, 8, WeekStart::Sunday), 1); + + assert_eq!(week_number(0, 0, WeekStart::Monday), 0); + assert_eq!(week_number(1, 1, WeekStart::Monday), 1); + assert_eq!(week_number(2, 2, WeekStart::Monday), 1); + assert_eq!(week_number(3, 3, WeekStart::Monday), 1); + assert_eq!(week_number(4, 4, WeekStart::Monday), 1); + assert_eq!(week_number(5, 5, WeekStart::Monday), 1); + assert_eq!(week_number(6, 6, WeekStart::Monday), 1); + assert_eq!(week_number(7, 7, WeekStart::Monday), 1); + assert_eq!(week_number(8, 8, WeekStart::Monday), 2); + + assert_eq!(week_number(0, 365, WeekStart::Sunday), 53); + } + + #[test] + fn test_iso_8601_year_and_week() { + assert_eq!(iso_8601_year_and_week_number(2025, 0, 362), (2025, 52)); + assert_eq!(iso_8601_year_and_week_number(2025, 1, 363), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2025, 2, 364), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2025, 3, 365), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2026, 4, 1), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2026, 5, 2), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2026, 6, 3), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2026, 0, 4), (2026, 1)); + assert_eq!(iso_8601_year_and_week_number(2026, 1, 5), (2026, 2)); + + assert_eq!(iso_8601_year_and_week_number(2026, 0, 361), (2026, 52)); + assert_eq!(iso_8601_year_and_week_number(2026, 1, 362), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2026, 2, 363), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2026, 3, 364), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2026, 4, 365), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2027, 5, 1), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2027, 6, 2), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2027, 0, 3), (2026, 53)); + assert_eq!(iso_8601_year_and_week_number(2027, 1, 4), (2027, 1)); + + assert_eq!(iso_8601_year_and_week_number(2020, 0, 362), (2020, 52)); + assert_eq!(iso_8601_year_and_week_number(2020, 1, 363), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2020, 2, 364), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2020, 3, 365), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2020, 4, 366), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2021, 5, 1), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2021, 6, 2), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2021, 0, 3), (2020, 53)); + assert_eq!(iso_8601_year_and_week_number(2021, 1, 4), (2021, 1)); + } + + #[test] + fn test_is_leap_year() { + assert!(is_leap_year(2000)); + assert!(!is_leap_year(2001)); + assert!(is_leap_year(2004)); + assert!(!is_leap_year(2100)); + assert!(!is_leap_year(2200)); + assert!(!is_leap_year(2300)); + assert!(is_leap_year(2400)); + } +} diff --git a/src/format/write.rs b/src/format/write.rs new file mode 100644 index 00000000..254f0935 --- /dev/null +++ b/src/format/write.rs @@ -0,0 +1,83 @@ +//! This module is a copy of the [`std::io::Write`] implementation, +//! in order to use it in a no-std context. +//! +//! [`std::io::Write`]: + +use core::fmt; + +use crate::Error; + +/// An `Adapter` implements [`core::fmt::Write`] from a [`Write`] object, +/// storing write errors instead of discarding them. +struct Adapter<'a, T: ?Sized> { + /// Inner writer. + inner: &'a mut T, + /// Write result. + error: Result<(), Error>, +} + +impl fmt::Write for Adapter<'_, T> { + fn write_str(&mut self, s: &str) -> fmt::Result { + match self.inner.write_all(s.as_bytes()) { + Ok(()) => Ok(()), + Err(e) => { + self.error = Err(e); + Err(fmt::Error) + } + } + } +} + +/// Simplified copy of the [`std::io::Write`] trait. +/// +/// [`std::io::Write`]: +pub(crate) trait Write { + /// Write a buffer into this writer, returning how many bytes were written. + fn write(&mut self, data: &[u8]) -> Result; + + /// Attempts to write an entire buffer into this writer. + fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Error> { + while !buf.is_empty() { + match self.write(buf)? { + 0 => return Err(Error::WriteZero), + n => buf = &buf[n..], + } + } + Ok(()) + } + + /// Writes a formatted string into this writer, returning any error encountered. + fn write_fmt(&mut self, fmt_args: fmt::Arguments<'_>) -> Result<(), Error> { + let mut output = Adapter { + inner: self, + error: Ok(()), + }; + + match fmt::write(&mut output, fmt_args) { + Ok(()) => Ok(()), + Err(_) if output.error.is_err() => output.error, + Err(_) => Err(Error::FmtError), + } + } +} + +/// Write is implemented for `&mut [u8]` by copying into the slice, overwriting its data. +impl Write for &mut [u8] { + fn write(&mut self, data: &[u8]) -> Result { + let size = data.len().min(self.len()); + let (a, b) = core::mem::take(self).split_at_mut(size); + a.copy_from_slice(&data[..size]); + *self = b; + Ok(size) + } +} + +/// Write is implemented for `Vec` by appending to the vector, growing as needed. +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl Write for Vec { + fn write(&mut self, buf: &[u8]) -> Result { + self.extend_from_slice(buf); + Ok(buf.len()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3c82e0cb..00dacaa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] #![warn(clippy::all)] #![warn(clippy::pedantic)] #![warn(clippy::cargo)] @@ -18,7 +20,89 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_alias))] -//! WIP +/*! +This crate provides a Ruby 3.1.2 compatible `strftime` function, which formats time according to the directives in the given format string. + +The directives begin with a percent `%` character. Any text not listed as a directive will be passed through to the output string. + +Each directive consists of a percent `%` character, zero or more flags, optional minimum field width, +optional modifier and a conversion specifier as follows: + +```text +% +``` + +## Flags + +| Flag | Description | +|------|----------------------------------------------------------------------------------------| +| `-` | Use left padding, ignoring width and removing all other padding options in most cases. | +| `_` | Use spaces for padding. | +| `0` | Use zeros for padding. | +| `^` | Convert the resulting string to uppercase. | +| `#` | Change case of the resulting string. | + + +## Width + +The minimum field width specifies the minimum width. + +## Modifiers + +The modifiers are `E` and `O`. They are ignored. + +## Specifiers + +| Specifier | Example | Description | +|------------|---------------|-----------------------------------------------------------------------------------------------------------------------| +| `%Y` | `-2001` | Year with century if provided, zero-padded to at least 4 digits plus the possible negative sign. | +| `%C` | `-21` | `Year / 100` using Euclidian division, zero-padded to at least 2 digits. | +| `%y` | `99` | `Year % 100` in `00..=99`, using Euclidian remainder, zero-padded to 2 digits. | +| `%m` | `01` | Month of the year in `01..=12`, zero-padded to 2 digits. | +| `%B` | `July` | Locale independent full month name. | +| `%b`, `%h` | `Jul` | Locale independent abbreviated month name, using the first 3 letters. | +| `%d` | `01` | Day of the month in `01..=31`, zero-padded to 2 digits. | +| `%e` | ` 1` | Day of the month in ` 1..=31`, blank-padded to 2 digits. | +| `%j` | `001` | Day of the year in `001..=366`, zero-padded to 3 digits. | +| `%H` | `00` | Hour of the day (24-hour clock) in `00..=23`, zero-padded to 2 digits. | +| `%k` | ` 0` | Hour of the day (24-hour clock) in ` 0..=23`, blank-padded to 2 digits. | +| `%I` | `01` | Hour of the day (12-hour clock) in `01..=12`, zero-padded to 2 digits. | +| `%l` | ` 1` | Hour of the day (12-hour clock) in ` 1..=12`, blank-padded to 2 digits. | +| `%P` | `am` | Lowercase meridian indicator (`"am"` or `"pm"`). | +| `%p` | `AM` | Uppercase meridian indicator (`"AM"` or `"PM"`). | +| `%M` | `00` | Minute of the hour in `00..=59`, zero-padded to 2 digits. | +| `%S` | `00` | Second of the minute in `00..=60`, zero-padded to 2 digits. | +| `%L` | `123` | Troncated fractional seconds digits, with 3 digits by default. Number of digits is specified by the width field. | +| `%N` | `123456789` | Troncated fractional seconds digits, with 9 digits by default. Number of digits is specified by the width field. | +| `%z` | `+0200` | Zero-padded signed time zone UTC hour and minute offsets (`+hhmm`). | +| `%:z` | `+02:00` | Zero-padded signed time zone UTC hour and minute offsets with colons (`+hh:mm`). | +| `%::z` | `+02:00:00` | Zero-padded signed time zone UTC hour, minute and second offsets with colons (`+hh:mm:ss`). | +| `%:::z` | `+02` | Zero-padded signed time zone UTC hour offset, with optional minute and second offsets with colons (`+hh[:mm[:ss]]`). | +| `%Z` | `CEST` | Platform-dependent abbreviated time zone name. | +| `%A` | `Sunday` | Locale independent full weekday name. | +| `%a` | `Sun` | Locale independent abbreviated weekday name, using the first 3 letters. | +| `%u` | `1` | Day of the week from Monday in `1..=7`, zero-padded to 1 digit. | +| `%w` | `0` | Day of the week from Sunday in `0..=6`, zero-padded to 1 digit. | +| `%G` | `-2001` | Same as `%Y`, but using the ISO 8601 week-based year. [^1] | +| `%g` | `99` | Same as `%y`, but using the ISO 8601 week-based year. [^1] | +| `%V` | `01` | ISO 8601 week number in `01..=53`, zero-padded to 2 digits. [^1] | +| `%U` | `00` | Week number from Sunday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Sunday of the year. | +| `%W` | `00` | Week number from Monday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Monday of the year. | +| `%s` | `86400` | Number of seconds since `1970-01-01 00:00:00 UTC`, zero-padded to at least 1 digit. | +| `%n` | `\n` | Newline character `'\n'`. | +| `%t` | `\t` | Tab character `'\t'`. | +| `%%` | `%` | Literal `'%'` character. | +| `%c` | `Sun Jul 8 00:23:45 2001` | Date and time, equivalent to `"%a %b %e %H:%M:%S %Y"`. | +| `%D`, `%x` | `07/08/01` | Date, equivalent to `"%m/%d/%y"`. | +| `%F` | `2001-07-08` | ISO 8601 date, equivalent to `"%Y-%m-%d"`. | +| `%v` | ` 8-JUL-2001` | VMS date, equivalent to `"%e-%^b-%4Y"`. | +| `%r` | `12:23:45 AM` | 12-hour time, equivalent to `"%I:%M:%S %p"`. | +| `%R` | `00:23` | 24-hour time without seconds, equivalent to `"%H:%M"`. | +| `%T`, `%X` | `00:23:45` | 24-hour time, equivalent to `"%H:%M:%S"`. | + +[^1]: `%G`, `%g`, `%V`: Week 1 of ISO 8601 is the first week with at least 4 days in that year. +The days before the first week are in the last week of the previous year. +*/ #![doc(html_root_url = "https://docs.rs/strftime-ruby/0.1.0")] @@ -27,11 +111,204 @@ #[doc = include_str!("../README.md")] mod readme {} +mod format; + #[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); +mod tests; + +use core::fmt; + +/// Error type returned by the `strftime` functions. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Error { + /// Provided format string is ended by an unterminated format specifier. + InvalidFormatString, + /// Formatted string is too large and could cause an out-of-memory error. + FormattedStringTooLarge, + /// Provided buffer for the [`buffered::strftime`] function is too small for the formatted string. + /// + /// This corresponds to the [`std::io::ErrorKind::WriteZero`] variant. + /// + /// [`std::io::ErrorKind::WriteZero`]: + WriteZero, + /// Formatting error, corresponding to [`core::fmt::Error`]. + FmtError, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidFormatString => write!(f, "invalid format string"), + Error::FormattedStringTooLarge => write!(f, "formatted string too large"), + Error::WriteZero => write!(f, "failed to write the whole buffer"), + Error::FmtError => write!(f, "formatter error"), + } + } +} + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl std::error::Error for Error {} + +/// Common methods needed for formatting _time_. +/// +/// This should be implemented for structs representing a _time_. +/// +/// All the `strftime` functions take as input an implementation of this trait. +pub trait Time { + /// Returns the year for _time_ (including the century). + fn year(&self) -> i32; + /// Returns the month of the year in `1..=12` for _time_. + fn month(&self) -> u8; + /// Returns the day of the month in `1..=31` for _time_. + fn day(&self) -> u8; + /// Returns the hour of the day in `0..=23` for _time_. + fn hour(&self) -> u8; + /// Returns the minute of the hour in `0..=59` for _time_. + fn minute(&self) -> u8; + /// Returns the second of the minute in `0..=60` for _time_. + fn second(&self) -> u8; + /// Returns the number of nanoseconds in `0..=999_999_999` for _time_. + fn nanoseconds(&self) -> u32; + /// Returns an integer representing the day of the week in `0..=6`, with `Sunday == 0`. + fn day_of_week(&self) -> u8; + /// Returns an integer representing the day of the year in `1..=366`. + fn day_of_year(&self) -> u16; + /// Returns the number of seconds as a signed integer since the Epoch. + fn to_int(&self) -> i64; + /// Returns true if the time zone is UTC. + fn is_utc(&self) -> bool; + /// Returns the offset in seconds between the timezone of _time_ and UTC. + fn utc_offset(&self) -> i32; + /// Returns the name of the time zone as a string. + fn time_zone(&self) -> &str; +} + +// Check that the Time trait is object-safe +const _: Option<&dyn Time> = None; + +/// Provides a buffered `strftime` implementation using a format string with arbitrary bytes. +pub mod buffered { + use super::{Error, Time}; + use crate::format::TimeFormatter; + + /// Format a _time_ implementation with the specified format byte string, + /// writing in the provided buffer and returning the written subslice. + /// + /// See the [crate-level documentation](crate) for a complete description of possible format specifiers. + /// + /// # Examples + /// + /// ``` + /// use strftime::buffered::strftime; + /// use strftime::Time; + /// + /// // Not shown: create a time implementation with the year 1970 + /// // let time = ...; + /// # include!("mock.rs.in"); + /// # fn main() -> Result<(), strftime::Error> { + /// # let time = MockTime { year: 1970, ..Default::default() }; + /// assert_eq!(time.year(), 1970); + /// + /// let mut buf = [0u8; 8]; + /// assert_eq!(strftime(&time, b"%Y", &mut buf)?, b"1970"); + /// assert_eq!(buf, *b"1970\0\0\0\0"); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Can produce an [`Error`](crate::Error) when the formatting fails. + pub fn strftime<'a>( + time: &impl Time, + format: &[u8], + buf: &'a mut [u8], + ) -> Result<&'a mut [u8], Error> { + let len = buf.len(); + + let mut cursor = &mut buf[..]; + TimeFormatter::new(time, format).fmt(&mut cursor)?; + let remaining_len = cursor.len(); + + Ok(&mut buf[..len - remaining_len]) + } +} + +/// Provides a `strftime` implementation using a format string with arbitrary bytes. +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub mod bytes { + use super::{Error, Time}; + use crate::format::TimeFormatter; + + /// Format a _time_ implementation with the specified format byte string. + /// + /// See the [crate-level documentation](crate) for a complete description of possible format specifiers. + /// + /// # Examples + /// + /// ``` + /// use strftime::bytes::strftime; + /// use strftime::Time; + /// + /// // Not shown: create a time implementation with the year 1970 + /// // let time = ...; + /// # include!("mock.rs.in"); + /// # fn main() -> Result<(), strftime::Error> { + /// # let time = MockTime { year: 1970, ..Default::default() }; + /// assert_eq!(time.year(), 1970); + /// + /// assert_eq!(strftime(&time, b"%Y")?, b"1970"); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Can produce an [`Error`](crate::Error) when the formatting fails. + pub fn strftime(time: &impl Time, format: &[u8]) -> Result, Error> { + let mut buf = Vec::new(); + TimeFormatter::new(time, format).fmt(&mut buf)?; + Ok(buf) + } +} + +/// Provides a `strftime` implementation using a UTF-8 format string. +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub mod string { + use super::{Error, Time}; + use crate::format::TimeFormatter; + + /// Format a _time_ implementation with the specified UTF-8 format string. + /// + /// See the [crate-level documentation](crate) for a complete description of possible format specifiers. + /// + /// # Examples + /// + /// ``` + /// use strftime::string::strftime; + /// use strftime::Time; + /// + /// // Not shown: create a time implementation with the year 1970 + /// // let time = ...; + /// # include!("mock.rs.in"); + /// # fn main() -> Result<(), strftime::Error> { + /// # let time = MockTime { year: 1970, ..Default::default() }; + /// assert_eq!(time.year(), 1970); + /// + /// assert_eq!(strftime(&time, "%Y")?, "1970"); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Can produce an [`Error`](crate::Error) when the formatting fails. + pub fn strftime(time: &impl Time, format: &str) -> Result { + let mut buf = Vec::new(); + TimeFormatter::new(time, format).fmt(&mut buf)?; + Ok(String::from_utf8(buf).expect("formatted string should be valid UTF-8")) } } diff --git a/src/mock.rs.in b/src/mock.rs.in new file mode 100644 index 00000000..bbeee2e2 --- /dev/null +++ b/src/mock.rs.in @@ -0,0 +1,35 @@ +macro_rules! create_mock_time { + ($($field_name:ident: $field_type:ty),*,) => { + #[derive(Default)] + struct MockTime<'a> { + $($field_name: $field_type),*, + } + + impl<'a> MockTime<'a> { + #[allow(clippy::too_many_arguments)] + fn new($($field_name: $field_type),*) -> Self { + Self { $($field_name),* } + } + } + + impl<'a> Time for MockTime<'a> { + $(fn $field_name(&self) -> $field_type { self.$field_name })* + } + }; +} + +create_mock_time!( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + nanoseconds: u32, + day_of_week: u8, + day_of_year: u16, + to_int: i64, + is_utc: bool, + utc_offset: i32, + time_zone: &'a str, +); diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 00000000..77295842 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,874 @@ +use crate::format::TimeFormatter; +use crate::{Error, Time}; + +include!("mock.rs.in"); + +fn check_format(time: &MockTime<'_>, format: &str, expected: Result<&str, Error>) { + const SIZE: usize = 100; + let mut buf = [0u8; SIZE]; + let mut cursor = &mut buf[..]; + + let result = TimeFormatter::new(time, format).fmt(&mut cursor); + let written = SIZE - cursor.len(); + let data = core::str::from_utf8(&buf[..written]).unwrap(); + + assert_eq!(result.map(|_| data), expected); +} + +fn check_all(times: &[MockTime<'_>], format: &str, all_expected: &[Result<&str, Error>]) { + assert_eq!(times.len(), all_expected.len()); + for (time, &expected) in times.iter().zip(all_expected) { + check_format(time, format, expected); + } +} + +#[test] +#[rustfmt::skip] +fn test_format_year_4_digits() { + let times = [ + MockTime { year: -1111, ..Default::default() }, + MockTime { year: -11, ..Default::default() }, + MockTime { year: 1, ..Default::default() }, + MockTime { year: 1111, ..Default::default() }, + ]; + + check_all(×, "'%Y'", &[Ok("'-1111'"), Ok("'-0011'"), Ok("'0001'"), Ok("'1111'")]); + check_all(×, "'%1Y'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%4Y'", &[Ok("'-1111'"), Ok("'-011'"), Ok("'0001'"), Ok("'1111'")]); + check_all(×, "'%-_5Y'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%-05Y'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%0_5Y'", &[Ok("'-1111'"), Ok("' -11'"), Ok("' 1'"), Ok("' 1111'")]); + check_all(×, "'%_05Y'", &[Ok("'-1111'"), Ok("'-0011'"), Ok("'00001'"), Ok("'01111'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_year_div_100() { + let times = [ + MockTime { year: -1111, ..Default::default() }, + MockTime { year: -11, ..Default::default() }, + MockTime { year: 1, ..Default::default() }, + MockTime { year: 1111, ..Default::default() }, + ]; + + check_all(×, "'%C'", &[Ok("'-12'"), Ok("'-1'"), Ok("'00'"), Ok("'11'")]); + check_all(×, "'%1C'", &[Ok("'-12'"), Ok("'-1'"), Ok("'0'"), Ok("'11'")]); + check_all(×, "'%4C'", &[Ok("'-012'"), Ok("'-001'"), Ok("'0000'"), Ok("'0011'")]); + check_all(×, "'%-_4C'", &[Ok("'-12'"), Ok("'-1'"), Ok("'0'"), Ok("'11'")]); + check_all(×, "'%-04C'", &[Ok("'-12'"), Ok("'-1'"), Ok("'0'"), Ok("'11'")]); + check_all(×, "'%0_4C'", &[Ok("' -12'"), Ok("' -1'"), Ok("' 0'"), Ok("' 11'")]); + check_all(×, "'%_04C'", &[Ok("'-012'"), Ok("'-001'"), Ok("'0000'"), Ok("'0011'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_year_rem_100() { + let times = [ + MockTime { year: -1111, ..Default::default() }, + MockTime { year: -11, ..Default::default() }, + MockTime { year: 1, ..Default::default() }, + MockTime { year: 1111, ..Default::default() }, + ]; + + check_all(×, "'%y'", &[Ok("'89'"), Ok("'89'"), Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1y'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4y'", &[Ok("'0089'"), Ok("'0089'"), Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_y'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0y'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_y'", &[Ok("'89'"), Ok("'89'"), Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0y'", &[Ok("'89'"), Ok("'89'"), Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_month() { + let times = [ + MockTime { month: 1, ..Default::default() }, + MockTime { month: 11, ..Default::default() }, + ]; + + check_all(×, "'%m'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1m'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4m'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_m'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0m'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_m'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0m'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_month_name() { + let times = [MockTime { month: 7, ..Default::default() }]; + + check_all(×, "'%B'", &[Ok("'July'")]); + check_all(×, "'%1B'", &[Ok("'July'")]); + check_all(×, "'%6B'", &[Ok("' July'")]); + check_all(×, "'%-_#^6B'", &[Ok("'JULY'")]); + check_all(×, "'%-0^6B'", &[Ok("'JULY'")]); + check_all(×, "'%0_#6B'", &[Ok("' JULY'")]); + check_all(×, "'%_06B'", &[Ok("'00July'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_month_name_abbr() { + let times = [MockTime { month: 7, ..Default::default() }]; + + check_all(×, "'%b'", &[Ok("'Jul'")]); + check_all(×, "'%1b'", &[Ok("'Jul'")]); + check_all(×, "'%6b'", &[Ok("' Jul'")]); + check_all(×, "'%-_#^6b'", &[Ok("'JUL'")]); + check_all(×, "'%-0^6b'", &[Ok("'JUL'")]); + check_all(×, "'%0_#6b'", &[Ok("' JUL'")]); + check_all(×, "'%_06b'", &[Ok("'000Jul'")]); + + check_all(×, "'%h'", &[Ok("'Jul'")]); + check_all(×, "'%1h'", &[Ok("'Jul'")]); + check_all(×, "'%6h'", &[Ok("' Jul'")]); + check_all(×, "'%-_#^6h'", &[Ok("'JUL'")]); + check_all(×, "'%-0^6h'", &[Ok("'JUL'")]); + check_all(×, "'%0_#6h'", &[Ok("' JUL'")]); + check_all(×, "'%_06h'", &[Ok("'000Jul'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_month_day_zero() { + let times = [ + MockTime { day: 1, ..Default::default() }, + MockTime { day: 11, ..Default::default() }, + ]; + + check_all(×, "'%d'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1d'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4d'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_d'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0d'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_d'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0d'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_month_day_space() { + let times = [ + MockTime { day: 1, ..Default::default() }, + MockTime { day: 11, ..Default::default() }, + ]; + + check_all(×, "'%e'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%1e'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4e'", &[Ok("' 1'"), Ok("' 11'")]); + check_all(×, "'%-_e'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0e'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_e'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0e'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_year_day() { + let times = [ + MockTime { day_of_year: 1, ..Default::default() }, + MockTime { day_of_year: 300, ..Default::default() }, + ]; + + check_all(×, "'%j'", &[Ok("'001'"), Ok("'300'")]); + check_all(×, "'%1j'", &[Ok("'1'"), Ok("'300'")]); + check_all(×, "'%4j'", &[Ok("'0001'"), Ok("'0300'")]); + check_all(×, "'%-_j'", &[Ok("'1'"), Ok("'300'")]); + check_all(×, "'%-0j'", &[Ok("'1'"), Ok("'300'")]); + check_all(×, "'%0_j'", &[Ok("' 1'"), Ok("'300'")]); + check_all(×, "'%_0j'", &[Ok("'001'"), Ok("'300'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_hour_24h_zero() { + let times = [ + MockTime { hour: 1, ..Default::default() }, + MockTime { hour: 11, ..Default::default() }, + ]; + + check_all(×, "'%H'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1H'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4H'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_H'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0H'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_H'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0H'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_hour_24h_space() { + let times = [ + MockTime { hour: 1, ..Default::default() }, + MockTime { hour: 11, ..Default::default() }, + ]; + + check_all(×, "'%k'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%1k'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4k'", &[Ok("' 1'"), Ok("' 11'")]); + check_all(×, "'%-_k'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0k'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_k'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0k'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_hour_12h_zero() { + let times = [ + MockTime { hour: 13, ..Default::default() }, + MockTime { hour: 0, ..Default::default() }, + ]; + + check_all(×, "'%I'", &[Ok("'01'"), Ok("'12'")]); + check_all(×, "'%1I'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%4I'", &[Ok("'0001'"), Ok("'0012'")]); + check_all(×, "'%-_I'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%-0I'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%0_I'", &[Ok("' 1'"), Ok("'12'")]); + check_all(×, "'%_0I'", &[Ok("'01'"), Ok("'12'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_hour_12h_space() { + let times = [ + MockTime { hour: 13, ..Default::default() }, + MockTime { hour: 0, ..Default::default() }, + ]; + + check_all(×, "'%l'", &[Ok("' 1'"), Ok("'12'")]); + check_all(×, "'%1l'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%4l'", &[Ok("' 1'"), Ok("' 12'")]); + check_all(×, "'%-_l'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%-0l'", &[Ok("'1'"), Ok("'12'")]); + check_all(×, "'%0_l'", &[Ok("' 1'"), Ok("'12'")]); + check_all(×, "'%_0l'", &[Ok("'01'"), Ok("'12'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_meridian_lower() { + let times = [ + MockTime { hour: 11, ..Default::default() }, + MockTime { hour: 12, ..Default::default() }, + ]; + + check_all(×, "'%P'", &[Ok("'am'"), Ok("'pm'")]); + check_all(×, "'%1P'", &[Ok("'am'"), Ok("'pm'")]); + check_all(×, "'%4P'", &[Ok("' am'"), Ok("' pm'")]); + check_all(×, "'%-_#^4P'", &[Ok("'AM'"), Ok("'PM'")]); + check_all(×, "'%-0^4P'", &[Ok("'AM'"), Ok("'PM'")]); + check_all(×, "'%0_#4P'", &[Ok("' AM'"), Ok("' PM'")]); + check_all(×, "'%_04P'", &[Ok("'00am'"), Ok("'00pm'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_meridian_upper() { + let times = [ + MockTime { hour: 11, ..Default::default() }, + MockTime { hour: 12, ..Default::default() }, + ]; + + check_all(×, "'%p'", &[Ok("'AM'"), Ok("'PM'")]); + check_all(×, "'%1p'", &[Ok("'AM'"), Ok("'PM'")]); + check_all(×, "'%4p'", &[Ok("' AM'"), Ok("' PM'")]); + check_all(×, "'%-_#^4p'", &[Ok("'am'"), Ok("'pm'")]); + check_all(×, "'%-0^4p'", &[Ok("'AM'"), Ok("'PM'")]); + check_all(×, "'%0_#4p'", &[Ok("' am'"), Ok("' pm'")]); + check_all(×, "'%_04p'", &[Ok("'00AM'"), Ok("'00PM'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_minute() { + let times = [ + MockTime { minute: 1, ..Default::default() }, + MockTime { minute: 11, ..Default::default() }, + ]; + + check_all(×, "'%M'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1M'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4M'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_M'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0M'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_M'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0M'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_second() { + let times = [ + MockTime { second: 1, ..Default::default() }, + MockTime { second: 11, ..Default::default() }, + ]; + + check_all(×, "'%S'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1S'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4S'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_S'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0S'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_S'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0S'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_milli_second() { + let times = [ + MockTime { nanoseconds: 1, ..Default::default() }, + MockTime { nanoseconds: 123_456_789, ..Default::default() }, + ]; + + check_all(×, "'%L'", &[Ok("'000'"), Ok("'123'")]); + check_all(×, "'%00L'", &[Ok("'000'"), Ok("'123'")]); + check_all(×, "'%0L'", &[Ok("'000'"), Ok("'123'")]); + check_all(×, "'%1L'", &[Ok("'0'"), Ok("'1'")]); + check_all(×, "'%2L'", &[Ok("'00'"), Ok("'12'")]); + check_all(×, "'%3L'", &[Ok("'000'"), Ok("'123'")]); + check_all(×, "'%4L'", &[Ok("'0000'"), Ok("'1234'")]); + check_all(×, "'%5L'", &[Ok("'00000'"), Ok("'12345'")]); + check_all(×, "'%6L'", &[Ok("'000000'"), Ok("'123456'")]); + check_all(×, "'%7L'", &[Ok("'0000000'"), Ok("'1234567'")]); + check_all(×, "'%8L'", &[Ok("'00000000'"), Ok("'12345678'")]); + check_all(×, "'%9L'", &[Ok("'000000001'"), Ok("'123456789'")]); + check_all(×, "'%12L'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%-12L'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%_12L'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%012L'", &[Ok("'000000001000'"), Ok("'123456789000'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_fractional_second() { + let times = [ + MockTime { nanoseconds: 1, ..Default::default() }, + MockTime { nanoseconds: 123_456_789, ..Default::default() }, + ]; + + check_all(×, "'%N'", &[Ok("'000000001'"), Ok("'123456789'")]); + check_all(×, "'%00N'", &[Ok("'000000001'"), Ok("'123456789'")]); + check_all(×, "'%0N'", &[Ok("'000000001'"), Ok("'123456789'")]); + check_all(×, "'%1N'", &[Ok("'0'"), Ok("'1'")]); + check_all(×, "'%2N'", &[Ok("'00'"), Ok("'12'")]); + check_all(×, "'%3N'", &[Ok("'000'"), Ok("'123'")]); + check_all(×, "'%4N'", &[Ok("'0000'"), Ok("'1234'")]); + check_all(×, "'%5N'", &[Ok("'00000'"), Ok("'12345'")]); + check_all(×, "'%6N'", &[Ok("'000000'"), Ok("'123456'")]); + check_all(×, "'%7N'", &[Ok("'0000000'"), Ok("'1234567'")]); + check_all(×, "'%8N'", &[Ok("'00000000'"), Ok("'12345678'")]); + check_all(×, "'%9N'", &[Ok("'000000001'"), Ok("'123456789'")]); + check_all(×, "'%12N'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%-12N'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%_12N'", &[Ok("'000000001000'"), Ok("'123456789000'")]); + check_all(×, "'%012N'", &[Ok("'000000001000'"), Ok("'123456789000'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_time_zone_offset_hour_minute() { + let times = [ + MockTime { is_utc: true, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 561, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 3600, ..Default::default() }, + ]; + + check_all(×, "'%z'", &[Ok("'+0000'"), Ok("'+0000'"), Ok("'+0009'"), Ok("'+0100'")]); + check_all(×, "'%1z'", &[Ok("'+0000'"), Ok("'+0000'"), Ok("'+0009'"), Ok("'+0100'")]); + check_all(×, "'%6z'", &[Ok("'+00000'"), Ok("'+00000'"), Ok("'+00009'"), Ok("'+00100'")]); + check_all(×, "'%-6z'", &[Ok("'-00000'"), Ok("'+00000'"), Ok("'+00009'"), Ok("'+00100'")]); + check_all(×, "'%-_6z'", &[Ok("' -000'"), Ok("' +000'"), Ok("' +009'"), Ok("' +100'")]); + check_all(×, "'%-06z'", &[Ok("'-00000'"), Ok("'+00000'"), Ok("'+00009'"), Ok("'+00100'")]); + check_all(×, "'%0_6z'", &[Ok("' +000'"), Ok("' +000'"), Ok("' +009'"), Ok("' +100'")]); + check_all(×, "'%_06z'", &[Ok("'+00000'"), Ok("'+00000'"), Ok("'+00009'"), Ok("'+00100'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_time_zone_offset_hour_minute_colon() { + let times = [ + MockTime { is_utc: true, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 561, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 3600, ..Default::default() }, + ]; + + check_all(×, "'%:z'", &[Ok("'+00:00'"), Ok("'+00:00'"), Ok("'+00:09'"), Ok("'+01:00'")]); + check_all(×, "'%1:z'", &[Ok("'+00:00'"), Ok("'+00:00'"), Ok("'+00:09'"), Ok("'+01:00'")]); + check_all(×, "'%7:z'", &[Ok("'+000:00'"), Ok("'+000:00'"), Ok("'+000:09'"), Ok("'+001:00'")]); + check_all(×, "'%-7:z'", &[Ok("'-000:00'"), Ok("'+000:00'"), Ok("'+000:09'"), Ok("'+001:00'")]); + check_all(×, "'%-_7:z'", &[Ok("' -0:00'"), Ok("' +0:00'"), Ok("' +0:09'"), Ok("' +1:00'")]); + check_all(×, "'%-07:z'", &[Ok("'-000:00'"), Ok("'+000:00'"), Ok("'+000:09'"), Ok("'+001:00'")]); + check_all(×, "'%0_7:z'", &[Ok("' +0:00'"), Ok("' +0:00'"), Ok("' +0:09'"), Ok("' +1:00'")]); + check_all(×, "'%_07:z'", &[Ok("'+000:00'"), Ok("'+000:00'"), Ok("'+000:09'"), Ok("'+001:00'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_time_zone_offset_hour_minute_second_colon() { + let times = [ + MockTime { is_utc: true, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 561, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 3600, ..Default::default() }, + ]; + + check_all(×, "'%::z'", &[Ok("'+00:00:00'"), Ok("'+00:00:00'"), Ok("'+00:09:21'"), Ok("'+01:00:00'")]); + check_all(×, "'%1::z'", &[Ok("'+00:00:00'"), Ok("'+00:00:00'"), Ok("'+00:09:21'"), Ok("'+01:00:00'")]); + check_all(×, "'%10::z'", &[Ok("'+000:00:00'"), Ok("'+000:00:00'"), Ok("'+000:09:21'"), Ok("'+001:00:00'")]); + check_all(×, "'%-10::z'", &[Ok("'-000:00:00'"), Ok("'+000:00:00'"), Ok("'+000:09:21'"), Ok("'+001:00:00'")]); + check_all(×, "'%-_10::z'", &[Ok("' -0:00:00'"), Ok("' +0:00:00'"), Ok("' +0:09:21'"), Ok("' +1:00:00'")]); + check_all(×, "'%-010::z'", &[Ok("'-000:00:00'"), Ok("'+000:00:00'"), Ok("'+000:09:21'"), Ok("'+001:00:00'")]); + check_all(×, "'%0_10::z'", &[Ok("' +0:00:00'"), Ok("' +0:00:00'"), Ok("' +0:09:21'"), Ok("' +1:00:00'")]); + check_all(×, "'%_010::z'", &[Ok("'+000:00:00'"), Ok("'+000:00:00'"), Ok("'+000:09:21'"), Ok("'+001:00:00'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_time_zone_offset_colon_minimal() { + let times = [ + MockTime { is_utc: true, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 0, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 540, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 561, ..Default::default() }, + MockTime { is_utc: false, utc_offset: 3600, ..Default::default() }, + ]; + + check_all(×, "'%:::z'", &[Ok("'+00'"), Ok("'+00'"), Ok("'+00:09'"), Ok("'+00:09:21'"), Ok("'+01'")]); + check_all(×, "'%1:::z'", &[Ok("'+00'"), Ok("'+00'"), Ok("'+00:09'"), Ok("'+00:09:21'"), Ok("'+01'")]); + check_all(×, "'%10:::z'", &[Ok("'+000000000'"), Ok("'+000000000'"), Ok("'+000000:09'"), Ok("'+000:09:21'"), Ok("'+000000001'")]); + check_all(×, "'%-10:::z'", &[Ok("'-000000000'"), Ok("'+000000000'"), Ok("'+000000:09'"), Ok("'+000:09:21'"), Ok("'+000000001'")]); + check_all(×, "'%-_10:::z'", &[Ok("' -0'"), Ok("' +0'"), Ok("' +0:09'"), Ok("' +0:09:21'"), Ok("' +1'")]); + check_all(×, "'%-010:::z'", &[Ok("'-000000000'"), Ok("'+000000000'"), Ok("'+000000:09'"), Ok("'+000:09:21'"), Ok("'+000000001'")]); + check_all(×, "'%0_10:::z'", &[Ok("' +0'"), Ok("' +0'"), Ok("' +0:09'"), Ok("' +0:09:21'"), Ok("' +1'")]); + check_all(×, "'%_010:::z'", &[Ok("'+000000000'"), Ok("'+000000000'"), Ok("'+000000:09'"), Ok("'+000:09:21'"), Ok("'+000000001'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_time_zone_name() { + let times = [ + MockTime { time_zone: "", ..Default::default() }, + MockTime { time_zone: "UTC", ..Default::default() }, + MockTime { time_zone: "+0000", ..Default::default() }, + ]; + + check_all(×, "'%Z'", &[Ok("''"), Ok("'UTC'") , Ok("'+0000'")]); + check_all(×, "'%1Z'", &[Ok("''"), Ok("'UTC'") , Ok("'+0000'")]); + check_all(×, "'%6Z'", &[Ok("''"), Ok("' UTC'"), Ok("' +0000'")]); + check_all(×, "'%-_#^6Z'", &[Ok("''"), Ok("'utc'") , Ok("'+0000'")]); + check_all(×, "'%-0^6Z'", &[Ok("''"), Ok("'UTC'") , Ok("'+0000'")]); + check_all(×, "'%0_#6Z'", &[Ok("''"), Ok("' utc'"), Ok("' +0000'")]); + check_all(×, "'%_06Z'", &[Ok("''"), Ok("'000UTC'"), Ok("'0+0000'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_day_name() { + let times = [MockTime { day_of_week: 1, ..Default::default() }]; + + check_all(×, "'%A'", &[Ok("'Monday'")]); + check_all(×, "'%1A'", &[Ok("'Monday'")]); + check_all(×, "'%8A'", &[Ok("' Monday'")]); + check_all(×, "'%-_#^8A'", &[Ok("'MONDAY'")]); + check_all(×, "'%-0^8A'", &[Ok("'MONDAY'")]); + check_all(×, "'%0_#8A'", &[Ok("' MONDAY'")]); + check_all(×, "'%_08A'", &[Ok("'00Monday'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_day_name_abbr() { + let times = [MockTime { day_of_week: 1, ..Default::default() }]; + + check_all(×, "'%a'", &[Ok("'Mon'")]); + check_all(×, "'%1a'", &[Ok("'Mon'")]); + check_all(×, "'%8a'", &[Ok("' Mon'")]); + check_all(×, "'%-_#^8a'", &[Ok("'MON'")]); + check_all(×, "'%-0^8a'", &[Ok("'MON'")]); + check_all(×, "'%0_#8a'", &[Ok("' MON'")]); + check_all(×, "'%_08a'", &[Ok("'00000Mon'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_day_from_1() { + let times = [MockTime { day_of_week: 7, ..Default::default() }]; + + check_all(×, "'%u'", &[Ok("'7'")]); + check_all(×, "'%1u'", &[Ok("'7'")]); + check_all(×, "'%4u'", &[Ok("'0007'")]); + check_all(×, "'%-_4u'", &[Ok("'7'")]); + check_all(×, "'%-04u'", &[Ok("'7'")]); + check_all(×, "'%0_4u'", &[Ok("' 7'")]); + check_all(×, "'%_04u'", &[Ok("'0007'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_day_from_0() { + let times = [MockTime { day_of_week: 0, ..Default::default() }]; + + check_all(×, "'%w'", &[Ok("'0'")]); + check_all(×, "'%1w'", &[Ok("'0'")]); + check_all(×, "'%4w'", &[Ok("'0000'")]); + check_all(×, "'%-_4w'", &[Ok("'0'")]); + check_all(×, "'%-04w'", &[Ok("'0'")]); + check_all(×, "'%0_4w'", &[Ok("' 0'")]); + check_all(×, "'%_04w'", &[Ok("'0000'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_year_iso_8601() { + let times = [ + MockTime { year: -1111, day_of_year: 30, ..Default::default() }, + MockTime { year: -11, day_of_year: 30, ..Default::default() }, + MockTime { year: 1, day_of_year: 30, ..Default::default() }, + MockTime { year: 1111, day_of_year: 30, ..Default::default() }, + ]; + + check_all(×, "'%G'", &[Ok("'-1111'"), Ok("'-0011'"), Ok("'0001'"), Ok("'1111'")]); + check_all(×, "'%1G'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%4G'", &[Ok("'-1111'"), Ok("'-011'"), Ok("'0001'"), Ok("'1111'")]); + check_all(×, "'%-_5G'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%-05G'", &[Ok("'-1111'"), Ok("'-11'"), Ok("'1'"), Ok("'1111'")]); + check_all(×, "'%0_5G'", &[Ok("'-1111'"), Ok("' -11'"), Ok("' 1'"), Ok("' 1111'")]); + check_all(×, "'%_05G'", &[Ok("'-1111'"), Ok("'-0011'"), Ok("'00001'"), Ok("'01111'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_year_iso_8601_rem_100() { + let times = [ + MockTime { year: -1111, day_of_year: 30, ..Default::default() }, + MockTime { year: -11, day_of_year: 30, ..Default::default() }, + MockTime { year: 1, day_of_year: 30, ..Default::default() }, + MockTime { year: 1111, day_of_year: 30, ..Default::default() }, + ]; + + check_all(×, "'%g'", &[Ok("'89'"), Ok("'89'"), Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1g'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4g'", &[Ok("'0089'"), Ok("'0089'"), Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_g'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0g'", &[Ok("'89'"), Ok("'89'"), Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_g'", &[Ok("'89'"), Ok("'89'"), Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0g'", &[Ok("'89'"), Ok("'89'"), Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_number_iso_8601() { + let times = [ + MockTime { year: 2000, day_of_year: 7, ..Default::default() }, + MockTime { year: 2000, day_of_year: 80, ..Default::default() }, + ]; + + check_all(×, "'%V'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1V'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4V'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_V'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0V'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_V'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0V'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_number_from_sunday() { + let times = [ + MockTime { year: 2000, day_of_year: 7, ..Default::default() }, + MockTime { year: 2000, day_of_year: 77, ..Default::default() }, + ]; + + check_all(×, "'%U'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1U'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4U'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_U'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0U'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_U'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0U'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_week_number_from_monday() { + let times = [ + MockTime { year: 2000, day_of_year: 7, ..Default::default() }, + MockTime { year: 2000, day_of_year: 77, ..Default::default() }, + ]; + + check_all(×, "'%W'", &[Ok("'01'"), Ok("'11'")]); + check_all(×, "'%1W'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4W'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_W'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0W'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_W'", &[Ok("' 1'"), Ok("'11'")]); + check_all(×, "'%_0W'", &[Ok("'01'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_seconds_since_epoch() { + let times = [ + MockTime { to_int: 1, ..Default::default() }, + MockTime { to_int: 11, ..Default::default() }, + ]; + + check_all(×, "'%s'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%1s'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%4s'", &[Ok("'0001'"), Ok("'0011'")]); + check_all(×, "'%-_s'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%-0s'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%0_s'", &[Ok("'1'"), Ok("'11'")]); + check_all(×, "'%_0s'", &[Ok("'1'"), Ok("'11'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_newline() { + let times = [MockTime::default()]; + + check_all(×, "'%n'", &[Ok("'\n'")]); + check_all(×, "'%1n'", &[Ok("'\n'")]); + check_all(×, "'%6n'", &[Ok("' \n'")]); + check_all(×, "'%-_#^6n'", &[Ok("'\n'")]); + check_all(×, "'%-0^6n'", &[Ok("'\n'")]); + check_all(×, "'%0_#6n'", &[Ok("' \n'")]); + check_all(×, "'%_06n'", &[Ok("'00000\n'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_tabulation() { + let times = [MockTime::default()]; + + check_all(×, "'%t'", &[Ok("'\t'")]); + check_all(×, "'%1t'", &[Ok("'\t'")]); + check_all(×, "'%6t'", &[Ok("' \t'")]); + check_all(×, "'%-_#^6t'", &[Ok("'\t'")]); + check_all(×, "'%-0^6t'", &[Ok("'\t'")]); + check_all(×, "'%0_#6t'", &[Ok("' \t'")]); + check_all(×, "'%_06t'", &[Ok("'00000\t'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_percent() { + let times = [MockTime::default()]; + + check_all(×, "'%%'", &[Ok("'%'")]); + check_all(×, "'%1%'", &[Ok("'%'")]); + check_all(×, "'%6%'", &[Ok("' %'")]); + check_all(×, "'%-_#^6%'", &[Ok("'%'")]); + check_all(×, "'%-0^6%'", &[Ok("'%'")]); + check_all(×, "'%0_#6%'", &[Ok("' %'")]); + check_all(×, "'%_06%'", &[Ok("'00000%'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_date_time() { + let times = [ + MockTime::new(1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""), + MockTime::new(-1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""), + ]; + + check_all(×, "'%c'", &[Ok("'Thu Jan 1 00:00:00 1970'"), Ok("'Thu Jan 1 00:00:00 -1970'")]); + check_all(×, "'%1c'", &[Ok("'Thu Jan 1 00:00:00 1970'"), Ok("'Thu Jan 1 00:00:00 -1970'")]); + check_all(×, "'%30c'", &[Ok("' Thu Jan 1 00:00:00 1970'"), Ok("' Thu Jan 1 00:00:00 -1970'")]); + check_all(×, "'%-^_#30c'", &[Ok("' THU JAN 1 00:00:00 1970'"), Ok("' THU JAN 1 00:00:00 -1970'")]); + check_all(×, "'%-0^30c'", &[Ok("'000000THU JAN 1 00:00:00 1970'"), Ok("'00000THU JAN 1 00:00:00 -1970'")]); + check_all(×, "'%0_#30c'", &[Ok("' Thu Jan 1 00:00:00 1970'"), Ok("' Thu Jan 1 00:00:00 -1970'")]); + check_all(×, "'%_030c'", &[Ok("'000000Thu Jan 1 00:00:00 1970'"), Ok("'00000Thu Jan 1 00:00:00 -1970'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_date() { + let times = [ + MockTime { year: 1234, month: 5, day: 6, ..Default::default() }, + MockTime { year: -1234, month: 5, day: 6, ..Default::default() }, + ]; + + check_all(×, "'%D'", &[Ok("'05/06/34'"), Ok("'05/06/66'")]); + check_all(×, "'%1D'", &[Ok("'05/06/34'"), Ok("'05/06/66'")]); + check_all(×, "'%10D'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%-^_#10D'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%-0^10D'", &[Ok("'0005/06/34'"), Ok("'0005/06/66'")]); + check_all(×, "'%0_#10D'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%_010D'", &[Ok("'0005/06/34'"), Ok("'0005/06/66'")]); + + check_all(×, "'%x'", &[Ok("'05/06/34'"), Ok("'05/06/66'")]); + check_all(×, "'%1x'", &[Ok("'05/06/34'"), Ok("'05/06/66'")]); + check_all(×, "'%10x'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%-^_#10x'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%-0^10x'", &[Ok("'0005/06/34'"), Ok("'0005/06/66'")]); + check_all(×, "'%0_#10x'", &[Ok("' 05/06/34'"), Ok("' 05/06/66'")]); + check_all(×, "'%_010x'", &[Ok("'0005/06/34'"), Ok("'0005/06/66'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_iso_8601() { + let times = [ + MockTime { year: 1234, month: 5, day: 6, ..Default::default() }, + MockTime { year: -1234, month: 5, day: 6, ..Default::default() }, + ]; + + check_all(×, "'%F'", &[Ok("'1234-05-06'"), Ok("'-1234-05-06'")]); + check_all(×, "'%1F'", &[Ok("'1234-05-06'"), Ok("'-1234-05-06'")]); + check_all(×, "'%12F'", &[Ok("' 1234-05-06'"), Ok("' -1234-05-06'")]); + check_all(×, "'%-^_#12F'", &[Ok("' 1234-05-06'"), Ok("' -1234-05-06'")]); + check_all(×, "'%-0^12F'", &[Ok("'001234-05-06'"), Ok("'0-1234-05-06'")]); + check_all(×, "'%0_#12F'", &[Ok("' 1234-05-06'"), Ok("' -1234-05-06'")]); + check_all(×, "'%_012F'", &[Ok("'001234-05-06'"), Ok("'0-1234-05-06'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_vms_date() { + let times = [ + MockTime { year: 1234, month: 7, day: 6, ..Default::default() }, + MockTime { year: -1234, month: 7, day: 6, ..Default::default() }, + ]; + + check_all(×, "'%v'", &[Ok("' 6-JUL-1234'"), Ok("' 6-JUL--1234'")]); + check_all(×, "'%1v'", &[Ok("' 6-JUL-1234'"), Ok("' 6-JUL--1234'")]); + check_all(×, "'%13v'", &[Ok("' 6-JUL-1234'"), Ok("' 6-JUL--1234'")]); + check_all(×, "'%-^_#13v'", &[Ok("' 6-JUL-1234'"), Ok("' 6-JUL--1234'")]); + check_all(×, "'%-0^13v'", &[Ok("'00 6-JUL-1234'"), Ok("'0 6-JUL--1234'")]); + check_all(×, "'%0_#13v'", &[Ok("' 6-JUL-1234'"), Ok("' 6-JUL--1234'")]); + check_all(×, "'%_013v'", &[Ok("'00 6-JUL-1234'"), Ok("'0 6-JUL--1234'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_time_12h() { + let times = [ + MockTime { hour: 11, minute: 2, second: 3, ..Default::default() }, + MockTime { hour: 12, minute: 2, second: 3, ..Default::default() }, + ]; + + check_all(×, "'%r'", &[Ok("'11:02:03 AM'"), Ok("'12:02:03 PM'")]); + check_all(×, "'%1r'", &[Ok("'11:02:03 AM'"), Ok("'12:02:03 PM'")]); + check_all(×, "'%13r'", &[Ok("' 11:02:03 AM'"), Ok("' 12:02:03 PM'")]); + check_all(×, "'%-^_#13r'", &[Ok("' 11:02:03 AM'"), Ok("' 12:02:03 PM'")]); + check_all(×, "'%-0^13r'", &[Ok("'0011:02:03 AM'"), Ok("'0012:02:03 PM'")]); + check_all(×, "'%0_#13r'", &[Ok("' 11:02:03 AM'"), Ok("' 12:02:03 PM'")]); + check_all(×, "'%_013r'", &[Ok("'0011:02:03 AM'"), Ok("'0012:02:03 PM'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_hour_minute_24h() { + let times = [MockTime { hour: 13, minute: 2, ..Default::default() }]; + + check_all(×, "'%R'", &[Ok("'13:02'")]); + check_all(×, "'%1R'", &[Ok("'13:02'")]); + check_all(×, "'%7R'", &[Ok("' 13:02'")]); + check_all(×, "'%-^_#7R'", &[Ok("' 13:02'")]); + check_all(×, "'%-0^7R'", &[Ok("'0013:02'")]); + check_all(×, "'%0_#7R'", &[Ok("' 13:02'")]); + check_all(×, "'%_07R'", &[Ok("'0013:02'")]); +} + +#[test] +#[rustfmt::skip] +fn test_format_combination_time_24h() { + let times = [MockTime { hour: 13, minute: 2, second: 3, ..Default::default() }]; + + check_all(×, "'%T'", &[Ok("'13:02:03'")]); + check_all(×, "'%1T'", &[Ok("'13:02:03'")]); + check_all(×, "'%10T'", &[Ok("' 13:02:03'")]); + check_all(×, "'%-^_#10T'", &[Ok("' 13:02:03'")]); + check_all(×, "'%-0^10T'", &[Ok("'0013:02:03'")]); + check_all(×, "'%0_#10T'", &[Ok("' 13:02:03'")]); + check_all(×, "'%_010T'", &[Ok("'0013:02:03'")]); + + check_all(×, "'%X'", &[Ok("'13:02:03'")]); + check_all(×, "'%1X'", &[Ok("'13:02:03'")]); + check_all(×, "'%10X'", &[Ok("' 13:02:03'")]); + check_all(×, "'%-^_#10X'", &[Ok("' 13:02:03'")]); + check_all(×, "'%-0^10X'", &[Ok("'0013:02:03'")]); + check_all(×, "'%0_#10X'", &[Ok("' 13:02:03'")]); + check_all(×, "'%_010X'", &[Ok("'0013:02:03'")]); +} + +#[test] +fn test_format_invalid() { + let time = MockTime::default(); + + check_format(&time, "%", Err(Error::InvalidFormatString)); + check_format(&time, "%-4", Err(Error::InvalidFormatString)); + check_format(&time, "%-", Err(Error::InvalidFormatString)); + check_format(&time, "%-_", Err(Error::InvalidFormatString)); +} + +#[test] +fn test_format_literal() { + let time = MockTime::default(); + + check_format(&time, "% ", Ok("% ")); + check_format(&time, "%-4 ", Ok("%-4 ")); + check_format(&time, "%- ", Ok("%- ")); + check_format(&time, "%-_ ", Ok("%-_ ")); + + check_format(&time, "'%:'", Ok("'%:'")); + check_format(&time, "'%::'", Ok("'%::'")); + check_format(&time, "'%:::'", Ok("'%:::'")); + check_format(&time, "'%:::m'", Ok("'%:::m'")); + check_format(&time, "'%::::z'", Ok("'%::::z'")); +} + +#[test] +fn test_format_with_modifiers() { + let time = MockTime::new(1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""); + + check_format(&time, "%EY, %Oy, %EE, %OO", Ok("1970, 70, %EE, %OO")); +} + +#[test] +fn test_format_large_width() { + let time = MockTime::new(1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""); + + check_format(&time, "%2147483647m", Err(Error::WriteZero)); + check_format(&time, "%2147483648m", Ok("%2147483648m")); + check_format(&time, "%-100000000m", Ok("1")); +} + +#[cfg(feature = "alloc")] +#[test] +fn test_format_formatted_string_too_large() { + let time = MockTime::new(1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""); + + let mut buf = Vec::new(); + let result = TimeFormatter::new(&time, "%4718593m").fmt(&mut buf); + + assert_eq!(buf.len(), 4_718_592); + assert_eq!(result, Err(Error::FormattedStringTooLarge)); +} + +#[test] +fn test_format_small_buffer() { + let time = MockTime::new(1970, 1, 1, 0, 0, 0, 0, 4, 1, 0, false, 0, ""); + + let mut buf = [0u8; 3]; + let result = TimeFormatter::new(&time, "%Y").fmt(&mut &mut buf[..]); + assert_eq!(result, Err(Error::WriteZero)); +} + +#[test] +fn test_format_empty() { + let time = MockTime::default(); + + check_format(&time, "", Ok("")); +}