mirror of
https://github.com/tiffany352/rink-rs
synced 2024-11-10 13:44:15 +00:00
commit
8f7642efe9
8 changed files with 816 additions and 59 deletions
2
.codecov.yml
Normal file
2
.codecov.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
ignore:
|
||||
- "src/bin"
|
36
.travis.yml
Normal file
36
.travis.yml
Normal file
|
@ -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"
|
24
src/ast.rs
24
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<T: ::std::fmt::Display>(e: T, expected: &str) {
|
||||
assert_eq!(format!("{}", e), expected);
|
||||
}
|
||||
|
||||
impl From<i64> 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)");
|
||||
}
|
||||
}
|
||||
|
|
282
src/date.rs
282
src/date.rs
|
@ -119,8 +119,8 @@ pub fn parse_date<I>(
|
|||
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,15 +274,11 @@ impl GenericDateTime {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTime, String> {
|
||||
let mut best = None;
|
||||
for pat in &context.datepatterns {
|
||||
//println!("Tring {:?} against {}", date, show_datepattern(pat));
|
||||
let attempt = || -> Result<GenericDateTime, (String, usize)> {
|
||||
fn attempt(date: &[DateToken], pat: &[DatePattern]) -> Result<GenericDateTime, (String, usize)> {
|
||||
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 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 {}",
|
||||
|
@ -328,8 +324,13 @@ pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTi
|
|||
_ => Err((format!("Failed to construct a useful datetime"), count))
|
||||
}
|
||||
}
|
||||
};
|
||||
match attempt() {
|
||||
}
|
||||
|
||||
pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTime, String> {
|
||||
let mut best = None;
|
||||
for pat in &context.datepatterns {
|
||||
//println!("Tring {:?} against {}", date, show_datepattern(pat));
|
||||
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<DatePattern> {
|
||||
parse_datepattern(&mut s.chars().peekable()).unwrap()
|
||||
}
|
||||
|
||||
fn parse_with_tz(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Parsed, Option<Tz>) {
|
||||
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<DateToken>, 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<Vec<DatePattern>, 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, String>,
|
||||
substances: &BTreeMap<String, Substance>) -> Option<Substance> {
|
||||
|
|
|
@ -545,3 +545,83 @@ pub fn tokens(iter: &mut Iter) -> Vec<Token> {
|
|||
}
|
||||
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\"")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
352
tests/query.rs
352
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",
|
||||
test(
|
||||
"W -> J",
|
||||
"Conformance error: 1 watt (power) != 1 joule (energy)\n\
|
||||
Suggestions: multiply left side by time, multiply right side by frequency");
|
||||
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 <Malformed octal literal: No digits after 0o>",
|
||||
);
|
||||
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 <Expected `*/`, got EOF>");
|
||||
}
|
||||
|
||||
#[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 <Invalid escape sequence \\a>");
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue