danwi 0.2.3

SI units library
Documentation
use std::{env, path::Path};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = env::var("OUT_DIR")?;
    let out_path = Path::new(&out_dir);

    println!("cargo:rerun-if-changed=dimensions.txt");
    dimensions::generate(&out_path.join("dimensions_generated.rs"))?;

    println!("cargo:rerun-if-changed=units.txt");
    units::generate(&out_path.join("units_generated.rs"))?;

    Ok(())
}

type Error = Box<dyn std::error::Error>;

fn to_pascal_case(s: &str) -> String {
    s.split(|c: char| c == '_' || c == '-' || c.is_whitespace())
        .filter(|word| !word.is_empty())
        .map(|word| {
            let mut chars = word.chars();
            chars
                .next()
                .map(|first| {
                    first
                        .to_uppercase()
                        .chain(chars.as_str().to_lowercase().chars())
                        .collect::<String>()
                })
                .unwrap_or_default()
        })
        .collect()
}

mod dimensions {
    use super::*;
    use std::{fmt::Write, fs, path::PathBuf};

    #[derive(Debug, Clone)]
    struct Dimension {
        name: String,
        exponents: [i8; 7],
        doc: Option<String>,
    }

    pub fn generate(output_path: &PathBuf) -> Result<(), Error> {
        let content = fs::read_to_string("dimensions.txt")?;
        let dimensions = parse_dimensions(&content)?;
        validate_dimensions(&dimensions)?;
        let code = generate_code(&dimensions)?;
        fs::write(output_path, code)?;
        Ok(())
    }

    fn parse_dimensions(content: &str) -> Result<Vec<Dimension>, Error> {
        let mut dimensions = Vec::new();
        let mut line_num = 0;

        for line in content.lines() {
            line_num += 1;
            let trimmed_line = line.trim();

            if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
                continue;
            }

            let Some(dim) = parse_dimension_line(trimmed_line) else {
                panic!("invalid dimension on line {}: {}", line_num, trimmed_line);
            };

            dimensions.push(dim);
        }

        assert!(!dimensions.is_empty());
        Ok(dimensions)
    }

    fn parse_dimension_line(line: &str) -> Option<Dimension> {
        // Split by #
        let parts: Vec<&str> = line.splitn(2, '#').collect();
        let definition = parts[0].trim();
        let doc = parts.get(1).map(|s| s.trim().to_string());

        // Parse name and exponents
        let mut parts = definition.splitn(2, ':');
        let name = parts.next()?.trim().to_string();
        let exponents = parts.next()?.trim();

        // Parse exponents
        let exponents: Vec<i8> = exponents
            .split_whitespace()
            .filter_map(|s| s.parse().ok())
            .collect();

        if exponents.len() != 7 {
            return None;
        }

        Some(Dimension {
            name,
            exponents: exponents.try_into().ok()?,
            doc,
        })
    }

    fn validate_dimensions(dimensions: &[Dimension]) -> Result<(), Error> {
        let mut seen = std::collections::HashSet::new();

        for dim in dimensions {
            if !seen.insert(&dim.name) {
                panic!("duplicate dimension name: {}", dim.name);
            }
        }

        Ok(())
    }

    fn generate_code(dimensions: &[Dimension]) -> Result<String, Error> {
        let mut code = String::new();

        writeln!(
            &mut code,
            "// This file is automatically generated by build.rs"
        )?;
        writeln!(&mut code, "// Do not edit manually!")?;
        writeln!(&mut code, "// Source: dimensions.txt")?;
        writeln!(&mut code)?;
        writeln!(&mut code, "use typenum::*;")?;
        writeln!(&mut code)?;

        for dim in dimensions {
            if let Some(ref doc) = dim.doc {
                writeln!(code, "/// {}", doc)?;
            }

            write!(code, "pub type {} = Dimension<", to_pascal_case(&dim.name))?;

            for (i, &exp) in dim.exponents.iter().enumerate() {
                if i > 0 {
                    write!(code, ", ")?;
                }

                write!(
                    code,
                    "{}",
                    match exp {
                        -4 => "N4",
                        -3 => "N3",
                        -2 => "N2",
                        -1 => "N1",
                        0 => "Z0",
                        1 => "P1",
                        2 => "P2",
                        3 => "P3",
                        4 => "P4",
                        _ => unreachable!(),
                    }
                )?;
            }

            writeln!(code, ">;")?;
            writeln!(code)?;
        }

        Ok(code)
    }
}

mod units {
    use super::*;
    use std::{fmt::Write, fs, path::PathBuf};

    #[derive(Debug, Clone)]
    struct Unit {
        name: String,
        symbol: String,
        dimension: String,
    }

    pub fn generate(output_path: &PathBuf) -> Result<(), Error> {
        let content = fs::read_to_string("units.txt")?;
        let units = parse_units(&content)?;
        let code = generate_code(&units)?;
        fs::write(output_path, code)?;
        Ok(())
    }

    fn parse_units(content: &str) -> Result<Vec<Unit>, Error> {
        let mut units = Vec::new();

        for line in content.lines() {
            let trimmed_line = line.trim();

            if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
                continue;
            }

            let parts: Vec<&str> = trimmed_line.split_whitespace().collect();

            assert_eq!(parts.len(), 3);

            units.push(Unit {
                name: parts[0].into(),
                symbol: parts[1].into(),
                dimension: parts[2].into(),
            });
        }

        Ok(units)
    }

    fn generate_code(units: &[Unit]) -> Result<String, Error> {
        let mut code = String::new();

        writeln!(
            &mut code,
            "// This file is automatically generated by build.rs"
        )?;
        writeln!(&mut code, "// Do not edit manually!")?;
        writeln!(&mut code, "// Source: units.txt")?;
        writeln!(&mut code)?;
        writeln!(&mut code, "define_units! {{")?;

        for unit in units {
            let dimension_type = to_pascal_case(&unit.dimension);

            writeln!(
                &mut code,
                "    {} ({}): {},",
                unit.name, unit.symbol, dimension_type
            )?;
        }

        writeln!(&mut code, "}}")?;

        Ok(code)
    }
}