//! Leser `bygning.toml` og fletter inn scenarioendringer og tilleggsfiler. use crate::modell::{Bygning, Scenario}; use crate::Result; use anyhow::Context; use serde::Deserialize; use std::path::Path; use toml::Value; pub fn les_bygning(sti: &Path) -> Result { let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?; toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display())) } /// Valg for selve importen, fra tabellen `[import]` i en tilleggsfil. #[derive(Deserialize, Default, Clone, Debug)] #[serde(deny_unknown_fields)] pub struct Importvalg { /// Rom som står uoppvarmet og holdes utenfor oppvarmet BRA. Et rom treffes når navnet /// inneholder en av tekstene, uten hensyn til store og små bokstaver. #[serde(default)] pub uoppvarmede_rom: Vec, } /// En tilleggsfil til en importert modell: det modellen ikke beskriver, som drift, varmeanlegg /// og scenarioer, og rettelser til det den beskriver. pub struct Tillegg { pub endringer: Value, pub import: Importvalg, } pub fn les_tillegg(sti: &Path) -> Result { let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?; let mut endringer: Value = toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))?; let import = match endringer.as_table_mut().and_then(|t| t.remove("import")) { Some(v) => v .try_into() .with_context(|| format!("tolker [import] i {}", sti.display()))?, None => Importvalg::default(), }; Ok(Tillegg { endringer, import }) } /// Fletter en tilleggsfil inn i en importert bygning, etter de samme reglene som scenarioene. /// /// Returnerer også navnemønstrene med `*` som ikke traff noen oppføring, slik at en tilleggsfil /// som har gått ut av takt med modellen blir synlig. pub fn flett_tillegg(b: &Bygning, tillegg: &Value) -> Result<(Bygning, Vec)> { let mut grunn = Value::try_from(b).context("serialiserer den importerte bygningen")?; let mut bom = Vec::new(); flett(&mut grunn, tillegg, &mut bom); let bygning = grunn.try_into().context( "tilleggsfilen gir en ugyldig bygningsbeskrivelse. Et navn som ikke finnes i modellen, \ legges til som ny oppføring og må da være komplett", )?; Ok((bygning, bom)) } /// Lager en ny bygning der scenarioets endringer er flettet inn. /// /// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches /// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges /// til, og `slett = true` fjerner et element. Et navn med `*` er et mønster som treffer alle /// oppføringer det passer på, for eksempel `"Yttervegg*"`. Andre verdier erstattes. pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result { let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?; flett(&mut grunn, &s.endringer, &mut Vec::new()); grunn .try_into() .with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn)) } /// Forskjellen mellom to bygningsbeskrivelser, som en tilleggsfil. /// /// Resultatet er det minste som må flettes inn i `fra` for å få `til`: endrede og nye felter, /// nye elementer i lister, og `slett = true` for elementer som bare finnes i `fra`. Fletting /// kan ikke fjerne et enkeltfelt, så felter som bare finnes i `fra`, listes som merknader. pub fn differanse(fra: &Value, til: &Value) -> (Value, Vec) { let mut merknader = Vec::new(); let v = diff_verdi(fra, til, "", &mut merknader).unwrap_or(Value::Table(Default::default())); (v, merknader) } fn diff_verdi(fra: &Value, til: &Value, sti: &str, merknader: &mut Vec) -> Option { if fra == til { return None; } match (fra, til) { (Value::Table(f), Value::Table(t)) => { let mut ut = toml::map::Map::new(); for (k, v) in t { let bane = if sti.is_empty() { k.clone() } else { format!("{sti}.{k}") }; match f.get(k) { Some(gammel) => { if let Some(d) = diff_verdi(gammel, v, &bane, merknader) { ut.insert(k.clone(), d); } } None => { ut.insert(k.clone(), v.clone()); } } } for k in f.keys() { if !t.contains_key(k) { let bane = if sti.is_empty() { k.clone() } else { format!("{sti}.{k}") }; merknader.push(format!( "«{bane}» finnes i modellen, men ikke i bygningsbeskrivelsen. En tilleggsfil kan ikke fjerne et felt." )); } } (!ut.is_empty()).then_some(Value::Table(ut)) } (Value::Array(f), Value::Array(t)) if t.iter().all(|e| navnenokkel(e).is_some()) => { let mut ut: Vec = Vec::new(); for element in t { let navn = navnenokkel(element).unwrap_or_default(); match f.iter().find(|e| navnenokkel(e).as_deref() == Some(&navn)) { Some(gammel) => { let bane = format!("{sti}.{navn}"); if let Some(Value::Table(mut endring)) = diff_verdi(gammel, element, &bane, merknader) { endring.insert("navn".into(), Value::String(navn)); ut.push(Value::Table(endring)); } } None => ut.push(element.clone()), } } for gammel in f { let navn = navnenokkel(gammel).unwrap_or_default(); if !t.iter().any(|e| navnenokkel(e).as_deref() == Some(&navn)) { let mut slett = toml::map::Map::new(); slett.insert("navn".into(), Value::String(navn)); slett.insert("slett".into(), Value::Boolean(true)); ut.push(Value::Table(slett)); } } (!ut.is_empty()).then_some(Value::Array(ut)) } _ => Some(til.clone()), } } /// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst. fn passer(moenster: &str, navn: &str) -> bool { let deler: Vec<&str> = moenster.split('*').collect(); let (forst, sist) = (deler[0], deler[deler.len() - 1]); if deler.len() == 1 { return moenster == navn; } let Some(mut rest) = navn.strip_prefix(forst) else { return false; }; for del in &deler[1..deler.len() - 1] { match rest.find(del) { Some(i) => rest = &rest[i + del.len()..], None => return false, } } rest.ends_with(sist) } fn navnenokkel(v: &Value) -> Option { let t = v.as_table()?; t.get("navn") .and_then(|x| x.as_str()) .or_else(|| t.get("type").and_then(|x| x.as_str())) .map(|s| s.to_string()) } fn skal_slettes(v: &Value) -> bool { v.as_table() .and_then(|t| t.get("slett")) .and_then(|x| x.as_bool()) .unwrap_or(false) } fn flett(grunn: &mut Value, over: &Value, bom: &mut Vec) { match (grunn, over) { (Value::Table(g), Value::Table(o)) => { for (k, v) in o { if k == "slett" { continue; } match g.get_mut(k) { Some(eksisterende) => flett(eksisterende, v, bom), None => { g.insert(k.clone(), v.clone()); } } } } (Value::Array(g), Value::Array(o)) => { for element in o { match navnenokkel(element) { Some(moenster) if moenster.contains('*') => { let treffer = |e: &Value| navnenokkel(e).is_some_and(|n| passer(&moenster, &n)); let antall = g.iter().filter(|e| treffer(e)).count(); if antall == 0 { bom.push(moenster.clone()); } else if skal_slettes(element) { g.retain(|e| !treffer(e)); } else { // Mønsteret skal ikke erstatte navnene det treffer. let mut endring = element.clone(); if let Some(t) = endring.as_table_mut() { t.remove("navn"); } for e in g.iter_mut().filter(|e| treffer(e)) { flett(e, &endring, bom); } } } Some(navn) => { let treff = g .iter() .position(|e| navnenokkel(e).as_deref() == Some(&navn)); match treff { Some(i) if skal_slettes(element) => { g.remove(i); } Some(i) => flett(&mut g[i], element, bom), None if skal_slettes(element) => {} None => g.push(element.clone()), } } None => g.push(element.clone()), } } } (g, o) => *g = o.clone(), } } #[cfg(test)] mod tester { use super::*; const GRUNN: &str = r#" [bygning] navn = "Testhus" kategori = "smaahus" byggeaar = 1948 oppvarmet_bra = 100.0 [[flate]] navn = "Yttervegg" type = "vegg" areal = 80.0 u_verdi = 1.0 [[flate]] navn = "Tak" type = "tak" areal = 100.0 u_verdi = 0.8 "#; #[test] fn scenario_endrer_u_verdi_for_navngitt_flate() { let b: Bygning = toml::from_str(GRUNN).unwrap(); let s = Scenario { navn: "Etterisolert".into(), beskrivelse: None, endringer: toml::from_str(r#"flate = [{ navn = "Yttervegg", u_verdi = 0.22 }]"#) .unwrap(), }; let ny = slaa_sammen_scenario(&b, &s).unwrap(); assert_eq!(ny.flate.len(), 2); assert_eq!(ny.flate[0].u_verdi, Some(0.22)); assert_eq!(ny.flate[1].u_verdi, Some(0.8)); } #[test] fn scenario_legger_til_og_sletter() { let b: Bygning = toml::from_str(GRUNN).unwrap(); let s = Scenario { navn: "Ombygd".into(), beskrivelse: None, endringer: toml::from_str( r#"flate = [{ navn = "Tak", slett = true }, { navn = "Nytt tak", type = "tak", areal = 100.0, u_verdi = 0.13 }]"#, ) .unwrap(), }; let ny = slaa_sammen_scenario(&b, &s).unwrap(); let navn: Vec<&str> = ny.flate.iter().map(|f| f.navn.as_str()).collect(); assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]); } #[test] fn moenster_i_navn_treffer_flere_oppforinger() { let b: Bygning = toml::from_str(GRUNN).unwrap(); let mut grunn = Value::try_from(&b).unwrap(); let mut bom = Vec::new(); let over: Value = toml::from_str( r#"flate = [{ navn = "*", masseklasse = "lett" }, { navn = "Yttervegg*", u_verdi = 0.3 }, { navn = "Kjeller*", u_verdi = 0.2 }]"#, ) .unwrap(); flett(&mut grunn, &over, &mut bom); let ny: Bygning = grunn.try_into().unwrap(); assert_eq!(ny.flate.len(), 2); assert_eq!(ny.flate[0].navn, "Yttervegg"); assert_eq!(ny.flate[0].u_verdi, Some(0.3)); assert_eq!(ny.flate[1].u_verdi, Some(0.8)); assert!(ny.flate.iter().all(|f| f.masseklasse.is_some())); assert_eq!(bom, vec!["Kjeller*".to_string()]); } #[test] fn differansen_gjenskaper_den_manuelle_filen() { let importert: Bygning = toml::from_str(GRUNN).unwrap(); let manuell: Bygning = toml::from_str( r#" [bygning] navn = "Testhus" kategori = "smaahus" byggeaar = 1948 oppvarmet_bra = 100.0 [[flate]] navn = "Yttervegg" type = "vegg" areal = 80.0 u_verdi = 0.22 [[flate]] navn = "Kjellervegg" type = "vegg" areal = 30.0 u_verdi = 3.13 [[scenario]] navn = "Nye vinduer" endringer.flate = [{ navn = "Yttervegg", u_verdi = 0.18 }] "#, ) .unwrap(); let (patch, merknader) = differanse( &Value::try_from(&importert).unwrap(), &Value::try_from(&manuell).unwrap(), ); // Taket finnes bare i importen, og skal slettes av tillegget. let tekst = toml::to_string(&patch).unwrap(); assert!(tekst.contains("slett"), "{tekst}"); assert!(merknader.is_empty(), "{merknader:?}"); let (gjenskapt, bom) = flett_tillegg(&importert, &patch).unwrap(); assert!(bom.is_empty()); assert_eq!( Value::try_from(&gjenskapt).unwrap(), Value::try_from(&manuell).unwrap() ); } #[test] fn moenster_passer() { assert!(passer("V-2*", "V-2 #3306")); assert!(passer("*sør", "Yttervegg, sør")); assert!(passer( "Kjellermur*uisolert*", "Kjellermur 30 cm uisolert, øst" )); assert!(!passer("V-2*", "VD-2 #11162")); assert!(passer("Tak", "Tak")); assert!(!passer("Tak", "Taket")); } #[test] fn scenario_endrer_skalarfelt() { let b: Bygning = toml::from_str(GRUNN).unwrap(); let s = Scenario { navn: "Varmepumpe".into(), beskrivelse: None, endringer: toml::from_str( r#" [infiltrasjon] n50 = 3.0 [oppvarming] kilde = [{ type = "luft_luft_vp", andel_rom = 0.6 }] "#, ) .unwrap(), }; let ny = slaa_sammen_scenario(&b, &s).unwrap(); assert_eq!(ny.infiltrasjon.n50, Some(3.0)); assert_eq!(ny.oppvarming.kilde.len(), 1); } }