How can I make custom parsing in user defined std::formatter without loosing standard formatting

184 views Asked by At

I am trying to make a std::formatter for a custom type with custom format specifiers. I cannot figure out how to do this without loosing all the standard formatting done in the formatting library.

Here is an example where I format a user type 'type_a' as roman numerals.

#include <iostream>
#include <format>
using namespace std;

struct type_a { int val; };

template<>
struct std::formatter<type_a> : formatter<string_view> {
    using base = formatter<string_view>;

    // How do I take out my special formatting characters and get the rest parsed by std::something?
    constexpr auto parse( format_parse_context & parse_ctx ) {
        auto pos = parse_ctx.begin();
        int bracket_count = 1;
        while( pos != parse_ctx.end() && bracket_count > 0 ) {
            if( *pos == '{' )
                ++bracket_count;
            else if( *pos == '}' )
                --bracket_count;
            else if( *pos == 'M' ) // Output as roman numerals
                roman_numerals = true;
            ++pos;
        }
        while( pos != parse_ctx.end() && bracket_count > 0 ) {
            if( *pos == '{' )
                ++bracket_count;
            else if( *pos == '}' )
                --bracket_count;
            ++pos;
        }
        --pos;
        return pos; // pos points to the last bracket.
    }

    template<template<typename, typename> typename Basic_Format_Context, typename Output_Iterator, typename CharT>
    auto format( const type_a & obj, Basic_Format_Context<Output_Iterator, CharT> & format_ctx ) const {
        auto roman_string = []( int val ) {
            string romans[] = {   "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };
            int values[]    = { 1000,  900, 500,  400, 100,   90,  50,   40,  10,    9,   5,    4,   1  };
            string result;
            for( int i = 0; i < 13; ++i ) {
                while( val - values[i] >= 0 ) {
                    result += romans[i];
                    val    -= values[i];
                }
            }
            return result;
        };
        string str;
        if( roman_numerals )
            format_to( back_inserter( str ), "{}", roman_string( obj.val ) );
        else
            format_to( back_inserter( str ), "{}", obj.val );
        return base::format( str, format_ctx );
    }    

    bool roman_numerals = false;
};

int main() {
    // Runtime parsed.
    cout << vformat( "The number '{0:>10}' as roman numerals '{1:>10}' \n", make_format_args( 123, "CXXIII" ) );
    cout << vformat( "The number '{0:>10}' as roman numerals '{0:>10M}' \n", make_format_args( type_a{ 123 } ) );
    // Compile time parsed
    cout <<  format( "The number '{0:>10}' as roman numerals '{0:>10M}' \n", type_a{ 123 } );
}

I am loosing the width I specify and the justification of the output.

How can I make such a formatter and keep all the standard formatting?

https://godbolt.org/z/nGcT3aKPc

Thanks in advance.

1

There are 1 answers

0
康桓瑋 On

How can I make such a formatter and keep all the standard formatting?

I think you can put the Roman numeral specifier before the standard spec and set roman_numerals by detecting its presence.

This allows you to reuse the standard formatter. Take the indicator "roman" as an example:

template<>
struct std::formatter<type_a> {
  std::formatter<string_view> underlying;
  bool roman_numerals = false;

  constexpr auto parse(std::format_parse_context& parse_ctx) {
    if (std::string_view(parse_ctx).starts_with("roman")) {
      roman_numerals = true;
      parse_ctx.advance_to(parse_ctx.begin() + 5);
    }
    return underlying.parse(parse_ctx);
  }
   
  auto format(const type_a& obj, std::format_context& format_ctx) const {
    auto roman_string = [](int val) { /* implementation */ };
    auto str = roman_numerals ? roman_string(obj.val)
                              : std::to_string(obj.val);
    return underlying.format(str, format_ctx);
  }
};

Then you can use it like

std::cout << std::format("The number '{0:>10}' as roman numerals '{1:roman>10}' \n", 123, type_a{123});

Demo