From c61e0d09457a23d56e8ab0c8fcea47a7391817b5 Mon Sep 17 00:00:00 2001 From: ryanbr Date: Sun, 19 Jul 2026 11:03:50 +1200 Subject: [PATCH] Repository.mergeActivityFileSteps: last-wins on duplicate day, not a trapping init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mergeActivityFileSteps built its day lookup with `Dictionary(uniqueKeysWithValues: base.map { ($0.day, $0) })`, which TRAPS (crashes) if `base` ever carries two rows for the same day. Two reasons to make it graceful: - Parity: the Kotlin twin (WhoopRepository.mergeActivityFileSteps) already does last-wins (`for (row in base) byDay[row.day] = row`) — no crash. - Convention: elsewhere in this same file the day-keyed dictionaries use the safe `Dictionary(…, uniquingKeysWith: { _, last in last })` form; this one was the outlier. `mergeDaily` de-dups by day today, so the trap is likely unreachable now — but using a trapping initializer on merged external data is fragile and diverges from Android. Swap to the two-arg last-wins init. Found while investigating #568 (unrelated to the steps-count fix in #584). App-target Swift, so no local build (GRDB/Combine wall) — the two-arg Dictionary init form was type-checked in isolation; `mergeActivityFileSteps` is covered by VitalSourceResolutionTests on macOS CI. --- Strand/Data/Repository.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Strand/Data/Repository.swift b/Strand/Data/Repository.swift index aba6660c53..cc87ce6db2 100644 --- a/Strand/Data/Repository.swift +++ b/Strand/Data/Repository.swift @@ -805,7 +805,10 @@ final class Repository: ObservableObject { nonisolated static func mergeActivityFileSteps(into base: [DailyMetric], _ activityFile: [DailyMetric]) -> [DailyMetric] { guard !activityFile.isEmpty else { return base } - var byDay = Dictionary(uniqueKeysWithValues: base.map { ($0.day, $0) }) + // Last-wins on a duplicate day rather than `uniqueKeysWithValues`, which TRAPS (crashes) if `base` + // ever carries two rows for one day. Matches the Kotlin twin's graceful merge and this file's own + // safe convention (the other `Dictionary(…, uniquingKeysWith:)` builders here). + var byDay = Dictionary(base.map { ($0.day, $0) }, uniquingKeysWith: { _, last in last }) for row in activityFile { guard let steps = row.steps, steps > 0 else { continue } if let existing = byDay[row.day] {