Тайна за представление чисел с плавающей точкой - PullRequest
0 голосов
/ 29 августа 2018

Я тестировал какое-то простое решение для своего приложения, и я столкнулся с некоторым случаем, когда у меня в голове возникает вопрос ... «Почему одно плавающее число представлено в JSON правильно (как я ожидаю), а другое нет ...?»

в этом случае преобразование из String в десятичную, а затем в JSON числа: «98,39» совершенно предсказуемо с человеческой точки зрения, но число: «98,40» выглядит не так красиво ...

И мой вопрос, может ли кто-нибудь объяснить, пожалуйста, мне, , почему преобразование из String в Decimal работает, как я ожидаю, для одного плавающего числа, а для другого - нет.

У меня много красного об ошибке числа с плавающей запятой, но я не могу понять, как происходит процесс Строка -> ... двоичное преобразование ...-> в Double имеет разную точность в обоих случаях.


Код моей детской площадки:

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    //Encode Person Struct as Data
    let encodedData = try? JSONEncoder().encode(price)

    //Create JSON
    var json: Any?
    if let data = encodedData {
        json = try? JSONSerialization.jsonObject(with: data, options: [])
    }

    //Print JSON Object
    if let json = json {
        print("Person JSON:\n" + String(describing: json) + "\n")
    }
}

let stringPriceOK =     "98.39"
let stringPriceNotOK =  "98.40"
let stringPriceNotOK2 = "98.99"

printJSON(from: stringPriceOK)
printJSON(from: stringPriceNotOK)
printJSON(from: stringPriceNotOK2)
/*
 ------------------------------------------------
 // OUTPUT:
 Person JSON:
 {
 amount = "98.39";
 }

 Person JSON:
 {
 amount = "98.40000000000001";
 }

 Person JSON:
 {
 amount = "98.98999999999999";
 }
 ------------------------------------------------
 */

Я искал / пытался выяснить, какие шаги были выполнены логической единицей для преобразования: "98,39" -> Десятичный -> Строка - с результатом "98,39" и с той же цепочкой преобразования: "98.40" -> Десятичный -> Строка - с результатом "98.40000000000001"

Большое спасибо за все ответы!

Ответы [ 3 ]

0 голосов
/ 30 августа 2018
#include <stdio.h>
int main ( void )
{
    float f;
    double d;

    f=98.39F;
    d=98.39;

    printf("%f\n",f);
    printf("%lf\n",d);
    return(1);
}
98.389999
98.390000

это не совсем загадка, как указал Саймон. Это просто, как работают компьютеры, вы используете машину Base 2, чтобы делать вещи 10 Base. Также как 1/3 очень простое число, но в десятой базе это 0,3333333. навсегда, не точный и не симпатичный, но в базе 3 это было бы что-то вроде 0.1 красиво и чисто. Например, цифры из базы 10 плохо сочетаются с базой 2 1/10.

float fun0 ( void )
{
    return(98.39F);
}
double fun1 ( void )
{
    return(98.39);
}
00000000 <fun0>:
   0:   e59f0000    ldr r0, [pc]    ; 8 <fun0+0x8>
   4:   e12fff1e    bx  lr
   8:   42c4c7ae    sbcmi   ip, r4, #45613056   ; 0x2b80000

0000000c <fun1>:
   c:   e59f0004    ldr r0, [pc, #4]    ; 18 <fun1+0xc>
  10:   e59f1004    ldr r1, [pc, #4]    ; 1c <fun1+0x10>
  14:   e12fff1e    bx  lr
  18:   c28f5c29    addgt   r5, pc, #10496  ; 0x2900
  1c:   405898f5    ldrshmi r9, [r8], #-133 ; 0xffffff7b

42c4c7ae  single
405898f5c28f5c29  double

0 10000101 10001001100011110101110
0 10000000101 1000100110001111010111000010100011110101110000101001

10001001100011110101110
1000100110001111010111000010100011110101110000101001

просто ясно глядя на мантиссы между ними, это не приведет к точному числу, поэтому в игру вступает округление и форматированная печать с большим количеством округлений ...

0 голосов
/ 30 августа 2018

Это чисто артефакт того, как NSNumber печатает себя.

JSONSerialization реализован в Objective-C и использует объекты Objective-C (NSDictionary, NSArray, NSString, NSNumber и т. Д.) Для представления значений, которые он десериализует из вашего JSON. Поскольку JSON содержит пустое число с десятичной точкой в ​​качестве значения для ключа "amount", JSONSerialization анализирует его как double и упаковывает его в NSNumber.

Каждый из этих классов Objective C реализует метод description для печати самого себя.

Объект, возвращаемый JSONSerialization, является NSDictionary. String(describing:) преобразует NSDictionary в String, отправив ему метод description. NSDictionary реализует description, отправляя description каждому из его ключей и значений, включая значение NSNumber для клавиши "amount".

Реализация NSNumber description форматирует значение double с использованием спецификатора printf %0.16g. (Я проверил с помощью дизассемблера.) О спецификаторе g в стандарте C написано

Наконец, если не используется флаг #, любые дробные нули удаляются из дробной части результата, а широкий символ десятичной запятой удаляется, если не осталось дробной части.

Ближайший двойной к 98,39 - это точно 98,3900000000000005684341886080801486968994140625. Таким образом, %0.16g форматирует это как %0.14f (см. Стандарт, почему 14, а не 16), что дает "98.39000000000000", а затем отбрасывает конечные нули, давая "98.39".

Ближайший двойной к 98.40 - это точно 98.400000000000005684341886080801486968994140625. Таким образом, %0.16g форматирует это как %0.14f, что дает "98.40000000000001" (из-за округления), и нет никаких конечных нулей для отсечки.

Вот почему, когда вы печатаете результат JSONSerialization.jsonObject(with:options:), вы получаете много дробных цифр для 98.40, но только две цифры для 98.39.

Если вы извлекаете суммы из объекта JSON и конвертируете их в собственный тип Double Swift, а затем печатаете эти Double s, вы получаете намного более короткий вывод, потому что Double реализует более умный алгоритм форматирования, который печатает самая короткая строка, которая при разборе выдает точно такой же Double.

Попробуйте это:

import Foundation

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    let data = try! JSONEncoder().encode(price)
    let jsonString = String(data: data, encoding: .utf8)!
    let jso = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
    let nsNumber = jso["amount"] as! NSNumber
    let double = jso["amount"] as! Double

    print("""
    Original string: \(string)
        json: \(jsonString)
        jso: \(jso)
        amount as NSNumber: \(nsNumber)
        amount as Double: \(double)

    """)
}

printJSON(from: "98.39")
printJSON(from: "98.40")
printJSON(from: "98.99")

Результат:

Original string: 98.39
    json: {"amount":98.39}
    jso: ["amount": 98.39]
    amount as NSNumber: 98.39
    amount as Double: 98.39

Original string: 98.40
    json: {"amount":98.4}
    jso: ["amount": 98.40000000000001]
    amount as NSNumber: 98.40000000000001
    amount as Double: 98.4

Original string: 98.99
    json: {"amount":98.99}
    jso: ["amount": 98.98999999999999]
    amount as NSNumber: 98.98999999999999
    amount as Double: 98.99

Обратите внимание, что как фактическая версия JSON (в строках, помеченных json:), так и версия Swift Double используют наименьшее количество цифр во всех случаях. В строках, которые используют -[NSNumber description] (помечены jso: и amount as NSNumber:), используются дополнительные цифры для некоторых значений.

0 голосов
/ 30 августа 2018

Кажется, что в какой-то момент представление JSON хранит значение в виде двоичной плавающей запятой.

В частности, ближайшим значением double (двоичный код IEEE) до 98,40 будет 98,400000000000005684341886080801486968994140625, что при округлении до 16 значащих цифр равно 98,40000000000001.

Почему 16 значимых цифр? Это хороший вопрос, поскольку 16 значащих цифр недостаточно для однозначной идентификации всех значений с плавающей запятой, например, 0.056183066649934776 и 0.05618306664993478 совпадают с 16 значащими цифрами, но соответствуют различным значениям. Что странно, так это то, что ваш код теперь печатает

["amount": 0.056183066649934998]

для обоих, что составляет 17 значащих цифр, но на самом деле совершенно неверное значение, на 32 единиц на последнем месте . Я понятия не имею, что там происходит.

См. https://www.exploringbinary.com/number-of-digits-required-for-round-trip-conversions/ для более подробной информации о необходимом количестве цифр для двоично-десятичных преобразований.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...