Pergunta

Estou usando NSDecimalNumber para armazenar um valor para a moeda.Estou tentando escrever um método chamado "centavos" que retorna a parte decimal do número como um NSString com um 0 inicial se o número for <10. Então, basicamente

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

NSString *s = [object cents:n];

E estou tentando criar um método que retornará 01, 02, etc ... até 99 como uma string.Não consigo descobrir como retornar a mantissa como uma string neste formato.Sinto que estou perdendo um método de conveniência, mas olhei para a documentação novamente e não vejo nada me surpreendendo.

Foi útil?

Solução

Atualizar: esta é uma resposta relativamente antiga, mas parece que as pessoas ainda a estão encontrando. Desejo atualizar isso para correção - originalmente respondi à pergunta como fiz simplesmente para demonstrar como alguém poderia extrair casas decimais específicas de um double, mas não defendo isso como uma forma de representar informações de moeda.

Nunca use números de ponto flutuante para representar informações de moeda. Assim que começar a lidar com números decimais (como faria com dólares com centavos), você introduzirá possíveis erros de ponto flutuante em seu código. Um computador não pode representar todos os valores decimais com precisão (1/100.0, por exemplo, 1 cent, é representado como 0.01000000000000000020816681711721685132943093776702880859375 em minha máquina). Dependendo das moedas que você planeja representar, é sempre mais correto armazenar uma quantidade em termos de seu valor base (neste caso, centavos).

Se você armazenar seus valores em dólares em termos de centavos integrais, nunca encontrará erros de ponto flutuante para a maioria das operações e é trivialmente fácil converter centavos em dólares para formatação. Se você precisar aplicar impostos, por exemplo, ou multiplicar o valor dos centavos por um double, faça isso para obter um valor double, aplique arredondamento do banqueiro para arredondar para o centavo mais próximo e converter novamente para um número inteiro.

Fica mais complicado do que se você está tentando oferecer suporte a várias moedas diferentes, mas existem maneiras de lidar com isso.

tl; dr Não use números de ponto flutuante para representar a moeda e você ficará muito mais feliz e mais correto. NSDecimalNumber é capaz de representar com precisão (e precisamente) valores decimais, mas assim que você converter para double / float, você corre o risco de introduzir erros de ponto flutuante.


Isso pode ser feito com relativa facilidade:

  1. Obtenha o valor double do número decimal (isso funcionará assumindo que o número não seja muito grande para armazenar em dobro).
  2. Em uma variável separada, converta o double em um inteiro.
  3. Multiplique os dois números por 100 para contabilizar a perda de precisão (essencialmente, converta em centavos) e subtraia os dólares do original para obter o número de centavos.
  4. Retorno em uma string.

(Esta é uma fórmula geral para trabalhar com todos os números decimais - trabalhar com NSDecimalNumber requer apenas um pouco de cola para que funcione).

Na prática, seria assim (em uma categoria NSDecimalNumber):

- (NSString *)cents {
    double value = [self doubleValue];
    unsigned dollars = (unsigned)value;
    unsigned cents = (value * 100) - (dollars * 100);

    return [NSString stringWithFormat:@"%02u", cents];
}

Outras dicas

Esta é uma implementação mais segura que funcionará com valores que não se encaixam em um 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

Isso imprime 01:

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

Não concordo com o método de Itai Ferber, porque o uso do método doubleValue de NSNumber pode causar perda de precisão como 1,60 >> 1,5999999999, então seu valor de centavos será "59".Em vez disso, cortei "dólares" / "centavos" da string, obtida com 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];
}

Versão rápida, com função de bônus para obter apenas o valor em dólares também.

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

Testes:

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

Tenho uma abordagem diferente.Funciona para mim, mas não tenho certeza se pode causar problemas. Eu deixo NSDecimalNumber retornar o valor diretamente, retornando um valor inteiro ...

e.G .:

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

Essa abordagem seria mais cara, já que estou convertendo NSDecimalNumber duas vezes?

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top