diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe2ddff83..76cb56484b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ ### Performance - OpenCodex: parse only newly appended usage-log entries instead of rebuilding the entire cache on every refresh, cutting memory use while preserving compatibility with older app versions (#3140). Thanks @olddonkey! +### Usage & Spend +- Added AED (UAE dirham) to the display currency options (#3186). Thanks @samiashi! + ### Localization - Turkish: improve translations throughout settings and provider views, including the previously untranslated iCloud sync section (#3178). Thanks @husodrn46! diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index b1d07600da..ce2af53b57 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -91,6 +91,7 @@ enum PreferredCurrencyOption: String, CaseIterable, Identifiable { case sgd = "SGD" case inr = "INR" case chf = "CHF" + case aed = "AED" var id: String { self.rawValue @@ -113,6 +114,7 @@ enum PreferredCurrencyOption: String, CaseIterable, Identifiable { case .sgd: "SGD ($)" case .inr: "INR (₹)" case .chf: "CHF (Fr.)" + case .aed: "AED (د.إ)" } } } diff --git a/Sources/CodexBarCore/CurrencyExchange.swift b/Sources/CodexBarCore/CurrencyExchange.swift index 1908c2f2c3..c795d91856 100644 --- a/Sources/CodexBarCore/CurrencyExchange.swift +++ b/Sources/CodexBarCore/CurrencyExchange.swift @@ -15,7 +15,7 @@ public final class CurrencyExchange: @unchecked Sendable { /// All currency codes supported by the converter. public static let supportedCurrencies: [String] = [ - "USD", "GBP", "EUR", "CZK", "CNY", "JPY", "KRW", "CAD", "AUD", "HKD", "TWD", "SGD", "INR", "CHF", + "USD", "GBP", "EUR", "CZK", "CNY", "JPY", "KRW", "CAD", "AUD", "HKD", "TWD", "SGD", "INR", "CHF", "AED", ] private let lock = NSLock() @@ -36,6 +36,7 @@ public final class CurrencyExchange: @unchecked Sendable { "SGD": 1.34, "INR": 84.50, "CHF": 0.80, + "AED": 3.6725, ] private var lastFetchTime: Date? diff --git a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift index dd6abb881e..c27248c664 100644 --- a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift +++ b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift @@ -895,6 +895,20 @@ struct SettingsStoreCoverageTests { fresh.preferredCurrencyCode = "GBP" let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded.preferredCurrencyCode == "GBP") + + reloaded.preferredCurrencyCode = "AED" + let reloadedAED = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) + #expect(reloadedAED.preferredCurrencyCode == "AED") + } + + @Test + func `preferred currency picker matches every supported exchange currency`() { + let pickerCurrencies = PreferredCurrencyOption.allCases + .filter { $0 != .auto } + .map(\.rawValue) + + #expect(pickerCurrencies == CurrencyExchange.supportedCurrencies) + #expect(PreferredCurrencyOption.aed.label == "AED (د.إ)") } private static func makeSettingsStore( diff --git a/Tests/CodexBarTests/UsageFormatterTests.swift b/Tests/CodexBarTests/UsageFormatterTests.swift index a64529b175..cf26276170 100644 --- a/Tests/CodexBarTests/UsageFormatterTests.swift +++ b/Tests/CodexBarTests/UsageFormatterTests.swift @@ -476,7 +476,7 @@ struct UsageFormatterTests { } @Test - func `currency exchange converts rates and formats correctly`() { + func `currency exchange converts rates and formats correctly`() throws { let exchange = CurrencyExchange.shared let epsilon = 1e-9 // USD → USD is identity @@ -523,6 +523,16 @@ struct UsageFormatterTests { #expect(explicitCZK.contains("CZK")) #expect(explicitCZK.contains(".")) + let aedRate = try #require(exchange.rate(for: "AED")) + #expect(abs((exchange.convert(usdAmount: 10.0, to: "AED") ?? 0) - 10.0 * aedRate) < epsilon) + #expect(abs((exchange.convert(amount: 10.0, from: "AED", to: "USD") ?? 0) - 10.0 / aedRate) < epsilon) + #expect(abs((exchange.convert(amount: 10.0, from: "GBP", to: "AED") ?? 0) + - 10.0 / gbpRate * aedRate) < epsilon) + let explicitAED = UsageFormatter.convertedCostString(10.0, preferredCurrency: "AED", providerCurrency: "USD") + #expect(explicitAED == UsageFormatter.currencyString(10.0 * aedRate, currencyCode: "AED")) + #expect(explicitAED.hasPrefix("AED")) + #expect(explicitAED.range(of: #"\.\d{2}$"#, options: .regularExpression) != nil) + // CHF is supported: conversion through the USD pivot works both ways. let chfRate = exchange.rate(for: "CHF") ?? 0.80 #expect(abs((exchange.convert(usdAmount: 10.0, to: "CHF") ?? 0) - 10.0 * chfRate) < epsilon) @@ -550,6 +560,8 @@ struct UsageFormatterTests { #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: " eur ")) #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "KRW")) #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "CZK")) + #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "AED")) + #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: " aed ")) } @Test