如何格式化带有前导零和四舍五入小数的双精度数

How to format a double with leading zeroes and rounded decimals

是否有一种优雅的方法可以将双精度数格式化为小数点前具有固定数量数字(用前导零填充)的字符串,但仅显示需要的小数并具有最大精度?

例如,小数点前固定3位,小数点后最多2位:

// desired:
1.00000 -> "001"
1.10000 -> "001.1"
1.12345 -> "001.12"

// not:
1.10000 -> "0001.1" // too many leading zeroes
1.12345 -> "1.1235" // too few leading zeroes, too many digits after the decimal point

附加限制:

我们研究了 printf、stringstream、boost::format 和 fmtlib,但其中 none 似乎提供了对 before[=] 数字数量的特定控制39=]小数点。控制它的标准方法是调整字段宽度和精度,但这似乎并不能提供我们需要的粒度。

目前我们找到的最多 "elegant" 的解决方案如下(其中 123.1f 是输入值):

boost::trim_right_copy_if(fmt::format("{:06.2f}", 123.1f), boost::is_any_of("0"))

但我不禁想到必须有一个更 elegant/robust 的解决方案。


对于上下文,我们有一个显示 latitude/longitude 坐标的 GUI。我们的客户要求我们用前导零填充,但尽可能减少数字。这是减少不必要信息和尽可能避免混淆之间的折衷。例如:

W135°2'2.3344" -> W135°02'02.33"
W135°22.3344"  -> W135°00'22.33"
W135°2'3"      -> W135°02'03"
W135°22'2.999" -> W135°22'03"
W1°35"         -> W001°00'35"
W1°35'         -> W001°35'00"

这个怎么样:

#include <iostream>
#include <iomanip>
#include <string>
#include <sstream>

void
output(double d)
{
    std::stringstream pre;
    pre << static_cast<long int>(d);

    std::stringstream post;
    post << d-static_cast<long int>(d);

    int pre_digits = pre.str().length();
    int post_digits = post.str().length() - pre_digits;
    int width = pre_digits + post_digits + 2;

    if (post_digits > 2) {
        post_digits = 2;
        width = pre_digits + post_digits + 3;
    }

    std::cout << std::setfill('0')
            << std::setprecision(pre_digits + post_digits)
            << std::setw(width)
            << d
            << '\n';
}

int main()
{
    output(1.00000);
    output(1.10000);
    output(1.12345);

    return 0;
}

这导致:

001
001.1
001.12

更新:进行了一些编辑以确保输出与您正在寻找的相同。

您可以使用 {fmt} 或其他库通过分别格式化整数部分和小数部分来轻松完成此操作:

#include <cmath>
#include <fmt/core.h>

std::string double_to_string(double value) {
  double integral = 0;
  double fractional = std::modf(value, &integral);
  return fmt::format("{:03.0f}", integral) +
         fmt::format("{:.2g}", fractional).substr(1);
}

double_to_string(1.00000); // -> "001"
double_to_string(1.10000); // -> "001.1"
double_to_string(1.12345); // -> "001.12"

这可能需要微调以支持负值。

您也可以使用 fmt::memory_bufferfmt::format_to 来避免字符串分配。

您的解决方案也非常稳健,因为 fmt::format 的输出稳定且不受语言环境影响。

我最终使用了一次打印整个坐标的解决方案,因为我有大量舍入误差单独处理 degrees/minutes/seconds(例如 2.0° 被打印为 1°60')。它的灵感来自 vitaut 的解决方案,将整数和小数部分格式化为单独的字符串:

// value is the input in decimal degrees (e.g. 77° 20' = 77.3333333)
// deg_width controls if degrees are printed with 2 (latitude) or 3 (longitude) digits
std::string formatDMS (double value, size_t deg_width) 
{
    std::string res;
    static const int SECONDS_DECIMAL_PLACES = 2; // amount of decimals to print seconds with

    // Convert everything to int, to get rid of pesky floating-point errors
    static const int ONE_SECOND = std::pow (10, SECONDS_DECIMAL_PLACES);    // e.g. 1 second = 100 * 0.01 (seconds with 2 decimals)
    static const int ONE_MINUTE = 60 * ONE_SECOND;  // e.g. 1 minute = 6000 * 0.01 (seconds with 2 decimals)
    static const int ONE_DEGREE = 60 * ONE_MINUTE;  // e.g. 1 minute = 360000 * 0.01 (seconds with 2 decimals)

    const int value_incs = std::lround (value * ONE_DEGREE);
    const int degrees = value_incs / ONE_DEGREE;
    const int deg_rem = value_incs % ONE_DEGREE;
    const int minutes = deg_rem / ONE_MINUTE;
    const int min_rem = deg_rem % ONE_MINUTE;
    const int seconds = min_rem / ONE_SECOND;
    const int sec_rem = min_rem % ONE_SECOND;
    const double decimals = static_cast<double>(sec_rem) / ONE_SECOND;
    const auto decimals_string =
    fmt::format ("{:.{}g}", decimals, SECONDS_DECIMAL_PLACES).substr (1);

    const auto fmtstring = "{:0{}d}° {:02d}\' {:02d}{}\"";
    res += fmt::format (fmtstring, degrees, deg_width, minutes, seconds, decimals_string);
    return res;
}

formatDMS(77.0, 2);         // 77°00'00"
formatDMS(77.033333333, 2); // 77°02'00"
formatDMS(77.049722222, 2); // 77°02'59"
formatDMS(77.049888889, 2); // 77°02'59.6"
formatDMS(77.999999999, 2); // 78°00'00"
formatDMS(7.0, 3);          // 007°00'00"
formatDMS(7.2, 3);          // 007°12'00"
formatDMS(7.203333333, 3);  // 007°12'12"
formatDMS(7.203366667, 3);  // 007°12'12.12"
formatDMS(7.999999999, 3);  // 008°00'00"