Parte frazionaria di nsdecimalnumber
-
27-10-2019 - |
Domanda
Sto usando NSDecimalNumber per archiviare un valore per la valuta. Sto cercando di scrivere un metodo chiamato "Cents" che restituisce la parte decimale del numero come NSString con uno 0 se il numero è <10. Quindi in pratica
NSDecimalNumber *n = [[NSDecimalNumber alloc] initWithString:@"1234.55"];
NSString *s = [object cents:n];
E sto cercando di elaborare un metodo che restituirà 01, 02, ecc ... fino a 99 come stringa. Non riesco a capire come restituire la mantissa come stringa in questo formato. Mi sento come se mi mancasse un metodo di convenienza, ma ho guardato di nuovo la documentazione e non vedo nulla che mi salti fuori.
Soluzione
Aggiornare: Questa è una risposta relativamente vecchia, ma sembra che la gente lo stia ancora trovando. Voglio aggiornarlo per la correttezza - Inizialmente ho risposto alla domanda come ho fatto semplicemente per dimostrare come si potevano estrarre posti decimali specifici da un double
, ma non lo sostengo come un modo per rappresentare le informazioni valutarie.
Mai Utilizzare numeri a punto mobile per rappresentare le informazioni in valuta. Non appena inizi a gestire i numeri decimali (come faresti con dollari con centesimi), si introducono possibili errori di punta mobile nel tuo codice. Un computer non può rappresentare con precisione tutti i valori decimali (1/100.0
, ad esempio, 1 centesimi, è rappresentato come 0.01000000000000000020816681711721685132943093776702880859375
sulla mia macchina). A seconda delle valute che prevedi di rappresentare, è sempre più corretto archiviare una quantità in termini di importo di base (in questo caso, centesimi).
Se memorizzi i tuoi valori in dollari in termini di centesimi integrali, non ti imbatterai mai in errori a punta mobile per la maggior parte delle operazioni ed è banalmente facile convertire i centesimi in dollari per la formattazione. Se è necessario applicare le tasse, ad esempio, o moltiplicare il valore dei centesimi per un double
, fallo per ottenere un double
valore, applicare Arrotondamento del banchiere Per arrotondare al centesimo più vicino e convertire di nuovo in un numero intero.
Diventa più complicato di quello se stai cercando di supportare più valute diverse, ma ci sono modi per affrontarlo.
tl; dr Non usare numeri a punto mobile per rappresentare la valuta e sarai molto più felice e più corretto. NSDecimalNumber
è in grado di rappresentare con precisione (e precisamente) i valori decimali, ma non appena ti converti a double
/float
, corri il rischio di introdurre errori a virgola mobile.
Questo può essere fatto relativamente facilmente:
- Ottenere il
double
Valore del numero decimale (questo funzionerà supponendo che il numero non sia troppo grande per essere archiviato in un doppio). - In una variabile separata, lancia il
double
a un numero intero. - Moltiplica entrambi i numeri per 100 per tenere conto della perdita di precisione (essenzialmente, convertire in centesimi) e sottrarre dollari dall'originale per ottenere il numero di centesimi.
- Restituire in una stringa.
(Questa è una formula generale per lavorare con tutti i numeri decimali: lavorare con NSDecimalNumber
Richiede solo un po 'di codice colla per farlo funzionare).
In pratica, sarebbe così (in un NSDecimalNumber
categoria):
- (NSString *)cents {
double value = [self doubleValue];
unsigned dollars = (unsigned)value;
unsigned cents = (value * 100) - (dollars * 100);
return [NSString stringWithFormat:@"%02u", cents];
}
Altri suggerimenti
Questa è un'implementazione più sicura che funzionerà con valori che non si adattano a a 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
Questo stampe 01
:
NSDecimalNumber* dn = [[NSDecimalNumber alloc] initWithString:@"123456789012345678901234567890.0108"]; NSLog(@"%@", [dn centsString]);
Non sono d'accordo con il metodo di Itai Ferber, perché usando doubleValue
metodo di NSNumber
Può causare perdita di precisione come 1.60 >> 1.599999999, quindi il valore dei centesimi sarà "59". Invece ho tagliato "dollari"/"centesimi" dalla corda, ottenuto con 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];
}
Versione rapida, con funzione bonus per ottenere anche l'importo in dollari.
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"
}
}
Test:
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")
}
}
Ho un approccio diverso. Funziona per me ma non sono sicuro che potrebbe causare problemi? Lascio che nsdecimalnumber restituisca il valore direttamente restituendo un valore intero ...
per esempio:
// n is the NSDecimalNumber from above
NSInteger value = [n integerValue];
NSInteger cents = ([n doubleValue] *100) - (value *100);
Questo approccio sarebbe più costoso dal momento che sto convertendo due volte NSDecimalNumber?