如何按照以下方式格式化双打

How to format doubles in the following way?

本文关键字:格式化 方式 何按照      更新时间:2023-10-16

我使用的是C++,我想用以下明显的方式格式化双打。我曾尝试过使用字符串流来玩"固定"answers"科学",但我无法达到预期的输出。

double d = -5; // print "-5"
double d = 1000000000; // print "1000000000"
double d = 3.14; // print "3.14"
double d = 0.00000000001; // print "0.00000000001"
// Floating point error is acceptable:
double d = 10000000000000001; // print "10000000000000000"

根据要求,以下是我尝试过的东西:

#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
using namespace std;
string obvious_format_attempt1( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << d;
    return ss.str();
}
string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    return ss.str();
}
int main(int argc, char *argv[]) 
{
    cout << "Attempt #1" << endl;
    cout << obvious_format_attempt1(-5) << endl;
    cout << obvious_format_attempt1(1000000000) << endl;
    cout << obvious_format_attempt1(3.14) << endl;
    cout << obvious_format_attempt1(0.00000000001) << endl;
    cout << obvious_format_attempt1(10000000000000001) << endl;
    cout << endl << "Attempt #2" << endl;
    cout << obvious_format_attempt2(-5) << endl;
    cout << obvious_format_attempt2(1000000000) << endl;
    cout << obvious_format_attempt2(3.14) << endl;
    cout << obvious_format_attempt2(0.00000000001) << endl;
    cout << obvious_format_attempt2(10000000000000001) << endl;
    return 0;
}

打印以下内容:

Attempt #1
-5
1000000000
3.14
1e-11
1e+16
Attempt #2
-5.000000000000000
1000000000.000000000000000
3.140000000000000
0.000000000010000
10000000000000000.000000000000000

除非你写一些代码以某种方式分析数字,否则程序无法知道如何以你描述的方式格式化数字,即使这样也很困难。

这里所需要的是了解源代码中的输入格式,当编译器将十进制输入源代码转换为二进制形式存储在可执行文件中时,这种格式就会丢失。

一种可行的替代方案是输出到stringstream,然后从中修改输出以去除尾随零。类似这样的东西:

string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    string res = ss.str();
    // Do we have a dot?
    if ((string::size_type pos = res.rfind('.')) != string::npos)
    {
       while(pos > 0 && (res[pos] == '0' || res[pos] == '.')
       {
           pos--;
       }
       res = res.substr(pos);
    }
    return res;
}

我实际上并没有厌倦它,但作为一个粗略的草图,它应该是可行的。需要注意的是,如果你有类似0.1的东西,它很可能会打印为0.0999999999285或类似的东西,因为0.1不能以精确的形式表示为二进制。

准确格式化二进制浮点数非常棘手,而且传统上是错误的。1990年在同一期刊上发表的两篇论文确定,如果转换为二进制浮点数并返回的十进制值不使用比特定约束更多的十进制数字,则可以恢复其值(在C++中,对于适当的类型T,使用std::numeric_limits<T>::digits10表示(:

  • Clinger的"如何准确读取浮点数"描述了一种从十进制表示转换为二进制浮点的算法
  • Steele/White的"如何准确打印浮点数"描述了如何从二进制浮点值转换为十进制十进制值。有趣的是,该算法甚至转换为最短这样的十进制值

在这些论文发表时,二进制浮点("%f""%e""%g"(的C格式指令已经建立好,它们没有被更改为考虑新结果。这些格式化指令的规范问题在于,"%f"假定对小数点后的数字进行计数,并且没有格式说明符要求格式化具有特定数字数的数字,但不一定从小数点开始计数(例如,格式化具有小数点但可能具有多个前导零(。

格式说明符仍然没有得到改进,例如,包括另一个用于可能涉及许多零的非科学表示法的格式说明符。实际上,斯蒂尔/怀特算法的威力并没有完全暴露出来。遗憾的是,C++格式化并没有改善这种情况,只是将语义委托给C格式化指令。

而非设置std::ios_base::fixed并使用精度为std::numeric_limits<double>::digits10的方法是C和C++标准库提供的浮点格式的最接近方法。请求的确切格式可以通过使用std::ios_base::scientific格式化获得数字,解析结果,然后重写数字来获得。为了给这个进程提供一个良好的类似流的接口,它可以std::num_put<char>方面封装。

另一种选择是使用双重转换。此实现使用改进的(更快的(算法进行转换。它还公开了以某种形式获取数字的接口,尽管如果我记得正确的话,它不是直接作为字符序列。

您不能做您想做的事情,因为小数不能以浮点格式精确表示。换句话说,double不能精确地保持3.14,它将所有内容都存储为2的幂的分数,所以它将其存储为3+9175/65536或大约(在计算器上计算,你会得到3.139999389684375。(我意识到65536不是IEEE double的正确分母,但它的要点是正确的(。

这就是所谓的往返问题。你不能可靠地做

double x = 3.14;
cout << magic << x;

并获得"3.14">

如果必须解决往返问题,那么不要使用浮点。使用自定义的"decimal"类,或者使用字符串来保存值。

下面是一个可以使用的十进制类:https://stackoverflow.com/a/15320495/364818

我使用的是C++,我想用以下明显的方式格式化双打。

根据你的样品,我想你想要

  • 固定的而不是科学的符号
  • 合理(但不过分(的精度(这是为了用户显示,所以可以进行一点舍入(
  • 尾部的零被截断,以及
  • 如果数字看起来像整数,小数点也会被截断

以下功能正是这样做的:

    #include <cmath>
    #include <iomanip>
    #include <sstream>
    #include <string>

    std::string fixed_precision_string (double num) {
        // Magic numbers
        static const int prec_limit = 14;       // Change to 15 if you wish
        static const double log10_fuzz = 1e-15; // In case log10 is slightly off
        static const char decimal_pt = '.';     // Better: use std::locale
        if (num == 0.0) {
            return "0";
        }
        std::string result;
        if (num < 0.0) {
            result = '-';
            num = -num;
        }
        int ndigs = int(std::log10(num) + log10_fuzz);
        std::stringstream ss;
        if (ndigs >= prec_limit) {
            ss << std::fixed
               << std::setprecision(0)
               << num;
            result += ss.str();
        }
        else {
            ss << std::fixed
               << std::setprecision(prec_limit-ndigs)
               << num;
            result += ss.str();
            auto last_non_zero = result.find_last_not_of('0');
            if (result[last_non_zero] == decimal_pt) {
                result.erase(last_non_zero); 
            }
            else if (last_non_zero+1 < result.length()) {
                result.erase(last_non_zero+1);
            }
        }
        return result;
    }

如果您使用的计算机使用IEEE浮点,则不建议将prec_limit更改为16。虽然这将使您能够正确打印0.9999999999999999,但它也将5.1打印为5.0999999999999996,将9.99999998打印为9.99999998000000001。这是从我的电脑,您的结果可能会因不同的库而有所不同。

prec_limit更改为15是可以的,但它仍然会导致数字无法"正确"打印。只要您不想打印1.0-1e-15,指定的值(14(就可以很好地工作。

你可以做得更好,但这可能需要放弃标准库(见Dietmar Kühl的回答(。