feat: accept unambiguous currency names

This commit is contained in:
2026-10-22 14:10:33 +02:00
parent 0a85d21801
commit 1ec4c0d582
4 changed files with 63 additions and 49 deletions
+39 -16
View File
@@ -103,9 +103,28 @@ public enum Calculator {
"wbtc", "weth",
]
private static let currencyNames: [String: String] = [
"yen": "jpy", "japanese yen": "jpy",
"euro": "eur", "euros": "eur",
"danish krone": "dkk", "danish kroner": "dkk",
"swedish krona": "sek", "swedish kronor": "sek",
"norwegian krone": "nok", "norwegian kroner": "nok",
"us dollar": "usd", "us dollars": "usd",
"american dollar": "usd", "american dollars": "usd",
"canadian dollar": "cad", "canadian dollars": "cad",
"australian dollar": "aud", "australian dollars": "aud",
"new zealand dollar": "nzd", "new zealand dollars": "nzd",
"british pound": "gbp", "british pounds": "gbp", "pound sterling": "gbp",
"swiss franc": "chf", "swiss francs": "chf",
"chinese yuan": "cny", "yuan": "cny",
"indian rupee": "inr", "indian rupees": "inr",
"south korean won": "krw",
"bitcoin": "btc", "bitcoins": "btc", "ether": "eth", "ethereum": "eth",
]
private static func currencyCode(_ text: String, rates: CurrencyRates?) -> String? {
let code = text.lowercased()
if code == "yen" { return "jpy" }
if let code = currencyNames[code] { return code }
guard (2...12).contains(code.utf8.count),
code.utf8.allSatisfy({ (97...122).contains($0) || (48...57).contains($0) }),
ConversionUnit.parse(text) == nil else { return nil }
@@ -116,21 +135,25 @@ public enum Calculator {
private static func currencyQuery(_ query: String, rates: CurrencyRates?) -> CurrencyQuery? {
guard query.utf8.count <= 512 else { return nil }
let words = query.split(whereSeparator: \.isWhitespace)
guard words.count >= 3,
["to", "in", "as"].contains(words[words.count - 2].lowercased()),
let target = currencyCode(String(words[words.count - 1]), rates: rates) else { return nil }
let sourceText = words.dropLast(2).joined(separator: " ")
let characters = Array(sourceText)
guard characters.count > 1 else { return nil }
for length in stride(from: min(12, characters.count - 1), through: 2, by: -1) {
let suffix = String(characters.suffix(length))
guard let source = currencyCode(suffix, rates: rates) else { continue }
let boundary = characters[characters.count - length - 1]
// Digit-bearing tickers need a space so 21inch cannot silently become 2 × 1INCH.
guard boundary.isWhitespace || (suffix.allSatisfy(\.isLetter) && !boundary.isLetter) else { continue }
let expression = String(characters.dropLast(length)).trimmingCharacters(in: .whitespaces)
guard let amount = parse(expression) else { continue }
return CurrencyQuery(expression: expression, source: source, target: target, amount: amount)
guard words.count >= 3 else { return nil }
for separator in 1..<(words.count - 1)
where ["to", "in", "as"].contains(words[separator].lowercased()) {
guard let target = currencyCode(words[(separator + 1)...].joined(separator: " "), rates: rates) else {
continue
}
let sourceText = words[..<separator].joined(separator: " ")
let characters = Array(sourceText)
guard characters.count > 1 else { continue }
for length in stride(from: min(32, characters.count - 1), through: 2, by: -1) {
let suffix = String(characters.suffix(length))
guard let source = currencyCode(suffix, rates: rates) else { continue }
let boundary = characters[characters.count - length - 1]
// Digit-bearing tickers need a space so 21inch cannot silently become 2 × 1INCH.
guard boundary.isWhitespace || (suffix.allSatisfy { $0.isLetter || $0.isWhitespace } && !boundary.isLetter) else { continue }
let expression = String(characters.dropLast(length)).trimmingCharacters(in: .whitespaces)
guard let amount = parse(expression) else { continue }
return CurrencyQuery(expression: expression, source: source, target: target, amount: amount)
}
}
return nil
}