ifc_lite_core/project_units/
symbols.rs1use crate::units::get_si_prefix_multiplier;
13
14pub fn si_prefix_symbol(prefix: &str) -> &'static str {
17 match prefix.trim().to_ascii_uppercase().as_str() {
18 "EXA" => "E",
19 "PETA" => "P",
20 "TERA" => "T",
21 "GIGA" => "G",
22 "MEGA" => "M",
23 "KILO" => "k",
24 "HECTO" => "h",
25 "DECA" => "da",
26 "DECI" => "d",
27 "CENTI" => "c",
28 "MILLI" => "m",
29 "MICRO" => "\u{00B5}", "NANO" => "n",
31 "PICO" => "p",
32 "FEMTO" => "f",
33 "ATTO" => "a",
34 _ => "",
35 }
36}
37
38#[derive(Clone, Copy, Debug)]
40pub struct SiUnitName {
41 pub symbol: &'static str,
43 pub prefix_power: i32,
46 pub base_scale: f64,
50}
51
52pub fn si_unit_name(name: &str) -> Option<SiUnitName> {
54 let n = name.trim().trim_matches('.').to_ascii_uppercase();
55 let d = |symbol| SiUnitName { symbol, prefix_power: 1, base_scale: 1.0 };
56 Some(match n.as_str() {
57 "METRE" => d("m"),
58 "SQUARE_METRE" => SiUnitName { symbol: "m\u{00B2}", prefix_power: 2, base_scale: 1.0 },
59 "CUBIC_METRE" => SiUnitName { symbol: "m\u{00B3}", prefix_power: 3, base_scale: 1.0 },
60 "GRAM" => SiUnitName { symbol: "g", prefix_power: 1, base_scale: 1e-3 },
61 "SECOND" => d("s"),
62 "AMPERE" => d("A"),
63 "KELVIN" => d("K"),
64 "MOLE" => d("mol"),
65 "CANDELA" => d("cd"),
66 "RADIAN" => d("rad"),
67 "STERADIAN" => d("sr"),
68 "HERTZ" => d("Hz"),
69 "NEWTON" => d("N"),
70 "PASCAL" => d("Pa"),
71 "JOULE" => d("J"),
72 "WATT" => d("W"),
73 "COULOMB" => d("C"),
74 "VOLT" => d("V"),
75 "FARAD" => d("F"),
76 "OHM" => d("\u{03A9}"),
77 "SIEMENS" => d("S"),
78 "WEBER" => d("Wb"),
79 "TESLA" => d("T"),
80 "HENRY" => d("H"),
81 "DEGREE_CELSIUS" => d("\u{00B0}C"),
82 "LUMEN" => d("lm"),
83 "LUX" => d("lx"),
84 "BECQUEREL" => d("Bq"),
85 "GRAY" => d("Gy"),
86 "SIEVERT" => d("Sv"),
87 _ => return None,
88 })
89}
90
91pub fn si_unit_symbol_and_scale(name: &str, prefix: Option<&str>) -> Option<(String, f64)> {
97 let base = si_unit_name(name)?;
98 let (prefix_sym, prefix_mult) = match prefix {
99 Some(p) if !p.trim().trim_matches('.').is_empty() => {
100 (si_prefix_symbol(p), get_si_prefix_multiplier(p.trim().trim_matches('.')))
101 }
102 _ => ("", 1.0),
103 };
104 let symbol = format!("{prefix_sym}{}", base.symbol);
105 let scale = base.base_scale * prefix_mult.powi(base.prefix_power);
106 Some((symbol, scale))
107}
108
109pub fn conversion_unit_symbol(name: &str) -> String {
112 let clean = name.trim().trim_matches('\'').trim();
113 match clean.to_ascii_uppercase().as_str() {
114 "DEGREE" => "\u{00B0}".to_string(),
115 "GRAD" | "GON" => "gon".to_string(),
116 "MINUTE" => "\u{2032}".to_string(),
117 "SECOND" => "\u{2033}".to_string(),
118 "FOOT" | "FEET" => "ft".to_string(),
119 "INCH" => "in".to_string(),
120 "YARD" => "yd".to_string(),
121 "MILE" => "mi".to_string(),
122 "LITRE" | "LITER" => "L".to_string(),
123 "ACRE" => "acre".to_string(),
124 "POUND" | "POUND-MASS" | "LBM" => "lb".to_string(),
125 "POUND-FORCE" | "LBF" => "lbf".to_string(),
126 "OUNCE" => "oz".to_string(),
127 "TON-METRIC" | "TONNE" => "t".to_string(),
128 "PSI" => "psi".to_string(),
129 "BAR" => "bar".to_string(),
130 "KIP" => "kip".to_string(),
131 "MINUTE-TIME" | "MIN" => "min".to_string(),
132 "HOUR" => "h".to_string(),
133 "DAY" => "d".to_string(),
134 "BTU" => "Btu".to_string(),
135 "" => "".to_string(),
136 _ => clean.to_string(),
137 }
138}
139
140pub fn compose_derived(elements: &[(String, i32)]) -> String {
145 let mut num: Vec<String> = Vec::new();
146 let mut den: Vec<String> = Vec::new();
147 for (sym, exp) in elements {
148 if *exp == 0 || sym.is_empty() {
149 continue;
150 }
151 let piece = format!("{sym}{}", superscript(exp.unsigned_abs()));
152 if *exp > 0 {
153 num.push(piece);
154 } else {
155 den.push(piece);
156 }
157 }
158 let mid_dot = "\u{00B7}"; let numerator = if num.is_empty() { "1".to_string() } else { num.join(mid_dot) };
160 if den.is_empty() {
161 return if num.is_empty() { String::new() } else { numerator };
162 }
163 let denominator = den.join(mid_dot);
164 if den.len() > 1 {
165 format!("{numerator}/({denominator})")
166 } else {
167 format!("{numerator}/{denominator}")
168 }
169}
170
171fn superscript(mag: u32) -> String {
179 match mag {
180 1 => String::new(),
181 0 => "\u{2070}".to_string(),
182 2 => "\u{00B2}".to_string(),
183 3 => "\u{00B3}".to_string(),
184 n if (4..=9).contains(&n) => {
185 char::from_u32(0x2070 + n).map(String::from).unwrap_or_default()
187 }
188 n => {
189 n.to_string()
193 .chars()
194 .map(|c| match c {
195 '0' => '\u{2070}',
196 '1' => '\u{00B9}',
197 '2' => '\u{00B2}',
198 '3' => '\u{00B3}',
199 d => char::from_u32(0x2070 + (d as u32 - '0' as u32)).unwrap_or(d),
200 })
201 .collect()
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn prefixed_length_symbol_and_scale() {
212 let (sym, scale) = si_unit_symbol_and_scale("METRE", Some("MILLI")).unwrap();
213 assert_eq!(sym, "mm");
214 assert!((scale - 1e-3).abs() < 1e-15);
215 }
216
217 #[test]
218 fn kilogram_folds_gram_base_scale() {
219 let (sym, scale) = si_unit_symbol_and_scale("GRAM", Some("KILO")).unwrap();
220 assert_eq!(sym, "kg");
221 assert!((scale - 1.0).abs() < 1e-12);
222 }
223
224 #[test]
225 fn prefixed_area_uses_squared_power() {
226 let (sym, scale) = si_unit_symbol_and_scale("SQUARE_METRE", Some("CENTI")).unwrap();
227 assert_eq!(sym, "cm\u{00B2}");
228 assert!((scale - 1e-4).abs() < 1e-18);
229 }
230
231 #[test]
232 fn prefixed_volume_uses_cubed_power() {
233 let (sym, scale) = si_unit_symbol_and_scale("CUBIC_METRE", Some("MILLI")).unwrap();
238 assert_eq!(sym, "mm\u{00B3}");
239 assert!((scale - 1e-9).abs() < 1e-24, "expected 1e-9 (cubed), got {scale}");
240 }
241
242 #[test]
243 fn bare_square_metre() {
244 let (sym, scale) = si_unit_symbol_and_scale("SQUARE_METRE", None).unwrap();
245 assert_eq!(sym, "m\u{00B2}");
246 assert!((scale - 1.0).abs() < 1e-15);
247 }
248
249 #[test]
250 fn derived_volumetric_flow_rate() {
251 assert_eq!(
252 compose_derived(&[("m".into(), 3), ("s".into(), -1)]),
253 "m\u{00B3}/s"
254 );
255 }
256
257 #[test]
258 fn derived_specific_heat_parenthesises_denominator() {
259 assert_eq!(
260 compose_derived(&[("J".into(), 1), ("kg".into(), -1), ("K".into(), -1)]),
261 "J/(kg\u{00B7}K)"
262 );
263 }
264
265 #[test]
266 fn derived_pure_inverse() {
267 assert_eq!(compose_derived(&[("s".into(), -1)]), "1/s");
268 }
269
270 #[test]
275 fn derived_exponent_i32_min_does_not_overflow() {
276 let composed = compose_derived(&[("m".into(), i32::MIN)]);
277 assert_eq!(
278 composed,
279 "1/m\u{00B2}\u{00B9}\u{2074}\u{2077}\u{2074}\u{2078}\u{00B3}\u{2076}\u{2074}\u{2078}"
280 );
281 assert_eq!(
283 compose_derived(&[("m".into(), i32::MAX)]),
284 "m\u{00B2}\u{00B9}\u{2074}\u{2077}\u{2074}\u{2078}\u{00B3}\u{2076}\u{2074}\u{2077}"
285 );
286 }
287
288 #[test]
289 fn conversion_symbols() {
290 assert_eq!(conversion_unit_symbol("'DEGREE'"), "\u{00B0}");
291 assert_eq!(conversion_unit_symbol("FOOT"), "ft");
292 assert_eq!(conversion_unit_symbol("MYUNIT"), "MYUNIT");
293 }
294}