use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{err, ErrorContext},
fmt::{
strtime::{format::Formatter, parse::Parser},
Write,
},
tz::{Offset, OffsetConflict, TimeZone, TimeZoneDatabase},
util::{
self,
array_str::Abbreviation,
escape,
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 mut buf = alloc::string::String::new();
broken_down_time.format(format, &mut buf)?;
Ok(buf)
}
#[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>,
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>,
tzabbrev: Option<Abbreviation>,
#[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> {
let fmt = format.as_ref();
let mut formatter = Formatter { fmt, tm: self, wtr: &mut 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 mut buf = alloc::string::String::new();
self.format(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> {
let dt = self
.to_datetime()
.context("datetime required to parse zoned datetime")?;
match (self.offset, self.iana_time_zone()) {
(None, None) => Err(err!(
"either offset (from %z) or IANA time zone identifier \
(from %Q) is required for parsing zoned datetime",
)),
(Some(offset), None) => {
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",
)
})?;
Ok(ts.to_zoned(TimeZone::fixed(offset)))
}
(None, Some(iana)) => {
let tz = db.get(iana)?;
let zdt = tz.to_zoned(dt)?;
Ok(zdt)
}
(Some(offset), Some(iana)) => {
let tz = db.get(iana)?;
let azdt = OffsetConflict::Reject.resolve(dt, offset, tz)?;
let zdt = azdt.unambiguous().unwrap();
Ok(zdt)
}
}
}
#[inline]
pub fn to_timestamp(&self) -> Result<Timestamp, Error> {
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 {
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_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), C(1)).unwrap();
first
.with()
.day_of_year(doy.get())
.build()
.context("invalid 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), C(1)).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), C(1)).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 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 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_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;
}
}
impl<'a> From<&'a Zoned> for BrokenDownTime {
fn from(zdt: &'a Zoned) -> BrokenDownTime {
let (_, _, tzabbrev) = zdt.time_zone().to_offset(zdt.timestamp());
#[cfg(feature = "alloc")]
let iana = {
use alloc::string::ToString;
zdt.time_zone().iana_name().map(|s| s.to_string())
};
BrokenDownTime {
offset: Some(zdt.offset()),
tzabbrev: Abbreviation::new(tzabbrev),
#[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),
..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<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, Copy, Debug)]
struct Extension {
flag: Option<Flag>,
width: Option<u8>,
}
impl Extension {
#[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))
}
#[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))
}
}
#[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",
);
}
}