Pregunta

Estoy usando nsdecimalnumber para almacenar un valor para la moneda. Estoy tratando de escribir un método llamado "centavos" que devuelva la parte decimal del número como un nsstring con un 0 líder si el número es <10. Entonces, básicamente

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

NSString *s = [object cents:n];

Y estoy tratando de elaborar un método que devuelva 01, 02, etc. hasta 99 como una cadena. Parece que no puedo descubrir cómo devolver la Mantissa como una cadena en este formato. Siento que me estoy perdiendo un método de conveniencia, pero volví a mirar la documentación y no veo nada que me salte.

¿Fue útil?

Solución

Actualizar: Esta es una respuesta relativamente antigua, pero parece que la gente todavía la está encontrando. Quiero actualizar esto para la corrección: originalmente respondí la pregunta como lo hice simplemente para demostrar cómo se podía sacar lugares decimales específicos de un double, pero no abogo por esto como una forma de representar la información monetaria.

Nunca Use números de punto flotante para representar información de divisas. Tan pronto como comience a tratar con números decimales (como lo haría con dólares con centavos), introduce posibles errores de punto flotante en su código. Una computadora no puede representar todos los valores decimales con precisión (1/100.0, por ejemplo, 1 centavo, se representa como 0.01000000000000000020816681711721685132943093776702880859375 en mi máquina). Dependiendo de las monedas que planea representar, siempre es más correcto almacenar una cantidad en términos de su cantidad base (en este caso, centavos).

Si almacena sus valores en dólares en términos de centavos integrales, nunca se encontrará con errores de punto flotante para la mayoría de las operaciones, y es trivialmente fácil convertir centavos en dólares para formatear. Si necesita aplicar impuestos, por ejemplo, o multiplicar su valor de centavos por un double, haz eso para conseguir un double valor, aplicar redondeo del banquero para redondear al centavo más cercano y volver a un entero.

Se vuelve más complicado que si está tratando de apoyar múltiples monedas diferentes, pero hay formas de lidiar con eso.

tl; Dr No use números de punto flotante para representar la moneda y será mucho más feliz y más correcto. NSDecimalNumber es capaz de representar con precisión (y precisamente) los valores decimales, pero tan pronto como se convierte en double/float, corre el riesgo de introducir errores de punto flotante.


Esto se puede hacer relativamente fácilmente:

  1. Consigue el double Valor del número decimal (esto funcionará suponiendo que el número no sea demasiado grande para almacenar en un doble).
  2. En una variable separada, lanza el double a un entero.
  3. Multiplique ambos números por 100 para tener en cuenta la pérdida de precisión (esencialmente, convierta a centavos) y reste los dólares del original para obtener el número de centavos.
  4. Regresar en una cadena.

(Esta es una fórmula general para trabajar con todos los números decimales: trabajar con NSDecimalNumber Solo requiere un poco de código de pegamento para que funcione).

En la práctica, se vería así (en un NSDecimalNumber categoría):

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

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

Otros consejos

Esta es una implementación más segura que funcionará con valores que no encajan en un 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

Esto imprime 01:

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

No estoy de acuerdo con el método de Itai Ferber, porque usando de doubleValue método de NSNumber puede causar precisión como 1.60 >> 1.5999999999, por lo que su valor de centavos será "59". En su lugar, corté "dólares"/"centavos" de la cadena, obtenida 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];
}

Versión rápida, con función de bonificación para obtener solo la cantidad en dólares también.

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

Pruebas:

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

Tengo un enfoque diferente. Funciona para mí pero no estoy seguro de si puede causar algún problema. Dejo que NSDecimalNumber devuelva el valor directamente devolviendo un valor entero ...

p.ej:

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

¿Sería este enfoque más costoso ya que estoy convirtiendo nsdecimalnumber dos veces?

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top