Price display: station sign drops the £ (134.9) — matches roadside signs; pounds & pence uses forecourt superscript style £1.29⁹/L (small superscript third digit, small /L); attributed Text on list/widgets/Live Activity, Unicode superscript in Siri cards
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<FuelBoardLiveActivityAttributes>
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+59
-11
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user