Как отформатировать двойной с ведущими нулями и округленными десятичными числами - PullRequest
2 голосов
/ 22 января 2020

Существует ли элегантный способ форматирования двойного числа в виде строки с фиксированным количеством цифр перед десятичной запятой (с добавлением начальных нулей), но с отображением десятичных знаков только необходимого и с максимальной точностью?

Например, с фиксированными 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 , но ни один из них, похоже, не предлагает конкретных c элементов управления количеством цифр до десятичной точки. Стандартный способ контролировать это - регулировать ширину и точность поля, но кажется, что это не дает необходимой нам степени детализации.

Наиболее «элегантное» решение, которое мы до сих пор нашли, это следующее (где 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"

Ответы [ 3 ]

1 голос
/ 23 января 2020

Вы можете сделать это довольно легко с {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_buffer и fmt::format_to, чтобы избежать выделения строк.

Ваше решение также довольно надежное, поскольку вывод fmt::format стабилен и не зависит от локали.

1 голос
/ 22 января 2020

Как насчет этого:

#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

ОБНОВЛЕНИЕ : внесены некоторые изменения, чтобы убедиться, что выходные данные совпадают с тем, что ты искал.

0 голосов
/ 24 января 2020

В итоге я использовал решение, которое печатает всю координату за раз, так как у меня было множество ошибок округления, обрабатывающих градусы / минуты / секунды отдельно (например, 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"
...