You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
feat: implement pure number parsing
Implement GNU-compatible pure number interpretation that treats pure number as year or time based on the current parsing context.
This commit is contained in:
+34
-19
@@ -3,7 +3,7 @@
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
use super::{date, relative, time, weekday};
|
||||
use super::{date, relative, time, weekday, year};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
@@ -40,24 +40,6 @@ impl DateTimeBuilder {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_some() {
|
||||
Err("year cannot appear more than once")
|
||||
} else {
|
||||
date.year = Some(year);
|
||||
Ok(self)
|
||||
}
|
||||
} else {
|
||||
self.date = Some(date::Date {
|
||||
day: 1,
|
||||
month: 1,
|
||||
year: Some(year),
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
|
||||
if self.date.is_some() || self.timestamp.is_some() {
|
||||
Err("date cannot appear more than once")
|
||||
@@ -103,6 +85,39 @@ impl DateTimeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a pure number that can be interpreted as either a year or time
|
||||
/// depending on the current state of the builder.
|
||||
///
|
||||
/// If a date is already set but lacks a year, the number is interpreted as
|
||||
/// a year. Otherwise, it's interpreted as a time in HHMM, HMM, HH, or H
|
||||
/// format.
|
||||
pub(super) fn set_pure(mut self, pure: String) -> Result<Self, &'static str> {
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_none() {
|
||||
date.year = Some(year::year_from_str(&pure)?);
|
||||
return Ok(self);
|
||||
}
|
||||
}
|
||||
|
||||
let (mut hour_str, mut minute_str) = match pure.len() {
|
||||
1..=2 => (pure.as_str(), "0"),
|
||||
3..=4 => pure.split_at(pure.len() - 2),
|
||||
_ => {
|
||||
return Err("pure number must be 1-4 digits when interpreted as time");
|
||||
}
|
||||
};
|
||||
|
||||
let hour = time::hour24(&mut hour_str).map_err(|_| "invalid hour in pure number")?;
|
||||
let minute = time::minute(&mut minute_str).map_err(|_| "invalid minute in pure number")?;
|
||||
|
||||
let time = time::Time {
|
||||
hour,
|
||||
minute,
|
||||
..Default::default()
|
||||
};
|
||||
self.set_time(time)
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
|
||||
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
|
||||
|
||||
|
||||
+77
-14
@@ -33,6 +33,7 @@
|
||||
mod combined;
|
||||
mod date;
|
||||
mod epoch;
|
||||
mod pure;
|
||||
mod relative;
|
||||
mod time;
|
||||
mod timezone;
|
||||
@@ -59,13 +60,13 @@ use crate::ParseDateTimeError;
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum Item {
|
||||
Timestamp(f64),
|
||||
Year(u32),
|
||||
DateTime(combined::DateTime),
|
||||
Date(date::Date),
|
||||
Time(time::Time),
|
||||
Weekday(weekday::Weekday),
|
||||
Relative(relative::Relative),
|
||||
TimeZone(time::Offset),
|
||||
Pure(String),
|
||||
}
|
||||
|
||||
/// Build a `DateTime<FixedOffset>` from a `DateTimeBuilder` and a base date.
|
||||
@@ -97,7 +98,7 @@ pub(crate) fn at_local(
|
||||
/// timestamp = "@" , float ;
|
||||
///
|
||||
/// items = item , { item } ;
|
||||
/// item = datetime | date | time | relative | weekday | timezone | year ;
|
||||
/// item = datetime | date | time | relative | weekday | timezone | pure ;
|
||||
///
|
||||
/// datetime = date , [ "t" | whitespace ] , iso_time ;
|
||||
///
|
||||
@@ -178,6 +179,8 @@ pub(crate) fn at_local(
|
||||
///
|
||||
/// timezone = named_zone , [ time_offset ] ;
|
||||
///
|
||||
/// pure = { digit }
|
||||
///
|
||||
/// optional_whitespace = { whitespace } ;
|
||||
/// ```
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
@@ -217,9 +220,6 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
.set_timestamp(ts)
|
||||
.map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Year(year) => {
|
||||
builder = builder.set_year(year).map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::DateTime(dt) => {
|
||||
builder = builder
|
||||
.set_date(dt.date)
|
||||
@@ -246,6 +246,9 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
Item::Relative(rel) => {
|
||||
builder = builder.push_relative(rel);
|
||||
}
|
||||
Item::Pure(pure) => {
|
||||
builder = builder.set_pure(pure).map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
},
|
||||
Err(ErrMode::Backtrack(_)) => break,
|
||||
Err(e) => return Err(e),
|
||||
@@ -271,7 +274,7 @@ fn parse_item(input: &mut &str) -> ModalResult<Item> {
|
||||
relative::parse.map(Item::Relative),
|
||||
weekday::parse.map(Item::Weekday),
|
||||
timezone::parse.map(Item::TimeZone),
|
||||
year::parse.map(Item::Year),
|
||||
pure::parse.map(Item::Pure),
|
||||
)),
|
||||
)
|
||||
.parse_next(input)
|
||||
@@ -290,7 +293,8 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
|
||||
mod tests {
|
||||
use super::{at_date, parse, DateTimeBuilder};
|
||||
use chrono::{
|
||||
DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
|
||||
DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike,
|
||||
Utc,
|
||||
};
|
||||
|
||||
fn at_utc(builder: DateTimeBuilder) -> DateTime<FixedOffset> {
|
||||
@@ -386,13 +390,6 @@ mod tests {
|
||||
.to_string()
|
||||
.contains("time cannot appear more than once"));
|
||||
|
||||
let result = parse(&mut "2025-05-19 2024");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("year cannot appear more than once"));
|
||||
|
||||
let result = parse(&mut "2025-05-19 +00:00 +01:00");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
@@ -415,6 +412,46 @@ mod tests {
|
||||
let result = parse(&mut "2025-05-19 @1690466034");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
|
||||
// Pure number as year (too large).
|
||||
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"));
|
||||
|
||||
// Pure number as time (too long).
|
||||
let result = parse(&mut "01:02 12345");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("pure number must be 1-4 digits when interpreted as time"));
|
||||
|
||||
// Pure number as time (repeated time).
|
||||
let result = parse(&mut "01:02 1234");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("time cannot appear more than once"));
|
||||
|
||||
// Pure number as time (invalid hour).
|
||||
let result = parse(&mut "jul 18 2025 2400");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid hour in pure number"));
|
||||
|
||||
// Pure number as time (invalid minute).
|
||||
let result = parse(&mut "jul 18 2025 2360");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid minute in pure number"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -498,4 +535,30 @@ mod tests {
|
||||
assert_eq!(result.minute(), 0);
|
||||
assert_eq!(result.second(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure() {
|
||||
let now = Utc::now().fixed_offset();
|
||||
|
||||
// Pure number as year.
|
||||
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now).unwrap();
|
||||
assert_eq!(result.year(), 2025);
|
||||
|
||||
// Pure number as time.
|
||||
let result = at_date(parse(&mut "1230").unwrap(), now).unwrap();
|
||||
assert_eq!(result.hour(), 12);
|
||||
assert_eq!(result.minute(), 30);
|
||||
|
||||
let result = at_date(parse(&mut "123").unwrap(), now).unwrap();
|
||||
assert_eq!(result.hour(), 1);
|
||||
assert_eq!(result.minute(), 23);
|
||||
|
||||
let result = at_date(parse(&mut "12").unwrap(), now).unwrap();
|
||||
assert_eq!(result.hour(), 12);
|
||||
assert_eq!(result.minute(), 0);
|
||||
|
||||
let result = at_date(parse(&mut "1").unwrap(), now).unwrap();
|
||||
assert_eq!(result.hour(), 1);
|
||||
assert_eq!(result.minute(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a pure number string.
|
||||
//!
|
||||
//! The GNU docs say:
|
||||
//!
|
||||
//! > The precise interpretation of a pure decimal number depends on the
|
||||
//! > context in the date string.
|
||||
//! >
|
||||
//! > If the decimal number is of the form YYYYMMDD and no other calendar
|
||||
//! > date item (*note Calendar date items::) appears before it in the date
|
||||
//! > string, then YYYY is read as the year, MM as the month number and DD as
|
||||
//! > the day of the month, for the specified calendar date.
|
||||
//! >
|
||||
//! > If the decimal number is of the form HHMM and no other time of day
|
||||
//! > item appears before it in the date string, then HH is read as the hour
|
||||
//! > of the day and MM as the minute of the hour, for the specified time of
|
||||
//! > day. MM can also be omitted.
|
||||
//! >
|
||||
//! > If both a calendar date and a time of day appear to the left of a
|
||||
//! > number in the date string, but no relative item, then the number
|
||||
//! > overrides the year.
|
||||
|
||||
use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
|
||||
use super::primitive::s;
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<String> {
|
||||
s(take_while(1.., AsChar::is_dec_digit))
|
||||
.map(|s: &str| s.to_owned())
|
||||
.parse_next(input)
|
||||
}
|
||||
+2
-2
@@ -214,7 +214,7 @@ fn colon(input: &mut &str) -> ModalResult<()> {
|
||||
}
|
||||
|
||||
/// Parse a number of hours in `0..24`(preceded by whitespace)
|
||||
fn hour24(input: &mut &str) -> ModalResult<u32> {
|
||||
pub(super) fn hour24(input: &mut &str) -> ModalResult<u32> {
|
||||
s(dec_uint).verify(|x| *x < 24).parse_next(input)
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ fn hour12(input: &mut &str) -> ModalResult<u32> {
|
||||
}
|
||||
|
||||
/// Parse a number of minutes (preceded by whitespace)
|
||||
fn minute(input: &mut &str) -> ModalResult<u32> {
|
||||
pub(super) fn minute(input: &mut &str) -> ModalResult<u32> {
|
||||
s(dec_uint).verify(|x| *x < 60).parse_next(input)
|
||||
}
|
||||
|
||||
|
||||
+12
-16
@@ -10,13 +10,9 @@
|
||||
//! strings. For example, `"00"` is interpreted as `2000`, whereas `"0"`,
|
||||
//! `"000"`, or `"0000"` are interpreted as `0`.
|
||||
|
||||
use winnow::{error::ErrMode, stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
|
||||
use super::primitive::{ctx_err, s};
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<u32> {
|
||||
year_from_str(year_str(input)?).map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
use super::primitive::s;
|
||||
|
||||
// TODO: Leverage `TryFrom` trait.
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
|
||||
@@ -56,23 +52,23 @@ pub(super) fn year_str<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
use super::year_from_str;
|
||||
|
||||
#[test]
|
||||
fn test_year() {
|
||||
// 2-characters are converted to 19XX/20XX
|
||||
assert_eq!(parse(&mut "10").unwrap(), 2010u32);
|
||||
assert_eq!(parse(&mut "68").unwrap(), 2068u32);
|
||||
assert_eq!(parse(&mut "69").unwrap(), 1969u32);
|
||||
assert_eq!(parse(&mut "99").unwrap(), 1999u32);
|
||||
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!(parse(&mut "468").unwrap(), 468u32);
|
||||
assert_eq!(parse(&mut "469").unwrap(), 469u32);
|
||||
assert_eq!(parse(&mut "1568").unwrap(), 1568u32);
|
||||
assert_eq!(parse(&mut "1569").unwrap(), 1569u32);
|
||||
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!(parse(&mut "10000").is_err());
|
||||
assert!(year_from_str("10000").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user