diff --git a/libcxx/docs/Status/FormatPaper.csv b/libcxx/docs/Status/FormatPaper.csv --- a/libcxx/docs/Status/FormatPaper.csv +++ b/libcxx/docs/Status/FormatPaper.csv @@ -10,7 +10,7 @@ `[time.syn] `_,"Formatter ``chrono::local-time-format-t``",A ```` implementation,Not assigned,,, `[time.syn] `_,"Formatter ``chrono::day``",,Mark de Wever,|Complete|, Clang 16 `[time.syn] `_,"Formatter ``chrono::month``",,Mark de Wever,|In Progress|, -`[time.syn] `_,"Formatter ``chrono::year``",,Mark de Wever,|In Progress|, +`[time.syn] `_,"Formatter ``chrono::year``",,Mark de Wever,|Complete|, Clang 16 `[time.syn] `_,"Formatter ``chrono::weekday``",,Mark de Wever,|In Progress|, `[time.syn] `_,"Formatter ``chrono::weekday_indexed``",,Mark de Wever,|In Progress|, `[time.syn] `_,"Formatter ``chrono::weekday_last``",,Mark de Wever,|In Progress|, diff --git a/libcxx/include/__chrono/convert_to_tm.h b/libcxx/include/__chrono/convert_to_tm.h --- a/libcxx/include/__chrono/convert_to_tm.h +++ b/libcxx/include/__chrono/convert_to_tm.h @@ -11,6 +11,7 @@ #define _LIBCPP___CHRONO_CONVERT_TO_TM_H #include <__chrono/day.h> +#include <__chrono/year.h> #include <__concepts/same_as.h> #include <__config> @@ -33,6 +34,8 @@ if constexpr (same_as<_ChronoCalendarTimePoint, chrono::day>) __result.tm_mday = static_cast(__value); + else if constexpr (same_as<_ChronoCalendarTimePoint, chrono::year>) + __result.tm_year = static_cast(__value) - 1900; else static_assert(sizeof(_ChronoCalendarTimePoint) == 0, "Add the missing type specialization"); diff --git a/libcxx/include/__chrono/formatter.h b/libcxx/include/__chrono/formatter.h --- a/libcxx/include/__chrono/formatter.h +++ b/libcxx/include/__chrono/formatter.h @@ -13,13 +13,17 @@ #include <__chrono/convert_to_tm.h> #include <__chrono/day.h> #include <__chrono/parser_std_format_spec.h> +#include <__chrono/statically_widen.h> +#include <__chrono/year.h> #include <__config> #include <__format/concepts.h> #include <__format/format_parse_context.h> #include <__format/formatter.h> #include <__format/formatter_output.h> #include <__format/parser_std_format_spec.h> +#include <__memory/addressof.h> #include +#include #include #include #include @@ -52,6 +56,37 @@ /// /// When no chrono-specs are provided it uses the stream formatter. +template +_LIBCPP_HIDE_FROM_ABI void __format_year(int __year, basic_stringstream<_CharT>& __sstr) { + __year += 1900; // tm_year to year conversion. + if (__year < 0) { + __sstr << _CharT('-'); + // The chrono library requires a year fits in a short. + __year = -__year; + } + + __sstr << std::format(_LIBCPP_STATICALLY_WIDEN(_CharT, "{:04}"), __year); +} + +template +_LIBCPP_HIDE_FROM_ABI void __format_century(int __year, basic_stringstream<_CharT>& __sstr) { + int __century = (__year + 1900) / 100; // tm_year to year conversion. + // TODO FMT Write an issue + // [tab:time.format.spec] + // %C The year divided by 100 using floored division. If the result is a + // single decimal digit, it is prefixed with 0. + // + // -1 is not a single digit, this probably needs wording like %Y to always + // have at least 2 digits. + // If the result has less than four digits it is zero-padded with 0 to two digits. + // is less -> has less + // left-padded -> zero-padded, otherwise the proper value would be 000-0. + if (__century < 0) + __sstr << __century; + else + __sstr << std::format(_LIBCPP_STATICALLY_WIDEN(_CharT, "{:02}"), __century); +} + template _LIBCPP_HIDE_FROM_ABI void __format_chrono_using_chrono_specs( const _Tp& __value, basic_stringstream<_CharT>& __sstr, basic_string_view<_CharT> __chrono_specs) { @@ -74,7 +109,49 @@ __sstr << *__it; break; + case _CharT('C'): + if (__t.tm_year < 1000) + __formatter::__format_century(__t.tm_year, __sstr); + else + __facet.put({__sstr}, __sstr, _CharT(' '), std::addressof(__t), __s, __it + 1); + break; + + // Unlike time_put and strftime the formatting library requires %Y + // + // [tab:time.format.spec] + // The year as a decimal number. If the result is less than four digits + // it is left-padded with 0 to four digits. + // + // This means years in the range (-1000, 1000) need manual formatting. + // It's unclear whether %EY needs the same treatment. For example the + // Japanese EY contains the era name and year. This is zero padded to 2 + // digits time_put (note older glibc versions didn't do padding.) + // However most eras won't reach a 100 years, let alone 1000. So + // padding to 4 digits seems unwanted for Japanese. + // + // The same applies to %Ex since that too depends on the era. + // + // %x the locale's date representation is currently doesn't handle the + // zero-padding too. + // + // The 4 digits can be implemented better at a later time. On POSIX + // systems the required information can be extracted by nl_langinfo + // https://man7.org/linux/man-pages/man3/nl_langinfo.3.html + // + // Note since year < -1000 is expected to be rare it uses the more + // expensive year routine. + // + // TODO FMT evaluate the comment above. + + case _CharT('Y'): + if (__t.tm_year < 1000) + __formatter::__format_year(__t.tm_year, __sstr); + else + __facet.put({__sstr}, __sstr, _CharT(' '), std::addressof(__t), __s, __it + 1); + break; + case _CharT('O'): + case _CharT('E'): ++__it; [[fallthrough]]; default: @@ -146,7 +223,19 @@ } }; -#endif //if _LIBCPP_STD_VER > 17 && !defined(_LIBCPP_HAS_NO_INCOMPLETE_FORMAT) +template <__fmt_char_type _CharT> +struct _LIBCPP_TEMPLATE_VIS _LIBCPP_AVAILABILITY_FORMAT formatter + : public __formatter_chrono<_CharT> { +public: + using _Base = __formatter_chrono<_CharT>; + + _LIBCPP_HIDE_FROM_ABI constexpr auto parse(basic_format_parse_context<_CharT>& __parse_ctx) + -> decltype(__parse_ctx.begin()) { + return _Base::__parse(__parse_ctx, __format_spec::__fields_chrono, __format_spec::__flags::__year); + } +}; + +#endif // if _LIBCPP_STD_VER > 17 && !defined(_LIBCPP_HAS_NO_LOCALIZATION) _LIBCPP_END_NAMESPACE_STD diff --git a/libcxx/include/__chrono/ostream.h b/libcxx/include/__chrono/ostream.h --- a/libcxx/include/__chrono/ostream.h +++ b/libcxx/include/__chrono/ostream.h @@ -12,6 +12,7 @@ #include <__chrono/day.h> #include <__chrono/statically_widen.h> +#include <__chrono/year.h> #include <__config> #include #include @@ -40,6 +41,13 @@ : std::format(_LIBCPP_STATICALLY_WIDEN(_CharT, "{:02} is not a valid day"), static_cast(__d))); } +template +_LIBCPP_HIDE_FROM_ABI _LIBCPP_AVAILABILITY_FORMAT basic_ostream<_CharT, _Traits>& +operator<<(basic_ostream<_CharT, _Traits>& __os, const year& __y) { + return __os << (__y.ok() ? std::format(_LIBCPP_STATICALLY_WIDEN(_CharT, "{:%Y}"), __y) + : std::format(_LIBCPP_STATICALLY_WIDEN(_CharT, "{:%Y} is not a valid year"), __y)); +} + } // namespace chrono #endif //if _LIBCPP_STD_VER > 17 && !defined(_LIBCPP_HAS_NO_INCOMPLETE_FORMAT) diff --git a/libcxx/include/chrono b/libcxx/include/chrono --- a/libcxx/include/chrono +++ b/libcxx/include/chrono @@ -355,6 +355,9 @@ constexpr year operator+(const years& x, const year& y) noexcept; constexpr year operator-(const year& x, const years& y) noexcept; constexpr years operator-(const year& x, const year& y) noexcept; +template + basic_ostream& + operator<<(basic_ostream& os, const year& y); // 25.8.6, class weekday // C++20 class weekday; @@ -620,6 +623,7 @@ namespace std { template struct formatter; // C++20 + template struct formatter; // C++20 } // namespace std namespace chrono { diff --git a/libcxx/test/libcxx/utilities/format/format.formatter/format.formatter.spec/formattable.compile.pass.cpp b/libcxx/test/libcxx/utilities/format/format.formatter/format.formatter.spec/formattable.compile.pass.cpp --- a/libcxx/test/libcxx/utilities/format/format.formatter/format.formatter.spec/formattable.compile.pass.cpp +++ b/libcxx/test/libcxx/utilities/format/format.formatter/format.formatter.spec/formattable.compile.pass.cpp @@ -143,7 +143,7 @@ assert_is_formattable(); assert_is_not_formattable(); - assert_is_not_formattable(); + assert_is_formattable(); assert_is_not_formattable(); assert_is_not_formattable(); diff --git a/libcxx/test/std/time/time.cal/time.cal.year/time.cal.year.nonmembers/ostream.pass.cpp b/libcxx/test/std/time/time.cal/time.cal.year/time.cal.year.nonmembers/ostream.pass.cpp new file mode 100644 --- /dev/null +++ b/libcxx/test/std/time/time.cal/time.cal.year/time.cal.year.nonmembers/ostream.pass.cpp @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03, c++11, c++14, c++17 +// UNSUPPORTED: libcpp-has-no-localization +// UNSUPPORTED: libcpp-has-no-incomplete-format + +// TODO FMT Investigate Windows issues. +// UNSUPPORTED: msvc, target={{.+}}-windows-gnu + +// REQUIRES: locale.fr_FR.UTF-8 +// REQUIRES: locale.ja_JP.UTF-8 + +// +// class year; + +// template +// basic_ostream& +// operator<<(basic_ostream& os, const year& year); + +#include +#include +#include + +#include "make_string.h" +#include "platform_support.h" // locale name macros +#include "test_macros.h" + +#define SV(S) MAKE_STRING_VIEW(CharT, S) + +template +static std::basic_string stream_c_locale(std::chrono::year year) { + std::basic_stringstream sstr; + sstr << year; + return sstr.str(); +} + +template +static std::basic_string stream_fr_FR_locale(std::chrono::year year) { + std::basic_stringstream sstr; + const std::locale locale(LOCALE_fr_FR_UTF_8); + sstr.imbue(locale); + sstr << year; + return sstr.str(); +} + +template +static std::basic_string stream_ja_JP_locale(std::chrono::year year) { + std::basic_stringstream sstr; + const std::locale locale(LOCALE_ja_JP_UTF_8); + sstr.imbue(locale); + sstr << year; + return sstr.str(); +} + +template +static void test() { + assert(stream_c_locale(std::chrono::year{-32'768}) == SV("-32768 is not a valid year")); + assert(stream_c_locale(std::chrono::year{-32'767}) == SV("-32767")); + assert(stream_c_locale(std::chrono::year{0}) == SV("0000")); + assert(stream_c_locale(std::chrono::year{1970}) == SV("1970")); + assert(stream_c_locale(std::chrono::year{32'767}) == SV("32767")); + + assert(stream_fr_FR_locale(std::chrono::year{-32'768}) == SV("-32768 is not a valid year")); + assert(stream_fr_FR_locale(std::chrono::year{-32'767}) == SV("-32767")); + assert(stream_fr_FR_locale(std::chrono::year{0}) == SV("0000")); + assert(stream_fr_FR_locale(std::chrono::year{1970}) == SV("1970")); + assert(stream_fr_FR_locale(std::chrono::year{32'767}) == SV("32767")); + + assert(stream_ja_JP_locale(std::chrono::year{-32'768}) == SV("-32768 is not a valid year")); + assert(stream_ja_JP_locale(std::chrono::year{-32'767}) == SV("-32767")); + assert(stream_ja_JP_locale(std::chrono::year{0}) == SV("0000")); + assert(stream_ja_JP_locale(std::chrono::year{1970}) == SV("1970")); + assert(stream_ja_JP_locale(std::chrono::year{32'767}) == SV("32767")); +} + +int main(int, char**) { + test(); + +#ifndef TEST_HAS_NO_WIDE_CHARACTERS + test(); +#endif + + return 0; +} diff --git a/libcxx/test/std/time/time.syn/formatter.year.pass.cpp b/libcxx/test/std/time/time.syn/formatter.year.pass.cpp new file mode 100644 --- /dev/null +++ b/libcxx/test/std/time/time.syn/formatter.year.pass.cpp @@ -0,0 +1,295 @@ +//===----------------------------------------------------------------------===// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03, c++11, c++14, c++17 +// UNSUPPORTED: libcpp-has-no-localization +// UNSUPPORTED: libcpp-has-no-incomplete-format + +// TODO FMT Investigate Windows issues. +// UNSUPPORTED: msvc, target={{.+}}-windows-gnu + +// REQUIRES: locale.fr_FR.UTF-8 +// REQUIRES: locale.ja_JP.UTF-8 + +// + +// template struct formatter; + +#include +#include + +#include +#include +#include +#include +#include + +#include "formatter_tests.h" +#include "make_string.h" +#include "platform_support.h" // locale name macros +#include "string_literal.h" +#include "test_macros.h" + +template +static void test_no_chrono_specs() { + check.template operator()<"{}">(SV("-32767"), std::chrono::year{-32'767}); + check.template operator()<"{}">(SV("-1000"), std::chrono::year{-1000}); + check.template operator()<"{}">(SV("-0100"), std::chrono::year{-100}); + check.template operator()<"{}">(SV("-0010"), std::chrono::year{-10}); + check.template operator()<"{}">(SV("-0001"), std::chrono::year{-1}); + check.template operator()<"{}">(SV("0000"), std::chrono::year{0}); + check.template operator()<"{}">(SV("0001"), std::chrono::year{1}); + check.template operator()<"{}">(SV("0010"), std::chrono::year{10}); + check.template operator()<"{}">(SV("0100"), std::chrono::year{100}); + check.template operator()<"{}">(SV("1000"), std::chrono::year{1000}); + check.template operator()<"{}">(SV("32727"), std::chrono::year{32'727}); + + // Invalid year + check.template operator()<"{}">(SV("-32768 is not a valid year"), std::chrono::year{-32'768}); + check.template operator()<"{}">(SV("-32768 is not a valid year"), std::chrono::year{32'768}); +} + +template +static void test_valid_values() { + constexpr string_literal fmt{ + "{:" + "%%C='%C'%t" + "%%EC='%EC'%t" + "%%y='%y'%t" + "%%Ey='%Ey'%t" + "%%Oy='%Oy'%t" + "%%Y='%Y'%t" + "%%EY='%EY'%t" + "%n}"}; + constexpr string_literal lfmt{ + "{:L" + "%%C='%C'%t" + "%%EC='%EC'%t" + "%%y='%y'%t" + "%%Ey='%Ey'%t" + "%%Oy='%Oy'%t" + "%%Y='%Y'%t" + "%%EY='%EY'%t" + "%n}"}; + + const std::locale loc(LOCALE_ja_JP_UTF_8); + std::locale::global(std::locale(LOCALE_fr_FR_UTF_8)); + + // Non localized output using C-locale + check.template operator()( + SV("%C='00'\t" +#if defined(__APPLE__) + "%EC='00'\t" +#else + "%EC='0'\t" +#endif + "%y='00'\t" + "%Ey='00'\t" + "%Oy='00'\t" + "%Y='0000'\t" +#if defined(__APPLE__) + "%EY='0000'\t" +#elif defined(_AIX) + "%EY=''\t" +#else + "%EY='0'\t" +#endif + "\n"), + std::chrono::year{0}); + + check.template operator()( + SV("%C='19'\t" + "%EC='19'\t" + "%y='70'\t" + "%Ey='70'\t" + "%Oy='70'\t" + "%Y='1970'\t" + "%EY='1970'\t" + "\n"), + std::chrono::year{1970}); + + check.template operator()( + SV("%C='20'\t" + "%EC='20'\t" + "%y='38'\t" + "%Ey='38'\t" + "%Oy='38'\t" + "%Y='2038'\t" + "%EY='2038'\t" + "\n"), + std::chrono::year{2038}); + + // Use the global locale (fr_FR) + check.template operator()( + SV("%C='00'\t" +#if defined(__APPLE__) + "%EC='00'\t" +#else + "%EC='0'\t" +#endif + "%y='00'\t" + "%Ey='00'\t" + "%Oy='00'\t" + "%Y='0000'\t" +#if defined(__APPLE__) + "%EY='0000'\t" +#elif defined(_AIX) + "%EY=''\t" +#else + "%EY='0'\t" +#endif + "\n"), + std::chrono::year{0}); + + check.template operator()( + SV("%C='19'\t" + "%EC='19'\t" + "%y='70'\t" + "%Ey='70'\t" + "%Oy='70'\t" + "%Y='1970'\t" + "%EY='1970'\t" + "\n"), + std::chrono::year{1970}); + + check.template operator()( + SV("%C='20'\t" + "%EC='20'\t" + "%y='38'\t" + "%Ey='38'\t" + "%Oy='38'\t" + "%Y='2038'\t" + "%EY='2038'\t" + "\n"), + std::chrono::year{2038}); + + // Use supplied locale (ja_JP). This locale has a different alternate. +#if defined(__APPLE__) || defined(_AIX) + + check.template operator()( + SV("%C='00'\t" +# if defined(__APPLE__) + "%EC='00'\t" +# else + "%EC='0'\t" +# endif + "%y='00'\t" + "%Ey='00'\t" + "%Oy='00'\t" + "%Y='0000'\t" +#elif defined(_AIX) + "%EY=''\t" +#else + "%EY='0'\t" +#endif + "\n"), + std::chrono::year{0}); + + check.template operator()( + SV("%C='19'\t" + "%EC='19'\t" + "%y='70'\t" + "%Ey='70'\t" + "%Oy='70'\t" + "%Y='1970'\t" +#if defined(__APPLE__) + "%EY='1970'\t" +#else + "%EY=''\t" +#endif + "\n"), + std::chrono::year{1970}); + + check.template operator()( + SV("%C='20'\t" + "%EC='20'\t" + "%y='38'\t" + "%Ey='38'\t" + "%Oy='38'\t" + "%Y='2038'\t" + "%EY='2038'\t" + "\n"), + std::chrono::year{2038}); + +#else // defined(__APPLE__) || defined(_AIX) + lcheck.template operator()( + loc, + SV("%C='00'\t" + "%EC='紀元前'\t" + "%y='00'\t" +// https://sourceware.org/bugzilla/show_bug.cgi?id=23758 +# if defined(__GLIBC__) && __GLIBC__ <= 2 && __GLIBC_MINOR__ < 29 + "%Ey='1'\t" +# else + "%Ey='01'\t" +# endif + "%Oy='〇'\t" + "%Y='0000'\t" +// https://sourceware.org/bugzilla/show_bug.cgi?id=23758 +# if defined(__GLIBC__) && __GLIBC__ <= 2 && __GLIBC_MINOR__ < 29 + "%EY='紀元前1年'\t" +# else + "%EY='紀元前01年'\t" +# endif + "\n"), + std::chrono::year{0}); + + lcheck.template operator()( + loc, + SV("%C='19'\t" + "%EC='昭和'\t" + "%y='70'\t" + "%Ey='45'\t" + "%Oy='七十'\t" + "%Y='1970'\t" + "%EY='昭和45年'\t" + "\n"), + std::chrono::year{1970}); + + // Note this test will fail if the Reiwa era ends before 2038. + lcheck.template operator()( + loc, + SV("%C='20'\t" + "%EC='令和'\t" + "%y='38'\t" + "%Ey='20'\t" + "%Oy='三十八'\t" + "%Y='2038'\t" + "%EY='令和20年'\t" + "\n"), + std::chrono::year{2038}); +#endif // defined(__APPLE__) || defined(_AIX) + + std::locale::global(std::locale::classic()); +} + +template +static void test() { + test_no_chrono_specs(); + test_valid_values(); + check_invalid_types( + {SV("C"), SV("y"), SV("Y"), SV("EC"), SV("Ey"), SV("EY"), SV("Oy")}, std::chrono::year{1970}); + + check_exception("Expected '%' or '}' in the chrono format-string", SV("{:A"), std::chrono::year{1970}); + check_exception("The chrono-specs contains a '{'", SV("{:%%{"), std::chrono::year{1970}); + check_exception("End of input while parsing the modifier chrono conversion-spec", SV("{:%"), std::chrono::year{1970}); + check_exception("End of input while parsing the modifier E", SV("{:%E"), std::chrono::year{1970}); + check_exception("End of input while parsing the modifier O", SV("{:%O"), std::chrono::year{1970}); + + // Precision not allowed + check_exception("Expected '%' or '}' in the chrono format-string", SV("{:.3}"), std::chrono::year{1970}); +} + +int main(int, char**) { + test(); + +#ifndef TEST_HAS_NO_WIDE_CHARACTERS + test(); +#endif + + return 0; +} diff --git a/libcxx/test/std/utilities/format/format.formatter/format.formatter.spec/types.compile.pass.cpp b/libcxx/test/std/utilities/format/format.formatter/format.formatter.spec/types.compile.pass.cpp --- a/libcxx/test/std/utilities/format/format.formatter/format.formatter.spec/types.compile.pass.cpp +++ b/libcxx/test/std/utilities/format/format.formatter/format.formatter.spec/types.compile.pass.cpp @@ -185,7 +185,7 @@ assert_formatter_is_enabled(); assert_formatter_is_disabled(); - assert_formatter_is_disabled(); + assert_formatter_is_enabled(); assert_formatter_is_disabled(); assert_formatter_is_disabled(); diff --git a/libcxx/utils/ci/run-buildbot b/libcxx/utils/ci/run-buildbot --- a/libcxx/utils/ci/run-buildbot +++ b/libcxx/utils/ci/run-buildbot @@ -188,7 +188,7 @@ ! grep -rn '[^ -~]' libcxx/include libcxx/src libcxx/test libcxx/benchmarks \ --exclude '*.dat' \ --exclude 'format_tests.h' \ - --exclude 'formatter.day.pass.cpp' \ + --exclude 'formatter.*.pass.cpp' \ --exclude 'grep.pass.cpp' \ --exclude 'locale-specific_form.pass.cpp' \ --exclude 'std_format_spec_string_unicode.bench.cpp' \