use crate::error::DateDurationParseError;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
#[allow(clippy::exhaustive_structs)] pub struct DateDuration {
pub is_negative: bool,
pub years: u32,
pub months: u32,
pub weeks: u32,
pub days: u32,
}
impl DateDuration {
pub fn try_from_str(s: &str) -> Result<Self, DateDurationParseError> {
Self::try_from_utf8(s.as_bytes())
}
pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, DateDurationParseError> {
let mut s = code_units;
let mut is_negative = false;
match s {
[b'-', rest @ ..] => {
is_negative = true;
s = rest;
}
[b'+', ..] => return Err(DateDurationParseError::PlusNotAllowed),
_ => {}
}
match s {
[b'P', rest @ ..] => s = rest,
_ => return Err(DateDurationParseError::InvalidStructure),
}
if s.is_empty() {
return Err(DateDurationParseError::InvalidStructure);
}
let mut years: u32 = 0;
let mut months: u32 = 0;
let mut weeks: u32 = 0;
let mut days: u32 = 0;
let mut seen_years = false;
let mut seen_months = false;
let mut seen_weeks = false;
let mut seen_days = false;
while !s.is_empty() {
if matches!(s, [b'T', ..]) {
return Err(DateDurationParseError::TimeNotSupported);
}
let mut value: u64 = 0;
let mut has_digits = false;
while let [b @ b'0'..=b'9', rest @ ..] = s {
value = value
.checked_mul(10)
.and_then(|v| v.checked_add((b - b'0') as u64))
.ok_or(DateDurationParseError::NumberOverflow)?;
s = rest;
has_digits = true;
}
if !has_digits {
return Err(DateDurationParseError::MissingValue);
}
match s {
[b'Y', rest @ ..] => {
if seen_years {
return Err(DateDurationParseError::DuplicateUnit);
}
years =
u32::try_from(value).map_err(|_| DateDurationParseError::NumberOverflow)?;
seen_years = true;
s = rest;
}
[b'M', rest @ ..] => {
if seen_months {
return Err(DateDurationParseError::DuplicateUnit);
}
months =
u32::try_from(value).map_err(|_| DateDurationParseError::NumberOverflow)?;
seen_months = true;
s = rest;
}
[b'W', rest @ ..] => {
if seen_weeks {
return Err(DateDurationParseError::DuplicateUnit);
}
weeks =
u32::try_from(value).map_err(|_| DateDurationParseError::NumberOverflow)?;
seen_weeks = true;
s = rest;
}
[b'D', rest @ ..] => {
if seen_days {
return Err(DateDurationParseError::DuplicateUnit);
}
days =
u32::try_from(value).map_err(|_| DateDurationParseError::NumberOverflow)?;
seen_days = true;
s = rest;
}
_ => return Err(DateDurationParseError::InvalidStructure),
}
}
Ok(Self {
is_negative,
years,
months,
weeks,
days,
})
}
pub fn for_years(years: i32) -> Self {
Self {
is_negative: years.is_negative(),
years: years.unsigned_abs(),
..Default::default()
}
}
pub fn for_months(months: i32) -> Self {
Self {
is_negative: months.is_negative(),
months: months.unsigned_abs(),
..Default::default()
}
}
pub fn for_weeks(weeks: i32) -> Self {
Self {
is_negative: weeks.is_negative(),
weeks: weeks.unsigned_abs(),
..Default::default()
}
}
pub fn for_days(days: i32) -> Self {
Self {
is_negative: days.is_negative(),
days: days.unsigned_abs(),
..Default::default()
}
}
pub(crate) fn for_weeks_and_days(days: i32) -> Self {
let weeks = days / 7;
let days = days % 7;
Self::from_signed_ymwd(0, 0, weeks, days)
}
pub(crate) fn from_signed_ymwd(years: i32, months: i32, weeks: i32, days: i32) -> Self {
let is_negative = years.is_negative()
|| months.is_negative()
|| weeks.is_negative()
|| days.is_negative();
if is_negative
&& (years.is_positive()
|| months.is_positive()
|| weeks.is_positive()
|| days.is_positive())
{
debug_assert!(false, "mixed signs in from_signed_ymd");
}
Self {
is_negative,
years: years.unsigned_abs(),
months: months.unsigned_abs(),
weeks: weeks.unsigned_abs(),
days: days.unsigned_abs(),
}
}
#[inline]
pub(crate) fn add_years_to(&self, year: i32) -> i32 {
if !self.is_negative {
match year.checked_add_unsigned(self.years) {
Some(x) => x,
None => {
debug_assert!(false, "{year} + {self:?} out of year range");
i32::MAX
}
}
} else {
match year.checked_sub_unsigned(self.years) {
Some(x) => x,
None => {
debug_assert!(false, "{year} - {self:?} out of year range");
i32::MIN
}
}
}
}
#[inline]
pub(crate) fn add_months_to(&self, month: u8) -> i32 {
debug_assert!(i32::try_from(self.months).is_ok());
if !self.is_negative {
i32::from(month) + (self.months as i32)
} else {
i32::from(month) - (self.months as i32)
}
}
#[inline]
pub(crate) fn add_weeks_and_days_to(&self, day: u8) -> i32 {
debug_assert!(i32::try_from(self.weeks).is_ok());
if !self.is_negative {
let day = i32::from(day) + (self.weeks as i32) * 7;
match day.checked_add_unsigned(self.days) {
Some(x) => x,
None => {
debug_assert!(false, "{day} + {self:?} out of day range");
i32::MAX
}
}
} else {
let day = i32::from(day) - (self.weeks as i32) * 7;
match day.checked_sub_unsigned(self.days) {
Some(x) => x,
None => {
debug_assert!(false, "{day} - {self:?} out of day range");
i32::MIN
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_unit_durations() {
let d = DateDuration::try_from_str("P1D").unwrap();
assert_eq!(
d,
DateDuration {
days: 1,
..Default::default()
}
);
let d = DateDuration::try_from_str("P3W").unwrap();
assert_eq!(
d,
DateDuration {
weeks: 3,
..Default::default()
}
);
let d = DateDuration::try_from_str("P5M").unwrap();
assert_eq!(
d,
DateDuration {
months: 5,
..Default::default()
}
);
let d = DateDuration::try_from_str("P7Y").unwrap();
assert_eq!(
d,
DateDuration {
years: 7,
..Default::default()
}
);
}
#[test]
fn parse_multi_unit_durations() {
let d = DateDuration::try_from_str("P1Y3M5W7D").unwrap();
assert_eq!(
d,
DateDuration {
years: 1,
months: 3,
weeks: 5,
days: 7,
..Default::default()
}
);
}
#[test]
fn parse_negative_durations() {
let d = DateDuration::try_from_str("-P9W").unwrap();
assert_eq!(
d,
DateDuration {
is_negative: true,
weeks: 9,
..Default::default()
}
);
}
}