Skip to content

[P2] Activity route distance differs by one ULP across Swift and Kotlin #99

Description

@bhelm

Summary

Activity route-derived distance differs by one binary64 ULP between the public Swift and Kotlin twins for a reachable ordered three-point route. Swift and an independent args-derived oracle produce 0x403e89813f76c75c; Kotlin produces 0x403e89813f76c75d.

Verified on meta032 032983d2bed9bf50dfcfc4d06ef707f3818cdc26 and current upstream main aa0037acbf7fc2ef972ba4676ae3e18d508be1ef. The relevant sources are unchanged between those revisions.

Exact reproduction

Input route, in order:

point latitude longitude
1 52.5 13.4
2 52.5001 13.4001
3 52.5002 13.4003

Exact binary64 results:

calculation decimal metres bits
segment 1, both 13.01783294966102 402a0921667f2bac
segment 2, Swift/oracle 17.519295466875995 403184f08c373186
segment 2, Kotlin 17.519295466876 403184f08c373187
total, Swift/oracle 30.537128416537016 403e89813f76c75c
total, Kotlin 30.53712841653702 403e89813f76c75d

Final delta: 3.552713678800501e-15 metres, exactly one ULP at this result.

Cause

Earth radius, haversine expression, segment order, and route accumulation order match.

Swift converts degrees as x * pi / 180. Kotlin calls Math.toRadians, equivalent for this purpose to x / 180 * pi. The different intermediate rounding changes the second segment by one ULP and therefore changes the final total by one ULP.

Reachability and impact

GPX, TCX, and FIT imports use routeDistanceM when the file supplies a usable route but no authoritative summary/lap/session distance. Both app import paths then persist activity.distanceM as a database Double. The value is also available to workout detail, aggregate distance, local access, CSV/route export, and sharing paths.

Impact is negligible for a person: current UI formatting rounds far above this delta, and activity-file imports deliberately persist strain as nil/null. No distance-to-strain or distance-to-scoring dependency was found. This is P2 because it violates the deterministic exact Swift/Kotlin twin contract and blocks exact parity validation, not because it materially changes a displayed workout metric.

Existing coverage

Public importer tests cover route fallback only with broad tolerances: approximately 222 metres with 30-metre accuracy on both platforms. Kotlin RouteMath tests use 20-metre and 1-metre tolerances. Those tests cannot detect a one-ULP cross-platform drift.

The exact parity case activity_distance_ordered_three_points exposed the mismatch. Existing Native evidence stopped at this new gate; it was not retried or fixed.

Provenance

Kotlin RouteMath was introduced with the Android GPS workout work on 2026-06-10 (commits 27d7c79ca5deae736e7ae168ea1bde736e532221 / 79ca428d393d529dffd01ff09ae7e184047ab8a6). The paired Swift and Kotlin activity-file importers were introduced together on 2026-06-20 in commit 4b69da6fa0b950687d4268ce525a5033b3cb9835. The differing conversion forms have therefore been inherited since the Swift twin was added.

Duplicate audit

Open and closed issues and pull requests in bhelm/noop and ryanbr/noop were searched for activity_distance_ordered_three_points, routeDistanceM, haversine, toRadians, 1 ULP, route distance parity, the exact result bits, and the exact coordinates. No exact duplicate exists.

Related but distinct:

Upstream-only acceptance

  • Choose and document one explicit degree-to-radian operation order as canonical for both platforms.
  • Preserve the current earth radius, haversine formula, point order, and accumulation order.
  • Add mirrored public tests that compare exact Double bit patterns for the three-point route above and an adjacent one-segment control.
  • The three-point result must be identical on Swift and Kotlin. If the existing Swift/oracle order is selected, the expected bits are 403e89813f76c75c.
  • Preserve existing empty/single-point zero behavior and ordinary route-distance accuracy.
  • Implement Production only upstream from a fresh current-main branch. Do not transplant a Product fix into the fork parity harness.
  • Native execution requires separate authorization.

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