//! Import fra regneark (CSV), for eksempel en Vectorworks-arbeidstegning eksportert med //! «Export Worksheet». Formatet er bevisst enkelt, slik at det kan settes opp som en //! rapport i BIM-verktøyet og eksporteres direkte. //! //! Hver rad begynner med en type: //! //! ```text //! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall //! bygning,Nordhusstykket 5,140,,,,smaahus,1948, //! vegg,Yttervegg sør,32,1.0,180,90,ute,, //! vindu,Stue sør,4.2,2.8,180,90,,0.75,2 //! ``` //! //! For `bygning`-raden brukes kolonnene: navn, oppvarmet BRA, (tom), (tom), (tom), //! kategori, byggeår. use crate::modell::*; use crate::Result; use anyhow::{bail, Context}; use std::path::Path; pub struct Importrapport { pub bygning: Bygning, pub merknader: Vec, } pub fn les(sti: &Path) -> Result { let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?; fra_tekst( &tekst, sti.file_stem() .and_then(|s| s.to_str()) .unwrap_or("Bygning"), ) } fn skilletegn(tekst: &str) -> u8 { let forste = tekst.lines().find(|l| !l.trim().is_empty()).unwrap_or(""); if forste.matches(';').count() > forste.matches(',').count() { b';' } else { b',' } } fn tall(s: Option<&str>) -> Option { let s = s?.trim().replace(',', "."); if s.is_empty() { return None; } s.parse().ok() } pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { let mut leser = csv::ReaderBuilder::new() .delimiter(skilletegn(tekst)) .flexible(true) .has_headers(true) .from_reader(tekst.as_bytes()); let mut merknader = Vec::new(); let mut flater = Vec::new(); let mut vinduer = Vec::new(); let mut navn = standardnavn.to_string(); let mut kategori = Kategori::Smaahus; let mut byggeaar = 2020; let mut bra = 0.0; for (nr, rad) in leser.records().enumerate() { let rad = rad.with_context(|| format!("rad {}", nr + 2))?; let felt = |i: usize| rad.get(i).map(|s| s.trim()).filter(|s| !s.is_empty()); let Some(typ) = felt(0).map(|s| s.to_lowercase()) else { continue; }; let radnavn = felt(1).unwrap_or("uten navn").to_string(); match typ.as_str() { "bygning" => { navn = radnavn; bra = tall(felt(2)).unwrap_or(0.0); if let Some(k) = felt(6) { kategori = match k.to_lowercase().as_str() { "smaahus" | "småhus" | "enebolig" => Kategori::Smaahus, "boligblokk" | "leilighet" => Kategori::Boligblokk, "kontorbygg" | "kontor" => Kategori::Kontorbygg, "skolebygg" | "skole" => Kategori::Skolebygg, "barnehage" => Kategori::Barnehage, annen => { merknader .push(format!("Ukjent bygningskategori «{annen}»; bruker småhus.")); Kategori::Smaahus } }; } if let Some(a) = tall(felt(7)) { byggeaar = a as i32; } } "vegg" | "tak" | "gulv" => { let ft = match typ.as_str() { "vegg" => FlateType::Vegg, "tak" => FlateType::Tak, _ => FlateType::Gulv, }; let mot = match felt(6).map(|s| s.to_lowercase()).as_deref() { Some("grunn") | Some("grunnen") => Mot::Grunn, Some("uoppvarmet") | Some("kjeller") | Some("loft") => Mot::Uoppvarmet, _ => Mot::Ute, }; let Some(areal) = tall(felt(2)) else { merknader.push(format!( "Raden «{radnavn}» mangler areal og er hoppet over." )); continue; }; flater.push(Flate { navn: radnavn, typ: ft, areal, orientering: tall(felt(4)).unwrap_or(0.0), helning: tall(felt(5)), u_verdi: tall(felt(3)), lag: None, masseklasse: None, masseplassering: Masseplassering::Fordelt, mot, b_faktor: None, absorptans: None, omkrets: None, dybde: None, kommentar: None, }); } "vindu" | "dør" | "dor" => { let Some(areal) = tall(felt(2)) else { merknader.push(format!( "Raden «{radnavn}» mangler areal og er hoppet over." )); continue; }; let Some(u) = tall(felt(3)) else { merknader.push(format!( "Raden «{radnavn}» mangler U-verdi og er hoppet over." )); continue; }; vinduer.push(Vindu { navn: radnavn, areal, antall: tall(felt(8)).unwrap_or(1.0), orientering: tall(felt(4)).unwrap_or(0.0), helning: tall(felt(5)).unwrap_or(90.0), u_verdi: u, g_verdi: tall(felt(7)), karmandel: None, solskjerming: None, horisont: None, dor: typ != "vindu", kommentar: None, }); } annen => merknader.push(format!("Ukjent radtype «{annen}» på rad {}.", nr + 2)), } } if flater.is_empty() { bail!("regnearket inneholder ingen flater (rader som begynner med vegg, tak eller gulv)"); } if bra <= 0.0 { bra = flater .iter() .filter(|f| f.typ == FlateType::Gulv) .map(|f| f.areal) .sum(); merknader.push(format!( "Regnearket mangler en bygning-rad med oppvarmet BRA; satt til {bra:.0} m² av gulvarealet." )); } merknader.push( "Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning." .into(), ); let bygning = Bygning { bygning: BygningInfo { navn, kategori, byggeaar, oppvarmet_bra: bra, oppvarmet_volum: None, antall_boenheter: 1, bygningens_oppvarmede_bra: None, adresse: None, gnr_bnr: None, beskrivelse: Some("Importert fra regneark".into()), energiraadgiver: None, oppdragsgiver: None, }, klima: Klima::default(), flate: flater, vindu: vinduer, kuldebroer: Kuldebroer::default(), infiltrasjon: Infiltrasjon::default(), ventilasjon: Ventilasjon::default(), tappevann: Tappevann::default(), oppvarming: Oppvarming::default(), distribusjon: Distribusjon::default(), solcelle: None, kjoling: Kjoling::default(), lufting: Lufting::default(), drift: Drift::default(), scenario: Vec::new(), }; Ok(Importrapport { bygning, merknader }) } #[cfg(test)] mod tester { use super::*; #[test] fn leser_enkelt_regneark() { let csv = "type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall\n\ bygning,Testhus,140,,,,smaahus,1948,\n\ vegg,Yttervegg sør,32,1.0,180,90,ute,,\n\ gulv,Kjellergulv,70,0.8,,180,grunn,,\n\ vindu,Stue sør,2.1,2.8,180,90,,0.75,2\n"; let r = fra_tekst(csv, "x").unwrap(); assert_eq!(r.bygning.bygning.navn, "Testhus"); assert_eq!(r.bygning.bygning.oppvarmet_bra, 140.0); assert_eq!(r.bygning.bygning.byggeaar, 1948); assert_eq!(r.bygning.flate.len(), 2); assert_eq!(r.bygning.flate[1].mot, Mot::Grunn); assert_eq!(r.bygning.vindu.len(), 1); assert_eq!(r.bygning.vindu[0].antall, 2.0); } #[test] fn godtar_semikolon_og_desimalkomma() { let csv = "type;navn;areal;u_verdi\nvegg;Vegg;32,5;1,0\n"; let r = fra_tekst(csv, "x").unwrap(); assert_eq!(r.bygning.flate[0].areal, 32.5); assert_eq!(r.bygning.flate[0].u_verdi, Some(1.0)); } }