diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 906387ea9..b0415eefc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,11 +11,11 @@ cache:
- _ccache/
variables:
- FEDORA_IMAGE: "registry.gitlab.gnome.org/gnome/glib/fedora:v22"
+ FEDORA_IMAGE: "registry.gitlab.gnome.org/gnome/glib/fedora:v23"
COVERITY_IMAGE: "registry.gitlab.gnome.org/gnome/glib/coverity:v7"
- DEBIAN_IMAGE: "registry.gitlab.gnome.org/gnome/glib/debian-stable:v16"
+ DEBIAN_IMAGE: "registry.gitlab.gnome.org/gnome/glib/debian-stable:v17"
ALPINE_IMAGE: "registry.gitlab.gnome.org/gnome/glib/alpine:v0"
- MINGW_IMAGE: "registry.gitlab.gnome.org/gnome/glib/mingw:v12"
+ MINGW_IMAGE: "registry.gitlab.gnome.org/gnome/glib/mingw:v13"
MESON_TEST_TIMEOUT_MULTIPLIER: 4
G_MESSAGES_DEBUG: all
MESON_COMMON_OPTIONS: "--buildtype debug --wrap-mode=nodownload --fatal-meson-warnings"
diff --git a/.gitlab-ci/debian-stable.Dockerfile b/.gitlab-ci/debian-stable.Dockerfile
index 592a6b356..027829bd3 100644
--- a/.gitlab-ci/debian-stable.Dockerfile
+++ b/.gitlab-ci/debian-stable.Dockerfile
@@ -64,6 +64,7 @@ RUN locale-gen de_DE.UTF-8 \
&& locale-gen lt_LT.UTF-8 \
&& locale-gen pl_PL.UTF-8 \
&& locale-gen ru_RU.UTF-8 \
+ && locale-gen th_TH.UTF-8 \
&& locale-gen tr_TR.UTF-8
ENV LANG=C.UTF-8 LANGUAGE=C.UTF-8 LC_ALL=C.UTF-8
diff --git a/.gitlab-ci/fedora.Dockerfile b/.gitlab-ci/fedora.Dockerfile
index 5e28542ab..057db215f 100644
--- a/.gitlab-ci/fedora.Dockerfile
+++ b/.gitlab-ci/fedora.Dockerfile
@@ -32,6 +32,7 @@ RUN dnf -y update \
glibc-langpack-lt \
glibc-langpack-pl \
glibc-langpack-ru \
+ glibc-langpack-th \
glibc-langpack-tr \
"gnome-desktop-testing >= 2018.1" \
gtk-doc \
diff --git a/.gitlab-ci/mingw.Dockerfile b/.gitlab-ci/mingw.Dockerfile
index d801255ce..f95171836 100644
--- a/.gitlab-ci/mingw.Dockerfile
+++ b/.gitlab-ci/mingw.Dockerfile
@@ -1,4 +1,4 @@
-FROM registry.gitlab.gnome.org/gnome/glib/fedora:v22
+FROM registry.gitlab.gnome.org/gnome/glib/fedora:v23
USER root
diff --git a/glib/gdatetime-private.c b/glib/gdatetime-private.c
new file mode 100644
index 000000000..19f575a7d
--- /dev/null
+++ b/glib/gdatetime-private.c
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2023 Philip Withnall
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library 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 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, see .
+ */
+
+#include "glib.h"
+#include "gdatetime-private.h"
+
+/**
+ * _g_era_date_compare:
+ * @date1: first date
+ * @date2: second date
+ *
+ * Compare two #GEraDates for ordering, taking into account negative and
+ * positive infinity.
+ *
+ * Returns: strcmp()-style integer, `<0` indicates `date1 < date2`, `0`
+ * indicates `date1 == date2`, `>0` indicates `date1 > date2`
+ * Since: 2.80
+ */
+int
+_g_era_date_compare (const GEraDate *date1,
+ const GEraDate *date2)
+{
+ if (date1->type == G_ERA_DATE_SET &&
+ date2->type == G_ERA_DATE_SET)
+ {
+ if (date1->year != date2->year)
+ return date1->year - date2->year;
+ if (date1->month != date2->month)
+ return date1->month - date2->month;
+ return date1->day - date2->day;
+ }
+
+ if (date1->type == date2->type)
+ return 0;
+
+ if (date1->type == G_ERA_DATE_MINUS_INFINITY || date2->type == G_ERA_DATE_PLUS_INFINITY)
+ return -1;
+ if (date1->type == G_ERA_DATE_PLUS_INFINITY || date2->type == G_ERA_DATE_MINUS_INFINITY)
+ return 1;
+
+ g_assert_not_reached ();
+}
+
+static gboolean
+parse_era_date (const char *str,
+ const char *endptr,
+ GEraDate *out_date)
+{
+ const char *str_endptr = NULL;
+ int year_multiplier;
+ guint64 year, month, day;
+
+ year_multiplier = (str[0] == '-') ? -1 : 1;
+ if (str[0] == '-' || str[0] == '+')
+ str++;
+
+ year = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
+ g_assert (str_endptr <= endptr);
+ if (str_endptr == endptr || *str_endptr != '/' || year > G_MAXINT)
+ return FALSE;
+ str = str_endptr + 1;
+
+ month = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
+ g_assert (str_endptr <= endptr);
+ if (str_endptr == endptr || *str_endptr != '/' || month < 1 || month > 12)
+ return FALSE;
+ str = str_endptr + 1;
+
+ day = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
+ g_assert (str_endptr <= endptr);
+ if (str_endptr != endptr || day < 1 || day > 31)
+ return FALSE;
+
+ /* Success */
+ out_date->type = G_ERA_DATE_SET;
+ out_date->year = year_multiplier * year;
+ out_date->month = month;
+ out_date->day = day;
+
+ return TRUE;
+}
+
+/**
+ * _g_era_description_segment_ref:
+ * @segment: a #GEraDescriptionSegment
+ *
+ * Increase the ref count of @segment.
+ *
+ * Returns: (transfer full): @segment
+ * Since: 2.80
+ */
+GEraDescriptionSegment *
+_g_era_description_segment_ref (GEraDescriptionSegment *segment)
+{
+ g_atomic_ref_count_inc (&segment->ref_count);
+ return segment;
+}
+
+/**
+ * _g_era_description_segment_unref:
+ * @segment: (transfer full): a #GEraDescriptionSegment to unref
+ *
+ * Decreases the ref count of @segment.
+ *
+ * Since: 2.80
+ */
+void
+_g_era_description_segment_unref (GEraDescriptionSegment *segment)
+{
+ if (g_atomic_ref_count_dec (&segment->ref_count))
+ {
+ g_free (segment->era_format);
+ g_free (segment->era_name);
+ g_free (segment);
+ }
+}
+
+/**
+ * _g_era_description_parse:
+ * @desc: an `ERA` description string from `nl_langinfo()`
+ *
+ * Parse an ERA description string. See [`nl_langinfo(3)`](man:nl_langinfo(3)).
+ *
+ * Example description string for th_TR.UTF-8:
+ * ```
+ * +:1:-543/01/01:+*:พ.ศ.:%EC %Ey
+ * ```
+ *
+ * @desc must be in UTF-8, so all conversion from the locale encoding must
+ * happen before this function is called. The resulting `era_name` and
+ * `era_format` in the returned segments will be in UTF-8.
+ *
+ * Returns: (transfer full) (nullable) (element-type GEraDescriptionSegment):
+ * array of one or more parsed era segments, or %NULL if parsing failed
+ * Since: 2.80
+ */
+GPtrArray *
+_g_era_description_parse (const char *desc)
+{
+ GPtrArray *segments = g_ptr_array_new_with_free_func ((GDestroyNotify) _g_era_description_segment_unref);
+
+ for (const char *p = desc; *p != '\0';)
+ {
+ const char *next_colon, *endptr = NULL;
+ GEraDescriptionSegment *segment = NULL;
+ char direction;
+ guint64 offset;
+ GEraDate start_date, end_date;
+ char *era_name = NULL, *era_format = NULL;
+
+ /* direction */
+ direction = *p++;
+ if (direction != '+' && direction != '-')
+ goto error;
+
+ if (*p++ != ':')
+ goto error;
+
+ /* offset */
+ next_colon = strchr (p, ':');
+ if (next_colon == NULL)
+ goto error;
+
+ offset = g_ascii_strtoull (p, (gchar **) &endptr, 10);
+ if (endptr != next_colon)
+ goto error;
+ p = next_colon + 1;
+
+ /* start_date */
+ next_colon = strchr (p, ':');
+ if (next_colon == NULL)
+ goto error;
+
+ if (!parse_era_date (p, next_colon, &start_date))
+ goto error;
+ p = next_colon + 1;
+
+ /* end_date */
+ next_colon = strchr (p, ':');
+ if (next_colon == NULL)
+ goto error;
+
+ if (strncmp (p, "-*", 2) == 0)
+ end_date.type = G_ERA_DATE_MINUS_INFINITY;
+ else if (strncmp (p, "+*", 2) == 0)
+ end_date.type = G_ERA_DATE_PLUS_INFINITY;
+ else if (!parse_era_date (p, next_colon, &end_date))
+ goto error;
+ p = next_colon + 1;
+
+ /* era_name */
+ next_colon = strchr (p, ':');
+ if (next_colon == NULL)
+ goto error;
+
+ if (next_colon - p == 0)
+ goto error;
+ era_name = g_strndup (p, next_colon - p);
+ p = next_colon + 1;
+
+ /* era_format; either the final field in the segment (followed by a
+ * semicolon) or the description (followed by nul) */
+ next_colon = strchr (p, ';');
+ if (next_colon == NULL)
+ next_colon = p + strlen (p);
+
+ if (next_colon - p == 0)
+ {
+ g_free (era_name);
+ goto error;
+ }
+ era_format = g_strndup (p, next_colon - p);
+ if (*next_colon == ';')
+ p = next_colon + 1;
+ else
+ p = next_colon;
+
+ /* Successfully parsed that segment. */
+ segment = g_new0 (GEraDescriptionSegment, 1);
+ g_atomic_ref_count_init (&segment->ref_count);
+ segment->offset = offset;
+ segment->start_date = start_date;
+ segment->end_date = end_date;
+ segment->direction_multiplier =
+ ((_g_era_date_compare (&segment->start_date, &segment->end_date) <= 0) ? 1 : -1) *
+ ((direction == '-') ? -1 : 1);
+ segment->era_name = g_steal_pointer (&era_name);
+ segment->era_format = g_steal_pointer (&era_format);
+
+ g_ptr_array_add (segments, g_steal_pointer (&segment));
+ }
+
+ return g_steal_pointer (&segments);
+
+error:
+ g_ptr_array_unref (segments);
+ return NULL;
+}
diff --git a/glib/gdatetime-private.h b/glib/gdatetime-private.h
new file mode 100644
index 000000000..3e804dd47
--- /dev/null
+++ b/glib/gdatetime-private.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 Philip Withnall
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library 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 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, see .
+ */
+
+#pragma once
+
+#include "glib.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GEraDate:
+ * @type: the type of date
+ * @year: year of the date, in the Gregorian calendar
+ * @month: month of the date, in the Gregorian calendar
+ * @day: day of the date, in the Gregorian calendar
+ *
+ * A date from a #GEraDescriptionSegment.
+ *
+ * If @type is %G_ERA_DATE_SET, @year, @month and @day are valid. Otherwise,
+ * they are undefined.
+ *
+ * Since: 2.80
+ */
+typedef struct {
+ enum {
+ G_ERA_DATE_SET,
+ G_ERA_DATE_PLUS_INFINITY,
+ G_ERA_DATE_MINUS_INFINITY,
+ } type;
+ int year;
+ int month;
+ int day;
+} GEraDate;
+
+int _g_era_date_compare (const GEraDate *date1,
+ const GEraDate *date2);
+
+/**
+ * GEraDescriptionSegment:
+ * @ref_count: reference count
+ * @direction_multiplier: `-1` or `1` depending on the order of @start_date and
+ * @end_date
+ * @offset: offset of the first year in the era
+ * @start_date: start date (in the Gregorian calendar) of the era
+ * @end_date: end date (in the Gregorian calendar) of the era
+ * @era_name: (not nullable): name of the era
+ * @era_format: (not nullable): format string to use for `%EY`
+ *
+ * A segment of an `ERA` description string, describing a single era. See
+ * [`nl_langinfo(3)`](man:nl_langinfo(3)).
+ *
+ * Since: 2.80
+ */
+typedef struct {
+ gatomicrefcount ref_count;
+ int direction_multiplier;
+ guint64 offset;
+ GEraDate start_date; /* inclusive */
+ GEraDate end_date; /* inclusive */
+ char *era_name; /* UTF-8 encoded */
+ char *era_format; /* UTF-8 encoded */
+} GEraDescriptionSegment;
+
+GPtrArray *_g_era_description_parse (const char *desc);
+
+GEraDescriptionSegment *_g_era_description_segment_ref (GEraDescriptionSegment *segment);
+void _g_era_description_segment_unref (GEraDescriptionSegment *segment);
+
+G_END_DECLS
diff --git a/glib/gdatetime.c b/glib/gdatetime.c
index a41e1bfee..ae50c27c7 100644
--- a/glib/gdatetime.c
+++ b/glib/gdatetime.c
@@ -69,6 +69,7 @@
#include "gconvert.h"
#include "gconvertprivate.h"
#include "gdatetime.h"
+#include "gdatetime-private.h"
#include "gfileutils.h"
#include "ghash.h"
#include "glibintl.h"
@@ -162,8 +163,11 @@ static const guint16 days_in_year[2][13] =
#define GET_AMPM_IS_LOCALE TRUE
#define PREFERRED_DATE_TIME_FMT nl_langinfo (D_T_FMT)
+#define PREFERRED_ERA_DATE_TIME_FMT nl_langinfo (ERA_D_T_FMT)
#define PREFERRED_DATE_FMT nl_langinfo (D_FMT)
+#define PREFERRED_ERA_DATE_FMT nl_langinfo (ERA_D_FMT)
#define PREFERRED_TIME_FMT nl_langinfo (T_FMT)
+#define PREFERRED_ERA_TIME_FMT nl_langinfo (ERA_T_FMT)
#define PREFERRED_12HR_TIME_FMT nl_langinfo (T_FMT_AMPM)
static const gint weekday_item[2][7] =
@@ -187,6 +191,10 @@ static const gint month_item[2][12] =
#define MONTH_FULL(d) nl_langinfo (month_item[1][g_date_time_get_month (d) - 1])
#define MONTH_FULL_IS_LOCALE TRUE
+#define ERA_DESCRIPTION nl_langinfo (ERA)
+#define ERA_DESCRIPTION_IS_LOCALE TRUE
+#define ERA_DESCRIPTION_N_SEGMENTS (int) (gintptr) nl_langinfo (_NL_TIME_ERA_NUM_ENTRIES)
+
#else
#define GET_AMPM(d) (get_fallback_ampm (g_date_time_get_hour (d)))
@@ -194,12 +202,15 @@ static const gint month_item[2][12] =
/* Translators: this is the preferred format for expressing the date and the time */
#define PREFERRED_DATE_TIME_FMT C_("GDateTime", "%a %b %e %H:%M:%S %Y")
+#define PREFERRED_ERA_DATE_TIME_FMT PREFERRED_DATE_TIME_FMT
/* Translators: this is the preferred format for expressing the date */
#define PREFERRED_DATE_FMT C_("GDateTime", "%m/%d/%y")
+#define PREFERRED_ERA_DATE_FMT PREFERRED_DATE_FMT
/* Translators: this is the preferred format for expressing the time */
#define PREFERRED_TIME_FMT C_("GDateTime", "%H:%M:%S")
+#define PREFERRED_ERA_TIME_FMT PREFERRED_TIME_FMT
/* Translators: this is the preferred format for expressing 12 hour time */
#define PREFERRED_12HR_TIME_FMT C_("GDateTime", "%I:%M:%S %p")
@@ -219,6 +230,10 @@ static const gint month_item[2][12] =
#define MONTH_FULL(d) (get_month_name_standalone (g_date_time_get_month (d)))
#define MONTH_FULL_IS_LOCALE FALSE
+#define ERA_DESCRIPTION NULL
+#define ERA_DESCRIPTION_IS_LOCALE FALSE
+#define ERA_DESCRIPTION_N_SEGMENTS 0
+
static const gchar *
get_month_name_standalone (gint month)
{
@@ -2865,6 +2880,131 @@ initialize_alt_digits (void)
}
#endif /* HAVE_LANGINFO_OUTDIGIT */
+/* Look up the era which contains @datetime, in the ERA description from libc
+ * which corresponds to the currently set LC_TIME locale. The ERA is parsed and
+ * cached the first time this function is called (or when LC_TIME changes).
+ * See nl_langinfo(3).
+ *
+ * The return value is (transfer full). */
+static GEraDescriptionSegment *
+date_time_lookup_era (GDateTime *datetime,
+ gboolean locale_is_utf8)
+{
+ static GMutex era_mutex;
+ static GPtrArray *static_era_description = NULL; /* (mutex era_mutex) (element-type GEraDescriptionSegment) */
+ static const char *static_era_description_locale = NULL; /* (mutex era_mutex) */
+ const char *current_lc_time = setlocale (LC_TIME, NULL);
+ GPtrArray *local_era_description; /* (element-type GEraDescriptionSegment) */
+ GEraDate datetime_date;
+
+ g_mutex_lock (&era_mutex);
+
+ if (static_era_description_locale != current_lc_time)
+ {
+ const char *era_description_str;
+ size_t era_description_str_len;
+ char *tmp = NULL;
+
+ era_description_str = ERA_DESCRIPTION;
+ if (era_description_str != NULL)
+ {
+ /* FIXME: glibc 2.37 seems to return the era segments nul-separated rather
+ * than semicolon-separated (which is what nl_langinfo(3) specifies).
+ * Fix that up before sending it to the parsing code.
+ * See https://sourceware.org/bugzilla/show_bug.cgi?id=31030*/
+ {
+ /* Work out the length of the whole description string, regardless
+ * of whether it uses nuls or semicolons as separators. */
+ int n_entries = ERA_DESCRIPTION_N_SEGMENTS;
+ const char *s = era_description_str;
+
+ for (int i = 1; i < n_entries; i++)
+ {
+ const char *next_semicolon = strchr (s, ';');
+ const char *next_nul = strchr (s, '\0');
+
+ if (next_semicolon != NULL && next_semicolon < next_nul)
+ s = next_semicolon + 1;
+ else
+ s = next_nul + 1;
+ }
+
+ era_description_str_len = strlen (s) + (s - era_description_str);
+
+ /* Replace all the nuls with semicolons. */
+ era_description_str = tmp = g_memdup2 (era_description_str, era_description_str_len + 1);
+ s = era_description_str;
+
+ for (int i = 1; i < n_entries; i++)
+ {
+ char *next_nul = strchr (s, '\0');
+
+ if ((size_t) (next_nul - era_description_str) >= era_description_str_len)
+ break;
+
+ *next_nul = ';';
+ s = next_nul + 1;
+ }
+ }
+
+ /* Convert from the LC_TIME encoding to UTF-8 if needed. */
+ if (!locale_is_utf8 && ERA_DESCRIPTION_IS_LOCALE)
+ {
+ char *tmp2 = NULL;
+ era_description_str = tmp2 = g_locale_to_utf8 (era_description_str, -1, NULL, NULL, NULL);
+ g_free (tmp);
+ tmp = g_steal_pointer (&tmp2);
+ }
+
+ g_clear_pointer (&static_era_description, g_ptr_array_unref);
+
+ if (era_description_str != NULL)
+ static_era_description = _g_era_description_parse (era_description_str);
+ }
+
+ if (static_era_description == NULL)
+ g_warning ("Could not parse ERA description: %s", era_description_str);
+
+ g_free (tmp);
+
+ static_era_description_locale = current_lc_time;
+ }
+
+ if (static_era_description == NULL)
+ {
+ g_mutex_unlock (&era_mutex);
+ return NULL;
+ }
+
+ local_era_description = g_ptr_array_ref (static_era_description);
+ g_mutex_unlock (&era_mutex);
+
+ /* Search through the eras and see if one matches. */
+ datetime_date.type = G_ERA_DATE_SET;
+ datetime_date.year = g_date_time_get_year (datetime);
+ datetime_date.month = g_date_time_get_month (datetime);
+ datetime_date.day = g_date_time_get_day_of_month (datetime);
+
+ for (unsigned int i = 0; i < local_era_description->len; i++)
+ {
+ GEraDescriptionSegment *segment = g_ptr_array_index (local_era_description, i);
+
+ if ((_g_era_date_compare (&segment->start_date, &datetime_date) <= 0 &&
+ _g_era_date_compare (&datetime_date, &segment->end_date) <= 0) ||
+ (_g_era_date_compare (&segment->end_date, &datetime_date) <= 0 &&
+ _g_era_date_compare (&datetime_date, &segment->start_date) <= 0))
+ {
+ /* @datetime is within this era segment. */
+ g_ptr_array_unref (local_era_description);
+ return _g_era_description_segment_ref (segment);
+ }
+ }
+
+ g_ptr_array_unref (local_era_description);
+
+ return NULL;
+}
+
static void
format_number (GString *str,
gboolean use_alt_digits,
@@ -3043,6 +3183,7 @@ g_date_time_format_utf8 (GDateTime *datetime,
guint colons;
gunichar c;
gboolean alt_digits = FALSE;
+ gboolean alt_era = FALSE;
gboolean pad_set = FALSE;
gboolean mod_case = FALSE;
gboolean name_is_utf8;
@@ -3069,6 +3210,7 @@ g_date_time_format_utf8 (GDateTime *datetime,
colons = 0;
alt_digits = FALSE;
+ alt_era = FALSE;
pad_set = FALSE;
mod_case = FALSE;
@@ -3129,14 +3271,31 @@ g_date_time_format_utf8 (GDateTime *datetime,
break;
case 'c':
{
- if (g_strcmp0 (PREFERRED_DATE_TIME_FMT, "") == 0)
+ const char *subformat = alt_era ? PREFERRED_ERA_DATE_TIME_FMT : PREFERRED_DATE_TIME_FMT;
+
+ /* Fallback */
+ if (alt_era && g_strcmp0 (subformat, "") == 0)
+ subformat = PREFERRED_DATE_TIME_FMT;
+
+ if (g_strcmp0 (subformat, "") == 0)
return FALSE;
- if (!g_date_time_format_locale (datetime, PREFERRED_DATE_TIME_FMT,
+ if (!g_date_time_format_locale (datetime, subformat,
outstr, locale_is_utf8))
return FALSE;
}
break;
case 'C':
+ if (alt_era)
+ {
+ GEraDescriptionSegment *era = date_time_lookup_era (datetime, locale_is_utf8);
+ if (era != NULL)
+ {
+ g_string_append (outstr, era->era_name);
+ _g_era_description_segment_unref (era);
+ break;
+ }
+ }
+
format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
g_date_time_get_year (datetime) / 100);
break;
@@ -3214,6 +3373,9 @@ g_date_time_format_utf8 (GDateTime *datetime,
case 'O':
alt_digits = TRUE;
goto next_mod;
+ case 'E':
+ alt_era = TRUE;
+ goto next_mod;
case 'p':
if (!format_ampm (datetime, outstr, locale_is_utf8,
mod_case && g_strcmp0 (mod, "#") == 0 ? FALSE
@@ -3270,29 +3432,78 @@ g_date_time_format_utf8 (GDateTime *datetime,
break;
case 'x':
{
- if (g_strcmp0 (PREFERRED_DATE_FMT, "") == 0)
+ const char *subformat = alt_era ? PREFERRED_ERA_DATE_FMT : PREFERRED_DATE_FMT;
+
+ /* Fallback */
+ if (alt_era && g_strcmp0 (subformat, "") == 0)
+ subformat = PREFERRED_DATE_FMT;
+
+ if (g_strcmp0 (subformat, "") == 0)
return FALSE;
- if (!g_date_time_format_locale (datetime, PREFERRED_DATE_FMT,
+ if (!g_date_time_format_locale (datetime, subformat,
outstr, locale_is_utf8))
return FALSE;
}
break;
case 'X':
{
- if (g_strcmp0 (PREFERRED_TIME_FMT, "") == 0)
+ const char *subformat = alt_era ? PREFERRED_ERA_TIME_FMT : PREFERRED_TIME_FMT;
+
+ /* Fallback */
+ if (alt_era && g_strcmp0 (subformat, "") == 0)
+ subformat = PREFERRED_TIME_FMT;
+
+ if (g_strcmp0 (subformat, "") == 0)
return FALSE;
- if (!g_date_time_format_locale (datetime, PREFERRED_TIME_FMT,
+ if (!g_date_time_format_locale (datetime, subformat,
outstr, locale_is_utf8))
return FALSE;
}
break;
case 'y':
- format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
- g_date_time_get_year (datetime) % 100);
+ if (alt_era)
+ {
+ GEraDescriptionSegment *era = date_time_lookup_era (datetime, locale_is_utf8);
+ if (era != NULL)
+ {
+ int delta = g_date_time_get_year (datetime) - era->start_date.year;
+
+ /* Both these years are in the Gregorian calendar (CE/BCE),
+ * which has no year zero. So take one from the delta if they
+ * cross across where year zero would be. */
+ if ((g_date_time_get_year (datetime) < 0) != (era->start_date.year < 0))
+ delta -= 1;
+
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ era->offset + delta * era->direction_multiplier);
+ _g_era_description_segment_unref (era);
+ break;
+ }
+ }
+
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ g_date_time_get_year (datetime) % 100);
break;
case 'Y':
- format_number (outstr, alt_digits, 0, 0,
- g_date_time_get_year (datetime));
+ if (alt_era)
+ {
+ GEraDescriptionSegment *era = date_time_lookup_era (datetime, locale_is_utf8);
+ if (era != NULL)
+ {
+ if (!g_date_time_format_utf8 (datetime, era->era_format,
+ outstr, locale_is_utf8))
+ {
+ _g_era_description_segment_unref (era);
+ return FALSE;
+ }
+
+ _g_era_description_segment_unref (era);
+ break;
+ }
+ }
+
+ format_number (outstr, alt_digits, 0, 0,
+ g_date_time_get_year (datetime));
break;
case 'z':
{
@@ -3461,6 +3672,22 @@ g_date_time_format_utf8 (GDateTime *datetime,
* `strftime()` extension expected to be added to the future POSIX specification,
* `%Ob` and `%Oh` are GNU `strftime()` extensions. Since: 2.56
*
+ * Since GLib 2.80, when `E` is used with `%c`, `%C`, `%x`, `%X`, `%y` or `%Y`,
+ * the date is formatted using an alternate era representation specific to the
+ * locale. This is typically used for the Thai solar calendar or Japanese era
+ * names, for example.
+ *
+ * - `%Ec`: the preferred date and time representation for the current locale,
+ * using the alternate era representation
+ * - `%EC`: the name of the era
+ * - `%Ex`: the preferred date representation for the current locale without
+ * the time, using the alternate era representation
+ * - `%EX`: the preferred time representation for the current locale without
+ * the date, using the alternate era representation
+ * - `%Ey`: the year since the beginning of the era denoted by the `%EC`
+ * specifier
+ * - `%EY`: the full alternative year representation
+ *
* Returns: (transfer full) (nullable): a newly allocated string formatted to
* the requested format or %NULL in the case that there was an error (such
* as a format specifier not being supported in the current locale). The
diff --git a/glib/meson.build b/glib/meson.build
index 5940e6bc3..95e863e46 100644
--- a/glib/meson.build
+++ b/glib/meson.build
@@ -277,6 +277,7 @@ glib_sources += files(
'gdataset.c',
'gdate.c',
'gdatetime.c',
+ 'gdatetime-private.c',
'gdir.c',
'genviron.c',
'gerror.c',
diff --git a/glib/tests/gdatetime.c b/glib/tests/gdatetime.c
index f7aa9a2d2..52d8a3d49 100644
--- a/glib/tests/gdatetime.c
+++ b/glib/tests/gdatetime.c
@@ -1,6 +1,7 @@
/* gdatetime-tests.c
*
* Copyright (C) 2009-2010 Christian Hergert
+ * Copyright 2023 Philip Withnall
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@@ -28,6 +29,8 @@
#include
#include
+#include "gdatetime-private.h"
+
#ifdef G_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include
@@ -1718,6 +1721,12 @@ test_non_utf8_printf (void)
TEST_PRINTF ("%%", "%");
TEST_PRINTF ("%", "");
TEST_PRINTF ("%9", NULL);
+ TEST_PRINTF ("%Ec", "平成21年10月24日 00時00分00秒");
+ TEST_PRINTF ("%EC", "平成");
+ TEST_PRINTF ("%Ex", "平成21年10月24日");
+ TEST_PRINTF ("%EX", "00時00分00秒");
+ TEST_PRINTF ("%Ey", "21");
+ TEST_PRINTF ("%EY", "平成21年");
setlocale (LC_ALL, oldlocale);
g_free (oldlocale);
@@ -1873,6 +1882,30 @@ test_modifiers (void)
g_test_skip ("langinfo not available, skipping O modifier tests");
#endif
+ setlocale (LC_ALL, "en_GB.utf-8");
+ if (strstr (setlocale (LC_ALL, NULL), "en_GB") != NULL)
+ {
+ TEST_PRINTF_DATE (2009, 1, 1, "%c", "thu 01 jan 2009 00:00:00 utc");
+ TEST_PRINTF_DATE (2009, 1, 1, "%Ec", "thu 01 jan 2009 00:00:00 utc");
+
+ TEST_PRINTF_DATE (2009, 1, 1, "%C", "20");
+ TEST_PRINTF_DATE (2009, 1, 1, "%EC", "20");
+
+ TEST_PRINTF_DATE (2009, 1, 2, "%x", "02/01/09");
+ TEST_PRINTF_DATE (2009, 1, 2, "%Ex", "02/01/09");
+
+ TEST_PRINTF_TIME (1, 2, 3, "%X", "01:02:03");
+ TEST_PRINTF_TIME (1, 2, 3, "%EX", "01:02:03");
+
+ TEST_PRINTF_DATE (2009, 1, 1, "%y", "09");
+ TEST_PRINTF_DATE (2009, 1, 1, "%Ey", "09");
+
+ TEST_PRINTF_DATE (2009, 1, 1, "%Y", "2009");
+ TEST_PRINTF_DATE (2009, 1, 1, "%EY", "2009");
+ }
+ else
+ g_test_skip ("locale en_GB not available, skipping E modifier tests");
+
setlocale (LC_ALL, oldlocale);
g_free (oldlocale);
}
@@ -2212,6 +2245,164 @@ test_all_dates (void)
g_time_zone_unref (timezone);
}
+static void
+test_date_time_eras_japan (void)
+{
+ gchar *oldlocale;
+
+ oldlocale = g_strdup (setlocale (LC_ALL, NULL));
+ setlocale (LC_ALL, "ja_JP.utf-8");
+ if (strstr (setlocale (LC_ALL, NULL), "ja_JP") == NULL)
+ {
+ g_test_skip ("locale ja_JP.utf-8 not available, skipping Japanese era tests");
+ g_free (oldlocale);
+ return;
+ }
+
+ /* See https://en.wikipedia.org/wiki/Japanese_era_name
+ * First test the Reiwa era (令和) */
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ec", "令和05年06月01日 00時00分00秒");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EC", "令和");
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ex", "令和05年06月01日");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EX", "00時00分00秒");
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ey", "05");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EY", "令和05年");
+
+ /* Heisei era (平成) */
+ TEST_PRINTF_DATE (2019, 04, 30, "%Ec", "平成31年04月30日 00時00分00秒");
+ TEST_PRINTF_DATE (2019, 04, 30, "%EC", "平成");
+ TEST_PRINTF_DATE (2019, 04, 30, "%Ex", "平成31年04月30日");
+ TEST_PRINTF_DATE (2019, 04, 30, "%EX", "00時00分00秒");
+ TEST_PRINTF_DATE (2019, 04, 30, "%Ey", "31");
+ TEST_PRINTF_DATE (2019, 04, 30, "%EY", "平成31年");
+
+ /* Shōwa era (昭和) */
+ TEST_PRINTF_DATE (1926, 12, 25, "%Ec", "昭和元年12月25日 00時00分00秒");
+ TEST_PRINTF_DATE (1926, 12, 25, "%EC", "昭和");
+ TEST_PRINTF_DATE (1926, 12, 25, "%Ex", "昭和元年12月25日");
+ TEST_PRINTF_DATE (1926, 12, 25, "%EX", "00時00分00秒");
+ TEST_PRINTF_DATE (1926, 12, 25, "%Ey", "01");
+ TEST_PRINTF_DATE (1926, 12, 25, "%EY", "昭和元年");
+
+ setlocale (LC_ALL, oldlocale);
+ g_free (oldlocale);
+}
+
+static void
+test_date_time_eras_thailand (void)
+{
+ gchar *oldlocale;
+
+ oldlocale = g_strdup (setlocale (LC_ALL, NULL));
+ setlocale (LC_ALL, "th_TH.utf-8");
+ if (strstr (setlocale (LC_ALL, NULL), "th_TH") == NULL)
+ {
+ g_test_skip ("locale th_TH.utf-8 not available, skipping Thai era tests");
+ g_free (oldlocale);
+ return;
+ }
+
+ /* See https://en.wikipedia.org/wiki/Thai_solar_calendar */
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ec", "วันพฤหัสบดีที่ 1 มิถุนายน พ.ศ. 2566, 00.00.00 น.");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EC", "พ.ศ.");
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ex", " 1 มิ.ย. 2566");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EX", "00.00.00 น.");
+ TEST_PRINTF_DATE (2023, 06, 01, "%Ey", "2566");
+ TEST_PRINTF_DATE (2023, 06, 01, "%EY", "พ.ศ. 2566");
+
+ TEST_PRINTF_DATE (01, 06, 01, "%Ex", " 1 มิ.ย. 544");
+
+ setlocale (LC_ALL, oldlocale);
+ g_free (oldlocale);
+}
+
+static void
+test_date_time_eras_parsing (void)
+{
+ struct
+ {
+ const char *desc;
+ gboolean expected_success;
+ size_t expected_n_segments;
+ }
+ vectors[] =
+ {
+ /* Some successful parsing: */
+ { "", TRUE, 0 },
+ /* From https://github.com/bminor/glibc/blob/9fd3409842b3e2d31cff5dbd6f96066c430f0aa2/localedata/locales/th_TH#L233: */
+ { "+:1:-543/01/01:+*:พ.ศ.:%EC %Ey", TRUE, 1 },
+ /* From https://github.com/bminor/glibc/blob/9fd3409842b3e2d31cff5dbd6f96066c430f0aa2/localedata/locales/ja_JP#L14967C5-L14977C60: */
+ { "+:2:2020/01/01:+*:令和:%EC%Ey年;"
+ "+:1:2019/05/01:2019/12/31:令和:%EC元年;"
+ "+:2:1990/01/01:2019/04/30:平成:%EC%Ey年;"
+ "+:1:1989/01/08:1989/12/31:平成:%EC元年;"
+ "+:2:1927/01/01:1989/01/07:昭和:%EC%Ey年;"
+ "+:1:1926/12/25:1926/12/31:昭和:%EC元年;"
+ "+:2:1913/01/01:1926/12/24:大正:%EC%Ey年;"
+ "+:1:1912/07/30:1912/12/31:大正:%EC元年;"
+ "+:6:1873/01/01:1912/07/29:明治:%EC%Ey年;"
+ "+:1:0001/01/01:1872/12/31:西暦:%EC%Ey年;"
+ "+:1:-0001/12/31:-*:紀元前:%EC%Ey年", TRUE, 11 },
+ { "-:2:2020/01/01:-*:令和:%EC%Ey年", TRUE, 1 },
+ { "+:2:2020/01/01:2020/01/01:令和:%EC%Ey年", TRUE, 1 },
+ { "+:2:+2020/01/01:+*:令和:%EC%Ey年", TRUE, 1 },
+ /* Some errors: */
+ { ".:2:2020/01/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+.2:2020/01/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+", FALSE, 0 },
+ { "+:", FALSE, 0 },
+ { "+::", FALSE, 0 },
+ { "+:200", FALSE, 0 },
+ { "+:2nonsense", FALSE, 0 },
+ { "+:2nonsense:", FALSE, 0 },
+ { "+:2:", FALSE, 0 },
+ { "+:2::", FALSE, 0 },
+ { "+:2:2020-01/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020nonsense/01/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:18446744073709551615/01/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01-01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01nonsense/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/00/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/13/01:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01/00:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01/32:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01/01nonsense:+*:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01/01", FALSE, 0 },
+ { "+:2:2020/01/01:", FALSE, 0 },
+ { "+:2:2020/01/01::", FALSE, 0 },
+ { "+:2:2020/01/01:2021-01-01:令和:%EC%Ey年", FALSE, 0 },
+ { "+:2:2020/01/01:+*", FALSE, 0 },
+ { "+:2:2020/01/01:+*:", FALSE, 0 },
+ { "+:2:2020/01/01:+*::", FALSE, 0 },
+ { "+:2:2020/01/01:+*:令和", FALSE, 0 },
+ { "+:2:2020/01/01:+*:令和:", FALSE, 0 },
+ { "+:2:2020/01/01:+*:令和:;", FALSE, 0 },
+ };
+
+ for (size_t i = 0; i < G_N_ELEMENTS (vectors); i++)
+ {
+ GPtrArray *segments = NULL;
+
+ g_test_message ("Vector %" G_GSIZE_FORMAT ": %s", i, vectors[i].desc);
+
+ segments = _g_era_description_parse (vectors[i].desc);
+
+ if (vectors[i].expected_success)
+ {
+ g_assert_nonnull (segments);
+ g_assert_cmpuint (segments->len, ==, vectors[i].expected_n_segments);
+ }
+ else
+ {
+ g_assert_null (segments);
+ }
+
+ g_clear_pointer (&segments, g_ptr_array_unref);
+ }
+}
+
static void
test_z (void)
{
@@ -3249,6 +3440,10 @@ main (gint argc,
g_test_add_func ("/GDateTime/dst", test_GDateTime_dst);
g_test_add_func ("/GDateTime/test_z", test_z);
g_test_add_func ("/GDateTime/test-all-dates", test_all_dates);
+ g_test_add_func ("/GDateTime/eras/japan", test_date_time_eras_japan);
+ g_test_add_func ("/GDateTime/eras/thailand", test_date_time_eras_thailand);
+ g_test_add_func ("/GDateTime/eras/parsing", test_date_time_eras_parsing);
+
g_test_add_func ("/GTimeZone/find-interval", test_find_interval);
g_test_add_func ("/GTimeZone/adjust-time", test_adjust_time);
g_test_add_func ("/GTimeZone/no-header", test_no_header);
diff --git a/glib/tests/meson.build b/glib/tests/meson.build
index 084cabb86..829beb8f5 100644
--- a/glib/tests/meson.build
+++ b/glib/tests/meson.build
@@ -40,6 +40,7 @@ glib_tests = {
'error' : {},
'fileutils' : {},
'gdatetime' : {
+ 'source' : ['gdatetime.c', '../gdatetime-private.c'],
'suite' : ['slow'],
# musl: GDateTime/format_mixed/non_utf8_time_non_utf8_messages should be
# skipped but it's not. The fix should be on musl side:
diff --git a/meson.build b/meson.build
index d48f13903..92aefb554 100644
--- a/meson.build
+++ b/meson.build
@@ -1311,6 +1311,11 @@ if cc.links('''#include
str = nl_langinfo (ABMON_12);
str = nl_langinfo (DAY_1);
str = nl_langinfo (ABDAY_7);
+ str = nl_langinfo (ERA);
+ str = nl_langinfo (ERA_D_T_FMT);
+ str = nl_langinfo (ERA_D_FMT);
+ str = nl_langinfo (ERA_T_FMT);
+ str = nl_langinfo (_NL_TIME_ERA_NUM_ENTRIES);
return 0;
}''', name : 'nl_langinfo (PM_STR)')
glib_conf.set('HAVE_LANGINFO_TIME', 1)