diff --git a/CHANGELOG.md b/CHANGELOG.md index a18b489b..fe6eb910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: ``` @@ -16,6 +18,8 @@ 50% > 34820000 to million 34.82 million + > (5 volts) / (2 ohms) + 2.5 amperes ``` ### v1.3.3 (2023-12-08) diff --git a/core/src/ast.rs b/core/src/ast.rs index 6009699e..b3780d30 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -258,7 +258,7 @@ impl Expr { pub(crate) fn format( &self, attrs: Attrs, - ctx: &crate::Context, + ctx: &mut crate::Context, int: &I, ) -> Result { Ok(match self { diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index 7ca90dbe..9466fbae 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -151,6 +151,25 @@ impl BigRat { self.num.try_as_usize(int) } + pub(crate) fn try_as_i64(mut self, int: &I) -> Result { + 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(mut self, int: &I) -> Result { if self.is_definitely_zero() { return Ok(0.0); diff --git a/core/src/num/complex.rs b/core/src/num/complex.rs index 636fc6ee..531a9b43 100644 --- a/core/src/num/complex.rs +++ b/core/src/num/complex.rs @@ -50,6 +50,13 @@ impl Complex { self.real.try_as_usize(int) } + pub(crate) fn try_as_i64(self, int: &I) -> Result { + 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() diff --git a/core/src/num/real.rs b/core/src/num/real.rs index c6ffb9b2..79cc39fd 100644 --- a/core/src/num/real.rs +++ b/core/src/num/real.rs @@ -118,6 +118,19 @@ impl Real { } } + pub(crate) fn try_as_i64(self, int: &I) -> Result { + 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(self, int: &I) -> Result { match self.pattern { Pattern::Simple(s) => s.try_as_usize(int), diff --git a/core/src/num/unit.rs b/core/src/num/unit.rs index f63fd681..bd0d7486 100644 --- a/core/src/num/unit.rs +++ b/core/src/num/unit.rs @@ -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}; @@ -727,7 +728,12 @@ impl Value { }) } - pub(crate) fn simplify(self, int: &I) -> Result { + pub(crate) fn simplify( + self, + attrs: Attrs, + ctx: &mut crate::Context, + int: &I, + ) -> Result { if !self.simplifiable { return Ok(self); } @@ -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, @@ -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::, _>>()?; + 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 { @@ -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; diff --git a/core/src/parser.rs b/core/src/parser.rs index 89521a55..b552b5dc 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -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)) diff --git a/core/src/units.rs b/core/src/units.rs index e5dfbea7..898a207a 100644 --- a/core/src/units.rs +++ b/core/src/units.rs @@ -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)] diff --git a/core/src/units/builtin.rs b/core/src/units/builtin.rs index d9340315..4813a90d 100644 --- a/core/src/units/builtin.rs +++ b/core/src/units/builtin.rs @@ -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@!", ""), @@ -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")]; diff --git a/core/src/value.rs b/core/src/value.rs index 8cdad64a..3d2a8852 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -318,7 +318,7 @@ impl Value { &self, indent: usize, attrs: Attrs, - ctx: &crate::Context, + ctx: &mut crate::Context, int: &I, ) -> Result { let mut spans = vec![]; @@ -335,13 +335,13 @@ impl Value { indent: usize, spans: &mut Vec, 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); } diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 9b1b91b4..a0046e3a 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -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] @@ -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] @@ -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"); +}