From 11d370b3b86c84015ef83aff6b24e42e1bf648b8 Mon Sep 17 00:00:00 2001 From: Yuri Chukhlib Date: Sun, 23 Aug 2026 12:21:54 +0200 Subject: [PATCH] Fix playback rate display dropping meaningful digits (#963) PlaybackRate.displayText/compactDisplayText used String(format: "%.2gx"), whose significant-figure notation dropped meaningful digits (1.25 -> "1.2x") and rounded others (1.75 -> "1.8x"). Replace with a fixed two-decimal format + trailing-zero strip so whole rates have no decimal (2 -> "2x"), halves show one (1.5 -> "1.5x"), and quarter-steps are preserved (1.25 -> "1.25x"). Adds a Swift Testing suite (PlaybackRateTests) proving red->green. --- Yattee/Services/Player/PlayerState.swift | 17 +++++++- YatteeTests/PlaybackRateTests.swift | 51 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 YatteeTests/PlaybackRateTests.swift diff --git a/Yattee/Services/Player/PlayerState.swift b/Yattee/Services/Player/PlayerState.swift index 76533df2..6fc1828a 100644 --- a/Yattee/Services/Player/PlayerState.swift +++ b/Yattee/Services/Player/PlayerState.swift @@ -131,13 +131,26 @@ enum PlaybackRate: Double, CaseIterable, Identifiable, Sendable { if rawValue == 1.0 { return String(localized: "player.playbackRate.normal") } - return String(format: "%.2gx", rawValue) + return formattedRate } /// Compact display text that always shows numeric value (e.g., "1x", "1.5x"). /// Use this in space-constrained UI like the player pill. var compactDisplayText: String { - String(format: "%.2gx", rawValue) + formattedRate + } + + /// The rate with an `x` suffix rendered with a fixed two-decimal format and + /// the trailing `.0` stripped, so whole rates have no decimal + /// (2 → "2x", 1.5 → "1.5x", 1.25 → "1.25x"). Replaces `%.2g`, whose + /// significant-figure notation dropped meaningful digits (1.25 → "1.2") and + /// rounded others (1.75 → "1.8"). `String(format:)` keeps the period decimal + /// separator, matching the player's other speed labels. + private var formattedRate: String { + var value = String(format: "%.2f", rawValue) + while value.hasSuffix("0") { value.removeLast() } + if value.hasSuffix(".") { value.removeLast() } + return "\(value)x" } } diff --git a/YatteeTests/PlaybackRateTests.swift b/YatteeTests/PlaybackRateTests.swift new file mode 100644 index 00000000..ea331f38 --- /dev/null +++ b/YatteeTests/PlaybackRateTests.swift @@ -0,0 +1,51 @@ +// +// PlaybackRateTests.swift +// YatteeTests +// +// Tests for PlaybackRate display-text formatting. +// + +import Testing +import Foundation +@testable import Yattee + +@Suite("PlaybackRate Display Tests") +struct PlaybackRateDisplayTests { + @Test("displayText renders every rate without losing precision") + func displayTextPrecision() { + #expect(PlaybackRate.x025.displayText == "0.25x") + #expect(PlaybackRate.x05.displayText == "0.5x") + #expect(PlaybackRate.x075.displayText == "0.75x") + #expect(PlaybackRate.x125.displayText == "1.25x") + #expect(PlaybackRate.x15.displayText == "1.5x") + #expect(PlaybackRate.x175.displayText == "1.75x") + #expect(PlaybackRate.x2.displayText == "2x") + #expect(PlaybackRate.x25.displayText == "2.5x") + #expect(PlaybackRate.x3.displayText == "3x") + } + + @Test("displayText for the normal rate uses the localized label, not the numeric form") + func displayTextNormal() { + let normal = PlaybackRate.x1.displayText + #expect(normal == String(localized: "player.playbackRate.normal")) + #expect(!normal.hasSuffix("x")) + } + + @Test("compactDisplayText always shows the numeric value") + func compactDisplayText() { + #expect(PlaybackRate.x1.compactDisplayText == "1x") + #expect(PlaybackRate.x125.compactDisplayText == "1.25x") + #expect(PlaybackRate.x15.compactDisplayText == "1.5x") + #expect(PlaybackRate.x175.compactDisplayText == "1.75x") + #expect(PlaybackRate.x2.compactDisplayText == "2x") + #expect(PlaybackRate.x3.compactDisplayText == "3x") + } + + @Test("every case has a non-empty display string") + func allCasesNonEmpty() { + for rate in PlaybackRate.allCases { + #expect(!rate.displayText.isEmpty) + #expect(!rate.compactDisplayText.isEmpty) + } + } +}