physical-quantity 0.0.5

dimension and unit system for general physical physical quantities.
extern crate alloc;

use alloc:: {
    vec::Vec,
};
use const_array_attrs::sorted;
use const_frac::Frac;
use keyword_parser::bin_searcher::{ longest_matcher, simple_matcher };

use combine:: {
    Parser, RangeStream,
    attempt, choice, from_str, optional,  satisfy, many1,
    error::StringStreamError,
    parser:: {
        char:: { char, string },
        range::take_while1,
    },
};

use crate:: {
    DynDim, Unit,
    predefined::dim,
    unit::Conv,
};

#[sorted]
const SI_PREFIX: [(&str, i8); 31] = [
    ("Y", 24),
    ("Z", 21),
    ("E", 18),
    ("P", 15),
    ("T", 12),
    ("G", 9), ("\u{3310}", 9), //    ("M", 6), ("\u{334B}", 6), //    ("k", 3), ("\u{3314}", 3), //    ("h", 2),
    ("da", 1), ("\u{3372}", 1), //    ("d", -1), ("\u{3325}", -1), //    ("c", -2), ("\u{3322}", -2), //    ("m", -3), ("\u{3349}", -3), //    ("\u{00B5}", -6), // μ
    ("\u{03BC}", -6), // μ
    ("\u{3343}", -6), //    ("n", -9), ("\u{3328}", -9), //    ("p", -12), ("\u{3330}", -12), //    ("f", -15),
    ("a", -18),
    ("z", -21),
    ("y", -24),
];

fn prefix<'a, I>() -> impl 'a + Parser<I, Output = i8>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
{
    longest_matcher(&SI_PREFIX)
}

fn is_delim(c: char) -> bool {
    match c {
        | '*'
        | '\u{00B7}' // ·
        | '\u{00D7}' // ×
        | '\u{200B}' // ZERO WIDTH SPACE
        | '\u{200C}' // ZERO WIDTH NON-JOINER
        | '\u{200D}' // ZERO WIDTH JOINER
        | '\u{2022}' //        | '\u{2219}' //        | '\u{22C5}' //        | '\u{2715}' //        | '\u{2716}' //        | '\u{30FB}' //        | '\u{FF65}' //        => true,
        c => c.is_whitespace()
    }
}

fn delim<'a, I>() -> impl 'a + Parser<I, Output = ()>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
{
    take_while1(is_delim).map(|_| ())
}

fn is_slash(c: char) -> bool {
    match c {
        | '/'
        | '\u{00F7}' // ÷
        | '\u{2044}' // ⁄ fraction slash
        | '\u{2215}' //  ∕ division slash
        | '\u{27CB}' //  ⟋ mathematical rising diagonal
        | '\u{29F8}' //  ⧸ big solidus
        | '\u{6BCE}' //        => true,
        _ => false,
    }
}

fn is_character(c: char) -> bool {
    match c {
        | '-'
        | '+'
        | '[' | ']'
        | '(' | ')' // reserved.
        | '{' | '}' // reserved.
        | '<' | '>' // reserved.
        => false,
        c => !c.is_ascii_digit() && !is_slash(c) && !is_delim(c),
    }
}

fn name<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac,DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    K: AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    take_while1(is_character).flat_map(move |s| match simple_matcher(def).parse(s) {
        Ok((result, _)) => Ok(result.into()),
        Err(e) => Err(e),
    })
}

fn term<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac, DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
    K: 'a + AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    choice((
        attempt((prefix(), name(def))).map(|(i, info)| Unit {
            a: info.a * Frac::from_exp10(i as i16),
            b: info.b * Frac::from_exp10(i as i16),
            .. info
        }),
        name(def).map(|info| info)
    ))
}

fn exp<'a, I>() -> impl 'a + Parser<I, Output = i8>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
{
    (
        optional(char('-')).map(|sign| sign.map_or(1, |_| -1)),
        (from_str(take_while1(|c: I::Token| c.is_ascii_digit())))
    ).map(|(sign, i): (i8, i8)| sign * i)
}

fn element<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac, DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
    K: 'a + AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    (term(def), optional(exp())).map(|(u, exp)| match exp {
        Some(exp) => u.powi(exp),
        None => u,
    })
}

fn prod<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac, DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
    K: 'a + AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    many1((element(def), optional(delim())).map(|(e, _)| e)).map(|v: Vec<_>| {
        let mut it = v.iter();

        match it.next() {
            Some(&spec) => it.fold(spec, |ret, spec| ret * spec),
            None => Default::default(),
        }
    })
}

fn comp<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac, DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
    K: 'a + AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    (prod(def), optional((satisfy(is_slash), optional(delim()), prod(def))))
    .map(|(num, denom)| match denom {
        Some((_, _, denom)) => num / &denom,
        None => num,
    })
}

fn modifier<'a, I>() -> impl 'a + Parser<I, Output = &'a str>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
{
    choice((
        attempt(string("abs")),
        attempt(string("dif")),
        attempt(string("gage")),
    ))
}

pub fn unit<'a, I, K, T>(def: T) -> impl 'a + Parser<I, Output = Unit<Frac, DynDim>>
where
    I: 'a + RangeStream<Token = char, Range = &'a str, Error = StringStreamError>,
    I::Position: Default,
    K: 'a + AsRef<str>,
    T: 'a + AsRef<[(K, (Conv, DynDim))]> + Copy,
{
    (comp(def), optional((char('['), modifier(), char(']')).map(|(_, attr, _)| attr)))
    .map(|(u, attr)| {
        if u.dim == dim::PRESSURE {
            match attr {
                Some(attr) if attr == "gage" => Unit {
                    b: Frac::from_int(101325000),
                    .. u
                },
                _ => u,
            }
        } else {
            u
        }
    })
}

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

    #[test]
    fn test_prefix() {
        let s = "m";

        assert_eq!(
            prefix().parse(s).unwrap(),
            (-3, "")
        );
    }

    #[test]
    fn test_name() {
        let s = "m";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];

        assert_eq!(
            name(&def).parse(s).unwrap().0,
            Unit {
                a: Frac::from_int(1),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::LENGTH),
            }
        );
    }

    #[test]
    fn test_dimensionless() {
        let s = "\u{2014}";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];

        assert_eq!(
            name(&def).parse(s).unwrap().0,
            Unit {
                a: Frac::from_int(1),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::DIMENSIONLESS),
            }
        );
    }

    #[test]
    fn test_term() {
        let s = "kg";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];
        let mut p = term(&def);
        let result = p.parse(s);

        assert_eq!(
            result.unwrap().0,
            Unit {
                a: Frac::from_int(1000),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::MASS),
            }
        );
    }

    #[test]
    fn test_element() {
        let s = "m2";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];

        assert_eq!(
            element(&def).parse(s).unwrap().0,
            Unit {
                a: Frac::from_int(1),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::AREA),
            }
        );
    }

    #[test]
    fn test_prod() {
        let s = "N・m";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];

        assert_eq!(
            prod(&def).parse(s).unwrap().0,
            Unit {
                a: Frac::from_int(1000),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::ENERGY),
            }
        );
    }

    #[test]
    fn test_unit() {
        let s = "kg/m*s2";
        let def = &predefined::unit::DEFAULT_UNIT_DEF[..];

        assert_eq!(
            unit(&def).parse(s).unwrap().0,
            Unit {
                a: Frac::from_int(1000),
                b: Frac::from_int(0),
                dim: DynDim::from(dim::PRESSURE),
            }
        );
    }
}