【问题标题】:How to format a double with leading zeroes and rounded decimals如何使用前导零和四舍五入的小数格式化双精度
【发布时间】:2020-01-22 13:59:25
【问题描述】:

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

例如固定小数点前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

附加约束:

  • 小数点需要实际四舍五入,而不是用空格填充
  • 负数不是问题,在此之前它们会被过滤掉
  • 输入在 [0.0, 180.0] 范围内

我们研究了 printf、stringstream、boost::format 和 fmtlib,但它们似乎都没有提供对小数点之前位数的具体控制。控制这一点的标准方法是调整字段宽度和精度,但这似乎不能提供我们需要的粒度。

目前我们找到的最“优雅”的解决方案如下(123.1f 是输入值):

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

但我不禁想到必须有一个更优雅/更强大的解决方案。


对于上下文,我们有一个显示纬度/经度坐标的 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"

【问题讨论】:

    标签: c++ string formatting fmt


    【解决方案1】:

    这个怎么样:

    #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
    

    更新:进行了一些编辑以确保输出与您要查找的内容相同。

    【讨论】:

    • 很酷,而且所有这些都不需要外部库。 1.12345 仍然需要四舍五入到 "001.12",但这只是一个小调整。不过,我希望有一个不错的单线,让我们看看是否有人找到。
    • 我一直在测试您在我们软件中的最新更新,发现输入 >= 10 有问题。请尝试以下输入值:123.12345123.0000076.54321、@987654328 @
    • 是的....我猜需要进行一些调整来处理所有案例...我只真正测试了您提供的案例。我认为标准库不可能实现通用的单线
    【解决方案2】:

    您可以使用 {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 的输出稳定且不受语言环境的影响。

    【讨论】:

    • 简短但高效,我喜欢它。我仍然遇到像 11.999999 这样的值的问题(将显示为 11.0),所以我最终使用了不同的解决方案来解决这个问题;但是,它深受您的启发。谢谢!
    【解决方案3】:

    我最终使用了一次打印整个坐标的解决方案,因为我有大量舍入误差分别处理度/分/秒(例如,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"
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-10
      • 2012-04-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多