use ifc_schema::SchemaVersion;
pub(crate) type Dimensions = [i32; 7];
const IFC4_FARAD: Dimensions = [-2, -1, 4, 2, 0, 0, 0];
const IFC2X3_FARAD: Dimensions = [-2, -1, 4, 1, 0, 0, 0];
const IFC4_CAPACITANCE: Dimensions = [-2, -1, 4, 2, 0, 0, 0];
const IFC2X3_CAPACITANCE: Dimensions = [-2, 1, 4, 1, 0, 0, 0];
pub(crate) fn name_power(name: &str) -> i32 {
match name.to_ascii_uppercase().as_str() {
"SQUARE_METRE" => 2,
"CUBIC_METRE" => 3,
_ => 1,
}
}
pub(crate) fn si_name_dimensions(version: SchemaVersion, name: &str) -> Option<Dimensions> {
Some(match name.to_ascii_uppercase().as_str() {
"METRE" => [1, 0, 0, 0, 0, 0, 0],
"SQUARE_METRE" => [2, 0, 0, 0, 0, 0, 0],
"CUBIC_METRE" => [3, 0, 0, 0, 0, 0, 0],
"GRAM" => [0, 1, 0, 0, 0, 0, 0],
"SECOND" => [0, 0, 1, 0, 0, 0, 0],
"AMPERE" => [0, 0, 0, 1, 0, 0, 0],
"KELVIN" | "DEGREE_CELSIUS" => [0, 0, 0, 0, 1, 0, 0],
"MOLE" => [0, 0, 0, 0, 0, 1, 0],
"CANDELA" | "LUMEN" => [0, 0, 0, 0, 0, 0, 1],
"RADIAN" | "STERADIAN" => [0; 7],
"HERTZ" | "BECQUEREL" => [0, 0, -1, 0, 0, 0, 0],
"NEWTON" => [1, 1, -2, 0, 0, 0, 0],
"PASCAL" => [-1, 1, -2, 0, 0, 0, 0],
"JOULE" => [2, 1, -2, 0, 0, 0, 0],
"WATT" => [2, 1, -3, 0, 0, 0, 0],
"COULOMB" => [0, 0, 1, 1, 0, 0, 0],
"VOLT" => [2, 1, -3, -1, 0, 0, 0],
"FARAD" => match version {
SchemaVersion::Ifc2x3 => IFC2X3_FARAD,
SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3 => IFC4_FARAD,
},
"OHM" => [2, 1, -3, -2, 0, 0, 0],
"SIEMENS" => [-2, -1, 3, 2, 0, 0, 0],
"WEBER" => [2, 1, -2, -1, 0, 0, 0],
"TESLA" => [0, 1, -2, -1, 0, 0, 0],
"HENRY" => [2, 1, -2, -2, 0, 0, 0],
"LUX" => [-2, 0, 0, 0, 0, 0, 1],
"GRAY" | "SIEVERT" => [2, 0, -2, 0, 0, 0, 0],
_ => return None,
})
}
pub(crate) fn unit_enum_dimensions(version: SchemaVersion, unit_type: &str) -> Option<Dimensions> {
Some(match unit_type.to_ascii_uppercase().as_str() {
"LENGTHUNIT" => [1, 0, 0, 0, 0, 0, 0],
"MASSUNIT" => [0, 1, 0, 0, 0, 0, 0],
"TIMEUNIT" => [0, 0, 1, 0, 0, 0, 0],
"ELECTRICCURRENTUNIT" => [0, 0, 0, 1, 0, 0, 0],
"THERMODYNAMICTEMPERATUREUNIT" => [0, 0, 0, 0, 1, 0, 0],
"AMOUNTOFSUBSTANCEUNIT" => [0, 0, 0, 0, 0, 1, 0],
"LUMINOUSINTENSITYUNIT" | "LUMINOUSFLUXUNIT" => [0, 0, 0, 0, 0, 0, 1],
"PLANEANGLEUNIT" | "SOLIDANGLEUNIT" => [0; 7],
"AREAUNIT" => [2, 0, 0, 0, 0, 0, 0],
"VOLUMEUNIT" => [3, 0, 0, 0, 0, 0, 0],
"ABSORBEDDOSEUNIT" | "DOSEEQUIVALENTUNIT" => [2, 0, -2, 0, 0, 0, 0],
"RADIOACTIVITYUNIT" | "FREQUENCYUNIT" => [0, 0, -1, 0, 0, 0, 0],
"ELECTRICCAPACITANCEUNIT" => match version {
SchemaVersion::Ifc2x3 => IFC2X3_CAPACITANCE,
SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3 => IFC4_CAPACITANCE,
},
"ELECTRICCHARGEUNIT" => [0, 0, 1, 1, 0, 0, 0],
"ELECTRICCONDUCTANCEUNIT" => [-2, -1, 3, 2, 0, 0, 0],
"ELECTRICVOLTAGEUNIT" => [2, 1, -3, -1, 0, 0, 0],
"ELECTRICRESISTANCEUNIT" => [2, 1, -3, -2, 0, 0, 0],
"ENERGYUNIT" => [2, 1, -2, 0, 0, 0, 0],
"FORCEUNIT" => [1, 1, -2, 0, 0, 0, 0],
"INDUCTANCEUNIT" => [2, 1, -2, -2, 0, 0, 0],
"ILLUMINANCEUNIT" => [-2, 0, 0, 0, 0, 0, 1],
"MAGNETICFLUXUNIT" => [2, 1, -2, -1, 0, 0, 0],
"MAGNETICFLUXDENSITYUNIT" => [0, 1, -2, -1, 0, 0, 0],
"POWERUNIT" => [2, 1, -3, 0, 0, 0, 0],
"PRESSUREUNIT" => [-1, 1, -2, 0, 0, 0, 0],
_ => return None,
})
}
pub(crate) fn si_to_base(prefix_exponent: i32, name: &str) -> (f64, f64) {
let upper = name.to_ascii_uppercase();
let power = name_power(&upper);
let base_exponent = if upper == "GRAM" { -3 } else { 0 };
let scale = 10f64.powi(prefix_exponent * power + base_exponent);
let offset = if upper == "DEGREE_CELSIUS" {
273.15
} else {
0.0
};
(scale, offset)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn express(rel: &str) -> Option<String> {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let Some(root) = [
"../../../../references/ifc-spec",
"../../references/ifc-spec",
]
.into_iter()
.map(|path| crate_dir.join(path))
.find(|path| path.is_dir()) else {
assert!(
std::env::var_os("IFC_SPEC_REQUIRED").is_none(),
"IFC_SPEC_REQUIRED is set but references/ifc-spec was not found; \
run scripts/fetch-ifc-schemas.sh"
);
return None;
};
let bytes = std::fs::read(root.join(rel)).ok()?;
Some(bytes.iter().map(|&b| b as char).collect())
}
fn case_rows(source: &str, function: &str) -> Vec<(String, Dimensions)> {
let start = source
.to_ascii_lowercase()
.find(&format!("function {}", function.to_ascii_lowercase()))
.expect("function declared");
let body = &source[start..];
let body = &body[..body.find("END_FUNCTION").expect("function ends")];
let body = &body[body.find("CASE").expect("a CASE statement")..];
let mut rows = Vec::new();
for arm in body.split(';') {
let Some((label, rest)) = arm.split_once(':') else {
continue;
};
let Some(open) = rest.find("IfcDimensionalExponents") else {
continue;
};
let numbers = &rest[open..];
let inner = &numbers[numbers.find('(').unwrap() + 1..numbers.find(')').unwrap()];
let values: Vec<i32> = inner
.split(',')
.map(|n| n.trim().parse().expect("integer exponent"))
.collect();
let label = label.split_whitespace().last().unwrap().to_owned();
rows.push((label, values.try_into().expect("seven exponents")));
}
rows
}
fn check(version: SchemaVersion, rel: &str) {
let Some(source) = express(rel) else {
return;
};
let si = case_rows(&source, "IfcDimensionsForSiUnit");
assert_eq!(si.len(), 31, "{rel}: 30 names plus OTHERWISE");
for (name, dimensions) in si {
if name == "OTHERWISE" {
assert_eq!(si_name_dimensions(version, &name), None);
continue;
}
assert_eq!(
si_name_dimensions(version, &name),
Some(dimensions),
"{rel}: {name}"
);
}
let units = case_rows(&source, "IfcCorrectDimensions");
assert_eq!(units.len(), 29, "{rel}: every IfcUnitEnum but USERDEFINED");
for (unit_type, dimensions) in units {
assert_eq!(
unit_enum_dimensions(version, &unit_type),
Some(dimensions),
"{rel}: {unit_type}"
);
}
assert_eq!(unit_enum_dimensions(version, "USERDEFINED"), None);
}
#[test]
fn ifc4_tables_match_the_normative_functions() {
check(SchemaVersion::Ifc4, "ifc4-add2-tc1/IFC4.exp");
}
#[test]
fn ifc2x3_tables_match_the_normative_functions() {
check(SchemaVersion::Ifc2x3, "ifc2x3-tc1/IFC2X3_TC1.exp");
}
#[test]
fn ifc4x3_tables_match_the_normative_functions() {
check(SchemaVersion::Ifc4x3, "ifc4x3-add2/IFC4X3_ADD2.exp");
}
#[test]
fn prefixes_apply_before_the_power_and_mass_is_based_on_the_kilogram() {
assert_eq!(si_to_base(-3, "SQUARE_METRE"), (1e-6, 0.0));
assert_eq!(si_to_base(-3, "CUBIC_METRE").0, 1e-9);
assert_eq!(si_to_base(0, "GRAM").0, 1e-3);
assert_eq!(si_to_base(3, "GRAM").0, 1.0);
assert_eq!(si_to_base(0, "DEGREE_CELSIUS"), (1.0, 273.15));
}
}