Merge branch '3119-Ey-years' into 'main'

gdatetime: Add support for %E modifier to g_date_time_format()

Closes #3119

See merge request GNOME/glib!3696
This commit is contained in:
Philip Withnall 2023-11-28 20:08:10 +00:00
commit 2ada5ba0f1
12 changed files with 788 additions and 16 deletions

View File

@ -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"

View File

@ -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

View File

@ -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 \

View File

@ -1,4 +1,4 @@
FROM registry.gitlab.gnome.org/gnome/glib/fedora:v22
FROM registry.gitlab.gnome.org/gnome/glib/fedora:v23
USER root

View File

@ -66,9 +66,9 @@
* @dmy: this is set if @day, @month and @year are valid
* @day: the day of the day-month-year representation of the date,
* as a number between 1 and 31
* @month: the day of the day-month-year representation of the date,
* @month: the month of the day-month-year representation of the date,
* as a number between 1 and 12
* @year: the day of the day-month-year representation of the date
* @year: the year of the day-month-year representation of the date
*
* `GDate` is a struct for calendrical calculations.
*

254
glib/gdatetime-private.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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;
}

85
glib/gdatetime-private.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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

View File

@ -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,
@ -2902,6 +3042,7 @@ format_number (GString *str,
if (alt_digits == NULL)
alt_digits = ascii_digits;
g_free (alt_digits_locale);
alt_digits_locale = g_strdup (current_ctype_locale);
}
@ -3043,6 +3184,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 +3211,7 @@ g_date_time_format_utf8 (GDateTime *datetime,
colons = 0;
alt_digits = FALSE;
alt_era = FALSE;
pad_set = FALSE;
mod_case = FALSE;
@ -3129,14 +3272,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 +3374,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 +3433,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 +3673,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

View File

@ -277,6 +277,7 @@ glib_sources += files(
'gdataset.c',
'gdate.c',
'gdatetime.c',
'gdatetime-private.c',
'gdir.c',
'genviron.c',
'gerror.c',

View File

@ -1,6 +1,7 @@
/* gdatetime-tests.c
*
* Copyright (C) 2009-2010 Christian Hergert <chris@dronelabs.com>
* Copyright 2023 Philip Withnall
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@ -28,6 +29,8 @@
#include <glib/gstdio.h>
#include <locale.h>
#include "gdatetime-private.h"
#ifdef G_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
@ -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)
{
@ -3182,6 +3373,7 @@ main (gint argc,
* categories. Unset it to avoid interference with tests. */
g_unsetenv ("CHARSET");
setlocale (LC_ALL, "C.UTF-8");
g_test_init (&argc, &argv, NULL);
/* GDateTime Tests */
@ -3249,6 +3441,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);

View File

@ -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:

View File

@ -1311,6 +1311,11 @@ if cc.links('''#include <langinfo.h>
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)