Skip to content

Commit

Permalink
Quantity namespacing (#125)
Browse files Browse the repository at this point in the history
Makes quantities occupy a different namespace from units. This means you can no longer write them in expressions as though they were units, which was confusing.

Fixes #12, which is one of the longest standing bugs in Rink.
  • Loading branch information
tiffany352 committed Apr 10, 2022
1 parent 1ae832a commit 8f57001
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 95 deletions.
30 changes: 15 additions & 15 deletions core/definitions.units
Expand Up @@ -531,7 +531,7 @@ angle ? radian
solid_angle ? sr
force ? acceleration mass
pressure ? force / area
stress pascal
#stress pascal
charge ? A s
capacitance ? charge / electrical_potential
resistance ? electrical_potential / current
Expand Down Expand Up @@ -1280,28 +1280,28 @@ ozcu ouncecopper # in circuitboard fabrication

!category radiometric "Radiometric Units"

radiant_energy energy # Basic unit of radiation
radiant_energy_density energy / volume
radiant_flux power
spectral_flux_frequency power / frequency
# 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_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
# 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
# spectral_exposure_wavelength energy / volume

!endcategory

Expand Down Expand Up @@ -3077,7 +3077,7 @@ thermal_resistance ? 1/thermal_conductance
# of sheets of insulation or cloth that are of specified thickness.

thermal_admittance ? thermal_conductivity / length
thermal_insulance thermal_resistivity length
# thermal_insulance thermal_resistivity length
thermal_insulation ? thermal_resistivity length

Rvalue degR ft^2 hr / btu
Expand Down
3 changes: 1 addition & 2 deletions core/src/commands/search.rs
Expand Up @@ -15,10 +15,9 @@ pub(crate) fn search_internal<'a>(
) -> Vec<&'a str> {
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 = base_units.chain(units).chain(quantities).chain(substances);
let iter = base_units.chain(units).chain(substances);
crate::algorithms::search_impl(iter, query, num_results)
}

Expand Down
16 changes: 7 additions & 9 deletions core/src/loader/registry.rs
Expand Up @@ -46,14 +46,6 @@ impl Registry {
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
}

Expand Down Expand Up @@ -129,7 +121,13 @@ impl Registry {
None
}

/// Given a unit name, try to return a canonical name (expanding aliases and such)
/// Given a unit name, tries to find a canonical name for it.
///
/// # Examples
///
/// * `kg` -> `kilogram` (base units are converted to long name)
/// * `mm` -> `millimeter` (prefixes are converted to long form)
/// * `micron` -> `micrometer` (aliases are expanded)
pub fn canonicalize(&self, name: &str) -> Option<String> {
let res = self.canonicalize_with_prefix(name);
if res.is_some() {
Expand Down
179 changes: 110 additions & 69 deletions core/src/runtime/eval.rs
Expand Up @@ -656,86 +656,127 @@ fn to_list(ctx: &Context, top: &Number, list: &[&str]) -> Result<Vec<NumberParts
.collect())
}

pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, QueryError> {
match *expr {
Query::Expr(Expr::Unit { ref name })
if {
let a = ctx.registry.definitions.contains_key(name);
let b = ctx
.canonicalize(name)
.map(|x| ctx.registry.definitions.contains_key(&*x))
.unwrap_or(false);
let c = ctx.registry.base_units.contains(&**name);
let d = ctx
.canonicalize(name)
.map(|x| ctx.registry.base_units.contains(&*x))
.unwrap_or(false);
a || b || c || d
} =>
{
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.registry
.definitions
.get(&name)
.or_else(|| ctx.registry.definitions.get(&*canon))
} {
if ctx.registry.base_units.contains(&*name) {
break;
}
let unit_canon = ctx.canonicalize(unit).unwrap_or_else(|| unit.clone());
if ctx.registry.base_units.contains(&**unit) {
name = unit.clone();
canon = unit_canon;
/// Returns true if this unit has a definition that can be shown. Units
/// with prefixes can't be.
fn can_show_definition(ctx: &Context, name: &str) -> bool {
if ctx.registry.definitions.contains_key(name) {
return true;
}

if ctx.registry.base_units.contains(name) {
return true;
}

if let Some(name) = ctx.canonicalize(name) {
// Canonicalizing doesn't always result in a unit that actually exists, because it can return units that still have a prefix, e.g. micrometer.
if ctx.registry.definitions.contains_key(&name) {
return true;
}
if ctx.registry.base_units.contains(&name[..]) {
return true;
}
}

false
}

/// Recursively expands aliases to find the canonical version of the
/// unit, so that its most fundamental definition can be shown.
///
/// # Examples
///
/// - `ft` -> (`foot`, `foot`)
fn expand_aliases(ctx: &Context, name: &str) -> (String, String) {
let mut name = name.to_owned();
let mut canon = ctx.canonicalize(&name).unwrap_or_else(|| name.clone());

while let Some(&Expr::Unit { name: ref unit }) = {
ctx.registry
.definitions
.get(&name)
.or_else(|| ctx.registry.definitions.get(&*canon))
} {
if ctx.registry.base_units.contains(&*name) {
break;
}
let unit_canon = ctx.canonicalize(unit).unwrap_or_else(|| unit.clone());
if ctx.registry.base_units.contains(&**unit) {
name = unit.clone();
canon = unit_canon;
break;
}
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;
}
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);
name = unit.clone();
canon = unit_canon;
break;
}
} else {
assert!(name != unit_canon || canon != unit_canon);
name = unit_canon.clone();
canon = unit_canon;
}
} else {
assert!(name != *unit || canon != unit_canon);
name = unit.clone();
canon = unit_canon.clone();
canon = unit_canon;
break;
}
} else {
assert!(name != unit_canon || canon != unit_canon);
name = unit_canon.clone();
canon = unit_canon;
}
let (def, def_expr, res) = if ctx.registry.base_units.contains(&*name) {
let parts = ctx
.lookup(&name)
.expect("Lookup of base unit failed")
.to_parts(ctx);
let def = if let Some(ref q) = parts.quantity {
format!("base unit of {}", q)
} else {
assert!(name != *unit || canon != unit_canon);
name = unit.clone();
canon = unit_canon.clone();
}
}

(name, canon)
}

/// Given a name, returns the dimensionality if it is a quantity, or None if it isn't.
fn find_quantity(ctx: &Context, name: &str) -> Option<Dimensionality> {
for (dims, quantity_name) in &ctx.registry.quantities {
if quantity_name == name {
return Some(dims.clone());
}
}
None
}

pub(crate) fn eval_query(ctx: &Context, expr: &Query) -> Result<QueryReply, QueryError> {
match *expr {
Query::Expr(Expr::Unit { ref name }) if can_show_definition(ctx, name) => {
let (name, canon_name) = expand_aliases(ctx, name);

let (def, def_expr, value) =
if let Some(base_unit) = ctx.registry.base_units.get(&*name) {
let parts = Number::one_unit(base_unit.clone()).to_parts(ctx);
let def = if let Some(ref q) = parts.quantity {
format!("base unit of {}", q)
} else {
"base unit".to_string()
};
(Some(def), None, None)
} else if let Some(dims) = find_quantity(ctx, &name) {
let def = ctx
.registry
.definitions
.get(&name)
.expect("quantities should always have definitions");
let description = format!("physical quantity for {def} ({dims})");

(Some(description), None, None)
} else {
"base unit".to_string()
let def = ctx.registry.definitions.get(&name);
(
def.as_ref().map(|x| x.to_string()),
def,
ctx.lookup(&name).map(|x| x.to_parts(ctx)),
)
};
(Some(def), None, None)
} else {
let def = ctx.registry.definitions.get(&name);
(
def.as_ref().map(|x| x.to_string()),
def,
ctx.lookup(&name).map(|x| x.to_parts(ctx)),
)
};
Ok(QueryReply::Def(Box::new(DefReply {
canon_name: canon,
def,
def_expr: def_expr.as_ref().map(|x| ExprReply::from(*x)),
value: res,
doc: ctx.registry.docs.get(&name).cloned(),
canon_name,
def,
value,
})))
}
Query::Convert(ref top, Conversion::None, Some(base), digits) => {
Expand Down
39 changes: 39 additions & 0 deletions core/src/types/dimensionality.rs
Expand Up @@ -7,6 +7,7 @@ use std::{
btree_map::{IntoIter, Iter},
BTreeMap,
},
fmt,
iter::FromIterator,
ops,
};
Expand Down Expand Up @@ -93,6 +94,44 @@ impl<'a> ops::Div for &'a Dimensionality {
}
}

impl fmt::Display for Dimensionality {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
let mut have_frac = false;
let mut have_numerator = false;
for (base_unit, &power) in &self.dims {
if !first && power > 0 {
write!(fmt, " ")?;
}
first = false;
if power == 1 {
have_numerator = true;
write!(fmt, "{}", base_unit)?;
} else if power > 0 {
have_numerator = true;
write!(fmt, "{}^{}", base_unit, power)?;
} else {
have_frac = true;
}
}
if have_frac {
if have_numerator {
write!(fmt, " ")?;
}
write!(fmt, "/")?;
for (base_unit, &power) in &self.dims {
let power = -power;
if power == 1 {
write!(fmt, " {}", base_unit)?;
} else if power > 0 {
write!(fmt, " {}^{}", base_unit, power)?;
}
}
}
Ok(())
}
}

/////////////////////////////////////////
// Compatiblity with BTreeMap interface

Expand Down
4 changes: 4 additions & 0 deletions core/src/types/number.rs
Expand Up @@ -46,6 +46,10 @@ impl Number {
}
}

pub fn new_dims(value: Numeric, unit: Dimensionality) -> Number {
Number { value, unit }
}

/// Creates a value with a single dimension.
pub fn new_unit(num: Numeric, unit: BaseUnit) -> Number {
let unit = Dimensionality::base_unit(unit);
Expand Down
28 changes: 28 additions & 0 deletions core/tests/query.rs
Expand Up @@ -651,3 +651,31 @@ fn test_atom_symbol() {
"oganesson: atomic_number = 118; molar_mass = approx. 294.2139 gram / mole",
);
}

#[test]
fn gold_density_should_be_error() {
test(
"gold density",
"No such unit density, did you mean paperdensity?",
);
}

#[test]
fn quantities_disallowed() {
test(
"energy / time",
"No such unit energy, did you mean mass_energy?",
);
}

#[test]
fn quantity_defs() {
test(
"power",
"Definition: power = physical quantity for energy / time (kg m^2 / s^3)",
);
test(
"energy",
"Definition: energy = physical quantity for force length (kg m^2 / s^2)",
);
}

0 comments on commit 8f57001

Please sign in to comment.