#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(test(attr(warn(unused))))]
#![doc(test(attr(allow(unused_extern_crates))))]
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![warn(clippy::default_numeric_fallback)]
#![warn(clippy::else_if_without_else)]
#![warn(clippy::fn_to_numeric_cast_any)]
#![warn(clippy::get_unwrap)]
#![warn(clippy::if_then_some_else_none)]
#![warn(clippy::mixed_read_write_in_expression)]
#![warn(clippy::partial_pub_fields)]
#![warn(clippy::rest_pat_in_fully_bound_structs)]
#![warn(clippy::str_to_string)]
#![warn(clippy::string_to_string)]
#![warn(clippy::todo)]
#![warn(clippy::try_err)]
#![warn(clippy::undocumented_unsafe_blocks)]
#![warn(clippy::unneeded_field_pattern)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::return_self_not_must_use)]
#![allow(clippy::enum_glob_use)]
#![allow(clippy::module_name_repetitions)]
macro_rules! validate {
($id:ident, $min:expr, $max:expr) => {{
#[allow(unused_comparisons)]
if $id < $min || $id > $max {
panic!(concat!(
"Invalid ",
stringify!($id),
": Valid range is ",
stringify!($min),
" <= ",
stringify!($id),
" <= ",
stringify!($max)
));
}
}};
($id:ident <= $max:expr) => {{
#[allow(unused_comparisons)]
if $id > $max {
panic!(concat!(
"Invalid ",
stringify!($id),
": Valid maximum ",
stringify!($id),
" is ",
stringify!($max)
));
}
}};
}
mod datetime;
mod util;
pub use datetime::{DateTime, JulianDay};
use fundu_core::config::{Config, ConfigBuilder, Delimiter, NumbersLike};
pub use fundu_core::error::{ParseError, TryFromDurationError};
use fundu_core::parse::{
DurationRepr, Fract, Parser, ReprParserMultiple, ReprParserTemplate, Whole,
};
use fundu_core::time::TimeUnit::*;
pub use fundu_core::time::{Duration, SaturatingInto};
use fundu_core::time::{Multiplier, TimeUnit, TimeUnitsLike};
#[cfg(test)]
pub use rstest_reuse;
use util::{to_lowercase_u64, trim_whitespace};
const DELIMITER: Delimiter = |byte| byte == b' ' || byte.wrapping_sub(9) < 5;
const CONFIG: Config = ConfigBuilder::new()
.allow_time_unit_delimiter()
.allow_ago()
.disable_exponent()
.disable_infinity()
.allow_negative()
.number_is_optional()
.parse_multiple(None)
.allow_sign_delimiter()
.inner_delimiter(DELIMITER)
.outer_delimiter(DELIMITER)
.build();
const TIME_UNITS: TimeUnits = TimeUnits {};
const TIME_KEYWORDS: TimeKeywords = TimeKeywords {};
const NUMERALS: Numerals = Numerals {};
const SECOND_UNIT: (TimeUnit, Multiplier) = (Second, Multiplier(1, 0));
const MINUTE_UNIT: (TimeUnit, Multiplier) = (Minute, Multiplier(1, 0));
const HOUR_UNIT: (TimeUnit, Multiplier) = (Hour, Multiplier(1, 0));
const DAY_UNIT: (TimeUnit, Multiplier) = (Day, Multiplier(1, 0));
const WEEK_UNIT: (TimeUnit, Multiplier) = (Week, Multiplier(1, 0));
const FORTNIGHT_UNIT: (TimeUnit, Multiplier) = (Week, Multiplier(2, 0));
const MONTH_UNIT: (TimeUnit, Multiplier) = (Month, Multiplier(1, 0));
const YEAR_UNIT: (TimeUnit, Multiplier) = (Year, Multiplier(1, 0));
const PARSER: RelativeTimeParser<'static> = RelativeTimeParser::new();
enum FuzzyUnit {
Month,
Year,
}
struct FuzzyTime {
unit: FuzzyUnit,
value: i64,
}
impl FuzzyTime {}
enum ParseFuzzyOutput {
Duration(Duration),
FuzzyTime(FuzzyTime),
}
struct DurationReprParser<'a>(DurationRepr<'a>);
impl<'a> DurationReprParser<'a> {
fn parse(&mut self) -> Result<Duration, ParseError> {
let is_negative = self.0.is_negative.unwrap_or_default();
let time_unit = self.0.unit.unwrap_or(self.0.default_unit);
let digits = self.0.input;
match (&self.0.whole, &self.0.fract) {
(None, None) if self.0.numeral.is_some() => {
let Multiplier(coefficient, exponent) =
self.0.numeral.unwrap() * time_unit.multiplier() * self.0.multiplier;
Ok(self
.0
.parse_duration_with_fixed_number(coefficient, exponent))
}
(None, None) if self.0.unit.is_some() => {
let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
let duration_is_negative = is_negative ^ coefficient.is_negative();
Ok(DurationRepr::calculate_duration(
duration_is_negative,
1,
0,
coefficient,
))
}
(None, None) => {
unreachable!() }
(None, Some(_)) if time_unit == TimeUnit::Second => Err(ParseError::InvalidInput(
"Fraction without a whole number".to_owned(),
)),
(Some(whole), None) => {
let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
let duration_is_negative = is_negative ^ coefficient.is_negative();
let (seconds, attos) = match Whole::parse(&digits[whole.0..whole.1], None, None) {
Some(seconds) => (seconds, 0),
None if duration_is_negative => return Ok(Duration::MIN),
None => return Ok(Duration::MAX),
};
Ok(DurationRepr::calculate_duration(
duration_is_negative,
seconds,
attos,
coefficient,
))
}
(Some(_), Some(fract)) if time_unit == TimeUnit::Second && fract.is_empty() => Err(
ParseError::InvalidInput("Fraction without a fractional number".to_owned()),
),
(Some(whole), Some(fract)) if time_unit == TimeUnit::Second => {
let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
let duration_is_negative = is_negative ^ coefficient.is_negative();
let (seconds, attos) = match Whole::parse(&digits[whole.0..whole.1], None, None) {
Some(seconds) => (seconds, Fract::parse(&digits[fract.0..fract.1], None, None)),
None if duration_is_negative => return Ok(Duration::MIN),
None => return Ok(Duration::MAX),
};
Ok(DurationRepr::calculate_duration(
duration_is_negative,
seconds,
attos,
coefficient,
))
}
(Some(_) | None, Some(_)) => Err(ParseError::InvalidInput(
"Fraction only allowed together with seconds as time unit".to_owned(),
)),
}
}
fn parse_fuzzy(&mut self) -> Result<ParseFuzzyOutput, ParseError> {
let fuzzy_unit = match self.0.unit {
Some(Month) => FuzzyUnit::Month,
Some(Year) => FuzzyUnit::Year,
_ => return self.parse().map(ParseFuzzyOutput::Duration),
};
if self.0.fract.is_some() {
return Err(ParseError::InvalidInput(
"Fraction only allowed together with seconds as time unit".to_owned(),
));
}
match self.0.whole {
None if self.0.numeral.is_some() => {
let Multiplier(coefficient, _) = self.0.numeral.unwrap() * self.0.multiplier;
Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: if self.0.is_negative.unwrap_or_default() {
coefficient.saturating_neg()
} else {
coefficient
},
}))
}
None => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: if self.0.is_negative.unwrap_or_default() ^ self.0.multiplier.is_negative() {
-1
} else {
1
},
})),
Some(whole) => {
let is_negative =
self.0.is_negative.unwrap_or_default() ^ self.0.multiplier.is_negative();
match Whole::parse(&self.0.input[whole.0..whole.1], None, None) {
Some(value) => match i64::try_from(value) {
Ok(value) if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: -value,
})),
Ok(value) => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value,
})),
Err(_) if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: i64::MIN,
})),
Err(_) => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: i64::MAX,
})),
},
None if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: i64::MIN,
})),
None => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
unit: fuzzy_unit,
value: i64::MAX,
})),
}
}
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct RelativeTimeParser<'a> {
raw: Parser<'a>,
}
impl<'a> RelativeTimeParser<'a> {
pub const fn new() -> Self {
Self {
raw: Parser::with_config(CONFIG),
}
}
#[inline]
pub fn parse(&self, source: &str) -> Result<Duration, ParseError> {
self.parse_with_date(source, None)
}
pub fn parse_with_date(
&self,
source: &str,
date: Option<DateTime>,
) -> Result<Duration, ParseError> {
let (years, months, duration) = self.parse_fuzzy(source)?;
if years == 0 && months == 0 {
return Ok(duration);
}
let orig = date.unwrap_or_else(DateTime::now_utc);
orig.checked_add_duration(&duration)
.and_then(|date| {
date.checked_add_gregorian(years, months, 0)
.and_then(|date| date.duration_since(orig))
})
.ok_or(ParseError::Overflow)
}
#[allow(clippy::missing_panics_doc)]
pub fn parse_fuzzy(&self, source: &str) -> Result<(i64, i64, Duration), ParseError> {
let trimmed = trim_whitespace(source);
let mut duration = Duration::ZERO;
let mut years = 0i64;
let mut months = 0i64;
let mut parser = &mut ReprParserMultiple::new(trimmed);
loop {
let (duration_repr, maybe_parser) = parser.parse(
&self.raw.config,
&TIME_UNITS,
Some(&TIME_KEYWORDS),
Some(&NUMERALS),
)?;
match DurationReprParser(duration_repr).parse_fuzzy()? {
ParseFuzzyOutput::Duration(parsed_duration) => {
duration = if duration.is_zero() {
parsed_duration
} else if parsed_duration.is_zero() {
duration
} else {
duration.saturating_add(parsed_duration)
}
}
ParseFuzzyOutput::FuzzyTime(fuzzy) => match fuzzy.unit {
FuzzyUnit::Month => months = months.saturating_add(fuzzy.value),
FuzzyUnit::Year => years = years.saturating_add(fuzzy.value),
},
}
match maybe_parser {
Some(p) => parser = p,
None => break Ok((years, months, duration)),
}
}
}
}
impl<'a> Default for RelativeTimeParser<'a> {
fn default() -> Self {
Self::new()
}
}
struct TimeUnits {}
impl TimeUnitsLike for TimeUnits {
#[inline]
fn is_empty(&self) -> bool {
false
}
#[inline]
fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
const SEC: [u64; 2] = [0x0000_0000_0063_6573, 0];
const SECS: [u64; 2] = [0x0000_0000_7363_6573, 0];
const SECOND: [u64; 2] = [0x0000_646E_6F63_6573, 0];
const SECONDS: [u64; 2] = [0x0073_646E_6F63_6573, 0];
const MIN: [u64; 2] = [0x0000_0000_006E_696D, 0];
const MINS: [u64; 2] = [0x0000_0000_736E_696D, 0];
const MINUTE: [u64; 2] = [0x0000_6574_756E_696D, 0];
const MINUTES: [u64; 2] = [0x0073_6574_756E_696D, 0];
const HOUR: [u64; 2] = [0x0000_0000_7275_6F68, 0];
const HOURS: [u64; 2] = [0x0000_0073_7275_6F68, 0];
const DAY: [u64; 2] = [0x0000_0000_0079_6164, 0];
const DAYS: [u64; 2] = [0x0000_0000_7379_6164, 0];
const WEEK: [u64; 2] = [0x0000_0000_6B65_6577, 0];
const WEEKS: [u64; 2] = [0x0000_0073_6B65_6577, 0];
const FORTNIGHT: [u64; 2] = [0x6867_696E_7472_6F66, 0x0000_0000_0000_0074];
const FORTNIGHTS: [u64; 2] = [0x6867_696E_7472_6F66, 0x0000_0000_0000_7374];
const MONTH: [u64; 2] = [0x0000_0068_746E_6F6D, 0];
const MONTHS: [u64; 2] = [0x0000_7368_746E_6F6D, 0];
const YEAR: [u64; 2] = [0x0000_0000_7261_6579, 0];
const YEARS: [u64; 2] = [0x0000_0073_7261_6579, 0];
match identifier.len() {
3 => match to_lowercase_u64(identifier) {
SEC => Some(SECOND_UNIT),
MIN => Some(MINUTE_UNIT),
DAY => Some(DAY_UNIT),
_ => None,
},
4 => match to_lowercase_u64(identifier) {
SECS => Some(SECOND_UNIT),
MINS => Some(MINUTE_UNIT),
DAYS => Some(DAY_UNIT),
HOUR => Some(HOUR_UNIT),
WEEK => Some(WEEK_UNIT),
YEAR => Some(YEAR_UNIT),
_ => None,
},
5 => match to_lowercase_u64(identifier) {
HOURS => Some(HOUR_UNIT),
WEEKS => Some(WEEK_UNIT),
YEARS => Some(YEAR_UNIT),
MONTH => Some(MONTH_UNIT),
_ => None,
},
6 => match to_lowercase_u64(identifier) {
SECOND => Some(SECOND_UNIT),
MINUTE => Some(MINUTE_UNIT),
MONTHS => Some(MONTH_UNIT),
_ => None,
},
7 => match to_lowercase_u64(identifier) {
SECONDS => Some(SECOND_UNIT),
MINUTES => Some(MINUTE_UNIT),
_ => None,
},
9 => (to_lowercase_u64(identifier) == FORTNIGHT).then_some(FORTNIGHT_UNIT),
10 => (to_lowercase_u64(identifier) == FORTNIGHTS).then_some(FORTNIGHT_UNIT),
_ => None,
}
}
}
struct TimeKeywords {}
impl TimeUnitsLike for TimeKeywords {
#[inline]
fn is_empty(&self) -> bool {
false
}
#[inline]
fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
const NOW: [u64; 2] = [0x0000_0000_0077_6F6E, 0];
const YESTERDAY: [u64; 2] = [0x6164_7265_7473_6579, 0x0000_0000_0000_0079];
const TOMORROW: [u64; 2] = [0x776F_7272_6F6D_6F74, 0];
const TODAY: [u64; 2] = [0x0000_0079_6164_6F74, 0];
match identifier.len() {
3 => (to_lowercase_u64(identifier) == NOW).then_some((TimeUnit::Day, Multiplier(0, 0))),
5 => {
(to_lowercase_u64(identifier) == TODAY).then_some((TimeUnit::Day, Multiplier(0, 0)))
}
8 => (to_lowercase_u64(identifier) == TOMORROW)
.then_some((TimeUnit::Day, Multiplier(1, 0))),
9 => (to_lowercase_u64(identifier) == YESTERDAY)
.then_some((TimeUnit::Day, Multiplier(-1, 0))),
_ => None,
}
}
}
struct Numerals {}
impl NumbersLike for Numerals {
#[inline]
fn get(&self, identifier: &str) -> Option<Multiplier> {
const LAST: [u64; 2] = [0x0000_0000_7473_616C, 0];
const THIS: [u64; 2] = [0x0000_0000_7369_6874, 0];
const NEXT: [u64; 2] = [0x0000_0000_7478_656E, 0];
const FIRST: [u64; 2] = [0x0000_0074_7372_6966, 0];
const THIRD: [u64; 2] = [0x0000_0064_7269_6874, 0];
const FOURTH: [u64; 2] = [0x0000_6874_7275_6F66, 0];
const FIFTH: [u64; 2] = [0x0000_0068_7466_6966, 0];
const SIXTH: [u64; 2] = [0x0000_0068_7478_6973, 0];
const SEVENTH: [u64; 2] = [0x0068_746E_6576_6573, 0];
const EIGHTH: [u64; 2] = [0x0000_6874_6867_6965, 0];
const NINTH: [u64; 2] = [0x0000_0068_746E_696E, 0];
const TENTH: [u64; 2] = [0x0000_0068_746E_6574, 0];
const ELEVENTH: [u64; 2] = [0x6874_6E65_7665_6C65, 0];
const TWELFTH: [u64; 2] = [0x0068_7466_6C65_7774, 0];
match identifier.len() {
4 => match to_lowercase_u64(identifier) {
LAST => Some(Multiplier(-1, 0)),
THIS => Some(Multiplier(0, 0)),
NEXT => Some(Multiplier(1, 0)),
_ => None,
},
5 => match to_lowercase_u64(identifier) {
FIRST => Some(Multiplier(1, 0)),
THIRD => Some(Multiplier(3, 0)),
FIFTH => Some(Multiplier(5, 0)),
SIXTH => Some(Multiplier(6, 0)),
NINTH => Some(Multiplier(9, 0)),
TENTH => Some(Multiplier(10, 0)),
_ => None,
},
6 => match to_lowercase_u64(identifier) {
FOURTH => Some(Multiplier(4, 0)),
EIGHTH => Some(Multiplier(8, 0)),
_ => None,
},
7 => match to_lowercase_u64(identifier) {
SEVENTH => Some(Multiplier(7, 0)),
TWELFTH => Some(Multiplier(12, 0)),
_ => None,
},
8 => (ELEVENTH == to_lowercase_u64(identifier)).then_some(Multiplier(11, 0)),
_ => None,
}
}
}
pub fn parse(source: &str) -> Result<Duration, ParseError> {
PARSER.parse(source)
}
pub fn parse_with_date(source: &str, date: Option<DateTime>) -> Result<Duration, ParseError> {
PARSER.parse_with_date(source, date)
}
pub fn parse_fuzzy(source: &str) -> Result<(i64, i64, Duration), ParseError> {
PARSER.parse_fuzzy(source)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relative_time_parser_new() {
assert_eq!(RelativeTimeParser::new(), RelativeTimeParser::default());
}
#[test]
fn test_time_units_is_empty_returns_false() {
assert!(!TimeUnits {}.is_empty());
}
#[test]
fn test_keywords_is_empty_returns_false() {
assert!(!TimeKeywords {}.is_empty());
}
}