Skip to content

Commit da06f2c

Browse files
committed
feat: reconstruct odometer gaps for trip history
1 parent d49744b commit da06f2c

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const numberOrNull = (value) => {
2+
const numeric = Number.parseFloat(String(value ?? "").replace(",", "."));
3+
return Number.isFinite(numeric) ? numeric : null;
4+
};
5+
6+
const rounded = (value) => Math.round(value * 1000) / 1000;
7+
8+
const canonicalTime = (trip) => trip?.end_time || trip?.start_time || null;
9+
10+
const endMileage = (trip) => {
11+
const explicit = numberOrNull(trip?.end_mileage ?? trip?.end_mileage_km);
12+
if (explicit !== null) return explicit;
13+
const start = numberOrNull(trip?.start_mileage ?? trip?.start_mileage_km);
14+
const distance = numberOrNull(trip?.distance_km ?? trip?.distance);
15+
return start !== null && distance !== null ? start + distance : null;
16+
};
17+
18+
const usableAnchor = (trip) => {
19+
if (!trip || trip.valid_for_statistics === false) return false;
20+
const start = numberOrNull(trip.start_mileage ?? trip.start_mileage_km);
21+
const end = endMileage(trip);
22+
return start !== null && end !== null && start >= 0 && end >= start;
23+
};
24+
25+
export function semanticTripTime(trip) {
26+
const attributes = trip?.attributes ?? {};
27+
return attributes.end_time || attributes.start_time || trip?.end_time || trip?.start_time ||
28+
trip?.last_updated || trip?.last_changed || null;
29+
}
30+
31+
export function tripFilterTime(trip) {
32+
if (trip?.reconstructed_gap || trip?.attributes?.reconstructed_gap) {
33+
const attributes = trip?.attributes ?? {};
34+
return attributes.gap_before_time || trip?.gap_before_time ||
35+
attributes.gap_after_time || trip?.gap_after_time || null;
36+
}
37+
return semanticTripTime(trip);
38+
}
39+
40+
export function insertOdometerGapRows(trips, { toleranceKm = 1, maxGapKm = 1000 } = {}) {
41+
const ordered = Array.isArray(trips) ? [...trips] : [];
42+
const result = [];
43+
44+
for (let index = 0; index < ordered.length; index += 1) {
45+
const current = ordered[index];
46+
const previous = index > 0 ? ordered[index - 1] : null;
47+
48+
if (previous && usableAnchor(previous) && usableAnchor(current)) {
49+
const previousEnd = endMileage(previous);
50+
const currentStart = numberOrNull(current.start_mileage ?? current.start_mileage_km);
51+
const gap = previousEnd !== null && currentStart !== null
52+
? rounded(currentStart - previousEnd)
53+
: null;
54+
55+
if (gap !== null && gap > toleranceKm && gap <= maxGapKm) {
56+
const previousId = String(previous.server_id || previous.id || index - 1);
57+
const currentId = String(current.server_id || current.id || index);
58+
result.push({
59+
id: `odometer-gap:${previousId}:${currentId}`,
60+
server_id: `odometer-gap:${previousId}:${currentId}`,
61+
source: "reconstructed_odometer_gap",
62+
sources: ["odometer_continuity"],
63+
reconstructed_gap: true,
64+
status: "reconstructed",
65+
confidence: "high",
66+
start_time: null,
67+
end_time: null,
68+
gap_after_time: canonicalTime(previous),
69+
gap_before_time: current.start_time || current.end_time || null,
70+
duration_seconds: null,
71+
distance_km: gap,
72+
start_mileage: rounded(previousEnd),
73+
end_mileage: rounded(currentStart),
74+
start_mileage_km: rounded(previousEnd),
75+
end_mileage_km: rounded(currentStart),
76+
average_speed: null,
77+
average_speed_kmh: null,
78+
energy_kwh: null,
79+
energy_per_100_km: null,
80+
fuel_consumption_l: null,
81+
fuel_consumption_l_100km: null,
82+
trip_type: "unknown",
83+
valid_for_statistics: false,
84+
quality_flags: ["reconstructed_odometer_gap"],
85+
});
86+
}
87+
}
88+
result.push(current);
89+
}
90+
return result;
91+
}

0 commit comments

Comments
 (0)