use std::{borrow::Cow, collections::HashMap, iter::Peekable, str::CharIndices, time::Duration};
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error {
#[error("unexpected character: {0}")]
UnexpectedChar(char),
#[error("unexpected unit: {0}")]
UnexpectedUnit(String),
#[error("expected a unit")]
ExpectedUnit,
#[error("expected a number")]
ExpectedNumber,
#[error("number was too large: {0}")]
Overflow(String),
}
#[derive(Debug, PartialEq, Eq)]
enum Token<'a> {
Number(u32),
Unit(&'a str),
}
struct Scanner<'a> {
source: &'a str,
chars: Peekable<CharIndices<'a>>,
}
impl<'a> Scanner<'a> {
fn new(source: &'a str) -> Self {
Scanner {
source,
chars: source.char_indices().peekable(),
}
}
fn scan_tokens(mut self) -> Result<Vec<Token<'a>>, Error> {
let mut tokens = vec![];
while let Some(&(i, c)) = self.chars.peek() {
match c {
c if self.should_skip(c) => {
self.chars.next();
}
c if c.is_ascii_digit() => {
tokens.push(Token::Number(self.scan_number(i)?));
}
c if c.is_ascii_alphabetic() => {
tokens.push(Token::Unit(self.scan_unit(i)));
}
unexpected => return Err(Error::UnexpectedChar(unexpected)),
};
}
Ok(tokens)
}
fn should_skip(&self, c: char) -> bool {
c.is_ascii_whitespace() || c == ','
}
fn scan_number(&mut self, start: usize) -> Result<u32, Error> {
let mut end = start;
while let Some((_, c)) = self.chars.peek() {
if !c.is_ascii_digit() {
break;
}
end = self.chars.next().unwrap().0;
}
self.source[start..=end]
.parse()
.map_err(|_e| Error::Overflow(self.source[start..=end].to_string()))
}
fn scan_unit(&mut self, start: usize) -> &'a str {
let mut end = start;
while let Some((_, c)) = self.chars.peek() {
if !c.is_ascii_alphabetic() {
break;
}
end = self.chars.next().unwrap().0;
}
&self.source[start..=end]
}
}
pub struct ParserUnits {
values: HashMap<String, Duration>,
}
impl ParserUnits {
pub fn new() -> Self {
ParserUnits {
values: HashMap::new(),
}
}
pub fn add_unit(&mut self, k: impl Into<String>, v: Duration) {
self.values.insert(k.into(), v);
}
fn get_duration(&self, k: &str) -> Option<&Duration> {
self.values.get(k)
}
}
impl Default for ParserUnits {
fn default() -> Self {
let mut parser_units = ParserUnits::new();
for u in ["h", "hr", "hrs", "hour", "hours"] {
parser_units.add_unit(u, Duration::from_secs(3600));
}
for u in ["m", "min", "mins", "minute", "minutes"] {
parser_units.add_unit(u, Duration::from_secs(60));
}
for u in ["s", "sec", "secs", "second", "seconds"] {
parser_units.add_unit(u, Duration::from_secs(1));
}
for u in ["ms", "msec", "msecs", "millisecond", "milliseconds"] {
parser_units.add_unit(u, Duration::from_millis(1));
}
parser_units
}
}
#[derive(Default)]
pub struct ParserOptions {
ignore_case: bool,
units: ParserUnits,
}
impl ParserOptions {
pub fn ignore_case(mut self, ignore: bool) -> Self {
self.ignore_case = ignore;
self
}
pub fn with_units(mut self, units: ParserUnits) -> Self {
self.units = units;
self
}
}
#[derive(Default)]
pub struct Parser {
options: ParserOptions,
}
impl Parser {
pub fn new(options: ParserOptions) -> Self {
Parser { options }
}
pub fn parse(&self, input: &str) -> Result<Duration, Error> {
let tokens = Scanner::new(input).scan_tokens()?;
self.parse_tokens(tokens)
}
fn parse_tokens(&self, tokens: Vec<Token>) -> Result<Duration, Error> {
let mut tokens = tokens.into_iter();
let mut dur = Duration::ZERO;
while let Some(token) = tokens.next() {
let num = match token {
Token::Number(n) => n,
Token::Unit(_) => return Err(Error::ExpectedNumber),
};
let unit = match tokens.next() {
Some(Token::Unit(u)) => u,
_ => return Err(Error::ExpectedUnit),
};
dur += num * self.get_unit_duration(unit)?;
}
Ok(dur)
}
fn get_unit_duration(&self, unit: &str) -> Result<Duration, Error> {
let unit = if self.options.ignore_case {
Cow::Owned(unit.to_lowercase())
} else {
Cow::Borrowed(unit)
};
match self.options.units.get_duration(&unit) {
Some(d) => Ok(*d),
None => Err(Error::UnexpectedUnit(unit.into_owned())),
}
}
}
pub fn parse(input: &str) -> Result<Duration, Error> {
Parser::default().parse(input)
}
#[cfg(test)]
mod tests {
use crate::{Scanner, Token};
#[test]
fn test_scanner() {
let scanner = Scanner::new("10 seconds");
let tokens = scanner.scan_tokens();
assert_eq!(tokens, Ok(vec![Token::Number(10), Token::Unit("seconds")]));
let scanner = Scanner::new("9hr1min");
let tokens = scanner.scan_tokens();
assert_eq!(
tokens,
Ok(vec![
Token::Number(9),
Token::Unit("hr"),
Token::Number(1),
Token::Unit("min"),
])
);
let scanner = Scanner::new("712635 days");
let tokens = scanner.scan_tokens();
assert_eq!(tokens, Ok(vec![Token::Number(712635), Token::Unit("days")]));
}
}