From: Bruno Haible Date: Tue, 15 Jul 2025 09:19:10 +0000 (+0200) Subject: nstrftime: Add support for non-Gregorian calendars. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4393ea5ae8135fa72ddbda1a0124a375c4815065;p=thirdparty%2Fgnulib.git nstrftime: Add support for non-Gregorian calendars. * lib/calendars.h: New file. * lib/calendar-thai.h: New file. * lib/calendar-persian.h: New file. * lib/calendar-ethiopian.h: New file. * lib/strftime.h (nstrftime): Document which directives don't work with non-Gregorian calendars. * lib/strftime.c (SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME): New macro. Include localcharset.h, localename.h, calendars.h. (CAL_ARGS): New macro. (my_strftime): Recognize locales with non-Gregorian calendars. Pass cal and caldate down to __strftime_internal. (__strftime_internal): Accept additional parameters cal, caldate. Remove rejection of modifier 'O' for directive 'Y' and allow a non-ASCII alternate digits base. Produce calendar-aware output for the directives 'b', 'h', 'B', 'x', 'd', 'e', 'm', 'Y'. * modules/nstrftime (Files): Add the calendar files. (Depends-on): Add localcharset. (Link): New section. * modules/fprintftime (Link): New section. * tests/test-nstrftime-DE.c: New file. * tests/test-nstrftime-TH.c: New file. * tests/test-nstrftime-IR.c: New file. * tests/test-nstrftime-ET.c: New file. * modules/nstrftime-tests (Files): Add them. (Depends-on): Add localcharset, setenv. (Makefile.am): Link test-nstrftime with $(INTL_MACOSX_LIBS). Arrange to compile and run test-nstrftime-DE, test-nstrftime-TH, test-nstrftime-IR, test-nstrftime-ET. --- diff --git a/ChangeLog b/ChangeLog index 6080d0ddbd..806e273cf8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,35 @@ +2025-07-15 Bruno Haible + + nstrftime: Add support for non-Gregorian calendars. + * lib/calendars.h: New file. + * lib/calendar-thai.h: New file. + * lib/calendar-persian.h: New file. + * lib/calendar-ethiopian.h: New file. + * lib/strftime.h (nstrftime): Document which directives don't work with + non-Gregorian calendars. + * lib/strftime.c (SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME): New macro. + Include localcharset.h, localename.h, calendars.h. + (CAL_ARGS): New macro. + (my_strftime): Recognize locales with non-Gregorian calendars. Pass cal + and caldate down to __strftime_internal. + (__strftime_internal): Accept additional parameters cal, caldate. + Remove rejection of modifier 'O' for directive 'Y' and allow a non-ASCII + alternate digits base. Produce calendar-aware output for the directives + 'b', 'h', 'B', 'x', 'd', 'e', 'm', 'Y'. + * modules/nstrftime (Files): Add the calendar files. + (Depends-on): Add localcharset. + (Link): New section. + * modules/fprintftime (Link): New section. + * tests/test-nstrftime-DE.c: New file. + * tests/test-nstrftime-TH.c: New file. + * tests/test-nstrftime-IR.c: New file. + * tests/test-nstrftime-ET.c: New file. + * modules/nstrftime-tests (Files): Add them. + (Depends-on): Add localcharset, setenv. + (Makefile.am): Link test-nstrftime with $(INTL_MACOSX_LIBS). Arrange to + compile and run test-nstrftime-DE, test-nstrftime-TH, test-nstrftime-IR, + test-nstrftime-ET. + 2025-07-15 Bruno Haible nstrftime: Remove old comment about OSF/1. diff --git a/lib/calendar-ethiopian.h b/lib/calendar-ethiopian.h new file mode 100644 index 0000000000..a6d0d9e774 --- /dev/null +++ b/lib/calendar-ethiopian.h @@ -0,0 +1,110 @@ +/* Support for the Ethiopian / Ge'ez calendar (used in Ethiopia). + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* Reference: https://en.wikipedia.org/wiki/Ethiopian_calendar */ + +static const struct calendar_month_name ethiopian_month_names[13] = +{ + /* Mäskäräm */ { "መስከረም", "መስ" }, + /* Ṭəqəmt */ { "ጥቅምት", "ጥን" }, + /* Ḫədar */ { "ኅዳር", "ኅዳ" }, + /* Taḫśaś */ { "ታኅሣሥ", "ታህ" }, + /* Ṭərr */ { "ጥር", "ጥር" }, + /* Yäkatit */ { "የካቲት", "የካ" }, + /* Mägabit */ { "መጋቢት", "መጋ" }, + /* Miyazya */ { "ሚያዝያ", "ሚያ" }, + /* Gənbot */ { "ግንቦት", "ግን" }, + /* Säne */ { "ሰኔ", "ሰኔ" }, + /* Ḥamle */ { "ሐምሌ", "ሐም" }, + /* Nähase */ { "ነሐሴ", "ነሐ" }, + /* Ṗagume */ { "ጳጉሜን" /* or "ጳጐሜን" or "ጳጉሜ" */, "ጳጉ" }, +}; + +static int +gregorian_to_ethiopian (struct calendar_date *result, + int greg_year, int greg_month, int greg_day) +{ + if (greg_year > 1900 && greg_year < 2100) + { + /* Simplify leap year calculations by considering year start + March 1. */ + greg_month -= 2; + if (greg_month < 0) + { + greg_month += 12; + greg_year -= 1; + } + int greg_days_since_march_1 = + /* greg_month 0 1 2 3 4 5 6 7 8 9 10 11 + days 0 31 61 92 122 153 184 214 245 275 306 337 */ + ((greg_month * 153 + 2) / 5) + + (greg_day - 1); + int greg_days_this_year = 365 + __isleap (greg_year + 1); + /* There are 171 days from Sep. 11 to Feb. 28 of the next year, + or from Sep. 12 to Feb. 29 of the next year (inclusive). */ + int days_since_year_start = greg_days_since_march_1 + 171; + int year = greg_year; + if (days_since_year_start >= greg_days_this_year) + { + days_since_year_start -= greg_days_this_year; + year += 1; + } + result->year = year - 8; + result->month = days_since_year_start / 30; /* in the range 0..12 ! */ + result->day = (days_since_year_start % 30) + 1; + result->month_names = ethiopian_month_names; + return 0; + } + return -1; +} + +static const struct calendar ethiopian_calendar = +{ + gregorian_to_ethiopian, + "%d/%m/%Y", + '0' +}; + + +#ifdef TEST + +#include +#include + +int main (int argc, char *argv[]) +{ + int greg_year = atoi (argv[1]); + int greg_month = atoi (argv[2]) - 1; + int greg_day = atoi (argv[3]); + struct calendar_date cday; + + if (gregorian_to_ethiopian (&cday, greg_year, greg_month, greg_day) == 0) + { + printf ("%d-%d-%d -> %d-%d(%s)-%02d\n", + greg_year, 1+greg_month, greg_day, + cday.year, 1+cday.month, cday.month_names[cday.month].full, cday.day); + } +} + +/* + * Local Variables: + * compile-command: "gcc -ggdb -DTEST -Wall -x c calendars.h" + * End: + */ + +#endif diff --git a/lib/calendar-persian.h b/lib/calendar-persian.h new file mode 100644 index 0000000000..96bf97baa6 --- /dev/null +++ b/lib/calendar-persian.h @@ -0,0 +1,165 @@ +/* Support for the Persian solar Hijri calendar (used in Iran). + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* Reference: https://en.wikipedia.org/wiki/Solar_Hijri_calendar + More info regarding the leap years: Some online date converters and also + Emacs (cal-persia.el) get the leap years wrong: they pretend that year 1403 + is a non-leap year and that 1404 is a leap year. The correct info, based on + https://fa.wikipedia.org/wiki/%DA%AF%D8%A7%D9%87%E2%80%8C%D8%B4%D9%85%D8%A7%D8%B1%DB%8C_%D8%B1%D8%B3%D9%85%DB%8C_%D8%A7%DB%8C%D8%B1%D8%A7%D9%86 + and https://github.com/movahhedi/persian-leap , + is that the following years are leap years: + 1276, 1280, 1284, 1288, 1292, 1296, 1300, 1304, + 1309, 1313, 1317, 1321, 1325, 1329, 1333, 1337, + 1342, 1346, 1350, 1354, 1358, 1362, 1366, 1370, + 1375, 1379, 1383, 1387, 1391, 1395, 1399, 1403, + 1408, 1412, 1416, 1420, 1424, 1428, 1432, 1436, + 1441, 1445, 1449, 1453, 1457, 1461, 1465, 1469, + 1474, 1478, 1482, 1486, 1490, 1494, 1498. + This is consistent with the table in + https://en.wikipedia.org/wiki/Solar_Hijri_calendar#Comparison_with_Gregorian_calendar + */ + +static const struct calendar_month_name persian_month_names[12] = +{ + /* Farvardin */ { "فروردین", "فروردین" }, + /* Ordibehesht */ { "اردیبهشت", "اردیبهشت" }, + /* Khordad */ { "خرداد", "خرداد" }, + /* Tir */ { "تیر", "تیر" }, + /* Mordad */ { "مرداد", "مرداد" }, + /* Shahrivar */ { "شهریور", "شهریور" }, + /* Mehr */ { "مهر", "مهر" }, + /* Aban */ { "آبان", "آبان" }, + /* Azar */ { "آذر", "آذر" }, + /* Dey */ { "دی", "دی" }, + /* Bahman */ { "بهمن", "بهمن" }, + /* Esfand */ { "اسفند", "اسفند" }, +}; + +static int +gregorian_to_persian (struct calendar_date *result, + int greg_year, int greg_month, int greg_day) +{ + if ((greg_year > 1925 && greg_year < 1975) + || (greg_year > 1978 && greg_year < 2100)) + { + /* Simplify leap year calculations by considering year start + March 1. */ + greg_month -= 2; + if (greg_month < 0) + { + greg_month += 12; + greg_year -= 1; + } + int greg_days_since_march_1 = + /* greg_month 0 1 2 3 4 5 6 7 8 9 10 11 + days 0 31 61 92 122 153 184 214 245 275 306 337 */ + ((greg_month * 153 + 2) / 5) + + (greg_day - 1); + int greg_days_since_1900_march_1 = + (greg_year - 1900) * 365 + + ((greg_year - 1900) / 4) + - ((greg_year - 1900) / 100) + + ((greg_year - 1600) / 400) + + greg_days_since_march_1; + /* The Hijri calendar currently uses 33-year cycles of 12053 days each + (8 leap years and 25 non-leap years). + For our purposes, let's define the start of such a cycle as the + beginning of the year that follows the leap year that follows + the 4 non-leap years: + 1931-03-22, 1964-03-21, 1997-03-21, 2030-03-21, 2063-03-21, 2096-03-20, + ... */ + int cycle33_number = (greg_days_since_1900_march_1 + 710) / 12053; + int days_since_cycle33_start = (greg_days_since_1900_march_1 + 710) % 12053; + /* In such a 33-year cycle, the days after a leap year end are at days + 0, 1461, 2922, 4383, 5844, 7305, 8766, 10227; + these are the multiples of 1461. + We call the period that starts with 3 or 4 non-leap years and ends + with a leap year a "leap cycle". Thus these 8 days are the beginning + of the 8 leap cycles in the 33-year cycle. */ + int leap_years_since_cycle33_start = days_since_cycle33_start / 1461; + if (leap_years_since_cycle33_start > 7) + leap_years_since_cycle33_start = 7; + int is_last_day_of_leap_year = + (((days_since_cycle33_start + 1) % 1461) == 0 + && days_since_cycle33_start <= 10227) + || (days_since_cycle33_start == 12053 - 1); + int days_since_leapcycle_start = + days_since_cycle33_start - leap_years_since_cycle33_start * 1461; + int full_years_since_leapcycle_start = + days_since_leapcycle_start / 365 - is_last_day_of_leap_year; + int full_years_since_cycle33_start = + leap_years_since_cycle33_start * 4 + full_years_since_leapcycle_start; + int year = 1277 + cycle33_number * 33 + full_years_since_cycle33_start; + int days_since_year_start = + days_since_leapcycle_start - full_years_since_leapcycle_start * 365; + int month; + int days_since_month_start; + if (days_since_year_start < 186) + { + month = days_since_year_start / 31; + days_since_month_start = days_since_year_start % 31; + } + else + { + month = (days_since_year_start - 6) / 30; + days_since_month_start = (days_since_year_start - 6) % 30; + } + result->year = year; + result->month = month; + result->day = days_since_month_start + 1; + result->month_names = persian_month_names; + return 0; + } + return -1; +} + +static const struct calendar persian_calendar = +{ + gregorian_to_persian, + "%OY/%Om/%Od", + 0xDBB0 /* The alternate digits are U+06F0..U+06F9. */ +}; + + +#ifdef TEST + +#include +#include + +int main (int argc, char *argv[]) +{ + int greg_year = atoi (argv[1]); + int greg_month = atoi (argv[2]) - 1; + int greg_day = atoi (argv[3]); + struct calendar_date cday; + + if (gregorian_to_persian (&cday, greg_year, greg_month, greg_day) == 0) + { + printf ("%d-%d-%d -> %d-%d(%s)-%02d\n", + greg_year, 1+greg_month, greg_day, + cday.year, 1+cday.month, cday.month_names[cday.month].full, cday.day); + } +} + +/* + * Local Variables: + * compile-command: "gcc -ggdb -DTEST -Wall -x c calendars.h" + * End: + */ + +#endif diff --git a/lib/calendar-thai.h b/lib/calendar-thai.h new file mode 100644 index 0000000000..d356904676 --- /dev/null +++ b/lib/calendar-thai.h @@ -0,0 +1,109 @@ +/* Support for the Thai solar calendar (used in Thailand). + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +/* Reference: https://en.wikipedia.org/wiki/Thai_solar_calendar */ + +static const struct calendar_month_name thai_month_names[15] = +{ + /* This array actually contains two overlapping arrays: + [0..11] are for greg_year >= 1941, + [3..14] are for greg_year < 1941. */ + /* January */ { "มกราคม", "ม.ค." }, + /* February */ { "กุมภาพันธ์", "ก.พ." }, + /* March */ { "มีนาคม", "มี.ค." }, + /* April */ { "เมษายน", "เม.ย." }, + /* May */ { "พฤษภาคม", "พ.ค." }, + /* June */ { "มิถุนายน", "มิ.ย." }, + /* July */ { "กรกฎาคม", "ก.ค." }, + /* August */ { "สิงหาคม", "ส.ค." }, + /* September */ { "กันยายน", "ก.ย." }, + /* October */ { "ตุลาคม", "ต.ค." }, + /* November */ { "พฤศจิกายน", "พ.ย." }, + /* December */ { "ธันวาคม", "ธ.ค." }, + /* January */ { "มกราคม", "ม.ค." }, + /* February */ { "กุมภาพันธ์", "ก.พ." }, + /* March */ { "มีนาคม", "มี.ค." }, +}; + +static int +gregorian_to_thai (struct calendar_date *result, + int greg_year, int greg_month, int greg_day) +{ + if (greg_year > 1912) + { + result->day = greg_day; + if (greg_year < 1941) + { + if (greg_month < 3) + { + result->year = greg_year + 542; + result->month = greg_month + 9; + } + else + { + result->year = greg_year + 543; + result->month = greg_month - 3; + } + result->month_names = thai_month_names + 3; + } + else + { + result->year = greg_year + 543; + result->month = greg_month; + result->month_names = thai_month_names; + } + return 0; + } + return -1; +} + +static const struct calendar thai_calendar = +{ + gregorian_to_thai, + "%d/%m/%Y", + '0' +}; + + +#ifdef TEST + +#include +#include + +int main (int argc, char *argv[]) +{ + int greg_year = atoi (argv[1]); + int greg_month = atoi (argv[2]) - 1; + int greg_day = atoi (argv[3]); + struct calendar_date cday; + + if (gregorian_to_thai (&cday, greg_year, greg_month, greg_day) == 0) + { + printf ("%d-%d-%d -> %d-%d(%s)-%02d\n", + greg_year, 1+greg_month, greg_day, + cday.year, 1+cday.month, cday.month_names[cday.month].full, cday.day); + } +} + +/* + * Local Variables: + * compile-command: "gcc -ggdb -DTEST -Wall -x c calendars.h" + * End: + */ + +#endif diff --git a/lib/calendars.h b/lib/calendars.h new file mode 100644 index 0000000000..b90281087c --- /dev/null +++ b/lib/calendars.h @@ -0,0 +1,49 @@ +/* Support for the non-Gregorian calendars. + Copyright (C) 2025 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +struct calendar_month_name +{ + const char *full; + const char *abbrev; +}; + +struct calendar_date +{ + int year; + int month; /* >= 0 */ + int day; /* >= 1 */ + const struct calendar_month_name *month_names; +}; + +struct calendar +{ + /* Converts a Gregorian date + (greg_year = year, greg_month = month - 1, greg_day = day) + to a date in this calendar and returns 0. + Upon failure, returns -1. */ + int (*from_gregorian) (struct calendar_date *result, + int greg_year, int greg_month, int greg_day); + /* Format string for the %x directive. */ + const char *d_fmt; + /* Base of alternate digits (assuming UTF-8 encoding). */ + unsigned int alt_digits_base; +}; + +#include "calendar-thai.h" +#include "calendar-persian.h" +#include "calendar-ethiopian.h" diff --git a/lib/strftime.c b/lib/strftime.c index e249ae23bf..6495a6847e 100644 --- a/lib/strftime.c +++ b/lib/strftime.c @@ -53,7 +53,7 @@ /* Whether to require GNU behavior for AM and PM indicators, even on other platforms. This matters only in non-C locales. The default is to require it; you can override this via - AC_DEFINE([REQUIRE_GNUISH_STRFTIME_AM_PM], 1) and if you do that + AC_DEFINE([REQUIRE_GNUISH_STRFTIME_AM_PM], [false]) and if you do that you may be able to omit Gnulib's localename module and its dependencies. */ #ifndef REQUIRE_GNUISH_STRFTIME_AM_PM # define REQUIRE_GNUISH_STRFTIME_AM_PM true @@ -63,6 +63,24 @@ # define REQUIRE_GNUISH_STRFTIME_AM_PM false #endif +/* Whether to include support for non-Gregorian calendars (outside of the scope + of ISO C, POSIX, and glibc). This matters only in non-C locales. + The default is to include it, except on platforms where retrieving the locale + name drags in too many dependencies + (LOCALENAME_ENHANCE_LOCALE_FUNCS || !SETLOCALE_NULL_ONE_MTSAFE). + You can override this via + AC_DEFINE([SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME], [false]) + and if you do that you may be able to omit Gnulib's localename module and its + dependencies. */ +#ifndef SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME +# define SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME true +#endif +#if defined _LIBC || (HAVE_ONLY_C_LOCALE || USE_C_LOCALE) \ + || (defined __OpenBSD__ || defined _AIX || defined __ANDROID__) +# undef SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME +# define SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME false +#endif + #if HAVE_ONLY_C_LOCALE || USE_C_LOCALE # include "c-ctype.h" #else @@ -158,6 +176,16 @@ enum pad_style ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0)) #endif +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME +/* Support for non-Gregorian calendars. */ +# include "localcharset.h" +# include "localename.h" +# include "calendars.h" +# define CAL_ARGS(x,y) x, y, +#else +# define CAL_ARGS(x,y) /* empty */ +#endif + #ifdef _LIBC # define mktime_z(tz, tm) mktime (tm) @@ -867,6 +895,8 @@ static CHAR_T const c_month_names[][sizeof "September"] = static size_t __strftime_internal (STREAM_OR_CHAR_T *, STRFTIME_ARG (size_t) const CHAR_T *, const struct tm *, + CAL_ARGS (const struct calendar *, + struct calendar_date *) bool, enum pad_style, int, bool * extra_args_spec LOCALE_PARAM); @@ -1081,9 +1111,36 @@ my_strftime (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) const CHAR_T *format, const struct tm *tp extra_args_spec LOCALE_PARAM) { +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + /* Recognize whether to use a non-Gregorian calendar. */ + const struct calendar *cal = NULL; + struct calendar_date caldate; + if (strcmp (locale_charset (), "UTF-8") == 0) + { + const char *loc = gl_locale_name_unsafe (LC_TIME, "LC_TIME"); + if (strlen (loc) >= 5 && !(loc[5] >= 'A' && loc[5] <= 'Z')) + { + if (memcmp (loc, "th_TH", 5) == 0) + cal = &thai_calendar; + else if (memcmp (loc, "fa_IR", 5) == 0) + cal = &persian_calendar; + else if (memcmp (loc, "am_ET", 5) == 0) + cal = ðiopian_calendar; + if (cal != NULL) + { + if (cal->from_gregorian (&caldate, + tp->tm_year + 1900, + tp->tm_mon, + tp->tm_mday) < 0) + cal = NULL; + } + } + } +#endif bool tzset_called = false; - return __strftime_internal (s, STRFTIME_ARG (maxsize) format, tp, false, - ZERO_PAD, -1, + return __strftime_internal (s, STRFTIME_ARG (maxsize) format, tp, + CAL_ARGS (cal, &caldate) + false, ZERO_PAD, -1, &tzset_called extra_args LOCALE_ARG); } libc_hidden_def (my_strftime) @@ -1095,7 +1152,10 @@ libc_hidden_def (my_strftime) static size_t __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) const CHAR_T *format, - const struct tm *tp, bool upcase, + const struct tm *tp, + CAL_ARGS (const struct calendar *cal, + struct calendar_date *caldate) + bool upcase, enum pad_style yr_spec, int width, bool *tzset_called extra_args_spec LOCALE_PARAM) { @@ -1107,7 +1167,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) #endif int saved_errno = errno; - int hour12 = tp->tm_hour; + #ifdef _NL_CURRENT /* We cannot make the following values variables since we must delay the evaluation of these values until really needed since some @@ -1167,6 +1227,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) const char *format_end = NULL; #endif + int hour12 = tp->tm_hour; if (hour12 > 12) hour12 -= 12; else @@ -1183,6 +1244,9 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) bool negative_number; /* The number is negative. */ bool always_output_a_sign; /* +/- should always be output. */ int tz_colon_mask; /* Bitmask of where ':' should appear. */ +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + unsigned int digits_base = '0'; /* '0' or some UCS-2 value. */ +#endif const CHAR_T *subfmt; CHAR_T *bufp; CHAR_T buf[1 @@ -1430,6 +1494,14 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) } if (modifier == L_('E')) goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + { + cpy (STRLEN (caldate->month_names[caldate->month].abbrev), + caldate->month_names[caldate->month].abbrev); + break; + } +#endif #ifdef _NL_CURRENT if (modifier == L_('O')) cpy (aam_len, a_altmonth); @@ -1454,6 +1526,14 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) to_uppcase = true; to_lowcase = false; } +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + { + cpy (STRLEN (caldate->month_names[caldate->month].full), + caldate->month_names[caldate->month].full); + break; + } +#endif #ifdef _NL_CURRENT if (modifier == L_('O')) cpy (STRLEN (f_altmonth), f_altmonth); @@ -1505,13 +1585,17 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) subformat_width: { size_t len = __strftime_internal (NULL, STRFTIME_ARG ((size_t) -1) - subfmt, tp, to_uppcase, - pad, subwidth, tzset_called + subfmt, tp, + CAL_ARGS (cal, caldate) + to_uppcase, pad, subwidth, + tzset_called extra_args LOCALE_ARG); add (len, __strftime_internal (p, STRFTIME_ARG (maxsize - i) - subfmt, tp, to_uppcase, - pad, subwidth, tzset_called + subfmt, tp, + CAL_ARGS (cal, caldate) + to_uppcase, pad, subwidth, + tzset_called extra_args LOCALE_ARG)); } break; @@ -1633,6 +1717,13 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) case L_('x'): if (modifier == L_('O')) goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + { + subfmt = cal->d_fmt; + goto subformat; + } +#endif #ifdef _NL_CURRENT if (! (modifier == L_('E') && (*(subfmt = @@ -1646,6 +1737,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) #else goto underlying_strftime; #endif + case L_('D'): if (modifier != 0) goto bad_format; @@ -1656,12 +1748,20 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) if (modifier == L_('E')) goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + DO_NUMBER (2, caldate->day); +#endif DO_NUMBER (2, tp->tm_mday); case L_('e'): if (modifier == L_('E')) goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + DO_NUMBER_SPACEPAD (2, caldate->day); +#endif DO_NUMBER_SPACEPAD (2, tp->tm_mday); /* All numeric formats set DIGITS and NUMBER_VALUE (or U_NUMBER_VALUE) @@ -1703,7 +1803,10 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) negating it. */ if (modifier == L_('O') && !negative_number) { -#ifdef _NL_CURRENT +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + digits_base = cal->alt_digits_base; +#elif defined _NL_CURRENT /* Get the locale specific alternate representation of the number. If none exist NULL is returned. */ const CHAR_T *cp = nl_get_alt_digit (u_number_value @@ -1718,9 +1821,6 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) break; } } -#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) -#else - goto underlying_strftime; #endif } @@ -1734,7 +1834,13 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) if (tz_colon_mask & 1) *--bufp = ':'; tz_colon_mask >>= 1; - *--bufp = u_number_value % 10 + L_('0'); +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + *--bufp = u_number_value % 10 + (digits_base & 0xFF); + if (digits_base >= 0x100) + *--bufp = digits_base >> 8; +#else + *--bufp = u_number_value % 10 + '0'; +#endif u_number_value /= 10; } while (u_number_value != 0 || tz_colon_mask != 0); @@ -1749,8 +1855,13 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) CHAR_T sign_char = (negative_number ? L_('-') : always_output_a_sign ? L_('+') : 0); - int numlen = buf + sizeof buf / sizeof buf[0] - bufp; - int shortage = width - !!sign_char - numlen; + int number_bytes = buf + sizeof buf / sizeof buf[0] - bufp; + int number_digits = number_bytes; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (digits_base >= 0x100) + number_digits = number_bytes / 2; +#endif + int shortage = width - !!sign_char - number_digits; int padding = pad == NO_PAD || shortage <= 0 ? 0 : shortage; if (sign_char) @@ -1766,7 +1877,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) width--; } - cpy (numlen, bufp); + cpy (number_bytes, bufp); } break; @@ -1827,6 +1938,10 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) if (modifier == L_('E')) goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + DO_SIGNED_NUMBER (2, false, caldate->month + 1U); +#endif DO_SIGNED_NUMBER (2, tp->tm_mon < -1, tp->tm_mon + 1U); #ifndef _LIBC @@ -2064,9 +2179,11 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) goto underlying_strftime; #endif } - if (modifier == L_('O')) - goto bad_format; +#if SUPPORT_NON_GREG_CALENDARS_IN_STRFTIME + if (cal != NULL) + DO_YEARISH (4, false, caldate->year); +#endif DO_YEARISH (4, tp->tm_year < -TM_YEAR_BASE, tp->tm_year + (unsigned int) TM_YEAR_BASE); diff --git a/lib/strftime.h b/lib/strftime.h index 3b7e20f236..a76c98c9c8 100644 --- a/lib/strftime.h +++ b/lib/strftime.h @@ -59,6 +59,15 @@ extern "C" { date and time: %c time zone: %z %Z nanosecond %N + In locales with non-Gregorian calendars, the following conversions don't + apply in the expected way: + date: + century %C + year %y + week-based year %G %g + week in year %U %W %V + day in year %j + year, month, day %D Store the result, as a string with a trailing NUL character, at the beginning of the array __S[0..__MAXSIZE-1] and return the length of diff --git a/modules/fprintftime b/modules/fprintftime index 2ebd38377f..2eacd8526f 100644 --- a/modules/fprintftime +++ b/modules/fprintftime @@ -19,6 +19,9 @@ lib_SOURCES += fprintftime.c Include: "fprintftime.h" +Link: +@INTL_MACOSX_LIBS@ + License: GPL diff --git a/modules/nstrftime b/modules/nstrftime index 6ff773f741..c61fb1e84a 100644 --- a/modules/nstrftime +++ b/modules/nstrftime @@ -5,6 +5,10 @@ Files: lib/strftime.h lib/nstrftime.c lib/strftime.c +lib/calendars.h +lib/calendar-thai.h +lib/calendar-persian.h +lib/calendar-ethiopian.h m4/nstrftime.m4 m4/tm_gmtoff.m4 @@ -16,6 +20,7 @@ errno-h extensions intprops libc-config +localcharset localename-unsafe-limited bool stdckdint-h @@ -30,6 +35,9 @@ lib_SOURCES += nstrftime.c Include: "strftime.h" +Link: +@INTL_MACOSX_LIBS@ + License: LGPL diff --git a/modules/nstrftime-tests b/modules/nstrftime-tests index b5c6ac2fa5..91c21edf77 100644 --- a/modules/nstrftime-tests +++ b/modules/nstrftime-tests @@ -3,6 +3,10 @@ tests/test-nstrftime-1.sh tests/test-nstrftime-2.sh tests/test-nstrftime.c tests/test-nstrftime.h +tests/test-nstrftime-DE.c +tests/test-nstrftime-TH.c +tests/test-nstrftime-IR.c +tests/test-nstrftime-ET.c tests/macros.h m4/locale-fr.m4 m4/codeset.m4 @@ -12,6 +16,8 @@ Depends-on: atoll c99 intprops +localcharset +setenv setlocale strerror @@ -21,9 +27,24 @@ gt_LOCALE_FR_UTF8 gl_MUSL_LIBC Makefile.am: -TESTS += test-nstrftime-1.sh test-nstrftime-2.sh +TESTS += \ + test-nstrftime-1.sh \ + test-nstrftime-2.sh \ + test-nstrftime-DE \ + test-nstrftime-TH \ + test-nstrftime-IR \ + test-nstrftime-ET TESTS_ENVIRONMENT += \ LOCALE_FR='@LOCALE_FR@' \ LOCALE_FR_UTF8='@LOCALE_FR_UTF8@' -check_PROGRAMS += test-nstrftime -test_nstrftime_LDADD = $(LDADD) $(SETLOCALE_LIB) +check_PROGRAMS += \ + test-nstrftime \ + test-nstrftime-DE \ + test-nstrftime-TH \ + test-nstrftime-IR \ + test-nstrftime-ET +test_nstrftime_LDADD = $(LDADD) $(SETLOCALE_LIB) @INTL_MACOSX_LIBS@ +test_nstrftime_DE_LDADD = $(LDADD) $(SETLOCALE_LIB) @INTL_MACOSX_LIBS@ +test_nstrftime_TH_LDADD = $(LDADD) $(SETLOCALE_LIB) @INTL_MACOSX_LIBS@ +test_nstrftime_IR_LDADD = $(LDADD) $(SETLOCALE_LIB) @INTL_MACOSX_LIBS@ +test_nstrftime_ET_LDADD = $(LDADD) $(SETLOCALE_LIB) @INTL_MACOSX_LIBS@ diff --git a/tests/test-nstrftime-DE.c b/tests/test-nstrftime-DE.c new file mode 100644 index 0000000000..e53d469f20 --- /dev/null +++ b/tests/test-nstrftime-DE.c @@ -0,0 +1,116 @@ +/* Test of nstrftime in Germany. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +#include + +/* Specification. */ +#include "strftime.h" + +#include +#include +#include +#include + +#include "localcharset.h" +#include "macros.h" + +#if defined _WIN32 && !defined __CYGWIN__ +# define LOCALE "German_Germany.65001" +#else +# define LOCALE "de_DE.UTF-8" +#endif + +#define DECLARE_TM(variable, greg_year, greg_month, greg_day) \ + struct tm variable = \ + { \ + .tm_year = (greg_year) - 1900, \ + .tm_mon = (greg_month) - 1, \ + .tm_mday = (greg_day), \ + .tm_hour = 12, .tm_min = 34, .tm_sec = 56 \ + }; \ + /* Fill the other fields. */ \ + time_t tt = timegm (&variable); \ + gmtime_r (&tt, &variable)/*;*/ + +int +main () +{ + setenv ("LC_ALL", LOCALE, 1); + if (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0 + || strcmp (locale_charset (), "UTF-8") != 0) + { + fprintf (stderr, "Skipping test: Unicode locale for Germany is not installed\n"); + return 77; + } + +#if MUSL_LIBC + fprintf (stderr, "Skipping test: system may not have localized month names\n"); + return 77; +#elif defined __OpenBSD__ + fprintf (stderr, "Skipping test: system does not have localized month names\n"); + return 77; +#else + + char buf[100]; + size_t ret; + /* Native Windows does not support dates before 1970-01-01. */ +# if !(defined _WIN32 && !defined __CYGWIN__) + { + DECLARE_TM (tm, 1969, 12, 28); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1969-12-28") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d. %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "28. Dezember 1969") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "28.12.1969") == 0 + || strcmp (buf, "28.12.69") == 0 /* musl, NetBSD, Solaris */); + } +# endif + { + DECLARE_TM (tm, 2025, 3, 1); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2025-03-01") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d. %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1. März 2025") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "01.03.2025") == 0 + || strcmp (buf, "01.03.25") == 0 /* musl, NetBSD, Solaris */); + } + + return test_exit_status; +#endif +} diff --git a/tests/test-nstrftime-ET.c b/tests/test-nstrftime-ET.c new file mode 100644 index 0000000000..a4e0051c63 --- /dev/null +++ b/tests/test-nstrftime-ET.c @@ -0,0 +1,135 @@ +/* Test of nstrftime in Ethiopia. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +#include + +/* Specification. */ +#include "strftime.h" + +#include +#include +#include +#include + +#include "localcharset.h" +#include "macros.h" + +#if defined _WIN32 && !defined __CYGWIN__ +# define LOCALE1 "Amharic_Ethiopia.65001" +# define LOCALE2 NULL +#else +# define LOCALE1 "am_ET.UTF-8" +# define LOCALE2 "am_ET" +#endif + +#define DECLARE_TM(variable, greg_year, greg_month, greg_day) \ + struct tm variable = \ + { \ + .tm_year = (greg_year) - 1900, \ + .tm_mon = (greg_month) - 1, \ + .tm_mday = (greg_day), \ + .tm_hour = 12, .tm_min = 34, .tm_sec = 56 \ + }; \ + /* Fill the other fields. */ \ + time_t tt = timegm (&variable); \ + gmtime_r (&tt, &variable)/*;*/ + +int +main () +{ + if (((setenv ("LC_ALL", LOCALE1, 1), + (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0)) + && (LOCALE2 == NULL + || (setenv ("LC_ALL", LOCALE2, 1), + (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0)))) + || strcmp (locale_charset (), "UTF-8") != 0) + { + fprintf (stderr, "Skipping test: Unicode locale for Ethiopia is not installed\n"); + return 77; + } + +#if defined __OpenBSD__ || defined _AIX || defined __ANDROID__ + fprintf (stderr, "Skipping test: determining the locale name is not worth it on this platform\n"); + return 77; +#else + + char buf[100]; + size_t ret; + /* Native Windows does not support dates before 1970-01-01. */ +# if !(defined _WIN32 && !defined __CYGWIN__) + { + DECLARE_TM (tm, 1930, 11, 2); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1923-02-23") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "23 ጥቅምት 1923") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "23/02/1923") == 0); + } + { + DECLARE_TM (tm, 1969, 12, 28); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1962-04-19") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "19 ታኅሣሥ 1962") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "19/04/1962") == 0); + } +# endif + { + DECLARE_TM (tm, 2025, 3, 1); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2017-06-22") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "22 የካቲት 2017") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "22/06/2017") == 0); + } + + return test_exit_status; +#endif +} diff --git a/tests/test-nstrftime-IR.c b/tests/test-nstrftime-IR.c new file mode 100644 index 0000000000..5f0c518489 --- /dev/null +++ b/tests/test-nstrftime-IR.c @@ -0,0 +1,190 @@ +/* Test of nstrftime in Iran. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +#include + +/* Specification. */ +#include "strftime.h" + +#include +#include +#include +#include + +#include "localcharset.h" +#include "macros.h" + +#if defined _WIN32 && !defined __CYGWIN__ +# define LOCALE1 "Persian_Iran.65001" +# define LOCALE2 NULL +#else +# define LOCALE1 "fa_IR.UTF-8" +# define LOCALE2 "fa_IR" +#endif + +#define DECLARE_TM(variable, greg_year, greg_month, greg_day) \ + struct tm variable = \ + { \ + .tm_year = (greg_year) - 1900, \ + .tm_mon = (greg_month) - 1, \ + .tm_mday = (greg_day), \ + .tm_hour = 12, .tm_min = 34, .tm_sec = 56 \ + }; \ + /* Fill the other fields. */ \ + time_t tt = timegm (&variable); \ + gmtime_r (&tt, &variable)/*;*/ + +int +main () +{ + if (((setenv ("LC_ALL", LOCALE1, 1), + (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0)) + && (LOCALE2 == NULL + || (setenv ("LC_ALL", LOCALE2, 1), + (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0)))) + || strcmp (locale_charset (), "UTF-8") != 0) + { + fprintf (stderr, "Skipping test: Unicode locale for Iran is not installed\n"); + return 77; + } + +#if defined __OpenBSD__ || defined _AIX || defined __ANDROID__ + fprintf (stderr, "Skipping test: determining the locale name is not worth it on this platform\n"); + return 77; +#else + + char buf[100]; + size_t ret; + /* Native Windows does not support dates before 1970-01-01. */ +# if !(defined _WIN32 && !defined __CYGWIN__) + { + DECLARE_TM (tm, 1967, 10, 26); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1346-08-04") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "4 آبان 1346") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û³Û´Û¶/Û¸/Û´") == 0); + } + { + DECLARE_TM (tm, 1969, 12, 28); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1348-10-07") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "7 دی 1348") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û³Û´Û¸/Û±Û°/Û·") == 0); + } +# endif + /* Verify that 1403 is a leap year and 1404 is not. */ + { + DECLARE_TM (tm, 2024, 3, 19); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1402-12-29") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "29 اسفند 1402") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û´Û°Û²/Û±Û²/Û²Û¹") == 0); + } + { + DECLARE_TM (tm, 2024, 3, 22); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1403-01-03") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "3 فروردین 1403") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û´Û°Û³/Û±/Û³") == 0); + } + { + DECLARE_TM (tm, 2025, 3, 19); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1403-12-29") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "29 اسفند 1403") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û´Û°Û³/Û±Û²/Û²Û¹") == 0); + } + { + DECLARE_TM (tm, 2025, 3, 22); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1404-01-02") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2 فروردین 1404") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "Û±Û´Û°Û´/Û±/Û²") == 0); + } + + return test_exit_status; +#endif +} diff --git a/tests/test-nstrftime-TH.c b/tests/test-nstrftime-TH.c new file mode 100644 index 0000000000..b65cab4ed3 --- /dev/null +++ b/tests/test-nstrftime-TH.c @@ -0,0 +1,129 @@ +/* Test of nstrftime in Thailand. + Copyright (C) 2025 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Bruno Haible , 2025. */ + +#include + +/* Specification. */ +#include "strftime.h" + +#include +#include +#include +#include + +#include "localcharset.h" +#include "macros.h" + +#if defined _WIN32 && !defined __CYGWIN__ +# define LOCALE "Thai_Thailand.65001" +#else +# define LOCALE "th_TH.UTF-8" +#endif + +#define DECLARE_TM(variable, greg_year, greg_month, greg_day) \ + struct tm variable = \ + { \ + .tm_year = (greg_year) - 1900, \ + .tm_mon = (greg_month) - 1, \ + .tm_mday = (greg_day), \ + .tm_hour = 12, .tm_min = 34, .tm_sec = 56 \ + }; \ + /* Fill the other fields. */ \ + time_t tt = timegm (&variable); \ + gmtime_r (&tt, &variable)/*;*/ + +int +main () +{ + setenv ("LC_ALL", LOCALE, 1); + if (setlocale (LC_ALL, "") == NULL + || strcmp (setlocale (LC_ALL, NULL), "C") == 0 + || strcmp (locale_charset (), "UTF-8") != 0) + { + fprintf (stderr, "Skipping test: Unicode locale for Thailand is not installed\n"); + return 77; + } + +#if defined __OpenBSD__ || defined _AIX || defined __ANDROID__ + fprintf (stderr, "Skipping test: determining the locale name is not worth it on this platform\n"); + return 77; +#else + + char buf[100]; + size_t ret; + /* Native Windows does not support dates before 1970-01-01. */ +# if !(defined _WIN32 && !defined __CYGWIN__) + { + DECLARE_TM (tm, 1939, 6, 23); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2482-03-23") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d. %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "23. มิถุนายน 2482") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "23/03/2482") == 0); + } + { + DECLARE_TM (tm, 1969, 12, 28); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2512-12-28") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d. %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "28. ธันวาคม 2512") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "28/12/2512") == 0); + } +# endif + { + DECLARE_TM (tm, 2025, 3, 1); + + ret = nstrftime (buf, sizeof (buf), "%Y-%m-%d", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "2568-03-01") == 0); + + ret = nstrftime (buf, sizeof (buf), "%-d. %B %Y", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "1. มีนาคม 2568") == 0); + + ret = nstrftime (buf, sizeof (buf), "%x", + &tm, (timezone_t) 0, 0); + ASSERT (ret > 0); + ASSERT (strcmp (buf, "01/03/2568") == 0); + } + + return test_exit_status; +#endif +}