From 80113456ae6ccc597a7d7c086db1a1a6edd9b859 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Fri, 14 Aug 2026 18:14:10 +0100 Subject: [PATCH] =?UTF-8?q?Price=20display:=20station=20sign=20drops=20the?= =?UTF-8?q?=20=C2=A3=20(134.9)=20=E2=80=94=20matches=20roadside=20signs;?= =?UTF-8?q?=20pounds=20&=20pence=20uses=20forecourt=20superscript=20style?= =?UTF-8?q?=20=C2=A31.29=E2=81=B9/L=20(small=20superscript=20third=20digit?= =?UTF-8?q?,=20small=20/L);=20attributed=20Text=20on=20list/widgets/Live?= =?UTF-8?q?=20Activity,=20Unicode=20superscript=20in=20Siri=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FuelBoard/ContentView.swift | 4 +- FuelBoard/SettingsView.swift | 2 +- FuelBoard/en.lproj/Localizable.strings | 2 +- .../FuelBoardSharedTests/FuelBoardTests.swift | 15 ++-- .../FuelBoardLiveActivityView.swift | 8 +-- FuelBoardWidgets/FuelPriceWidget.swift | 8 +-- Shared/FuelStore.swift | 70 ++++++++++++++++--- 7 files changed, 76 insertions(+), 33 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 64568af..a605b31 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -553,9 +553,7 @@ struct StationRow: View { Circle() .fill(ragColor) .frame(width: 8, height: 8) - Text(FuelStore.priceText(price, style: priceDisplayStyle)) - .font(.title3.bold().monospaced()) - .monospacedDigit() + FuelStore.priceTextAttributed(price, style: priceDisplayStyle, size: 20, weight: .bold) } if let deltaText { Text(deltaText) diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index 427ff09..e0131fb 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -103,7 +103,7 @@ struct SettingsView: View { } header: { Text("Units") } footer: { - Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (£129.9) or in pounds and pence (£1.299).") + Text("Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L).") } Section { diff --git a/FuelBoard/en.lproj/Localizable.strings b/FuelBoard/en.lproj/Localizable.strings index e2caf50..6832e2c 100644 --- a/FuelBoard/en.lproj/Localizable.strings +++ b/FuelBoard/en.lproj/Localizable.strings @@ -51,7 +51,7 @@ /* Settings tab */ "Units" = "Units"; "Price display" = "Price display"; -"Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (£129.9) or in pounds and pence (£1.299)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (£129.9) or in pounds and pence (£1.299)."; +"Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)." = "Distances and search radii across the app, widget and alerts are shown in this unit. Prices can be shown as on a station sign (129.9) or in pounds and pence (£1.29⁹/L)."; "Show introduction" = "Show introduction"; "Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts."; "Test alert notification (real data)" = "Test alert notification (real data)"; diff --git a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift index 6567758..8c787b5 100644 --- a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift +++ b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift @@ -566,15 +566,18 @@ final class SiriCheapestLookupTests: XCTestCase { final class PriceDisplayTests: XCTestCase { func testStationSignStyle() { - XCTAssertEqual(FuelStore.priceText(129.9, style: .stationSign), "£129.9") - XCTAssertEqual(FuelStore.priceText(135.0, style: .stationSign), "£135.0") - XCTAssertEqual(FuelStore.priceText(249.9, style: .stationSign), "£249.9") + // Bare pence figure, no £ — exactly the roadside sign. + XCTAssertEqual(FuelStore.priceText(129.9, style: .stationSign), "129.9") + XCTAssertEqual(FuelStore.priceText(135.0, style: .stationSign), "135.0") + XCTAssertEqual(FuelStore.priceText(249.9, style: .stationSign), "249.9") } func testPoundsPenceStyle() { - XCTAssertEqual(FuelStore.priceText(129.9, style: .poundsPence), "£1.299") - XCTAssertEqual(FuelStore.priceText(135.0, style: .poundsPence), "£1.350") - XCTAssertEqual(FuelStore.priceText(100.9, style: .poundsPence), "£1.009") + // Forecourt superscript style: £ + 2dp + small superscript third digit + /L. + XCTAssertEqual(FuelStore.priceText(129.9, style: .poundsPence), "£1.29⁹/L") + XCTAssertEqual(FuelStore.priceText(135.0, style: .poundsPence), "£1.35⁰/L") + XCTAssertEqual(FuelStore.priceText(100.9, style: .poundsPence), "£1.00⁹/L") + XCTAssertEqual(FuelStore.priceText(199.9, style: .poundsPence), "£1.99⁹/L") } func testSpokenAlwaysPounds() { diff --git a/FuelBoardWidgets/FuelBoardLiveActivityView.swift b/FuelBoardWidgets/FuelBoardLiveActivityView.swift index deda016..ee38808 100644 --- a/FuelBoardWidgets/FuelBoardLiveActivityView.swift +++ b/FuelBoardWidgets/FuelBoardLiveActivityView.swift @@ -70,8 +70,7 @@ private struct FuelBoardLiveActivityView: View { // RIGHT — price VStack(alignment: .trailing, spacing: 2) { - Text(context.state.priceText) - .font(.title2.bold().monospacedDigit()) + FuelStore.priceTextAttributed(context.state.pricePence, size: 22, weight: .bold) Text("Tap for directions") .font(.caption2) .foregroundStyle(.secondary) @@ -87,8 +86,7 @@ private struct FuelBoardLiveActivityPriceView: View { let context: ActivityViewContext var body: some View { - Text(context.state.priceText) - .font(.headline.bold().monospacedDigit()) + FuelStore.priceTextAttributed(context.state.pricePence, size: 17, weight: .bold) } } @@ -128,7 +126,7 @@ extension FuelBoardLiveActivityAttributes.ContentState { } /// Price string from pence, in the user's chosen display style - /// (station sign "£129.9" or pounds "£1.299"). + /// (station sign "129.9" or pounds "£1.29⁹/L"). var priceText: String { FuelStore.priceText(pricePence) } diff --git a/FuelBoardWidgets/FuelPriceWidget.swift b/FuelBoardWidgets/FuelPriceWidget.swift index 1f346d3..409e6a0 100644 --- a/FuelBoardWidgets/FuelPriceWidget.swift +++ b/FuelBoardWidgets/FuelPriceWidget.swift @@ -395,9 +395,7 @@ struct FuelPriceWidgetView: View { .font(.headline) .lineLimit(1) if let price = station.prices[entry.fuel] { - Text(FuelStore.priceText(price)) - .font(.system(size: 26, weight: .bold).monospaced()) - .foregroundStyle(.green) + FuelStore.priceTextAttributed(price, size: 26, weight: .bold, color: .green) } if let location = entry.location { Text(entry.unit.format(station.distanceKM(to: location.lat, lng2: location.lng)) + " away") @@ -455,9 +453,7 @@ struct FuelPriceWidgetView: View { Circle() .fill(ragColor(for: price, cheapest: cheapest)) .frame(width: 6, height: 6) - Text(FuelStore.priceText(price)) - .font(.caption.weight(.bold).monospaced()) - .foregroundStyle(.primary) + FuelStore.priceTextAttributed(price, size: 12, weight: .bold) } } } diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index b726608..d31fbdc 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -9,6 +9,7 @@ import Foundation import Security +import SwiftUI // MARK: - Fuel types @@ -145,19 +146,20 @@ enum DistanceUnit: String, Codable, CaseIterable, Identifiable { /// Display style for fuel prices. Internally prices are always stored and /// computed in pence-per-litre (GOV.UK's unit, e.g. 129.9); the style only -/// affects RENDERING: the station-sign convention shows the pence figure -/// ("£129.9", what a forecourt sign shows), pounds & pence shows the -/// converted value ("£1.299"). Calculations never see this. +/// affects RENDERING: the station-sign convention shows the bare pence +/// figure ("129.9", no £ — exactly what a UK forecourt sign shows), pounds & +/// pence shows the converted value in the forecourt's superscript style +/// ("£1.29⁹/L"). Calculations never see this. enum PriceDisplayStyle: String, Codable, CaseIterable, Identifiable { - case stationSign // £129.9 — matches the sign - case poundsPence // £1.299 + case stationSign // 129.9 — bare pence figure, like the roadside sign + case poundsPence // £1.29⁹/L — small superscript third digit, small /L var id: String { rawValue } var displayName: String { switch self { - case .stationSign: return "Station sign (£129.9)" - case .poundsPence: return "Pounds & pence (£1.299)" + case .stationSign: return "Station sign (129.9)" + case .poundsPence: return "Pounds & pence (£1.29⁹)" } } } @@ -454,7 +456,7 @@ struct FuelStore { saveString(unit.rawValue, service: distanceUnitKey) } - // MARK: Price display — station-sign (£129.9) vs pounds & pence (£1.299). + // MARK: Price display — station sign (129.9) vs pounds & pence (£1.29⁹/L). // Prices are always stored/computed in pence-per-litre; this style only // changes how they are RENDERED, so it can never affect calculations. // Default station sign = the forecourt convention. @@ -472,12 +474,58 @@ struct FuelStore { saveString(style.rawValue, service: priceDisplayStyleKey) } + /// Superscript digit glyphs for the pounds & pence format's small raised + /// third digit (the forecourt style: "£1.29⁹"). + private static let superscriptDigits: [Character] = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"] + /// Render a pence-per-litre price per the saved style: - /// stationSign -> "£129.9" (sign convention), poundsPence -> "£1.299". + /// stationSign -> "129.9" (bare pence figure, no £ — the roadside sign), + /// poundsPence -> "£1.29⁹/L" (pounds with a superscript third digit and a + /// small /L unit, matching UK garage displays). static func priceText(_ pence: Double, style: PriceDisplayStyle? = nil) -> String { switch style ?? loadPriceDisplayStyle() { - case .stationSign: return String(format: "£%.1f", pence) - case .poundsPence: return String(format: "£%.3f", pence / 100) + case .stationSign: + return String(format: "%.1f", pence) + case .poundsPence: + let tenths = Int((pence * 10).rounded()) + let whole = tenths / 1000 + let major = (tenths % 1000) / 10 + let minor = tenths % 10 + return String(format: "£%d.%02d", whole, major) + + String(superscriptDigits[minor]) + + "/L" + } + } + + /// Rich, forecourt-styled price text for SwiftUI surfaces. Same formats as + /// `priceText`, but the pounds & pence mode renders the third digit small + /// and superscripted and /L small and muted — proper text styling instead + /// of Unicode glyphs. Pass the surface's size/weight/color; the returned + /// Text carries its own fonts, so do NOT apply `.font(...)` on top. + static func priceTextAttributed(_ pence: Double, style: PriceDisplayStyle? = nil, + size: CGFloat = 17, weight: Font.Weight = .semibold, + color: Color = .primary) -> Text { + let s = style ?? loadPriceDisplayStyle() + let base = Font.system(size: size, weight: weight).monospaced() + switch s { + case .stationSign: + return Text(priceText(pence, style: s)).font(base).foregroundColor(color) + case .poundsPence: + let tenths = Int((pence * 10).rounded()) + let whole = tenths / 1000 + let major = (tenths % 1000) / 10 + let minor = tenths % 10 + let main = Text(String(format: "£%d.%02d", whole, major)) + .font(base) + .foregroundColor(color) + let sup = Text(String(superscriptDigits[minor])) + .font(.system(size: size * 0.6, weight: weight).monospaced()) + .baselineOffset(size * 0.35) + .foregroundColor(color) + let perL = Text("/L") + .font(.system(size: size * 0.5, weight: .regular).monospaced()) + .foregroundColor(color.opacity(0.55)) + return main + sup + perL } }