简体   繁体   中英

QJsonDocument::toJson() incorrect double precision

In my project I read from a json file with QJsonDocument::fromJson() . This works great, however when I try to write the QJsonDocument back to file with toJson() some of the doubles have messed up precision.

For example, calling toJson() on a document with a QJsonValue with a double value of 0.15 will save to file as 0.14999999999999999 . I do not want this.

This is because the Qt source file qjsonwriter.cpp at line 126 (Qt 5.6.2) reads:

json += QByteArray::number(d, 'g', std::numeric_limits<double>::digits10 + 2); // ::digits10 is 15

That +2 at the end there is messing me up. If this same call to QByteArray::number() instead has a precision of 15 (instead of 17), the result is exactly as I need... 0.15 .

I understand how the format of floating point precision causes the double to be limited in what it can represent. But if I limit the precision to 15 instead of 17, this has the effect of matching the input double precision, which I want.

How can I get around this?

Obviously... I could write my own Json parser, but that's last resort. And obviously I could edit the Qt source code, however my software is already deployed with the Qt5Core.dll included in everyone's install directory, and my updater is not designed to update any dll's. So I cannot edit the Qt source code.

Fingers crossed someone has a magic fix for this :)

this has the effect of matching the input double precision, which I want.

This request doesn't make much sense. A double doesn't carry any information about its precision - it only carries a value. 0.15, 0.1500 and 0.14999999999999999 are the exact same double value, and the JSON writer has no way to know how it was read from the file in first place ( if it was read from a file at all).

In general you cannot ask for maximum 15 digits of precision as you propose, as, depending from the particular value, up to 17 are required for a precise double->text->double roundtrip, so you would write incorrectly rounded values. What some JSON writers do however is to write numbers with the minimum number of decimals required to read the same double back . This is far from trivial to do numerically correctly unless you do - as many do - a loop from 15 to 17, write the number with such precision, parse it back and see if it comes back as the exact same double value. While this generates "nicer" (and smaller) output, it's more work and slows down the JSON write, so that's why probably Qt doesn't do this.

Still, you can write your own JSON write code and have this feature, for a simple recursive implementation I expect ~15 lines of code.

That being said, again, if you want to precisely match your input this won't save you - as it's simply impossible.

I just encountered this as well. I needed to produce relatively complex json objects to send to an api which expected assorted degrees of precision / significant digits depending upon the field. Rather than replace an entire Qt JSON implementation with a third party library (or roll my own!), I kludged a solution...

My full code base related to this is too extensive and elaborate to post and explain here. But the gist of it is simple enough.

I use a QVariantMap (or QVariantHash) to collect my data, and then convert that to json via the built-in QJsonObject::fromVariantMap or QJsonDocument::fromVariant functions. To control the serialization, I define a class called DataFormatOptions which has a decimalPrecision member and then I call a function called toMagicVar to create "magic variants" for my data structure to be converted to json bytes. To control for the number format / precision toMagicVar converts doubles and floats to strings that are in the desired format, and surrounds the string value with some "magic bytes". The way my actual code is written, one can easily do this on any "level" of the map/hash I'm building / formatting via recursive processing, but I've omitted those details...

const QString NO_QUOTE( "__NO_QUOT__" );

QVariant toMagicVar( const QVariant &var, const DataFormatOptions &opt )
{
...
    const QVariant::Type type( var.type() );
    const QMetaType::Type metaType( (QMetaType::Type)type );
... 
    if( opt.decimalPrecision != DataFormatOptions::DEFAULT_PRECISION
        && (type == QVariant::Type::Double || metaType == QMetaType::Float) )
    {
        static const char FORMAT( 'f' );
        const QString formatted( QString::number(
            var.toDouble(), FORMAT, opt.decimalPrecision ) );
        return QVariant( QString( NO_QUOTE + formatted + NO_QUOTE ) );
    }
... 
}

Once I have the json bytes I'm going to send across the network as a QByteArray , I remove the magic bytes so my numbers represented as quoted strings become numbers again in the json.

// This is where any "magic residue" is removed, or otherwise manipulated,
// to produce the desired final json bytes...
void scrubMagicBytes( QByteArray &bytes )
{
    static const QByteArray
          QUOTE( "\"" )
        , NO_QUOTE_PREFIX( QUOTE + NO_QUOTE.toLocal8Bit() )
        , NO_QUOTE_SUFFIX( NO_QUOTE.toLocal8Bit() + QUOTE );
    static const ushort
        NO_QUOTE_PREFIX_LEN( NO_QUOTE_PREFIX.length() ),
        NO_QUOTE_SUFFIX_LEN( NO_QUOTE_SUFFIX.length() );
    for( int idx = bytes.indexOf( NO_QUOTE_PREFIX ); idx != -1;
             idx = bytes.indexOf( NO_QUOTE_PREFIX ) )
        bytes.remove( idx, NO_QUOTE_PREFIX_LEN );
    for( int idx = bytes.indexOf( NO_QUOTE_SUFFIX ); idx != -1;
             idx = bytes.indexOf( NO_QUOTE_SUFFIX ) )
        bytes.remove( idx, NO_QUOTE_SUFFIX_LEN );
}

The above scrub function may be written more concisely, if you want to convert the QByteArray to QString and then back, in order to perform a "replace all", which QByteArray seems to not have for some reason?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM