Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ regex = "1.10.4"
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
winnow = "0.7.10"
num-traits = "0.2.19"
jiff = { version = "0.2.15", default-features = false, features = ["std"] }

[dev-dependencies]
rstest = "0.26"
4 changes: 2 additions & 2 deletions src/items/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ impl DateTimeBuilder {
if let Some(date::Date { year, month, day }) = self.date {
dt = new_date(
year.map(|x| x as i32).unwrap_or(dt.year()),
month,
day,
month as u32,
day as u32,
dt.hour(),
dt.minute(),
dt.second(),
Expand Down
69 changes: 40 additions & 29 deletions src/items/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,21 @@ use super::{
};

#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct Date {
pub day: u32,
pub month: u32,
pub year: Option<u32>,
pub(crate) struct Date {
pub(crate) day: u8,
pub(crate) month: u8,
pub(crate) year: Option<u16>,
}

impl TryFrom<(&str, u32, u32)> for Date {
impl TryFrom<(&str, u8, u8)> for Date {
type Error = &'static str;

/// Create a `Date` from a tuple of `(year, month, day)`.
///
/// Note: The `year` is represented as a `&str` to handle a specific GNU
/// compatibility quirk. See the comment in [`year`](super::year) for more
/// details.
fn try_from(value: (&str, u32, u32)) -> Result<Self, Self::Error> {
fn try_from(value: (&str, u8, u8)) -> Result<Self, Self::Error> {
let (year_str, month, day) = value;
let year = year_from_str(year_str)?;

Expand All @@ -80,11 +80,11 @@ impl TryFrom<(&str, u32, u32)> for Date {
}
}

impl TryFrom<(u32, u32)> for Date {
impl TryFrom<(u8, u8)> for Date {
type Error = &'static str;

/// Create a `Date` from a tuple of `(month, day)`.
fn try_from((month, day): (u32, u32)) -> Result<Self, Self::Error> {
fn try_from((month, day): (u8, u8)) -> Result<Self, Self::Error> {
if !(1..=12).contains(&month) {
return Err("month must be between 1 and 12");
}
Expand All @@ -104,14 +104,27 @@ impl TryFrom<(u32, u32)> for Date {
}
}

pub fn parse(input: &mut &str) -> ModalResult<Date> {
impl TryFrom<Date> for jiff::civil::Date {
type Error = &'static str;

fn try_from(date: Date) -> Result<Self, Self::Error> {
jiff::civil::Date::new(
date.year.unwrap_or(0) as i16,
date.month as i8,
date.day as i8,
)
.map_err(|_| "date is not valid")
}
}

pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
alt((iso1, iso2, us, literal1, literal2)).parse_next(input)
}

/// Parse `[year]-[month]-[day]`
///
/// This is also used by [`combined`](super::combined).
pub fn iso1(input: &mut &str) -> ModalResult<Date> {
pub(super) fn iso1(input: &mut &str) -> ModalResult<Date> {
let (year, _, month, _, day) =
(year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).parse_next(input)?;

Expand All @@ -123,19 +136,13 @@ pub fn iso1(input: &mut &str) -> ModalResult<Date> {
/// Parse `[year][month][day]`
///
/// This is also used by [`combined`](super::combined).
pub fn iso2(input: &mut &str) -> ModalResult<Date> {
pub(super) fn iso2(input: &mut &str) -> ModalResult<Date> {
let date_str = take_while(5.., AsChar::is_dec_digit).parse_next(input)?;
let len = date_str.len();

let year = &date_str[..len - 4];

let month = date_str[len - 4..len - 2]
.parse::<u32>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;

let day = date_str[len - 2..]
.parse::<u32>()
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid number")))?;
let month = month_from_str(&date_str[len - 4..len - 2])?;
let day = day_from_str(&date_str[len - 2..])?;

(year, month, day)
.try_into()
Expand All @@ -158,27 +165,21 @@ fn us(input: &mut &str) -> ModalResult<Date> {
//
// GNU quirk: interpret as [year]/[month]/[day] if the first part is at
// least 4 characters long.
let day = s2
.parse::<u32>()
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid number")))?;
let day = day_from_str(s2)?;
(s1, n, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
}
Some(s2) => {
// [month]/[day]/[year]
let month = s1
.parse::<u32>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;
let month = month_from_str(s1)?;
(s2, month, n)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
}
None => {
// [month]/[day]
let month = s1
.parse::<u32>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;
let month = month_from_str(s1)?;
(month, n).try_into().map_err(|e| ErrMode::Cut(ctx_err(e)))
}
}
Expand Down Expand Up @@ -239,7 +240,7 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
}

/// Parse the name of a month (case-insensitive)
fn literal_month(input: &mut &str) -> ModalResult<u32> {
fn literal_month(input: &mut &str) -> ModalResult<u8> {
s(alpha1)
.verify_map(|s: &str| {
Some(match s {
Expand All @@ -261,6 +262,16 @@ fn literal_month(input: &mut &str) -> ModalResult<u32> {
.parse_next(input)
}

fn month_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid u8 number")))
}

fn day_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid u8 number")))
}

#[cfg(test)]
mod tests {
use super::{parse, Date};
Expand Down
22 changes: 11 additions & 11 deletions src/items/year.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
use super::primitive::s;

// TODO: Leverage `TryFrom` trait.
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
let mut year = year_str
.parse::<u32>()
.map_err(|_| "year must be a valid number")?;
.parse::<u16>()
.map_err(|_| "year must be a valid u16 number")?;

// If year is 68 or smaller, then 2000 is added to it; otherwise, if year
// is less than 100, then 1900 is added to it.
Expand Down Expand Up @@ -57,16 +57,16 @@ mod tests {
#[test]
fn test_year() {
// 2-characters are converted to 19XX/20XX
assert_eq!(year_from_str("10").unwrap(), 2010u32);
assert_eq!(year_from_str("68").unwrap(), 2068u32);
assert_eq!(year_from_str("69").unwrap(), 1969u32);
assert_eq!(year_from_str("99").unwrap(), 1999u32);
assert_eq!(year_from_str("10").unwrap(), 2010u16);
assert_eq!(year_from_str("68").unwrap(), 2068u16);
assert_eq!(year_from_str("69").unwrap(), 1969u16);
assert_eq!(year_from_str("99").unwrap(), 1999u16);

// 3,4-characters are converted verbatim
assert_eq!(year_from_str("468").unwrap(), 468u32);
assert_eq!(year_from_str("469").unwrap(), 469u32);
assert_eq!(year_from_str("1568").unwrap(), 1568u32);
assert_eq!(year_from_str("1569").unwrap(), 1569u32);
assert_eq!(year_from_str("468").unwrap(), 468u16);
assert_eq!(year_from_str("469").unwrap(), 469u16);
assert_eq!(year_from_str("1568").unwrap(), 1568u16);
assert_eq!(year_from_str("1569").unwrap(), 1569u16);

// years greater than 9999 are not accepted
assert!(year_from_str("10000").is_err());
Expand Down
Loading