You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
9.0 KiB
401 lines
9.0 KiB
//! Parsing times from definition files
|
|
|
|
use std::{
|
|
str,
|
|
num::{self, NonZeroU64},
|
|
fmt,
|
|
iter::FromIterator,
|
|
|
|
};
|
|
|
|
/// Interval for job, in miliseconds.
|
|
#[derive(Debug,Clone,PartialEq,Eq,Hash,Ord,PartialOrd)]
|
|
pub enum Unit
|
|
{
|
|
Second,
|
|
Minute,
|
|
Hour,
|
|
Day,
|
|
Week,
|
|
Year,
|
|
// Fuck months lole
|
|
Aeon,
|
|
}
|
|
|
|
impl Unit
|
|
{
|
|
/// Multiplier to get to miliseconds
|
|
|
|
#[inline]
|
|
#[cfg(nightly)]
|
|
const fn multiplier(&self) -> u64 {
|
|
use Unit::*;
|
|
match self {
|
|
Second => 1000,
|
|
Minute => Second.multiplier() * 60,
|
|
Hour => Minute.multiplier() * 60,
|
|
Day => Hour.multiplier() * 24,
|
|
Week => Day.multiplier() * 7,
|
|
Year => Week.multiplier() * 52,
|
|
Aeon => Year.multiplier() * 1000000,
|
|
}
|
|
}
|
|
#[cfg(not(nightly))]
|
|
fn multiplier(&self) -> u64 {
|
|
use Unit::*;
|
|
match self {
|
|
Second => 1000,
|
|
Minute => Second.multiplier() * 60,
|
|
Hour => Minute.multiplier() * 60,
|
|
Day => Hour.multiplier() * 24,
|
|
Week => Day.multiplier() * 7,
|
|
Year => Week.multiplier() * 52,
|
|
Aeon => Year.multiplier() * 1000000,
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// A time object parsed from the definition file.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord,PartialOrd)]
|
|
pub struct Time
|
|
{
|
|
unit: Unit,
|
|
value: NonZeroU64,
|
|
|
|
/// Value in miliseconds
|
|
absolute: Option<NonZeroU64>,
|
|
}
|
|
|
|
impl FromIterator<Time> for Time
|
|
{
|
|
fn from_iter<I: IntoIterator<Item=Time>>(iter: I) -> Self
|
|
{
|
|
iter.into_iter().fold(Option::<Self>::None, |acc, x| {
|
|
Some(match acc {
|
|
Some(acc) => acc.extend(x).unwrap(),
|
|
None => x,
|
|
})
|
|
}).unwrap()
|
|
}
|
|
}
|
|
|
|
impl Time
|
|
{
|
|
fn into_abs(mut self) -> Result<Self, ParseError>
|
|
{
|
|
self.absolute = Some(NonZeroU64::new(self.unit.multiplier()
|
|
.checked_mul(u64::from(self.value))
|
|
.ok_or(ParseError::nz_err())?)
|
|
.ok_or(ParseError::nz_err())?);
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
/// Create a new `Time`
|
|
///
|
|
/// # Panics
|
|
/// If value in miliseconds would overflow a 64 bit unisnged integer
|
|
pub fn new(value: NonZeroU64, unit: Unit) -> Self
|
|
{
|
|
Self {
|
|
unit,
|
|
value,
|
|
absolute: None,
|
|
}.into_abs().expect("operation would result in overflow")
|
|
}
|
|
|
|
/// Create a new `Time` from miliseconds.
|
|
///
|
|
/// # Panics
|
|
/// If `value` is less than 1 second (1000ms)
|
|
pub fn from_ms(value: NonZeroU64) -> Self
|
|
{
|
|
let value_sec = NonZeroU64::new(u64::from(value) / 1000).expect("absolute `value` is too small for `Time`");
|
|
Self{
|
|
unit: Unit::Second,
|
|
value: value_sec,
|
|
absolute: Some(value),
|
|
}
|
|
}
|
|
|
|
/// Attempt to create a new `Time` from miliseconds.
|
|
pub fn try_from_ms(value: NonZeroU64) -> Result<Self, ParseError>
|
|
{
|
|
let value_sec = NonZeroU64::new(u64::from(value) / 1000).ok_or(ParseError::nz_err())?;
|
|
Ok(Self{
|
|
unit: Unit::Second,
|
|
value: value_sec,
|
|
absolute: Some(value),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Time
|
|
{
|
|
/// Get the absolute value of the interval in miliseconds
|
|
pub fn ms(&self) -> u64
|
|
{
|
|
self.absolute.unwrap().into()
|
|
}
|
|
|
|
fn extend(self, other: Self) -> Result<Self, ParseError>
|
|
{
|
|
Ok(Self::try_from_ms(unsafe{NonZeroU64::new_unchecked(u64::from(self.absolute.unwrap()) + u64::from(other.absolute.unwrap()))})?)
|
|
}
|
|
fn parse_chars<I>(from: &mut I) -> Result<Self, ParseError>
|
|
where I: Iterator<Item=char>
|
|
{
|
|
let mut token = String::new();
|
|
let mut unit = None;
|
|
let mut is_num = true;
|
|
for (i, tok) in (0..).zip(from)
|
|
{
|
|
if is_num && tok.is_whitespace() {
|
|
continue;
|
|
} else if is_num && tok.is_ascii_digit() {
|
|
token.push(tok);
|
|
} else if unit.is_none() {
|
|
is_num=false;
|
|
unit = match tok {
|
|
's' | 'S' => Some(Unit::Second),
|
|
'm' | 'M' => Some(Unit::Minute),
|
|
'h' | 'H' => Some(Unit::Hour),
|
|
'd' | 'D' => Some(Unit::Day),
|
|
'w' | 'W' => Some(Unit::Week),
|
|
'y' | 'Y' => Some(Unit::Year),
|
|
'∞' => Some(Unit::Aeon),
|
|
white if white.is_whitespace() => continue,
|
|
_ => return Err(ParseError::UnexpectedToken(tok, i)),
|
|
};
|
|
break;
|
|
} else {return Err(ParseError::UnexpectedToken(tok, i));}
|
|
}
|
|
|
|
Self {
|
|
unit: unit.unwrap_or(Unit::Second),
|
|
value: token.parse::<NonZeroU64>()?,
|
|
absolute: None,
|
|
}.into_abs()
|
|
}
|
|
}
|
|
|
|
impl str::FromStr for Time
|
|
{
|
|
type Err = ParseError;
|
|
|
|
fn from_str(from: &str) -> Result<Self,Self::Err>
|
|
{
|
|
Self::parse_chars(&mut from.chars())
|
|
}
|
|
}
|
|
|
|
/// Parse many `Time`s
|
|
pub fn parse_many(string: impl AsRef<str>, output: &mut Vec<Time>) -> Result<usize, ParseErrorMany>
|
|
{
|
|
let chars = string.as_ref();
|
|
let mut chars = chars.chars();
|
|
|
|
let mut i=0;
|
|
while chars.as_str().len() > 0 {
|
|
output.push(match Time::parse_chars(&mut chars) {
|
|
Ok(v) => v,
|
|
Err(e) => return Err(ParseErrorMany{error: e, line: i}),
|
|
});
|
|
|
|
i+=1;
|
|
}
|
|
|
|
Ok(i)
|
|
}
|
|
|
|
/// Parse many `Time`s into a single one
|
|
pub fn parse_many_into(string: impl AsRef<str>) -> Result<Time, ParseErrorMany>
|
|
{
|
|
let mut output = Vec::new();
|
|
parse_many(string, &mut output)?;
|
|
let mut line=0;
|
|
|
|
output.into_iter().fold(Option::<Result<Time, ParseErrorMany>>::None, |acc, x| {
|
|
line+=1;
|
|
match acc {
|
|
Some(Ok(acc)) => Some(acc.extend(x).map_err(|error| ParseErrorMany{error, line: line-1})),
|
|
None => Some(Ok(x)),
|
|
x=> x,
|
|
}
|
|
}).unwrap_or(Err(ParseErrorMany{error: ParseError::Empty, line:0}))
|
|
}
|
|
|
|
|
|
impl std::fmt::Display for Unit
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
use Unit::*;
|
|
write!(f, "{}", match self {
|
|
Second => 's',
|
|
Minute => 'm',
|
|
Hour => 'h',
|
|
Day => 'd',
|
|
Week => 'w',
|
|
Year => 'y',
|
|
Aeon => '∞',
|
|
})
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Time
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
write!(f, "{} {}", self.value, self.unit)
|
|
}
|
|
}
|
|
|
|
/// Shows which atom parse error originated
|
|
#[derive(Debug)]
|
|
pub struct ParseErrorMany {
|
|
error: ParseError,
|
|
line: usize,
|
|
}
|
|
impl ParseErrorMany
|
|
{
|
|
/// The atom number that error originated
|
|
pub fn atom(&self) -> usize
|
|
{
|
|
self.line
|
|
}
|
|
}
|
|
impl std::error::Error for ParseErrorMany
|
|
{
|
|
/// The `ParseError` source of this error
|
|
fn source(&self) -> Option<&(dyn std::error::Error +'static)>
|
|
{
|
|
Some(&self.error)
|
|
}
|
|
}
|
|
impl std::fmt::Display for ParseErrorMany
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
write!(f, "atom {}: {}", self.line, self.error)
|
|
}
|
|
}
|
|
|
|
|
|
#[cfg(nightly)]
|
|
type IntErrorKind = std::num::IntErrorKind;
|
|
#[cfg(not(nightly))]
|
|
type IntErrorKind = ();
|
|
|
|
/// Error in parsing `Time`
|
|
#[derive(Debug)]
|
|
pub enum ParseError {
|
|
Empty,
|
|
UnexpectedToken(char, usize),
|
|
InvalidNumber(Option<IntErrorKind>),
|
|
}
|
|
impl std::error::Error for ParseError{}
|
|
impl std::fmt::Display for ParseError
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
|
{
|
|
match self {
|
|
Self::Empty => write!(f, "empty string is not a time"),
|
|
Self::UnexpectedToken(tok, loc) => write!(f, "unexpected token `{}' at position {}", tok, loc),
|
|
Self::InvalidNumber(None) => write!(f, "number must be a valid non-zero 64 bit integer"),
|
|
#[cfg(nightly)]
|
|
Self::InvalidNumber(Some(kind)) => write!(f, "failed to parse integer: {:?}", kind),
|
|
#[cfg(not(nightly))]
|
|
Self::InvalidNumber(Some(_)) => write!(f, "failed to parse integer"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<num::TryFromIntError> for ParseError
|
|
{
|
|
fn from(_from: num::TryFromIntError) -> Self
|
|
{
|
|
Self::InvalidNumber(None)
|
|
}
|
|
}
|
|
|
|
impl ParseError
|
|
{
|
|
/// Non-zero conversion error
|
|
const fn nz_err() -> Self
|
|
{
|
|
Self::InvalidNumber(None)
|
|
}
|
|
}
|
|
|
|
impl From<num::ParseIntError> for ParseError
|
|
{
|
|
#[allow(unused_variables)]
|
|
fn from(from: num::ParseIntError) -> Self
|
|
{
|
|
#[cfg(nightly)]
|
|
return Self::InvalidNumber(Some(from.kind().clone()));
|
|
#[cfg(not(nightly))]
|
|
return Self::InvalidNumber(Some(()));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests
|
|
{
|
|
use super::*;
|
|
#[test]
|
|
fn parse_single()
|
|
{
|
|
let time: Time = "100∞".parse().expect("parse failed");
|
|
assert_eq!(time, Time::new(NonZeroU64::try_from(100).unwrap(), Unit::Aeon));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_many()
|
|
{
|
|
macro_rules! time {
|
|
($value:expr, $unit:expr) => {
|
|
Time::new(NonZeroU64::try_from($value).unwrap(), $unit)
|
|
}
|
|
};
|
|
let mut output = Vec::new();
|
|
let time = super::parse_many("1w4d 3h 2m 10s", &mut output).expect("parse failed");
|
|
|
|
assert_eq!(output.len(), time);
|
|
|
|
assert_eq!(&output[..],&[time!(1, Unit::Week),
|
|
time!(4, Unit::Day),
|
|
time!(3, Unit::Hour),
|
|
time!(2, Unit::Minute),
|
|
time!(10, Unit::Second)]);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_many_into_one()
|
|
{
|
|
macro_rules! time {
|
|
($value:expr, $unit:expr) => {
|
|
Time::new(NonZeroU64::try_from($value).unwrap(), $unit)
|
|
}
|
|
};
|
|
let mut output = Vec::new();
|
|
const STR_FROM: &str = "1w4d 3h 2m 10s";
|
|
let time = super::parse_many(STR_FROM, &mut output).expect("parse failed");
|
|
|
|
assert_eq!(output.len(), time);
|
|
|
|
assert_eq!(&output[..],&[time!(1, Unit::Week),
|
|
time!(4, Unit::Day),
|
|
time!(3, Unit::Hour),
|
|
time!(2, Unit::Minute),
|
|
time!(10, Unit::Second)]);
|
|
|
|
let output: Time = output.iter().cloned().collect();
|
|
let input = super::parse_many_into(STR_FROM).expect("parse into failed");
|
|
|
|
assert_eq!(output,input);
|
|
}
|
|
}
|