Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7d77547ac
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enok"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "enok"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
||||
license = "MIT"
|
||||
|
||||
+195
-22
@@ -26,6 +26,9 @@ pub struct Importrapport {
|
||||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
||||
}
|
||||
|
||||
/// Konstruksjonsnavn og avrundet plassering, som skiller én bygningsdel fra en annen.
|
||||
type Dublettnokkel = (String, i64, i64, i64, i64);
|
||||
|
||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||
|
||||
@@ -182,9 +185,59 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
plass,
|
||||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||||
normal: flate.map(|f| f.normal),
|
||||
z: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.hoyde).unwrap_or(0.0),
|
||||
});
|
||||
}
|
||||
|
||||
// Noen eksporter legger den samme bygningsdelen i flere etasjer. To deler med samme
|
||||
// konstruksjon, samme areal og samme plassering er den samme veggen eller det samme
|
||||
// dekket. Kopiene blir stående, slik at vinduene i dem finner veggen sin, men de telles
|
||||
// ikke med i klimaskjermen. Av kopiene beholdes den som ligger i den etasjen høyden
|
||||
// tilsier, slik at en kjellervegg havner i kjelleren.
|
||||
let konstruksjon = |navn: &str| navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||||
let mut grupper: Vec<(Dublettnokkel, Vec<u64>)> = Vec::new();
|
||||
for d in &deler {
|
||||
let Some((x, y, _, _, _)) = d.plass else {
|
||||
continue;
|
||||
};
|
||||
let cm = |v: f64| (v * 50.0).round() as i64;
|
||||
let nokkel = (
|
||||
konstruksjon(&d.navn),
|
||||
cm(x),
|
||||
cm(y),
|
||||
cm(d.z),
|
||||
(d.areal * 100.0).round() as i64,
|
||||
);
|
||||
match grupper.iter_mut().find(|(k, _)| *k == nokkel) {
|
||||
Some((_, medlemmer)) => medlemmer.push(d.nr),
|
||||
None => grupper.push((nokkel, vec![d.nr])),
|
||||
}
|
||||
}
|
||||
let mut dublett_av: HashMap<u64, u64> = HashMap::new();
|
||||
for (_, medlemmer) in grupper.iter().filter(|(_, m)| m.len() > 1) {
|
||||
let avstand_til_etasjen = |nr: u64| {
|
||||
let d = deler.iter().find(|d| d.nr == nr)?;
|
||||
let hoyde = d.etasje.and_then(|i| etasjer.get(i)).map(|e| e.hoyde)?;
|
||||
Some((d.z - hoyde).abs())
|
||||
};
|
||||
let beholdt = *medlemmer
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
let (fa, fb) = (avstand_til_etasjen(**a), avstand_til_etasjen(**b));
|
||||
match (fa, fb) {
|
||||
(Some(x), Some(y)) => x.total_cmp(&y).then(a.cmp(b)),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => a.cmp(b),
|
||||
}
|
||||
})
|
||||
.unwrap_or(&medlemmer[0]);
|
||||
for nr in medlemmer.iter().filter(|nr| **nr != beholdt) {
|
||||
dublett_av.insert(*nr, beholdt);
|
||||
}
|
||||
}
|
||||
let dubletter = dublett_av.len();
|
||||
|
||||
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||||
let ytre_deler: Vec<&Del> = deler
|
||||
.iter()
|
||||
@@ -234,8 +287,16 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
||||
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
||||
let terreng = terrenghoyder(&fil, skala);
|
||||
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||
// Ligger terrengmodellen over gulvet i første etasje, er den ikke lagt i bakkenivå, og da
|
||||
// sier den ingenting om hvor mye av grunnmuren som er nedgravd.
|
||||
let terreng_over_gulvet = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||
(Some((referanse, terrengnivaa)), Some(forste)) => {
|
||||
terrengnivaa > referanse + forste.hoyde + 0.5
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||
(Some((referanse, terrengnivaa)), Some(forste)) if !terreng_over_gulvet => {
|
||||
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
||||
}
|
||||
_ => 0.0,
|
||||
@@ -251,14 +312,14 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
|
||||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||||
let etasjekontekst = Etasjekontekst {
|
||||
nederste: (!etasjer.is_empty()).then_some(0),
|
||||
kjeller: kjelleretasje,
|
||||
forste_over,
|
||||
forste_over_hoyde: forste_over.and_then(|i| etasjer.get(i)).map(|e| e.hoyde),
|
||||
};
|
||||
let laveste = deler
|
||||
.iter()
|
||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||
.filter_map(|d| d.plass.map(|p| p.2))
|
||||
.map(|d| d.z)
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
|
||||
for d in &deler {
|
||||
@@ -271,16 +332,25 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
uten_geometri += 1;
|
||||
continue;
|
||||
}
|
||||
if dublett_av.contains_key(&d.nr) {
|
||||
continue;
|
||||
}
|
||||
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
||||
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
||||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||||
continue;
|
||||
};
|
||||
// En etasjeskiller i den laveste etasjen over terreng er grensen mot kjelleren.
|
||||
// En etasjeskiller like over kjelleren er grensen mot den uoppvarmede sonen. Høyden
|
||||
// avgjør, ikke etasjetilknytningen, som kan være feil i eksporten.
|
||||
let ved_kjellertaket = etasjekontekst
|
||||
.forste_over_hoyde
|
||||
.map(|h| (d.z - h).abs() < 0.6)
|
||||
.unwrap_or(false);
|
||||
let (mot, sone) = if kjelleretasje.is_some()
|
||||
&& !i_kjeller
|
||||
&& d.etasje == forste_over
|
||||
&& ved_kjellertaket
|
||||
&& typ == FlateType::Gulv
|
||||
&& mot != Mot::Grunn
|
||||
{
|
||||
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
||||
} else {
|
||||
@@ -324,6 +394,9 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
|
||||
// ── vinduer og dører ────────────────────────────────────────────
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut sette_vinduer: std::collections::HashSet<(String, (i64, i64, i64), i64)> =
|
||||
Default::default();
|
||||
let mut vindusdubletter = 0usize;
|
||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||
let dor = e.typ == "IFCDOOR";
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
@@ -340,7 +413,9 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
dor_vert.get(&r)
|
||||
} else {
|
||||
vindu_vert.get(&r)
|
||||
};
|
||||
}
|
||||
.map(|v| dublett_av.get(v).copied().unwrap_or(*v));
|
||||
let vert = vert.as_ref();
|
||||
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||||
let i_ytre_vegg = vert
|
||||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||||
@@ -354,6 +429,25 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
));
|
||||
continue;
|
||||
}
|
||||
// Det samme vinduet i to kopier av den samme veggen er ett vindu.
|
||||
let sted = e
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|p| global_plassering(&fil, p, skala))
|
||||
.map(|(x, y, z, _, _)| {
|
||||
let cm = |v: f64| (v * 50.0).round() as i64;
|
||||
(cm(x), cm(y), cm(z))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let nokkel = (
|
||||
konstruksjon(&navn_av(e, r, if dor { "Dør" } else { "Vindu" })),
|
||||
sted,
|
||||
(areal * 100.0).round() as i64,
|
||||
);
|
||||
if !sette_vinduer.insert(nokkel) {
|
||||
vindusdubletter += 1;
|
||||
continue;
|
||||
}
|
||||
let asimut = vert
|
||||
.and_then(|v| asimut_for_vegg.get(v))
|
||||
.copied()
|
||||
@@ -443,6 +537,44 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
||||
));
|
||||
}
|
||||
if !utelatt.is_empty() {
|
||||
// Gruppert per konstruksjon, slik at listen er til å lese.
|
||||
let mut grupper: Vec<(String, usize, f64)> = Vec::new();
|
||||
for (navn, areal, _) in &utelatt {
|
||||
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||||
match grupper.iter_mut().find(|(n, _, _)| *n == kort) {
|
||||
Some((_, antall, sum)) => {
|
||||
*antall += 1;
|
||||
*sum += areal;
|
||||
}
|
||||
None => grupper.push((kort, 1, *areal)),
|
||||
}
|
||||
}
|
||||
grupper.sort_by(|a, b| b.2.total_cmp(&a.2));
|
||||
let liste: Vec<String> = grupper
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|(navn, antall, sum)| {
|
||||
if *antall > 1 {
|
||||
format!("{navn} ×{antall} ({sum:.0} m²)")
|
||||
} else {
|
||||
format!("{navn} ({sum:.0} m²)")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
merknader.push(format!(
|
||||
"{} bygningsdeler er ikke regnet med i klimaskjermen, som innervegger og dekker inne i bygningen: {}{}.",
|
||||
utelatt.len(),
|
||||
liste.join(", "),
|
||||
if grupper.len() > liste.len() { " …" } else { "" }
|
||||
));
|
||||
}
|
||||
if dubletter + vindusdubletter > 0 {
|
||||
merknader.push(format!(
|
||||
"{} bygningsdeler ligger dobbelt i modellen, samme konstruksjon på samme sted, og er telt én gang. Det skjer når eksporten legger de samme delene i flere etasjer.",
|
||||
dubletter + vindusdubletter
|
||||
));
|
||||
}
|
||||
if !uten_uverdi.is_empty() {
|
||||
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
|
||||
// konstruksjon nevnes én gang, med antallet.
|
||||
@@ -561,7 +693,13 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
||||
rom.join(", ")
|
||||
)),
|
||||
});
|
||||
let grunnmur = if terreng.is_some() {
|
||||
let grunnmur = if terreng_over_gulvet {
|
||||
format!(
|
||||
"Terrengmodellen ligger over gulvet i første etasje, så den er ikke brukt. \
|
||||
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Legg \
|
||||
terrengmodellen i bakkenivå, eller sett `over_terreng` i meter på veggene."
|
||||
)
|
||||
} else if terreng.is_some() {
|
||||
format!(
|
||||
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
||||
{:.2} m under.",
|
||||
@@ -1110,6 +1248,8 @@ struct Del {
|
||||
plass: Option<(f64, f64, f64, f64, f64)>,
|
||||
areal: f64,
|
||||
normal: Option<[f64; 3]>,
|
||||
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
||||
z: f64,
|
||||
}
|
||||
|
||||
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
||||
@@ -1257,12 +1397,13 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
||||
/// Etasjeinndelingen som klassifiseringen trenger.
|
||||
struct Etasjekontekst {
|
||||
/// Nederste etasje i modellen.
|
||||
nederste: Option<usize>,
|
||||
/// Etasjen under terreng, dersom modellen har en.
|
||||
kjeller: Option<usize>,
|
||||
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
||||
forste_over: Option<usize>,
|
||||
/// Høyden på den etasjen, m. Geometrien veier tyngre enn etasjetilknytningen, som kan
|
||||
/// være feil i eksporten.
|
||||
forste_over_hoyde: Option<f64>,
|
||||
}
|
||||
|
||||
fn klassifiser(
|
||||
@@ -1297,20 +1438,28 @@ fn klassifiser(
|
||||
if d.ytre == Some(true) {
|
||||
return Some((FlateType::Gulv, Mot::Ute));
|
||||
}
|
||||
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
|
||||
let nederst = match (d.etasje, e.nederste) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => d
|
||||
.plass
|
||||
.map(|p| (p.2 - laveste).abs() < 0.5)
|
||||
.unwrap_or(false),
|
||||
};
|
||||
if nederst || navn.contains("kjellergulv") {
|
||||
// Mot grunnen i det nederste planet, eller når navnet sier det. Høyden av geometrien
|
||||
// avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
||||
let paa_grunnen = [
|
||||
"kjellergulv",
|
||||
"on grade",
|
||||
"på grunn",
|
||||
"paa grunn",
|
||||
"såle",
|
||||
"sale",
|
||||
];
|
||||
if (d.z - laveste).abs() < 0.5 || paa_grunnen.iter().any(|o| navn.contains(o)) {
|
||||
return Some((FlateType::Gulv, Mot::Grunn));
|
||||
}
|
||||
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
|
||||
if e.kjeller.is_some() && d.etasje.is_some() && d.etasje == e.forste_over {
|
||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||||
if e.kjeller.is_some() {
|
||||
let ved_forste_over = e
|
||||
.forste_over_hoyde
|
||||
.map(|h| (d.z - h).abs() < 0.6)
|
||||
.unwrap_or(false);
|
||||
if ved_forste_over || (d.etasje.is_some() && d.etasje == e.forste_over) {
|
||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||||
}
|
||||
}
|
||||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||||
return None;
|
||||
@@ -1678,6 +1827,11 @@ mod tester {
|
||||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
||||
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||||
fn prove() -> String {
|
||||
prove_med(&[(100, "Sorvegg", -5.0), (120, "Nordvegg", 5.0)])
|
||||
}
|
||||
|
||||
/// Som `prove`, men med de veggene testen ber om: (entitetsnummer, navn, y i meter).
|
||||
fn prove_med(vegger: &[(u64, &str, f64)]) -> String {
|
||||
let mut s = String::from(
|
||||
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
||||
);
|
||||
@@ -1727,8 +1881,9 @@ mod tester {
|
||||
p + 8
|
||||
));
|
||||
};
|
||||
vegg(100, "Sorvegg", -5.0);
|
||||
vegg(120, "Nordvegg", 5.0);
|
||||
for (nr, navn, y) in vegger {
|
||||
vegg(*nr, navn, *y);
|
||||
}
|
||||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||||
s
|
||||
}
|
||||
@@ -1774,6 +1929,24 @@ mod tester {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn samme_vegg_i_flere_etasjer_telles_en_gang() {
|
||||
// Den samme veggen på nøyaktig samme sted to ganger, slik en eksport gjør når den
|
||||
// legger de samme delene i flere etasjer.
|
||||
let tekst = prove_med(&[
|
||||
(100, "Sorvegg", -5.0),
|
||||
(120, "Nordvegg", 5.0),
|
||||
(140, "Sorvegg", -5.0),
|
||||
]);
|
||||
let r = fra_tekst(&tekst, "prove").unwrap();
|
||||
assert_eq!(r.bygning.flate.len(), 2, "flater: {:?}", r.bygning.flate);
|
||||
assert!(
|
||||
r.merknader.iter().any(|m| m.contains("dobbelt i modellen")),
|
||||
"{:?}",
|
||||
r.merknader
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegger_faar_himmelretning_i_navnet() {
|
||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||
|
||||
+17
-1
@@ -27,6 +27,9 @@ pub struct Hovedflate {
|
||||
pub normal: [f64; 3],
|
||||
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
||||
plan: [f64; 4],
|
||||
/// Midlere høyde over delens eget nullpunkt, m. Sier hvilken etasje delen ligger i når
|
||||
/// plasseringen i filen ikke gjør det.
|
||||
pub hoyde: f64,
|
||||
}
|
||||
|
||||
impl Nett {
|
||||
@@ -95,10 +98,16 @@ impl Nett {
|
||||
let (normal, sum) = grupper
|
||||
.into_iter()
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
let hoyde = if self.punkter.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
self.punkter.iter().map(|p| p[2]).sum::<f64>() / self.punkter.len() as f64 * skala
|
||||
};
|
||||
Some(Hovedflate {
|
||||
areal: sum / 2.0,
|
||||
normal,
|
||||
plan: self.utstrekning(normal, skala),
|
||||
hoyde,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,9 +182,16 @@ pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
||||
grupper.push(f);
|
||||
}
|
||||
}
|
||||
let areal = grupper.iter().map(|g| g.areal).sum();
|
||||
let areal: f64 = grupper.iter().map(|g| g.areal).sum();
|
||||
// Arealvektet høyde, slik at et sammensatt dekke havner der mesteparten av det ligger.
|
||||
let hoyde = if areal > 0.0 {
|
||||
grupper.iter().map(|g| g.hoyde * g.areal).sum::<f64>() / areal
|
||||
} else {
|
||||
grupper[0].hoyde
|
||||
};
|
||||
Some(Hovedflate {
|
||||
areal,
|
||||
hoyde,
|
||||
..grupper[0]
|
||||
})
|
||||
}
|
||||
|
||||
+39
-1
@@ -530,9 +530,10 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
}
|
||||
|
||||
s.push_str(&format!(
|
||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet med enok {} etter NS 3031:2025 (timesmetode, \
|
||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet {} med enok {} etter NS 3031:2025 (timesmetode, \
|
||||
NS-EN ISO 52016-1). Karakterskala fastsatt av Enova SF, gjeldende fra 1. januar 2026. \
|
||||
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
||||
i_dag(),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
s
|
||||
@@ -550,9 +551,34 @@ fn bunntekst(r: &Resultat) -> String {
|
||||
if let Some(e) = &b.energiraadgiver {
|
||||
deler.push(e.clone());
|
||||
}
|
||||
deler.push(format!("enok {} · {}", env!("CARGO_PKG_VERSION"), i_dag()));
|
||||
deler.join(" · ")
|
||||
}
|
||||
|
||||
/// Dagens dato som ISO-8601 i UTC.
|
||||
fn i_dag() -> String {
|
||||
let sekunder = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0);
|
||||
let (aar, maaned, dag) = dato_av_dagnummer(sekunder.div_euclid(86_400));
|
||||
format!("{aar:04}-{maaned:02}-{dag:02}")
|
||||
}
|
||||
|
||||
/// Kalenderdato av antall døgn siden 1970-01-01, etter Howard Hinnants algoritme.
|
||||
fn dato_av_dagnummer(dager: i64) -> (i64, i64, i64) {
|
||||
let z = dager + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let dag = doy - (153 * mp + 2) / 5 + 1;
|
||||
let maaned = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let aar = yoe + era * 400 + i64::from(maaned <= 2);
|
||||
(aar, maaned, dag)
|
||||
}
|
||||
|
||||
fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||
if helning < 20.0 {
|
||||
return "horisontal".into();
|
||||
@@ -566,3 +592,15 @@ fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
||||
retninger[i].into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::dato_av_dagnummer;
|
||||
|
||||
#[test]
|
||||
fn dagnummer_gir_riktig_dato() {
|
||||
assert_eq!(dato_av_dagnummer(0), (1970, 1, 1));
|
||||
assert_eq!(dato_av_dagnummer(19_723), (2024, 1, 1));
|
||||
assert_eq!(dato_av_dagnummer(20_351), (2025, 9, 20));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user