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.
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.
(value * 10).roundToInt(), arithmetic, half away from zero on the productString(format: "%.1f", lvl), Cprintf, half to even on the binary valueStressView.swiftusesString(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:
0.75,1.75and2.75agree, 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
.x5boundary 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.round1documents this precise difference and records a harness catching the two sides disagreeing on a real number, Kotlin64.1against Swift64.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 useprintf: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.25reachable 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.swiftaformatLeveldoing the same arithmetic as the Kotlin twin, clamped the same way, and routeStressView.swiftandStressWidget.swiftthrough it instead ofString(format:). That is the shaperound1already 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.