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:
bl
2026-09-16 17:51:19 +02:00
co-authored by Claude Opus 5
parent 32ecb0b132
commit 77c5d15042
4 changed files with 245 additions and 1 deletions
+127
View File
@@ -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))
}
/// 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.
fn passer(moenster: &str, navn: &str) -> bool {
let deler: Vec<&str> = moenster.split('*').collect();
@@ -237,6 +318,52 @@ u_verdi = 0.8
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]
fn moenster_passer() {
assert!(passer("V-2*", "V-2 #3306"));