Context refactor (#124)

Yet another giant refactor PR. I'm trying to clean up the Context internals. I moved a lot of the fields into a new Registry object with the hopes of making future refactors to it easier.

A lot of things are poorly named, and I've been going through and updating those. Some of these fields I didn't even know what they did until I studied them carefully, since I'd forgotten over time.

This makes some breaking changes to the Defs serialization format, but I didn't touch Unit or Substance which are the only ones that are used by the backend currently.

Some other refactors I did:
- Updated quantities to no longer reference units, only base units and other quantities.
- Added a debug dump option to the CLI for showing the contents of the Context.
- Merged Def::BaseUnit & Canonicalization.
- Merged Def::Prefix & SPrefix. I don't actually know what SPrefix stood for originally (standalone? definitely not short).
- Prefixes are now required to be dimensionless.
- Added missing tests for the Def serialization format.

I'm not entirely done yet though. There's still one dependency on Context from the loader, which is on the eval() method, used by units and substances. I'm still thinking about how I can reduce that coupling.
This commit is contained in:
Tiffany Bennett 2022-04-10 10:37:56 -07:00 committed by GitHub
parent 4c1d138b2a
commit b921d7a68d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 798 additions and 499 deletions

View file

@ -44,6 +44,14 @@ async fn main() -> Result<()> {
.long("config-path")
.help("Prints a path to the config file, then exits")
)
.arg(
Arg::new("dump")
.long("dump")
.help("Generates a file containing the contents of the Context object, then exits")
.takes_value(true)
.default_missing_value("dump.txt")
.hide(true)
)
.arg(
Arg::new("service")
.long("service")
@ -60,6 +68,15 @@ async fn main() -> Result<()> {
color_eyre::install()?;
let config = config::read_config()?;
if let Some(filename) = matches.value_of("dump") {
use std::io::Write;
let ctx = config::load(&config)?;
let mut file = std::fs::File::create(filename)?;
writeln!(&mut file, "{:#?}", ctx)?;
return Ok(());
}
if matches.is_present("config-path") {
println!("{}", config::config_path("config.toml").unwrap().display());
Ok(())

View file

@ -521,23 +521,23 @@ Hz hertz
#
dimensionless ? 1
length ? meter
length ? m
area ? length^2
volume ? length^3
mass ? kilogram
current ? ampere
amount ? mole
mass ? kg
current ? A
amount ? mol
angle ? radian
solid_angle ? steradian
force ? newton
pressure ? pascal
solid_angle ? sr
force ? acceleration mass
pressure ? force / area
stress pascal
charge ? coulomb
capacitance ? farad
resistance ? ohm
conductance ? siemens
inductance ? henry
frequency ? hertz
charge ? A s
capacitance ? charge / electrical_potential
resistance ? electrical_potential / current
conductance ? resistance^-1
inductance ? magnetic_flux / current
frequency ? time^-1
velocity ? length / time
acceleration ? velocity / time
jerk ? acceleration / time
@ -548,15 +548,15 @@ density ? mass / volume
linear_density ? mass / length
viscosity ? pressure time
kinematic_viscosity ? viscosity / density
magnetic_flux ? weber
magnetic_flux_density ? tesla
magnetic_flux ? electrical_potential time
magnetic_flux_density ? magnetic_flux / area
magnetization ? current / length
electrical_potential ? volt
electric_field ? newton / coulomb
electrical_potential ? power / current
electric_field ? force / charge
entropy ? energy / temperature
thermal_inductance ? energy temperature^2 / power^2
permittivity ? farad / meter
permeability ? henry / meter
permittivity ? capacitance / length
permeability ? inductance / length
angular_momentum ? mass area / time
area_density ? mass / area
catalytic_activity ? amount / time
@ -817,7 +817,7 @@ fine 1|1000 # Measure of gold purity
# with "temp".
#
temperature ? kelvin
temperature ? K
temperature_difference kelvin
# In 1741 Anders Celsius introduced a temperature scale with water boiling at
@ -1280,28 +1280,28 @@ ozcu ouncecopper # in circuitboard fabrication
!category radiometric "Radiometric Units"
radiant_energy J # Basic unit of radiation
radiant_energy_density J/m^3
radiant_flux W
spectral_flux_frequency W/Hz
spectral_flux_wavelength ? W/m
radiant_intensity ? W/sr
spectral_intensity_frequency ? W sr^-1 Hz^-1
spectral_intensity_wavelength ? W sr^-1 m^-1
radiance ? W sr^-1 m^-2
spectral_radiance_frequency ? W sr^-1 m^-2 Hz^-1
spectral_radiance_wavelength ? W sr^-1 m^-3
spectral_irradiance_frequency W m^-2 Hz^-1
spectral_irradiance_wavelength ? W/m^3
radiosity W/m^2
spectral_radiosity_frequency W m^-2 Hz^-1
spectral_radiosity_wavelength W m^-3
radiant_exitance W/m^2
spectral_exitance_frequency W m^-2 Hz^-1
spectral_exitance_wavelength W/m^3
radiant_exposure J/m^2
spectral_exposure_frequency ? J m^-2 Hz^-1
spectral_exposure_wavelength J/m^3
radiant_energy energy # Basic unit of radiation
radiant_energy_density energy / volume
radiant_flux power
spectral_flux_frequency power / frequency
spectral_flux_wavelength ? power / length
radiant_intensity ? power / solid_angle
spectral_intensity_frequency ? power / solid_angle frequency
spectral_intensity_wavelength ? power / solid_angle length
radiance ? power / solid_angle area
spectral_radiance_frequency ? power / solid_angle area frequency
spectral_radiance_wavelength ? power / solid_angle volume
spectral_irradiance_frequency power / area frequency
spectral_irradiance_wavelength ? power / volume
radiosity power / area
spectral_radiosity_frequency power / area frequency
spectral_radiosity_wavelength power / volume
radiant_exitance power / area
spectral_exitance_frequency power / area frequency
spectral_exitance_wavelength power / volume
radiant_exposure energy / area
spectral_exposure_frequency ? energy / area frequency
spectral_exposure_wavelength energy / volume
!endcategory
@ -1311,10 +1311,10 @@ spectral_exposure_wavelength J/m^3
!category photometric "Photometric Units"
luminous_intensity ? candela
luminous_flux ? lumen
luminous_energy ? talbot
illuminance ? lux
luminous_intensity ? cd
luminous_flux ? luminous_intensity solid_angle
luminous_energy ? luminous_flux time
illuminance ? luminous_flux / area
candle 1.02 candela # Standard unit for luminous intensity
hefnerunit 0.9 candle # in use before candela
@ -1350,7 +1350,7 @@ skot 1e-3 apostilb # measurements relating to dark adapted
# eyes.
# Luminance measures
luminance ? nit
luminance ? luminous_intensity / area
nit cd/m^2 # Luminance: the intensity per projected
stilb cd / cm^2 # area of an extended luminous source.
@ -1620,7 +1620,7 @@ C_illum C_apex1961
# and a description of how to compute the correction to mean time.
#
time ? second
time ? s
anomalisticyear 365.2596 days # The time between successive
# perihelion passages of the
@ -2982,7 +2982,7 @@ count /pound # For measuring the size of shrimp
# Other units of work, energy, power, etc
#
energy ? joule
energy ? force length
# Calories: energy to raise a gram of water one degree celsius
@ -3023,7 +3023,7 @@ therm UStherm
celsiusheatunit cal lb K / gram K
chu celsiusheatunit
power ? watt
power ? energy / time
# "Apparent" average power in an AC circuit, the product of rms voltage
# and rms current, equal to the true power in watts when voltage and
@ -3061,14 +3061,14 @@ chevalvapeur metrichorsepower
# cross sectional area, t is the time, and L is the length (thickness).
# Thermal conductivity is a material property.
thermal_conductivity ? power / area (temperature_difference/length)
thermal_conductivity ? power / area (temperature/length)
thermal_resistivity ? 1/thermal_conductivity
# Thermal conductance is the rate at which heat flows across a given
# object, so the area and thickness have been fixed. It depends on
# the size of the object and is hence not a material property.
thermal_conductance ? power / temperature_difference
thermal_conductance ? power / temperature
thermal_resistance ? 1/thermal_conductance
# Thermal admittance is the rate of heat flow per area across an

View file

@ -6,6 +6,9 @@ use std::collections::BTreeMap;
use crate::types::{BaseUnit, Dimensionality, Number, Numeric};
/// Breaks down the dimensionality of a unit into a simpler form that
/// uses SI derived units.
///
/// Uses a single-pass algorithm for finding a suitable decomposition. A
/// more robust one would require factorization, but that's expensive.
///
@ -15,10 +18,10 @@ use crate::types::{BaseUnit, Dimensionality, Number, Numeric};
/// complex decompositions.
pub(crate) fn fast_decompose(
value: &Number,
quantities: &BTreeMap<Dimensionality, String>,
derived_units: &BTreeMap<Dimensionality, String>,
) -> Dimensionality {
let mut best = None;
'outer: for (unit, name) in quantities.iter() {
'outer: for (unit, name) in derived_units.iter() {
// make sure we aren't doing something weird like introducing new base units
for (dim, pow) in unit.iter() {
let vpow = value.unit.get(dim).cloned().unwrap_or(0);

View file

@ -67,18 +67,14 @@ pub struct Property {
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum Def {
// Named dimension for backwards compat reasons.
#[serde(rename = "dimension")]
BaseUnit,
Canonicalization {
of: String,
BaseUnit {
#[serde(rename = "longName")]
long_name: Option<String>,
},
Prefix {
expr: ExprString,
},
#[serde(rename = "sprefix")]
SPrefix {
expr: ExprString,
#[serde(rename = "isLong")]
is_long: bool,
},
Unit {
expr: ExprString,
@ -91,6 +87,7 @@ pub enum Def {
properties: Vec<Property>,
},
Category {
#[serde(rename = "displayName")]
display_name: String,
},
Error {

View file

@ -13,12 +13,12 @@ pub(crate) fn search_internal<'a>(
query: &str,
num_results: usize,
) -> Vec<&'a str> {
let dimensions = ctx.dimensions.iter().map(|dim| &dim.id[..]);
let units = ctx.units.keys().map(|name| &name[..]);
let quantities = ctx.quantities.values().map(|name| &name[..]);
let substances = ctx.substances.keys().map(|name| &name[..]);
let base_units = ctx.registry.base_units.iter().map(|dim| &dim.id[..]);
let units = ctx.registry.units.keys().map(|name| &name[..]);
let quantities = ctx.registry.quantities.values().map(|name| &name[..]);
let substances = ctx.registry.substances.keys().map(|name| &name[..]);
let iter = dimensions.chain(units).chain(quantities).chain(substances);
let iter = base_units.chain(units).chain(quantities).chain(substances);
crate::algorithms::search_impl(iter, query, num_results)
}
@ -31,7 +31,7 @@ pub fn search(ctx: &Context, query: &str, num_results: usize) -> SearchReply {
.lookup(name)
.map(|x| x.to_parts(ctx))
.or_else(|| {
if ctx.substances.get(name).is_some() {
if ctx.registry.substances.get(name).is_some() {
Some(NumberParts {
quantity: Some("substance".to_owned()),
..Default::default()

View file

@ -2,35 +2,34 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use super::Registry;
use crate::ast::{DatePattern, Expr, Query};
use crate::output::{ConversionReply, Digits, NotFoundError, NumberParts, QueryError, QueryReply};
use crate::runtime::Substance;
use crate::types::{BaseUnit, BigInt, Dimensionality, Number, Numeric};
use crate::{commands, Value};
use chrono::{DateTime, Local, TimeZone};
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
/// The evaluation context that contains unit definitions.
#[derive(Debug)]
pub struct Context {
pub dimensions: BTreeSet<BaseUnit>,
pub canonicalizations: BTreeMap<String, String>,
pub units: BTreeMap<String, Number>,
pub quantities: BTreeMap<Dimensionality, String>,
pub reverse: BTreeMap<Dimensionality, String>,
pub prefixes: Vec<(String, Number)>,
pub definitions: BTreeMap<String, Expr>,
pub docs: BTreeMap<String, String>,
pub categories: BTreeMap<String, String>,
pub category_names: BTreeMap<String, String>,
pub datepatterns: Vec<Vec<DatePattern>>,
pub substances: BTreeMap<String, Substance>,
pub substance_symbols: BTreeMap<String, String>,
pub temporaries: BTreeMap<String, Number>,
/// Contains all the information about units.
pub registry: Registry,
/// Used only during initialization.
pub(crate) temporaries: BTreeMap<String, Number>,
/// The current time, as set by the caller.
///
/// This is used instead of directly asking the OS for the time
/// since it allows determinism in unit tests, and prevents edge
/// cases like `now - now` being non-zero.
pub now: DateTime<Local>,
pub short_output: bool,
/// Enables the use of chrono-humanize. It can be disabled for unit
/// tests, as well as in wasm builds where the time API panics.
pub use_humanize: bool,
/// Whether to save the previous query result and make it available
/// as the `ans` variable.
pub save_previous_result: bool,
/// The previous query result.
pub previous_result: Option<Number>,
}
@ -45,26 +44,11 @@ impl Context {
/// Creates a new, empty context
pub fn new() -> Context {
Context {
short_output: false,
registry: Registry::default(),
temporaries: BTreeMap::new(),
now: Local.timestamp(0, 0),
use_humanize: true,
save_previous_result: false,
now: Local.timestamp(0, 0),
dimensions: BTreeSet::new(),
prefixes: vec![],
datepatterns: vec![],
canonicalizations: BTreeMap::new(),
units: BTreeMap::new(),
quantities: BTreeMap::new(),
reverse: BTreeMap::new(),
definitions: BTreeMap::new(),
docs: BTreeMap::new(),
categories: BTreeMap::new(),
category_names: BTreeMap::new(),
substances: BTreeMap::new(),
substance_symbols: BTreeMap::new(),
temporaries: BTreeMap::new(),
previous_result: None,
}
}
@ -78,119 +62,25 @@ impl Context {
}
pub fn load_dates(&mut self, mut dates: Vec<Vec<DatePattern>>) {
self.datepatterns.append(&mut dates)
self.registry.datepatterns.append(&mut dates)
}
/// Given a unit name, returns its value if it exists. Supports SI
/// prefixes, plurals, bare dimensions like length, and quantities.
pub fn lookup(&self, name: &str) -> Option<Number> {
fn inner(ctx: &Context, name: &str) -> Option<Number> {
if name == "ans" || name == "ANS" || name == "_" {
return ctx.previous_result.clone();
}
if let Some(v) = ctx.temporaries.get(name).cloned() {
return Some(v);
}
if let Some(k) = ctx.dimensions.get(name) {
return Some(Number::one_unit(k.to_owned()));
}
if let Some(v) = ctx.units.get(name).cloned() {
return Some(v);
}
for (unit, quantity) in &ctx.quantities {
if name == quantity {
return Some(Number {
value: Numeric::one(),
unit: unit.clone(),
});
}
}
None
if name == "ans" || name == "ANS" || name == "_" {
return self.previous_result.clone();
}
if let Some(v) = self.temporaries.get(name).cloned() {
return Some(v);
}
let outer = |name: &str| -> Option<Number> {
if let Some(v) = inner(self, name) {
return Some(v);
}
for &(ref pre, ref value) in &self.prefixes {
if name.starts_with(pre) {
if let Some(v) = inner(self, &name[pre.len()..]) {
return Some((&v * value).unwrap());
}
}
}
None
};
let res = outer(name);
if res.is_some() {
return res;
}
// after so that "ks" is kiloseconds
if name.ends_with('s') {
let name = &name[0..name.len() - 1];
outer(name)
} else {
None
}
self.registry.lookup(name)
}
/// Given a unit name, try to return a canonical name (expanding aliases and such)
pub fn canonicalize(&self, name: &str) -> Option<String> {
fn inner(ctx: &Context, name: &str) -> Option<String> {
if let Some(v) = ctx.canonicalizations.get(name) {
return Some(v.clone());
}
if let Some(k) = ctx.dimensions.get(name) {
return Some((*k.id).clone());
}
if let Some(v) = ctx.definitions.get(name) {
if let Expr::Unit { ref name } = *v {
if let Some(r) = ctx.canonicalize(&*name) {
return Some(r);
} else {
return Some(name.clone());
}
} else {
// we cannot canonicalize it further
return Some(name.to_owned());
}
}
None
}
let outer = |name: &str| -> Option<String> {
if let Some(v) = inner(self, name) {
return Some(v);
}
for &(ref pre, ref val) in &self.prefixes {
if name.starts_with(pre) {
if let Some(v) = inner(self, &name[pre.len()..]) {
let mut pre = pre;
for &(ref other, ref otherval) in &self.prefixes {
if other.len() > pre.len() && val == otherval {
pre = other;
}
}
return Some(format!("{}{}", pre, v));
}
}
}
None
};
let res = outer(name);
if res.is_some() {
return res;
}
if name.ends_with('s') {
let name = &name[0..name.len() - 1];
outer(name)
} else {
None
}
self.registry.canonicalize(name)
}
/// Describes a value's unit, gives true if the unit is reciprocal
@ -213,21 +103,23 @@ impl Context {
unit: value.unit.clone(),
})
.unwrap();
if let Some(name) = self.quantities.get(&value.unit) {
if let Some(name) = self.registry.quantities.get(&value.unit) {
write!(buf, "{}", name).unwrap();
} else if let Some(name) = square.and_then(|square| self.quantities.get(&square.unit)) {
} else if let Some(name) =
square.and_then(|square| self.registry.quantities.get(&square.unit))
{
write!(buf, "{}^2", name).unwrap();
} else if let Some(name) = self.quantities.get(&inverse.unit) {
} else if let Some(name) = self.registry.quantities.get(&inverse.unit) {
recip = true;
write!(buf, "{}", name).unwrap();
} else {
let helper = |dim: &BaseUnit, pow: i64, buf: &mut Vec<u8>| {
let unit = Dimensionality::new_dim(dim.clone(), pow);
if let Some(name) = self.quantities.get(&unit) {
if let Some(name) = self.registry.quantities.get(&unit) {
write!(buf, " {}", name).unwrap();
} else {
let unit = Dimensionality::base_unit(dim.clone());
if let Some(name) = self.quantities.get(&unit) {
if let Some(name) = self.registry.quantities.get(&unit) {
write!(buf, " {}", name).unwrap();
} else {
write!(buf, " '{}'", dim).unwrap();
@ -256,7 +148,7 @@ impl Context {
}
for (dim, pow) in frac {
let unit = Dimensionality::new_dim(dim.clone(), pow);
if let Some(name) = self.quantities.get(&unit) {
if let Some(name) = self.registry.quantities.get(&unit) {
write!(buf, " {}", name).unwrap();
} else {
helper(dim, pow, &mut buf);

View file

@ -374,6 +374,7 @@ pub fn parse(iter: &mut Iter<'_>) -> Defs {
name,
def: Rc::new(Def::Prefix {
expr: ExprString(expr),
is_long: false,
}),
doc: doc.take(),
category: category.clone(),
@ -381,8 +382,9 @@ pub fn parse(iter: &mut Iter<'_>) -> Defs {
} else {
map.push(DefEntry {
name,
def: Rc::new(Def::SPrefix {
def: Rc::new(Def::Prefix {
expr: ExprString(expr),
is_long: true,
}),
doc: doc.take(),
category: category.clone(),
@ -391,30 +393,22 @@ pub fn parse(iter: &mut Iter<'_>) -> Defs {
} else {
// unit
if let Some(&Token::Bang) = iter.peek() {
// dimension
// base unit
iter.next();
if let Some(Token::Ident(ref long)) = iter.peek().cloned() {
let long_name = if let Some(Token::Ident(ref long)) = iter.peek().cloned() {
iter.next();
map.push(DefEntry {
name: name.clone(),
def: Rc::new(Def::BaseUnit),
doc: doc.take(),
category: category.clone(),
});
map.push(DefEntry {
name: long.clone(),
def: Rc::new(Def::Canonicalization { of: name.clone() }),
doc: doc.take(),
category: category.clone(),
});
Some(long.clone())
} else {
map.push(DefEntry {
name: name.clone(),
def: Rc::new(Def::BaseUnit),
doc: doc.take(),
category: category.clone(),
});
}
None
};
map.push(DefEntry {
name: name.clone(),
def: Rc::new(Def::BaseUnit { long_name }),
doc: doc.take(),
category: category.clone(),
});
} else if let Some(&Token::Question) = iter.peek() {
// quantity
iter.next();

View file

@ -3,40 +3,36 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use super::Context;
use crate::ast::{BinOpExpr, Def, DefEntry, Defs, Expr};
use crate::ast::{BinOpExpr, BinOpType, Def, DefEntry, Defs, Expr, UnaryOpExpr, UnaryOpType};
use crate::runtime::{Properties, Property, Substance, Value};
use crate::types::{BaseUnit, Number, Numeric};
use crate::types::{BaseUnit, Dimensionality, Number, Numeric};
use std::collections::{BTreeMap, BTreeSet};
use std::convert::TryInto;
use std::rc::Rc;
use std::sync::Arc;
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone)]
enum Name {
Unit(Rc<String>),
Prefix(Rc<String>),
Quantity(Rc<String>),
Category(Rc<String>),
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
enum Namespace {
Unit,
Prefix,
Quantity,
Category,
}
impl Name {
fn name(&self) -> String {
match &self {
Name::Unit(ref name)
| Name::Prefix(ref name)
| Name::Quantity(ref name)
| Name::Category(ref name) => (**name).clone(),
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone)]
struct Id {
namespace: Namespace,
name: Rc<String>,
}
struct Resolver {
interned: BTreeSet<Rc<String>>,
input: BTreeMap<Name, Rc<Def>>,
sorted: Vec<Name>,
unmarked: BTreeSet<Name>,
temp_marks: BTreeSet<Name>,
docs: BTreeMap<Name, String>,
categories: BTreeMap<Name, String>,
input: BTreeMap<Id, Rc<Def>>,
sorted: Vec<Id>,
unmarked: BTreeSet<Id>,
temp_marks: BTreeSet<Id>,
docs: BTreeMap<Id, String>,
categories: BTreeMap<Id, String>,
}
impl Resolver {
@ -52,106 +48,221 @@ impl Resolver {
}
}
fn lookup(&mut self, name: &Rc<String>) -> bool {
fn inner(ctx: &mut Resolver, name: &Rc<String>) -> bool {
[Name::Unit, Name::Prefix, Name::Quantity].iter().any(|f| {
let unit = f(name.clone());
ctx.input.contains_key(&unit) && {
ctx.visit(&unit);
true
}
})
}
let mut outer = |name: &Rc<String>| -> bool {
if inner(self, name) {
fn lookup_exact(&mut self, name: &Rc<String>, context: Namespace) -> bool {
let to_check: &[Namespace] = match context {
Namespace::Quantity => &[Namespace::Quantity],
_ => &[Namespace::Unit, Namespace::Prefix, Namespace::Quantity],
};
for namespace in to_check.iter().copied() {
let id = Id {
namespace,
name: name.clone(),
};
if self.input.contains_key(&id) {
self.visit(&id);
return true;
}
let mut found = vec![];
for pre in self.input.keys() {
if let Name::Prefix(ref pre) = *pre {
if (*name).starts_with(&**pre) {
found.push(pre.clone());
}
}
}
found.into_iter().any(|pre| {
inner(self, &Rc::new(name[pre.len()..].to_owned())) && {
let unit = Name::Prefix(pre);
self.visit(&unit);
true
}
})
};
}
false
}
outer(name)
fn lookup_with_prefix(&mut self, name: &Rc<String>, context: Namespace) -> bool {
if self.lookup_exact(name, context) {
return true;
}
let mut found = vec![];
for prefix in self.input.keys() {
if prefix.namespace == Namespace::Prefix && name.starts_with(&prefix.name[..]) {
found.push(prefix.clone());
}
}
found.into_iter().any(|pre| {
self.lookup_exact(&Rc::new(name[pre.name.len()..].to_owned()), context) && {
self.visit(&pre);
true
}
})
}
fn lookup(&mut self, name: &Rc<String>, context: Namespace) -> bool {
self.lookup_with_prefix(name, context)
|| name.ends_with('s') && {
let name = &Rc::new(name[0..name.len() - 1].to_owned());
outer(name)
self.lookup_with_prefix(name, context)
}
}
fn eval(&mut self, expr: &Expr) {
fn eval(&mut self, expr: &Expr, context: Namespace) {
match *expr {
Expr::Unit { ref name } => {
let name = self.intern(name);
self.lookup(&name);
self.lookup(&name, context);
}
Expr::BinOp(BinOpExpr {
ref left,
ref right,
..
}) => {
self.eval(left);
self.eval(right);
self.eval(left, context);
self.eval(right, context);
}
Expr::UnaryOp(ref unaryop) => self.eval(&unaryop.expr),
Expr::Of { ref expr, .. } => self.eval(expr),
Expr::UnaryOp(ref unaryop) => self.eval(&unaryop.expr, context),
Expr::Of { ref expr, .. } => self.eval(expr, context),
Expr::Mul { ref exprs }
| Expr::Call {
args: ref exprs, ..
} => {
for expr in exprs {
self.eval(expr);
self.eval(expr, context);
}
}
_ => (),
}
}
fn visit(&mut self, name: &Name) {
if self.temp_marks.get(name).is_some() {
println!("Unit {:?} has a dependency cycle", name);
fn visit(&mut self, id: &Id) {
if self.temp_marks.get(id).is_some() {
println!("Unit {:?} has a dependency cycle", id);
return;
}
if self.unmarked.get(name).is_some() {
self.temp_marks.insert(name.clone());
if let Some(v) = self.input.get(name).cloned() {
if self.unmarked.get(id).is_some() {
self.temp_marks.insert(id.clone());
if let Some(v) = self.input.get(id).cloned() {
match *v {
Def::Prefix { ref expr }
| Def::SPrefix { ref expr }
Def::Prefix { ref expr, .. }
| Def::Unit { ref expr }
| Def::Quantity { ref expr } => self.eval(expr),
Def::Canonicalization { ref of } => {
self.lookup(&Rc::new(of.clone()));
}
| Def::Quantity { ref expr } => self.eval(expr, id.namespace),
Def::Substance { ref properties, .. } => {
for prop in properties {
self.eval(&prop.input);
self.eval(&prop.output);
self.eval(&prop.input, id.namespace);
self.eval(&prop.output, id.namespace);
}
}
_ => (),
}
}
self.unmarked.remove(name);
self.temp_marks.remove(name);
self.sorted.push(name.clone());
self.unmarked.remove(id);
self.temp_marks.remove(id);
self.sorted.push(id.clone());
}
}
}
fn eval_prefix(prefixes: &BTreeMap<String, Numeric>, expr: &Expr) -> Result<Numeric, String> {
match expr {
Expr::Const { ref value } => Ok(value.clone()),
Expr::Unit { name: ref other } => {
if let Some(value) = prefixes.get(other).cloned() {
Ok(value)
} else {
Err(format!("References non-existent prefix {other}"))
}
}
Expr::BinOp(BinOpExpr {
op: BinOpType::Frac,
left,
right,
}) => {
let left = eval_prefix(prefixes, &*left)?;
let right = eval_prefix(prefixes, &*right)?;
Ok(&left / &right)
}
Expr::BinOp(BinOpExpr {
op: BinOpType::Pow,
left,
right,
}) => {
let left = eval_prefix(prefixes, &*left)?;
let right = eval_prefix(prefixes, &*right)?;
let right: i32 = right
.to_int()
.and_then(|value| value.try_into().ok())
.ok_or_else(|| "Exponent is too big".to_string())?;
Ok(left.pow(right))
}
Expr::UnaryOp(UnaryOpExpr {
op: UnaryOpType::Negative,
expr,
}) => {
let value = eval_prefix(prefixes, &*expr)?;
Ok(&value * &Numeric::from(-1))
}
ref expr => Err(format!("Not a numeric constant: {expr}")),
}
}
fn eval_quantity(
base_units: &BTreeSet<BaseUnit>,
quantities: &BTreeMap<String, Dimensionality>,
expr: &Expr,
) -> Result<Dimensionality, String> {
match *expr {
Expr::Unit { ref name } => {
if let Some(base) = base_units.get(&name[..]) {
Ok(Dimensionality::base_unit(base.clone()))
} else if let Some(quantity) = quantities.get(&name[..]) {
Ok(quantity.clone())
} else {
Err(format!("No quantity or base unit named {}", name))
}
}
Expr::Const { ref value } if *value == Numeric::one() => Ok(Dimensionality::default()),
Expr::Mul { ref exprs } => {
exprs
.iter()
.fold(Ok(Dimensionality::default()), |acc, value| {
let acc = acc?;
let value = eval_quantity(base_units, quantities, value)?;
Ok(&acc * &value)
})
}
Expr::BinOp(BinOpExpr {
op: BinOpType::Frac,
ref left,
ref right,
}) => {
let left = eval_quantity(base_units, quantities, &*left)?;
let right = eval_quantity(base_units, quantities, &*right)?;
Ok(&left / &right)
}
Expr::BinOp(BinOpExpr {
op: BinOpType::Pow,
ref left,
ref right,
}) => {
let left = eval_quantity(base_units, quantities, &*left)?;
match **right {
Expr::Const { ref value } => {
let value = value
.to_int()
.ok_or_else(|| "RHS of `^` is too big".to_string())?;
Ok(left.pow(value))
}
Expr::UnaryOp(UnaryOpExpr {
op: UnaryOpType::Negative,
ref expr,
}) => {
if let Expr::Const { ref value } = **expr {
let value = -value
.to_int()
.ok_or_else(|| "RHS of `^` is too big".to_string())?;
Ok(left.pow(value))
} else {
Err(format!("RHS of `^` must be a constant: {expr}"))
}
}
_ => Err(format!("RHS of `^` must be a constant: {expr}")),
}
}
Expr::UnaryOp(UnaryOpExpr {
op: UnaryOpType::Negative,
ref expr,
}) => Ok(eval_quantity(base_units, quantities, &*expr)?.recip()),
ref expr => Err(format!("Invalid expression in quantity: {expr}")),
}
}
pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
let mut resolver = Resolver {
interned: BTreeSet::new(),
@ -169,31 +280,58 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
category,
} in defs.defs.into_iter()
{
// Base units can have two different names from one Def, handle that here.
if let Def::BaseUnit {
long_name: Some(ref long_name),
} = *def
{
let long_name = resolver.intern(long_name);
resolver.input.insert(
Id {
namespace: Namespace::Unit,
name: long_name,
},
def.clone(),
);
}
let name = resolver.intern(&name);
let unit = match *def {
Def::Prefix { .. } | Def::SPrefix { .. } => Name::Prefix(name),
Def::Quantity { .. } => Name::Quantity(name),
Def::Category { .. } => Name::Category(name),
_ => Name::Unit(name),
let id = match *def {
Def::Prefix { .. } => Id {
namespace: Namespace::Prefix,
name,
},
Def::Quantity { .. } => Id {
namespace: Namespace::Quantity,
name,
},
Def::Category { .. } => Id {
namespace: Namespace::Category,
name,
},
_ => Id {
namespace: Namespace::Unit,
name,
},
};
if let Some(doc) = doc {
resolver.docs.insert(unit.clone(), doc);
resolver.docs.insert(id.clone(), doc);
}
if let Some(category) = category {
resolver.categories.insert(unit.clone(), category);
resolver.categories.insert(id.clone(), category);
}
if resolver.input.insert(unit.clone(), def).is_some() {
let (ty, name) = match unit {
Name::Prefix(ref name) => ("prefixes", name),
Name::Quantity(ref name) => ("quantities", name),
Name::Unit(ref name) => ("units", name),
Name::Category(ref name) => ("category", name),
if resolver.input.insert(id.clone(), def).is_some() {
let namespace = match id.namespace {
Namespace::Prefix => "prefixes",
Namespace::Quantity => "quantities",
Namespace::Unit => "units",
Namespace::Category => "category",
};
if ty != "category" {
println!("warning: multiple {} named {}", ty, name);
if namespace != "category" {
println!("warning: multiple {} named {}", namespace, id.name);
}
}
resolver.unmarked.insert(unit);
resolver.unmarked.insert(id);
}
while let Some(name) = resolver.unmarked.iter().next().cloned() {
@ -206,50 +344,57 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
(name, res)
});
let mut reverse = BTreeSet::new();
reverse.insert("newton");
reverse.insert("pascal");
reverse.insert("joule");
reverse.insert("watt");
reverse.insert("coulomb");
reverse.insert("volt");
reverse.insert("ohm");
reverse.insert("siemens");
reverse.insert("farad");
reverse.insert("weber");
reverse.insert("henry");
reverse.insert("tesla");
reverse.insert("lumen");
reverse.insert("lux");
reverse.insert("gray");
reverse.insert("katal");
let mut decomposition_units = BTreeSet::new();
decomposition_units.insert("newton");
decomposition_units.insert("pascal");
decomposition_units.insert("joule");
decomposition_units.insert("watt");
decomposition_units.insert("coulomb");
decomposition_units.insert("volt");
decomposition_units.insert("ohm");
decomposition_units.insert("siemens");
decomposition_units.insert("farad");
decomposition_units.insert("weber");
decomposition_units.insert("henry");
decomposition_units.insert("tesla");
decomposition_units.insert("lumen");
decomposition_units.insert("lux");
decomposition_units.insert("gray");
decomposition_units.insert("katal");
for (name, def) in udefs {
let name = name.name();
let mut prefix_lookup = BTreeMap::new();
let mut quantities = BTreeMap::new();
for (id, def) in udefs {
let name = id.name.to_string();
match *def {
Def::BaseUnit => {
ctx.dimensions.insert(BaseUnit::new(&*name));
}
Def::Canonicalization { ref of } => {
ctx.canonicalizations.insert(of.clone(), name.clone());
match ctx.lookup(of) {
Some(v) => {
ctx.definitions
.insert(name.clone(), Expr::new_unit(of.clone()));
ctx.units.insert(name.clone(), v);
}
None => {
println!("Canonicalization {} is malformed: {} not found", name, of)
}
Def::BaseUnit { ref long_name } => {
let unit = BaseUnit::new(&*name);
ctx.registry.base_units.insert(unit.clone());
if let Some(long_name) = long_name {
ctx.registry
.base_unit_long_names
.insert(name.clone(), long_name.clone());
ctx.registry
.definitions
.insert(long_name.clone(), Expr::new_unit(name.clone()));
ctx.registry
.units
.insert(long_name.clone(), Number::one_unit(unit));
}
}
Def::Unit { ref expr } => match ctx.eval(expr) {
Ok(Value::Number(v)) => {
if v.value == Numeric::one() && reverse.contains(&*name) {
ctx.reverse.insert(v.unit.clone(), name.clone());
// This is one of the SI derived units, so put it in the map.
if v.value == Numeric::one() && decomposition_units.contains(&*name) {
ctx.registry
.decomposition_units
.insert(v.unit.clone(), name.clone());
}
ctx.definitions.insert(name.clone(), expr.0.clone());
ctx.units.insert(name.clone(), v);
ctx.registry
.definitions
.insert(name.clone(), expr.0.clone());
ctx.registry.units.insert(name.clone(), v);
}
Ok(Value::Substance(sub)) => {
let sub = if sub.properties.name.contains('+') {
@ -257,41 +402,45 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
} else {
sub
};
if ctx.substances.insert(name.clone(), sub).is_some() {
if ctx.registry.substances.insert(name.clone(), sub).is_some() {
println!("Warning: Conflicting substances for {}", name);
}
}
Ok(_) => println!("Unit {} is not a number", name),
Err(e) => println!("Unit {} is malformed: {}", name, e),
},
Def::Prefix { ref expr } => match ctx.eval(expr) {
Ok(Value::Number(v)) => {
ctx.prefixes.push((name.clone(), v));
}
Ok(_) => println!("Prefix {} is not a number", name),
Err(e) => println!("Prefix {} is malformed: {}", name, e),
},
Def::SPrefix { ref expr } => match ctx.eval(expr) {
Ok(Value::Number(v)) => {
ctx.prefixes.push((name.clone(), v.clone()));
ctx.units.insert(name.clone(), v);
}
Ok(_) => println!("Prefix {} is not a number", name),
Err(e) => println!("Prefix {} is malformed: {}", name, e),
},
Def::Quantity { ref expr } => match ctx.eval(expr) {
Ok(Value::Number(v)) => {
let res = ctx.quantities.insert(v.unit, name.clone());
if !ctx.definitions.contains_key(&name) {
ctx.definitions.insert(name.clone(), expr.0.clone());
}
if let Some(old) = res {
println!("Warning: Conflicting quantities {} and {}", name, old);
Def::Prefix { ref expr, is_long } => match eval_prefix(&prefix_lookup, &expr.0) {
Ok(value) => {
prefix_lookup.insert(name.clone(), value.clone());
ctx.registry.prefixes.push((name.clone(), value.clone()));
if is_long {
ctx.registry
.units
.insert(name.clone(), Number::new(value.clone()));
}
}
Ok(_) => println!("Quantity {} is not a number", name),
Err(e) => println!("Quantity {} is malformed: {}", name, e),
Err(err) => println!("Prefix {name}: {err}"),
},
Def::Quantity { ref expr } => {
match eval_quantity(&ctx.registry.base_units, &quantities, &expr.0) {
Ok(dimensionality) => {
quantities.insert(name.clone(), dimensionality.clone());
let res = ctx
.registry
.quantities
.insert(dimensionality.clone(), name.clone());
if !ctx.registry.definitions.contains_key(&name) {
ctx.registry
.definitions
.insert(name.clone(), expr.0.clone());
}
if let Some(old) = res {
println!("Warning: Conflicting quantities {} and {}", name, old);
}
}
Err(err) => println!("Quantity {name}: {err}"),
}
}
Def::Substance {
ref properties,
ref symbol,
@ -370,7 +519,7 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
ctx.temporaries.clear();
match res {
Ok(res) => {
ctx.substances.insert(
ctx.registry.substances.insert(
name.clone(),
Substance {
amount: Number::one(),
@ -381,14 +530,17 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
},
);
if let Some(ref symbol) = symbol {
ctx.substance_symbols.insert(symbol.clone(), name.clone());
ctx.registry
.substance_symbols
.insert(symbol.clone(), name.clone());
}
}
Err(e) => println!("Substance {} is malformed: {}", name, e),
}
}
Def::Category { ref display_name } => {
ctx.category_names
ctx.registry
.category_names
.insert(name.clone(), display_name.clone());
}
Def::Error { ref message } => println!("Def {}: {}", name, message),
@ -396,15 +548,15 @@ pub(crate) fn load_defs(ctx: &mut Context, defs: Defs) {
}
for (name, val) in resolver.docs {
let name = name.name();
if ctx.docs.insert(name.clone(), val).is_some() {
let name = name.name.to_string();
if ctx.registry.docs.insert(name.clone(), val).is_some() {
println!("Doc conflict for {}", name);
}
}
for (name, val) in resolver.categories {
let name = name.name();
if ctx.categories.insert(name.clone(), val).is_some() {
let name = name.name.to_string();
if ctx.registry.categories.insert(name.clone(), val).is_some() {
println!("Category conflict for {}", name);
}
}

View file

@ -5,7 +5,9 @@
mod context;
pub mod gnu_units;
mod load;
mod registry;
pub use context::Context;
pub use registry::Registry;
pub(crate) use load::load_defs;

145
core/src/loader/registry.rs Normal file
View file

@ -0,0 +1,145 @@
use std::collections::{BTreeMap, BTreeSet};
use crate::{
ast::{DatePattern, Expr},
runtime::Substance,
types::{BaseUnit, Dimensionality, Number, Numeric},
};
#[derive(Default, Debug)]
pub struct Registry {
/// Contains the base units, e.g. `kg`, `bit`.
pub base_units: BTreeSet<BaseUnit>,
/// Mappings from short forms of base units to long forms, e.g. `kg` → `kilogram`.
pub base_unit_long_names: BTreeMap<String, String>,
/// Contains numerical values of units.
pub units: BTreeMap<String, Number>,
/// Maps dimensionality to named physical quantities like `energy`.
pub quantities: BTreeMap<Dimensionality, String>,
/// Maps dimensionality to names of SI derived units (newton,
/// pascal, etc.) for showing simplified forms of units.
pub decomposition_units: BTreeMap<Dimensionality, String>,
/// A list of prefixes that can be applied to units, like `kilo`.
pub prefixes: Vec<(String, Numeric)>,
/// Contains the original expressions defining a unit.
pub definitions: BTreeMap<String, Expr>,
/// Contains documentation strings.
pub docs: BTreeMap<String, String>,
/// Maps unit names to category IDs.
pub categories: BTreeMap<String, String>,
/// Maps category IDs to display names.
pub category_names: BTreeMap<String, String>,
/// Used for matching date formats.
pub datepatterns: Vec<Vec<DatePattern>>,
/// Objects or materials that have certain properties.
pub substances: BTreeMap<String, Substance>,
/// Maps elemental names (like `He`) to substance names (`helium`),
/// used for parsing molecular formulas, e.g. `H2O`.
pub substance_symbols: BTreeMap<String, String>,
}
impl Registry {
fn lookup_exact(&self, name: &str) -> Option<Number> {
if let Some(k) = self.base_units.get(name) {
return Some(Number::one_unit(k.to_owned()));
}
if let Some(v) = self.units.get(name).cloned() {
return Some(v);
}
for (unit, quantity) in &self.quantities {
if name == quantity {
return Some(Number {
value: Numeric::one(),
unit: unit.clone(),
});
}
}
None
}
fn lookup_with_prefix(&self, name: &str) -> Option<Number> {
if let Some(v) = self.lookup_exact(name) {
return Some(v);
}
for &(ref pre, ref value) in &self.prefixes {
if name.starts_with(pre) {
if let Some(v) = self.lookup_exact(&name[pre.len()..]) {
return Some((&v * &Number::new(value.clone())).unwrap());
}
}
}
None
}
pub(crate) fn lookup(&self, name: &str) -> Option<Number> {
let res = self.lookup_with_prefix(name);
if res.is_some() {
return res;
}
// Check for plurals, but only do this after exhausting every
// other possibility, so that `ks` is kiloseconds instead of
// kelvin.
if let Some(name) = name.strip_suffix('s') {
self.lookup_with_prefix(name)
} else {
None
}
}
fn canonicalize_exact(&self, name: &str) -> Option<String> {
if let Some(v) = self.base_unit_long_names.get(name) {
return Some(v.clone());
}
if let Some(base_unit) = self.base_units.get(name) {
return Some(base_unit.to_string());
}
if let Some(expr) = self.definitions.get(name) {
if let Expr::Unit { ref name } = *expr {
if let Some(canonicalized) = self.canonicalize(&*name) {
return Some(canonicalized);
} else {
return Some(name.clone());
}
} else {
// we cannot canonicalize it further
return Some(name.to_owned());
}
}
None
}
fn canonicalize_with_prefix(&self, name: &str) -> Option<String> {
if let Some(v) = self.canonicalize_exact(name) {
return Some(v);
}
for &(ref prefix, ref value) in &self.prefixes {
if let Some(name) = name.strip_prefix(prefix) {
if let Some(canonicalized) = self.canonicalize_exact(name) {
let mut prefix = prefix;
for &(ref other, ref otherval) in &self.prefixes {
if other.len() > prefix.len() && value == otherval {
prefix = other;
}
}
return Some(format!("{}{}", prefix, canonicalized));
}
}
}
None
}
/// Given a unit name, try to return a canonical name (expanding aliases and such)
pub fn canonicalize(&self, name: &str) -> Option<String> {
let res = self.canonicalize_with_prefix(name);
if res.is_some() {
return res;
}
if let Some(name) = name.strip_suffix('s') {
self.canonicalize_with_prefix(name)
} else {
None
}
}
}

View file

@ -357,7 +357,7 @@ fn attempt(
pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTime, String> {
let mut best = None;
for pat in &context.datepatterns {
for pat in &context.registry.datepatterns {
match attempt(context.now, date, pat) {
Ok(datetime) => return Ok(datetime),
Err((e, c)) => {

View file

@ -29,15 +29,32 @@ pub(crate) fn eval_expr(ctx: &Context, expr: &Expr) -> Result<Value, QueryError>
Expr::Unit { ref name } => ctx
.lookup(name)
.map(Value::Number)
.or_else(|| ctx.substances.get(name).cloned().map(Value::Substance))
.or_else(|| {
ctx.substance_symbols.get(name).and_then(|full_name| {
ctx.substances.get(full_name).cloned().map(Value::Substance)
})
ctx.registry
.substances
.get(name)
.cloned()
.map(Value::Substance)
})
.or_else(|| {
formula::substance_from_formula(name, &ctx.substance_symbols, &ctx.substances)
.map(Value::Substance)
ctx.registry
.substance_symbols
.get(name)
.and_then(|full_name| {
ctx.registry
.substances
.get(full_name)
.cloned()
.map(Value::Substance)
})
})
.or_else(|| {
formula::substance_from_formula(
name,
&ctx.registry.substance_symbols,
&ctx.registry.substances,
)
.map(Value::Substance)
})
.ok_or_else(|| QueryError::NotFound(ctx.unknown_unit_err(name))),
Expr::Quote { ref string } => Ok(Value::Number(Number::one_unit(BaseUnit::new(string)))),
@ -643,15 +660,15 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
match *expr {
Query::Expr(Expr::Unit { ref name })
if {
let a = ctx.definitions.contains_key(name);
let a = ctx.registry.definitions.contains_key(name);
let b = ctx
.canonicalize(name)
.map(|x| ctx.definitions.contains_key(&*x))
.map(|x| ctx.registry.definitions.contains_key(&*x))
.unwrap_or(false);
let c = ctx.dimensions.contains(&**name);
let c = ctx.registry.base_units.contains(&**name);
let d = ctx
.canonicalize(name)
.map(|x| ctx.dimensions.contains(&*x))
.map(|x| ctx.registry.base_units.contains(&*x))
.unwrap_or(false);
a || b || c || d
} =>
@ -659,22 +676,23 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
let mut name = name.clone();
let mut canon = ctx.canonicalize(&name).unwrap_or_else(|| name.clone());
while let Some(&Expr::Unit { name: ref unit }) = {
ctx.definitions
ctx.registry
.definitions
.get(&name)
.or_else(|| ctx.definitions.get(&*canon))
.or_else(|| ctx.registry.definitions.get(&*canon))
} {
if ctx.dimensions.contains(&*name) {
if ctx.registry.base_units.contains(&*name) {
break;
}
let unit_canon = ctx.canonicalize(unit).unwrap_or_else(|| unit.clone());
if ctx.dimensions.contains(&**unit) {
if ctx.registry.base_units.contains(&**unit) {
name = unit.clone();
canon = unit_canon;
break;
}
if ctx.definitions.get(unit).is_none() {
if ctx.definitions.get(&unit_canon).is_none() {
if !ctx.dimensions.contains(&**unit) {
if ctx.registry.definitions.get(unit).is_none() {
if ctx.registry.definitions.get(&unit_canon).is_none() {
if !ctx.registry.base_units.contains(&**unit) {
break;
} else {
assert!(name != *unit || canon != unit_canon);
@ -693,7 +711,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
canon = unit_canon.clone();
}
}
let (def, def_expr, res) = if ctx.dimensions.contains(&*name) {
let (def, def_expr, res) = if ctx.registry.base_units.contains(&*name) {
let parts = ctx
.lookup(&name)
.expect("Lookup of base unit failed")
@ -705,7 +723,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
};
(Some(def), None, None)
} else {
let def = ctx.definitions.get(&name);
let def = ctx.registry.definitions.get(&name);
(
def.as_ref().map(|x| x.to_string()),
def,
@ -717,7 +735,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
def,
def_expr: def_expr.as_ref().map(|x| ExprReply::from(*x)),
value: res,
doc: ctx.docs.get(&name).cloned(),
doc: ctx.registry.docs.get(&name).cloned(),
})))
}
Query::Convert(ref top, Conversion::None, Some(base), digits) => {
@ -853,7 +871,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
.map(|list| {
QueryReply::UnitList(UnitListReply {
rest: NumberParts {
quantity: ctx.quantities.get(&top.unit).cloned(),
quantity: ctx.registry.quantities.get(&top.unit).cloned(),
..Default::default()
},
list,
@ -945,7 +963,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
Query::Factorize(ref expr) => {
let mut val = None;
if let Expr::Unit { ref name } = *expr {
for (u, k) in &ctx.quantities {
for (u, k) in &ctx.registry.quantities {
if name == k {
val = Some(Number {
value: Numeric::one(),
@ -971,6 +989,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
Some(val) => val,
};
let quantities = ctx
.registry
.quantities
.iter()
.map(|(a, b)| (a.clone(), Rc::new(b.clone())))
@ -995,7 +1014,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
Query::UnitsFor(ref expr) => {
let mut val = None;
if let Expr::Unit { ref name } = *expr {
for (u, k) in &ctx.quantities {
for (u, k) in &ctx.registry.quantities {
if name == k {
val = Some(Number {
value: Numeric::one(),
@ -1022,11 +1041,11 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
};
let dim_name;
let mut out = vec![];
for (name, unit) in ctx.units.iter() {
if let Some(&Expr::Unit { .. }) = ctx.definitions.get(name) {
for (name, unit) in ctx.registry.units.iter() {
if let Some(&Expr::Unit { .. }) = ctx.registry.definitions.get(name) {
continue;
}
let category = ctx.categories.get(name);
let category = ctx.registry.categories.get(name);
if val.unit == unit.unit {
out.push((category, name));
}
@ -1035,7 +1054,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
dim_name = ctx
.canonicalize(dim.as_str())
.unwrap_or_else(|| dim.to_string());
let category = ctx.categories.get(&dim_name);
let category = ctx.registry.categories.get(&dim_name);
out.push((category, &dim_name));
}
out.sort_by(|&(ref c1, ref n1), &(ref c2, ref n2)| {
@ -1053,7 +1072,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
for (category, name) in out {
if category != cur_cat {
if !cur.is_empty() {
let cat_name = cur_cat.and_then(|x| ctx.category_names.get(x));
let cat_name = cur_cat.and_then(|x| ctx.registry.category_names.get(x));
categories.push(UnitsInCategory {
category: cat_name.map(ToOwned::to_owned),
units: cur.drain(..).collect(),
@ -1064,7 +1083,7 @@ pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, Quer
cur.push(name.clone());
}
if !cur.is_empty() {
let cat_name = cur_cat.and_then(|x| ctx.category_names.get(x));
let cat_name = cur_cat.and_then(|x| ctx.registry.category_names.get(x));
categories.push(UnitsInCategory {
category: cat_name.map(ToOwned::to_owned),
units: cur,

View file

@ -127,7 +127,7 @@ impl Substance {
if self.amount.dimless() {
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
doc: context.registry.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: self
.properties
@ -254,7 +254,7 @@ impl Substance {
};
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
doc: context.registry.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: once(Ok(Some(amount)))
.chain(self.properties.properties.iter().map(func))
@ -270,7 +270,7 @@ impl Substance {
if self.amount.dimless() {
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
doc: context.registry.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: self
.properties
@ -346,7 +346,7 @@ impl Substance {
};
Ok(SubstanceReply {
name: self.properties.name.clone(),
doc: context.docs.get(&self.properties.name).cloned(),
doc: context.registry.docs.get(&self.properties.name).cloned(),
amount: self.amount.to_parts(context),
properties: once(Ok(Some(amount)))
.chain(self.properties.properties.iter().map(func))

View file

@ -1,3 +1,5 @@
use crate::algorithms::btree_merge;
use super::BaseUnit;
use serde_derive::{Deserialize, Serialize};
use std::{
@ -51,6 +53,44 @@ impl Dimensionality {
pub fn is_dimensionless(&self) -> bool {
self.dims.is_empty()
}
pub fn recip(mut self) -> Dimensionality {
for (_, power) in self.dims.iter_mut() {
*power *= -1;
}
self
}
pub fn pow(mut self, exp: i64) -> Dimensionality {
for (_, power) in self.dims.iter_mut() {
*power *= exp;
}
self
}
}
impl<'a> ops::Mul for &'a Dimensionality {
type Output = Dimensionality;
fn mul(self, rhs: Self) -> Self::Output {
let dims = btree_merge(&self.dims, &rhs.dims, |a, b| {
if a + b != 0 {
Some(a + b)
} else {
None
}
});
Dimensionality { dims }
}
}
#[allow(clippy::suspicious_arithmetic_impl)]
impl<'a> ops::Div for &'a Dimensionality {
type Output = Dimensionality;
fn div(self, rhs: Self) -> Self::Output {
self * &rhs.clone().recip()
}
}
/////////////////////////////////////////
@ -85,9 +125,3 @@ impl IntoIterator for Dimensionality {
self.dims.into_iter()
}
}
impl From<Map> for Dimensionality {
fn from(dims: Map) -> Self {
Dimensionality { dims }
}
}

View file

@ -194,15 +194,15 @@ impl Number {
} else {
(self.value.clone(), (orig.0.clone(), orig.1))
};
for &(ref p, ref v) in &context.prefixes {
for &(ref p, ref v) in &context.registry.prefixes {
if !prefixes.contains(&**p) {
continue;
}
let abs = val.abs();
if abs >= v.value.pow(orig.1 as i32)
&& abs < (&v.value * &Numeric::from(1000)).pow(orig.1 as i32)
if abs >= v.pow(orig.1 as i32)
&& abs < (v * &Numeric::from(1000)).pow(orig.1 as i32)
{
let res = &val / &v.value.pow(orig.1 as i32);
let res = &val / &v.pow(orig.1 as i32);
// tonne special case
let unit = if &**(orig.0).id == "gram" && p == "mega" {
"tonne".to_string()
@ -227,17 +227,22 @@ impl Number {
let value = self.prettify(context);
let (exact, approx) = value.numeric_value(10, Digits::Default);
let quantity = context.quantities.get(&self.unit).cloned().or_else(|| {
if let Some((unit, power)) = self.unit.as_single() {
if power == 1 {
Some(unit.to_string())
let quantity = context
.registry
.quantities
.get(&self.unit)
.cloned()
.or_else(|| {
if let Some((unit, power)) = self.unit.as_single() {
if power == 1 {
Some(unit.to_string())
} else {
Some(format!("{}^{}", unit, power))
}
} else {
Some(format!("{}^{}", unit, power))
None
}
} else {
None
}
});
});
NumberParts {
raw_value: Some(self.clone()),
@ -294,13 +299,14 @@ impl Number {
}
fn pretty_unit(&self, context: &Context) -> Dimensionality {
let pretty = crate::algorithms::fast_decompose(self, &context.reverse);
let pretty = crate::algorithms::fast_decompose(self, &context.registry.decomposition_units);
pretty
.into_iter()
.map(|(k, p)| {
(
context
.canonicalizations
.registry
.base_unit_long_names
.get(&*k.id)
.map(|x| BaseUnit::new(x))
.unwrap_or(k),
@ -375,18 +381,10 @@ impl<'a> Neg for &'a Number {
impl<'a, 'b> Mul<&'b Number> for &'a Number {
type Output = Option<Number>;
#[allow(clippy::suspicious_arithmetic_impl)]
fn mul(self, other: &Number) -> Self::Output {
let val = crate::algorithms::btree_merge(&self.unit, &other.unit, |a, b| {
if a + b != 0 {
Some(a + b)
} else {
None
}
});
Some(Number {
value: &self.value * &other.value,
unit: val.into(),
unit: &self.unit * &other.unit,
})
}
}

View file

@ -72,15 +72,16 @@ fn check_defs() {
})
);
// Prefix and SPrefix.
// Prefixes
assert_json_eq!(
serde_json::to_value([
DefEntry::new(
"kilo",
None,
None,
Def::SPrefix {
expr: ExprString(expr("1000"))
Def::Prefix {
expr: ExprString(expr("1000")),
is_long: true,
},
),
DefEntry::new(
@ -88,7 +89,8 @@ fn check_defs() {
None,
None,
Def::Prefix {
expr: ExprString(expr("kilo"))
expr: ExprString(expr("kilo")),
is_long: false,
}
)
])
@ -98,7 +100,8 @@ fn check_defs() {
"name": "kilo",
"doc": null,
"category": null,
"type": "sprefix",
"type": "prefix",
"isLong": true,
"expr": "1000"
},
{
@ -106,6 +109,7 @@ fn check_defs() {
"doc": null,
"category": null,
"type": "prefix",
"isLong": false,
"expr": "kilo"
}
])
@ -113,33 +117,46 @@ fn check_defs() {
// Base units.
assert_json_eq!(
serde_json::to_value([
DefEntry::new("m", Some("base unit of length"), None, Def::BaseUnit),
DefEntry::new(
"meter",
None,
None,
Def::Canonicalization { of: "m".to_owned() }
)
])
serde_json::to_value([DefEntry::new(
"m",
Some("base unit of length"),
None,
Def::BaseUnit {
long_name: Some("meter".to_owned())
}
),])
.unwrap(),
json!([
{
"name": "m",
"longName": "meter",
"doc": "base unit of length",
"category": null,
"type": "dimension"
},
{
"name": "meter",
"doc": null,
"category": null,
"type": "canonicalization",
"of": "m"
"type": "baseUnit"
}
])
);
// Quantities
assert_json_eq!(
serde_json::to_value(DefEntry::new(
"watt",
Some("SI derived unit for power"),
None,
Def::Quantity {
expr: ExprString(expr("energy / time"))
}
))
.unwrap(),
json!({
"name": "watt",
"doc": "SI derived unit for power",
"category": null,
"type": "quantity",
"expr": "energy / time",
})
);
// Substances
assert_json_eq!(
serde_json::to_value(DefEntry::new(
@ -175,4 +192,44 @@ fn check_defs() {
}]
})
);
// Categories
assert_json_eq!(
serde_json::to_value(DefEntry::new(
"cool_beans",
Some("Units that are cool beans."),
None,
Def::Category {
display_name: "Cool Beans".to_owned(),
}
))
.unwrap(),
json!({
"name": "cool_beans",
"doc": "Units that are cool beans.",
"category": null,
"type": "category",
"displayName": "Cool Beans",
})
);
// Errors
assert_json_eq!(
serde_json::to_value(DefEntry::new(
"foo",
Some("Definition of foo"),
None,
Def::Error {
message: "Syntax error".to_owned()
}
))
.unwrap(),
json!({
"name": "foo",
"doc": "Definition of foo",
"category": null,
"type": "error",
"message": "Syntax error",
})
);
}

View file

@ -7,7 +7,7 @@ use rink_core::*;
#[test]
fn canonicalizations() {
let ctx = simple_context().unwrap();
for (name, value) in &ctx.units {
for (name, value) in &ctx.registry.units {
let canon = match ctx.canonicalize(&*name) {
Some(x) => x,
None => continue,

View file

@ -5,22 +5,14 @@ export interface DefBase {
}
export interface BaseUnit extends DefBase {
type: "dimension";
}
export interface Canonicalization extends DefBase {
type: "canonicalization";
of: string;
type: "baseUnit";
longName: string | null;
}
export interface Prefix extends DefBase {
type: "prefix";
expr: string;
}
export interface SPrefix extends DefBase {
type: "sprefix";
expr: string;
isLong: boolean;
}
export interface Unit extends DefBase {
@ -60,9 +52,7 @@ export interface Error extends DefBase {
export type Def =
| BaseUnit
| Canonicalization
| Prefix
| SPrefix
| Unit
| Quantity
| Substance

View file

@ -1,4 +1,3 @@
import { Def } from "./defs";
import { QueryResult } from "./reply";
export interface Query {