This patch adds proper support for the non-Gregorian calendars in use in
  - Thailand,
  - Iran,
  - Ethiopia,
to the 'nstrftime' module and, with it (automatically) to the GNU coreutils
'date' program (for output).

Why is this needed? Because these countries don't use the Gregorian calendar
primarily.

  * Thailand:
    https://en.wikipedia.org/wiki/Thai_calendar says
    "In Thailand, two main calendar systems are used alongside each other:
     the Thai solar calendar, based on the Gregorian calendar and used for
     official and most day-to-day purposes, and ..."

  * Iran:
    https://en.wikipedia.org/wiki/Solar_Hijri_calendar says
    "The Solar Hijri calendar is the official calendar of Iran."
    https://en.wikipedia.org/wiki/Solar_Hijri_calendar#Iran says
    "The present Iranian calendar was legally adopted on 31 March 1925"
    
https://en.wikipedia.org/wiki/Iranian_calendars#Modern_calendar:_Solar_Hijri_(SH)
    says "The present Iranian calendar was legally adopted on 31 March 1925"

  * Ethiopia:
    https://en.wikipedia.org/wiki/Ethiopian_calendar says it
    "is the official state civil calendar of Ethiopia".

And because glibc does not support it well. The effects of the change in a
system with libc:

* Thailand locale:

$ LC_ALL=th_TH.UTF-8 date-9.7
จ. 14 ก.ค. 2568 19:35:16 CEST
$ LC_ALL=th_TH.UTF-8 src/date
จ. 14 ก.ค. 2568 19:34:44 CEST
$ LC_ALL=th_TH.UTF-8 date-9.7 +%Y
2025
$ LC_ALL=th_TH.UTF-8 src/date +%Y
2568

You can see that glibc's strftime() supports the Thai calendar for
_some_ format directives, but not for others. In particular, it
doesn't for '%Y'.

* Iran locale:

$ LC_ALL=fa_IR date-9.7
دوشنبه ۱۴ ژوئیه ۲۵، ساعت ۱۹:۳۷:۵۱ (CEST)‬
$ LC_ALL=fa_IR src/date 
دوشنبه ۲۳ تیر ۲۵، ساعت ۱۹:۳۸:۱ (CEST)‬
$ LC_ALL=fa_IR date-9.7 +%Y-%m-%d
2025-07-14
$ LC_ALL=fa_IR src/date +%Y-%m-%d
1404-04-23

You can see that glibc's strftime() supports the Farsi digits but
not the Iranian calendar.

* Ethiopia locale:

$ LC_ALL=am_ET date-9.7
ሰኞ፣ ጁላይ 14 ቀን  7:40:21 ከሰዓት CEST 2025 ዓ/ም
$ LC_ALL=am_ET src/date
ሰኞ፣ ሐምሌ  7 ቀን  7:40:24 ከሰዓት CEST 2017 ዓ/ም
$ LC_ALL=am_ET date-9.7 +%Y-%m-%d
2025-07-14
$ LC_ALL=am_ET src/date +%Y-%m-%d
2017-11-07

You can see that glibc's strftime() prints wrong year, month, and day
values.

The ISO C 23 § 7.29.3.5 and POSIX
https://pubs.opengroup.org/onlinepubs/9799919799/functions/strftime.html
specifications specify an "alternative" choice feature. This code does
*NOT* make use of this "alternative" API.
It's better if the use of the specific calendar turns on the
particular years, months names and numbers, days etc. all at once.
Rationale:
  - Because the "alternatives" approach that makes it too easy to produce
    nonsensical output (as shown with %Y in the Thai locale, above).
  - Because there is no documentation anywhere what "alternative" actually
    means.
  - Because in ISO C and POSIX the month number is specified to be in the
    range 1..12, but some calendars (the Ethiopian calendar) have month
    numbers 1..13.
  - Because the month number to month name mapping may depend on the era
    in some calendars (the Thai calendar).

So, the code is careful not to mix Gregorian time elements (in 'struct tm')
with non-Gregorian time elements (in 'struct calendar_date').

Pádraig, you may add something like the following as a coreutils/NEWS entry:

  'date' now outputs dates in the country's native calendar for the Iranian
  locale (fa_IR) and for the Ethiopian locale (am_ET), and also does so more
  consistently for the Thailand locale (th_TH.UTF-8).


2025-07-15  Bruno Haible  <br...@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.

From 4393ea5ae8135fa72ddbda1a0124a375c4815065 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Tue, 15 Jul 2025 11:19:10 +0200
Subject: [PATCH] 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.
---
 ChangeLog                 |  32 +++++++
 lib/calendar-ethiopian.h  | 110 ++++++++++++++++++++++
 lib/calendar-persian.h    | 165 +++++++++++++++++++++++++++++++++
 lib/calendar-thai.h       | 109 ++++++++++++++++++++++
 lib/calendars.h           |  49 ++++++++++
 lib/strftime.c            | 155 +++++++++++++++++++++++++++----
 lib/strftime.h            |   9 ++
 modules/fprintftime       |   3 +
 modules/nstrftime         |   8 ++
 modules/nstrftime-tests   |  27 +++++-
 tests/test-nstrftime-DE.c | 116 +++++++++++++++++++++++
 tests/test-nstrftime-ET.c | 135 +++++++++++++++++++++++++++
 tests/test-nstrftime-IR.c | 190 ++++++++++++++++++++++++++++++++++++++
 tests/test-nstrftime-TH.c | 129 ++++++++++++++++++++++++++
 14 files changed, 1215 insertions(+), 22 deletions(-)
 create mode 100644 lib/calendar-ethiopian.h
 create mode 100644 lib/calendar-persian.h
 create mode 100644 lib/calendar-thai.h
 create mode 100644 lib/calendars.h
 create mode 100644 tests/test-nstrftime-DE.c
 create mode 100644 tests/test-nstrftime-ET.c
 create mode 100644 tests/test-nstrftime-IR.c
 create mode 100644 tests/test-nstrftime-TH.c

diff --git a/ChangeLog b/ChangeLog
index 6080d0ddbd..806e273cf8 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,35 @@
+2025-07-15  Bruno Haible  <br...@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  <br...@clisp.org>
 
 	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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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"
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 = &ethiopian_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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
+}
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
+}
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
+}
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 <https://www.gnu.org/licenses/>.  */
+
+/* Written by Bruno Haible <br...@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
+}
-- 
2.43.0

Reply via email to