代码之家  ›  专栏  ›  技术社区  ›  Christophe

用Swift中的数字格式写出货币金额的小数部分

  •  1
  • Christophe  · 技术社区  · 4 年前

    我需要将一个货币金额转换成一个纯文本(即支票、合同和其他法律文件)表示值的字符串。 这可以用 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" ,将小数部分乘以100,生成第二个数字字符串并将两个字符串串联起来。但这对于使用外语的语言环境无效,也不适用于小数点后有0或3位数的货币。

    有没有一种方法可以让小数部分正确地处理为一个纯文本整数,开箱即用,也许使用格式化程序的一些微妙的参数?有没有办法也包括货币的名称(例如,它是用数字形式拼写出来的 .currencyPlural )在合适的地方取决于语言环境?

    0 回复  |  直到 4 年前
        1
  •  1
  •   Roman Ryzhiy    4 年前

    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($0) 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
    
        2
  •  1
  •   Christophe    4 年前

    初步意见: 这里的主要问题是,我需要一个国际化的解决方案,它依赖于内置的iOS/macOS功能。因此,我不能使用任何基于给定的十进制分隔符的解决方案,因为它会根据用户的区域设置而改变,也不能使用硬编码字符串。这就是为什么我只能对Roman的解决方案投赞成票,因为它不能完全满足需求,而不能接受它。

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

    1.初始化格式化程序:

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

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

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

    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.以正确的语法形式获取货币全名

    decimals ,但我想要全名 spelledCurrency 货币。问题是有些语言是中性的。对于某些语言,你必须写出1个单位的单数,但从2开始是复数。对于某些语言,小数在单数和复数之间的选择很重要。幸运的是, NumberFormatter .currencyPlural . 这种奇怪的样式用数字来写数字,但是根据特定语言的规则来写普通货币。它的魔力:

    问题是我只想要货币的名称:没有空格,没有分隔符,没有数字。我希望它适用于日语(语言环境 "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")
     }
    

    我用了 value 定义为 Double . 但你很容易适应 Decimal 类型,whcih对货币更为稳健。诀窍是 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总装

    我们现在必须组装部件。但我们不应该说20美元5.7美元,而是应该正式地说20美元57美分。我决定更换 "and"

    // 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)"
    }
    

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

    以下是一些语言的测试结果:

    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
    

    最后一个测试显示的舍入错误 双倍 ,所以最好使用 十进制的 .