如何格式化浮点值,使其从不使用指数表示法,也不具有尾随零

How do I format a floating point value so that it never uses exponent notation nor has trailing zeros?

本文关键字:表示 指数 格式化      更新时间:2023-10-16

根据ios_base操纵器,在格式化不带指数表示法(带十进制数)的浮点数时,我基本上可以在defaultfloatfixed之间进行选择。

然而,我想选择最大精度,这将为许多数字(例如1.)的fixed产生许多尾随零,但避免使用指数表示法。如果设置为defaultfloat,它在大多数时间内看起来都是正确的,除非该值真的很小,但不是0.。在这种情况下,默认表示法会自行切换到科学表示法,这会破坏格式化输出的接收器(因为它不知道2.22045e-16的意思

那么,我怎么能既吃馅饼又吃馅饼呢?也就是说,没有不必要的尾随零的非指数表示法


注意:我没有测试defaultfloat标志的效果,因为我的gcc似乎还没有实现那个标志,但我认为它是默认设置,不使用任何标志即可应用。我确实检查了fixed标志,它的行为与预期的一样

一个简单的方法如下:

std::string float2string(double f)
{
    std::stringstream ss;
    ss << std::fixed << std::setprecision(122) << f;   // 122 is LARGE, but you may find that really tiny or really large numbers still don't work out... 
    std::string s = ss.str();
    std::string::size_type len = s.length();
    int zeros = 0;
    while(len > 1 && s[--len] == '0')
        zeros++;
    if (s[len] == '.')  // remove final '.' if number ends with '.'
        zeros++;
    s.resize(s.length()-zeros);

    return s;
}

我对它进行了一些测试。最大的问题是,它为一些数字提供了大量的小数,而像0.05这样的数字则是0.050000000000000002775557561562891351059079170227050781250.7变为:0.05000000000000000277555756156289135105907917022705078125

这是因为它不是二进制形式的"精确"数字

我认为解决方案是通过取整数位数(或floor(log(f)/log(10)+1)),然后从常数中减去这个数字,比如17,这是你可以从双位数中得到的位数。然后去掉多余的零。

我暂时不谈,因为我还有其他事情要做,我应该在不久前开始…;)

由于似乎没有一种正确的方法来处理stdlib,所以这里是我的包装器。

  template <typename T>
  struct FpFormat {
    template <typename Stream>
    static Stream& setfmt(Stream& str) {
      return str;
    }
    template <typename String>
    static String const& untrail(String const& str) {
      return str;
    }
  };
  template <typename T>
  struct FpFormatFloats {
    template <typename Stream>
    static auto setfmt(Stream& str) -> decltype(str << std::fixed << std::setprecision(std::numeric_limits<T>::digits10)) {
      return str << std::fixed << std::setprecision(std::numeric_limits<T>::digits10);
    }
    template <typename String>
    static String untrail(String str) {
      if (str.find('.') == String::npos)
        return str;
      return ([](String s){
        return String(s.begin(),s.begin()+((s.back() == '.')?(s.size()-1):s.size()));
      })(str.substr(0,(str+"0").find_last_not_of('0')+1));
    }
  };
  template <> struct FpFormat<float> : FpFormatFloats<float> {};
  template <> struct FpFormat<double> : FpFormatFloats<double> {};
  template <> struct FpFormat<long double> : FpFormatFloats<long double> {};
  template <typename T>
  std::string toString(T x) {
    std::stringstream str;
    FpFormat<T>::setfmt(str) << x;
    return FpFormat<T>::untrail(str.str());
  }

遗憾的是,在iostreamprintf()中似乎都没有这样做的代码。我的意思是:

  1. 以固定格式打印
  2. 打印最小位数,以便编码是无损的(即,最接近的有效浮点数字是您打印的数字)

您不能仅以非常高的精度打印并去掉尾随的零,因为可能存在不必要的非零数字。

我相信真正做到这一点的唯一好方法是使用像Ryu这样的专用代码。

另一种方法是以非常高的精度打印下一个和上一个数字,并取小数点后相同的数字(加一)。不过会很难看的。

没有内置的格式可以为您做到这一点。您可以从头开始生成自己的格式化程序,也可以编写一个对字符串进行后处理以删除不必要的尾随零的格式化程序。

例如,您可以使用正则表达式替换:

#include <iomanip>
#include <iostream>
#include <sstream>
#include <regex>
// not thoroughly tested
std::string format(double d, int precision = 100) {
   std::stringstream ss;
   ss << std::fixed << std::setprecision(precision);
   ss << d;
   return std::regex_replace(
       std::regex_replace(ss.str(), std::regex(R"((-?d*.d*?)0*$)"), "$1"),
       std::regex(R"(.$)"), "");
}
int main() {
   std::cout << format(-1) << 'n';
   std::cout << format(1e20) << 'n';
   std::cout << format(1e-20) << 'n';
   std::cout << format(2.22045e-16) << 'n';
}

尽管使用正则表达式可能不是一个特别有效的解决方案。