use crate::foundation::Unit;
use std::collections::HashMap;
pub fn canonical_mnemonic(raw: &str) -> String {
let stem = strip_vintage(raw.trim());
let key = stem.to_ascii_uppercase();
let canonical = match key.as_str() {
"PHIE" | "PHI" | "PHI_E" | "EFFPHI" | "PHIEF" => "PHIE",
"PHIT" | "PHI_T" | "TOTPHI" => "PHIT",
"NPHI" | "TNPH" | "NEU" | "CNL" => "NPHI",
"SW" | "SWE" | "SUWI" | "SW_E" => "SW",
"SWT" | "SW_T" | "SWTOT" => "SWT",
"GR" | "GRC" | "SGR" | "CGR" | "GAMMA" => "GR",
"VSH" | "VCL" | "VSHALE" | "VCLAY" | "VSHGR" => "VSH",
"PERM" | "K" | "KLOGH" | "KH" | "PERMH" => "PERM",
"RHOB" | "DEN" | "DENS" | "ZDEN" => "RHOB",
"DT" | "AC" | "SONIC" | "DTCO" => "DT",
"RT" | "RES" | "ILD" | "LLD" | "RDEP" => "RT",
"NTG" | "NET_GROSS" | "N_G" => "NTG",
_ => return stem.to_string(),
};
canonical.to_string()
}
pub fn canonical_mnemonic_with(raw: &str, aliases: &NameMap) -> String {
aliases.get(raw).unwrap_or_else(|| canonical_mnemonic(raw))
}
fn strip_vintage(s: &str) -> &str {
if let Some((head, tail)) = s.rsplit_once('_') {
if tail.len() == 4 && tail.bytes().all(|b| b.is_ascii_digit()) {
return head;
}
}
s
}
pub fn parse_length_unit(s: &str) -> Option<Unit> {
match s.trim().to_ascii_lowercase().as_str() {
"m" | "metre" | "metres" | "meter" | "meters" => Some(Unit::Metres),
"ft" | "f" | "feet" | "foot" => Some(Unit::Feet),
_ => None,
}
}
pub fn is_percent_unit(s: &str) -> bool {
matches!(
s.trim().to_ascii_lowercase().as_str(),
"%" | "percent" | "pct" | "p.u." | "pu"
)
}
pub fn harmonise_fraction(value: f64, unit: &str) -> f64 {
if is_percent_unit(unit) {
value / 100.0
} else {
value
}
}
pub fn harmonise_length(value: f64, from: Unit, to: Unit) -> f64 {
from.convert(value, to)
}
#[derive(Debug, Clone, Default)]
pub struct NameMap {
map: HashMap<String, String>,
}
impl NameMap {
pub fn new() -> Self {
Self::default()
}
pub fn from_pairs(pairs: impl IntoIterator<Item = (String, String)>) -> Self {
let mut m = Self::new();
for (alias, canonical) in pairs {
m.insert(alias, canonical);
}
m
}
pub fn insert(&mut self, alias: impl Into<String>, canonical: impl Into<String>) {
self.map
.insert(alias.into().trim().to_ascii_lowercase(), canonical.into());
}
pub fn canonical(&self, name: &str) -> String {
self.get(name).unwrap_or_else(|| name.trim().to_string())
}
pub fn get(&self, name: &str) -> Option<String> {
self.map
.get(name.trim().to_ascii_lowercase().as_str())
.cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn mnemonic_aliases_resolve_case_insensitively() {
assert_eq!(canonical_mnemonic("phi"), "PHIE");
assert_eq!(canonical_mnemonic(" Suwi "), "SW");
assert_eq!(canonical_mnemonic("VCL"), "VSH");
assert_eq!(canonical_mnemonic("NPHI"), "NPHI");
assert_eq!(canonical_mnemonic("PHIT"), "PHIT");
assert_eq!(canonical_mnemonic("NTG_PhieLam"), "NTG_PhieLam");
}
#[test]
fn vintage_suffix_is_stripped() {
assert_eq!(canonical_mnemonic("PHIE_2025"), "PHIE");
assert_eq!(canonical_mnemonic("SW_2025"), "SW");
assert_eq!(canonical_mnemonic("VShale_2025"), "VSH");
assert_eq!(canonical_mnemonic("SWT_2025"), "SWT");
assert_eq!(canonical_mnemonic("PHIT_2025"), "PHIT");
assert_eq!(canonical_mnemonic("PERM_Lam"), "PERM_Lam");
}
#[test]
fn user_alias_map_resolves_the_unguessable() {
let aliases = NameMap::from_pairs([
("NTG_PhieLam".to_string(), "NTG".to_string()),
("PERM_Lam_2025".to_string(), "PERM".to_string()),
]);
assert_eq!(canonical_mnemonic_with("NTG_PhieLam", &aliases), "NTG");
assert_eq!(canonical_mnemonic_with("perm_lam_2025", &aliases), "PERM"); assert_eq!(canonical_mnemonic_with("PHIE_2025", &aliases), "PHIE");
assert_eq!(NameMap::new().get("nope"), None);
}
#[test]
fn length_units_parse() {
assert_eq!(parse_length_unit("M"), Some(Unit::Metres));
assert_eq!(parse_length_unit("feet"), Some(Unit::Feet));
assert_eq!(parse_length_unit("furlong"), None);
}
#[test]
fn percent_harmonises_to_fraction() {
assert!(is_percent_unit("%"));
assert!(!is_percent_unit("v/v"));
assert_relative_eq!(harmonise_fraction(25.0, "%"), 0.25);
assert_relative_eq!(harmonise_fraction(0.25, "v/v"), 0.25);
assert!(harmonise_fraction(f64::NAN, "%").is_nan());
}
#[test]
fn length_harmonises_via_unit() {
assert_relative_eq!(harmonise_length(100.0, Unit::Feet, Unit::Metres), 30.48);
}
#[test]
fn name_map_is_identity_for_unknowns() {
let m = NameMap::from_pairs([
("Brent Gp".to_string(), "Brent".to_string()),
("DUNLIN GROUP".to_string(), "Dunlin".to_string()),
]);
assert_eq!(m.canonical("brent gp"), "Brent"); assert_eq!(m.canonical("Dunlin Group"), "Dunlin");
assert_eq!(m.canonical(" Statfjord "), "Statfjord"); }
}