Skip to content

iOS rounds a stress level with printf where Android uses arithmetic, diverging on three exact values #2167

Description

@ryanbr

What

Android and iOS round a 0-3 stress level with two different functions. They agree on every level either platform can realistically produce, so this is a latent parity gap rather than a live bug, filed because the codebase has already been bitten by this exact difference once.

how a level reaches one decimal
Android (value * 10).roundToInt(), arithmetic, half away from zero on the product
iOS String(format: "%.1f", lvl), C printf, half to even on the binary value

StressView.swift uses String(format:) at every level display (peak, avg, today, the trend rows). Android settled on arithmetic in #2165.

The divergence, exactly

Only values that are exact binary fractions can disagree, and inside 0-3 there are three:

value   iOS     Android
0.25    0.2     0.3
1.25    1.2     1.3
2.25    2.2     2.3

0.75, 1.75 and 2.75 agree, half to even landing on the same digit half up does.

Over two million continuous levels drawn across the domain: zero disagreements. A logistic squash lands within an ulp of a .x5 boundary about never, so on iOS, where the level stays a raw double all the way to the formatter, this cannot be reached in practice.

Why file it anyway

SleepStagerTrace.round1 documents this precise difference and records a harness catching the two sides disagreeing on a real number, Kotlin 64.1 against Swift 64.0, on a value produced by a multiplicative band rather than a logistic. Its conclusion was to do the arithmetic on both platforms rather than let one side use printf:

Multiplying and rounding is IEEE-deterministic, so both platforms do the same arithmetic to the same bits and cannot disagree, whichever side of .5 the product lands on.

Stress has not adopted that on the Swift side. The parity contract wants the analytic surfaces byte-identical, and "identical except on three values nothing currently generates" is a thing that stops being true the moment the level stops coming straight from a logistic.

Note that #2166 makes exactly 2.25 reachable on Android, since the widget's snapshot quantises to two decimals. It does not make it reachable on iOS, which has no equivalent step, so the two do not currently meet. Whichever way #2166 goes is worth deciding before this one.

What to change

Give StressTrace.swift a formatLevel doing the same arithmetic as the Kotlin twin, clamped the same way, and route StressView.swift and StressWidget.swift through it instead of String(format:). That is the shape round1 already established, and it would give the level one spelling across all four surfaces rather than the separator-only agreement #2165 reached.

Low priority. No user currently sees a wrong number from this.

Activity

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