Ta vare på manuelle endringer ved ny import
«enok tillegg modell.ifc bygning.toml» skriver forskjellen mellom modellen og en håndredigert bygningsbeskrivelse som en tilleggsfil: endrede verdier, nye oppføringer, «slett = true» for det bare modellen har, og scenarioene. Kommandoen kontrollerer at tillegget gjenskaper filen det er laget av, og sier fra om noe ikke lar seg uttrykke, siden fletting ikke kan fjerne et enkeltfelt. «enok import» nekter å skrive over en fil enok ikke har laget selv, og viser til kommandoen over. --overskriv går forbi. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -98,6 +98,7 @@ Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen
|
|||||||
| `enok beregn modell.ifc` | Beregner rett fra en modell, uten at `bygning.toml` finnes fra før |
|
| `enok beregn modell.ifc` | Beregner rett fra en modell, uten at `bygning.toml` finnes fra før |
|
||||||
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
||||||
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
|
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
|
||||||
|
| `enok tillegg modell.ifc bygning.toml` | Skriver forskjellen mellom modellen og en håndredigert fil som tilleggsfil |
|
||||||
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
||||||
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
||||||
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
||||||
@@ -210,6 +211,20 @@ enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml # samme, som
|
|||||||
`beregn` skriver bygningsbeskrivelsen modellen ga, til `bygning.toml` i utdatakatalogen, slik
|
`beregn` skriver bygningsbeskrivelsen modellen ga, til `bygning.toml` i utdatakatalogen, slik
|
||||||
at beregningen kan etterprøves.
|
at beregningen kan etterprøves.
|
||||||
|
|
||||||
|
En ny import skriver aldri over manuelle endringer, for de ligger ikke i den genererte filen.
|
||||||
|
Har du likevel en `bygning.toml` du har redigert for hånd, lager `enok tillegg` en tilleggsfil
|
||||||
|
av forskjellen mot modellen, så arbeidet følger med videre:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
enok tillegg modell.ifc bygning.toml --ut prosjekt.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Tillegget inneholder alt filen sier som modellen ikke sier: endrede U-verdier, drift,
|
||||||
|
varmeanlegg og scenarioer, og `slett = true` for oppføringer modellen har og filen ikke.
|
||||||
|
Kommandoen kontrollerer at tillegget gjenskaper filen det er laget av, og sier fra om noe
|
||||||
|
ikke lar seg uttrykke. `enok import` nekter dessuten å skrive over en fil enok ikke har laget
|
||||||
|
selv, med mindre `--overskriv` er oppgitt.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# prosjekt.toml
|
# prosjekt.toml
|
||||||
[import]
|
[import]
|
||||||
|
|||||||
+1
-1
@@ -8,5 +8,5 @@ pub mod step;
|
|||||||
pub mod toml_inn;
|
pub mod toml_inn;
|
||||||
|
|
||||||
pub use toml_inn::{
|
pub use toml_inn::{
|
||||||
flett_tillegg, les_bygning, les_tillegg, slaa_sammen_scenario, Importvalg, Tillegg,
|
differanse, flett_tillegg, les_bygning, les_tillegg, slaa_sammen_scenario, Importvalg, Tillegg,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,6 +71,87 @@ pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
|||||||
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst.
|
/// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst.
|
||||||
fn passer(moenster: &str, navn: &str) -> bool {
|
fn passer(moenster: &str, navn: &str) -> bool {
|
||||||
let deler: Vec<&str> = moenster.split('*').collect();
|
let deler: Vec<&str> = moenster.split('*').collect();
|
||||||
@@ -237,6 +318,52 @@ u_verdi = 0.8
|
|||||||
assert_eq!(bom, vec!["Kjeller*".to_string()]);
|
assert_eq!(bom, vec!["Kjeller*".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn moenster_passer() {
|
fn moenster_passer() {
|
||||||
assert!(passer("V-2*", "V-2 #3306"));
|
assert!(passer("V-2*", "V-2 #3306"));
|
||||||
|
|||||||
+102
@@ -31,6 +31,8 @@ enum Kommando {
|
|||||||
Beregn(Beregn),
|
Beregn(Beregn),
|
||||||
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
|
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
|
||||||
Import(Import),
|
Import(Import),
|
||||||
|
/// Lager en tilleggsfil av forskjellen mellom en modell og en ferdig bygningsbeskrivelse.
|
||||||
|
Tillegg(TilleggAv),
|
||||||
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
|
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
|
||||||
Klima(Klima),
|
Klima(Klima),
|
||||||
/// Viser energikarakterskalaen for en bygningskategori.
|
/// Viser energikarakterskalaen for en bygningskategori.
|
||||||
@@ -72,6 +74,20 @@ struct Import {
|
|||||||
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
||||||
#[arg(short, long, value_name = "FIL")]
|
#[arg(short, long, value_name = "FIL")]
|
||||||
ut: Option<PathBuf>,
|
ut: Option<PathBuf>,
|
||||||
|
/// Skriver over en utdatafil enok ikke har laget selv.
|
||||||
|
#[arg(long)]
|
||||||
|
overskriv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct TilleggAv {
|
||||||
|
/// BIM-modellen: .ifc, .gbxml eller .csv.
|
||||||
|
modell: PathBuf,
|
||||||
|
/// Bygningsbeskrivelsen med de manuelle endringene, som tillegget skal gjenskape.
|
||||||
|
bygning: PathBuf,
|
||||||
|
/// Utdatafil. Standard: prosjekt.toml ved siden av bygningsbeskrivelsen.
|
||||||
|
#[arg(short, long, value_name = "FIL")]
|
||||||
|
ut: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@@ -100,6 +116,7 @@ fn main() -> Result<()> {
|
|||||||
match cli.kommando {
|
match cli.kommando {
|
||||||
Kommando::Beregn(a) => beregn(a, &kilde),
|
Kommando::Beregn(a) => beregn(a, &kilde),
|
||||||
Kommando::Import(a) => import(a),
|
Kommando::Import(a) => import(a),
|
||||||
|
Kommando::Tillegg(a) => tillegg(a),
|
||||||
Kommando::Klima(a) => klima(a),
|
Kommando::Klima(a) => klima(a),
|
||||||
Kommando::Skala(a) => skala(a, &kilde),
|
Kommando::Skala(a) => skala(a, &kilde),
|
||||||
}
|
}
|
||||||
@@ -280,9 +297,26 @@ fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)])
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GENERERT: &str = "# Generert av enok";
|
||||||
|
|
||||||
fn import(a: Import) -> Result<()> {
|
fn import(a: Import) -> Result<()> {
|
||||||
let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
||||||
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
||||||
|
// En fil som ikke er skrevet av enok, kan være redigert for hånd. Da er det bedre å
|
||||||
|
// stoppe enn å skrive over arbeidet.
|
||||||
|
if !a.overskriv && ut.exists() {
|
||||||
|
let gammel = std::fs::read_to_string(&ut).unwrap_or_default();
|
||||||
|
if !gammel.starts_with(GENERERT) {
|
||||||
|
bail!(
|
||||||
|
"{} er ikke laget av enok og kan være redigert for hånd. \n\
|
||||||
|
Ta vare på endringene som en tilleggsfil med «enok tillegg {} {} --ut prosjekt.toml», \n\
|
||||||
|
og importer med «--tillegg prosjekt.toml». Bruk --overskriv for å skrive over.",
|
||||||
|
ut.display(),
|
||||||
|
a.fil.display(),
|
||||||
|
ut.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
|
let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
|
||||||
std::fs::write(&ut, innhold)?;
|
std::fs::write(&ut, innhold)?;
|
||||||
println!("Skrev {}", ut.display());
|
println!("Skrev {}", ut.display());
|
||||||
@@ -322,6 +356,74 @@ fn skriv_bygning(
|
|||||||
Ok(innhold)
|
Ok(innhold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Skriver forskjellen mellom modellen og en ferdig bygningsbeskrivelse som en tilleggsfil.
|
||||||
|
fn tillegg(a: TilleggAv) -> Result<()> {
|
||||||
|
let (importert, _, modell) = les_modell(&a.modell, None)?;
|
||||||
|
if !modell {
|
||||||
|
bail!("{} er ikke en BIM-modell", a.modell.display());
|
||||||
|
}
|
||||||
|
let manuell = inndata::les_bygning(&a.bygning)?;
|
||||||
|
let fra = toml::Value::try_from(&importert).context("serialiserer den importerte bygningen")?;
|
||||||
|
let til = toml::Value::try_from(&manuell).context("serialiserer bygningsbeskrivelsen")?;
|
||||||
|
let (patch, detaljer) = inndata::differanse(&fra, &til);
|
||||||
|
|
||||||
|
// Felter modellen har og filen ikke, kan et tillegg ikke fjerne. De er som regel
|
||||||
|
// standardverdier importen fyller ut, så de samles i én merknad i stedet for én hver.
|
||||||
|
let (ufjernbare, mut merknader): (Vec<String>, Vec<String>) = detaljer
|
||||||
|
.into_iter()
|
||||||
|
.partition(|m| m.contains("finnes i modellen"));
|
||||||
|
if !ufjernbare.is_empty() {
|
||||||
|
let felter: Vec<&str> = ufjernbare
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| m.split('«').nth(1)?.split('»').next())
|
||||||
|
.collect();
|
||||||
|
merknader.push(format!(
|
||||||
|
"{} felter finnes i modellen, men ikke i bygningsbeskrivelsen, og blir stående: {}{}. En tilleggsfil kan ikke fjerne et felt.",
|
||||||
|
felter.len(),
|
||||||
|
felter.iter().take(3).copied().collect::<Vec<_>>().join(", "),
|
||||||
|
if felter.len() > 3 { " …" } else { "" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontroller at tillegget faktisk gjenskaper filen det er laget av.
|
||||||
|
let (gjenskapt, bom) = inndata::flett_tillegg(&importert, &patch)?;
|
||||||
|
merknader.extend(
|
||||||
|
bom.into_iter()
|
||||||
|
.map(|m| format!("Navnemønsteret «{m}» traff ingenting.")),
|
||||||
|
);
|
||||||
|
if toml::Value::try_from(&gjenskapt).ok().as_ref() != Some(&til) {
|
||||||
|
merknader.push(
|
||||||
|
"Tillegget gjenskaper ikke bygningsbeskrivelsen helt. Gå gjennom filen før den brukes."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ut =
|
||||||
|
a.ut.unwrap_or_else(|| a.bygning.with_file_name("prosjekt.toml"));
|
||||||
|
let mut innhold = format!(
|
||||||
|
"# Tillegg til «{}», laget av forskjellen mot «{}».\n\
|
||||||
|
# Alt som ikke står i modellen, ligger her, og flettes inn ved hver import:\n\
|
||||||
|
# enok beregn \"{}\" --tillegg {}\n",
|
||||||
|
a.modell.display(),
|
||||||
|
a.bygning.display(),
|
||||||
|
a.modell.display(),
|
||||||
|
ut.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("prosjekt.toml")
|
||||||
|
);
|
||||||
|
for m in &merknader {
|
||||||
|
innhold.push_str(&format!("# {m}\n"));
|
||||||
|
}
|
||||||
|
innhold.push('\n');
|
||||||
|
innhold.push_str(&toml::to_string_pretty(&patch).context("serialiserer tillegget")?);
|
||||||
|
std::fs::write(&ut, innhold)?;
|
||||||
|
println!("Skrev {}", ut.display());
|
||||||
|
for m in &merknader {
|
||||||
|
println!(" • {m}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn klima(a: Klima) -> Result<()> {
|
fn klima(a: Klima) -> Result<()> {
|
||||||
if a.liste {
|
if a.liste {
|
||||||
for k in enok::klima::tmy::kommuner()? {
|
for k in enok::klima::tmy::kommuner()? {
|
||||||
|
|||||||
Reference in New Issue
Block a user