Numbers

Unit conversion

Quantities converted between units: length, mass, temperature, volume and kitchen measures, speed, energy, pressure, data sizes and more, with prefixes and compound units, mpg against L/100km, "5 ft 11 in" read, and values shown in their most readable unit.

Version 1.0.0 · 4 functions · 0 crates

#calculators#maths#locale#conversion

Functions

What use units.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use units_convert, units_parse from units (Quillo 0.6).

units_convert(value: float, from: str, to: str) → floator an error

value in from as to, to 12 significant digits: units_convert(100, "km/h", "mph"), (72, "°F", "°C"), (8, "L/100km", "mpg"), (1, "GiB", "MB").

units_parse(text: str, to: str, digits: int) → floator an error

A quantity written as people write it, in one unit: "5 ft 11 in", "1 h 30 min", "2 lb 3 oz", "72 °F", "3.5kg" as to (rounded to digits significant digits).

units_describe(unit_text: str) → anyor an error

What a unit is: { factor (to SI), dimension: "m·kg·s⁻²", kind: "force" }.

units_best(value: float, unit_text: str, system: str) → anyor an error

A value shown in the most readable unit of a system ("metric", "imperial" or "us"): 0.0023 km is "2.3 m", 3,000,000 B is "3 MB" (or "2.86 MiB" with "binary"), 90 min is "1.5 h". Gives { value, unit, text }.

Get it

$ quillo install units               # units.qwer into this folder$ quillo install units --example     # and its example, units.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/units.qwer$ curl -O https://quillolang.org/plugins/files/units.qwe

Then, in your program, all of it or only what you call:

main.qwe
use units.qweruse units_convert, units_parse from units      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

units.qwe
// A travel, kitchen and workshop helper: speeds, temperatures, fuel economy, recipe measures and// file sizes converted; heights and times read as people write them; values shown in their most// readable unit for metric and US readers.////   quillo run units.qwe      then open http://localhost:8080/convert and http://localhost:8080/bestuse "units.qwer"route GET /convert = {    speed: units_convert(100.0, "km/h", "mph")?,    oven: units_convert(350.0, "°F", "°C")?,    economy: units_convert(6.5, "L/100km", "mpg")?,    butter: units_convert(1.0, "cup", "mL")?,    disk: units_convert(1.0, "GiB", "MB")?,    torque: units_convert(100.0, "N*m", "lbf*ft")?,    pressure: units_convert(32.0, "psi", "bar")?,    height: units_parse("5 ft 11 in", "cm", 4)?,    run: units_parse("1 h 32 min 10 s", "min", 5)?,    wrong: units_convert(1.0, "kg", "m").error,}route GET /best = {    tiny: units_best(0.0023, "km", "metric")?.text,    file: units_best(3000000.0, "B", "metric")?.text,    binary: units_best(3000000.0, "B", "binary")?.text,    milk: units_best(2.0, "L", "us")?.text,    talk: units_best(5400.0, "s", "metric")?.text,    what: units_describe("kWh")?,}
$ quillo run units.qwe$ curl 'localhost:8080/convert'$ curl 'localhost:8080/best'

Source

units.qwer
//! Quantities converted between units: length, mass, time, temperature, area, volume (US and imperial kitchen measures), speed, force, energy, power, pressure, data (kB and KiB), angles and more, with SI prefixes and compound units ("kg*m/s^2", "km/h", "L/100km" against "mpg"), mixed quantities read ("5 ft 11 in", "1 h 30 min"), and a value shown in its most readable unit.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};use std::f64::consts::PI;/// Dimensions: length, mass, time, temperature, amount, current, luminosity, information.type Dim = [i32; 8];const ONE: Dim = [0; 8];const L: Dim = [1, 0, 0, 0, 0, 0, 0, 0];const M: Dim = [0, 1, 0, 0, 0, 0, 0, 0];const T: Dim = [0, 0, 1, 0, 0, 0, 0, 0];const K: Dim = [0, 0, 0, 1, 0, 0, 0, 0];const N: Dim = [0, 0, 0, 0, 1, 0, 0, 0];const I: Dim = [0, 0, 0, 0, 0, 1, 0, 0];const J: Dim = [0, 0, 0, 0, 0, 0, 1, 0];const B: Dim = [0, 0, 0, 0, 0, 0, 0, 1];fn dim(parts: &[(Dim, i32)]) -> Dim {    let mut d = ONE;    for (x, p) in parts {        for i in 0..8 {            d[i] += x[i] * p;        }    }    d}/// (names, factor to SI, dimensions, takes prefixes).fn table() -> Vec<(&'static [&'static str], f64, Dim, bool)> {    let vol = dim(&[(L, 3)]);    let force = dim(&[(M, 1), (L, 1), (T, -2)]);    let energy = dim(&[(M, 1), (L, 2), (T, -2)]);    let power = dim(&[(M, 1), (L, 2), (T, -3)]);    let pressure = dim(&[(M, 1), (L, -1), (T, -2)]);    let volt = dim(&[(M, 1), (L, 2), (T, -3), (I, -1)]);    vec![        (&["m", "meter", "metre", "meters", "metres"], 1.0, L, true),        (&["in", "inch", "inches", "\""], 0.0254, L, false),        (&["ft", "foot", "feet", "'"], 0.3048, L, false),        (&["yd", "yard", "yards"], 0.9144, L, false),        (&["mi", "mile", "miles"], 1609.344, L, false),        (&["nmi", "nautical_mile"], 1852.0, L, false),        (&["thou", "mil"], 2.54e-5, L, false),        (&["au"], 1.495978707e11, L, false),        (&["ly", "light_year"], 9.4607304725808e15, L, false),        (&["g", "gram", "grams"], 1e-3, M, true),        (&["t", "tonne", "tonnes"], 1000.0, M, false),        (&["lb", "lbs", "pound", "pounds"], 0.45359237, M, false),        (&["oz", "ounce", "ounces"], 0.028349523125, M, false),        (&["st", "stone"], 6.35029318, M, false),        (&["ton", "short_ton"], 907.18474, M, false),        (&["long_ton"], 1016.0469088, M, false),        (&["ct", "carat"], 2e-4, M, false),        (&["gr", "grain"], 6.479891e-5, M, false),        (&["s", "sec", "second", "seconds"], 1.0, T, true),        (&["min", "minute", "minutes"], 60.0, T, false),        (&["h", "hr", "hour", "hours"], 3600.0, T, false),        (&["d", "day", "days"], 86400.0, T, false),        (&["wk", "week", "weeks"], 604800.0, T, false),        (&["yr", "year", "years"], 31557600.0, T, false),        (&["K", "kelvin"], 1.0, K, true),        (&["mol"], 1.0, N, true),        (&["A", "amp", "ampere"], 1.0, I, true),        (&["cd", "candela"], 1.0, J, true),        (&["bit", "bits"], 1.0, B, true),        (&["B", "byte", "bytes"], 8.0, B, true),        (&["ha", "hectare"], 1e4, dim(&[(L, 2)]), false),        (&["acre", "acres"], 4046.8564224, dim(&[(L, 2)]), false),        (&["L", "l", "liter", "litre", "liters", "litres"], 1e-3, vol, true),        (&["gal", "gallon", "gallons"], 3.785411784e-3, vol, false),        (&["gal_imp", "imperial_gallon"], 4.54609e-3, vol, false),        (&["qt", "quart"], 9.46352946e-4, vol, false),        (&["pt", "pint"], 4.73176473e-4, vol, false),        (&["pt_imp", "imperial_pint"], 5.6826125e-4, vol, false),        (&["cup", "cups"], 2.365882365e-4, vol, false),        (&["floz", "fl_oz"], 2.95735295625e-5, vol, false),        (&["tbsp", "tablespoon", "tablespoons"], 1.478676478125e-5, vol, false),        (&["tsp", "teaspoon", "teaspoons"], 4.92892159375e-6, vol, false),        (&["bbl", "barrel"], 0.158987294928, vol, false),        (&["kn", "knot", "knots"], 1852.0 / 3600.0, dim(&[(L, 1), (T, -1)]), false),        (&["mph"], 1609.344 / 3600.0, dim(&[(L, 1), (T, -1)]), false),        (&["kph"], 1000.0 / 3600.0, dim(&[(L, 1), (T, -1)]), false),        (&["mpg"], 1609.344 / 3.785411784e-3, dim(&[(L, -2)]), false),        (&["mpg_imp"], 1609.344 / 4.54609e-3, dim(&[(L, -2)]), false),        (&["N", "newton"], 1.0, force, true),        (&["lbf"], 4.4482216152605, force, false),        (&["kgf"], 9.80665, force, false),        (&["J", "joule"], 1.0, energy, true),        (&["Wh"], 3600.0, energy, true),        (&["cal"], 4.184, energy, true),        (&["Cal"], 4184.0, energy, false),        (&["BTU", "btu"], 1055.05585262, energy, false),        (&["eV"], 1.602176634e-19, energy, true),        (&["therm"], 105505585.262, energy, false),        (&["W", "watt"], 1.0, power, true),        (&["hp"], 745.69987158227022, power, false),        (&["PS"], 735.49875, power, false),        (&["Pa", "pascal"], 1.0, pressure, true),        (&["bar"], 1e5, pressure, true),        (&["atm"], 101325.0, pressure, false),        (&["psi"], 6894.757293168, pressure, false),        (&["mmHg"], 133.322387415, pressure, false),        (&["inHg"], 3386.389, pressure, false),        (&["torr"], 133.322368421, pressure, false),        (&["V", "volt"], 1.0, volt, true),        (&["ohm", "Ω"], 1.0, dim(&[(M, 1), (L, 2), (T, -3), (I, -2)]), true),        (&["coulomb"], 1.0, dim(&[(I, 1), (T, 1)]), true),        (&["Ah"], 3600.0, dim(&[(I, 1), (T, 1)]), true),        (&["Hz", "hertz"], 1.0, dim(&[(T, -1)]), true),        (&["rpm"], 1.0 / 60.0, dim(&[(T, -1)]), false),        (&["rad", "radian"], 1.0, ONE, true),        (&["deg", "degree", "degrees", "°"], PI / 180.0, ONE, false),        (&["rev", "turn"], 2.0 * PI, ONE, false),        (&["grad", "gon"], PI / 200.0, ONE, false),        (&["arcmin"], PI / 10800.0, ONE, false),        (&["arcsec"], PI / 648000.0, ONE, false),        (&["%", "percent"], 0.01, ONE, false),        (&["ppm"], 1e-6, ONE, false),    ]}const PREFIXES: &[(&str, f64)] = &[    ("Ki", 1024.0), ("Mi", 1048576.0), ("Gi", 1073741824.0), ("Ti", 1099511627776.0), ("Pi", 1125899906842624.0),    ("da", 10.0), ("Y", 1e24), ("Z", 1e21), ("E", 1e18), ("P", 1e15), ("T", 1e12), ("G", 1e9), ("M", 1e6), ("k", 1e3), ("h", 1e2),    ("d", 1e-1), ("c", 1e-2), ("m", 1e-3), ("µ", 1e-6), ("μ", 1e-6), ("u", 1e-6), ("n", 1e-9), ("p", 1e-12), ("f", 1e-15), ("a", 1e-18),];/// Temperatures: (names, scale, offset) where kelvin = (x + offset) * scale.const TEMPS: &[(&[&str], f64, f64)] = &[    (&["°C", "C", "degC", "celsius"], 1.0, 273.15),    (&["°F", "F", "degF", "fahrenheit"], 5.0 / 9.0, 459.67),    (&["°R", "degR", "rankine"], 5.0 / 9.0, 0.0),];fn simple(name: &str) -> Result<(f64, Dim), String> {    let t = table();    if let Some((_, f, d, _)) = t.iter().find(|(n, ..)| n.contains(&name)) {        return Ok((*f, *d));    }    for (p, pf) in PREFIXES {        if let Some(rest) = name.strip_prefix(p) {            if let Some((_, f, d, ok)) = t.iter().find(|(n, ..)| n.contains(&rest)) {                if *ok {                    // Binary prefixes (Ki, Mi...) only for bits and bytes.                    if p.ends_with('i') && *d != B {                        continue;                    }                    return Ok((f * pf, *d));                }            }        }    }    Err(format!("unknown unit {:?}", name))}/// A compound unit ("kg*m/s^2", "km/h", "m²", "L/100km") as its factor to SI and its dimensions.fn unit(s: &str) -> Result<(f64, Dim), String> {    let s = s.trim().replace(' ', "").replace('·', "*").replace('²', "^2").replace('³', "^3");    if s.is_empty() || s == "1" {        return Ok((1.0, ONE));    }    if s.len() > 100 {        return Err("a unit is at most 100 characters".into());    }    let mut f = 1.0;    let mut d = ONE;    let mut sign = 1;    let mut cur = String::new();    let mut flush = |cur: &mut String, sign: i32, f: &mut f64, d: &mut Dim| -> Result<(), String> {        if cur.is_empty() {            return Ok(());        }        let (name, pow) = match cur.split_once('^') {            Some((n, p)) => (n.to_string(), p.parse::<i32>().map_err(|_| format!("bad power in {:?}", cur))?),            None => {                // "m2" means m^2; a number on its own ("100" in L/100km) is a factor.                let digits = cur.trim_start_matches(|c: char| !c.is_ascii_digit());                if let Ok(x) = cur.parse::<f64>() {                    *f *= x.powi(sign);                    cur.clear();                    return Ok(());                }                if !digits.is_empty() && digits.len() < cur.len() && digits.chars().all(|c| c.is_ascii_digit()) && simple(cur).is_err() {                    (cur[..cur.len() - digits.len()].to_string(), digits.parse().unwrap_or(1))                } else {                    (cur.clone(), 1)                }            }        };        // "100km": a number before a unit.        let lead: String = name.chars().take_while(|c| c.is_ascii_digit() || *c == '.').collect();        let (mult, name) = if !lead.is_empty() && lead.len() < name.len() { (lead.parse::<f64>().unwrap_or(1.0), name[lead.len()..].to_string()) } else { (1.0, name) };        let (uf, ud) = simple(&name)?;        *f *= (uf * mult).powi(pow * sign);        for i in 0..8 {            d[i] += ud[i] * pow * sign;        }        cur.clear();        Ok(())    };    for c in s.chars() {        match c {            '*' => {                flush(&mut cur, sign, &mut f, &mut d)?;            }            '/' => {                flush(&mut cur, sign, &mut f, &mut d)?;                sign = -1;            }            c => cur.push(c),        }    }    flush(&mut cur, sign, &mut f, &mut d)?;    Ok((f, d))}fn temp(name: &str) -> Option<(f64, f64)> {    let n = name.trim();    TEMPS.iter().find(|(ns, ..)| ns.contains(&n)).map(|(_, s, o)| (*s, *o))}fn dim_text(d: &Dim) -> String {    const NAMES: [&str; 8] = ["m", "kg", "s", "K", "mol", "A", "cd", "bit"];    let sup = |p: i32| -> String { p.to_string().chars().map(|c| match c { '-' => '⁻', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴', '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹', '0' => '⁰', o => o }).collect() };    let parts: Vec<String> = d.iter().enumerate().filter(|(_, p)| **p != 0).map(|(i, p)| if *p == 1 { NAMES[i].to_string() } else { format!("{}{}", NAMES[i], sup(*p)) }).collect();    if parts.is_empty() { "1".into() } else { parts.join("·") }}fn convert(x: f64, from: &str, to: &str) -> Result<f64, String> {    match (temp(from), temp(to)) {        (Some((s1, o1)), Some((s2, o2))) => return Ok((x + o1) * s1 / s2 - o2),        (Some((s1, o1)), None) if unit(to).map(|u| u.1 == K).unwrap_or(false) => return Ok((x + o1) * s1 / unit(to)?.0),        (None, Some((s2, o2))) if unit(from).map(|u| u.1 == K).unwrap_or(false) => return Ok(x * unit(from)?.0 / s2 - o2),        _ => {}    }    let (f1, d1) = unit(from)?;    let (f2, d2) = unit(to)?;    if d1 == d2 {        return Ok(x * f1 / f2);    }    // Inverse units (mpg against L/100km): the value is turned over.    let inv: Dim = std::array::from_fn(|i| -d1[i]);    if inv == d2 {        if x == 0.0 {            return Err("0 cannot be turned into an inverse unit".into());        }        return Ok(1.0 / (x * f1) / f2);    }    Err(format!("{} ({}) and {} ({}) measure different things", from, dim_text(&d1), to, dim_text(&d2)))}fn tidy(x: f64, digits: i32) -> f64 {    if x == 0.0 || !x.is_finite() {        return x;    }    let mag = x.abs().log10().floor() as i32;    let p = 10f64.powi(digits - 1 - mag);    (x * p).round() / p}/// `value` in `from` as `to`, to 12 significant digits: `units_convert(100, "km/h", "mph")`, `(72, "°F", "°C")`, `(8, "L/100km", "mpg")`, `(1, "GiB", "MB")`.pub fn units_convert(value: f64, from: &str, to: &str) -> Result<f64, String> {    Ok(tidy(convert(value, from, to)?, 12))}/// A quantity written as people write it, in one unit: `"5 ft 11 in"`, `"1 h 30 min"`, `"2 lb 3 oz"`, `"72 °F"`, `"3.5kg"` as `to` (rounded to `digits` significant digits).pub fn units_parse(text: &str, to: &str, digits: i64) -> Result<f64, String> {    let t = text.trim().replace(',', "");    let mut total = 0.0;    let mut any = false;    let mut rest = t.as_str();    while !rest.trim().is_empty() {        rest = rest.trim_start();        let n: String = rest.chars().take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+' || *c == 'e').collect();        let x: f64 = n.parse().map_err(|_| format!("{:?}: a number should come first", rest))?;        rest = rest[n.len()..].trim_start();        let u: String = rest.chars().take_while(|c| !c.is_ascii_digit() && *c != '-' && *c != '+' && !c.is_whitespace() || *c == '°').collect();        let u = u.trim_end_matches(['.', ',']).to_string();        rest = &rest[rest.find(u.as_str()).map(|i| i + u.len()).unwrap_or(0)..];        if u.is_empty() {            return Err(format!("{:?}: each number needs its unit", text));        }        total += convert(x, &u, to)?;        any = true;    }    if !any {        return Err("no quantity".into());    }    Ok(tidy(total, digits.clamp(1, 15) as i32))}/// What a unit is: `{ factor (to SI), dimension: "m·kg·s⁻²", kind: "force" }`.pub fn units_describe(unit_text: &str) -> Result<serde_json::Value, String> {    if let Some((s, o)) = temp(unit_text) {        return Ok(json!({ "factor": s, "offset": o, "dimension": "K", "kind": "temperature" }));    }    let (f, d) = unit(unit_text)?;    Ok(json!({ "factor": f, "dimension": dim_text(&d), "kind": kind(&d) }))}fn kind(d: &Dim) -> &'static str {    let k = [        (L, "length"), (M, "mass"), (T, "time"), (K, "temperature"), (B, "information"), (ONE, "ratio or angle"),        (dim(&[(L, 2)]), "area"), (dim(&[(L, 3)]), "volume"), (dim(&[(L, 1), (T, -1)]), "speed"), (dim(&[(L, 1), (T, -2)]), "acceleration"),        (dim(&[(M, 1), (L, 1), (T, -2)]), "force"), (dim(&[(M, 1), (L, 2), (T, -2)]), "energy"), (dim(&[(M, 1), (L, 2), (T, -3)]), "power"),        (dim(&[(M, 1), (L, -1), (T, -2)]), "pressure"), (dim(&[(T, -1)]), "frequency"), (dim(&[(L, -2)]), "fuel economy (distance per volume)"),        (dim(&[(M, 1), (L, -3)]), "density"), (dim(&[(B, 1), (T, -1)]), "data rate"), (dim(&[(I, 1), (T, 1)]), "charge"),    ];    k.iter().find(|(x, _)| x == d).map(|(_, n)| *n).unwrap_or("other")}/// A value shown in the most readable unit of a system (`"metric"`, `"imperial"` or `"us"`): 0.0023 km is "2.3 m", 3,000,000 B is "3 MB" (or "2.86 MiB" with `"binary"`), 90 min is "1.5 h". Gives `{ value, unit, text }`.pub fn units_best(value: f64, unit_text: &str, system: &str) -> Result<serde_json::Value, String> {    let (_, d) = if temp(unit_text).is_some() { (1.0, K) } else { unit(unit_text)? };    let candidates: Vec<&str> = match (kind(&d), system) {        ("length", "imperial") | ("length", "us") => vec!["in", "ft", "yd", "mi"],        ("length", _) => vec!["nm", "µm", "mm", "cm", "m", "km"],        ("mass", "imperial") => vec!["oz", "lb", "st"],        ("mass", "us") => vec!["oz", "lb", "ton"],        ("mass", _) => vec!["mg", "g", "kg", "t"],        ("time", _) => vec!["ms", "s", "min", "h", "d", "yr"],        ("volume", "us") => vec!["tsp", "tbsp", "floz", "cup", "qt", "gal"],        ("volume", "imperial") => vec!["floz", "pt_imp", "gal_imp"],        ("volume", _) => vec!["mL", "L", "m^3"],        ("information", "binary") => vec!["B", "KiB", "MiB", "GiB", "TiB"],        ("information", _) => vec!["B", "kB", "MB", "GB", "TB"],        ("energy", _) => vec!["J", "kJ", "MJ", "kWh", "MWh"],        ("power", _) => vec!["mW", "W", "kW", "MW"],        ("pressure", _) => vec!["Pa", "kPa", "bar"],        ("area", "imperial") | ("area", "us") => vec!["ft^2", "acre", "mi^2"],        ("area", _) => vec!["cm^2", "m^2", "ha", "km^2"],        ("frequency", _) => vec!["Hz", "kHz", "MHz", "GHz"],        ("speed", "imperial") | ("speed", "us") => vec!["mph"],        ("speed", _) => vec!["km/h"],        ("temperature", "us") => vec!["°F"],        ("temperature", _) => vec!["°C"],        _ => vec![unit_text],    };    // The largest unit in which the value is still 1 or more (in size).    let mut pick = candidates[0];    for c in &candidates {        if convert(value, unit_text, c)?.abs() >= 1.0 || kind(&d) == "temperature" || kind(&d) == "speed" {            pick = c;        }    }    let v = tidy(convert(value, unit_text, pick)?, 3);    let shown = if v.fract() == 0.0 && v.abs() < 1e15 { format!("{}", v as i64) } else { format!("{}", v) };    let sep = if pick.starts_with('°') { "" } else { " " };    Ok(json!({ "value": v, "unit": pick, "text": format!("{}{}{}", shown, sep, pick.replace("^2", "²").replace("^3", "³")) }))}
Finance finance.qwer · 1.0.0Loan payments and amortization tables, compound growth with deposits, net present value and internal rate of return, in cents.moneyloanscalculatorsmaths7 views · 6 downloadsFractions fractions.qwer · 1.0.0Exact fractions for recipes, carpentry and school maths: "1 1/2", "3/8" and "12.5%" read and calculated without rounding, expressions worked out, decimals turned into simple fractions, measures rounded to 1/16 inch, and recipes scaled.fractionsmathscalculatorsexact7 views · 9 downloadsBig numbers bignum.qwer · 1.0.0Integers and fractions of any size, exact: factorials, powers, gcd, modular powers, and fractions such as 1/3 + 1/6 = 1/2 with no rounding.mathsmoneyexactfractions8 views · 7 downloadsClustering clustering.qwer · 1.0.0Data put into groups: k-means with k chosen by the elbow and silhouette, DBSCAN for groups of any shape and their outliers (in metres for latitude and longitude), and hierarchical clustering, on rows of numbers or maps.statisticsanalyticssegmentsgeomaths7 views · 7 downloadsFrequency spectrum fft.qwer · 1.0.0The frequency spectrum of samples from a microphone or sensor: frequencies and their strength, the dominant ones found between bins, and the signal's level.signalsaudiosensorsmaths8 views · 7 downloadsGraphs graph.qwer · 1.0.0Graphs given as edges: shortest paths with distances, task order by topological sort with cycles named, connected groups, and what a node reaches.graphsroutesdependenciesmaths7 views · 7 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Unit conversion: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.