web-dev-qa-db-fra.com

Manière correcte d'instancier un NSDecimalNumber à partir de float ou double

Je cherche le meilleur moyen d'instancier un NSDecimalNumber à partir d'un double ou d'un court. Il existe les méthodes de classe et d'instance NSNumber suivantes ...

+NSNumber numberWithFloat
+NSNumber numberWithDouble
-NSNumber initWithFloat
-NSNumber initWithDouble

mais ceux-ci semblent renvoyer NSNumber. De l'autre côté, NSDecimalNumber définit ce qui suit:

+NSDecimalNumber decimalNumberWithMantissa:exponent:isNegative:
+NSDecimalNumber decimalNumberWithDecimal:

Il y a quelques possibilités ici sur la voie à suivre. Xcode génère un avertissement si NSDecimalNumber est défini sur la valeur de retour des méthodes pratiques NSNumber ci-dessus.

J'apprécierais votre avis sur la manière la plus propre et la plus correcte de procéder ...

37
Paul

Vous étiez sur la bonne voie (avec des mises en garde, voir ci-dessous). Vous devriez pouvoir simplement utiliser les initialiseurs de NSNumber car ils sont hérités par NSDecimalNumber.

NSDecimalNumber *floatDecimal = [[[NSDecimalNumber alloc] initWithFloat:42.13f] autorelease];
NSDecimalNumber *doubleDecimal = [[[NSDecimalNumber alloc] initWithDouble:53.1234] autorelease];
NSDecimalNumber *intDecimal = [[[NSDecimalNumber alloc] initWithInt:53] autorelease];

NSLog(@"floatDecimal floatValue=%6.3f", [floatDecimal floatValue]);
NSLog(@"doubleDecimal doubleValue=%6.3f", [doubleDecimal doubleValue]); 
NSLog(@"intDecimal intValue=%d", [intDecimal intValue]);

Plus d’informations sur ce sujet peuvent être trouvées ici .

Cependant, j'ai beaucoup discuté du problème du débordement de pile et du Web à propos de l'initialisation de NSDecimalNumber. Il semble y avoir quelques problèmes liés à la précision et à la conversion vers/depuis les doublons avec NSDecimalNumber. Surtout lorsque vous utilisez les membres d'initialisation hérités de NSNumber.

J'ai bousillé le test ci-dessous:

double dbl = 36.76662445068359375000;
id xx1 = [NSDecimalNumber numberWithDouble: dbl]; // Don't do this
id xx2 = [[[NSDecimalNumber alloc] initWithDouble: dbl] autorelease];
id xx3 = [NSDecimalNumber decimalNumberWithString:@"36.76662445068359375000"];
id xx4 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L exponent:-17 isNegative:NO];

NSLog(@"raw doubleValue: %.20f,              %.17f", dbl, dbl);

NSLog(@"xx1 doubleValue: %.20f, description: %@", [xx1 doubleValue], xx1);   
NSLog(@"xx2 doubleValue: %.20f, description: %@", [xx2 doubleValue], xx2);   
NSLog(@"xx3 doubleValue: %.20f, description: %@", [xx3 doubleValue], xx3);   
NSLog(@"xx4 doubleValue: %.20f, description: %@", [xx4 doubleValue], xx4);   

La sortie est:

raw doubleValue: 36.76662445068359375000               36.76662445068359375
xx1 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375
xx4 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

Ainsi, vous pouvez voir que lorsque vous utilisez la méthode pratique numberWithDouble sur NSNumber (que vous ne devriez pas vraiment utiliser car elle renvoie le type de pointeur incorrect) et que même l'initialiseur initWithDouble (cette IMO "devrait" pouvoir appeler) vous obtenez un numéro NSDecimalNumber. avec une représentation interne (renvoyée en appelant description sur l'objet) qui n'est pas aussi précise que celle que vous obtenez lorsque vous appelez decimalNumberWithString: ou decimalNumberWithMantissa:exponent:isNegative:.

Notez également que reconvertir en un double en appelant doubleValue sur l'instance NSDecimalNumber perd de la précision, mais est, chose intéressante, identique, quel que soit l'initialisateur que vous appelez.

En fin de compte, je pense qu'il est recommandé d'utiliser l'une des méthodes decimalNumberWith* déclarées au niveau de classe NSDecimalNumber pour créer vos instances NSDecimalNumber lorsque vous utilisez des nombres à virgule flottante de haute précision.

Alors, comment appelez-vous ces initialiseurs facilement lorsque vous avez un double, un flottant ou un autre NSNumber? 

Deux méthodes décrites ici "travail" mais ont toujours des problèmes de précision.

Si vous avez déjà le numéro stocké sous le numéro NSNumber, ceci devrait fonctionner:

id n = [NSNumber numberWithDouble:dbl];
id dn1 = [NSDecimalNumber decimalNumberWithDecimal:[n decimalValue]];

NSLog(@"  n doubleValue: %.20f, description: %@", [n doubleValue], n);   
NSLog(@"dn1 doubleValue: %.20f, description: %@", [dn1 doubleValue], dn1);  

Mais comme vous pouvez le voir dans la sortie ci-dessous, certains des chiffres les moins significatifs sont évincés:

  n doubleValue: 36.76662445068359375000, description: 36.76662445068359
dn1 doubleValue: 36.76662445068357953915, description: 36.76662445068359

Si le nombre est une primitive (float ou double) alors ceci devrait fonctionner:

id dn2 = [NSDecimalNumber decimalNumberWithMantissa:(dbl * pow(10, 17))
                                          exponent:-17 
                                        isNegative:(dbl < 0 ? YES : NO)];

NSLog(@"dn2 doubleValue: %.20f, description: %@", [dn2 doubleValue], dn2);

Mais vous aurez à nouveau des erreurs de précision. Comme vous pouvez le voir dans le résultat ci-dessous:

dn2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168

La raison pour laquelle je pense que cette perte de précision est due à l'implication de la multiplication de la virgule flottante, car le code suivant fonctionne très bien:

id dn3 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L 
                                           exponent:-17 
                                         isNegative:NO];

NSLog(@"dn3 doubleValue: %.20f, description: %@", [dn3 doubleValue], dn3);   

Sortie:

dn3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

Ainsi, la conversion/initialisation la plus cohérente d'une double ou float vers NSDecimalNumber utilise decimalNumberWithString:. Mais, comme Brad Larson l'a souligné dans sa réponse, cela pourrait être un peu lent. Sa technique de conversion à l'aide de NSScanner pourrait être meilleure si les performances deviennent un problème.

68
orj

Certains problèmes peuvent survenir si vous utilisez les initialiseurs de NSNumber lors de la création d'un NSDecimalNumber. Voir l'exemple posté dans cette question pour un bon exemple. De plus, je me fie à la parole de Bill Bumgarner à ce sujet de sa réponse sur la liste de diffusion cocoa-dev . Voir aussi la réponse d'Ashley ici .

Parce que je suis paranoïaque à propos de ces problèmes de conversion, j'ai déjà utilisé le code suivant pour créer des structures NSDecimal:

NSDecimal decimalValueForDouble(double doubleValue)
{
    NSDecimal result;
    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSScanner *theScanner = [[NSScanner alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];

    [theScanner scanDecimal:&result];
    [theScanner release];

    return result;
}

La raison du code complexe ici est que j'ai trouve que NSScanner est environ 90% plus rapide que l’initialisation de NSDecimalNumber à partir d’une chaîne. J'utilise des structures NSDecimal parce qu'elles peuvent apporter d'importants avantages en termes de performances par rapport à leurs frères NSDecimalNumber.

Pour une approche NSDecimalNumber plus simple mais légèrement plus lente, vous pouvez utiliser quelque chose comme: 

NSDecimalNumber *decimalNumberForDouble(double doubleValue)
{
    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSDecimalNumber *stringProcessor = [[NSDecimalNumber alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];
    return [stringProcessor autorelease];
}

Toutefois, si vous utilisez NSDecimalNumbers, vous devrez faire tout ce qui est en votre pouvoir pour éviter qu'une valeur ne soit convertie en un nombre à virgule flottante à tout moment. Relisez les valeurs de chaîne de vos champs de texte et convertissez-les directement en NSDecimalNumbers, faites votre calcul avec NSDecimalNumbers et écrivez-les sur le disque sous forme de NSStrings ou de valeurs décimales dans Core Data. Vous commencez à introduire des erreurs de représentation en virgule flottante dès que vos valeurs sont converties en nombres à virgule flottante en tout point.

15
Brad Larson

Tu pourrais essayer:

float myFloat = 42.0;
NSDecimal myFloatDecimal = [[NSNumber numberWithFloat:myFloat] decimalValue];
NSDecimalNumber* num = [NSDecimalNumber decimalNumberWithDecimal:myFloatDecimal];

Apparemment, il existe des moyens plus rapides de le faire , mais cela ne vaut peut-être pas la peine si le code n'est pas un goulot d'étranglement.

1
fbrereto

-[NSNumber initWithFloat:] et -[NSNumber initWithDouble:] ne renvoient pas les objets NSNumber *, ils renvoient id et NSDecimalNumber héritent de NSNumber, vous pouvez donc utiliser:

NSDecimalNumber *decimal = [[[NSDecimalNumber alloc] initWithFloat:1.1618] autorelease];

Et duh, je viens de me rendre compte que la même chose vaut pour +numberWithFloat:, donc vous pouvez simplement l'utiliser à la place.

0
Seth Kingsley