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

flexible std::complex formatter #1467

Closed
tesch1 opened this issue Dec 9, 2019 · 20 comments
Closed

flexible std::complex formatter #1467

tesch1 opened this issue Dec 9, 2019 · 20 comments

Comments

@tesch1
Copy link

tesch1 commented Dec 9, 2019

So... c++20 will have a formatter for std::complex (https://www.zverovich.net/2019/07/23/std-format-cpp20.html, https://wg21.link/p1636r0). In the N years between now and when that's widely available, is there something that will vaguely approximate what the standard will be? is there a proposed implementation yet? google doesn't find me anything, so...

I've come up with this, is it ok? Any ideas on how to improve it? It doesnt handle width and align properly yet. Wanted to share it at least, because I couldn't find anything like it, and hopefully someone can improve it....

If this is totally uninteresting feel free to close the issue, at least the next person searching for a std::complex formatter will be able to find a starting place :)

/// This formats a
/// complex number (a+bi) as (a+bi), offering the same formatting
/// options as the underlying type - with the addition of three
/// optional format options, only one of which may appear directly
/// after the ':' in the format spec (before any fill or align): '$'
/// (the default if no flag is specified), '*', and ',".  The '*' flag
/// adds a * before the 'i', producing (a+b*i), where a and b are the
/// formatted value_type values.  The ',' flag simply prints the real
/// and complex parts separated by a comma (same as iostreams' format).
/// As a concrete exmple, this formatter can produce either (3+5.4i)
/// or (3+5.4*i) or (3,5.4) for a complex<double> using the specs {:g}
/// | {:$g}, {:*g}, or {:,g}, respectively.  (this implementation is a
/// bit hacky - glad for cleanups).
///
template <typename T, typename Char>
struct fmt::formatter<std::complex<T>,Char> : public fmt::formatter<T,Char>
{
  typedef fmt::formatter<T,Char> base;
  enum style { expr, star, pair } style_ = expr;
  internal::dynamic_format_specs<Char> specs_;
  constexpr auto parse(format_parse_context & ctx) -> decltype(ctx.begin()) {
    using handler_type = internal::dynamic_specs_handler<format_parse_context>;
    auto type = internal::type_constant<T, Char>::value;
    internal::specs_checker<handler_type> handler(handler_type(specs_, ctx), type);
    auto it = ctx.begin();
    switch (*it) {
    case '$': style_ = style::expr; ctx.advance_to(++it); break;
    case '*': style_ = style::star; ctx.advance_to(++it); break;
    case ',': style_ = style::pair; ctx.advance_to(++it); break;
    default: break;
    }
    if (style_ != style::pair)
      parse_format_specs(ctx.begin(), ctx.end(), handler);
    //todo: fixup alignment
    return base::parse(ctx);
  }
  template <typename FormatCtx>
  auto format(const std::complex<T> & x, FormatCtx & ctx) -> decltype(ctx.out()) {
    format_to(ctx.out(), "(");
    if (x.real() || !x.imag())
      base::format(x.real(), ctx);
    if (x.imag()) {
      if (style_ == style::pair)
        format_to(ctx.out(), ",");
      else
        if (x.real() && x.imag() >= 0 && specs_.sign != sign::plus)
          format_to(ctx.out(), "+");
      base::format(x.imag(), ctx);
      if (style_ != style::pair) {
        if (style_ == style::star)
          format_to(ctx.out(), "*i");
        else
          format_to(ctx.out(), "i");
        if (std::is_same<typename std::decay<T>::type,float>::value)       format_to(ctx.out(), "f");
        if (std::is_same<typename std::decay<T>::type,long double>::value) format_to(ctx.out(), "l");
      }
    }
    return format_to(ctx.out(), ")");
  }
};
@vitaut
Copy link
Contributor

vitaut commented Dec 9, 2019

Looks interesting. I don't think that it needs to be in the core library considering that complex is somewhat niche and that it's not hard to implement its formatting via the extension API as you did. Thanks for sharing the code and hopefully someone finds it useful!

@vitaut vitaut closed this as completed Dec 9, 2019
@tesch1
Copy link
Author

tesch1 commented Dec 11, 2019

Ugh, of course that has a bug in it -- here's a link instead for future passers-by : https://gitlab.com/tesch1/cppduals/blob/master/duals/dual#L1276 at least they'll land somewhere close to a more tested version of the above.

Just a side note, in the above, I did a little hacky re-parsing of the fmt string when inheriting from a standard formatter: it would have been nice if the specs_ member was protected rather than private, to allow reuse of parse() from custom formatters that subclass formatter<> of a basic type.

On the accusation of niche -- I have to defend std::complex's honor a little here :o) a lot of people do use std::complex, probably more than use libfmt, even. A surprising amount of what Feynmann would have called science is done in C++, and I'd wager a limb that a lot of them also dont love the default (a,b) iostreams formatting and wish for something a little easier on the eyes.

I apologize for the noise, but this seems like a good place to more thoroughly collect some complex number formatting options. A brief survey:

language basic format result of sqrt(-1) result of sqrt(-1)-sqrt(-1)
C++ iostreams (3,4) (0,1) (0,0)
numpy (3+4j) 1j 0j
julia 3.0 + 4.0im 0.0 + 1.0im 0.0 + 0.0im
octave 3 + 4i 0 + 1i 0
mathematica* 1+i i 0
R (3+4i) (0+1i) (0+0i)
c++14 literals,float 3+4if 1if 0if
go (3+4i) (0+1i) (0+0i)

* - checked via wolframalpha

The variables :

  • default number format (I think inheriting the value_type's default format is the right thing)
  • whether to include parenthesis
    a) always (R) or
    b) only when both re and im parts are non-zero (numpy) or
    c) never
  • the amount of space around the +
  • the form of the imaginary unit: j, i, i, im
  • zero complex number should be 0, 0j, or 0+0j, mutually-exclusively:
    • if the real part should be omitted if it's zero,
    • if the imag part should be omitted if it's zero
  • whether the imaginary unit can be concatenated to indicate product (didn't include an example above, but it's true for a language i'm using) ie, (3+4*i)
  • whether there is a precision suffix, to be compatible with the std::complex string literals (float,double,long double) -> (1if, 1i, 1il)
    -> whether -0.0 and 0.0 are displayed as the same object

edit: clarify that c++ meant iostreams
edit: add go to survey table

@vitaut
Copy link
Contributor

vitaut commented Dec 12, 2019

a lot of them also dont love the default (a,b) iostreams formatting and wish for something a little easier on the eyes.

P1636 didn't make it into C++20, so there is an opportunity to improve the default formatting for complex in std::format. Since you've already done some research, would you be willing to write a small ISO C++ proposal to do that? I'll be happy to review and champion it at the ISO C++ standards committee meeting if you are unable to attend yourself.

@tesch1
Copy link
Author

tesch1 commented Dec 12, 2019

Yeah, would be glad to. I guess if I complain I should be willing to try to improve things too.

Another data point: python's discussion on the issue: https://bugs.python.org/issue1588

Although, I think they screwed it up a bit too, from a correctness perspective. make incorrect assumptions from time to time.

edit: an ounce of humility

@vitaut
Copy link
Contributor

vitaut commented Jun 23, 2020

Hey @tesch1, are you still interested in writing a proposal?

@tesch1
Copy link
Author

tesch1 commented Jun 24, 2020

hey, yeah, theoretically I am.. practically I dont have time. I started on it a bit here: https://github.com/tesch1/complex_fmt_scn if someone wants to take over that's perfectly ok w/ me.

@vitaut
Copy link
Contributor

vitaut commented Jun 25, 2020

I think the paper already contains all the important pieces. We don't really need a wording for the first revision. I can add examples, submit and champion it on your behalf.

@tesch1
Copy link
Author

tesch1 commented Jun 26, 2020

Sounds great to me!

@vitaut vitaut reopened this Jun 26, 2020
@sehe
Copy link

sehe commented Jul 2, 2020

@tesch1 Thanks for the legwork. I ran into a snag, ASan throws a fit + segtault unless I test the it before dereferencing:

    auto it = ctx.begin();
    if (it != ctx.end()) {

Repro:

#include <complex>
#include <vector>
#include <numeric>
#include <fmt/ranges.h>

template <typename T, typename Char>
struct fmt::formatter<std::complex<T>, Char> : public fmt::formatter<T, Char> {
    typedef fmt::formatter<T, Char> base;
    enum style { expr, star, pair } style_ = expr;
    internal::dynamic_format_specs<Char> specs_;
    constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
        using handler_type =
            internal::dynamic_specs_handler<format_parse_context>;
        auto type = internal::type_constant<T, Char>::value;
        internal::specs_checker<handler_type> handler(handler_type(specs_, ctx),
                                                      type);
        auto it = ctx.begin();
        /*if (it != ctx.end())*/ { // uncomment to remove SEGV
            switch (*it) {
                case '$': style_ = style::expr; ctx.advance_to(++it); break;
                case '*': style_ = style::star; ctx.advance_to(++it); break;
                case ',': style_ = style::pair; ctx.advance_to(++it); break;
                default: break;
            }
        }
        if (style_ != style::pair)
            parse_format_specs(ctx.begin(), ctx.end(), handler);
        // todo: fixup alignment
        return base::parse(ctx);
    }
    template <typename FormatCtx>
    auto format(const std::complex<T>& x, FormatCtx& ctx)
        -> decltype(ctx.out()) {
        format_to(ctx.out(), "(");
        if (x.real() || !x.imag())
            base::format(x.real(), ctx);
        if (x.imag()) {
            if (style_ == style::pair)
                format_to(ctx.out(), ",");
            else if (x.real() && x.imag() >= 0 && specs_.sign != sign::plus)
                format_to(ctx.out(), "+");
            base::format(x.imag(), ctx);
            if (style_ != style::pair) {
                if (style_ == style::star)
                    format_to(ctx.out(), "*i");
                else
                    format_to(ctx.out(), "i");
                if (std::is_same<typename std::decay<T>::type, float>::value)
                    format_to(ctx.out(), "f");
                if (std::is_same<typename std::decay<T>::type,
                                 long double>::value)
                    format_to(ctx.out(), "l");
            }
        }
        return format_to(ctx.out(), ")");
    }
};

int main() {
    using V = std::complex<double>;
    std::vector<V> v{ { 1, 1 }, { 2, 2 } };
    auto r = std::accumulate(v.begin(), v.end(), V{});

    fmt::print("Sum({}) -> {}\n", v, r);
}

@vitaut
Copy link
Contributor

vitaut commented Aug 3, 2020

I made minor corrections, added examples and rudimentary wording: https://fmt.dev/papers/d2197r0.html. If there are no objections I'll submit this to the next mailing.

@tesch1
Copy link
Author

tesch1 commented Aug 8, 2020

lgtm

@vitaut vitaut closed this as completed Aug 22, 2020
@orlandini
Copy link

@vitaut I was quite interested in the paper you've cited, however the link is dead. Would you be able to provide it for me? I am interested in using fmt in a library for numerical analysis and printing complex numbers with as much precision as possible would be super helpful.

@mwinterb
Copy link
Contributor

mwinterb commented Jan 28, 2022

@vitaut I was quite interested in the paper you've cited, however the link is dead. Would you be able to provide it for me? I am interested in using fmt in a library for numerical analysis and printing complex numbers with as much precision as possible would be super helpful.

It's probably this one:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2197r0.html

@orlandini
Copy link

@mwinterb please correct me should I be wrong, but there is no explicit implementation of

template <typename T, typename Char>
struct fmt::formatter<std::complex<T>, Char> 

in this paper, is there?

@tesch1
Copy link
Author

tesch1 commented Jan 29, 2022

@orlandini correct - the paper just defines the problem and the proposed interface - not the implementation.

There is an implementation (not necessarily coherent with the paper!) here https://gitlab.com/tesch1/cppduals/blob/master/duals/dual#L1379-1452 with some rough tests here https://gitlab.com/tesch1/cppduals/-/blob/master/tests/test_fmt.cpp#L40-151 (line numbers may shift as that library changes). I'm more or less passively maintaining this - it could certainly be improved and revised along with the paper towards something more perfect and beautiful; I can chip in, but I dont currently have Copious Free Time(TM) to push it forward.

@orlandini
Copy link

@tesch1 thanks a lot for the attention! I will take a look and see what I can do.

@peter-urban
Copy link

Hi all,

I found the code to implement a formatter for complex numbers very usefull. Unfortunately it does not compile anymore with fmt 10.0.0. (internal::dynamic_specs_handler does not exist anymore, just to start with)

I don't understand the code, nor fmt's internals well enough to fix this myself. Is anybody here able to provide an updated code snipped? (Or help me otherwise) :-)

Cheers

@vitaut
Copy link
Contributor

vitaut commented Jun 17, 2023

You can use existing formatters such as

fmt/include/fmt/core.h

Lines 2662 to 2664 in dd17f89

struct formatter<T, Char,
enable_if_t<detail::type_constant<T, Char>::value !=
detail::type::custom_type>> {
as an example how to handle dynamic format specs.

@peter-urban
Copy link

@vitaut thanks. I managed modify the code (https://gitlab.com/tesch1/cppduals/blob/master/duals/dual#L1331) such that it compiles and does what I want (at least it seems like this)

In case anybody finds this useful:

template<typename T, typename Char>
struct fmt::formatter<std::complex<T>, Char> : public fmt::formatter<T,Char>
{
  private:
    typedef fmt::formatter<T, Char> base;
    enum style
    {
        expr,
        star,
        pair
    } style_ = expr;

    detail::dynamic_format_specs<Char> specs_;

  public:
    template<typename ParseContext>
    FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char*
    {
        auto it = ctx.begin();
        if (it != ctx.end())
        {
            switch (*it)
            {
                case '$':
                    style_ = style::expr;
                    ctx.advance_to(++it);
                    break;
                case '*':
                    style_ = style::star;
                    ctx.advance_to(++it);
                    break;
                case ',':
                    style_ = style::pair;
                    ctx.advance_to(++it);
                    break;
                default:
                    break;
            }
        }

        auto type = detail::type_constant<T, Char>::value;
        auto end  = detail::parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx, type);
        if (type == detail::type::char_type)
            detail::check_char_specs(specs_);
        return end;
    }

    template<typename FormatContext>
    FMT_CONSTEXPR auto format(const std::complex<T>& x, FormatContext& ctx) const
        -> decltype(ctx.out())
    {
        format_to(ctx.out(), "(");
        if (style_ == style::pair)
        {
            base::format(x.real(), ctx);
            format_to(ctx.out(), ",");
            base::format(x.imag(), ctx);
            return format_to(ctx.out(), ")");
        }
        if (x.real() || !x.imag())
            base::format(x.real(), ctx);
        if (x.imag())
        {
            if (x.real() && x.imag() >= 0 && specs_.sign != sign::plus)
                format_to(ctx.out(), "+");
            base::format(x.imag(), ctx);
            if (style_ == style::star)
                format_to(ctx.out(), "*i");
            else
                format_to(ctx.out(), "i");
            if (std::is_same<typename std::decay<T>::type, float>::value)
                format_to(ctx.out(), "f");
            if (std::is_same<typename std::decay<T>::type, long double>::value)
                format_to(ctx.out(), "l");
        }
        return format_to(ctx.out(), ")");
    }
};

Note that I don't actually understand this code... Improvements welcome.

@vitaut
Copy link
Contributor

vitaut commented Mar 20, 2024

The default format for std::complex is now implemented.

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

6 participants