Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion src/items/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ impl DateTimeBuilder {
let d: civil::Date = if date.year.is_some() {
date.try_into()?
} else {
date.with_year(dt.date().year() as u16).try_into()?
let base_year = u32::try_from(dt.date().year())
.map_err(|_| "base year must be non-negative")?;
date.with_year(base_year).try_into()?
};
dt = dt.with().date(d).build()?;
}
Expand Down
16 changes: 8 additions & 8 deletions src/items/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ use super::{
pub(crate) struct Date {
pub(crate) day: u8,
pub(crate) month: u8,
pub(crate) year: Option<u16>,
pub(crate) year: Option<u32>,
}

impl Date {
pub(super) fn with_year(self, year: u16) -> Self {
pub(super) fn with_year(self, year: u32) -> Self {
Date {
day: self.day,
month: self.month,
Expand Down Expand Up @@ -118,12 +118,12 @@ 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")
let year = date.year.unwrap_or(0);
let year: i16 = year
.try_into()
.map_err(|_| "date year is outside the supported range")?;
jiff::civil::Date::new(year, date.month as i8, date.day as i8)
.map_err(|_| "date is not valid")
}
}

Expand Down
22 changes: 16 additions & 6 deletions src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,9 @@ mod tests {
let result = parse(&mut "2025-05-19 @1690466034");
assert!(result.is_err());

// Pure number as year (too large).
// Pure number as year (large years are parsed successfully).
let result = parse(&mut "jul 18 12:30 10000");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("year must be no greater than 9999"));
assert!(result.is_ok());

// Pure number as time (too long).
let result = parse(&mut "01:02 12345");
Expand Down Expand Up @@ -449,6 +445,20 @@ mod tests {
.contains("invalid minute in pure number"));
}

#[test]
fn negative_base_year_with_yearless_date_errors() {
let base = DateTime::new(-1, 1, 1, 0, 0, 0, 0)
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let result = parse_at_date(base, "11/14");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("base year must be non-negative"));
}

#[test]
fn relative_weekday() {
// Jan 1 2025 is a Wed
Expand Down
51 changes: 31 additions & 20 deletions src/items/year.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};

use super::primitive::s;

const GNU_MAX_YEAR: u32 = 2_147_485_547;
const GNU_MAX_YEAR_ERROR: &str = "year exceeds GNU maximum";

Comment thread
abhishekpradhan marked this conversation as resolved.
Outdated
// TODO: Leverage `TryFrom` trait.
pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
let mut year = year_str
.parse::<u16>()
.map_err(|_| "year must be a valid u16 number")?;
.parse::<u32>()
.map_err(|_| "year must be a non-negative integer")?;

// 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 All @@ -34,13 +37,8 @@ pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
}
}

// 2147485547 is the maximum value accepted by GNU, but chrono only
// behaves like GNU for years in the range: [0, 9999], so we keep in the
// range [0, 9999].
//
// See discussion in https://github.com/uutils/parse_datetime/issues/160.
if year > 9999 {
return Err("year must be no greater than 9999");
if year > GNU_MAX_YEAR {
return Err(GNU_MAX_YEAR_ERROR);
Comment thread
abhishekpradhan marked this conversation as resolved.
Outdated
}

Ok(year)
Expand All @@ -57,18 +55,31 @@ mod tests {
#[test]
fn test_year() {
// 2-characters are converted to 19XX/20XX
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);
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);

// 3,4-characters are converted verbatim
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);
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);

// years greater than 9999 are not accepted
assert!(year_from_str("10000").is_err());
// very large years are accepted up to GNU's upper bound
assert_eq!(year_from_str("10000").unwrap(), 10000u32);
assert_eq!(year_from_str("2147485547").unwrap(), 2_147_485_547u32);
assert_eq!(
year_from_str("2147485548").unwrap_err(),
"year exceeds GNU maximum"
);
}

#[test]
fn test_year_errors() {
assert_eq!(
year_from_str("not-a-year").unwrap_err(),
"year must be a non-negative integer"
);
}
}