在 Swift 中使用 NumberFormatter 拼出货币金额的小数部分

Spelling out the fractional part of a monetary amount using a NumberFormatter in Swift

我需要将货币金额转换为以纯文本(即支票、合同和其他法律文件)表示价值的字符串。 这可以通过 NumberFormatter 在使用数字样式 .spellOut:

时将数字转换为纯文本来完成
var curAmount = 10097.43
var fmtAmount : String

let formatter = NumberFormatter()
formatter.locale = Locale.init(identifier:"en_US.UTF-8")  // for demo only 
//formatter.locale = Locale.current  // Normal case is user's preferred locale
formatter.numberStyle = .spellOut
if let fmtAmount = formatter.string(from: curAmount as NSNumber) {
    print ("Amount: \(fmtAmount)")
}

对于数字的整数部分,它工作正常,但不幸的是,在我尝试过的所有六种语言中,小数部分都被处理为一系列独立的数字。例如:

ten thousand ninety-seven point four three

但是我需要小数部分也用数字表示,比如

ten thousand ninety-seven point forty three
ten thousand ninety-seven dollars forty three

我当然可以获取字符串,删除 "point" 之后的所有内容,将小数部分乘以百,生成第二个数字字符串并将两个字符串连接起来。但这不适用于使用外语的语言环境,也不适用于点后零或三位数字的货币。

有没有办法将小数部分作为纯文本整数正确处理,开箱即用,也许使用格式化程序的一些微妙参数?有没有一种方法可以根据区域设置的语言在正确的位置也包括货币名称(即用数字样式拼写 .currencyPlural)?

其中一种可能的方式是这样的:

let formatter = NumberFormatter()
formatter.locale = Locale.init(identifier:"en_US.UTF-8")
formatter.numberStyle = .spellOut

let curAmount = 10097.43

let curAmountSplitted = String(curAmount).split(separator: ".") // Splits the number into [String] array of integer and fractional parts of the number
let curAmountStrings = curAmountSplitted.compactMap{formatter.string(from: Int([=10=]) as! NSNumber)} // creates a [String] array by applying a closure to every element of curAmountSplitted array

print(curAmountStrings.joined(separator: " point ")) // joins curAmountStrings with a separator
// prints ten thousand ninety-seven point forty-three

初步评论: 这里的主要问题是,我想要一个依赖于 build-in iOS/macOS 功能的国际化解决方案。因此,我不能使用任何基于给定小数点分隔符的解决方案,因为它会根据用户的语言环境而改变,也不能使用 hard-coded 字符串。这就是为什么我只能为Roman的代码非常优雅的解决方案点赞,而不是接受它,因为它不完全符合要求。

let value = 12345.678      // value to spell
currency: String? = nil   // ISO currency if you want one specific

1.Initialize 格式化程序:

let formatter = NumberFormatter()  // formatter with user's default locale

如果我想使用不同的语言或不同的国家格式,我必须明确覆盖语言环境:

formatter.locale = Locale.init(identifier: "en_US.UTF-8")  // for demo only 

如果我使用区域设置国家/地区的货币,这很好用。但是如果我使用另一种货币,格式化程序的行为对于某些格式化样式可能是错误的。所以,诀窍是使用 currency-specific locale:

if currency != nil {
    let newLocale = "\(formatter.locale.identifier)@currency=\(currency!)"
    formatter.locale = Locale(identifier:newLocale)
}

有趣的 Foundation 功能是当我更改货币时,货币特定区域设置,即货币代码、货币符号和位数会自动更改:

formatter.numberStyle = .currency
let decimals = max(formatter.minimumFractionDigits, formatter.maximumFractionDigits)

2.Get 正确语法形式的完整货币名称

现在我有了decimals,但我想要货币的全称spelledCurrency。问题是某些语言将其设为中性。对于某些语言,您必须为 1 个单位写单数,但从 2 开始写复数。对于某些语言,小数点在选择单数和复数时很重要。幸运的是,NumberFormatter 允许使用 .currencyPlural 样式来解决这个问题。这种奇怪的风格以数字形式书写数字,但根据语言特定规则书写普通货币。这很神奇:

问题是我只想要货币名称:没有 space,没有分隔符,没有数字。我希望它也适用于日语(语言环境 "ja_JP.UTF-8")。幸运的是,我们可以很容易地过滤字符串:

formatter.numberStyle = .currencyPlural
var spelledCurrency : String
if let plural = formatter.string(from: value as NSNumber) {
    // Keep only the currency spelling: trim spaces, punctuation and digits
    // ASSUMPTION: decimal and thousand separators belong to punctuation
    var filter = CharacterSet()
    filter.formUnion(.punctuationCharacters)
    filter.formUnion(.decimalDigits)
    filter.formUnion(.whitespaces)
    spelledCurrency = plural.trimmingCharacters(in: filter)
 }
 else {
    // Fall back - worst case: the ISO code XXX for no currency is used
    spelledCurrency = formatter.currencyCode ?? (formatter.internationalCurrencySymbol ?? "XXX")
 }

3.Separate 数值分量

我用了一个 value 定义为 Double。但是你可以很容易地适应 Decimal 类型,这对货币来说更稳健。诀窍是让 wholePart 以货币单位表示,wholeDecimal,即以与货币子单位数相对应的整数表示的部分(这是已知的,这要归功于货币的小数位数):

let decimalPart = value.truncatingRemainder(dividingBy: 1)
let wholePart = value - decimalPart
let wholeDecimals = floor(decimalPart * pow(10.0, Double(decimals)))

4。拼出组件

这里我们使用原生的 .spellOut 功能来拼写数字。如果你尝试学习外语,那就太棒了;-)

formatter.numberStyle = .spellOut
let wholeString = formatter.string(from: wholePart as NSNumber) ?? "???"
let decimalString = formatter.string(from: wholeDecimals as NSNumber) ?? "???"

5。总装

我们现在必须 assemble 组件。但不是说二十美元点五点七分,我们应该正式说二十美元五十七美分。我决定将 "and" 替换为货币的本地化名称,然后是 sub-units 的数字,而不表达子单位本身。它适用于我知道的几种语言:

// ASSUMPTION: currency is always between whole and fractional
// TO BE VERIFIED: Ok for Right to left writing?
var spelled : String
if (decimals>0 && wholeDecimals != 0) {
    spelled = "\(wholeString) \(spelledCurrency) \(decimalString)"
} else {
    spelled = "\(wholeString) \(spelledCurrency)"
}

只需要打印(或return)最终值:

print (">>>>> \(spelled)")

6。测试

这里是一些语言的测试结果:

en, USD: twelve thousand three hundred forty-five US dollars sixty-seven
fr, EUR: douze mille trois cent quarante-cinq euros soixante-sept
de, EUR: zwölf­tausend­drei­hundert­fünf­und­vierzig Euro sieben­und­sechzig
nl, EUR: twaalf­duizend­drie­honderd­vijf­en­veertig euro zeven­en­zestig
it, EUR: dodici­mila­tre­cento­quaranta­cinque euro sessanta­sette
es, EUR: doce mil trescientos cuarenta y cinco euros sesenta y siete
ja, JPY: 一万二千三百四十五 円
fr, TND: douze mille trois cent quarante-cinq dinars tunisiens six cent soixante-dix-sept

上次测试显示 Double 有舍入误差,所以最好使用 Decimal.

我还使用了 1 到 2 欧元之间的金额,有和没有子单位,以及整数。