use octofhir_ucum::fhir::{FhirError, FhirQuantity, convert_quantity};
use octofhir_ucum::{analyse, is_comparable, unit_divide, unit_multiply, validate};
use rust_decimal::Decimal;
use std::str::FromStr;
pub fn validate_unit(unit: &str) -> bool {
if is_time_unit(unit) {
return true;
}
validate(unit).is_ok()
}
pub fn canonicalize_quantity(value: f64, unit: &str) -> Option<(f64, String)> {
if unit.is_empty() {
return None;
}
let canonical = octofhir_ucum::get_canonical_units(unit).ok()?;
if canonical.unit.is_empty() {
return None;
}
let canonical_value = value * canonical.factor + canonical.offset;
Some((canonical_value, canonical.unit))
}
pub fn convert_units(value: Decimal, from_unit: &str, to_unit: &str) -> Result<Decimal, String> {
let ucum_from = calendar_to_ucum_unit(from_unit);
let ucum_to = calendar_to_ucum_unit(to_unit);
let value_f64 = value
.to_string()
.parse::<f64>()
.map_err(|e| format!("Failed to convert value to f64: {}", e))?;
let source_quantity = FhirQuantity::with_ucum_code(value_f64, &ucum_from);
match convert_quantity(&source_quantity, &ucum_to) {
Ok(converted) => {
let rounded_value = (converted.value * 1e10).round() / 1e10;
Decimal::try_from(rounded_value)
.or_else(|_| Decimal::from_str(&format!("{:.10}", rounded_value)))
.map_err(|e| format!("Failed to convert result to Decimal: {}", e))
}
Err(FhirError::UcumError(e)) => Err(format!("UCUM conversion error: {}", e)),
Err(e) => Err(format!("Conversion error: {}", e)),
}
}
pub fn units_are_comparable(unit1: &str, unit2: &str) -> bool {
let ucum_unit1 = calendar_to_ucum_unit(unit1);
let ucum_unit2 = calendar_to_ucum_unit(unit2);
is_comparable(&ucum_unit1, &ucum_unit2).unwrap_or(false)
}
pub fn quantities_are_equivalent(
value1: Decimal,
unit1: &str,
value2: Decimal,
unit2: &str,
) -> Result<bool, String> {
let ucum_unit1 = calendar_to_ucum_unit(unit1);
let ucum_unit2 = calendar_to_ucum_unit(unit2);
let value1_f64 = value1
.to_string()
.parse::<f64>()
.map_err(|e| format!("Failed to convert value1 to f64: {}", e))?;
let value2_f64 = value2
.to_string()
.parse::<f64>()
.map_err(|e| format!("Failed to convert value2 to f64: {}", e))?;
let _q1 = FhirQuantity::with_ucum_code(value1_f64, &ucum_unit1);
let q2 = FhirQuantity::with_ucum_code(value2_f64, &ucum_unit2);
if !is_comparable(&ucum_unit1, &ucum_unit2).unwrap_or(false) {
return Ok(false);
}
if ucum_unit1 == ucum_unit2 {
let tolerance = value1_f64.abs() * 0.01;
let diff = (value1_f64 - value2_f64).abs();
return Ok(diff <= tolerance);
}
match convert_quantity(&q2, &ucum_unit1) {
Ok(converted) => {
let max_value = value1_f64.abs().max(converted.value.abs());
let tolerance = max_value * 0.01;
let diff = (value1_f64 - converted.value).abs();
Ok(diff <= tolerance)
}
Err(_) => Ok(false),
}
}
pub fn multiply_units(unit1: &str, unit2: &str) -> Result<String, String> {
let ucum_unit1 = calendar_to_ucum_unit(unit1);
let ucum_unit2 = calendar_to_ucum_unit(unit2);
match unit_multiply(&ucum_unit1, &ucum_unit2) {
Ok(result) => Ok(result.expression),
Err(e) => Err(format!("Unit multiplication error: {}", e)),
}
}
pub fn divide_units(numerator: &str, denominator: &str) -> Result<String, String> {
let ucum_numerator = calendar_to_ucum_unit(numerator);
let ucum_denominator = calendar_to_ucum_unit(denominator);
match unit_divide(&ucum_numerator, &ucum_denominator) {
Ok(result) => Ok(result.expression),
Err(e) => Err(format!("Unit division error: {}", e)),
}
}
#[allow(dead_code)]
pub fn get_canonical_unit(unit: &str) -> Result<String, String> {
match analyse(unit) {
Ok(analysis) => Ok(analysis.expression),
Err(e) => Err(format!("Unit analysis error: {}", e)),
}
}
#[allow(dead_code)]
pub fn normalize_unit_string(unit: &str) -> String {
let cleaned = unit.trim_start_matches('{').trim_end_matches('}');
match cleaned {
"days" => "d".to_string(),
"day" => "d".to_string(),
"weeks" => "wk".to_string(),
"week" => "wk".to_string(),
"months" => "mo".to_string(),
"month" => "mo".to_string(),
"years" => "a".to_string(),
"year" => "a".to_string(),
"hours" => "h".to_string(),
"hour" => "h".to_string(),
"minutes" => "min".to_string(),
"minute" => "min".to_string(),
"seconds" => "s".to_string(),
"second" => "s".to_string(),
"milliseconds" => "ms".to_string(),
"millisecond" => "ms".to_string(),
_ => cleaned.to_string(),
}
}
pub fn calendar_to_ucum_unit(unit: &str) -> String {
match unit.to_lowercase().as_str() {
"year" | "years" => "a".to_string(),
"month" | "months" => "mo".to_string(),
"week" | "weeks" => "wk".to_string(),
"day" | "days" => "d".to_string(),
"hour" | "hours" => "h".to_string(),
"minute" | "minutes" => "min".to_string(),
"second" | "seconds" => "s".to_string(),
"millisecond" | "milliseconds" => "ms".to_string(),
_ => unit.to_string(),
}
}
#[allow(dead_code)]
pub fn ucum_to_calendar_unit(unit: &str) -> String {
match unit {
"a" => "year".to_string(),
"mo" => "month".to_string(),
"wk" => "week".to_string(),
"d" => "day".to_string(),
"h" => "hour".to_string(),
"min" => "minute".to_string(),
"s" => "second".to_string(),
"ms" => "millisecond".to_string(),
_ => unit.to_string(),
}
}
pub fn is_time_unit(unit: &str) -> bool {
matches!(
unit,
"a" | "mo"
| "wk"
| "d"
| "h"
| "min"
| "s"
| "ms"
| "year"
| "years"
| "month"
| "months"
| "week"
| "weeks"
| "day"
| "days"
| "hour"
| "hours"
| "minute"
| "minutes"
| "second"
| "seconds"
| "millisecond"
| "milliseconds"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_unit() {
assert!(validate_unit("mg"));
assert!(validate_unit("g"));
assert!(validate_unit("kg"));
assert!(validate_unit("m/s"));
assert!(validate_unit("cm2"));
assert!(!validate_unit("invalid_unit"));
}
#[test]
fn test_units_are_comparable() {
assert!(units_are_comparable("g", "mg"));
assert!(units_are_comparable("m", "cm"));
assert!(units_are_comparable("d", "wk"));
assert!(!units_are_comparable("g", "m"));
assert!(!units_are_comparable("s", "kg"));
}
#[test]
fn test_multiply_units() {
assert_eq!(multiply_units("m", "m").unwrap(), "m.m");
let result = multiply_units("kg", "m/s2").unwrap();
assert!(result == "kg.m/s2" || result == "kg.m.s-2");
}
#[test]
fn test_divide_units() {
assert_eq!(divide_units("m", "s").unwrap(), "m/s");
assert_eq!(divide_units("m", "m").unwrap(), "m/m");
}
#[test]
fn test_normalize_unit_string() {
assert_eq!(normalize_unit_string("{week}"), "wk");
assert_eq!(normalize_unit_string("days"), "d");
assert_eq!(normalize_unit_string("mg"), "mg");
}
}