【问题标题】:Fractional Part of NSDecimalNumberNSDecimalNumber 的小数部分
【发布时间】:2011-07-18 08:45:58
【问题描述】:

我正在使用 NSDecimalNumber 来存储货币值。我正在尝试编写一个名为“cents”的方法,如果数字

NSDecimalNumber *n = [[NSDecimalNumber alloc] initWithString:@"1234.55"];

NSString *s = [object cents:n];

我正在尝试设计一种方法,该方法将返回 01、02 等...最多 99 作为字符串。我似乎无法弄清楚如何以这种格式将尾数作为字符串返回。我觉得我错过了一种方便的方法,但我再次查看了文档,并没有看到任何突然出现在我身上的东西。

【问题讨论】:

    标签: objective-c xcode nsdecimalnumber mantissa


    【解决方案1】:

    更新:这是一个相对较旧的答案,但看起来人们仍在寻找它。为了正确起见,我想更新它——我最初回答这个问题只是为了演示如何从double 中提取特定的小数位,但我不提倡将其作为表示货币信息的一种方式。

    切勿使用浮点数来表示货币信息。一旦你开始处理十进制数(就像你处理美元和美分一样),你就会在代码中引入可能的浮点错误。计算机无法准确表示所有十进制值(1/100.0,例如 1 美分,在我的机器上表示为 0.01000000000000000020816681711721685132943093776702880859375)。根据您计划表示的货币,以基本金额(在本例中为美分)来存储数量总是更正确的。

    如果您以整数美分的形式存储美元值,则大多数操作都不会遇到浮点错误,而且很容易将美分转换为美元进行格式化。例如,如果您需要征税,或者将您的美分值乘以double,则执行此操作以获得double 值,应用banker's rounding 舍入到最接近的美分,然后转换回整数。

    如果您尝试支持多种不同的货币,情况会变得更加复杂,但有一些方法可以解决这个问题。

    tl;dr 不要使用浮点数来表示货币,你会更快乐,更正确。 NSDecimalNumber 能够准确(且精确地)表示十进制值,但一旦转换为 double/float,就会面临引入浮点错误的风险。


    这可以相对容易地完成:

    1. 获取十进制数的double 值(假设该数字不会太大而无法存储在双精度数中)。
    2. 在单独的变量中,将 double 强制转换为整数。
    3. 将这两个数字乘以 100 以说明精度损失(本质上是转换为美分)并从原始数字中减去美元以获得美分的数量。
    4. 以字符串形式返回。

    (这是处理所有十进制数的通用公式 - 使用 NSDecimalNumber 只需要一点胶水代码即可使其工作)。

    实际上,它看起来像这样(在NSDecimalNumber 类别中):

    - (NSString *)cents {
        double value = [self doubleValue];
        unsigned dollars = (unsigned)value;
        unsigned cents = (value * 100) - (dollars * 100);
    
        return [NSString stringWithFormat:@"%02u", cents];
    }
    

    【讨论】:

    • 聪明。虽然我很惊讶 Cocoa 没有提供更好的解决方案。
    • 我同意斯蒂芬的观点。谢谢,我以为我缺少某种可以包装此逻辑的功能。效果很好,尽管有箍。
    • 使用浮点数/双精度货币很可能永远不是一个好主意
    • @fabb 这是我的一个很老的答案,但你是对的。我已经更新了它以供参考。
    • 感谢您的关心 - 您的 tl;dr 是错误的。 NSDecimalNumber 不将值存储为浮点数。使用NSDecimalNumber,并且从不将其转换为float/double,可以为您提供精确的小数位。 NSDecimalNumber 上也有精确算术的方法。
    【解决方案2】:

    这是一个更安全的实现,它适用于不适合 double 的值:

    @implementation NSDecimalNumber (centsStringAddition) - (NSString*) 美分字符串; { NSDecimal value = [self decimalValue]; NSDecimal 美元; NSDecimalRound(&dollars, &value, 0, NSRoundPlain); NSDecimal 美分; NSDecimalSubtract(&ampcents, &value, &dollars, NSRoundPlain); 双 dv = [[[NSDecimalNumber decimalNumberWithDecimal:cents] decimalNumberByMultiplyingByPowerOf10:2] doubleValue]; return [NSString stringWithFormat:@"%02d", (int)dv]; } @结尾

    这打印01:

    NSDecimalNumber* dn = [[NSDecimalNumber alloc] initWithString:@"123456789012345678901234567890.0108"]; NSLog(@"%@", [dn centsString]);

    【讨论】:

      【解决方案3】:

      我不同意Itai Ferber 的方法,因为使用NSNumberdoubleValue 方法会导致精度损失,例如1.60 >> 1.5999999999,所以你的分值将是“59”。相反,我从使用NSNumberFormatter 获得的字符串中删除了“dollars”/“cents”:

      +(NSString*)getSeparatedTextForAmount:(NSNumber*)amount centsPart:(BOOL)cents
      {
          static NSNumberFormatter* fmt2f = nil;
          if(!fmt2f){
              fmt2f = [[NSNumberFormatter alloc] init];
              [fmt2f setNumberStyle:NSNumberFormatterDecimalStyle];
              [fmt2f setMinimumFractionDigits:2]; // |
              [fmt2f setMaximumFractionDigits:2]; // | - exactly two digits for "money" value
          }
          NSString str2f = [fmt2f stringFromNumber:amount];
          NSRange r = [str2f rangeOfString:fmt2f.decimalSeparator];
          return cents ? [str2f substringFromIndex:r.location + 1] : [str2f substringToIndex:r.location];
      }
      

      【讨论】:

      • 这是最好的答案,没有转换为double 或任何其他不正确的货币类型并使用NSNumberFormatter
      【解决方案4】:

      Swift 版本,也有获取美元金额的奖励功能。

      extension NSDecimalNumber {
      
          /// Returns the dollars only, suitable for printing. Chops the cents.
          func dollarsOnlyString() -> String {
              let behaviour = NSDecimalNumberHandler(roundingMode:.RoundDown,
                                                     scale: 0, raiseOnExactness: false,
                                                     raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)
      
              let rounded = decimalNumberByRoundingAccordingToBehavior(behaviour)
              let formatter = NSNumberFormatter()
              formatter.numberStyle = .NoStyle
              let str = formatter.stringFromNumber(rounded)
              return str ?? "0"
      
          }
      
          /// Returns the cents only, e.g. "00" for no cents.
          func centsOnlyString() -> String {
              let behaviour = NSDecimalNumberHandler(roundingMode:.RoundDown,
                                                     scale: 0, raiseOnExactness: false,
                                                     raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)
      
              let rounded = decimalNumberByRoundingAccordingToBehavior(behaviour)
              let centsOnly = decimalNumberBySubtracting(rounded)
              let centsAsWholeNumber = centsOnly.decimalNumberByMultiplyingByPowerOf10(2)
              let formatter = NSNumberFormatter()
              formatter.numberStyle = .NoStyle
              formatter.formatWidth = 2
              formatter.paddingCharacter = "0"
              let str = formatter.stringFromNumber(centsAsWholeNumber)
              return str ?? "00"
          }
      }
      

      测试:

      class NSDecimalNumberTests: XCTestCase {
      
          func testDollarsBreakdown() {
              var amount = NSDecimalNumber(mantissa: 12345, exponent: -2, isNegative: false)
              XCTAssertEqual(amount.dollarsOnlyString(), "123")
              XCTAssertEqual(amount.centsOnlyString(), "45")
      
              // Check doesn't round dollars up.
              amount = NSDecimalNumber(mantissa: 12365, exponent: -2, isNegative: false)
              XCTAssertEqual(amount.dollarsOnlyString(), "123")
              XCTAssertEqual(amount.centsOnlyString(), "65")
      
              // Check zeros
              amount = NSDecimalNumber(mantissa: 0, exponent: 0, isNegative: false)
              XCTAssertEqual(amount.dollarsOnlyString(), "0")
              XCTAssertEqual(amount.centsOnlyString(), "00")
      
              // Check padding
              amount = NSDecimalNumber(mantissa: 102, exponent: -2, isNegative: false)
              XCTAssertEqual(amount.dollarsOnlyString(), "1")
              XCTAssertEqual(amount.centsOnlyString(), "02")
          }
      }
      

      【讨论】:

      • 谢谢你
      • 我相信当数字为负数时这不起作用,因为 RoundDown 根据 developer.apple.com/reference/foundation/…> 从零四舍五入。并且基于负值在向上或向下舍入模式之间切换很简单,除非测试负值并不简单,最容易做到doubleValue >= 0。然而,Swift 3 中的 Decimal 现在公开了一个 sign 属性(进展!)。
      【解决方案5】:

      我有不同的方法。它对我有用,但我不确定它是否会导致任何问题? 我让 NSDecimalNumber 通过返回一个整数值直接返回值...

      例如:

      // n is the NSDecimalNumber from above 
      NSInteger value = [n integerValue];
      NSInteger cents = ([n doubleValue] *100) - (value *100);
      

      由于我将 NSDecimalNumber 转换两次,这种方法会更昂贵吗?

      【讨论】:

        猜你喜欢
        • 2016-04-20
        • 1970-01-01
        • 2017-05-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多