libfly  6.2.2
C++20 utility library for Linux, macOS, and Windows
format_specifier.hpp
1 #pragma once
2 
3 #include "fly/concepts/concepts.hpp"
4 #include "fly/types/string/concepts.hpp"
5 #include "fly/types/string/detail/classifier.hpp"
6 #include "fly/types/string/detail/format_parameter_type.hpp"
7 #include "fly/types/string/detail/format_parse_context.hpp"
8 #include "fly/types/string/lexer.hpp"
9 #include "fly/types/string/literals.hpp"
10 
11 #include <array>
12 #include <cstddef>
13 #include <optional>
14 #include <string>
15 #include <type_traits>
16 
17 namespace fly::detail {
18 
114 template <fly::StandardCharacter CharType>
116 {
118 
119  BasicFormatSpecifier() = default;
120 
127  explicit constexpr BasicFormatSpecifier(FormatParseContext &context);
128 
129  BasicFormatSpecifier(const BasicFormatSpecifier &) = default;
131 
132  BasicFormatSpecifier &operator=(const BasicFormatSpecifier &) = default;
133  BasicFormatSpecifier &operator=(BasicFormatSpecifier &&) = default;
134 
135  enum class Alignment : std::uint8_t
136  {
137  Default,
138  Left,
139  Right,
140  Center,
141  };
142 
143  enum class Sign : std::uint8_t
144  {
145  Default,
146  Always,
147  NegativeOnly,
148  NegativeOnlyWithPositivePadding,
149  };
150 
151  // For runtime convenience, this enumeration is valued such that binary, octal, decimal, and
152  // hexadecimal presentation types correspond to their base (2, 8, 10, and 16, respectively).
153  enum class Type : std::uint8_t
154  {
155  None = 20,
156  Character = 21,
157  String = 22,
158  Pointer = 23,
159  Binary = 2,
160  Octal = 8,
161  Decimal = 10,
162  Hex = 16,
163  HexFloat = 24,
164  Scientific = 25,
165  Fixed = 26,
166  General = 27,
167  };
168 
169  enum class Case : std::uint8_t
170  {
171  Lower,
172  Upper,
173  };
174 
182  constexpr void parse(FormatParseContext &context);
183 
197  template <typename FormatContext>
198  std::size_t width(FormatContext &context, std::size_t fallback) const;
199 
213  template <typename FormatContext>
214  std::size_t precision(FormatContext &context, std::size_t fallback) const;
215 
222  template <fly::DerivedFrom<BasicFormatSpecifier> FormatterType>
223  constexpr void copy_formatting_options_into(FormatterType &formatter) const
224  {
225  // Note: This is defined inline due to: https://bugs.llvm.org/show_bug.cgi?id=48020
226  formatter.m_position = m_position;
227 
228  formatter.m_fill = m_fill;
229  formatter.m_alignment = m_alignment;
230 
231  formatter.m_sign = m_sign;
232  formatter.m_alternate_form = m_alternate_form;
233  formatter.m_zero_padding = m_zero_padding;
234 
235  formatter.m_width = m_width;
236  formatter.m_width_position = m_width_position;
237 
238  formatter.m_precision = m_precision;
239  formatter.m_precision_position = m_precision_position;
240 
241  formatter.m_locale_specific_form = m_locale_specific_form;
242 
243  formatter.m_type = m_type;
244  formatter.m_case = m_case;
245 
246  formatter.m_was_parsed_as_standard_formatter = m_was_parsed_as_standard_formatter;
247  }
248 
254  template <typename T>
255  friend bool operator==(
256  const BasicFormatSpecifier<T> &specifier1,
257  const BasicFormatSpecifier<T> &specifier2);
258 
259  std::size_t m_position {0};
260 
261  std::optional<CharType> m_fill {std::nullopt};
262  Alignment m_alignment {Alignment::Default};
263 
264  Sign m_sign {Sign::Default};
265  bool m_alternate_form {false};
266  bool m_zero_padding {false};
267 
268  std::optional<std::size_t> m_width {std::nullopt};
269  std::optional<std::size_t> m_width_position {std::nullopt};
270 
271  std::optional<std::size_t> m_precision {std::nullopt};
272  std::optional<std::size_t> m_precision_position {std::nullopt};
273 
274  bool m_locale_specific_form {false};
275 
276  Type m_type {Type::None};
277  Case m_case {Case::Lower};
278 
279  std::optional<ParameterType> m_parameter_type {std::nullopt};
280 
281  std::size_t m_parse_index {0};
282  std::size_t m_size {0};
283 
284  bool m_was_parsed_as_standard_formatter {false};
285 
286 private:
295  constexpr void parse_fill_and_alignment(FormatParseContext &context);
296 
302  constexpr void parse_sign(FormatParseContext &context);
303 
318  constexpr void parse_alternate_form_and_zero_padding(FormatParseContext &context);
319 
327  constexpr void parse_width(FormatParseContext &context);
328 
340  constexpr void parse_precision(FormatParseContext &context);
341 
347  constexpr std::optional<BasicFormatSpecifier>
348  parse_nested_specifier(FormatParseContext &context);
349 
358  constexpr void parse_locale_specific_form(FormatParseContext &context);
359 
369  constexpr void parse_type(FormatParseContext &context);
370 
377  constexpr void infer_type(FormatParseContext &context);
378 
385  constexpr void validate(FormatParseContext &context);
386 
394  constexpr void validate_type(FormatParseContext &context, ParameterType parameter_type);
395 
407  template <typename FormatContext>
408  static std::optional<std::size_t> resolve(FormatContext &context, std::size_t position);
409 
418  constexpr std::optional<ParameterType> resolve_parameter_type(FormatParseContext &context);
419 
427  static constexpr std::optional<Type> type_of(CharType ch);
428 
432  constexpr bool is_numeric() const;
433 
434  static constexpr std::array<std::pair<CharType, Type>, 17> s_type_map {{
435  {FLY_CHR(CharType, 'c'), Type::Character},
436  {FLY_CHR(CharType, 's'), Type::String},
437  {FLY_CHR(CharType, 'p'), Type::Pointer},
438  {FLY_CHR(CharType, 'b'), Type::Binary},
439  {FLY_CHR(CharType, 'B'), Type::Binary},
440  {FLY_CHR(CharType, 'o'), Type::Octal},
441  {FLY_CHR(CharType, 'd'), Type::Decimal},
442  {FLY_CHR(CharType, 'x'), Type::Hex},
443  {FLY_CHR(CharType, 'X'), Type::Hex},
444  {FLY_CHR(CharType, 'a'), Type::HexFloat},
445  {FLY_CHR(CharType, 'A'), Type::HexFloat},
446  {FLY_CHR(CharType, 'e'), Type::Scientific},
447  {FLY_CHR(CharType, 'E'), Type::Scientific},
448  {FLY_CHR(CharType, 'f'), Type::Fixed},
449  {FLY_CHR(CharType, 'F'), Type::Fixed},
450  {FLY_CHR(CharType, 'g'), Type::General},
451  {FLY_CHR(CharType, 'G'), Type::General},
452  }};
453 
454  static constexpr const auto s_left_brace = FLY_CHR(CharType, '{');
455  static constexpr const auto s_right_brace = FLY_CHR(CharType, '}');
456  static constexpr const auto s_less_than_sign = FLY_CHR(CharType, '<');
457  static constexpr const auto s_greater_than_sign = FLY_CHR(CharType, '>');
458  static constexpr const auto s_caret = FLY_CHR(CharType, '^');
459  static constexpr const auto s_plus_sign = FLY_CHR(CharType, '+');
460  static constexpr const auto s_minus_sign = FLY_CHR(CharType, '-');
461  static constexpr const auto s_space = FLY_CHR(CharType, ' ');
462  static constexpr const auto s_number_sign = FLY_CHR(CharType, '#');
463  static constexpr const auto s_zero = FLY_CHR(CharType, '0');
464  static constexpr const auto s_letter_l = FLY_CHR(CharType, 'L');
465  static constexpr const auto s_decimal = FLY_CHR(CharType, '.');
466 };
467 
474 #define FLY_DEFINE_FORMATTER(CharType, parameter_type) \
475  using FormatSpecifier = fly::detail::BasicFormatSpecifier<CharType>; \
476  using FormatSpecifier::m_position; \
477  using FormatSpecifier::m_fill; \
478  using FormatSpecifier::m_alignment; \
479  using FormatSpecifier::m_sign; \
480  using FormatSpecifier::m_alternate_form; \
481  using FormatSpecifier::m_zero_padding; \
482  using FormatSpecifier::m_width; \
483  using FormatSpecifier::m_width_position; \
484  using FormatSpecifier::m_precision; \
485  using FormatSpecifier::m_precision_position; \
486  using FormatSpecifier::m_locale_specific_form; \
487  using FormatSpecifier::m_type; \
488  using FormatSpecifier::m_case; \
489  using FormatSpecifier::m_parameter_type; \
490  \
491  Formatter() \
492  { \
493  m_parameter_type = parameter_type; \
494  } \
495  \
496  explicit Formatter(FormatSpecifier specifier) noexcept : FormatSpecifier(std::move(specifier)) \
497  { \
498  }
499 
500 //==================================================================================================
501 template <fly::StandardCharacter CharType>
503  m_position(context.next_position())
504 {
505  infer_type(context);
506 }
507 
508 //==================================================================================================
509 template <fly::StandardCharacter CharType>
511 {
512  m_was_parsed_as_standard_formatter = true;
513 
514  parse_fill_and_alignment(context);
515  parse_sign(context);
516  parse_alternate_form_and_zero_padding(context);
517  parse_width(context);
518  parse_precision(context);
519  parse_locale_specific_form(context);
520  parse_type(context);
521 
522  validate(context);
523 
524  if (!context.has_error() && !context.lexer().consume_if(s_right_brace))
525  {
526  context.on_error("Detected unclosed replacement field - must end with }");
527  }
528 }
529 
530 //==================================================================================================
531 template <fly::StandardCharacter CharType>
532 constexpr void BasicFormatSpecifier<CharType>::parse_fill_and_alignment(FormatParseContext &context)
533 {
534  if (auto next = context.lexer().peek(1); next)
535  {
536  if ((*next == s_less_than_sign) || (*next == s_greater_than_sign) || (*next == s_caret))
537  {
538  m_fill = context.lexer().consume().value();
539  }
540  }
541 
542  if (context.lexer().consume_if(s_less_than_sign))
543  {
544  m_alignment = Alignment::Left;
545  }
546  else if (context.lexer().consume_if(s_greater_than_sign))
547  {
548  m_alignment = Alignment::Right;
549  }
550  else if (context.lexer().consume_if(s_caret))
551  {
552  m_alignment = Alignment::Center;
553  }
554 }
555 
556 //==================================================================================================
557 template <fly::StandardCharacter CharType>
558 constexpr void BasicFormatSpecifier<CharType>::parse_sign(FormatParseContext &context)
559 {
560  if (context.lexer().consume_if(s_plus_sign))
561  {
562  m_sign = Sign::Always;
563  }
564  else if (context.lexer().consume_if(s_minus_sign))
565  {
566  m_sign = Sign::NegativeOnly;
567  }
568  else if (context.lexer().consume_if(s_space))
569  {
570  m_sign = Sign::NegativeOnlyWithPositivePadding;
571  }
572 }
573 
574 //==================================================================================================
575 template <fly::StandardCharacter CharType>
576 constexpr void
577 BasicFormatSpecifier<CharType>::parse_alternate_form_and_zero_padding(FormatParseContext &context)
578 {
579  if (context.lexer().consume_if(s_number_sign))
580  {
581  m_alternate_form = true;
582  }
583 
584  if (context.lexer().consume_if(s_zero) && (m_alignment == Alignment::Default))
585  {
586  m_zero_padding = true;
587  }
588 }
589 
590 //==================================================================================================
591 template <fly::StandardCharacter CharType>
592 constexpr void BasicFormatSpecifier<CharType>::parse_width(FormatParseContext &context)
593 {
594  if (auto width = context.lexer().consume_number(); width)
595  {
596  m_width = static_cast<std::size_t>(*width);
597  }
598  else if (context.lexer().consume_if(s_left_brace))
599  {
600  if (auto nested = parse_nested_specifier(context); nested)
601  {
602  m_width_position = nested->m_position;
603  }
604  }
605 }
606 
607 //==================================================================================================
608 template <fly::StandardCharacter CharType>
609 constexpr void BasicFormatSpecifier<CharType>::parse_precision(FormatParseContext &context)
610 {
611  if (context.lexer().consume_if(s_decimal))
612  {
613  if (auto precision = context.lexer().consume_number(); precision)
614  {
615  m_precision = static_cast<std::size_t>(*precision);
616  }
617  else if (context.lexer().consume_if(s_left_brace))
618  {
619  if (auto nested = parse_nested_specifier(context); nested)
620  {
621  m_precision_position = nested->m_position;
622  }
623  }
624  else
625  {
626  context.on_error(
627  "Expected a non-negative precision or nested replacement field after decimal");
628  }
629  }
630 }
631 
632 //==================================================================================================
633 template <fly::StandardCharacter CharType>
634 constexpr auto BasicFormatSpecifier<CharType>::parse_nested_specifier(FormatParseContext &context)
635  -> std::optional<BasicFormatSpecifier>
636 {
637  // The opening { will have already been consumed, so the starting position is one less.
638  const auto starting_position = context.lexer().position() - 1;
639 
640  BasicFormatSpecifier specifier {};
641  specifier.m_position = context.next_position();
642 
643  if (auto parameter_type = context.parameter_type(specifier.m_position); parameter_type)
644  {
645  specifier.infer_type(context);
646  }
647 
648  if (!context.lexer().consume_if(s_right_brace))
649  {
650  context.on_error("Detected unclosed replacement field - must end with }");
651  return std::nullopt;
652  }
653 
654  specifier.m_size = context.lexer().position() - starting_position;
655  return specifier;
656 }
657 
658 //==================================================================================================
659 template <fly::StandardCharacter CharType>
660 constexpr void
661 BasicFormatSpecifier<CharType>::parse_locale_specific_form(FormatParseContext &context)
662 {
663  if (context.lexer().consume_if(s_letter_l))
664  {
665  m_locale_specific_form = true;
666  }
667 }
668 
669 //==================================================================================================
670 template <fly::StandardCharacter CharType>
671 constexpr void BasicFormatSpecifier<CharType>::parse_type(FormatParseContext &context)
672 {
673  if (auto ch = context.lexer().peek(); ch)
674  {
675  if (auto type = type_of(ch.value()); type)
676  {
677  m_type = type.value();
678  context.lexer().consume();
679 
680  if (BasicClassifier<CharType>::is_upper(ch.value()))
681  {
682  m_case = Case::Upper;
683  }
684  }
685  }
686 }
687 
688 //==================================================================================================
689 template <fly::StandardCharacter CharType>
690 constexpr void BasicFormatSpecifier<CharType>::infer_type(FormatParseContext &context)
691 {
692  auto parameter_type = resolve_parameter_type(context);
693 
694  if (parameter_type == ParameterType::Character)
695  {
696  m_type = Type::Character;
697  }
698  else if (parameter_type == ParameterType::String)
699  {
700  m_type = Type::String;
701  }
702  else if (parameter_type == ParameterType::Pointer)
703  {
704  m_type = Type::Pointer;
705  }
706  else if (parameter_type == ParameterType::Integral)
707  {
708  m_type = Type::Decimal;
709  }
710  else if (parameter_type == ParameterType::FloatingPoint)
711  {
712  m_type = Type::General;
713  }
714  else if (parameter_type == ParameterType::Boolean)
715  {
716  m_type = Type::String;
717  }
718 }
719 
720 //==================================================================================================
721 template <fly::StandardCharacter CharType>
722 constexpr void BasicFormatSpecifier<CharType>::validate(FormatParseContext &context)
723 {
724  auto parameter_type = resolve_parameter_type(context);
725 
726  // Validate the fill character.
727  if (m_fill && ((m_fill == s_left_brace) || (m_fill == s_right_brace)))
728  {
729  context.on_error("Characters { and } are not allowed as fill characters");
730  }
731  else if (m_fill && (static_cast<std::make_unsigned_t<CharType>>(*m_fill) >= 0x80))
732  {
733  context.on_error("Non-ascii characters are not allowed as fill characters");
734  }
735 
736  // Validate the sign.
737  if ((m_sign != Sign::Default) && !is_numeric())
738  {
739  context.on_error("Sign may only be used with numeric presentation types");
740  }
741 
742  // Validate the alternate form.
743  if (m_alternate_form && (!is_numeric() || (m_type == Type::Decimal)))
744  {
745  context.on_error(
746  "Alternate form may only be used with non-decimal numeric presentation types");
747  }
748 
749  // Validate the zero-padding option.
750  if (m_zero_padding && !is_numeric())
751  {
752  context.on_error("Zero-padding may only be used with numeric presentation types");
753  }
754 
755  // Validate the width value.
756  if (m_width && (*m_width == 0))
757  {
758  context.on_error("Width must be a positive (non-zero) value");
759  }
760  else if (m_width_position)
761  {
762  if (context.parameter_type(*m_width_position) != ParameterType::Integral)
763  {
764  context.on_error("Position of width parameter must be an integral type");
765  }
766  }
767 
768  // Validate the precision value.
769  if (m_precision || m_precision_position)
770  {
771  if ((parameter_type != ParameterType::String) &&
772  (parameter_type != ParameterType::FloatingPoint))
773  {
774  context.on_error("Precision may only be used for string and floating-point types");
775  }
776  else if (m_precision_position)
777  {
778  if (context.parameter_type(*m_precision_position) != ParameterType::Integral)
779  {
780  context.on_error("Position of precision parameter must be an integral type");
781  }
782  }
783  }
784 
785  // Validate the locale-specifc form.
786  if (m_locale_specific_form &&
787  ((parameter_type != ParameterType::Integral) &&
788  (parameter_type != ParameterType::FloatingPoint) &&
789  (parameter_type != ParameterType::Boolean)))
790  {
791  context.on_error("Locale-specific form may only be used for numeric and boolean types");
792  }
793 
794  // Validate the presentation type.
795  if (parameter_type && (m_type != Type::None))
796  {
797  validate_type(context, *parameter_type);
798  }
799 }
800 
801 //==================================================================================================
802 template <fly::StandardCharacter CharType>
803 constexpr void BasicFormatSpecifier<CharType>::validate_type(
804  FormatParseContext &context,
805  ParameterType parameter_type)
806 {
807  if (parameter_type == ParameterType::Character)
808  {
809  if ((m_type != Type::Character) && (m_type != Type::Binary) && (m_type != Type::Octal) &&
810  (m_type != Type::Decimal) && (m_type != Type::Hex))
811  {
812  context.on_error("Character types must be formatted with {} or {:cbBodxX}");
813  }
814  }
815  else if (parameter_type == ParameterType::String)
816  {
817  if (m_type != Type::String)
818  {
819  context.on_error("String types must be formatted with {} or {:s}");
820  }
821  }
822  else if (parameter_type == ParameterType::Pointer)
823  {
824  if (m_type != Type::Pointer)
825  {
826  context.on_error("Pointer types must be formatted with {} or {:p}");
827  }
828  }
829  else if (parameter_type == ParameterType::Integral)
830  {
831  if ((m_type != Type::Character) && (m_type != Type::Binary) && (m_type != Type::Octal) &&
832  (m_type != Type::Decimal) && (m_type != Type::Hex))
833  {
834  context.on_error("Integral types must be formatted with {} or one of {:cbBodxX}");
835  }
836  }
837  else if (parameter_type == ParameterType::FloatingPoint)
838  {
839  if ((m_type != Type::HexFloat) && (m_type != Type::Scientific) && (m_type != Type::Fixed) &&
840  (m_type != Type::General))
841  {
842  context.on_error(
843  "Floating-point types must be formatted with {} or one of {:aAeEfFgG}");
844  }
845  }
846  else if (parameter_type == ParameterType::Boolean)
847  {
848  if ((m_type != Type::Character) && (m_type != Type::String) && (m_type != Type::Binary) &&
849  (m_type != Type::Octal) && (m_type != Type::Decimal) && (m_type != Type::Hex))
850  {
851  context.on_error("Boolean types must be formatted with {} or one of {:csbBodxX}");
852  }
853  }
854 }
855 
856 //==================================================================================================
857 template <fly::StandardCharacter CharType>
858 template <typename FormatContext>
859 inline std::size_t
860 BasicFormatSpecifier<CharType>::width(FormatContext &context, std::size_t fallback) const
861 {
862  if (m_width_position)
863  {
864  return resolve(context, *m_width_position).value_or(fallback);
865  }
866 
867  return m_width.value_or(fallback);
868 }
869 
870 //==================================================================================================
871 template <fly::StandardCharacter CharType>
872 template <typename FormatContext>
873 inline std::size_t
874 BasicFormatSpecifier<CharType>::precision(FormatContext &context, std::size_t fallback) const
875 {
876  if (m_precision_position)
877  {
878  return resolve(context, *m_precision_position).value_or(fallback);
879  }
880 
881  return m_precision.value_or(fallback);
882 }
883 
884 //==================================================================================================
885 template <fly::StandardCharacter CharType>
886 template <typename FormatContext>
887 inline std::optional<std::size_t>
888 BasicFormatSpecifier<CharType>::resolve(FormatContext &context, std::size_t position)
889 {
890  return context.arg(position).visit([](auto value) -> std::optional<std::size_t> {
891  using T = std::remove_cvref_t<decltype(value)>;
892  std::optional<std::size_t> resolved;
893 
894  if constexpr (fly::UnsignedIntegral<T>)
895  {
896  resolved = static_cast<std::size_t>(value);
897  }
898  else if constexpr (fly::SignedIntegral<T>)
899  {
900  if (value >= 0)
901  {
902  resolved = static_cast<std::size_t>(value);
903  }
904  }
905 
906  return resolved;
907  });
908 }
909 
910 //==================================================================================================
911 template <fly::StandardCharacter CharType>
912 constexpr std::optional<ParameterType>
913 BasicFormatSpecifier<CharType>::resolve_parameter_type(FormatParseContext &context)
914 {
915  if (m_parameter_type)
916  {
917  return *m_parameter_type;
918  }
919 
920  return context.parameter_type(m_position);
921 }
922 
923 //==================================================================================================
924 template <fly::StandardCharacter CharType>
925 constexpr auto BasicFormatSpecifier<CharType>::type_of(CharType ch) -> std::optional<Type>
926 {
927  auto it = std::find_if(s_type_map.begin(), s_type_map.end(), [&ch](const auto &item) {
928  return item.first == ch;
929  });
930 
931  if (it == s_type_map.end())
932  {
933  return std::nullopt;
934  }
935 
936  return it->second;
937 }
938 
939 //==================================================================================================
940 template <fly::StandardCharacter CharType>
941 constexpr bool BasicFormatSpecifier<CharType>::is_numeric() const
942 {
943  switch (m_type)
944  {
945  case Type::Binary:
946  case Type::Octal:
947  case Type::Decimal:
948  case Type::Hex:
949  case Type::HexFloat:
950  case Type::Scientific:
951  case Type::Fixed:
952  case Type::General:
953  return true;
954 
955  default:
956  return false;
957  }
958 }
959 
960 //==================================================================================================
961 template <typename T>
962 bool operator==(
963  const BasicFormatSpecifier<T> &specifier1,
964  const BasicFormatSpecifier<T> &specifier2)
965 {
966  return (specifier1.m_position == specifier2.m_position) &&
967  (specifier1.m_fill == specifier2.m_fill) &&
968  (specifier1.m_alignment == specifier2.m_alignment) &&
969  (specifier1.m_sign == specifier2.m_sign) &&
970  (specifier1.m_alternate_form == specifier2.m_alternate_form) &&
971  (specifier1.m_zero_padding == specifier2.m_zero_padding) &&
972  (specifier1.m_width == specifier2.m_width) &&
973  (specifier1.m_width_position == specifier2.m_width_position) &&
974  (specifier1.m_precision == specifier2.m_precision) &&
975  (specifier1.m_precision_position == specifier2.m_precision_position) &&
976  (specifier1.m_locale_specific_form == specifier2.m_locale_specific_form) &&
977  (specifier1.m_type == specifier2.m_type) && (specifier1.m_case == specifier2.m_case);
978 }
979 
980 } // namespace fly::detail
Definition: string.hpp:51
static constexpr bool is_upper(CharType ch)
Definition: classifier.hpp:223
Definition: format_parse_context.hpp:22
constexpr bool has_error() const
Definition: format_parse_context.hpp:193
void on_error(const char *error)
Definition: format_parse_context.hpp:186
constexpr fly::BasicLexer< CharType > & lexer()
Definition: format_parse_context.hpp:172
Definition: format_specifier.hpp:116
std::size_t precision(FormatContext &context, std::size_t fallback) const
Definition: format_specifier.hpp:874
std::size_t width(FormatContext &context, std::size_t fallback) const
Definition: format_specifier.hpp:860
friend bool operator==(const BasicFormatSpecifier< T > &specifier1, const BasicFormatSpecifier< T > &specifier2)
Definition: format_specifier.hpp:962
constexpr void parse(FormatParseContext &context)
Definition: format_specifier.hpp:510
constexpr void copy_formatting_options_into(FormatterType &formatter) const
Definition: format_specifier.hpp:223