Дробная часть NSDecimalNumber
-
27-10-2019 - |
Вопрос
Я использую 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
, Вы рискуете ввести ошибки с плавающей точкой.
Это можно сделать относительно легко:
- Получить
double
Стоимость десятичного числа (это будет работать, предполагая, что число не слишком большое, чтобы хранить в двойном). - В отдельной переменной бросьте
double
на целое число. - Умножьте оба числа на 100, чтобы учесть потерю точности (по существу, преобразовать в центы) и вычитайте доллары из оригинала, чтобы получить количество центов.
- Вернуть в строку.
(Это общая формула для работы со всеми десятичными цифрами - работа с 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(¢s, &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?