Вопрос

Я использую NSDecimalNumber для хранения значения для валюты. Я пытаюсь написать метод, называемый «центы», который возвращает десятичную часть числа в качестве NSString с ведущим 0, если число <10. Так что в основном

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

NSString *s = [object cents:n];

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

Это было полезно?

Решение

Обновлять: Это относительно старый ответ, но похоже, что люди все еще находят его. Я хочу обновить это для правильности - я изначально ответил на вопрос, как и просто, чтобы продемонстрировать, как можно было бы вытащить определенные десятичные десятичные места из 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];
}

Другие советы

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

@implementation NSDecimalNumber (centsStringAddition)

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

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

    NSDecimal cents;
    NSDecimalSubtract(&cents, &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]);

Я не согласен с методом Итаи Фербера, потому что использование 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];
}

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

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

например:

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

Будет ли этот подход более дорогим, так как я дважды конвертирую NSDecimalNumber?

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top