diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..7cc32ca --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "src/bin" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4ff14a1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: rust +sudo: required +rust: + - nightly +cache: cargo + +script: + - rm -f target/debug/rink-* target/debug/query-* target/debug/canonicalize-* + - cargo build --verbose + - cargo test --verbose + +addons: + apt: + packages: + - libcurl4-openssl-dev + - libelf-dev + - libdw-dev + - cmake + - gcc + - binutils-dev + - libiberty-dev + +after_success: | + wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && + tar xzf master.tar.gz && + cd kcov-master && + mkdir build && + cd build && + cmake .. && + make && + make install DESTDIR=../../kcov-build && + cd ../.. && + rm -rf kcov-master && + for file in target/debug/*-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && + bash <(curl -s https://codecov.io/bash) && + echo "Uploaded code coverage" diff --git a/src/ast.rs b/src/ast.rs index b23b0d2..be3893b 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -81,6 +81,7 @@ pub enum Query { } #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub enum DatePattern { Literal(String), Match(String), @@ -312,3 +313,26 @@ impl fmt::Display for DateToken { } } } + +#[cfg(test)] +mod test { + use super::Expr::{self, *}; + + fn check(e: T, expected: &str) { + assert_eq!(format!("{}", e), expected); + } + + impl From for Expr { + fn from(x: i64) -> Self { + Const(x.into()) + } + } + + #[test] + fn test_display_call() { + check(Call("f".into(), vec![]), "f()"); + check(Call("f".into(), vec![1.into()]), "f(1)"); + check(Call("f".into(), vec![1.into(), 2.into()]), "f(1, 2)"); + check(Call("f".into(), vec![1.into(), 2.into(), 3.into()]), "f(1, 2, 3)"); + } +} diff --git a/src/date.rs b/src/date.rs index 4fb2411..12ff770 100644 --- a/src/date.rs +++ b/src/date.rs @@ -119,8 +119,8 @@ pub fn parse_date( let value = u32::from_str_radix(&**s, 10).unwrap(); out.hour_mod_12 = Some(value % 12); Ok(()) - }, - x => Err(format!("Expected 2-digit hour24, got {}", ts(x))) + } + x => Err(format!("Expected 2-digit hour12, got {}", ts(x))), }, "hour24" => match tok { Some(DateToken::Number(ref s, None)) if s.len() == 2 => { @@ -274,62 +274,63 @@ impl GenericDateTime { } } +fn attempt(date: &[DateToken], pat: &[DatePattern]) -> Result { + let mut parsed = Parsed::new(); + let mut tz = None; + let mut iter = date.iter().cloned().peekable(); + let res = parse_date(&mut parsed, &mut tz, &mut iter, pat); + let count = iter.count(); + let res = if count > 0 && res.is_ok() { + Err(format!("Expected eof, got {}", + date[date.len()-count..].iter() + .map(ToString::to_string) + .collect::>().join(""))) + } else { + res + }; + try!(res.map_err(|e| (e, count))); + let time = parsed.to_naive_time(); + let date = parsed.to_naive_date(); + if let Some(tz) = tz { + match (time, date) { + (Ok(time), Ok(date)) => + tz.from_local_datetime(&date.and_time(time)).earliest().ok_or_else(|| (format!( + "Datetime does not represent a valid moment in time" + ), count)).map(GenericDateTime::Timezone), + (Ok(time), Err(_)) => + Ok(UTC::now().with_timezone(&tz).date().and_time(time).unwrap()).map( + GenericDateTime::Timezone), + (Err(_), Ok(date)) => + tz.from_local_date(&date).earliest().map(|x| x.and_hms(0, 0, 0)).ok_or_else(|| (format!( + "Datetime does not represent a valid moment in time" + ), count)).map(GenericDateTime::Timezone), + _ => Err((format!("Failed to construct a useful datetime"), count)) + } + } else { + let offset = parsed.to_fixed_offset().unwrap_or(FixedOffset::east(0)); + match (time, date) { + (Ok(time), Ok(date)) => + Ok(GenericDateTime::Fixed(DateTime::::from_utc( + date.and_time(time), offset + ))), + (Ok(time), Err(_)) => + Ok(GenericDateTime::Fixed(UTC::now().with_timezone( + &offset + ).date().and_time(time).unwrap())), + (Err(_), Ok(date)) => + Ok(GenericDateTime::Fixed(Date::::from_utc( + date, offset + ).and_hms(0, 0, 0))), + _ => Err((format!("Failed to construct a useful datetime"), count)) + } + } +} + pub fn try_decode(date: &[DateToken], context: &Context) -> Result { let mut best = None; for pat in &context.datepatterns { //println!("Tring {:?} against {}", date, show_datepattern(pat)); - let attempt = || -> Result { - let mut parsed = Parsed::new(); - let mut tz = None; - let mut iter = date.iter().cloned().peekable(); - let res = parse_date(&mut parsed, &mut tz, &mut iter, &pat[..]); - let count = iter.count(); - let res = if count > 0 && res.is_ok() { - Err(format!("Expected eof, got {}", - date[date.len()-count..].iter() - .map(ToString::to_string) - .collect::>().join(""))) - } else { - res - }; - try!(res.map_err(|e| (e, count))); - let time = parsed.to_naive_time(); - let date = parsed.to_naive_date(); - if let Some(tz) = tz { - match (time, date) { - (Ok(time), Ok(date)) => - tz.from_local_datetime(&date.and_time(time)).earliest().ok_or_else(|| (format!( - "Datetime does not represent a valid moment in time" - ), count)).map(GenericDateTime::Timezone), - (Ok(time), Err(_)) => - Ok(UTC::now().with_timezone(&tz).date().and_time(time).unwrap()).map( - GenericDateTime::Timezone), - (Err(_), Ok(date)) => - tz.from_local_date(&date).earliest().map(|x| x.and_hms(0, 0, 0)).ok_or_else(|| (format!( - "Datetime does not represent a valid moment in time" - ), count)).map(GenericDateTime::Timezone), - _ => Err((format!("Failed to construct a useful datetime"), count)) - } - } else { - let offset = parsed.to_fixed_offset().unwrap_or(FixedOffset::east(0)); - match (time, date) { - (Ok(time), Ok(date)) => - Ok(GenericDateTime::Fixed(DateTime::::from_utc( - date.and_time(time), offset - ))), - (Ok(time), Err(_)) => - Ok(GenericDateTime::Fixed(UTC::now().with_timezone( - &offset - ).date().and_time(time).unwrap())), - (Err(_), Ok(date)) => - Ok(GenericDateTime::Fixed(Date::::from_utc( - date, offset - ).and_hms(0, 0, 0))), - _ => Err((format!("Failed to construct a useful datetime"), count)) - } - } - }; - match attempt() { + match attempt(date, pat) { Ok(datetime) => return Ok(datetime), Err((e, c)) => { //println!("{}", e); @@ -474,3 +475,264 @@ impl Context { None } } + +#[cfg(test)] +mod tests { + use super::*; + + fn pattern(s: &str) -> Vec { + parse_datepattern(&mut s.chars().peekable()).unwrap() + } + + fn parse_with_tz(date: Vec, pat: &str) -> (Result<(), String>, Parsed, Option) { + let mut parsed = Parsed::new(); + let mut tz = None; + let pat = pattern(pat); + let res = parse_date(&mut parsed, &mut tz, &mut date.into_iter().peekable(), &pat); + + (res, parsed, tz) + } + + fn parse(date: Vec, pat: &str) -> (Result<(), String>, Parsed) { + let (res, parsed, _) = parse_with_tz(date, pat); + (res, parsed) + } + + #[test] + fn test_literal() { + let date = vec![DateToken::Literal("abc".into())]; + let (res, parsed) = parse(date.clone(), "'abc'"); + assert_eq!(parsed, Parsed::new()); + assert!(res.is_ok()); + + let (res, parsed) = parse(date, "'def'"); + assert_eq!(parsed, Parsed::new()); + assert_eq!(res, Err("Expected `def`, got `abc`".into())); + } + + #[test] + fn test_year_plus() { + let mut expected = Parsed::new(); + expected.set_year(123).unwrap(); + + let date = vec![ + DateToken::Plus, + DateToken::Number(format!("{}", expected.year.unwrap()), None), + ]; + let (res, parsed) = parse(date.clone(), "year"); + assert!(res.is_ok()); + assert_eq!(parsed, expected); + + let date = vec![DateToken::Number( + format!("{}", expected.year.unwrap()), + None, + )]; + let (res, parsed2) = parse(date.clone(), "year"); + assert!(res.is_ok()); + assert_eq!(parsed2, parsed); + } + + #[test] + fn test_complicated_date_input() { + let mut expected = Parsed::new(); + expected.set_year(123).unwrap(); + expected.set_month(5).unwrap(); + expected.set_day(2).unwrap(); + expected.set_ampm(true).unwrap(); + expected.set_hour(13).unwrap(); + expected.set_minute(57).unwrap(); + + let date = vec![ + DateToken::Number(format!("{}", expected.day.unwrap()), None), + DateToken::Space, + DateToken::Literal("Pm".into()), + DateToken::Dash, + DateToken::Number(format!("{:02}", expected.month.unwrap()), None), + DateToken::Colon, + DateToken::Number(format!("{}", expected.year.unwrap()), None), + DateToken::Space, + DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None), + DateToken::Dash, + DateToken::Number(format!("{:02}", expected.minute.unwrap()), None), + DateToken::Space, + DateToken::Literal("May".into()), + ]; + + let (res, parsed) = parse(date, "day meridiem-monthnum:year hour12-min monthname"); + assert!(res.is_ok()); + assert_eq!(parsed, expected); + } + + #[test] + fn ad_bc() { + let year = -100; + let mut expected = Parsed::new(); + expected.set_year(year + 1).unwrap(); + expected.set_hour(7).unwrap(); + + let date = vec![ + DateToken::Number(format!("{}", year.abs()), None), + DateToken::Space, + DateToken::Literal("bce".into()), + DateToken::Space, + DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None), + DateToken::Space, + DateToken::Literal("am".into()), + ]; + + let (res, parsed) = parse(date, "year adbc hour12 meridiem"); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(parsed, expected); + } + + #[test] + fn ad_bc_wrong() { + for date in vec![ + vec![DateToken::Literal("foo".into())], + vec![DateToken::Plus], + ] { + let (res, _) = parse(date, "adbc"); + assert!(res.is_err()); + } + } + + #[test] + fn wrong_length_24h() { + let date = vec![DateToken::Number("7".into(), None)]; + let (res, _) = parse(date, "hour24"); + assert_eq!(res, Err(format!("Expected 2-digit hour24, got `{}`", 7))); + } + + #[test] + fn test_24h() { + let (res, parsed) = parse(vec![DateToken::Number("23".into(), None)], "hour24"); + assert!(res.is_ok()); + assert_eq!(parsed.hour_div_12, Some(1)); + assert_eq!(parsed.hour_mod_12, Some(11)); + } + + #[test] + fn seconds() { + let mut expected = Parsed::new(); + expected.set_second(27).unwrap(); + expected.set_nanosecond(12345).unwrap(); + + let date = vec![DateToken::Number("27".into(), Some("000012345".into()))]; + let (res, parsed) = parse(date, "sec"); + assert!(res.is_ok()); + assert_eq!(parsed, expected); + + let date = vec![DateToken::Number("27".into(), None)]; + let (res, parsed) = parse(date, "sec"); + assert!(res.is_ok()); + expected.nanosecond = None; + assert_eq!(parsed, expected); + } + + #[test] + fn test_offset() { + let date = vec![DateToken::Plus, DateToken::Number("0200".into(), None)]; + let (res, parsed) = parse(date, "offset"); + assert!(res.is_ok()); + assert_eq!(parsed.offset, Some(2 * 3600)); + + let date = vec![ + DateToken::Dash, + DateToken::Number("01".into(), None), + DateToken::Colon, + DateToken::Number("23".into(), None), + ]; + let (res, parsed) = parse(date, "offset"); + assert!(res.is_ok()); + assert_eq!(parsed.offset, Some(-(1 * 60 + 23) * 60)); + + let date = vec![DateToken::Literal("Europe/London".into())]; + let (res, parsed, tz) = parse_with_tz(date, "offset"); + assert!(res.is_ok(), res.unwrap_err()); + assert_eq!(tz.unwrap(), Tz::Europe__London); + assert_eq!(parsed.offset, None); + } + + #[test] + fn test_weekday() { + let date = vec![DateToken::Literal("saturday".into())]; + let (res, parsed) = parse(date, "weekday"); + assert!(res.is_ok()); + assert_eq!(parsed.weekday, Some(Weekday::Sat)); + + let date = vec![DateToken::Literal("sun".into())]; + assert!(parse(date, "weekday").0.is_ok()); + + let date = vec![DateToken::Literal("snu".into())]; + assert_eq!(parse(date, "weekday").0, Err("Unknown weekday: snu".into())); + } + + #[test] + fn test_monthname() { + for (i, &s) in [ + "jan", "feb", "mar", "apr", "may", "june", "jul", "AUGUST", "SEp", "Oct", "novemBer", + "dec", + ] + .into_iter() + .enumerate() + { + let date = vec![DateToken::Literal(s.into())]; + let (res, parsed) = parse(date, "monthname"); + assert!(res.is_ok()); + assert_eq!(parsed.month, Some(i as u32 + 1)); + } + + let date = vec![DateToken::Literal("foobar".into())]; + let (res, parsed) = parse(date, "monthname"); + assert_eq!(res, Err("Unknown month name: foobar".into())); + assert_eq!(parsed.month, None); + } + + #[test] + fn test_parse_datepattern() { + use self::DatePattern::*; + + fn parse(s: &str) -> Result, String> { + parse_datepattern(&mut s.chars().peekable()) + } + + assert_eq!( + parse("-:['abc']"), + Ok(vec![Dash, Colon, Optional(vec![Literal("abc".into())])]) + ); + assert!(parse("-:['abc'").is_err()); + assert!(parse("*").is_err()); + } + + #[test] + fn test_attempt() { + use self::DateToken::*; + fn n(x: &str) -> DateToken { + Number(x.into(), None) + } + + macro_rules! check_attempt { + ($date:expr, $pat:expr) => {{ + let pat = parse_datepattern(&mut $pat.chars().peekable()).unwrap(); + attempt($date, pat.as_ref()) + }}; + } + + let tz = Literal("Europe/London".into()); + + let date = &[n("23"), Space, n("05"), Space, tz.clone()]; + let res = check_attempt!(date, "hour24 min offset"); + assert!(res.is_ok(), "{:?}", res); + + let date = &[n("23"), Space, tz.clone()]; + let res = check_attempt!(date, "hour24 offset"); + assert_eq!( + res, + Err(("Failed to construct a useful datetime".into(), 0)) + ); + + let date = &[n("2018"), Space, n("01"), Space, n("01"), Space, tz.clone()]; + let res = check_attempt!(date, "year monthnum day offset"); + assert!(res.is_ok(), "{:?}", res); + } +} diff --git a/src/formula.rs b/src/formula.rs index 5b9233e..bbd9b8f 100644 --- a/src/formula.rs +++ b/src/formula.rs @@ -56,6 +56,9 @@ impl<'a> Iterator for TokenIterator<'a> { } } +/** + * Compute the molar mass of a compound given its chemical formula. + */ pub fn substance_from_formula(formula: &str, symbols: &BTreeMap, substances: &BTreeMap) -> Option { diff --git a/src/gnu_units.rs b/src/gnu_units.rs index 37ba02e..926c33c 100644 --- a/src/gnu_units.rs +++ b/src/gnu_units.rs @@ -545,3 +545,83 @@ pub fn tokens(iter: &mut Iter) -> Vec { } out } + +#[cfg(test)] +mod tests { + use super::*; + use ast::Expr; + + fn do_parse(s: &str) -> Expr { + let mut iter = TokenIterator::new(s).peekable(); + parse_term(&mut iter) + } + + macro_rules! expect { + ($expr:expr, $pattern:path, $expected:expr) => { + match do_parse($expr) { + $pattern(s) => assert_eq!(s, $expected), + x => panic!("{}", x), + } + }; + } + + #[test] + fn test_parse_term_plus() { + let expr = do_parse("+1"); + + if let Expr::Plus(x) = expr { + if let Expr::Const(x) = *x { + if x != 1.into() { + panic!("number != 1"); + } + } else { + panic!("argument of x is not Expr::Const"); + } + } else { + panic!("missing plus"); + } + } + + #[test] + fn test_missing_bracket() { + match do_parse("(") { + Expr::Error(ref s) => assert_eq!(s, "Expected ), got Eof"), + x => panic!("Wrong result: {}", x), + } + } + + #[test] + fn test_escapes() { + expect!( + "\\\r", + Expr::Error, + "Expected term, got Error(\"Expected LF or CRLF line endings\")" + ); + expect!("\\\r\n1", Expr::Const, 1.into()); + + expect!( + "\\a", + Expr::Error, + "Expected term, got Error(\"Invalid escape: \\\\a\")" + ); + expect!( + "\\", + Expr::Error, + "Expected term, got Error(\"Unexpected EOF\")" + ); + } + + #[test] + fn test_float_leading_dot() { + use gmp::mpq::Mpq; + use gmp::mpz::Mpz; + let num = Mpz::from(123); + let den = Mpz::from(1000); + expect!(".123", Expr::Const, Num::Mpq(Mpq::ratio(&num, &den))); + } + + #[test] + fn test_escaped_quotes() { + expect!("\"ab\\\"\"", Expr::Unit, "ab\"") + } +} diff --git a/src/text_query.rs b/src/text_query.rs index 7b9f316..00d1d2d 100644 --- a/src/text_query.rs +++ b/src/text_query.rs @@ -138,6 +138,7 @@ impl<'a> Iterator for TokenIterator<'a> { } if let Some('*') = self.0.next() { if let Some(&'/') = self.0.peek() { + self.0.next(); return Some(Token::Comment(lines)) } } @@ -357,7 +358,7 @@ impl<'a> Iterator for TokenIterator<'a> { let mut frac = String::new(); self.0.next(); while let Some(c) = self.0.peek().cloned() { - if x.is_digit(10) { + if c.is_digit(10) { self.0.next(); frac.push(c); } else { @@ -563,7 +564,8 @@ fn parse_term(iter: &mut Iter) -> Expr { }, Token::Percent => Expr::Unit("percent".to_owned()), Token::Date(toks) => Expr::Date(toks), - x => Expr::Error(format!("Expected term, got {}", describe(&x))) + Token::Comment(_) => parse_term(iter), + x => Expr::Error(format!("Expected term, got {}", describe(&x))), } } diff --git a/tests/query.rs b/tests/query.rs index 44dd8df..1974947 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -27,6 +27,22 @@ fn test(input: &str, output: &str) { }); } +fn test_starts_with(input: &str, output: &str) { + let mut iter = text_query::TokenIterator::new(input.trim()).peekable(); + let expr = text_query::parse_query(&mut iter); + CONTEXT.with(|ctx| { + let res = ctx.eval_outer(&expr); + let res = match res { + Ok(v) => v.to_string(), + Err(v) => v.to_string(), + }; + assert!( + res.starts_with(output), + format!("\n'{}' !=\n'{}'", res, output) + ); + }); +} + #[test] fn test_definition() { test("watt", "Definition: watt = J / s = 1 watt (power; kg m^2 / s^3)"); @@ -108,9 +124,31 @@ fn test_factorize() { #[test] fn test_conformance() { - test("W -> J", - "Conformance error: 1 watt (power) != 1 joule (energy)\n\ - Suggestions: multiply left side by time, multiply right side by frequency"); + test( + "W -> J", + "Conformance error: 1 watt (power) != 1 joule (energy)\n\ + Suggestions: multiply left side by time, multiply right side by frequency", + ); + test( + "W/s -> J^2", + "Conformance error: 1 newton^2 / kilogram != 1 joule^2\n\ + Suggestions: multiply left side by moment_of_inertia, divide right side by moment_of_inertia", + ); + test( + "m^2 -> kg^2", + "Conformance error: 1 meter^2 (area) != 1 kilogram^2 (kg^2)\n\ + Suggestions: multiply left side by linear_density^2, multiply right side by area / mass^2", + ); + test( + "c -> kg", + "Conformance error: 299792458 meter / second (velocity) != 1 kilogram (mass)\n\ + Suggestions: multiply left side by mass time / length, multiply right side by length / mass time" + ); + test( + "1/m -> 'abc'", + "Conformance error: 1 / meter (m^-1) != 1 abc (abc)\n\ + Suggestions: multiply left side by 'abc' length, divide right side by 'abc' length", + ); } #[test] @@ -153,6 +191,13 @@ fn test_bases() { test("pi m -> bin m", "approx. 11.00100 meter (length)"); test("100K -> hex °C", "Conversion to °C is not defined in base 16"); test("now -> hex +00:00", "Conversion to 00:00 is not defined in base 16"); + test("256 -> base 16", "100 (dimensionless)"); + + test( + "123 -> base 37", + "Unsupported base 37, must be from 2 to 36", + ); + test("123 -> base 0xf", "Expected decimal base, got hex"); } #[test] @@ -221,3 +266,306 @@ fn percent_operator() { test("120% 2", "2.4 (dimensionless)"); test("% 1", "0.01 (dimensionless)"); } + +#[test] +fn test_kilosecond() { + test("1ks", "16 minute, 40 second (time)"); + test("1kss", "16 minute, 40 second (time)"); +} + +#[test] +#[should_panic] +fn test_second_double_prefix() { + let mut iter = text_query::TokenIterator::new("mks").peekable(); + let expr = text_query::parse_query(&mut iter); + CONTEXT.with(|ctx| { + ctx.eval_outer(&expr).unwrap(); + }); +} + +#[test] +fn test_missing_substance() { + test( + "density of flubber", + "No such unit flubber, did you mean flour?", + ); +} + +#[test] +fn test_missing_property() { + test("mass of flour", "No such property mass of flour"); +} + +#[test] +fn test_unary_operators() { + test("+--+42", "42 (dimensionless)"); + test("++-+42", "-42 (dimensionless)"); +} + +#[test] +fn test_equals() { + test("a = kg N / W^2", "1 second^2 / gray meter"); + test( + "1 = kg", + "= is currently only used for inline unit definitions: expected unit, got 1", + ); +} + +#[test] +fn mismatched_units() { + test( + "W - kg", + "Subtraction of units with mismatched units is not meaningful: \ + <1 watt (power)> - <1 kilogram (mass)>", + ); +} + +#[test] +fn temperature_with_dimension() { + test("kg °C", "Expected dimensionless, got: <1 kilogram (mass)>"); +} + +#[test] +fn test_functions() { + test("exp(ln(10))", "approx. 10.00000 (dimensionless)"); + test("log2(65536)", "approx. 16 (dimensionless)"); + test("10^log10(123)", "approx. 123.0000 (dimensionless)"); + test("log(27, 3)", "approx. 3 (dimensionless)"); + + test("sin(pi/2)", "approx. 1 (dimensionless)"); + test("cos(asin(0.5) - pi/2)", "approx. 0.5000000 (dimensionless)"); + test("atan(tan(0.42))", "approx. 0.4199999 (dimensionless)"); + test("acos(1)", "approx. 0 (dimensionless)"); + test("acosh(cosh(1))", "approx. 1 (dimensionless)"); + test("asinh(sinh(0.123))", "approx. 0.1230000 (dimensionless)"); + test("atanh(tanh(1.23))", "approx. 1.230000 (dimensionless)"); + + test("hypot(3 m, 4 m)", "approx. 5 meter (length)"); + test("atan2(7, 6)", "approx. 0.8621700 (dimensionless)"); +} + +#[test] +fn test_equal_rhs() { + test("1 -> a=3", "1/3, approx. 0.3333333 a (dimensionless)"); +} + +#[test] +fn test_pow_with_dimension() { + test( + "2^m", + "Exponent must be dimensionless: <2 (dimensionless)> ^ <1 meter (length)>", + ); +} + +#[test] +fn test_reciprocal_conversion() { + test( + "miles / gallon -> l / 100km", + "Conformance error: approx. 425143.7 / meter^2 (fuel_efficiency) != 10000 micrometer^2 (area)\n\ + Suggestions: Reciprocal conversion, invert one side", + ); +} + +#[test] +fn test_non_conversion_input() { + test("g", "Definition: gram = (1 / 1000) kg = 1 gram (mass; kg)"); +} + +#[test] +fn test_of_non_substance() { + test("mass of 1kg", "Not defined: mass of <1 kilogram (mass)>"); +} + +#[test] +fn test_mul_not_defined() { + test( + "#2018-10-03# * kg", + "Operation is not defined: <1 (dimensionless)> * <2018-10-03 00:00:00 +00:00>", + ); +} + +#[test] +fn test_log_base_with_dimension() { + test( + "log(10, 5m)", + "Base must be dimensionless: log(10 (dimensionless), 5 meter (length))", + ); +} + +#[test] +fn test_hypot_dimension_mismatch() { + test( + "hypot(3s, 4m)", + "Arguments to hypot must have matching dimensionality: \ + hypot(3 second (time), 4 meter (length))", + ); +} + +#[test] +fn test_radix() { + test("0xff", "255 (dimensionless)"); + test( + "0off", + "Expected term, got ", + ); + test("0b101010", "42 (dimensionless)"); + test("0o10lux", "8 lux (illuminance)"); +} + +#[test] +fn test_comments() { + test("1 // *3", "1 (dimensionless)"); + test("1 + /*2*/ 3", "4 (dimensionless)"); + test("1 + /*2", "Expected term, got "); +} + +#[test] +fn test_leading_dot() { + test(".12345Ee3", "123.45 (dimensionless)"); +} + +#[test] +fn test_underscores_in_number() { + test("123_456\u{2009}789", "123456789 (dimensionless)"); +} + +#[test] +fn test_date_input() { + test_starts_with( + "#2018-10-04T09:13:25.123 +2:00#", + "2018-10-04 11:13:25.123 +02:00", + ); +} + +#[test] +fn test_unicode_arrow() { + test("pound → kg", "approx. 0.4535923 kilogram (mass)"); +} + +#[test] +fn test_attributes() { + test( + "roman mile", + "Definition: romanmile = 8 stadia = 1.48 kilometer (length; m)", + ); + test( + "romanmile", + "Definition: romanmile = 8 stadia = 1.48 kilometer (length; m)", + ); + test( + "international", + "Attribute must be followed by ident, got eof", + ); +} + +#[test] +fn test_search() { + test( + "search cm", + "Search results: CM¥ (money), cmil (area), cminv (energy), \ + cmcapacitance (capacitance), sccm (power)", + ); +} + +#[test] +fn test_digits() { + test( + "ln(1234) -> digits 100", + "approx. 7.11801620446533345187845043255947530269622802734375 (dimensionless)", + ); +} + +#[test] +fn test_escapes() { + test("'ab\\'cd\\n\\t'", "1 ab'cd\n\t (ab'cd\n\t)"); + test("'x\\a'", "Expected term, got "); +} + +#[test] +fn test_missing_bracket() { + test("(1+2", "Expected `)`, got eof"); +} + +#[test] +fn test_to_timezone() { + test_starts_with( + "#2000-01-01 12:46 Asia/Tokyo# -> GMT", + "2000-01-01 03:46:00 GMT", + ); +} + +#[test] +fn test_missing_base() { + test("3 -> base", "Expected decimal base, got eof"); +} + +#[test] +fn test_date_difference() { + test_starts_with("now - (now - 3days)", "2 day, 23 hour, 59 minute, 59.99"); +} + +#[test] +fn test_date_time_formats() { + test_starts_with("#1970-01-01 10:30 GMT#", "1970-01-01 10:30:00 GMT"); + test_starts_with("(now-#10:30#) - (now-#11:30#)", "59 minute, 59.99"); +} + +#[test] +fn test_no_calls_on_rhs() { + test( + "1 -> sin(2)", + "Calls are not allowed in the right hand side of conversions", + ); +} + +#[test] +fn test_conversion_to_list() { + test( + "ly -> teram,Gm,Mm,km,m", + "9.46 kiloteram, 730 gigameter, 472 megameter, 580 kilometer, 800 meter (length)", + ); + test( + "1 -> m, hour", + "Units in unit list must conform: <1 meter (length)> ; <3.6 kilosecond (time)>", + ); + test( + "1g -> m, cm", + "Conformance error: 1 gram (mass) != 1 meter (length)\n\ + Suggestions: divide left side by linear_density, multiply right side by linear_density", + ); +} + +#[test] +fn test_definition_with_doc() { + test( + "mass", + "Definition: kilogram = base unit of mass. Equal to the mass of the \ + international prototype of the kilogram. 3rd CGPM (1901, CR, 70).", + ); +} + +#[test] +fn test_try_decode_fail() { + test( + "#abc#", + "Most likely pattern `--monthnum-day[ hour24:min[:sec][ offset]]` failed: \ + Expected `-`, got `abc`", + ) +} + +#[test] +fn test_formula() { + test( + "methane=CH4", + "CH4: molar_mass = 0.01604276 kilogram / mole", + ); + test( + "NaCl", + "NaCl: molar_mass = approx. 0.05844246 kilogram / mole", + ); + test( + "C8H10N4O2", + "C8H10N4O2: molar_mass = approx. 0.1941931 kilogram / mole", + ); + test("C60", "C60: molar_mass = 0.72066 kilogram / mole"); +}