Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Новый манипулятор вывода чисел с плавающей точкой #590

Open
asherikov opened this issue Mar 8, 2024 · 4 comments

Comments

@asherikov
Copy link

Добавить манипулятор вывода чисел с плавающей точкой для компактного форматирования без потери точности и типа, существующие механизмы этого не позволяют (https://en.cppreference.com/w/cpp/io/manip):

  • std::setprecision низкоуровневая функция, в сочетании со стандартным форматированием целочисленных значений теряется точка при выводе, что приводит к потере информации о типе и возможным проблемам в обрабатывающем коде на других языках;
  • вывод std::scientific или std::setprecision + std::showpoint избыточен.

Пример:

#include <iostream>
#include <iomanip>
#include <limits>

int main()
{
    double a = 1.;
    double b = 0.0000000000001;

    std::cout << a << " | "  << b << std::endl;
    std::cout << std::scientific << a << " | "  << b << std::endl;
    std::cout << std::defaultfloat;
    std::cout << std::setprecision(std::numeric_limits<long double>::digits10 + 1) << a << " | " << b << std::endl;
    std::cout << std::defaultfloat;
    std::cout << std::showpoint << std::setprecision(std::numeric_limits<long double>::digits10 + 1) << a << " | " << b << std::endl;

    return 0;
}

Результат:

1 | 1e-13
1.000000e+00 | 1.000000e-13
1 | 1.00000000000000003e-13
1.000000000000000000 | 1.000000000000000030e-13

Хочется:

// std::cout << std::numericdata << a << " | " << b << std::endl;
1. | 1.00000000000000003e-13
@apolukhin
Copy link
Member

apolukhin commented Mar 24, 2024

Сейчас комитет отходит от iostream в пользу форматирования через std::format или std::print. У них есть форматтеры :f, которые всегда выводят без экспоненты и с точкой:

#include <iostream>
#include <print>

int main()
{
    double a = 1.;
    double b = 0.0000000000001;
    double с = 0.3000020000001;
    auto digits = std::numeric_limits<double>::digits10 + 1;
    constexpr auto format_str = R"(
      {2} =>
        :g          {0:g}
        :e          {0:e}
        :f          {0:f}
        :.digits_g  {0:.{1}g}
        :.digits_e  {0:.{1}e}
        :.1_e       {0:.1e}
        :.digits_f  {0:.{1}f}
        :<1_e       {0:<1e}
        :e<1        {0:e<0}
    )";
    std::print(std::cout, format_str, a, digits, "1.");
    std::print(std::cout, format_str, b, digits, "0.0000000000001");
    std::print(std::cout, format_str, с, digits, "0.3000020000001");
}

Вывод:

      1. =>
        :g          1
        :e          1.000000e+00
        :f          1.000000
        :.digits_g  1
        :.digits_e  1.0000000000000000e+00
        :.1_e       1.0e+00
        :.digits_f  1.0000000000000000
        :<1_e       1.000000e+00
        :e<1        1
    
      0.0000000000001 =>
        :g          1e-13
        :e          1.000000e-13
        :f          0.000000
        :.digits_g  1e-13
        :.digits_e  1.0000000000000000e-13
        :.1_e       1.0e-13
        :.digits_f  0.0000000000001000
        :<1_e       1.000000e-13
        :e<1        1e-13
    
      0.3000020000001 =>
        :g          0.300002
        :e          3.000020e-01
        :f          0.300002
        :.digits_g  0.3000020000001
        :.digits_e  3.0000200000010002e-01
        :.1_e       3.0e-01
        :.digits_f  0.3000020000001000
        :<1_e       3.000020e-01
        :e<1        0.3000020000001

Онлайн песочница с примером https://godbolt.org/z/PG8Pd4nfP

Однако эти форматы либо не ставят .0 перед экспонентой, либо не теряют в точности, либо не предоставляют наиболее короткое предстваление. кажется что поведение по умолчани, (:g формат) ближе всего, хоть и не ставят .0 перед экспонентой

А зачем понадобилось иметь именно .0 перед экспонентой? Запись числа - плохой источник информации о типе, т.к. 1.0e3 может быть представимо в виде int, short, double, float, rational, decimal без значимой потери точности

@asherikov
Copy link
Author

  1. Суть та же, приходится выбирать один из вариантов поведения. Опция # ("For floating-point types, the alternate form causes the result of the conversion of finite values to always contain a decimal-point character", https://en.cppreference.com/w/cpp/utility/format/spec) тоже добавляет бесполезные нули.

  2. Например при парсинге YAML файлов сгенерированых программой на C++ из питона значения интерпретируются как целые или как числа с плавающей точкой в зависимости от наличия точки:

# 1.yaml
a:
    b: 1
    c: 1.
import yaml
with open('1.yaml', 'r') as file:
    x = yaml.safe_load(file)
type(x['a']['b']) # int
type(x['a']['c']) # float

Понятно что это ненадежный источник информации о типе, но она может служить подсказкой парсеру. Здесь вопрос скорее не в надежности представления типа, а в том что более соотвествует ожиданиям и позволит избежать некоторых ошибок. boost::lexical_cast, например, не распарсит 1. как int.

@asherikov
Copy link
Author

Пара замечаний в догонку:

  1. Имеет значение не только то что тип может быть угадан неправильно, но и то что он может быть угадан по разному, например, если значение сменилось с 1.0 на 1.1 или наоборот.

  2. Насколько я понимаю std::format не является заменой манипулятору -- непосредственный вывод может производиться внешней библиотекой.

  3. Возможно могут быть подводные камни в не-C локалях.

@asherikov
Copy link
Author

Слепил простой демонстратор моих хотелок если кому-то интересно -> https://github.com/asherikov/numdata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants