]> git.ipfire.org Git - thirdparty/gnulib.git/commitdiff
nstrftime: Add support for non-Gregorian calendars.
authorBruno Haible <bruno@clisp.org>
Tue, 15 Jul 2025 09:19:10 +0000 (11:19 +0200)
committerBruno Haible <bruno@clisp.org>
Tue, 15 Jul 2025 10:19:54 +0000 (12:19 +0200)
* 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.

14 files changed:
ChangeLog
lib/calendar-ethiopian.h [new file with mode: 0644]
lib/calendar-persian.h [new file with mode: 0644]
lib/calendar-thai.h [new file with mode: 0644]
lib/calendars.h [new file with mode: 0644]
lib/strftime.c
lib/strftime.h
modules/fprintftime
modules/nstrftime
modules/nstrftime-tests
tests/test-nstrftime-DE.c [new file with mode: 0644]
tests/test-nstrftime-ET.c [new file with mode: 0644]
tests/test-nstrftime-IR.c [new file with mode: 0644]
tests/test-nstrftime-TH.c [new file with mode: 0644]

index 6080d0ddbd42787ff9d320804eb3358e10f6c466..806e273cf87d28459ea90a2d1c0bb7cf3fa4850e 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,35 @@
+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.
diff --git a/lib/calendar-ethiopian.h b/lib/calendar-ethiopian.h
new file mode 100644 (file)
index 0000000..a6d0d9e
--- /dev/null
@@ -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 <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
diff --git a/lib/calendar-persian.h b/lib/calendar-persian.h
new file mode 100644 (file)
index 0000000..96bf97b
--- /dev/null
@@ -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 <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
diff --git a/lib/calendar-thai.h b/lib/calendar-thai.h
new file mode 100644 (file)
index 0000000..d356904
--- /dev/null
@@ -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 <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
diff --git a/lib/calendars.h b/lib/calendars.h
new file mode 100644 (file)
index 0000000..b902810
--- /dev/null
@@ -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 <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"
index e249ae23bf4a7c452efd157e3ca64ba1ccef5b41..6495a6847e0e813d3ae591f077323a377e9fc360 100644 (file)
@@ -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
 # 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);
 
index 3b7e20f236a5b85428a48fbde15d782dd1cf28eb..a76c98c9c8254af5c3240175b293724cd541a6ec 100644 (file)
@@ -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
index 2ebd38377fcef406019d3c4d794e11a903dfc992..2eacd8526f9f73e26d486f2e559d7374cadc66b5 100644 (file)
@@ -19,6 +19,9 @@ lib_SOURCES += fprintftime.c
 Include:
 "fprintftime.h"
 
+Link:
+@INTL_MACOSX_LIBS@
+
 License:
 GPL
 
index 6ff773f74142f9bf88aada78ac9980cb2ce0bc8b..c61fb1e84a6ed07b7f652f7a35e64f2247fed3f1 100644 (file)
@@ -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
 
index b5c6ac2fa50a087978510a2effa5c54eb8d7b173..91c21edf77bc873a279931c9bd2cd10cf33f5d21 100644 (file)
@@ -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 (file)
index 0000000..e53d469
--- /dev/null
@@ -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 <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
+}
diff --git a/tests/test-nstrftime-ET.c b/tests/test-nstrftime-ET.c
new file mode 100644 (file)
index 0000000..a4e0051
--- /dev/null
@@ -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 <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
+}
diff --git a/tests/test-nstrftime-IR.c b/tests/test-nstrftime-IR.c
new file mode 100644 (file)
index 0000000..5f0c518
--- /dev/null
@@ -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 <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
+}
diff --git a/tests/test-nstrftime-TH.c b/tests/test-nstrftime-TH.c
new file mode 100644 (file)
index 0000000..b65cab4
--- /dev/null
@@ -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 <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
+}