2026-09-14 18:03:00 +02:00
|
|
|
|
//! Innebygde datafiler (normerte verdier, karakterskala, vekting, SPF, materialer, klimakorreksjon).
|
|
|
|
|
|
//! Filene ligger i `data/` og bygges inn i binæren; de kan overstyres med `--data DIR`.
|
|
|
|
|
|
|
|
|
|
|
|
use crate::modell::Kategori;
|
|
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
|
|
const SKALA: &str = include_str!("../data/skala.toml");
|
|
|
|
|
|
const VEKTING: &str = include_str!("../data/vekting.toml");
|
|
|
|
|
|
const SPF: &str = include_str!("../data/varmepumpe_spf.toml");
|
|
|
|
|
|
const MATERIALER: &str = include_str!("../data/materialer.toml");
|
|
|
|
|
|
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
|
|
|
|
|
|
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
|
|
|
|
|
|
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
|
2026-09-15 20:05:20 +02:00
|
|
|
|
const NORMERTE_BARNEHAGE: &str = include_str!("../data/normerte/barnehage.toml");
|
|
|
|
|
|
const NORMERTE_KONTORBYGG: &str = include_str!("../data/normerte/kontorbygg.toml");
|
|
|
|
|
|
const NORMERTE_SKOLEBYGG: &str = include_str!("../data/normerte/skolebygg.toml");
|
|
|
|
|
|
const NORMERTE_IDRETTSBYGG: &str = include_str!("../data/normerte/idrettsbygg.toml");
|
|
|
|
|
|
const NORMERTE_KULTURBYGG: &str = include_str!("../data/normerte/kulturbygg.toml");
|
2026-09-14 18:03:00 +02:00
|
|
|
|
|
|
|
|
|
|
/// Katalog som kan overstyre de innebygde datafilene.
|
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
|
pub struct Datakilde {
|
|
|
|
|
|
pub katalog: Option<PathBuf>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Datakilde {
|
|
|
|
|
|
pub fn ny(katalog: Option<PathBuf>) -> Self {
|
|
|
|
|
|
Self { katalog }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn les(&self, relativ: &str, innebygd: &str) -> Result<String> {
|
|
|
|
|
|
if let Some(k) = &self.katalog {
|
|
|
|
|
|
let p = k.join(relativ);
|
|
|
|
|
|
if p.exists() {
|
|
|
|
|
|
return std::fs::read_to_string(&p)
|
|
|
|
|
|
.with_context(|| format!("leser {}", p.display()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(innebygd.to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn skala(&self) -> Result<Skala> {
|
|
|
|
|
|
let s = self.les("skala.toml", SKALA)?;
|
|
|
|
|
|
let m: HashMap<String, SkalaKategori> = toml::from_str(&s).context("skala.toml")?;
|
|
|
|
|
|
Ok(Skala(m))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn vekting(&self) -> Result<Vekting> {
|
|
|
|
|
|
let s = self.les("vekting.toml", VEKTING)?;
|
|
|
|
|
|
toml::from_str(&s).context("vekting.toml")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn varmepumper(&self) -> Result<Vec<VarmepumpeSpf>> {
|
|
|
|
|
|
let s = self.les("varmepumpe_spf.toml", SPF)?;
|
|
|
|
|
|
let t: SpfFil = toml::from_str(&s).context("varmepumpe_spf.toml")?;
|
|
|
|
|
|
Ok(t.varmepumpe)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn materialer(&self) -> Result<Materialer> {
|
|
|
|
|
|
let s = self.les("materialer.toml", MATERIALER)?;
|
|
|
|
|
|
toml::from_str(&s).context("materialer.toml")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn normerte(&self, kategori: Kategori) -> Result<Normerte> {
|
|
|
|
|
|
let (fil, innebygd) = match kategori {
|
|
|
|
|
|
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
|
|
|
|
|
|
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
|
2026-09-15 20:05:20 +02:00
|
|
|
|
Kategori::Barnehage => ("normerte/barnehage.toml", NORMERTE_BARNEHAGE),
|
|
|
|
|
|
Kategori::Kontorbygg => ("normerte/kontorbygg.toml", NORMERTE_KONTORBYGG),
|
|
|
|
|
|
Kategori::Skolebygg => ("normerte/skolebygg.toml", NORMERTE_SKOLEBYGG),
|
|
|
|
|
|
Kategori::Idrettsbygg => ("normerte/idrettsbygg.toml", NORMERTE_IDRETTSBYGG),
|
|
|
|
|
|
Kategori::Kulturbygg => ("normerte/kulturbygg.toml", NORMERTE_KULTURBYGG),
|
2026-09-14 18:03:00 +02:00
|
|
|
|
annen => {
|
2026-09-15 20:05:20 +02:00
|
|
|
|
// Kategorier uten offentlig tilgjengelige normerte verdier krever egen datafil.
|
2026-09-14 18:03:00 +02:00
|
|
|
|
let fil = format!("normerte/{}.toml", annen.nokkel());
|
|
|
|
|
|
let p = self
|
|
|
|
|
|
.katalog
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|k| k.join(&fil))
|
|
|
|
|
|
.filter(|p| p.exists())
|
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
|
anyhow!(
|
|
|
|
|
|
"normerte inndata for kategori «{}» mangler: legg {} i data-katalogen",
|
|
|
|
|
|
annen.nokkel(),
|
|
|
|
|
|
fil
|
|
|
|
|
|
)
|
|
|
|
|
|
})?;
|
|
|
|
|
|
let s = std::fs::read_to_string(&p)?;
|
|
|
|
|
|
return toml::from_str(&s).with_context(|| p.display().to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let s = self.les(fil, innebygd)?;
|
|
|
|
|
|
toml::from_str(&s).context(fil)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn klimakorreksjon(&self) -> Result<Klimakorreksjon> {
|
|
|
|
|
|
let s = self.les("klimakorreksjon.csv", KLIMAKORREKSJON)?;
|
|
|
|
|
|
Klimakorreksjon::fra_csv(&s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── karakterskala ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct SkalaKategori {
|
|
|
|
|
|
pub navn: String,
|
|
|
|
|
|
/// Øvre grense for A, B, C, D, E, F (kWh/m²). G har ingen grense.
|
|
|
|
|
|
pub grenser: [f64; 6],
|
|
|
|
|
|
/// Arealkorreksjon k for hver grense: grense + k / A.
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub arealkorreksjon: Option<[f64; 6]>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub struct Skala(pub HashMap<String, SkalaKategori>);
|
|
|
|
|
|
|
|
|
|
|
|
impl Skala {
|
|
|
|
|
|
pub fn for_kategori(&self, k: Kategori) -> Result<&SkalaKategori> {
|
|
|
|
|
|
self.0
|
|
|
|
|
|
.get(k.nokkel())
|
|
|
|
|
|
.ok_or_else(|| anyhow!("karakterskala mangler for {}", k.nokkel()))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── vekting ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Vekting {
|
|
|
|
|
|
pub faktor: Vektingsfaktorer,
|
|
|
|
|
|
pub regler: Vektingsregler,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Vektingsfaktorer {
|
|
|
|
|
|
pub elektrisitet: f64,
|
|
|
|
|
|
pub fjernvarme: f64,
|
|
|
|
|
|
pub fjernkjoling: f64,
|
|
|
|
|
|
pub biobrensel_lukket: f64,
|
|
|
|
|
|
pub biobrensel_aapen: f64,
|
|
|
|
|
|
pub olje: f64,
|
|
|
|
|
|
pub gass: f64,
|
|
|
|
|
|
pub annet: f64,
|
|
|
|
|
|
pub eksportert: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Vektingsregler {
|
|
|
|
|
|
pub vedovn_maks_andel_rom: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── varmepumper ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct SpfFil {
|
|
|
|
|
|
varmepumpe: Vec<VarmepumpeSpf>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct VarmepumpeSpf {
|
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
|
pub typ: String,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub turtemperatur: Option<f64>,
|
|
|
|
|
|
pub beskrivelse: String,
|
|
|
|
|
|
pub smaahus: f64,
|
|
|
|
|
|
pub boligblokk: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Slår opp veiledende SPF for en varmepumpetype. Ved turtemperatur velges nærmeste tabellverdi.
|
|
|
|
|
|
pub fn spf_oppslag(
|
|
|
|
|
|
tabell: &[VarmepumpeSpf],
|
|
|
|
|
|
typ: &str,
|
|
|
|
|
|
turtemp: Option<f64>,
|
|
|
|
|
|
kategori: Kategori,
|
|
|
|
|
|
) -> Option<f64> {
|
|
|
|
|
|
let mut best: Option<(f64, &VarmepumpeSpf)> = None;
|
|
|
|
|
|
for rad in tabell.iter().filter(|r| r.typ == typ) {
|
|
|
|
|
|
let avstand = match (rad.turtemperatur, turtemp) {
|
|
|
|
|
|
(Some(t), Some(v)) => (t - v).abs(),
|
|
|
|
|
|
(Some(t), None) => (t - 45.0).abs(), // uten oppgitt turtemperatur: radiatoranlegg 45/37
|
|
|
|
|
|
_ => 0.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
if best.is_none_or(|(a, _)| avstand < a) {
|
|
|
|
|
|
best = Some((avstand, rad));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
best.map(|(_, r)| match kategori {
|
|
|
|
|
|
Kategori::Smaahus => r.smaahus,
|
|
|
|
|
|
_ => r.boligblokk,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── materialer ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Materialer {
|
|
|
|
|
|
pub materiale: Vec<Materiale>,
|
|
|
|
|
|
pub periode: Vec<Periode>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Materiale {
|
|
|
|
|
|
pub navn: String,
|
|
|
|
|
|
pub lambda: f64,
|
|
|
|
|
|
pub rho: f64,
|
|
|
|
|
|
pub c: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Periode {
|
|
|
|
|
|
pub navn: String,
|
|
|
|
|
|
pub fra: i32,
|
|
|
|
|
|
pub til: i32,
|
|
|
|
|
|
pub vegg: f64,
|
|
|
|
|
|
pub tak: f64,
|
|
|
|
|
|
pub gulv: f64,
|
|
|
|
|
|
pub vindu: f64,
|
|
|
|
|
|
pub kuldebro: f64,
|
|
|
|
|
|
pub n50: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Materialer {
|
|
|
|
|
|
pub fn materiale(&self, navn: &str) -> Option<&Materiale> {
|
|
|
|
|
|
self.materiale
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|m| m.navn.eq_ignore_ascii_case(navn))
|
|
|
|
|
|
}
|
|
|
|
|
|
pub fn periode_for(&self, byggeaar: i32) -> Option<&Periode> {
|
|
|
|
|
|
self.periode
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|p| byggeaar >= p.fra && byggeaar <= p.til)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── normerte inndata ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Normerte {
|
|
|
|
|
|
pub kategori: String,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub kilde: String,
|
|
|
|
|
|
pub settpunkt: Settpunkt,
|
|
|
|
|
|
pub driftstid: Driftstider,
|
|
|
|
|
|
pub belysning: EffektPost,
|
|
|
|
|
|
pub utstyr: EffektPost,
|
|
|
|
|
|
pub varmtvann: EffektPost,
|
|
|
|
|
|
pub personer: PersonPost,
|
|
|
|
|
|
pub ventilasjon: VentilasjonNormert,
|
|
|
|
|
|
pub termisk: TermiskNormert,
|
|
|
|
|
|
pub pumper: PumperNormert,
|
|
|
|
|
|
/// Valgfrie 24-verdiers profiler (W/m² per klokketime) som overstyrer driftstid × effekt.
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub profil_belysning: Option<[f64; 24]>,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub profil_utstyr: Option<[f64; 24]>,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub profil_varmtvann: Option<[f64; 24]>,
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub profil_personer: Option<[f64; 24]>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Settpunkt {
|
|
|
|
|
|
pub oppvarming: f64,
|
|
|
|
|
|
pub oppvarming_natt: f64,
|
|
|
|
|
|
pub kjoling: f64,
|
|
|
|
|
|
pub ventilasjon_tilluft: f64,
|
2026-09-15 20:05:20 +02:00
|
|
|
|
/// Tilluftstemperatur når anlegget har kjølebatteri. Utelatt: lik `ventilasjon_tilluft`.
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub ventilasjon_tilluft_kjoling: Option<f64>,
|
2026-09-14 18:03:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-15 20:05:20 +02:00
|
|
|
|
#[derive(Deserialize, serde::Serialize, Clone, Debug)]
|
2026-09-14 18:03:00 +02:00
|
|
|
|
pub struct Driftstider {
|
|
|
|
|
|
pub oppvarming: Tidsrom,
|
|
|
|
|
|
pub belysning: Tidsrom,
|
|
|
|
|
|
pub utstyr: Tidsrom,
|
|
|
|
|
|
pub varmtvann: Tidsrom,
|
|
|
|
|
|
pub personer: Tidsrom,
|
|
|
|
|
|
pub ventilasjon: Tidsrom,
|
2026-09-15 20:05:20 +02:00
|
|
|
|
/// Driftsdager per uke, regnet fra mandag (NS 3031 Tabell A.3).
|
2026-09-14 18:03:00 +02:00
|
|
|
|
#[serde(default = "sju")]
|
|
|
|
|
|
pub dager_per_uke: u8,
|
2026-09-15 20:05:20 +02:00
|
|
|
|
/// Driftsuker per år (NS 3031 Tabell A.3). Ukene uten drift legges i ferier, se [`FERIEUKER`].
|
|
|
|
|
|
#[serde(default = "femtito")]
|
|
|
|
|
|
pub uker_per_aar: u8,
|
2026-09-14 18:03:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn sju() -> u8 {
|
|
|
|
|
|
7
|
|
|
|
|
|
}
|
2026-09-15 20:05:20 +02:00
|
|
|
|
fn femtito() -> u8 {
|
|
|
|
|
|
52
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Uker uten drift, i den rekkefølgen de tas ut når en kategori har færre enn 52 driftsuker:
|
|
|
|
|
|
/// fellesferien først, deretter jul, vinterferie, påske og høstferie, og så uke for uke
|
|
|
|
|
|
/// utover sommeren og nyttår.
|
|
|
|
|
|
pub const FERIEUKER: [u8; 16] = [28, 29, 30, 31, 27, 32, 52, 8, 14, 40, 26, 33, 1, 25, 34, 9];
|
|
|
|
|
|
|
|
|
|
|
|
impl Driftstider {
|
|
|
|
|
|
/// Om døgn nummer `dag` (0 = 1. januar) er en driftsdag.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Året regnes å begynne på en mandag. Døgn 364 hører til uke 52.
|
|
|
|
|
|
pub fn driftsdag(&self, dag: usize) -> bool {
|
|
|
|
|
|
if dag % 7 >= self.dager_per_uke.min(7) as usize {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
let uke = (dag / 7 + 1).min(52) as u8;
|
|
|
|
|
|
let fri = (52u8.saturating_sub(self.uker_per_aar) as usize).min(FERIEUKER.len());
|
|
|
|
|
|
!FERIEUKER[..fri].contains(&uke)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn driftsdager_per_aar(&self) -> usize {
|
|
|
|
|
|
(0..365).filter(|&d| self.driftsdag(d)).count()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-14 18:03:00 +02:00
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
|
|
|
|
|
|
pub struct Tidsrom {
|
|
|
|
|
|
pub start: u8,
|
|
|
|
|
|
pub slutt: u8,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Tidsrom {
|
|
|
|
|
|
pub fn aktiv(&self, klokketime: usize) -> bool {
|
|
|
|
|
|
let h = klokketime as u8;
|
|
|
|
|
|
if self.start <= self.slutt {
|
|
|
|
|
|
h >= self.start && h < self.slutt
|
|
|
|
|
|
} else {
|
|
|
|
|
|
h >= self.start || h < self.slutt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pub fn timer(&self) -> f64 {
|
|
|
|
|
|
if self.start <= self.slutt {
|
|
|
|
|
|
(self.slutt - self.start) as f64
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(24 + self.slutt - self.start) as f64
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct EffektPost {
|
|
|
|
|
|
pub effekt: f64,
|
|
|
|
|
|
pub varmetilskudd: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct PersonPost {
|
|
|
|
|
|
pub varmetilskudd: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct VentilasjonNormert {
|
|
|
|
|
|
pub luftmengde_driftstid: f64,
|
|
|
|
|
|
pub luftmengde_utenfor_driftstid: f64,
|
|
|
|
|
|
pub sfp: f64,
|
|
|
|
|
|
pub temperaturvirkningsgrad: f64,
|
|
|
|
|
|
pub frostsikring_virkningsgrad: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct TermiskNormert {
|
|
|
|
|
|
pub varmekapasitet_normalisert: f64,
|
|
|
|
|
|
pub kuldebroer_normalisert: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
|
|
|
|
pub struct PumperNormert {
|
|
|
|
|
|
pub effekt_vannbaaren: f64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── klimakorreksjon ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Klimakorrigeringsfaktorer KF per klimasted og bygningskategori (NS 3031:2025, tillegg).
|
|
|
|
|
|
pub struct Klimakorreksjon {
|
|
|
|
|
|
pub kategorier: Vec<String>,
|
|
|
|
|
|
pub steder: Vec<(String, String, Vec<f64>)>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Klimakorreksjon {
|
|
|
|
|
|
fn fra_csv(s: &str) -> Result<Self> {
|
|
|
|
|
|
let mut rdr = csv::ReaderBuilder::new()
|
|
|
|
|
|
.has_headers(true)
|
|
|
|
|
|
.from_reader(s.as_bytes());
|
|
|
|
|
|
let kategorier: Vec<String> = rdr
|
|
|
|
|
|
.headers()?
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.skip(2)
|
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let mut steder = Vec::new();
|
|
|
|
|
|
for rad in rdr.records() {
|
|
|
|
|
|
let rad = rad?;
|
|
|
|
|
|
let sted = rad.get(0).unwrap_or("").to_string();
|
|
|
|
|
|
let fylke = rad.get(1).unwrap_or("").to_string();
|
|
|
|
|
|
let verdier: Result<Vec<f64>, _> =
|
|
|
|
|
|
rad.iter().skip(2).map(|v| v.parse::<f64>()).collect();
|
|
|
|
|
|
steder.push((
|
|
|
|
|
|
sted,
|
|
|
|
|
|
fylke,
|
|
|
|
|
|
verdier.context("klimakorreksjon.csv: tallverdi")?,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(Self { kategorier, steder })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// KF for et klimasted og en kategori. Navnet sammenlignes normalisert (æøå → ae/o/aa).
|
|
|
|
|
|
pub fn faktor(&self, sted: &str, kategori: Kategori) -> Option<f64> {
|
|
|
|
|
|
let kol = self
|
|
|
|
|
|
.kategorier
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.position(|k| k == kategori.nokkel())?;
|
|
|
|
|
|
let mål = normaliser_stedsnavn(sted);
|
|
|
|
|
|
self.steder
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|(s, _, _)| normaliser_stedsnavn(s) == mål)
|
|
|
|
|
|
.map(|(_, _, v)| v[kol])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Normaliserer stedsnavn for sammenligning: små bokstaver, æ→ae, ø→o, å→aa, uten parentes-fylke.
|
|
|
|
|
|
pub fn normaliser_stedsnavn(s: &str) -> String {
|
|
|
|
|
|
let s = s.split('(').next().unwrap_or(s).trim();
|
|
|
|
|
|
s.chars()
|
|
|
|
|
|
.flat_map(|c| match c.to_ascii_lowercase() {
|
|
|
|
|
|
'æ' => "ae".chars().collect::<Vec<_>>(),
|
|
|
|
|
|
'ø' => "o".chars().collect(),
|
|
|
|
|
|
'å' => "aa".chars().collect(),
|
|
|
|
|
|
'Æ' => "ae".chars().collect(),
|
|
|
|
|
|
'Ø' => "o".chars().collect(),
|
|
|
|
|
|
'Å' => "aa".chars().collect(),
|
|
|
|
|
|
'-' | ' ' => vec![],
|
|
|
|
|
|
c => vec![c],
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn finnes(p: &Path) -> bool {
|
|
|
|
|
|
p.exists()
|
|
|
|
|
|
}
|