Получение NSDecimalNumber из определенной в локали строки?

У меня есть строка s, которая зависит от локали (например, 0,01 или 0,01). Я хочу преобразовать эту строку в NSDecimalNumber. Из примеров, которые я видел до сих пор в interwebs, это достигается с помощью NSNumberFormatter a la:

NSString *s = @"0.07";

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[formatter setGeneratesDecimalNumbers:YES];
NSDecimalNumber *decimalNumber = [formatter numberFromString:s];

NSLog([decimalNumber stringValue]); // prints 0.07000000000000001

Я использую режим 10.4 (в дополнение к тому, чтобы быть рекомендованным в документации, это также единственный доступный на iPhone режим), но указывающий на форматтер, который я хочу создать десятичные числа. Обратите внимание, что я упростил свой пример (я действительно имею дело с валютными строками). Тем не менее, я, очевидно, что-то не так, чтобы вернуть значение, которое иллюстрирует неточность чисел с плавающей запятой.

Каков правильный метод преобразования определенной строки языка в NSDecimalNumber?

Изменить: Обратите внимание, что мой пример для простоты. Вопрос, который я задаю, также должен касаться того, когда вам нужно взять определенную локальную валютную строку и преобразовать ее в NSDecimalNumber. Кроме того, это можно расширить до определенной процентной строки в локали и преобразовать ее в NSDecimalNumber.

Ответы

Ответ 1

На основании ответа Boaz Stuller я зарегистрировал ошибку для Apple по этой проблеме. Пока это не будет разрешено, вот обходные решения, которые я решил использовать как лучший подход. Эти обходные пути просто полагаются на округление десятичного числа до соответствующей точности, что является простым подходом, который может дополнять ваш существующий код (а не переходить от форматирования к сканерам).

Общие номера

По сути, я просто округляю число, основанное на правилах, которые имеют смысл для моей ситуации. Итак, YMMV в зависимости от точности, которую вы поддерживаете.

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[formatter setGeneratesDecimalNumbers:TRUE];

NSString *s = @"0.07";

// Create your desired rounding behavior that is appropriate for your situation
NSDecimalNumberHandler *roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:2 raiseOnExactness:FALSE raiseOnOverflow:TRUE raiseOnUnderflow:TRUE raiseOnDivideByZero:TRUE]; 

NSDecimalNumber *decimalNumber = [formatter numberFromString:s];
NSDecimalNumber *roundedDecimalNumber = [decimalNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior];

NSLog([decimalNumber stringValue]); // prints 0.07000000000000001
NSLog([roundedDecimalNumber stringValue]); // prints 0.07

Валюта

Обработка валют (это фактическая проблема, которую я пытаюсь решить) - это всего лишь небольшая вариация при обработке общих чисел. Ключ состоит в том, что масштаб поведения округления определяется максимальными дробными цифрами, используемыми валютой локали.

NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[currencyFormatter setGeneratesDecimalNumbers:TRUE];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

// Here is the key: use the maximum fractional digits of the currency as the scale
int currencyScale = [currencyFormatter maximumFractionDigits];

NSDecimalNumberHandler *roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:currencyScale raiseOnExactness:FALSE raiseOnOverflow:TRUE raiseOnUnderflow:TRUE raiseOnDivideByZero:TRUE]; 

// image s is some locale specific currency string (eg, $0.07 or €0.07)
NSDecimalNumber *decimalNumber = (NSDecimalNumber*)[currencyFormatter numberFromString:s];
NSDecimalNumber *roundedDecimalNumber = [decimalNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior];

NSLog([decimalNumber stringValue]); // prints 0.07000000000000001
NSLog([roundedDecimalNumber stringValue]); // prints 0.07

Ответ 2

Спустя годы:

+(NSDecimalNumber *)decimalNumberWithString:(NSString *)numericString в NSDecimalNumber.

Ответ 3

Это работает:

NSString *s = @"0.07";

NSScanner* scanner = [NSScanner localizedScannerWithString:s];
NSDecimal decimal;
[scanner scanDecimal:&decimal];
NSDecimalNumber *decimalNumber = [NSDecimalNumber decimalNumberWithDecimal:decimal];

NSLog([decimalNumber stringValue]); // prints 0.07

Также сообщите об ошибке. Это определенно не правильное поведение, которое вы там видите.

Изменить: пока Apple не исправляет это (а затем каждый потенциальный пользователь не обновляет фиксированную версию OSX), вам, вероятно, придется сворачивать свой собственный парсер с помощью NSScanner или принимать двойную точность "только" для введенных номеров. Если вы планируете иметь бюджет Пентагона в этом приложении, я бы предложил его. Реально, удваивается с точностью до 14 знаков после запятой, поэтому на сумму менее триллиона долларов они будут меньше, чем копейки. Мне пришлось написать свои собственные процедуры синтаксического анализа на основе NSDateFormatter для проекта, и я потратил буквально месяц на обработку всех забавных случаев края (например, как только Швеция имеет день недели, включенный в ее длинную дату).

Ответ 4

См. также Лучший способ хранения значений валюты на С++

Лучшим способом обработки валюты является использование целочисленного значения для наименьшей единицы валюты, т.е. центов за доллары/евро и т.д. Вы избежите ошибок точности, связанных с плавающей точкой, в вашем коде.

С учетом этого лучший способ анализа строк, содержащих значение валюты, - это сделать это вручную (с настраиваемым символом десятичной точки). Разделите строку в десятичной точке и проанализируйте как первую, так и вторую часть как целочисленные значения. Затем используйте построение своего комбинированного значения.