QJsonDocument::toJson() 不正确的双精度
QJsonDocument::toJson() incorrect double precision
在我的项目中,我使用 QJsonDocument::fromJson()
从 json 文件中读取。这很好用,但是当我尝试用 toJson()
将 QJsonDocument
写回文件时,一些双打已经搞砸了精度。
例如,对具有双精度值 0.15
的 QJsonValue
的文档调用 toJson()
将保存到文件中作为 0.14999999999999999
。我不要这个。
这是因为Qt源文件qjsonwriter.cpp at line 126 (Qt 5.6.2)
是这样写的:
json += QByteArray::number(d, 'g', std::numeric_limits<double>::digits10 + 2); // ::digits10 is 15
最后的 +2 把我搞砸了。如果对 QByteArray::number()
的相同调用的精度为 15(而不是 17),则结果完全符合我的需要...0.15
.
我了解浮点精度的格式如何导致双精度数在它可以表示的范围内受到限制。但是,如果我将精度限制为 15 而不是 17,这会达到我想要的匹配输入双精度的效果。
我该如何解决这个问题?
显然...我可以编写自己的 Json 解析器,但那是最后的手段。显然我可以编辑 Qt 源代码,但是我的软件已经部署在 Qt5Core.dll 中,包含在每个人的安装目录中,而且我的更新程序并非设计用于更新任何 dll。所以我无法编辑Qt源代码。
希望有人能解决这个问题:)
this has the effect of matching the input double precision, which I want.
这个要求没有多大意义。 double 不包含任何关于其精度的信息——它只包含一个值。 0.15、0.1500 和 0.14999999999999999 是完全相同的双精度值,JSON 作者无法知道它是如何首先从文件中读取的(if 它被读取完全来自文件)。
一般来说,你不能像你建议的那样要求最大 15 位的精度,因为根据特定值,精确的 double->text->double 往返最多需要 17 位,所以你会写错四舍五入的价值。然而,一些 JSON 作者所做的是使用 读取相同双回 所需的最小小数位数来编写数字。除非您像许多人那样做从 15 到 17 的循环,以如此精确的方式写下数字,然后解析它并查看它是否返回为完全相同的双精度值,否则要正确地进行数字计算远非易事。虽然这会生成 "nicer"(和更小的)输出,但它需要更多工作并减慢 JSON 写入速度,所以这就是 Qt 可能不这样做的原因。
不过,您可以编写自己的 JSON 代码并具有此功能,对于简单的递归实现,我预计大约需要 15 行代码。
话虽这么说,但如果您想精确匹配您的输入,这也救不了您 - 因为这根本不可能。
我也刚遇到。然而,我 kludged 一个解决方案,而不是用第三方库(或者我自己的!)替换整个 Qt JSON 实现...
我与此相关的完整代码库过于广泛和详尽,无法 post 并在此处进行解释。但是解决这一点的要点很简单。
首先,我使用 QVariantMap(或 QVariantHash)收集数据,然后通过内置的 QJsonObject::fromVariantMap
或 QJsonDocument::fromVariant
函数将其转换为 json。为了控制序列化,我定义了一个名为 DataFormatOptions
的 class,它有一个 decimalPrecision
成员(并设置了对其他此类格式化选项的轻松扩展..),然后我调用了一个名为 toMagicVar
为要转换为 json 字节的数据结构创建“魔术变体”。为了控制数字格式/精度,toMagicVar
将双精度数和浮点数转换为所需格式的字符串,并用一些“魔术字节”包围字符串值。我的实际代码的编写方式,可以很容易地在 map/hash 的任何“级别”上执行此操作,我正在通过递归处理构建/格式化,但我省略了这些细节...
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' );
static const QRegExp trailingPointAndZeros( "\.?0+$" );
QString formatted( QString::number(
var.toDouble(), FORMAT, opt.decimalPrecision ) );
formatted.remove( trailingPointAndZeros );
return QVariant( QString( NO_QUOTE + formatted + NO_QUOTE ) );
}
...
}
请注意,我 trim 通过 formatted.remove
删除了所有无关的数字。如果您希望数据始终包含小数点后的 X 位数字,您可以选择跳过该步骤。 (或者您可能想通过 DataFormatOptions
来控制它?)
一旦我有了 json 字节,我将作为 QByteArray
通过网络发送,我删除了魔法字节,所以我用引号引起来的数字在 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 EMPTY, QUOTE( "\"" ),
NO_QUOTE_PREFIX( QUOTE + NO_QUOTE.toLocal8Bit() ),
NO_QUOTE_SUFFIX( NO_QUOTE.toLocal8Bit() + QUOTE );
bytes.replace( NO_QUOTE_PREFIX, EMPTY );
bytes.replace( NO_QUOTE_SUFFIX, EMPTY );
}
在我的项目中,我使用 QJsonDocument::fromJson()
从 json 文件中读取。这很好用,但是当我尝试用 toJson()
将 QJsonDocument
写回文件时,一些双打已经搞砸了精度。
例如,对具有双精度值 0.15
的 QJsonValue
的文档调用 toJson()
将保存到文件中作为 0.14999999999999999
。我不要这个。
这是因为Qt源文件qjsonwriter.cpp at line 126 (Qt 5.6.2)
是这样写的:
json += QByteArray::number(d, 'g', std::numeric_limits<double>::digits10 + 2); // ::digits10 is 15
最后的 +2 把我搞砸了。如果对 QByteArray::number()
的相同调用的精度为 15(而不是 17),则结果完全符合我的需要...0.15
.
我了解浮点精度的格式如何导致双精度数在它可以表示的范围内受到限制。但是,如果我将精度限制为 15 而不是 17,这会达到我想要的匹配输入双精度的效果。
我该如何解决这个问题?
显然...我可以编写自己的 Json 解析器,但那是最后的手段。显然我可以编辑 Qt 源代码,但是我的软件已经部署在 Qt5Core.dll 中,包含在每个人的安装目录中,而且我的更新程序并非设计用于更新任何 dll。所以我无法编辑Qt源代码。
希望有人能解决这个问题:)
this has the effect of matching the input double precision, which I want.
这个要求没有多大意义。 double 不包含任何关于其精度的信息——它只包含一个值。 0.15、0.1500 和 0.14999999999999999 是完全相同的双精度值,JSON 作者无法知道它是如何首先从文件中读取的(if 它被读取完全来自文件)。
一般来说,你不能像你建议的那样要求最大 15 位的精度,因为根据特定值,精确的 double->text->double 往返最多需要 17 位,所以你会写错四舍五入的价值。然而,一些 JSON 作者所做的是使用 读取相同双回 所需的最小小数位数来编写数字。除非您像许多人那样做从 15 到 17 的循环,以如此精确的方式写下数字,然后解析它并查看它是否返回为完全相同的双精度值,否则要正确地进行数字计算远非易事。虽然这会生成 "nicer"(和更小的)输出,但它需要更多工作并减慢 JSON 写入速度,所以这就是 Qt 可能不这样做的原因。
不过,您可以编写自己的 JSON 代码并具有此功能,对于简单的递归实现,我预计大约需要 15 行代码。
话虽这么说,但如果您想精确匹配您的输入,这也救不了您 - 因为这根本不可能。
我也刚遇到。然而,我 kludged 一个解决方案,而不是用第三方库(或者我自己的!)替换整个 Qt JSON 实现...
我与此相关的完整代码库过于广泛和详尽,无法 post 并在此处进行解释。但是解决这一点的要点很简单。
首先,我使用 QVariantMap(或 QVariantHash)收集数据,然后通过内置的 QJsonObject::fromVariantMap
或 QJsonDocument::fromVariant
函数将其转换为 json。为了控制序列化,我定义了一个名为 DataFormatOptions
的 class,它有一个 decimalPrecision
成员(并设置了对其他此类格式化选项的轻松扩展..),然后我调用了一个名为 toMagicVar
为要转换为 json 字节的数据结构创建“魔术变体”。为了控制数字格式/精度,toMagicVar
将双精度数和浮点数转换为所需格式的字符串,并用一些“魔术字节”包围字符串值。我的实际代码的编写方式,可以很容易地在 map/hash 的任何“级别”上执行此操作,我正在通过递归处理构建/格式化,但我省略了这些细节...
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' );
static const QRegExp trailingPointAndZeros( "\.?0+$" );
QString formatted( QString::number(
var.toDouble(), FORMAT, opt.decimalPrecision ) );
formatted.remove( trailingPointAndZeros );
return QVariant( QString( NO_QUOTE + formatted + NO_QUOTE ) );
}
...
}
请注意,我 trim 通过 formatted.remove
删除了所有无关的数字。如果您希望数据始终包含小数点后的 X 位数字,您可以选择跳过该步骤。 (或者您可能想通过 DataFormatOptions
来控制它?)
一旦我有了 json 字节,我将作为 QByteArray
通过网络发送,我删除了魔法字节,所以我用引号引起来的数字在 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 EMPTY, QUOTE( "\"" ),
NO_QUOTE_PREFIX( QUOTE + NO_QUOTE.toLocal8Bit() ),
NO_QUOTE_SUFFIX( NO_QUOTE.toLocal8Bit() + QUOTE );
bytes.replace( NO_QUOTE_PREFIX, EMPTY );
bytes.replace( NO_QUOTE_SUFFIX, EMPTY );
}