Skip to content

[P2] Activity import distance text rounds 10.5 km differently on Swift and Kotlin #96

Closed
ryanbr/noop
#1499

Description

@bhelm

Summary

ActivityFile.importNote() and ActivityFileImporter.summaryText use different tie-breaking rules above the 10 km display threshold. Swift formats with %.0f under nearest/ties-to-even; Kotlin uses roundToInt(), whose documented ties go toward positive infinity. At the exactly representable 10.5 km boundary Swift prints 10 km and Kotlin prints 11 km.

This is public Swift/Kotlin parity and honest-description drift. Stored distanceM remains unchanged.

Verified on meta032 032983d2bed9bf50dfcfc4d06ef707f3818cdc26 and current upstream main aa0037acbf7fc2ef972ba4676ae3e18d508be1ef.

Exact reproduction

Use otherwise minimal GPX / Running activities with no GPS, HR, steps, or average-HR suffixes.

distanceM formatter Swift Kotlin
9500 importNote Imported GPX · 9.50 km Imported GPX · 9.50 km
10500 importNote Imported GPX · 10 km Imported GPX · 11 km
11500 importNote Imported GPX · 12 km Imported GPX · 12 km
9500 summaryText Imported a 9.50 km Running activity Imported a 9.50 km Running activity
10500 summaryText Imported a 10 km Running activity Imported a 11 km Running activity
11500 summaryText Imported a 12 km Running activity Imported a 12 km Running activity

The .5 values are exactly representable as binary Double; this is tie policy, not precision noise.

Production reachability

  • Swift DataSourcesView writes activity.importNote() into persisted WorkoutRow.notes, then assigns summaryText to visible completion text.
  • Kotlin importExport persists the note; WorkoutsScreen renders Notes. It returns summaryText as ImportSummary.message, which DataSourcesScreen displays in a Toast.

Thus importNote is durable descriptive drift and summaryText is immediate UI drift. No metric is corrupted.

Distinction from #91

#91 and upstream PR ryanbr#1477 concern only positive steps omitted from Swift summaryText. #91 explicitly says the separate per-workout note remains intact. PR ryanbr#1477 adds 1.00 km summary controls and the Swift step suffix; it touches neither distance formatter nor any rounding boundary.

Cause and provenance

Both implementations entered together in upstream commit 4b69da6fa0b950687d4268ce525a5033b3cb9835 (6.0.0 offline activity import, 2026-06-20): Swift uses String(format: "%.0f", km) while Kotlin uses km.roundToInt().

Severity and tests

P2: persisted and immediately visible descriptive output violates the explicit twin/honest-summary contract by 1 km at a reachable boundary. Parsed and persisted distance stays correct. No direct public tests on current main pin either formatter at 9.5/10.5/11.5 km; PR ryanbr#1477 only tests summaryText at 1.00 km for steps.

Duplicate audit

Open and closed issues and PRs in bhelm/noop and ryanbr/noop were searched for ActivityFileImporter rounding, summaryText distance, importNote, 10.5 km, activity import summary, distance rounding, FIT distance, imported km, and roundToInt String format activity. No exact duplicate exists.

Related but non-duplicate: fork #91 / upstream PR ryanbr#1477, upstream ryanbr#483/ryanbr#489, ryanbr#568/ryanbr#584, ryanbr#585/ryanbr#1058/ryanbr#1088, and ryanbr#1205/ryanbr#1238.

Upstream-only acceptance

  • Choose and document one explicit cross-platform policy: for these nonnegative distances, round half up / ties away from zero, making 10.5 km exactly 11 km.
  • Add mirrored public tests for both importNote and summaryText at 9.5, 10.5, and 11.5 km. Canonical 10.5 output is 11 km on both platforms.
  • Preserve the two-decimal path below 10 km, field order, separators, sport normalization, suffix behavior, and exact distanceM.
  • Implement Production only upstream from a fresh current-main branch; do not transplant Product into the fork harness.
  • Native execution requires separate authorization.

Activity

  1. bhelm commented on Aug 21, 2026

    @bhelm
    OwnerAuthor

    Resolved upstream by ryanbr/noop#1499, merge commit 940ded8e, which is contained in current upstream main. The merged fix includes regression coverage and passed upstream CI. Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions