Дробная часть NSDecimalNumber - PullRequest
7 голосов
/ 18 марта 2011

Я использую NSDecimalNumber для хранения значения для валюты.Я пытаюсь написать метод с именем "центов", который возвращает десятичную часть числа в виде строки NSS с лидирующим 0, если число <10. Итак, в основном </p>

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

NSString *s = [object cents:n];

создать метод, который вернет 01, 02 и т. д. до 99 в виде строки.Я не могу понять, как вернуть мантиссу в виде строки в этом формате.Я чувствую, что мне не хватает удобного метода, но я снова посмотрел документацию и не вижу ничего, что выпрыгнуло на меня.

Ответы [ 5 ]

11 голосов
/ 12 августа 2011

Это более безопасная реализация, которая будет работать со значениями, которые не вписываются в double:

@implementation NSDecimalNumber (centsStringAddition)

- (NSString*) centsString;
{
    NSDecimal value = [self decimalValue];

    NSDecimal dollars;
    NSDecimalRound(&dollars, &value, 0, NSRoundPlain);

    NSDecimal cents;
    NSDecimalSubtract(&ampcents, &value, &dollars, NSRoundPlain);

    double dv = [[[NSDecimalNumber decimalNumberWithDecimal:cents] decimalNumberByMultiplyingByPowerOf10:2] doubleValue];
    return [NSString stringWithFormat:@"%02d", (int)dv];
}

@end

Это печатает 01:

    NSDecimalNumber* dn = [[NSDecimalNumber alloc] initWithString:@"123456789012345678901234567890.0108"];
    NSLog(@"%@", [dn centsString]);
11 голосов
/ 18 марта 2011

Обновление: Это довольно старый ответ, но похоже, что люди все еще находят его.Я хочу обновить это для корректности - первоначально я ответил на вопрос, поскольку я просто продемонстрировал, как можно извлечь конкретные десятичные знаки из double, но я не защищаю это как способ представления информации о валюте.

Никогда использовать числа с плавающей запятой для представления информации о валюте.Как только вы начинаете работать с десятичными числами (как если бы вы использовали доллары с центами), вы вводите в код возможные ошибки с плавающей запятой.Компьютер не может точно представить все десятичные значения (1/100.0, например, 1 цент, представлен на моем компьютере как 0.01000000000000000020816681711721685132943093776702880859375).В зависимости от того, какую валюту вы планируете представлять, всегда правильнее хранить количество в пересчете на его базовую сумму (в данном случае в центах).

Если вы храните свои долларовые значения в виде целых центов,Вы никогда не столкнетесь с ошибками с плавающей запятой для большинства операций, и банально легко конвертировать центы в доллары для форматирования.Если вам нужно, например, применить налог или умножить значение ваших центов на double, сделайте это, чтобы получить значение double, примените округление банкира для округления до ближайшего цента и обратного преобразованияв целое число.

Это становится сложнее, чем если вы пытаетесь поддерживать несколько разных валют, но есть способы справиться с этим.

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];
}
2 голосов
/ 08 марта 2016

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")
    }
}
2 голосов
/ 02 марта 2013

Я не согласен с методом Итая Фербера, потому что использование метода doubleValue для NSNumber может привести к потере точности, например 1.60 >> 1.5999999999, поэтому значение ваших центов будет равно "59".Вместо этого я вырезал «доллары» / «центы» из строки, полученной с помощью NSNumberFormatter:

+(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];
}
1 голос
/ 12 августа 2011

У меня другой подход. Это работает для меня, но я не уверен, может ли это вызвать какие-либо проблемы? Я позволил NSDecimalNumber возвращать значение напрямую, возвращая целочисленное значение ...

например:.

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

Будет ли такой подход дороже, так как я конвертирую NSDecimalNumber два раза?

...