2026-09-15 20:05:20 +02:00
|
|
|
//! Leser `bygning.toml` og fletter inn scenarioendringer og tilleggsfiler.
|
2026-09-14 18:03:00 +02:00
|
|
|
|
|
|
|
|
use crate::modell::{Bygning, Scenario};
|
|
|
|
|
use crate::Result;
|
|
|
|
|
use anyhow::Context;
|
2026-09-15 20:05:20 +02:00
|
|
|
use serde::Deserialize;
|
2026-09-14 18:03:00 +02:00
|
|
|
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()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
/// 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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
/// 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
|
2026-09-15 20:05:20 +02:00
|
|
|
/// 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.
|
2026-09-14 18:03:00 +02:00
|
|
|
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
|
|
|
|
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
2026-09-15 20:05:20 +02:00
|
|
|
flett(&mut grunn, &s.endringer, &mut Vec::new());
|
2026-09-14 18:03:00 +02:00
|
|
|
grunn
|
|
|
|
|
.try_into()
|
|
|
|
|
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:51:19 +02:00
|
|
|
/// 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()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
fn flett(grunn: &mut Value, over: &Value, bom: &mut Vec<String>) {
|
2026-09-14 18:03:00 +02:00
|
|
|
match (grunn, over) {
|
|
|
|
|
(Value::Table(g), Value::Table(o)) => {
|
|
|
|
|
for (k, v) in o {
|
|
|
|
|
if k == "slett" {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
match g.get_mut(k) {
|
2026-09-15 20:05:20 +02:00
|
|
|
Some(eksisterende) => flett(eksisterende, v, bom),
|
2026-09-14 18:03:00 +02:00
|
|
|
None => {
|
|
|
|
|
g.insert(k.clone(), v.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(Value::Array(g), Value::Array(o)) => {
|
|
|
|
|
for element in o {
|
|
|
|
|
match navnenokkel(element) {
|
2026-09-15 20:05:20 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-14 18:03:00 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-09-15 20:05:20 +02:00
|
|
|
Some(i) => flett(&mut g[i], element, bom),
|
2026-09-14 18:03:00 +02:00
|
|
|
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"]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
#[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()]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:51:19 +02:00
|
|
|
#[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()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
#[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"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:03:00 +02:00
|
|
|
#[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);
|
|
|
|
|
}
|
|
|
|
|
}
|