From d7ecf52330423393c72f60ca9fec298a35aa44a8 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:43:20 +0200 Subject: [PATCH] Fix VU interval deduplication and slot normalization --- .../VehicleUnitXmlExtractionService.java | 62 +++++++++++++++++-- .../VehicleUnitXmlExtractionServiceTest.java | 54 ++++++++++++++++ .../service/VehicleUnitXmlSamples.java | 37 +++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionService.java index ccaf78a..4254799 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionService.java @@ -114,7 +114,7 @@ public class VehicleUnitXmlExtractionService { continue; } - builder.vehicleUsageIntervals.add(new ExtractedCardVehicleUsageInterval( + builder.addVehicleUsageInterval(new ExtractedCardVehicleUsageInterval( "VUIW-" + (i + 1), from, to, @@ -124,9 +124,9 @@ public class VehicleUnitXmlExtractionService { vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(), path )); - vuCardIwIntervals.add(new VuCardIwInterval( + addVuCardIwInterval(vuCardIwIntervals, new VuCardIwInterval( driverKey, - normalizeToken(text(record, "cardSlotNumber")), + normalizeVuSlot(text(record, "cardSlotNumber")), from, to, path @@ -270,7 +270,7 @@ public class VehicleUnitXmlExtractionService { parsedChanges.add(new ActivityChange( from, normalizeActivity(text(change, "activity")), - normalizeToken(text(change, "slot")), + normalizeVuSlot(text(change, "slot")), normalizeToken(text(change, "cardStatus")), normalizeToken(text(change, "drivingStatus")), dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]" @@ -967,11 +967,12 @@ public class VehicleUnitXmlExtractionService { String explicitSlot, List vuCardIwIntervals ) { + String normalizedExplicitSlot = normalizeVuSlot(explicitSlot); if (explicitDriverKey != null) { - return List.of(new DriverAssignment(explicitDriverKey, explicitSlot)); + return List.of(new DriverAssignment(explicitDriverKey, normalizedExplicitSlot)); } return vuCardIwIntervals.stream() - .filter(iw -> explicitSlot == null || explicitSlot.equals(iw.slot())) + .filter(iw -> normalizedExplicitSlot == null || normalizedExplicitSlot.equals(iw.slot())) .filter(iw -> iw.covers(occurredAt)) .map(iw -> new DriverAssignment(iw.driverKey(), iw.slot())) .toList(); @@ -985,6 +986,22 @@ public class VehicleUnitXmlExtractionService { return List.copyOf(unique.values()); } + private void addVuCardIwInterval(List intervals, VuCardIwInterval candidate) { + boolean alreadyPresent = intervals.stream().anyMatch(existing -> + sameVuCardIwInterval(existing, candidate) + ); + if (!alreadyPresent) { + intervals.add(candidate); + } + } + + private boolean sameVuCardIwInterval(VuCardIwInterval existing, VuCardIwInterval candidate) { + return java.util.Objects.equals(existing.driverKey(), candidate.driverKey()) + && java.util.Objects.equals(existing.slot(), candidate.slot()) + && java.util.Objects.equals(existing.from(), candidate.from()) + && java.util.Objects.equals(existing.to(), candidate.to()); + } + private String driverKeyFromCardNode(Element node, String basePath) { String cardNation = text(node, basePath + "/cardIssuingMemberState"); String cardNumber = driverKeyFactory.canonicalCardNumber(joinCardNumber(node, basePath + "/cardNumber")); @@ -1073,6 +1090,18 @@ public class VehicleUnitXmlExtractionService { return value.trim().toUpperCase().replace('-', '_').replace(' ', '_'); } + private String normalizeVuSlot(String value) { + String normalized = normalizeToken(value); + if (normalized == null) { + return null; + } + return switch (normalized) { + case "0", "DRIVER" -> "DRIVER"; + case "1", "CO_DRIVER", "CODRIVER" -> "CO_DRIVER"; + default -> normalized; + }; + } + private String mapPlaceEntryType(String entryType) { String normalized = normalizeToken(entryType); if (normalized == null) { @@ -1200,6 +1229,27 @@ public class VehicleUnitXmlExtractionService { } } + private void addVehicleUsageInterval(ExtractedCardVehicleUsageInterval candidate) { + boolean alreadyPresent = vehicleUsageIntervals.stream().anyMatch(existing -> + sameVehicleUsageInterval(existing, candidate) + ); + if (!alreadyPresent) { + vehicleUsageIntervals.add(candidate); + } + } + + private boolean sameVehicleUsageInterval( + ExtractedCardVehicleUsageInterval existing, + ExtractedCardVehicleUsageInterval candidate + ) { + return java.util.Objects.equals(existing.from(), candidate.from()) + && java.util.Objects.equals(existing.to(), candidate.to()) + && java.util.Objects.equals(existing.odometerBeginKm(), candidate.odometerBeginKm()) + && java.util.Objects.equals(existing.odometerEndKm(), candidate.odometerEndKm()) + && java.util.Objects.equals(existing.registrationKey(), candidate.registrationKey()) + && java.util.Objects.equals(existing.vehicleKey(), candidate.vehicleKey()); + } + private DriverExtractionSession build() { return new DriverExtractionSession( driverKey, diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionServiceTest.java index ea85096..e0b4446 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlExtractionServiceTest.java @@ -140,6 +140,60 @@ class VehicleUnitXmlExtractionServiceTest { assertThat(session.warnings()).isEmpty(); } + @Test + void matchesNumericVuCardSlotNumbersToDriverActivitySlots() throws Exception { + TachographFileSession session = service.extract( + new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXmlWithNumericCardSlots()), "VehicleUnit"), + new TachographFileSessionMetadata( + "default", + "legalrequirements-vehicleunit", + "sample-vu", + "sample-vu.ddd", + "abc", + 10, + "42", + "def", + false, + null + ), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + DriverExtractionSession driver = session.driversByKey().get("12:12345678901200"); + assertThat(driver).isNotNull(); + assertThat(driver.cardActivityIntervals()).hasSize(2); + assertThat(driver.cardActivityIntervals()).extracting("slot").containsOnly("DRIVER"); + assertThat(session.warnings()).isEmpty(); + } + + @Test + void deduplicatesEquivalentVehicleUnitVehicleUsageIntervals() throws Exception { + TachographFileSession session = service.extract( + new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXmlWithDuplicateVehicleUsageInterval()), "VehicleUnit"), + new TachographFileSessionMetadata( + "default", + "legalrequirements-vehicleunit", + "sample-vu", + "sample-vu.ddd", + "abc", + 10, + "42", + "def", + false, + null + ), + Instant.now(), + Instant.now().plus(4, ChronoUnit.HOURS) + ); + + DriverExtractionSession firstDriver = session.driversByKey().get("12:12345678901200"); + assertThat(firstDriver).isNotNull(); + assertThat(firstDriver.cardVehicleUsageIntervals()).hasSize(1); + assertThat(firstDriver.cardVehicleUsageIntervals().get(0).from()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z")); + assertThat(firstDriver.cardVehicleUsageIntervals().get(0).to()).isEqualTo(OffsetDateTime.parse("2026-04-01T11:00:00Z")); + } + @Test void keepsOpenVuCardUsageRecordWithoutClosingItAtDownloadPeriodEnd() throws Exception { TachographFileSession session = service.extract( diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlSamples.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlSamples.java index 5a42fa1..4b128b5 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlSamples.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/VehicleUnitXmlSamples.java @@ -314,4 +314,41 @@ final class VehicleUnitXmlSamples { """; } + + static String vehicleUnitXmlWithNumericCardSlots() { + return vehicleUnitXmlWithActivityDownloadTime() + .replace("DRIVER", "0"); + } + + static String vehicleUnitXmlWithDuplicateVehicleUsageInterval() { + String duplicateRecord = """ + + + Muster + Max + + + DRIVER_CARD + 12 + + 123456789012 + 0 + 0 + + 2 + + 2031-04-01T00:00:00Z + 2026-04-01T08:00:00Z + 1000 + DRIVER + 2026-04-01T11:00:00Z + 1100 + NO_ENTRY + + """; + return vehicleUnitXml().replace( + duplicateRecord, + duplicateRecord + duplicateRecord + ); + } }