[PATCH v3] libstdc++: Fixed localized empty-spec formatting for months/weekdays [PR121154]

Jonathan Wakely jwakely@redhat.com
Fri Jul 18 13:04:16 GMT 2025


On 18/07/25 14:01 +0200, Tomasz Kamiński wrote:
>Previously for localized output, if _M_debug option was send, the _M_check_ok
>completed succesfully and _M_locale_fmt was called for months/weekdays that
>are !ok().
>
>This patch lifts debug checks from each conversion function into _M_check_ok,
>that in case of !ok() values return a string_view containing the kind of
>calendar data, to be included after "is not a valid" string. The localized
>output (_M_locale_fmt) is not used if string is non-empty.
>The emitting of this message is now handled in _M_format_to, further reducing
>each specifier function.
>
>To handle weekday (%a,%A) and month (%b,%B), _M_check_ok now accepts a
>mutable reference to conversion specifier, and updates it to corresponding
>numeric value (%w, %m). Extra care needs to be taken to handle a month(0)
>that needs to be printed as single digit in debug format.
>
>Finally, the _M_time_point is replaced with _M_needs_ok_check member, that
>indicates if input contains any user-suplied values that are checked for
>being ok() and these values are referenced in chrono-specs.
>
>	PR libstdc++/PR121154
>
>libstdc++-v3/ChangeLog:
>
>	* include/bits/chrono_io.h _ChronoSpec::_M_time_point: Remove.
>	(_ChronoSpec::_M_needs_ok_check): Define
>	(__formatter_chrono::_M_parse): Set _M_needs_ok_check.
>	(__formatter_chrono::_M_check_ok): Check values also for debug mode,
>	and return __string_view.
>	(__formatter_chrono::_M_format_to): Handle results of _M_check_ok.
>	(__formatter_chrono::_M_wi, __formatter_chrono::_M_a_A)
>	(__formatter_chrono::_M_b_B, __formatter_chrono::_M_C_y_Y)
>	(__formatter_chrono::_M_d_e, __formatter_chrono::_M_F):
>	Removed handling of _M_debug.
>	(__formatter_chrono::__M_m): Print zero unpadded in _M_debug mode.
>	(__formatter_duration::_S_spec_for): Remove _M_time_point refernce.
>	(__formatter_duration::_M_parse): Override _M_needs_ok_check.
>	* testsuite/std/time/month/io.cc: Test for localized !ok() values.
>	* testsuite/std/time/weekday/io.cc: Test for localized !ok() values.
>---
>v3 just fixes some typos in comments and commit message.
>
> libstdc++-v3/include/bits/chrono_io.h         | 139 +++++++++++-------
> libstdc++-v3/testsuite/std/time/month/io.cc   |   7 +
> libstdc++-v3/testsuite/std/time/weekday/io.cc |   2 +
> 3 files changed, 96 insertions(+), 52 deletions(-)
>
>diff --git a/libstdc++-v3/include/bits/chrono_io.h b/libstdc++-v3/include/bits/chrono_io.h
>index fe3c9126749..95e0d489cf4 100644
>--- a/libstdc++-v3/include/bits/chrono_io.h
>+++ b/libstdc++-v3/include/bits/chrono_io.h
>@@ -280,8 +280,9 @@ namespace __format
>       // in the format-spec, e.g. "{:L%a}" is localized and locale-specific,
>       // but "{:L}" is only localized and "{:%a}" is only locale-specific.
>       unsigned _M_locale_specific : 1;
>-      // Indicates that we are handling time_point.
>-      unsigned _M_time_point : 1;
>+      // Indicates if parts that are checked for ok are comming directly from input,

"are comming directly from input" -> "come directly from the input"

or "the source" instead of "the input" ? that's what a comment below
says.

OK for trunk with that changed - thanks.


>+      // instead of being computed.
>+      unsigned _M_needs_ok_check : 1;
>       // Indicates that duration should be treated as floating point.
>       unsigned _M_floating_point_rep : 1;
>       // Indicate that duration uses user-defined representation.
>@@ -570,7 +571,15 @@ namespace __format
>
> 	  _ChronoSpec<_CharT> __spec = __def;
>
>-	  auto __finalize = [this, &__spec] {
>+	  auto __finalize = [this, &__spec, &__def] {
>+	    using enum _ChronoParts;
>+            _ChronoParts __checked
>+	      = __spec._M_debug ? _YearMonthDay|_IndexedWeekday
>+	                        : _Month|_Weekday;
>+	    // n.b. for calendar types __def._M_needed contains only parts
>+	    // copied from source, remaining ones are computed, and thus ok
>+	    __spec._M_needs_ok_check
>+	      = __spec._M_needs(__def._M_needed & __checked);
> 	    _M_spec = __spec;
> 	  };
>
>@@ -821,10 +830,10 @@ namespace __format
> 	    __throw_format_error("chrono format error: unescaped '%' in "
> 				 "chrono-specs");
>
>-	  _M_spec = __spec;
>-	  _M_spec._M_chrono_specs
>-		 = __string_view(__chrono_specs, __first - __chrono_specs);
>+	  __spec._M_chrono_specs
>+	    = __string_view(__chrono_specs, __first - __chrono_specs);
>
>+	  __finalize();
> 	  return __first;
> 	}
>
>@@ -972,30 +981,71 @@ namespace __format
> 	  return __out;
> 	}
>
>-      void
>-      _M_check_ok(const _ChronoData<_CharT>& __t, _CharT __conv) const
>+      __string_view
>+      _M_check_ok(const _ChronoData<_CharT>& __t, _CharT& __conv) const
>       {
>-	// n.b. for time point all date parts are computed, so
>-	// they are always ok.
>-	if (_M_spec._M_time_point || _M_spec._M_debug)
>-	  return;
>+	if (!_M_spec._M_debug)
>+	  {
>+	    switch (__conv)
>+	    {
>+	    case 'a':
>+	    case 'A':
>+	      if (!__t._M_weekday.ok()) [[unlikely]]
>+		__throw_format_error("format error: invalid weekday");
>+	      break;
>+	    case 'b':
>+	    case 'h':
>+	    case 'B':
>+	      if (!__t._M_month.ok()) [[unlikely]]
>+		__throw_format_error("format error: invalid month");
>+	      break;
>+	    default:
>+	      break;
>+	    }
>+	    return __string_view();
>+	  }
>
> 	switch (__conv)
> 	{
>+	// %\0 is extension for handling weekday index
>+	case '\0':
>+	  if (__t._M_weekday_index < 1 || __t._M_weekday_index > 5) [[unlikely]]
>+	    return _GLIBCXX_WIDEN("index");
>+	  break;
> 	case 'a':
> 	case 'A':
> 	  if (!__t._M_weekday.ok()) [[unlikely]]
>-	    __throw_format_error("format error: invalid weekday");
>-	  return;
>+	    {
>+	      __conv = 'w'; // print as decimal number
>+	      return _GLIBCXX_WIDEN("weekday");
>+	    }
>+	  break;
> 	case 'b':
> 	case 'h':
> 	case 'B':
> 	  if (!__t._M_month.ok()) [[unlikely]]
>-	    __throw_format_error("format error: invalid month");
>-	  return;
>+	    {
>+	      __conv = 'm'; // print as decimal number
>+	      return _GLIBCXX_WIDEN("month");
>+	    }
>+	  break;
>+	case 'd':
>+	case 'e':
>+	  if (!__t._M_day.ok()) [[unlikely]]
>+	    return _GLIBCXX_WIDEN("day");
>+	  break;
>+	case 'F':
>+	  if (!(__t._M_year/__t._M_month/__t._M_day).ok()) [[unlikely]]
>+	    return _GLIBCXX_WIDEN("date");
>+	  break;
>+	case 'Y':
>+	  if (!__t._M_year.ok()) [[unlikely]]
>+	    return _GLIBCXX_WIDEN("year");
>+	  break;
> 	default:
>-	  return;
>+	  break;
> 	}
>+	return __string_view();
>       }
>
>       template<typename _OutIter, typename _FormatContext>
>@@ -1054,9 +1104,12 @@ namespace __format
> 	  do
> 	    {
> 	      _CharT __c = *__first++;
>-	      _M_check_ok(__t, __c);
>+	      __string_view __invalid;
>+	      if (_M_spec._M_needs_ok_check)
>+		__invalid = _M_check_ok(__t, __c);
>
>-	      if (__use_locale_fmt && _S_localized_spec(__c, __mod)) [[unlikely]]
>+	      if (__invalid.empty() &&__use_locale_fmt
>+		    && _S_localized_spec(__c, __mod)) [[unlikely]]
> 		__out = _M_locale_fmt(std::move(__out), __fc.locale(),
> 				      __tm, __c, __mod);
> 	      else switch (__c)
>@@ -1164,6 +1217,14 @@ namespace __format
> 		  __first = __last;
> 		  break;
> 		}
>+
>+	      if (!__invalid.empty())
>+		{
>+		  constexpr __string_view __pref = _GLIBCXX_WIDEN(" is not a valid ");
>+		  __out = __format::__write(std::move(__out), __pref);
>+		  __out = __format::__write(std::move(__out), __invalid);
>+		}
>+
> 	      __mod = _CharT();
> 	      // Scan for next '%' and write out everything before it.
> 	      __string_view __str(__first, __last - __first);
>@@ -1193,9 +1254,6 @@ namespace __format
> 	  // %\0 Extension to format weekday index, used only by empty format spec
> 	  _CharT __buf[3];
> 	  __out = __format::__write(std::move(__out), _S_str_d1(__buf, __wi));
>-	  if (_M_spec._M_debug && (__wi < 1 || __wi > 5))
>-	    __out = __format::__write(std::move(__out),
>-		      __string_view(_GLIBCXX_WIDEN(" is not a valid index")));
> 	  return std::move(__out);
> 	}
>
>@@ -1205,15 +1263,6 @@ namespace __format
> 	{
> 	  // %a Locale's abbreviated weekday name.
> 	  // %A Locale's full weekday name.
>-	  if (_M_spec._M_debug && !__wd.ok())
>-	    {
>-	      _CharT __buf[3];
>-	      __out = __format::__write(std::move(__out),
>-					_S_str_d1(__buf, __wd.c_encoding()));
>-	      return __format::__write(std::move(__out),
>-			__string_view(_GLIBCXX_WIDEN(" is not a valid weekday")));
>-	    }
>-
> 	  __string_view __str = _S_weekdays[__wd.c_encoding()];
> 	  if (!__full)
> 	    __str = __str.substr(0, 3);
>@@ -1226,15 +1275,6 @@ namespace __format
> 	{
> 	  // %b Locale's abbreviated month name.
> 	  // %B Locale's full month name.
>-	  if (_M_spec._M_debug && !__m.ok())
>-	    {
>-	      _CharT __buf[3];
>-	      __out = __format::__write(std::move(__out),
>-					_S_str_d1(__buf, (unsigned)__m));
>-	      return __format::__write(std::move(__out),
>-			__string_view(_GLIBCXX_WIDEN(" is not a valid month")));
>-	    }
>-
> 	  __string_view __str = _S_months[(unsigned)__m - 1];
> 	  if (!__full)
> 	    __str = __str.substr(0, 3);
>@@ -1303,10 +1343,6 @@ namespace __format
> 		}
> 	      __out = __format::__write(std::move(__out), __sv);
> 	    }
>-
>-	  if (_M_spec._M_debug && __conv == 'Y' && !__y.ok()) [[unlikely]]
>-	    __out = __format::__write(std::move(__out),
>-		      __string_view(_GLIBCXX_WIDEN(" is not a valid year")));
> 	  return __out;
> 	}
>
>@@ -1365,9 +1401,6 @@ namespace __format
> 	    }
>
> 	  __out = __format::__write(std::move(__out), __sv);
>-	  if (_M_spec._M_debug && !__d.ok()) [[unlikely]]
>-	    __out = __format::__write(std::move(__out),
>-		      __string_view(_GLIBCXX_WIDEN(" is not a valid day")));
> 	  return std::move(__out);
> 	}
>
>@@ -1404,9 +1437,6 @@ namespace __format
> 	      __out = __format::__write(std::move(__out), __sv);
> 	    }
>
>-	  if (_M_spec._M_debug && !(__t._M_year/__t._M_month/__t._M_day).ok())
>-	    __out = __format::__write(std::move(__out),
>-		      __string_view(_GLIBCXX_WIDEN(" is not a valid date")));
> 	  return std::move(__out);
> 	}
>
>@@ -1474,8 +1504,10 @@ namespace __format
> 	{
> 	  // %m  month as a decimal number.
> 	  // %Om Locale's alternative representation.
>-
> 	  auto __i = (unsigned)__m;
>+	  if (__i == 0 && _M_spec._M_debug) [[unlikely]]
>+	    // 0 should not be padded to two digits
>+	    return __format::__write(std::move(__out), _S_digit(0));
>
> 	  _CharT __buf[3];
> 	  return __format::__write(std::move(__out), _S_str_d2(__buf, __i));
>@@ -1852,7 +1884,6 @@ namespace __format
> 	  using enum _ChronoParts;
>
> 	  _ChronoSpec<_CharT> __res{};
>-	  __res._M_time_point = (__parts & _DateTime) == _DateTime;
> 	  __res._M_floating_point_rep = chrono::treat_as_floating_point_v<_Rep>;
> 	  __res._M_custom_rep = !is_arithmetic_v<_Rep>;
> 	  __res._M_prec = chrono::hh_mm_ss<_Duration>::fractional_width;
>@@ -1901,6 +1932,10 @@ namespace __format
>
> 	  auto __res
> 	    = __formatter_chrono<_CharT>::_M_parse(__pc, __parts, __def);
>+	  // n.b. durations do not contain date parts, and for time point all
>+	  // date parts are computed, and they are always ok.
>+	  _M_spec._M_needs_ok_check = false;
>+
> 	  // check for custom floating point durations, if digits of output
> 	  // will contain subseconds, then formatters must support specifying
> 	  // precision.
>diff --git a/libstdc++-v3/testsuite/std/time/month/io.cc b/libstdc++-v3/testsuite/std/time/month/io.cc
>index 99ec0737305..edfa196b71c 100644
>--- a/libstdc++-v3/testsuite/std/time/month/io.cc
>+++ b/libstdc++-v3/testsuite/std/time/month/io.cc
>@@ -24,6 +24,9 @@ test_ostream()
>   ss.imbue(std::locale(ISO_8859(15,fr_FR)));
>   ss << month(1);
>   VERIFY( ss.str() == "janv." );
>+  ss.str("");
>+  ss << month(0) << '|' << month(13);
>+  VERIFY( ss.str() == "0 is not a valid month|13 is not a valid month" );
> }
>
> void
>@@ -66,6 +69,10 @@ test_format()
>   VERIFY( s == "Jan" );
>   s = std::format(loc_fr, "{:L%b}", month(1));
>   VERIFY( s == "janv." );
>+  s = std::format(loc_fr, "{:L}", month(0));
>+  VERIFY( s == "0 is not a valid month" );
>+  s = std::format(loc_fr, "{:L}", month(13));
>+  VERIFY( s == "13 is not a valid month" );
>
>   std::string_view specs = "aAbBcCdDeFgGhHIjmMpqQrRSTuUVwWxXyYzZ";
>   std::string_view my_specs = "bBhm";
>diff --git a/libstdc++-v3/testsuite/std/time/weekday/io.cc b/libstdc++-v3/testsuite/std/time/weekday/io.cc
>index a56cdaef88c..90a9bcbbb62 100644
>--- a/libstdc++-v3/testsuite/std/time/weekday/io.cc
>+++ b/libstdc++-v3/testsuite/std/time/weekday/io.cc
>@@ -69,6 +69,8 @@ test_format()
>   VERIFY( s == "Mon" );
>   s = std::format(loc_fr, "{:L%a}", weekday(1));
>   VERIFY( s == "lun." );
>+  s = std::format(loc_fr, "{:L}", weekday(25));
>+  VERIFY( s == "25 is not a valid weekday" );
>
>   std::string_view specs = "aAbBcCdDeFgGhHIjmMpqQrRSTuUVwWxXyYzZ";
>   std::string_view my_specs = "aAuw";
>-- 
>2.49.0
>
>



More information about the Libstdc++ mailing list