Files
enok/src/inndata/toml_inn.rs
T

401 lines
14 KiB
Rust
Raw Normal View History

//! 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<Bygning> {
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<String>,
}
/// 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<Tillegg> {
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<String>)> {
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<Bygning> {
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<String>) {
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<String>) -> Option<Value> {
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<Value> = 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<String> {
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<String>) {
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);
}
}