use crate::{
civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
error::{err, ErrorContext},
fmt::{
strtime::{format::Formatter, parse::Parser},
Write,
},
tz::{Offset, OffsetConflict, TimeZone, TimeZoneDatabase},
util::{
self, escape,
rangeint::RInto,
t::{self, C},
},
Error, Timestamp, Zoned,
};
mod format;
mod parse;
#[inline]
pub fn parse(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<BrokenDownTime, Error> {
BrokenDownTime::parse(format, input)
}
#[cfg(any(test, feature = "alloc"))]
#[inline]
pub fn format(
format: impl AsRef<[u8]>,
broken_down_time: impl Into<BrokenDownTime>,
) -> Result<alloc::string::String, Error> {
let broken_down_time: BrokenDownTime = broken_down_time.into();
let format = format.as_ref();
let mut buf = alloc::string::String::with_capacity(format.len());
broken_down_time.format(format, &mut buf)?;
Ok(buf)
}
#[derive(Clone, Debug)]
pub struct Config<C> {
custom: C,
lenient: bool,
}
impl Config<DefaultCustom> {
#[inline]
pub const fn new() -> Config<DefaultCustom> {
Config { custom: DefaultCustom::new(), lenient: false }
}
}
impl<C> Config<C> {
#[inline]
pub fn custom<U: Custom>(self, custom: U) -> Config<U> {
Config { custom, lenient: self.lenient }
}
#[inline]
pub fn lenient(self, yes: bool) -> Config<C> {
Config { lenient: yes, ..self }
}
}
pub trait Custom: Sized {
fn format_datetime<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%Y M%m %-d, %a %H:%M:%S", wtr)
}
fn format_date<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%Y M%m %-d", wtr)
}
fn format_time<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%H:%M:%S", wtr)
}
fn format_12hour_time<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%-I:%M:%S %p", wtr)
}
}
#[derive(Clone, Debug, Default)]
pub struct DefaultCustom(());
impl DefaultCustom {
pub const fn new() -> DefaultCustom {
DefaultCustom(())
}
}
impl Custom for DefaultCustom {}
#[derive(Clone, Debug, Default)]
pub struct PosixCustom(());
impl PosixCustom {
pub const fn new() -> PosixCustom {
PosixCustom(())
}
}
impl Custom for PosixCustom {
fn format_datetime<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%a %b %e %H:%M:%S %Y", wtr)
}
fn format_date<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%m/%d/%y", wtr)
}
fn format_time<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%H:%M:%S", wtr)
}
fn format_12hour_time<W: Write>(
&self,
config: &Config<Self>,
_ext: &Extension,
tm: &BrokenDownTime,
wtr: &mut W,
) -> Result<(), Error> {
tm.format_with_config(config, "%I:%M:%S %p", wtr)
}
}
#[derive(Debug, Default)]
pub struct BrokenDownTime {
year: Option<t::Year>,
month: Option<t::Month>,
day: Option<t::Day>,
day_of_year: Option<t::DayOfYear>,
iso_week_year: Option<t::ISOYear>,
iso_week: Option<t::ISOWeek>,
week_sun: Option<t::WeekNum>,
week_mon: Option<t::WeekNum>,
hour: Option<t::Hour>,
minute: Option<t::Minute>,
second: Option<t::Second>,
subsec: Option<t::SubsecNanosecond>,
offset: Option<Offset>,
weekday: Option<Weekday>,
meridiem: Option<Meridiem>,
timestamp: Option<Timestamp>,
tz: Option<TimeZone>,
#[cfg(feature = "alloc")]
iana: Option<alloc::string::String>,
}
impl BrokenDownTime {
#[inline]
pub fn parse(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<BrokenDownTime, Error> {
BrokenDownTime::parse_mono(format.as_ref(), input.as_ref())
}
#[inline]
fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result<BrokenDownTime, Error> {
let mut pieces = BrokenDownTime::default();
let mut p = Parser { fmt, inp, tm: &mut pieces };
p.parse().context("strptime parsing failed")?;
if !p.inp.is_empty() {
return Err(err!(
"strptime expects to consume the entire input, but \
{remaining:?} remains unparsed",
remaining = escape::Bytes(p.inp),
));
}
Ok(pieces)
}
#[inline]
pub fn parse_prefix(
format: impl AsRef<[u8]>,
input: impl AsRef<[u8]>,
) -> Result<(BrokenDownTime, usize), Error> {
BrokenDownTime::parse_prefix_mono(format.as_ref(), input.as_ref())
}
#[inline]
fn parse_prefix_mono(
fmt: &[u8],
inp: &[u8],
) -> Result<(BrokenDownTime, usize), Error> {
let mkoffset = util::parse::offseter(inp);
let mut pieces = BrokenDownTime::default();
let mut p = Parser { fmt, inp, tm: &mut pieces };
p.parse().context("strptime parsing failed")?;
let remainder = mkoffset(p.inp);
Ok((pieces, remainder))
}
#[inline]
pub fn format<W: Write>(
&self,
format: impl AsRef<[u8]>,
mut wtr: W,
) -> Result<(), Error> {
self.format_with_config(&Config::new(), format, &mut wtr)
}
#[inline]
pub fn format_with_config<W: Write, L: Custom>(
&self,
config: &Config<L>,
format: impl AsRef<[u8]>,
wtr: &mut W,
) -> Result<(), Error> {
let fmt = format.as_ref();
let mut formatter = Formatter { config, fmt, tm: self, wtr };
formatter.format().context("strftime formatting failed")?;
Ok(())
}
#[cfg(feature = "alloc")]
#[inline]
pub fn to_string(
&self,
format: impl AsRef<[u8]>,
) -> Result<alloc::string::String, Error> {
let format = format.as_ref();
let mut buf = alloc::string::String::with_capacity(format.len());
self.format(format, &mut buf)?;
Ok(buf)
}
#[cfg(feature = "alloc")]
#[inline]
pub fn to_string_with_config<L: Custom>(
&self,
config: &Config<L>,
format: impl AsRef<[u8]>,
) -> Result<alloc::string::String, Error> {
let format = format.as_ref();
let mut buf = alloc::string::String::with_capacity(format.len());
self.format_with_config(config, format, &mut buf)?;
Ok(buf)
}
#[inline]
pub fn to_zoned(&self) -> Result<Zoned, Error> {
self.to_zoned_with(crate::tz::db())
}
#[inline]
pub fn to_zoned_with(
&self,
db: &TimeZoneDatabase,
) -> Result<Zoned, Error> {
match (self.offset, self.iana_time_zone()) {
(None, None) => {
if let Some(ts) = self.timestamp {
return Ok(ts.to_zoned(TimeZone::unknown()));
}
Err(err!(
"either offset (from %z) or IANA time zone identifier \
(from %Q) is required for parsing zoned datetime",
))
}
(Some(offset), None) => {
let ts = match self.timestamp {
Some(ts) => ts,
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let ts =
offset.to_timestamp(dt).with_context(|| {
err!(
"parsed datetime {dt} and offset {offset}, \
but combining them into a zoned datetime \
is outside Jiff's supported timestamp range",
)
})?;
ts
}
};
Ok(ts.to_zoned(TimeZone::fixed(offset)))
}
(None, Some(iana)) => {
let tz = db.get(iana)?;
match self.timestamp {
Some(ts) => Ok(ts.to_zoned(tz)),
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
Ok(tz.to_zoned(dt)?)
}
}
}
(Some(offset), Some(iana)) => {
let tz = db.get(iana)?;
match self.timestamp {
Some(ts) => {
let zdt = ts.to_zoned(tz);
if zdt.offset() != offset {
return Err(err!(
"parsed time zone offset `{offset}`, but \
offset from timestamp `{ts}` for time zone \
`{iana}` is `{got}`",
got = zdt.offset(),
));
}
Ok(zdt)
}
None => {
let dt = self.to_datetime().context(
"datetime required to parse zoned datetime",
)?;
let azdt =
OffsetConflict::Reject.resolve(dt, offset, tz)?;
let zdt = azdt.unambiguous().unwrap();
Ok(zdt)
}
}
}
}
}
#[inline]
pub fn to_timestamp(&self) -> Result<Timestamp, Error> {
if let Some(timestamp) = self.timestamp() {
return Ok(timestamp);
}
let dt = self
.to_datetime()
.context("datetime required to parse timestamp")?;
let offset =
self.to_offset().context("offset required to parse timestamp")?;
offset.to_timestamp(dt).with_context(|| {
err!(
"parsed datetime {dt} and offset {offset}, \
but combining them into a timestamp is outside \
Jiff's supported timestamp range",
)
})
}
#[inline]
fn to_offset(&self) -> Result<Offset, Error> {
let Some(offset) = self.offset else {
return Err(err!(
"parsing format did not include time zone offset directive",
));
};
Ok(offset)
}
#[inline]
pub fn to_datetime(&self) -> Result<DateTime, Error> {
let date =
self.to_date().context("date required to parse datetime")?;
let time =
self.to_time().context("time required to parse datetime")?;
Ok(DateTime::from_parts(date, time))
}
#[inline]
pub fn to_date(&self) -> Result<Date, Error> {
let Some(year) = self.year else {
if let Some(date) = self.to_date_from_iso()? {
return Ok(date);
}
return Err(err!("missing year, date cannot be created"));
};
let mut date = self.to_date_from_gregorian(year)?;
if date.is_none() {
date = self.to_date_from_iso()?;
}
if date.is_none() {
date = self.to_date_from_day_of_year(year)?;
}
if date.is_none() {
date = self.to_date_from_week_sun(year)?;
}
if date.is_none() {
date = self.to_date_from_week_mon(year)?;
}
let Some(date) = date else {
return Err(err!(
"a month/day, day-of-year or week date must be \
present to create a date, but none were found",
));
};
if let Some(weekday) = self.weekday {
if weekday != date.weekday() {
return Err(err!(
"parsed weekday {weekday} does not match \
weekday {got} from parsed date {date}",
weekday = weekday_name_full(weekday),
got = weekday_name_full(date.weekday()),
));
}
}
Ok(date)
}
#[inline]
fn to_date_from_gregorian(
&self,
year: t::Year,
) -> Result<Option<Date>, Error> {
let (Some(month), Some(day)) = (self.month, self.day) else {
return Ok(None);
};
Ok(Some(Date::new_ranged(year, month, day).context("invalid date")?))
}
#[inline]
fn to_date_from_day_of_year(
&self,
year: t::Year,
) -> Result<Option<Date>, Error> {
let Some(doy) = self.day_of_year else { return Ok(None) };
Ok(Some({
let first =
Date::new_ranged(year, C(1).rinto(), C(1).rinto()).unwrap();
first
.with()
.day_of_year(doy.get())
.build()
.context("invalid date")?
}))
}
#[inline]
fn to_date_from_iso(&self) -> Result<Option<Date>, Error> {
let (Some(y), Some(w), Some(d)) =
(self.iso_week_year, self.iso_week, self.weekday)
else {
return Ok(None);
};
let wd = ISOWeekDate::new_ranged(y, w, d)
.context("invalid ISO 8601 week date")?;
Ok(Some(wd.date()))
}
#[inline]
fn to_date_from_week_sun(
&self,
year: t::Year,
) -> Result<Option<Date>, Error> {
let (Some(week), Some(weekday)) = (self.week_sun, self.weekday) else {
return Ok(None);
};
let week = i16::from(week);
let wday = i16::from(weekday.to_sunday_zero_offset());
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
.context("invalid date")?;
let first_sunday = first_of_year
.nth_weekday_of_month(1, Weekday::Sunday)
.map(|d| d.day_of_year())
.context("invalid date")?;
let doy = if week == 0 {
let days_before_first_sunday = 7 - wday;
let doy = first_sunday
.checked_sub(days_before_first_sunday)
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
)
})?;
if doy == 0 {
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Sunday based week number `{week}` \
in year `{year}`",
));
}
doy
} else {
let days_since_first_sunday = (week - 1) * 7 + wday;
let doy = first_sunday + days_since_first_sunday;
doy
};
let date = first_of_year
.with()
.day_of_year(doy)
.build()
.context("invalid date")?;
Ok(Some(date))
}
#[inline]
fn to_date_from_week_mon(
&self,
year: t::Year,
) -> Result<Option<Date>, Error> {
let (Some(week), Some(weekday)) = (self.week_mon, self.weekday) else {
return Ok(None);
};
let week = i16::from(week);
let wday = i16::from(weekday.to_monday_zero_offset());
let first_of_year = Date::new_ranged(year, C(1).rinto(), C(1).rinto())
.context("invalid date")?;
let first_monday = first_of_year
.nth_weekday_of_month(1, Weekday::Monday)
.map(|d| d.day_of_year())
.context("invalid date")?;
let doy = if week == 0 {
let days_before_first_monday = 7 - wday;
let doy = first_monday
.checked_sub(days_before_first_monday)
.ok_or_else(|| {
err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
)
})?;
if doy == 0 {
return Err(err!(
"weekday `{weekday:?}` is not valid for \
Monday based week number `{week}` \
in year `{year}`",
));
}
doy
} else {
let days_since_first_monday = (week - 1) * 7 + wday;
let doy = first_monday + days_since_first_monday;
doy
};
let date = first_of_year
.with()
.day_of_year(doy)
.build()
.context("invalid date")?;
Ok(Some(date))
}
#[inline]
pub fn to_time(&self) -> Result<Time, Error> {
let Some(hour) = self.hour_ranged() else {
if self.minute.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include minute directive (cannot have \
smaller time units with bigger time units missing)",
));
}
if self.second.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
));
}
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include hour directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
}
return Ok(Time::midnight());
};
let Some(minute) = self.minute else {
if self.second.is_some() {
return Err(err!(
"parsing format did not include minute directive, \
but did include second directive (cannot have \
smaller time units with bigger time units missing)",
));
}
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include minute directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
}
return Ok(Time::new_ranged(hour, C(0), C(0), C(0)));
};
let Some(second) = self.second else {
if self.subsec.is_some() {
return Err(err!(
"parsing format did not include second directive, \
but did include fractional second directive (cannot have \
smaller time units with bigger time units missing)",
));
}
return Ok(Time::new_ranged(hour, minute, C(0), C(0)));
};
let Some(subsec) = self.subsec else {
return Ok(Time::new_ranged(hour, minute, second, C(0)));
};
Ok(Time::new_ranged(hour, minute, second, subsec))
}
#[inline]
pub fn year(&self) -> Option<i16> {
self.year.map(|x| x.get())
}
#[inline]
pub fn month(&self) -> Option<i8> {
self.month.map(|x| x.get())
}
#[inline]
pub fn day(&self) -> Option<i8> {
self.day.map(|x| x.get())
}
#[inline]
pub fn day_of_year(&self) -> Option<i16> {
self.day_of_year.map(|x| x.get())
}
#[inline]
pub fn iso_week_year(&self) -> Option<i16> {
self.iso_week_year.map(|x| x.get())
}
#[inline]
pub fn iso_week(&self) -> Option<i8> {
self.iso_week.map(|x| x.get())
}
#[inline]
pub fn sunday_based_week(&self) -> Option<i8> {
self.week_sun.map(|x| x.get())
}
#[inline]
pub fn monday_based_week(&self) -> Option<i8> {
self.week_mon.map(|x| x.get())
}
#[inline]
pub fn hour(&self) -> Option<i8> {
self.hour_ranged().map(|x| x.get())
}
#[inline]
fn hour_ranged(&self) -> Option<t::Hour> {
let hour = self.hour?;
Some(match self.meridiem() {
None => hour,
Some(Meridiem::AM) => hour % C(12),
Some(Meridiem::PM) => (hour % C(12)) + C(12),
})
}
#[inline]
pub fn minute(&self) -> Option<i8> {
self.minute.map(|x| x.get())
}
#[inline]
pub fn second(&self) -> Option<i8> {
self.second.map(|x| x.get())
}
#[inline]
pub fn subsec_nanosecond(&self) -> Option<i32> {
self.subsec.map(|x| x.get())
}
#[inline]
pub fn offset(&self) -> Option<Offset> {
self.offset
}
#[inline]
pub fn iana_time_zone(&self) -> Option<&str> {
#[cfg(feature = "alloc")]
{
self.iana.as_deref()
}
#[cfg(not(feature = "alloc"))]
{
None
}
}
#[inline]
pub fn weekday(&self) -> Option<Weekday> {
self.weekday
}
#[inline]
pub fn meridiem(&self) -> Option<Meridiem> {
self.meridiem
}
#[inline]
pub fn timestamp(&self) -> Option<Timestamp> {
self.timestamp
}
#[inline]
pub fn set_year(&mut self, year: Option<i16>) -> Result<(), Error> {
self.year = match year {
None => None,
Some(year) => Some(t::Year::try_new("year", year)?),
};
Ok(())
}
#[inline]
pub fn set_month(&mut self, month: Option<i8>) -> Result<(), Error> {
self.month = match month {
None => None,
Some(month) => Some(t::Month::try_new("month", month)?),
};
Ok(())
}
#[inline]
pub fn set_day(&mut self, day: Option<i8>) -> Result<(), Error> {
self.day = match day {
None => None,
Some(day) => Some(t::Day::try_new("day", day)?),
};
Ok(())
}
#[inline]
pub fn set_day_of_year(&mut self, day: Option<i16>) -> Result<(), Error> {
self.day_of_year = match day {
None => None,
Some(day) => Some(t::DayOfYear::try_new("day-of-year", day)?),
};
Ok(())
}
#[inline]
pub fn set_iso_week_year(
&mut self,
year: Option<i16>,
) -> Result<(), Error> {
self.iso_week_year = match year {
None => None,
Some(year) => Some(t::ISOYear::try_new("year", year)?),
};
Ok(())
}
#[inline]
pub fn set_iso_week(
&mut self,
week_number: Option<i8>,
) -> Result<(), Error> {
self.iso_week = match week_number {
None => None,
Some(wk) => Some(t::ISOWeek::try_new("week-number", wk)?),
};
Ok(())
}
#[inline]
pub fn set_sunday_based_week(
&mut self,
week_number: Option<i8>,
) -> Result<(), Error> {
self.week_sun = match week_number {
None => None,
Some(wk) => Some(t::WeekNum::try_new("week-number", wk)?),
};
Ok(())
}
#[inline]
pub fn set_monday_based_week(
&mut self,
week_number: Option<i8>,
) -> Result<(), Error> {
self.week_mon = match week_number {
None => None,
Some(wk) => Some(t::WeekNum::try_new("week-number", wk)?),
};
Ok(())
}
#[inline]
pub fn set_hour(&mut self, hour: Option<i8>) -> Result<(), Error> {
self.hour = match hour {
None => None,
Some(hour) => Some(t::Hour::try_new("hour", hour)?),
};
Ok(())
}
#[inline]
pub fn set_minute(&mut self, minute: Option<i8>) -> Result<(), Error> {
self.minute = match minute {
None => None,
Some(minute) => Some(t::Minute::try_new("minute", minute)?),
};
Ok(())
}
#[inline]
pub fn set_second(&mut self, second: Option<i8>) -> Result<(), Error> {
self.second = match second {
None => None,
Some(second) => Some(t::Second::try_new("second", second)?),
};
Ok(())
}
#[inline]
pub fn set_subsec_nanosecond(
&mut self,
subsec_nanosecond: Option<i32>,
) -> Result<(), Error> {
self.subsec = match subsec_nanosecond {
None => None,
Some(subsec_nanosecond) => Some(t::SubsecNanosecond::try_new(
"subsecond-nanosecond",
subsec_nanosecond,
)?),
};
Ok(())
}
#[inline]
pub fn set_offset(&mut self, offset: Option<Offset>) {
self.offset = offset;
}
#[cfg(feature = "alloc")]
#[inline]
pub fn set_iana_time_zone(&mut self, id: Option<alloc::string::String>) {
self.iana = id;
}
#[inline]
pub fn set_weekday(&mut self, weekday: Option<Weekday>) {
self.weekday = weekday;
}
#[inline]
pub fn set_meridiem(&mut self, meridiem: Option<Meridiem>) {
self.meridiem = meridiem;
}
#[inline]
pub fn set_timestamp(&mut self, timestamp: Option<Timestamp>) {
self.timestamp = timestamp;
}
}
impl<'a> From<&'a Zoned> for BrokenDownTime {
fn from(zdt: &'a Zoned) -> BrokenDownTime {
#[cfg(feature = "alloc")]
let iana = {
use alloc::string::ToString;
zdt.time_zone().iana_name().map(|s| s.to_string())
};
BrokenDownTime {
offset: Some(zdt.offset()),
timestamp: Some(zdt.timestamp()),
tz: Some(zdt.time_zone().clone()),
#[cfg(feature = "alloc")]
iana,
..BrokenDownTime::from(zdt.datetime())
}
}
}
impl From<Timestamp> for BrokenDownTime {
fn from(ts: Timestamp) -> BrokenDownTime {
let dt = Offset::UTC.to_datetime(ts);
BrokenDownTime {
offset: Some(Offset::UTC),
timestamp: Some(ts),
..BrokenDownTime::from(dt)
}
}
}
impl From<DateTime> for BrokenDownTime {
fn from(dt: DateTime) -> BrokenDownTime {
let (d, t) = (dt.date(), dt.time());
BrokenDownTime {
year: Some(d.year_ranged()),
month: Some(d.month_ranged()),
day: Some(d.day_ranged()),
hour: Some(t.hour_ranged()),
minute: Some(t.minute_ranged()),
second: Some(t.second_ranged()),
subsec: Some(t.subsec_nanosecond_ranged()),
meridiem: Some(Meridiem::from(t)),
..BrokenDownTime::default()
}
}
}
impl From<Date> for BrokenDownTime {
fn from(d: Date) -> BrokenDownTime {
BrokenDownTime {
year: Some(d.year_ranged()),
month: Some(d.month_ranged()),
day: Some(d.day_ranged()),
..BrokenDownTime::default()
}
}
}
impl From<ISOWeekDate> for BrokenDownTime {
fn from(wd: ISOWeekDate) -> BrokenDownTime {
BrokenDownTime {
iso_week_year: Some(wd.year_ranged()),
iso_week: Some(wd.week_ranged()),
weekday: Some(wd.weekday()),
..BrokenDownTime::default()
}
}
}
impl From<Time> for BrokenDownTime {
fn from(t: Time) -> BrokenDownTime {
BrokenDownTime {
hour: Some(t.hour_ranged()),
minute: Some(t.minute_ranged()),
second: Some(t.second_ranged()),
subsec: Some(t.subsec_nanosecond_ranged()),
meridiem: Some(Meridiem::from(t)),
..BrokenDownTime::default()
}
}
}
pub struct Display<'f> {
pub(crate) fmt: &'f [u8],
pub(crate) tm: BrokenDownTime,
}
impl<'f> core::fmt::Display for Display<'f> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use crate::fmt::StdFmtWrite;
self.tm.format(self.fmt, StdFmtWrite(f)).map_err(|_| core::fmt::Error)
}
}
impl<'f> core::fmt::Debug for Display<'f> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("Display")
.field("fmt", &escape::Bytes(self.fmt))
.field("tm", &self.tm)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Meridiem {
AM,
PM,
}
impl From<Time> for Meridiem {
fn from(t: Time) -> Meridiem {
if t.hour() < 12 {
Meridiem::AM
} else {
Meridiem::PM
}
}
}
#[derive(Clone, Debug)]
pub struct Extension {
flag: Option<Flag>,
width: Option<u8>,
colons: u8,
}
impl Extension {
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_flag<'i>(
fmt: &'i [u8],
) -> Result<(Option<Flag>, &'i [u8]), Error> {
let byte = fmt[0];
let flag = match byte {
b'_' => Flag::PadSpace,
b'0' => Flag::PadZero,
b'-' => Flag::NoPad,
b'^' => Flag::Uppercase,
b'#' => Flag::Swapcase,
_ => return Ok((None, fmt)),
};
let fmt = &fmt[1..];
if fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after flag \
{byte:?}, but found end of format string",
byte = escape::Byte(byte),
));
}
Ok((Some(flag), fmt))
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_width<'i>(
fmt: &'i [u8],
) -> Result<(Option<u8>, &'i [u8]), Error> {
let mut digits = 0;
while digits < fmt.len() && fmt[digits].is_ascii_digit() {
digits += 1;
}
if digits == 0 {
return Ok((None, fmt));
}
let (digits, fmt) = util::parse::split(fmt, digits).unwrap();
let width = util::parse::i64(digits)
.context("failed to parse conversion specifier width")?;
let width = u8::try_from(width).map_err(|_| {
err!("{width} is too big, max is {max}", max = u8::MAX)
})?;
if fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after width \
{width}, but found end of format string",
));
}
Ok((Some(width), fmt))
}
#[cfg_attr(feature = "perf-inline", inline(always))]
fn parse_colons<'i>(fmt: &'i [u8]) -> Result<(u8, &'i [u8]), Error> {
let mut colons = 0;
while colons < 3 && colons < fmt.len() && fmt[colons] == b':' {
colons += 1;
}
let fmt = &fmt[usize::from(colons)..];
if colons > 0 && fmt.is_empty() {
return Err(err!(
"expected to find specifier directive after {colons} colons, \
but found end of format string",
));
}
Ok((u8::try_from(colons).unwrap(), fmt))
}
}
#[derive(Clone, Copy, Debug)]
enum Flag {
PadSpace,
PadZero,
NoPad,
Uppercase,
Swapcase,
}
fn weekday_name_full(wd: Weekday) -> &'static str {
match wd {
Weekday::Sunday => "Sunday",
Weekday::Monday => "Monday",
Weekday::Tuesday => "Tuesday",
Weekday::Wednesday => "Wednesday",
Weekday::Thursday => "Thursday",
Weekday::Friday => "Friday",
Weekday::Saturday => "Saturday",
}
}
fn weekday_name_abbrev(wd: Weekday) -> &'static str {
match wd {
Weekday::Sunday => "Sun",
Weekday::Monday => "Mon",
Weekday::Tuesday => "Tue",
Weekday::Wednesday => "Wed",
Weekday::Thursday => "Thu",
Weekday::Friday => "Fri",
Weekday::Saturday => "Sat",
}
}
fn month_name_full(month: t::Month) -> &'static str {
match month.get() {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
unk => unreachable!("invalid month {unk}"),
}
}
fn month_name_abbrev(month: t::Month) -> &'static str {
match month.get() {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
unk => unreachable!("invalid month {unk}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_non_delimited() {
insta::assert_snapshot!(
Timestamp::strptime("%Y%m%d-%H%M%S%z", "20240730-005625+0400").unwrap(),
@"2024-07-29T20:56:25Z",
);
insta::assert_snapshot!(
Zoned::strptime("%Y%m%d-%H%M%S%z", "20240730-005625+0400").unwrap(),
@"2024-07-30T00:56:25+04:00[+04:00]",
);
}
#[test]
fn ok_non_ascii() {
let fmt = "%Y年%m月%d日,%H时%M分%S秒";
let dt = crate::civil::date(2022, 2, 4).at(3, 58, 59, 0);
insta::assert_snapshot!(
dt.strftime(fmt),
@"2022年02月04日,03时58分59秒",
);
insta::assert_debug_snapshot!(
DateTime::strptime(fmt, "2022年02月04日,03时58分59秒").unwrap(),
@"2022-02-04T03:58:59",
);
}
}