use std::cell::RefCell;
use std::collections::HashMap;
use rink_core::output::QueryReply;
use rink_core::{Context, eval as rink_eval, simple_context};
thread_local! {
static CONTEXT: RefCell<Option<Context>> = const { RefCell::new(None) };
static UNIT_CACHE: RefCell<HashMap<String, bool>> =
RefCell::new(HashMap::new());
}
fn with_context<T>(f: impl FnOnce(&mut Context) -> T) -> T {
CONTEXT.with(|cell| {
let mut slot = cell.borrow_mut();
let context = slot.get_or_insert_with(|| {
simple_context().expect("the bundle-files feature is enabled")
});
f(context)
})
}
pub fn eval(query: &str) -> Result<(f64, Option<String>), String> {
let reply = with_context(|ctx| rink_eval(ctx, query))
.map_err(|error| first_line(&error.to_string()))?;
extract(&reply).ok_or_else(|| "not a numeric result".to_string())
}
pub fn scale_of(unit: &str) -> Result<f64, String> {
let (value, _) = eval(&format!("1 {unit}"))?;
Ok(value)
}
pub fn is_unit(symbol: &str) -> bool {
if symbol.is_empty() {
return false;
}
if let Some(hit) = UNIT_CACHE.with(|c| c.borrow().get(symbol).copied()) {
return hit;
}
let result = with_context(|ctx| rink_eval(ctx, symbol)).is_ok();
UNIT_CACHE.with(|c| c.borrow_mut().insert(symbol.to_string(), result));
result
}
fn extract(reply: &QueryReply) -> Option<(f64, Option<String>)> {
let parts = match reply {
QueryReply::Number(parts) => parts,
QueryReply::Conversion(conversion) => &conversion.value,
_ => return None,
};
let value = parts.raw_value.as_ref()?.value.to_f64();
Some((value, parts.unit.clone()))
}
fn first_line(message: &str) -> String {
message.lines().next().unwrap_or(message).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_between_compatible_units() {
let (value, unit) = eval("123 MPa -> bar").unwrap();
assert!((value - 1230.0).abs() < 1e-6);
assert_eq!(unit.as_deref(), Some("bar"));
}
#[test]
fn exponent_and_compound_units_convert() {
let (value, _) = eval("1 dm^3 -> cm^3").unwrap();
assert!((value - 1000.0).abs() < 1e-6);
let (value, _) = eval("N/mm^2 -> MPa").unwrap();
assert!((value - 1.0).abs() < 1e-9);
}
#[test]
fn arithmetic_with_units_produces_a_derived_unit() {
let (value, unit) = eval("1 m * 2 m").unwrap();
assert!((value - 2.0).abs() < 1e-9);
assert!(unit.unwrap().contains("meter"));
}
#[test]
fn addition_picks_a_single_unit() {
let (value, _) = eval("1 m + 50 cm").unwrap();
assert!((value - 1.5).abs() < 1e-9);
}
#[test]
fn incompatible_units_error() {
assert!(eval("5 N -> bar").is_err());
assert!(eval("1 m + 1 s").is_err());
}
#[test]
fn is_unit_recognizes_units_but_not_unknown_identifiers() {
assert!(is_unit("m"));
assert!(is_unit("kN"));
assert!(is_unit("MPa"));
assert!(is_unit("dm"));
assert!(!is_unit("notaunit"));
assert!(!is_unit(""));
}
}