Implement suffixes for celsius and fahrenheit

This commit is contained in:
Tiffany Bennett 2016-08-14 16:44:26 -04:00
parent a7e847781b
commit aec9dad521
3 changed files with 208 additions and 96 deletions

View file

@ -247,7 +247,7 @@ impl Context {
/// Evaluates an expression to compute its value, *excluding* `->`
/// conversions.
pub fn eval(&self, expr: &::unit_defs::Expr) -> Result<Value, String> {
use unit_defs::Expr;
use unit_defs::{Expr, SuffixOp};
macro_rules! operator {
($left:ident $op:ident $opname:tt $right:ident) => {{
@ -272,12 +272,43 @@ impl Context {
Expr::Date(ref date) => date::try_decode(date, self).map(Value::DateTime),
Expr::Neg(ref expr) => self.eval(&**expr).and_then(|v| -&v),
Expr::Plus(ref expr) => self.eval(&**expr),
Expr::DegC => Err(format!("°C is an operator")),
Expr::DegF => Err(format!("°F is an operator")),
Expr::Frac(ref left, ref right) => operator!(left div / right),
Expr::Add(ref left, ref right) => operator!(left add + right),
Expr::Sub(ref left, ref right) => operator!(left sub - right),
Expr::Pow(ref left, ref right) => operator!(left pow ^ right),
Expr::Suffix(SuffixOp::Celsius, ref left) => {
let left = try!(self.eval(&**left));
let left = match left {
Value::Number(left) => left,
_ => return Err(format!("Left-hand side of celsius literal must be number"))
};
if left.1 != BTreeMap::new() {
Err(format!("Left-hand side of celsius literal must be dimensionless"))
} else {
let left = (&left * &self.lookup("kelvin").expect("Missing kelvin unit")).unwrap();
Ok(Value::Number((&left + &self.lookup("zerocelsius")
.expect("Missing zerocelsius constant")).unwrap()))
}
},
Expr::Suffix(SuffixOp::Fahrenheit, ref left) => {
let left = try!(self.eval(&**left));
let left = match left {
Value::Number(left) => left,
_ => return Err(format!("Left-hand side of fahrenheit literal must be number"))
};
if left.1 != BTreeMap::new() {
Err(format!("Left-hand side of fahrenheit literal must be dimensionless"))
} else {
let left = (&left * &self.lookup("Rankine").expect("Missing rankine unit")).unwrap();
Ok(Value::Number((&left + &self.lookup("zerofahrenheit")
.expect("Missing zerofahrenheit constant")).unwrap()))
}
},
// TODO: A type might not implement * on Number, and this would fail
Expr::Mul(ref args) => args.iter().fold(Ok(Value::Number(Number::one())), |a, b| {
a.and_then(|a| {
@ -354,6 +385,8 @@ impl Context {
},
Expr::Neg(ref v) => self.eval_unit_name(v),
Expr::Plus(ref v) => self.eval_unit_name(v),
Expr::Suffix(_, _) | Expr::DegC | Expr::DegF =>
Err(format!("Temperature conversions must not be compound units")),
Expr::Date(_) => Err(format!("Dates are not allowed in the right hand side of conversions")),
Expr::Convert(_, _) => Err(format!("Conversions are not allowed in the right hand of conversions")),
Expr::Error(ref e) => Err(e.clone()),
@ -364,6 +397,88 @@ impl Context {
pub fn eval_outer(&self, expr: &::unit_defs::Expr) -> Result<String, String> {
use unit_defs::Expr;
let conformance_err = |top: &Number, bottom: &Number| -> String {
use std::io::Write;
let mut buf = vec![];
let width = 12;
let mut topu = top.clone();
topu.0 = Mpq::one();
let mut bottomu = bottom.clone();
bottomu.0 = Mpq::one();
let left = topu.show(self);
let right = bottomu.show(self);
if self.short_output {
writeln!(buf, "Conformance error [ {left} || {right} ]",
left=left, right=right).unwrap();
} else {
writeln!(buf, concat!("Conformance error\n",
"{:>width$}: {left}\n",
"{:>width$}: {right}"),
"Left side", "Right side", left=left, right=right, width=width).unwrap();
}
let diff = (&topu / &bottomu).unwrap();
let (recip, desc) = self.describe_unit(&diff.invert());
let word = match recip {
false => "multiply",
true => "divide"
};
writeln!(buf, "{:>width$}: {word} left side by {}", "Suggestion",
desc.trim(), width=width, word=word).unwrap();
let (recip, desc) = self.describe_unit(&diff);
let word = match recip {
false => "multiply",
true => "divide"
};
writeln!(buf, "{:>width$} {word} right side by {}", "",
desc.trim(), width=width, word=word).unwrap();
String::from_utf8(buf).unwrap()
};
let show = |raw: &Number, bottom: &Number, bottom_name: BTreeMap<String, isize>| -> String {
let number = raw.show_number_part();
let mut unit_top = vec![];
let mut unit_frac = vec![];
for (name, exp) in bottom_name.into_iter() {
if exp < 0 {
unit_frac.push((name, -exp));
} else {
unit_top.push((name, exp));
}
}
let unit_top = unit_top.into_iter().fold(String::new(), |mut acc, (name, exp)| {
acc.push(' ');
acc.push_str(&*name);
if exp != 1 {
acc.push_str(&*format!("^{}", exp));
}
acc
});
let unit_frac = unit_frac.into_iter().fold(String::new(), |mut acc, (name, exp)| {
if acc.len() > 0 {
acc.push(' ');
}
acc.push_str(&*name);
if exp != 1 {
acc.push_str(&*format!("^{}", exp));
}
acc
});
let unit_frac = if unit_frac.len() > 0 {
format!(" / {}", unit_frac)
} else {
unit_frac
};
let reduced = match self.describe_unit(&bottom) {
(false, v) => v,
(true, v) => format!("1 / {}", v)
};
format!("{number}{unit_top}{unit_frac} ({reduced})",
number=number, unit_top=unit_top,
unit_frac=unit_frac, reduced=reduced)
};
match *expr {
Expr::Convert(ref top, ref bottom) => match (self.eval(&**top), self.eval(&**bottom), self.eval_unit_name(&**bottom)) {
(Ok(top), Ok(bottom), Ok(bottom_name)) => {
@ -372,92 +487,53 @@ impl Context {
_ => return Err(format!("Conversion of non-numbers is not defined"))
};
if top.1 != bottom.1 {
use std::io::Write;
let mut buf = vec![];
let width = 12;
let mut topu = top.clone();
topu.0 = Mpq::one();
let mut bottomu = bottom.clone();
bottomu.0 = Mpq::one();
let left = topu.show(self);
let right = bottomu.show(self);
if self.short_output {
writeln!(buf, "Conformance error [ {left} || {right} ]",
left=left, right=right).unwrap();
} else {
writeln!(buf, concat!("Conformance error\n",
"{:>width$}: {left}\n",
"{:>width$}: {right}"),
"Left side", "Right side", left=left, right=right, width=width).unwrap();
}
let diff = (&topu / &bottomu).unwrap();
let (recip, desc) = self.describe_unit(&diff.invert());
let word = match recip {
false => "multiply",
true => "divide"
};
writeln!(buf, "{:>width$}: {word} left side by {}", "Suggestion",
desc.trim(), width=width, word=word).unwrap();
let (recip, desc) = self.describe_unit(&diff);
let word = match recip {
false => "multiply",
true => "divide"
};
writeln!(buf, "{:>width$} {word} right side by {}", "",
desc.trim(), width=width, word=word).unwrap();
Err(String::from_utf8(buf).unwrap())
Err(conformance_err(&top, &bottom))
} else {
let raw = match &top / &bottom {
Some(raw) => raw,
None => return Err(format!("Division by zero: {} / {}",
top.show(self), bottom.show(self)))
};
let number = raw.show_number_part();
let mut unit_top = vec![];
let mut unit_frac = vec![];
for (name, exp) in bottom_name.into_iter() {
if exp < 0 {
unit_frac.push((name, -exp));
} else {
unit_top.push((name, exp));
}
}
let unit_top = unit_top.into_iter().fold(String::new(), |mut acc, (name, exp)| {
acc.push(' ');
acc.push_str(&*name);
if exp != 1 {
acc.push_str(&*format!("^{}", exp));
}
acc
});
let unit_frac = unit_frac.into_iter().fold(String::new(), |mut acc, (name, exp)| {
if acc.len() > 0 {
acc.push(' ');
}
acc.push_str(&*name);
if exp != 1 {
acc.push_str(&*format!("^{}", exp));
}
acc
});
let unit_frac = if unit_frac.len() > 0 {
format!(" / {}", unit_frac)
} else {
unit_frac
};
let reduced = match self.describe_unit(&bottom) {
(false, v) => v,
(true, v) => format!("1 / {}", v)
};
Ok(format!("{number}{unit_top}{unit_frac} ({reduced})",
number=number, unit_top=unit_top,
unit_frac=unit_frac, reduced=reduced))
Ok(show(&raw, &bottom, bottom_name))
}
},
(Ok(ref top), Err(ref e), _) => match **bottom {
Expr::DegC => {
let top = match *top {
Value::Number(ref num) => num,
_ => return Err(format!("Expected number"))
};
let bottom = self.lookup("kelvin").expect("Unit kelvin missing");
if top.1 != bottom.1 {
Err(conformance_err(&top, &bottom))
} else {
let res = (top - &self.lookup("zerocelsius")
.expect("Unit zerocelsius missing")).unwrap();
let mut name = BTreeMap::new();
name.insert("°C".to_owned(), 1);
Ok(show(&res, &bottom, name))
}
},
Expr::DegF => {
let top = match *top {
Value::Number(ref num) => num,
_ => return Err(format!("Expected number"))
};
let bottom = self.lookup("kelvin").expect("Unit kelvin missing");
if top.1 != bottom.1 {
Err(conformance_err(&top, &bottom))
} else {
let res = (top - &self.lookup("zerofahrenheit")
.expect("Unit zerofahrenheit missing")).unwrap();
let res = (&res / &self.lookup("Rankine").expect("Unit Rankine missing")).unwrap();
let mut name = BTreeMap::new();
name.insert("°F".to_owned(), 1);
Ok(show(&res, &bottom, name))
}
},
_ => Err(e.clone())
},
(Err(e), _, _) => Err(e),
(_, Err(e), _) => Err(e),
(_, _, Err(e)) => Err(e),
},
_ => {

View file

@ -31,6 +31,8 @@ pub enum Token {
DashArrow,
Hash,
ImaginaryUnit,
DegC,
DegF,
Error(String),
}
@ -246,7 +248,11 @@ impl<'a> Iterator for TokenIterator<'a> {
break;
}
}
Token::Ident(buf)
match &*buf {
"degC" | "°C" | "celsius" => Token::DegC,
"degF" | "°F" | "fahrenheit" => Token::DegF,
_ => Token::Ident(buf)
}
}
};
Some(res)
@ -255,6 +261,12 @@ impl<'a> Iterator for TokenIterator<'a> {
pub type Iter<'a> = Peekable<TokenIterator<'a>>;
#[derive(Debug, Clone)]
pub enum SuffixOp {
Celsius,
Fahrenheit,
}
#[derive(Debug, Clone)]
pub enum Expr {
Unit(String),
@ -270,6 +282,9 @@ pub enum Expr {
Plus(Box<Expr>),
Convert(Box<Expr>, Box<Expr>),
Equals(Box<Expr>, Box<Expr>),
Suffix(SuffixOp, Box<Expr>),
DegC,
DegF,
Error(String),
}
@ -345,9 +360,9 @@ fn parse_pow(mut iter: &mut Iter) -> Expr {
fn parse_mul(mut iter: &mut Iter) -> Expr {
let mut terms = vec![parse_pow(iter)];
loop { match *iter.peek().unwrap() {
Token::Equals | Token::Plus | Token::Minus | Token::DashArrow | Token::TriplePipe |
Token::RPar | Token::Newline | Token::Comment(_) | Token::Eof => break,
loop { match iter.peek().cloned().unwrap() {
Token::DegC | Token::DegF | Token::Equals | Token::Plus | Token::Minus | Token::DashArrow |
Token::TriplePipe | Token::RPar | Token::Newline | Token::Comment(_) | Token::Eof => break,
Token::Slash => {
iter.next();
let right = parse_pow(iter);
@ -370,17 +385,32 @@ fn parse_mul(mut iter: &mut Iter) -> Expr {
}
}
fn parse_add(mut iter: &mut Iter) -> Expr {
fn parse_suffix(mut iter: &mut Iter) -> Expr {
let left = parse_mul(iter);
match iter.peek().cloned().unwrap() {
Token::DegC => {
iter.next();
Expr::Suffix(SuffixOp::Celsius, Box::new(left))
},
Token::DegF => {
iter.next();
Expr::Suffix(SuffixOp::Fahrenheit, Box::new(left))
},
_ => left
}
}
fn parse_add(mut iter: &mut Iter) -> Expr {
let left = parse_suffix(iter);
match *iter.peek().unwrap() {
Token::Plus => {
iter.next();
let right = parse_add(iter);
let right = parse_suffix(iter);
Expr::Add(Box::new(left), Box::new(right))
},
Token::Minus => {
iter.next();
let right = parse_add(iter);
let right = parse_suffix(iter);
Expr::Sub(Box::new(left), Box::new(right))
},
_ => left
@ -404,7 +434,11 @@ pub fn parse_expr(mut iter: &mut Iter) -> Expr {
match iter.peek().cloned().unwrap() {
Token::DashArrow => {
iter.next();
let right = parse_eq(iter);
let right = match iter.peek().cloned().unwrap() {
Token::DegC => Expr::DegC,
Token::DegF => Expr::DegF,
_ => parse_eq(iter)
};
Expr::Convert(Box::new(left), Box::new(right))
},
_ => left

View file

@ -1189,13 +1189,13 @@ basispoint := 1/100 percent// Used in finance
// Temperature difference
// The units below are NOT an absolute temperature measurement in Fahrenheit,
// but represents the size of a degree in the specified systems.
degcelsius := K
degreeCelsius := K // Per http://physics.nist.gov/Pubs/SP811/sec04.html#4.2.1.1
//degcelsius := K
//degreeCelsius := K // Per http://physics.nist.gov/Pubs/SP811/sec04.html#4.2.1.1
// This is allowable in the SI under resolutions 3 and 4 of the 13th CGPM:
// http://www.bipm.org/en/CGPM/db/13/3/
degC := K // The *size* of a degree in the Celsius scale.
//degC := K // The *size* of a degree in the Celsius scale.
// This is identical to the size of a Kelvin.
// WARNING: This should only be used when
// you're indicating the *difference* between
@ -1234,11 +1234,11 @@ zerocelsius := 273.15 K // Defined by the 10th CGPM, 1954, Resolution 3;
// and a degree Celsius are the same, but
// the zero point of the Celsius scale is actually
// set to .01 Kelvin below the triple point.
degfahrenheit := 5/9 degC // The *size* of a degree in the Fahrenheit scale.
degreeFahrenheit := degfahrenheit // The *size* of a degree in the Fahrenheit scale.
degF := degfahrenheit // WARNING: These should only be used when
//degfahrenheit := 5/9 kelvin // The *size* of a degree in the Fahrenheit scale.
//degreeFahrenheit := degfahrenheit // The *size* of a degree in the Fahrenheit scale.
//degF := degfahrenheit // WARNING: These should only be used when
// you're indicating the *difference* between
// two temperatures, (say, how much energy to
// raise the temperature of a gram of water by 5
@ -1254,7 +1254,7 @@ degF := degfahrenheit // WARNING: These should only be used when
// to body heat (for reasons unknown).
\u2109 := degfahrenheit // Single Unicode codepoint for
//\u2109 := degfahrenheit // Single Unicode codepoint for
// DEGREE FAHRENHEIT
degreesRankine := 5/9 K
@ -1264,7 +1264,9 @@ degreerankine := degrankine // is at absolute zero.
degR := degrankine
Rankine := degreesrankine
degreaumur := 10/8 degC // The Reaumur scale was used in Europe and
zerofahrenheit := zerocelsius - 32 degR
//degreaumur := 10/8 kelvin // The Reaumur scale was used in Europe and
// particularly in France. It is defined
// to be 0 at the freezing point of water
// and 80 at the boiling point. Reaumur
@ -2857,7 +2859,7 @@ Wh := watt hour
Rvalue := degrankine ft^2 hr / Btu // r-value, U.S. insulation figure
Cvalue := 1/Rvalue // C-value U.S. insulation conductance rating
kvalue := Btu in / (ft^2 hr degF) // k-value, insulation conductance/in thick
kvalue := Btu in / (ft^2 hr degrankine) // k-value, insulation conductance/in thick
Uvalue := 1/Rvalue
europeanUvalue := watt / (m^2 K)
RSI := K m^2 / W // SI insulation figure