Skip to content

Commit

Permalink
Implement more advanced unit simplification
Browse files Browse the repository at this point in the history
Fixes #150
Relates to #247
  • Loading branch information
printfn committed Dec 27, 2023
1 parent 1a4e6b1 commit d85324b
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 13 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

* Change unit simplification and unit aliasing to be simpler and more
consistent. Units like `%` and `million` are now simplified unless you
explicitly convert your result to one of those units.
explicitly convert your result to one of those units. fend will now also
simplify certain combinations of units, such as `volts / ohms` becoming
`amperes`.

For example:
```
Expand All @@ -16,6 +18,8 @@
50%
> 34820000 to million
34.82 million
> (5 volts) / (2 ohms)
2.5 amperes
```
### v1.3.3 (2023-12-08)
Expand Down
2 changes: 1 addition & 1 deletion core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ impl Expr {
pub(crate) fn format<I: Interrupt>(
&self,
attrs: Attrs,
ctx: &crate::Context,
ctx: &mut crate::Context,
int: &I,
) -> Result<String, FendError> {
Ok(match self {
Expand Down
19 changes: 19 additions & 0 deletions core/src/num/bigrat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ impl BigRat {
self.num.try_as_usize(int)
}

pub(crate) fn try_as_i64<I: Interrupt>(mut self, int: &I) -> Result<i64, FendError> {
self = self.simplify(int)?;
if self.den != 1.into() {
return Err(FendError::FractionToInteger);
}
let res = self.num.try_as_usize(int)?;
let res: i64 = res.try_into().map_err(|_| FendError::OutOfRange {
value: Box::new(res),
range: Range {
start: RangeBound::None,
end: RangeBound::Open(Box::new(i64::MAX)),
},
})?;
Ok(match self.sign {
Sign::Positive => res,
Sign::Negative => -res,
})
}

pub(crate) fn into_f64<I: Interrupt>(mut self, int: &I) -> Result<f64, FendError> {
if self.is_definitely_zero() {
return Ok(0.0);
Expand Down
7 changes: 7 additions & 0 deletions core/src/num/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ impl Complex {
self.real.try_as_usize(int)
}

pub(crate) fn try_as_i64<I: Interrupt>(self, int: &I) -> Result<i64, FendError> {
if self.imag != 0.into() {
return Err(FendError::ComplexToInteger);
}
self.real.try_as_i64(int)
}

#[inline]
pub(crate) fn real(&self) -> Real {
self.real.clone()
Expand Down
13 changes: 13 additions & 0 deletions core/src/num/real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ impl Real {
}
}

pub(crate) fn try_as_i64<I: Interrupt>(self, int: &I) -> Result<i64, FendError> {
match self.pattern {
Pattern::Simple(s) => s.try_as_i64(int),
Pattern::Pi(n) => {
if n == 0.into() {
Ok(0)
} else {
Err(FendError::CannotConvertToInteger)
}
}
}
}

pub(crate) fn try_as_usize<I: Interrupt>(self, int: &I) -> Result<usize, FendError> {
match self.pattern {
Pattern::Simple(s) => s.try_as_usize(int),
Expand Down
53 changes: 48 additions & 5 deletions core/src/num/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ use crate::num::dist::Dist;
use crate::num::{Base, FormattingStyle};
use crate::scope::Scope;
use crate::serialize::{deserialize_bool, deserialize_usize, serialize_bool, serialize_usize};
use crate::units::{lookup_default_unit, query_unit_static};
use crate::{ast, ident::Ident};
use crate::{Attrs, Span, SpanKind};
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::ops::Neg;
use std::sync::Arc;
use std::{fmt, io};
Expand Down Expand Up @@ -727,7 +728,12 @@ impl Value {
})
}

pub(crate) fn simplify<I: Interrupt>(self, int: &I) -> Result<Self, FendError> {
pub(crate) fn simplify<I: Interrupt>(
self,
attrs: Attrs,
ctx: &mut crate::Context,
int: &I,
) -> Result<Self, FendError> {
if !self.simplifiable {
return Ok(self);
}
Expand Down Expand Up @@ -815,8 +821,7 @@ impl Value {

// remove units with exponent == 0
res_components.retain(|unit_exponent| unit_exponent.exponent != 0.into());

Ok(Self {
let result = Self {
value: res_value,
unit: Unit {
components: res_components,
Expand All @@ -825,7 +830,23 @@ impl Value {
base: self.base,
format: self.format,
simplifiable: self.simplifiable,
})
};

if result.unit.has_pos_and_neg_base_unit_exponents() {
// try and replace unit with a default one, e.g. `kilogram` or `ampere`
let (hashmap, _) = result.unit.to_hashmap_and_scale(int)?;
let mut base_units = hashmap
.into_iter()
.map(|(k, v)| v.try_as_i64(int).map(|v| format!("{}^{v}", k.name())))
.collect::<Result<Vec<String>, _>>()?;
base_units.sort();
if let Some(new_unit) = lookup_default_unit(&base_units.join(" ")) {
let rhs = query_unit_static(new_unit, attrs, ctx, int)?.expect_num()?;
return result.convert_to(rhs, int);
}
}

Ok(result)
}

pub(crate) fn unit_equal_to(&self, rhs: &str) -> bool {
Expand Down Expand Up @@ -956,6 +977,28 @@ impl Unit {
Ok(Self { components: cs })
}

fn has_pos_and_neg_base_unit_exponents(&self) -> bool {
if self.components.len() <= 1 {
return false;
}

let mut pos = HashSet::new();
let mut neg = HashSet::new();
for comp in &self.components {
let component_sign = comp.exponent > 0.into();
for (base, base_exp) in &comp.unit.base_units {
let base_sign = base_exp > &0.into();
let combined_sign = component_sign == base_sign; // xnor
if combined_sign {
pos.insert(base);
} else {
neg.insert(base);
}
}
}
pos.intersection(&neg).next().is_some()
}

pub(crate) fn equal_to(&self, rhs: &str) -> bool {
if self.components.len() != 1 {
return false;
Expand Down
8 changes: 8 additions & 0 deletions core/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ fn parse_number(input: &[Token]) -> ParseResult<'_> {
fn parse_ident(input: &[Token]) -> ParseResult<'_> {
match parse_token(input)? {
(Token::Ident(ident), remaining) => {
if ident.as_str() == "light" {
if let Ok((ident2, remaining2)) = parse_ident(remaining) {
return Ok((
Expr::Apply(Box::new(Expr::Ident(ident)), Box::new(ident2)),
remaining2,
));
}
}
if let Ok(((), remaining2)) = parse_fixed_symbol(remaining, Symbol::Of) {
let (inner, remaining3) = parse_parens_or_literal(remaining2)?;
Ok((Expr::Of(ident, Box::new(inner)), remaining3))
Expand Down
1 change: 1 addition & 0 deletions core/src/units.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::Attrs;

mod builtin;

pub(crate) use builtin::lookup_default_unit;
pub(crate) use builtin::IMPLICIT_UNIT_MAP;

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
Expand Down
25 changes: 24 additions & 1 deletion core/src/units/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ struct UnitDef {
type UnitTuple = (&'static str, &'static str, &'static str, &'static str);

const BASE_UNITS: &[UnitTuple] = &[
("unitless", "", "=1", ""),
("second", "seconds", "l@!", ""),
("meter", "meters", "l@!", ""),
("kilogram", "kilograms", "l@!", ""),
Expand Down Expand Up @@ -835,6 +834,30 @@ pub(crate) fn query_unit(
None
}

const DEFAULT_UNITS: &[(&str, &str)] = &[
("second^-1", "hertz"),
("kilogram^1 meter^1 second^-2", "newton"),
("kilogram^1 meter^-1 second^-2", "pascal"),
("kilogram^1 meter^2 second^-2", "joule"),
("kilogram^1 meter^2 second^-3", "watt"),
("ohm", "ampere^-2 kilogram meter^2 second^-3"),
("volt", "ampere^-1 kilogram meter^2 second^-3"),
("liter", "meter^3"),
];

pub(crate) fn lookup_default_unit(base_units: &str) -> Option<&str> {
if let Some((_, unit)) = DEFAULT_UNITS.iter().find(|(base, _)| *base == base_units) {
return Some(unit);
}
if let Some((singular, _, _, _)) = BASE_UNITS
.iter()
.find(|(singular, _, _, _)| format!("{singular}^1") == base_units)
{
return Some(singular);
}
None
}

/// used for implicit unit addition, e.g. 5'5 -> 5'5"
pub(crate) const IMPLICIT_UNIT_MAP: &[(&str, &str)] = &[("'", "\""), ("foot", "inches")];

Expand Down
6 changes: 3 additions & 3 deletions core/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ impl Value {
&self,
indent: usize,
attrs: Attrs,
ctx: &crate::Context,
ctx: &mut crate::Context,
int: &I,
) -> Result<String, FendError> {
let mut spans = vec![];
Expand All @@ -335,13 +335,13 @@ impl Value {
indent: usize,
spans: &mut Vec<Span>,
attrs: Attrs,
ctx: &crate::Context,
ctx: &mut crate::Context,
int: &I,
) -> Result<(), FendError> {
match self {
Self::Num(n) => {
n.clone()
.simplify(int)?
.simplify(attrs, ctx, int)?
.format(ctx, int)?
.spans(spans, attrs);
}
Expand Down
14 changes: 12 additions & 2 deletions core/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2194,7 +2194,7 @@ fn units_10() {

#[test]
fn units_11() {
test_eval("1 light year", "1 light year");
test_eval("1 light year", "1 light_year");
}

#[test]
Expand Down Expand Up @@ -2279,7 +2279,7 @@ fn units_33() {

#[test]
fn units_34() {
test_eval("1 light year", "1 light year");
test_eval("0.5 light year", "0.5 light_years");
}

#[test]
Expand Down Expand Up @@ -5803,3 +5803,13 @@ fn oc() {
fn to_million() {
test_eval_simple("5 to million", "0.000005 million");
}

#[test]
fn ohms_law() {
test_eval("(5 volts) / (2 ohms)", "2.5 amperes");
}

#[test]
fn simplification_sec_hz() {
test_eval("c/(145MHz)", "approx. 2.0675341931 meters");
}

0 comments on commit d85324b

Please sign in to comment.