* 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 <bruno@clisp.org>
+
+ 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 <bruno@clisp.org>
nstrftime: Remove old comment about OSF/1.
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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 <stdio.h>
+#include <stdlib.h>
+
+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
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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 <stdio.h>
+#include <stdlib.h>
+
+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
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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 <stdio.h>
+#include <stdlib.h>
+
+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
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 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"
/* 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
# 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
((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)
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);
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)
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)
{
#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
const char *format_end = NULL;
#endif
+ int hour12 = tp->tm_hour;
if (hour12 > 12)
hour12 -= 12;
else
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
}
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);
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);
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;
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 =
#else
goto underlying_strftime;
#endif
+
case L_('D'):
if (modifier != 0)
goto bad_format;
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)
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
break;
}
}
-#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)
-#else
- goto underlying_strftime;
#endif
}
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);
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)
width--;
}
- cpy (numlen, bufp);
+ cpy (number_bytes, bufp);
}
break;
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
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);
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
Include:
"fprintftime.h"
+Link:
+@INTL_MACOSX_LIBS@
+
License:
GPL
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
extensions
intprops
libc-config
+localcharset
localename-unsafe-limited
bool
stdckdint-h
Include:
"strftime.h"
+Link:
+@INTL_MACOSX_LIBS@
+
License:
LGPL
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
atoll
c99
intprops
+localcharset
+setenv
setlocale
strerror
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@
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "strftime.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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
+}
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "strftime.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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
+}
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "strftime.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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
+}
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
+
+#include <config.h>
+
+/* Specification. */
+#include "strftime.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#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
+}