querydown 0.0.1

An expressive DSL for writing relational database queries that compile to SQL
Documentation
use std::collections::HashSet;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;

use chumsky::prelude::*;

use crate::syntax_tree::*;
use crate::tokens::*;

use super::utils::*;

pub fn duration() -> impl QdParser<Duration> {
    let part = |sym: char| positive_float().then_ignore(just(sym));
    let large_part = choice((
        part('Y').map(|value| Part { kind: Year, value }),
        part('M').map(|value| Part { kind: Month, value }),
        part('W').map(|value| Part { kind: Week, value }),
        part('D').map(|value| Part { kind: Day, value }),
    ));
    #[rustfmt::skip]
    let small_part = choice((
        part('H').map(|value| Part { kind: Hour, value }),
        part('M').map(|value| Part { kind: Minute, value }),
        part('S').map(|value| Part { kind: Second, value }),
    ));
    just(LITERAL_DURATION_PREFIX).ignore_then(
        large_part
            .repeated()
            .chain::<Part, _, _>(
                just('T')
                    .ignore_then(small_part.repeated().at_least(1))
                    .or_not()
                    .flatten(),
            )
            .try_map(|v, span| assemble(v).map_err(|s| Simple::custom(span, s))),
    )
}

#[derive(Debug, PartialEq, Eq, Hash)]
enum Kind {
    Year,
    Month,
    Week,
    Day,
    Hour,
    Minute,
    Second,
}
use Kind::*;

impl Display for Kind {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match self {
            Year => f.write_str("Year"),
            Month => f.write_str("Month"),
            Week => f.write_str("Week"),
            Day => f.write_str("Day"),
            Hour => f.write_str("Hour"),
            Minute => f.write_str("Minute"),
            Second => f.write_str("Second"),
        }
    }
}

struct Part {
    value: f64,
    kind: Kind,
}

fn assemble(parts: Vec<Part>) -> Result<Duration, String> {
    if parts.len() == 0 {
        return Err("Duration must have at least one part".to_string());
    }
    let mut kinds_seen: HashSet<Kind> = HashSet::new();
    let mut duration = Duration::default();
    for part in parts {
        if kinds_seen.contains(&part.kind) {
            return Err(format!("Duration can't have two {} parts.", part.kind));
        }
        match part.kind {
            Year => duration.years = part.value,
            Month => duration.months = part.value,
            Week => duration.weeks = part.value,
            Day => duration.days = part.value,
            Hour => duration.hours = part.value,
            Minute => duration.minutes = part.value,
            Second => duration.seconds = part.value,
        }
        kinds_seen.insert(part.kind);
    }
    return Ok(duration);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_duration() {
        let parse = |s: &str| duration().then_ignore(end()).parse(s);
        assert_eq!(
            parse("@1Y2.2M0.3W4444DT5H6M7S"),
            Ok(Duration {
                years: 1.0,
                months: 2.2,
                weeks: 0.3,
                days: 4444.0,
                hours: 5.0,
                minutes: 6.0,
                seconds: 7.0,
            })
        );
        assert_eq!(parse("@0Y"), Ok(Duration::default()));
        assert_eq!(parse("@T0S"), Ok(Duration::default()));
        assert!(parse("@1M").is_ok());
        assert!(parse("@T1M").is_ok());
        assert!(parse("@1MT1M").is_ok());
        assert!(parse("@").is_err());
        assert!(parse("@1").is_err());
        assert!(parse("@1YY").is_err());
        assert!(parse("@1T").is_err());
        assert!(parse("@1YT").is_err());
        assert!(parse("@1YT1").is_err());
        assert!(parse("@1TM").is_err());
        assert!(parse("@1Y2Y").is_err());
        assert!(parse("@1Y0Y").is_err());
    }
}