//! Leser EnergyPlus Weather (EPW) og gir 8760 timesverdier. //! //! Feltnumre følger EPW-spesifikasjonen (0-basert etter de åtte headerlinjene): //! 6 tørrkuletemperatur °C, 7 duggpunkt °C, 8 relativ fuktighet %, 9 lufttrykk Pa, //! 12 horisontal infrarød stråling W/m², 13 global horisontal W/m², 14 direkte normal W/m², //! 15 diffus horisontal W/m², 20 vindretning °, 21 vindhastighet m/s. use crate::{Result, TIMER}; use anyhow::{bail, Context}; use std::path::Path; /// Gjør bytene i en EPW-fil om til tekst. /// /// EPW-filer er ikke UTF-8. Stedsnavn skrives i Latin-1, så «Indre Østfold» kommer som /// byten 0xD8 der Ø skal stå. Tolket som UTF-8 blir navnet ødelagt, og da slår ikke /// klimakorrigeringsfaktoren til, siden den slås opp på stedsnavnet. Filen leses derfor /// som UTF-8 når den er gyldig UTF-8, og ellers som Latin-1. pub fn avkod(bytes: &[u8]) -> String { match std::str::from_utf8(bytes) { Ok(s) => s.to_string(), Err(_) => bytes.iter().map(|b| *b as char).collect(), } } #[derive(Clone, Debug)] pub struct Vaerdata { pub sted: String, pub land: String, pub bredde: f64, pub lengde: f64, pub tidssone: f64, pub hoyde: f64, /// Utetemperatur, °C. pub t_ute: Vec, /// Relativ fuktighet, %. pub rf: Vec, /// Global horisontal solstråling, W/m². pub ghi: Vec, /// Direkte normal solstråling, W/m². pub dni: Vec, /// Diffus horisontal solstråling, W/m². pub dhi: Vec, /// Horisontal infrarød stråling fra himmelen, W/m². pub ir_himmel: Vec, /// Vindhastighet, m/s. pub vind: Vec, } impl Vaerdata { pub fn fra_fil(sti: &Path) -> Result { let innhold = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?; let tekst = String::from_utf8_lossy(&innhold); Self::fra_tekst(&tekst).with_context(|| format!("tolker {}", sti.display())) } pub fn fra_tekst(tekst: &str) -> Result { let mut linjer = tekst.lines(); let location = linjer.next().context("EPW mangler LOCATION-linje")?; let f: Vec<&str> = location.split(',').collect(); if f.len() < 10 || !f[0].eq_ignore_ascii_case("LOCATION") { bail!("første linje er ikke en gyldig LOCATION-linje"); } let tall = |i: usize| -> f64 { f.get(i).and_then(|s| s.trim().parse().ok()).unwrap_or(0.0) }; let mut v = Vaerdata { sted: f[1].trim().to_string(), land: f.get(3).unwrap_or(&"").trim().to_string(), bredde: tall(6), lengde: tall(7), tidssone: tall(8), hoyde: tall(9), t_ute: Vec::with_capacity(TIMER), rf: Vec::with_capacity(TIMER), ghi: Vec::with_capacity(TIMER), dni: Vec::with_capacity(TIMER), dhi: Vec::with_capacity(TIMER), ir_himmel: Vec::with_capacity(TIMER), vind: Vec::with_capacity(TIMER), }; // Sju gjenstående headerlinjer. for _ in 0..7 { linjer.next(); } for linje in linjer { if linje.trim().is_empty() { continue; } let d: Vec<&str> = linje.split(',').collect(); if d.len() < 22 { continue; } let g = |i: usize| -> f64 { d[i].trim().parse::().unwrap_or(0.0) }; // EPW bruker 9999/999999 som «mangler». let mangler = |x: f64, grense: f64| if x >= grense { 0.0 } else { x }; v.t_ute.push(g(6)); v.rf.push(g(8)); v.ir_himmel.push(mangler(g(12), 9999.0)); v.ghi.push(mangler(g(13), 9999.0).max(0.0)); v.dni.push(mangler(g(14), 9999.0).max(0.0)); v.dhi.push(mangler(g(15), 9999.0).max(0.0)); v.vind.push(mangler(g(21), 999.0)); if v.t_ute.len() == TIMER { break; } } if v.t_ute.len() != TIMER { bail!( "EPW inneholder {} timer, forventet {}", v.t_ute.len(), TIMER ); } Ok(v) } pub fn aarsmiddeltemperatur(&self) -> f64 { self.t_ute.iter().sum::() / self.t_ute.len() as f64 } pub fn maanedsmiddeltemperatur(&self) -> [f64; 12] { let mut sum = [0.0; 12]; let mut antall = [0.0; 12]; for (t, v) in self.t_ute.iter().enumerate() { let m = crate::maaned_for_time(t); sum[m] += v; antall[m] += 1.0; } let mut ut = [0.0; 12]; for m in 0..12 { ut[m] = if antall[m] > 0.0 { sum[m] / antall[m] } else { 0.0 }; } ut } /// Graddager under 17 °C, til informasjon i rapporten. pub fn graddager(&self, basis: f64) -> f64 { self.t_ute .chunks(24) .map(|d| { let m = d.iter().sum::() / d.len() as f64; (basis - m).max(0.0) }) .sum() } } #[cfg(test)] mod tester { use super::*; #[test] fn latin1_stedsnavn_leses_riktig() { // «Indre Østfold» der Ø er Latin-1-byten 0xD8. let mut bytes = b"LOCATION,Indre ".to_vec(); bytes.push(0xD8); bytes.extend_from_slice(b"stfold,None,NOR,CERRA,x,59.58,11.16,1, 130"); assert_eq!(avkod(&bytes).split(',').nth(1), Some("Indre Østfold")); } #[test] fn gyldig_utf8_gaar_uendret_gjennom() { assert_eq!(avkod("Indre Østfold".as_bytes()), "Indre Østfold"); } }