You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Fixes Negative numbers for @ not being recognized (#43)
* Fixes Negative numbers for @ not being recognized This commit resolves #40. Adds new file and functions parse_timestamp. Adds tests for handling negative numbers.
This commit is contained in:
Generated
+17
@@ -118,6 +118,22 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
@@ -138,6 +154,7 @@ name = "parse_datetime"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
"regex",
|
||||
]
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@ readme = "README.md"
|
||||
[dependencies]
|
||||
regex = "1.9"
|
||||
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
|
||||
nom = "7.1.3"
|
||||
|
||||
+12
-4
@@ -14,10 +14,12 @@ use std::fmt::{self, Display};
|
||||
|
||||
// Expose parse_datetime
|
||||
mod parse_relative_time;
|
||||
mod parse_timestamp;
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone};
|
||||
|
||||
use parse_relative_time::parse_relative_time;
|
||||
use parse_timestamp::parse_timestamp;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseDateTimeError {
|
||||
@@ -169,9 +171,9 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
|
||||
}
|
||||
|
||||
// Parse epoch seconds
|
||||
if s.as_ref().bytes().next() == Some(b'@') {
|
||||
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") {
|
||||
if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
|
||||
if let Ok(timestamp) = parse_timestamp(s.as_ref()) {
|
||||
if let Some(timestamp_date) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
|
||||
if let Ok(dt) = naive_dt_to_fixed_offset(date, timestamp_date) {
|
||||
return Ok(dt);
|
||||
}
|
||||
}
|
||||
@@ -359,15 +361,21 @@ mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
#[test]
|
||||
fn test_positive_offsets() {
|
||||
fn test_positive_and_negative_offsets() {
|
||||
let offsets: Vec<i64> = vec![
|
||||
0, 1, 2, 10, 100, 150, 2000, 1234400000, 1334400000, 1692582913, 2092582910,
|
||||
];
|
||||
|
||||
for offset in offsets {
|
||||
// positive offset
|
||||
let time = Utc.timestamp_opt(offset, 0).unwrap();
|
||||
let dt = parse_datetime(format!("@{}", offset));
|
||||
assert_eq!(dt.unwrap(), time);
|
||||
|
||||
// negative offset
|
||||
let time = Utc.timestamp_opt(-offset, 0).unwrap();
|
||||
let dt = parse_datetime(format!("@-{}", offset));
|
||||
assert_eq!(dt.unwrap(), time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use crate::ParseDateTimeError;
|
||||
use chrono::{Duration, Local, NaiveDate, Utc};
|
||||
use regex::Regex;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use core::fmt;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use nom::branch::alt;
|
||||
use nom::character::complete::{char, digit1};
|
||||
use nom::combinator::all_consuming;
|
||||
use nom::multi::fold_many0;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::tuple;
|
||||
use nom::{self, IResult};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseTimestampError {
|
||||
InvalidNumber(ParseIntError),
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Display for ParseTimestampError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidInput => {
|
||||
write!(f, "Invalid input string: cannot be parsed as a timestamp")
|
||||
}
|
||||
Self::InvalidNumber(err) => {
|
||||
write!(f, "Invalid timestamp number: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseTimestampError {}
|
||||
|
||||
// TODO is this necessary
|
||||
impl From<ParseIntError> for ParseTimestampError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
Self::InvalidNumber(err)
|
||||
}
|
||||
}
|
||||
|
||||
type NomError<'a> = nom::Err<nom::error::Error<&'a str>>;
|
||||
|
||||
impl<'a> From<NomError<'a>> for ParseTimestampError {
|
||||
fn from(_err: NomError<'a>) -> Self {
|
||||
Self::InvalidInput
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_timestamp(s: &str) -> Result<i64, ParseTimestampError> {
|
||||
let s = s.trim().to_lowercase();
|
||||
let s = s.as_str();
|
||||
|
||||
let res: IResult<&str, (char, &str)> = all_consuming(preceded(
|
||||
char('@'),
|
||||
tuple((
|
||||
// Note: to stay compatible with gnu date this code allows
|
||||
// multiple + and - and only considers the last one
|
||||
fold_many0(
|
||||
// parse either + or -
|
||||
alt((char('+'), char('-'))),
|
||||
// start with a +
|
||||
|| '+',
|
||||
// whatever we get (+ or -), update the accumulator to that value
|
||||
|_, c| c,
|
||||
),
|
||||
digit1,
|
||||
)),
|
||||
))(s);
|
||||
|
||||
let (_, (sign, number_str)) = res?;
|
||||
|
||||
let mut number = number_str.parse::<i64>()?;
|
||||
|
||||
if sign == '-' {
|
||||
number *= -1;
|
||||
}
|
||||
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::parse_timestamp::parse_timestamp;
|
||||
|
||||
#[test]
|
||||
fn test_valid_timestamp() {
|
||||
assert_eq!(parse_timestamp("@1234"), Ok(1234));
|
||||
assert_eq!(parse_timestamp("@99999"), Ok(99999));
|
||||
assert_eq!(parse_timestamp("@-4"), Ok(-4));
|
||||
assert_eq!(parse_timestamp("@-99999"), Ok(-99999));
|
||||
assert_eq!(parse_timestamp("@+4"), Ok(4));
|
||||
assert_eq!(parse_timestamp("@0"), Ok(0));
|
||||
|
||||
// gnu date accepts numbers signs and uses the last sign
|
||||
assert_eq!(parse_timestamp("@---+12"), Ok(12));
|
||||
assert_eq!(parse_timestamp("@+++-12"), Ok(-12));
|
||||
assert_eq!(parse_timestamp("@+----+12"), Ok(12));
|
||||
assert_eq!(parse_timestamp("@++++-123"), Ok(-123));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_timestamp() {
|
||||
assert!(parse_timestamp("@").is_err());
|
||||
assert!(parse_timestamp("@+--+").is_err());
|
||||
assert!(parse_timestamp("@+1ab2").is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user