Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve currency localization ergonomics #34

Merged
merged 1 commit into from
Mar 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 66 additions & 9 deletions Sources/Currency/AnyCurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,23 +186,80 @@ extension AnyCurrency {

// MARK: String Representation

// price.description 3000.98 USD
// price.debugDescription USD(3000.98)
// price.playgroundDescription USD(3000.98)
// "\(localize: price)" $3,000.98
// "\(localize: price, withFormatter: ...)" $3,000.98
// "\(localize: price, forLocale: .current)" $3,000.98
// price.description 3000.98 USD
// price.debugDescription USD(3000.98)
// price.playgroundDescription USD(3000.98)
// "\(localize: price)" $3,000.98
// "\(localize: price, with: ...)" $3,000.98
// "\(localize: price, for: ...)" $3,000.98

extension AnyCurrency {
public var description: String { return "\(self.amount.description) \(Self.metadata.alphabeticCode)" }
public var debugDescription: String { return "\(Self.metadata.alphabeticCode)(\(self.amount.description))"}
public var playgroundDescription: Any { return self.debugDescription }

/// Creates a string representation of the currency value, localized to a particular locale.
///
/// let usd = USD(30.03)
/// print(usd.localizedString(for: .init(identifier: "fr_FR")))
/// // 30,03 $US
/// print(usd.localizedString())
/// // $30.03, assuming `Locale.current` is "en_US"
///
/// - Note: This can also be done with String interpolation:
///
/// let pounds = GBP(30.03)
/// let localizedString = "\(localize: pounds, for: Locale(identifier: "de_DE"))"
/// print(localizedString)
/// // 30,03 £
/// print("\(localize: pounds)")
/// // £30.03, assuming `Locale.current` is "en_US"
///
/// - Parameters:
/// - locale: The Locale to localize the value for. The default is `.current`, ig. the runtime environment's Locale.
/// - nilValue: The String representation to use if the value is `nil` or localization fails. The default is `"nil"`.
/// - Returns: A localized String representation of the currency value.
public func localizedString(for locale: Locale = .current, nilDescription nilValue: String = "nil") -> String {
return "\(localize: self, for: locale, nilDescription: nilValue)"
}

/// Creates a string representation of the currency value, using the provided formatter.
///
/// let formatter = NumberFormatter()
/// formatter.numberStyle = .currency
/// formatter.currencyGroupingSeparator = " "
/// formatter.currencyDecimalSeparator = "'"
/// formatter.currencyCode = "GBP"
///
/// let pounds = GBP(14928.02)
/// print(pounds.localizedString(using: formatter))
/// // £14 928'02
///
///- Note: This can also be done with String interpolation:
///
/// let formatter = ...
/// let currency = ...
/// let localizedString = "\(localize: currency, with: formatter)"
///
/// - Important: Since `Foundation.NumberFormatter` is a class, this method does not set the `currencyCode` property automatically.
///
/// If it did, this method would no longer be thread safe, and would mutate the provided instance.
///
/// If the same formatter is to be used for different currencies, the property will need to be updated before calling this method.
///
/// - Parameters:
/// - formatter: The pre-configured formatter to use.
/// - nilValue: The String representation to use if the value is `nil` or localization fails. The default is `"nil"`.
/// - Returns: A localized String representation of the currency value.
public func localizedString(using formatter: NumberFormatter, nilDescription nilValue: String = "nil") -> String {
return "\(localize: self, with: formatter, nilDescription: nilValue)"
}
}

extension String.StringInterpolation {
public mutating func appendInterpolation<Currency: AnyCurrency>(
localize value: Currency?,
forLocale locale: Locale = .current,
for locale: Locale = .current,
nilDescription nilValue: String = "nil"
) {
guard case let .some(value) = value else { return nilValue.write(to: &self) }
Expand All @@ -212,12 +269,12 @@ extension String.StringInterpolation {
formatter.locale = locale
formatter.currencyCode = Currency.metadata.alphabeticCode

self.appendInterpolation(localize: value, withFormatter: formatter, nilDescription: nilValue)
self.appendInterpolation(localize: value, with: formatter, nilDescription: nilValue)
}

public mutating func appendInterpolation<Currency: AnyCurrency>(
localize value: Currency?,
withFormatter formatter: NumberFormatter,
with formatter: NumberFormatter,
nilDescription nilValue: String = "nil"
) {
guard case let .some(value) = value else { return nilValue.write(to: &self) }
Expand Down
22 changes: 13 additions & 9 deletions Tests/CurrencyTests/AnyCurrencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ extension AnyCurrencyTests {

func testStringInterpolation_customLocale() {
let pounds = GBP(14928.789)
XCTAssertEqual("\(localize: pounds, forLocale: .init(identifier: "en_UK"))", "£14,928.79")
XCTAssertEqual("\(localize: pounds, forLocale: .init(identifier: "de_DE"))", "14.928,79 £")
XCTAssertEqual("\(localize: pounds, for: .init(identifier: "en_UK"))", "£14,928.79")
XCTAssertEqual("\(localize: pounds, for: .init(identifier: "de_DE"))", "14.928,79 £")

let yen = JPY(400.9)
#if swift(<5.1) && os(Linux)
Expand All @@ -142,17 +142,19 @@ extension AnyCurrencyTests {
let expectedFrenchYen = "401 JPY"
let expectedGreekYen = "401 JP¥"
#endif
XCTAssertEqual("\(localize: yen, forLocale: .init(identifier: "fr"))", expectedFrenchYen)
XCTAssertEqual("\(localize: yen, forLocale: .init(identifier: "el"))", expectedGreekYen)
let frenchLocale = Locale(identifier: "fr")
XCTAssertEqual("\(localize: yen, for: frenchLocale)", expectedFrenchYen)
XCTAssertEqual(yen.localizedString(for: frenchLocale), expectedFrenchYen)
XCTAssertEqual("\(localize: yen, for: .init(identifier: "el"))", expectedGreekYen)

let dinar = KWD(100.9289)
#if swift(<5.2) && os(Linux)
let expectedIrishDinar = "KWD100.929"
#else
let expectedIrishDinar = "KWD 100.929"
#endif
XCTAssertEqual("\(localize: dinar, forLocale: .init(identifier: "ga"))", expectedIrishDinar)
XCTAssertEqual("\(localize: dinar, forLocale: .init(identifier: "hr"))", "100,929 KWD")
XCTAssertEqual("\(localize: dinar, for: .init(identifier: "ga"))", expectedIrishDinar)
XCTAssertEqual("\(localize: dinar, for: .init(identifier: "hr"))", "100,929 KWD")
}

func testStringInterpolation_customFormatter() {
Expand All @@ -163,11 +165,13 @@ extension AnyCurrencyTests {

let pounds = GBP(14928.018)
formatter.currencyCode = GBP.alphabeticCode
XCTAssertEqual("\(localize: pounds, withFormatter: formatter)", "£14 928'02")
XCTAssertEqual("\(localize: pounds, with: formatter)", "£14 928'02")

let expectedYenResult = "¥4 001"
let yen = JPY(4000.9)
formatter.currencyCode = JPY.alphabeticCode
XCTAssertEqual("\(localize: yen, withFormatter: formatter)", "¥4 001")
XCTAssertEqual("\(localize: yen, with: formatter)", expectedYenResult)
XCTAssertEqual(yen.localizedString(using: formatter), expectedYenResult)

let dinar = KWD(92.0299)
formatter.currencyCode = KWD.alphabeticCode
Expand All @@ -176,7 +180,7 @@ extension AnyCurrencyTests {
#else
let expectedDinar = "KWD 92'030"
#endif
XCTAssertEqual("\(localize: dinar, withFormatter: formatter)", expectedDinar)
XCTAssertEqual("\(localize: dinar, with: formatter)", expectedDinar)
}

func testStringInterpolation_optional() {
Expand Down