Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Strand/Screens/StressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ struct StressView: View {
if let ratio = f.lfhf {
StatTile(
label: "Autonomic balance (LF/HF)",
value: String(format: "%.1f", ratio),
value: StressTrace.formatRatio(ratio),
caption: String(localized: "Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward)."),
accent: StressRamp.steady
)
Expand Down
10 changes: 10 additions & 0 deletions StrandTests/StressTraceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ final class StressTraceTests: XCTestCase {
XCTAssertEqual(StressTrace.formatLevel(2.25), "2.3")
}

func testTheLfHfRatioSharesTheLevelArithmeticWithoutItsCeiling() {
// 9/4 is an exact binary 2.25: Java's %.1f said "2.3" while C's said "2.2" (#2167).
XCTAssertEqual(StressTrace.formatRatio(9.0 / 4.0), "2.3")
XCTAssertEqual(StressTrace.formatRatio(0.25), "0.3")
XCTAssertEqual(StressTrace.formatRatio(1.75), "1.8")
// No stress-domain ceiling: a sympathetic-leaning ratio prints as itself, not as 3.0.
XCTAssertEqual(StressTrace.formatRatio(4.25), "4.3")
XCTAssertEqual(StressTrace.formatRatio(-0.3), "0.0")
}

func testAStressLevelRoundsToOneDecimal() {
XCTAssertEqual(StressTrace.formatLevel(1.85), "1.9")
XCTAssertEqual(StressTrace.formatLevel(1.96), "2.0")
Expand Down
12 changes: 12 additions & 0 deletions StrandiOSShared/StressTrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ public enum StressTrace {
return "\(tenths / 10).\(tenths % 10)"
}

/// Prints the LF/HF ratio to one decimal with the same tenths arithmetic as `formatLevel`.
///
/// The two platforms disagreed through their printf-family formatters in OPPOSITE directions —
/// Java's `%.1f` rounds half UP where C's rounds half to EVEN — so an exact quotient such as
/// 9/4 spelled "2.3" on Android and "2.2" here. A ratio has no domain ceiling, so only the
/// floor is braced; band powers cannot go negative, but the brace keeps the two surfaces
/// identical rather than trusting that.
public static func formatRatio(_ value: Double) -> String {
let tenths = max(Int((value * 10).rounded(.toNearestOrAwayFromZero)), 0)
return "\(tenths / 10).\(tenths % 10)"
}

/// The one placement rule, shared so a dot and its vertex cannot land apart.
private static func place(ts: Int64, level: Double, t0: Int64, span: CGFloat,
width: CGFloat, height: CGFloat) -> Pt {
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/java/com/noop/ui/StressScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ private fun StressAdvancedCard(
StatTile(
modifier = m,
label = uiString(R.string.l10n_stress_screen_autonomic_balance_lf_hf_776cb6f7),
value = String.format(Locale.US, "%.1f", ratio),
value = StressTrace.formatRatio(ratio),
caption = "Sympathetic vs parasympathetic tone from frequency-domain HRV. Higher leans sympathetic (stress-ward).",
accent = StressRamp.STEADY,
)
Expand Down
14 changes: 14 additions & 0 deletions android/app/src/main/java/com/noop/widget/StressTrace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@ object StressTrace {
return "${tenths / 10}.${tenths % 10}"
}

/**
* Prints the LF/HF ratio to one decimal with the same tenths arithmetic as [formatLevel].
*
* The two platforms disagreed through their printf-family formatters in OPPOSITE directions —
* Java's `%.1f` rounds half UP where C's rounds half to EVEN — so an exact quotient such as
* 9/4 spelled "2.3" here and "2.2" on iOS. A ratio has no domain ceiling, so only the floor is
* braced; band powers cannot go negative, but the brace keeps the two surfaces identical rather
* than trusting that.
*/
fun formatRatio(value: Double): String {
val tenths = (value * 10).roundToInt().coerceAtLeast(0)
return "${tenths / 10}.${tenths % 10}"
}

/**
* The labels up the left edge, top-down: the top of the domain down to zero.
*
Expand Down
18 changes: 18 additions & 0 deletions android/app/src/test/java/com/noop/widget/StressTraceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ class StressTraceTest {
assertEquals("0.0", StressTrace.formatLevel(0.04))
}

/** The exact binary quarters, matching the Swift pin — each side guards its own direction (#2167). */
@Test
fun `a level rounds half away from zero like iOS`() {
assertEquals("0.3", StressTrace.formatLevel(0.25))
assertEquals("1.3", StressTrace.formatLevel(1.25))
assertEquals("2.3", StressTrace.formatLevel(2.25))
}

/** The LF/HF ratio shares the level's tenths arithmetic without its ceiling. */
@Test
fun `the lf hf ratio shares the level arithmetic without its ceiling`() {
assertEquals("2.3", StressTrace.formatRatio(9.0 / 4.0))
assertEquals("0.3", StressTrace.formatRatio(0.25))
assertEquals("1.8", StressTrace.formatRatio(1.75))
assertEquals("4.3", StressTrace.formatRatio(4.25))
assertEquals("0.0", StressTrace.formatRatio(-0.3))
}

// #2166: the snapshot used to round to two decimals, so the widget rounded twice where the card
// rounded once and the two printed different tenths on 5% of levels.

Expand Down
Loading