diff --git a/FuelBoard/AlertsView.swift b/FuelBoard/AlertsView.swift index 1291fa4..0910886 100644 --- a/FuelBoard/AlertsView.swift +++ b/FuelBoard/AlertsView.swift @@ -32,8 +32,16 @@ struct AlertsView: View { /// The Stations-tab search distance shown in the user's unit, e.g. /// "5 miles" or "8 km" — for the "Follow search" rows. private var searchInUnitLabel: String { - let km = Double(stationLimit) * 1.60934 // stationLimit is always miles - return "\(Int(distanceUnit.fromKM(km).rounded())) \(distanceUnit.label)" + let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles + return "\(shown) \(distanceUnit.label(for: Double(shown)))" + } + + /// A miles value (5/10/15) shown truthfully in the user's unit, e.g. + /// "5 miles" (miles unit) or "8 km" (km unit) — picker labels must match + /// what the option really means. + private func distanceLabel(forMiles miles: Int) -> String { + let shown = distanceUnit.displayMiles(miles) + return "\(shown) \(distanceUnit.label(for: Double(shown)))" } private enum AlertRadiusChoice: Hashable { @@ -102,7 +110,7 @@ struct AlertsView: View { Picker("Radius", selection: alertChoice) { Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch) ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in - Text("\\(value) \\(distanceUnit.label)").tag(AlertRadiusChoice.fixed(value)) + Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value)) } } } header: { @@ -146,7 +154,7 @@ struct AlertsView: View { Picker("Distance", selection: laChoice) { Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch) ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in - Text("\\(miles) \\(distanceUnit.label)").tag(LADistanceChoice.fixed(miles)) + Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles)) } } } header: { diff --git a/FuelBoard/StationsView.swift b/FuelBoard/StationsView.swift index f60fa39..7dc8257 100644 --- a/FuelBoard/StationsView.swift +++ b/FuelBoard/StationsView.swift @@ -30,11 +30,11 @@ struct StationsView: View { Section { if let location { if sortMode == .closest { - Text("Closest \(selectedFuel.displayName) stations — nearest first, best value within \(stationLimit) \(distanceUnit.label) · \(totalCount) stations") + Text("Closest \(selectedFuel.displayName) stations — nearest first, best value within \(distanceUnit.displayMiles(stationLimit)) \(distanceUnit.label(for: Double(distanceUnit.displayMiles(stationLimit)))) · \(totalCount) stations") .font(.footnote) .foregroundStyle(.secondary) } else { - Text("Cheapest \(selectedFuel.displayName) within \(stationLimit) \(distanceUnit.label) — \(totalCount) stations · tap a station for directions.") + Text("Cheapest \(selectedFuel.displayName) within \(distanceUnit.displayMiles(stationLimit)) \(distanceUnit.label(for: Double(distanceUnit.displayMiles(stationLimit)))) — \(totalCount) stations · tap a station for directions.") .font(.footnote) .foregroundStyle(.secondary) } @@ -55,7 +55,8 @@ struct StationsView: View { Section("Distance") { Picker("Distance", selection: $stationLimit) { ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in - Text("\(miles) \(distanceUnit.label)").tag(miles) + let shown = distanceUnit.displayMiles(miles) + Text("\(shown) \(distanceUnit.label(for: Double(shown)))").tag(miles) } } .pickerStyle(.segmented) diff --git a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift index 1fdac14..0db81fb 100644 --- a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift +++ b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift @@ -353,3 +353,22 @@ final class AlertRadiusFollowTests: XCTestCase { XCTAssertEqual(eff, 5 * 1.60934, accuracy: 0.001) } } + +final class DistanceLabelTests: XCTestCase { + func testUnitLabelPluralizesMiles() { + XCTAssertEqual(DistanceUnit.miles.label(for: 1), "mile") + XCTAssertEqual(DistanceUnit.miles.label(for: 2), "miles") + XCTAssertEqual(DistanceUnit.miles.label(for: 5), "miles") + XCTAssertEqual(DistanceUnit.kilometers.label(for: 1), "km") + XCTAssertEqual(DistanceUnit.kilometers.label(for: 8), "km") + } + + func testDisplayMilesShowsTrueDistance() { + // Picker labels must match what the option really means. + XCTAssertEqual(DistanceUnit.miles.displayMiles(5), 5) + XCTAssertEqual(DistanceUnit.miles.displayMiles(15), 15) + XCTAssertEqual(DistanceUnit.kilometers.displayMiles(5), 8) // 5 mi = 8.05 km + XCTAssertEqual(DistanceUnit.kilometers.displayMiles(10), 16) // 10 mi = 16.1 km + XCTAssertEqual(DistanceUnit.kilometers.displayMiles(15), 24) // 15 mi = 24.1 km + } +} diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index 79e5b8d..b6a7b05 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -105,6 +105,22 @@ enum DistanceUnit: String, Codable, CaseIterable, Identifiable { } } + /// Full word for a specific value — pluralizes miles ("1 mile" vs + /// "5 miles"); metric is always "km". + func label(for value: Double) -> String { + switch self { + case .miles: return value == 1 ? "mile" : "miles" + case .kilometers: return "km" + } + } + + /// A whole-mile distance (as stored/used by search + Live Activity) + /// shown in this unit, rounded to a whole number for picker labels: + /// 5 miles -> "5" (miles unit) or "8" (km unit). + func displayMiles(_ miles: Int) -> Int { + Int(fromKM(Double(miles) * 1.60934).rounded()) + } + /// Convert a value expressed in this unit to km. func toKM(_ value: Double) -> Double { switch self {