ucum-units 0.1.0

A total, conformance-tested implementation of UCUM (Unified Code for Units of Measure): parse, validate, analyze, compare and convert units.
Documentation
//! Build script: parse the vendored, verbatim `ucum-essence.xml` and emit a
//! generated Rust source file (`$OUT_DIR/ucum_tables.rs`) containing the prefix
//! and atom tables.
//!
//! Parsing happens here, at build time, so that no XML/serialization dependency
//! leaks into the runtime dependency tree of the published crate. The essence
//! file is redistributed verbatim under the UCUM Copyright Notice and License
//! (see `vendor/UCUM-LICENSE.md`); this script only *reads* it.

use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::Path;

fn main() {
    println!("cargo:rerun-if-changed=vendor/ucum-essence.xml");
    println!("cargo:rerun-if-changed=build.rs");

    let xml = fs::read_to_string("vendor/ucum-essence.xml")
        .expect("vendor/ucum-essence.xml must be present");
    let doc = roxmltree::Document::parse(&xml).expect("ucum-essence.xml must be valid XML");

    let mut prefixes = String::new();
    let mut atoms = String::new();

    for node in doc.descendants() {
        match node.tag_name().name() {
            "prefix" => {
                let code = node.attribute("Code").expect("prefix Code");
                let ci_code = node.attribute("CODE").expect("prefix CODE");
                let name = child_text(&node, "name").unwrap_or_default();
                let value = node
                    .children()
                    .find(|c| c.tag_name().name() == "value")
                    .and_then(|v| v.attribute("value"))
                    .expect("prefix value");
                let factor: f64 = value.parse().expect("prefix value f64");
                writeln!(
                    prefixes,
                    "    PrefixDef {{ code: {code:?}, ci_code: {ci_code:?}, name: {name:?}, factor: {factor:?} }},"
                )
                .unwrap();
            }
            "base-unit" => {
                let code = node.attribute("Code").expect("base Code");
                let ci_code = node.attribute("CODE").expect("base CODE");
                let name = child_text(&node, "name").unwrap_or_default();
                let dim = node.attribute("dim").expect("base dim");
                let idx = dim_index(dim);
                writeln!(
                    atoms,
                    "    AtomDef {{ code: {code:?}, ci_code: {ci_code:?}, name: {name:?}, is_metric: true, is_arbitrary: false, kind: AtomKind::Base({idx}) }},"
                )
                .unwrap();
            }
            "unit" => {
                let code = node.attribute("Code").expect("unit Code");
                let ci_code = node.attribute("CODE").expect("unit CODE");
                let name = child_text(&node, "name").unwrap_or_default();
                let is_metric = node.attribute("isMetric") == Some("yes");
                let is_special = node.attribute("isSpecial") == Some("yes");
                let is_arbitrary = node.attribute("isArbitrary") == Some("yes");

                let value = node
                    .children()
                    .find(|c| c.tag_name().name() == "value")
                    .expect("unit value element");

                let kind = if is_special {
                    let func = value
                        .children()
                        .find(|c| c.tag_name().name() == "function")
                        .expect("special unit function");
                    let fname = func.attribute("name").expect("function name");
                    let fval: f64 = func
                        .attribute("value")
                        .expect("function value")
                        .parse()
                        .expect("f");
                    let funit = func.attribute("Unit").expect("function Unit");
                    format!(
                        "AtomKind::Special {{ func: {fname:?}, value: {fval:?}, unit: {funit:?} }}"
                    )
                } else {
                    let unit = value.attribute("Unit").expect("value Unit");
                    let v: f64 = value
                        .attribute("value")
                        .expect("value attr")
                        .parse()
                        .expect("value f64");
                    format!("AtomKind::Derived {{ value: {v:?}, unit: {unit:?} }}")
                };

                writeln!(
                    atoms,
                    "    AtomDef {{ code: {code:?}, ci_code: {ci_code:?}, name: {name:?}, is_metric: {is_metric}, is_arbitrary: {is_arbitrary}, kind: {kind} }},"
                )
                .unwrap();
            }
            _ => {}
        }
    }

    let out = format!(
        "// @generated by build.rs from vendor/ucum-essence.xml. Do not edit.\n\
         pub static PREFIXES: &[PrefixDef] = &[\n{prefixes}];\n\n\
         pub static ATOMS: &[AtomDef] = &[\n{atoms}];\n"
    );

    let dest = Path::new(&env::var("OUT_DIR").unwrap()).join("ucum_tables.rs");
    fs::write(dest, out).expect("write generated tables");
}

/// Read the text content of the first child element named `tag`.
fn child_text<'a>(node: &roxmltree::Node<'a, 'a>, tag: &str) -> Option<String> {
    node.children()
        .find(|c| c.tag_name().name() == tag)
        .and_then(|c| c.text())
        .map(|s| s.trim().to_string())
}

/// Map a UCUM base-quantity dimension letter to its index in the exponent
/// vector `[length, time, mass, angle, temperature, charge, luminous]`.
fn dim_index(dim: &str) -> usize {
    match dim {
        "L" => 0, // length        (m)
        "T" => 1, // time          (s)
        "M" => 2, // mass          (g)
        "A" => 3, // plane angle   (rad)
        "C" => 4, // temperature   (K)
        "Q" => 5, // electric charge (C)
        "F" => 6, // luminous intensity (cd)
        other => panic!("unknown base dimension letter: {other}"),
    }
}