summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCorentin Noël <corentin@elementary.io>2021-11-11 01:37:14 +0100
committerCorentin Noël <corentin@elementary.io>2021-11-12 09:53:50 +0100
commitc7eb146d164ff92736f0f15df7142fe58b7116c2 (patch)
treef665c8055e48fbd0833ea8000dac0885be16711f
parent7b7ee85d8866423f83df1ac2e2536f6a04d39429 (diff)
downloadlibical-git-tintou/ical-time-format.tar.gz
libical-glib: Create i_cal_time_formattintou/ical-time-format
Directly adapted from g_date_time_format, this allows to create a formatted string from a ICalTime object.
-rw-r--r--CMakeLists.txt64
-rw-r--r--config.h.cmake9
-rw-r--r--src/libical-glib/CMakeLists.txt3
-rw-r--r--src/libical-glib/i-cal-time-format.c1371
-rw-r--r--src/libical-glib/i-cal-time-format.h35
-rw-r--r--src/libical-glib/tools/header-header-template2
-rw-r--r--src/test/libical-glib/CMakeLists.txt1
-rwxr-xr-xsrc/test/libical-glib/format.py39
8 files changed, 1524 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ae1f08f6..498395f0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -339,6 +339,70 @@ if(WIN32 OR WINCE)
endif()
endif()
+# Check for nl_langinfo and LC_TIME parts that are needed in i-cal-time-format.c
+include(CheckCSourceCompiles)
+check_c_source_compiles(
+"#include <langinfo.h>
+int main (int argc, char ** argv) {
+ char *str;
+ str = nl_langinfo (PM_STR);
+ str = nl_langinfo (D_T_FMT);
+ str = nl_langinfo (D_FMT);
+ str = nl_langinfo (T_FMT);
+ str = nl_langinfo (T_FMT_AMPM);
+ str = nl_langinfo (MON_1);
+ str = nl_langinfo (ABMON_12);
+ str = nl_langinfo (DAY_1);
+ str = nl_langinfo (ABDAY_7);
+ return 0;
+}" HAVE_LANGINFO_TIME)
+
+# Check for nl_langinfo and alternative month names
+check_c_source_compiles(
+"#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+#include <langinfo.h>
+int main (int argc, char ** argv) {
+ char *str;
+ str = nl_langinfo (ALTMON_1);
+ str = nl_langinfo (ALTMON_2);
+ str = nl_langinfo (ALTMON_3);
+ str = nl_langinfo (ALTMON_4);
+ str = nl_langinfo (ALTMON_5);
+ str = nl_langinfo (ALTMON_6);
+ str = nl_langinfo (ALTMON_7);
+ str = nl_langinfo (ALTMON_8);
+ str = nl_langinfo (ALTMON_9);
+ str = nl_langinfo (ALTMON_10);
+ str = nl_langinfo (ALTMON_11);
+ str = nl_langinfo (ALTMON_12);
+ return 0;
+}" HAVE_LANGINFO_ALTMON)
+
+# Check for nl_langinfo and abbreviated alternative month names
+check_c_source_compiles(
+"#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+#include <langinfo.h>
+int main (int argc, char ** argv) {
+ char *str;
+ str = nl_langinfo (_NL_ABALTMON_1);
+ str = nl_langinfo (_NL_ABALTMON_2);
+ str = nl_langinfo (_NL_ABALTMON_3);
+ str = nl_langinfo (_NL_ABALTMON_4);
+ str = nl_langinfo (_NL_ABALTMON_5);
+ str = nl_langinfo (_NL_ABALTMON_6);
+ str = nl_langinfo (_NL_ABALTMON_7);
+ str = nl_langinfo (_NL_ABALTMON_8);
+ str = nl_langinfo (_NL_ABALTMON_9);
+ str = nl_langinfo (_NL_ABALTMON_10);
+ str = nl_langinfo (_NL_ABALTMON_11);
+ str = nl_langinfo (_NL_ABALTMON_12);
+ return 0;
+}" HAVE_LANGINFO_ABALTMON)
+
include(ConfigureChecks.cmake)
add_definitions(-DHAVE_CONFIG_H)
configure_file(config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h)
diff --git a/config.h.cmake b/config.h.cmake
index b7e1d5c9..ee642204 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -168,6 +168,15 @@
/* Define to 1 if you have the <wctype.h> header file. */
#cmakedefine HAVE_WCTYPE_H 1
+/* Define to 1 if you have the needed nl_langinfo and LC_TIME parts. */
+#cmakedefine HAVE_LANGINFO_TIME ${HAVE_LANGINFO_TIME}
+
+/* Define to 1 if you have nl_langinfo and alternative month names. */
+#cmakedefine HAVE_LANGINFO_ALTMON ${HAVE_LANGINFO_ALTMON}
+
+/* Define to 1 if you have nl_langinfo and abbreviated alternative month names. */
+#cmakedefine HAVE_LANGINFO_ABALTMON ${HAVE_LANGINFO_ABALTMON}
+
/* Define to make icalerror_* calls abort instead of internally signalling an
error */
#define ICAL_ERRORS_ARE_FATAL ${ICAL_ERRORS_ARE_FATAL}
diff --git a/src/libical-glib/CMakeLists.txt b/src/libical-glib/CMakeLists.txt
index f734024e..0f17926a 100644
--- a/src/libical-glib/CMakeLists.txt
+++ b/src/libical-glib/CMakeLists.txt
@@ -58,6 +58,7 @@ list(APPEND LIBICAL_GLIB_HEADERS
${CMAKE_CURRENT_BINARY_DIR}/libical-glib.h
${CMAKE_CURRENT_BINARY_DIR}/i-cal-object.h
${CMAKE_CURRENT_BINARY_DIR}/i-cal-forward-declarations.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/i-cal-time-format.h
)
# add the command to generate the source code from the api files
@@ -106,6 +107,8 @@ list(APPEND LIBICAL_GLIB_SOURCES
${CMAKE_CURRENT_BINARY_DIR}/libical-glib-private.h
${CMAKE_CURRENT_BINARY_DIR}/i-cal-object.c
${CMAKE_CURRENT_BINARY_DIR}/i-cal-object.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/i-cal-time-format.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/i-cal-time-format.h
)
include_directories(
diff --git a/src/libical-glib/i-cal-time-format.c b/src/libical-glib/i-cal-time-format.c
new file mode 100644
index 00000000..64bf473b
--- /dev/null
+++ b/src/libical-glib/i-cal-time-format.c
@@ -0,0 +1,1371 @@
+/*
+ * Copyright 2021 Collabora, Ltd. (https://collabora.com)
+ * Copyright 2021 Corentin Noël <corentin.noel@collabora.com>
+ * Copyright 2009-2010 Christian Hergert <chris@dronelabs.com>
+ * Copyright 2010 Thiago Santos <thiago.sousa.santos@collabora.com>
+ * Copyright 2010 Emmanuele Bassi <ebassi@linux.intel.com>
+ * Copyright 2010 Codethink Limited
+ * Copyright 2018 Tomasz Miąsko
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of version 2.1. of the GNU Lesser General Public License
+ * as published by the Free Software Foundation.
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * Adapted from gdatetime.c in GLib
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "i-cal-time-format.h"
+#include <libical-glib/i-cal-timezone.h>
+
+/* langinfo.h in glibc 2.27 defines ALTMON_* only if _GNU_SOURCE is defined. */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#define GETTEXT_PACKAGE "glib20"
+#include <glib/gi18n-lib.h>
+
+#ifdef HAVE_LANGINFO_TIME
+#include <langinfo.h>
+#endif
+
+#ifndef G_OS_WIN32
+#include <sys/time.h>
+#include <time.h>
+#else
+#if defined (_MSC_VER) && (_MSC_VER < 1800)
+/* fallback implementation for isnan() on VS2012 and earlier */
+#define isnan _isnan
+#endif
+#endif /* !G_OS_WIN32 */
+
+/* Time conversion {{{1 */
+
+#define UNIX_EPOCH_START 719163
+#define INSTANT_TO_UNIX(instant) \
+ ((instant)/USEC_PER_SECOND - UNIX_EPOCH_START * SEC_PER_DAY)
+#define INSTANT_TO_UNIX_USECS(instant) \
+ ((instant) - UNIX_EPOCH_START * SEC_PER_DAY * USEC_PER_SECOND)
+#define UNIX_TO_INSTANT(unix) \
+ (((gint64) (unix) + UNIX_EPOCH_START * SEC_PER_DAY) * USEC_PER_SECOND)
+#define UNIX_USECS_TO_INSTANT(unix_usecs) \
+ ((gint64) (unix_usecs) + UNIX_EPOCH_START * SEC_PER_DAY * USEC_PER_SECOND)
+#define UNIX_TO_INSTANT_IS_VALID(unix) \
+ ((gint64) (unix) <= INSTANT_TO_UNIX (G_MAXINT64))
+#define UNIX_USECS_TO_INSTANT_IS_VALID(unix_usecs) \
+ ((gint64) (unix_usecs) <= INSTANT_TO_UNIX_USECS (G_MAXINT64))
+
+#define DAYS_IN_4YEARS 1461 /* days in 4 years */
+#define DAYS_IN_100YEARS 36524 /* days in 100 years */
+#define DAYS_IN_400YEARS 146097 /* days in 400 years */
+
+#define USEC_PER_SECOND (G_GINT64_CONSTANT (1000000))
+#define USEC_PER_MINUTE (G_GINT64_CONSTANT (60000000))
+#define USEC_PER_HOUR (G_GINT64_CONSTANT (3600000000))
+#define USEC_PER_MILLISECOND (G_GINT64_CONSTANT (1000))
+#define USEC_PER_DAY (G_GINT64_CONSTANT (86400000000))
+#define SEC_PER_DAY (G_GINT64_CONSTANT (86400))
+
+#define SECS_PER_MINUTE (60)
+#define SECS_PER_HOUR (60 * SECS_PER_MINUTE)
+#define SECS_PER_DAY (24 * SECS_PER_HOUR)
+#define SECS_PER_YEAR (365 * SECS_PER_DAY)
+#define SECS_PER_JULIAN (DAYS_PER_PERIOD * SECS_PER_DAY)
+
+#define GREGORIAN_LEAP(y) ((((y) % 4) == 0) && (!((((y) % 100) == 0) && (((y) % 400) != 0))))
+#define JULIAN_YEAR(d) ((d)->julian / 365.25)
+#define DAYS_PER_PERIOD (G_GINT64_CONSTANT (2914695))
+
+static const guint16 days_in_months[2][13] =
+{
+ { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
+ { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
+};
+
+static const guint16 days_in_year[2][13] =
+{
+ { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
+ { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
+};
+
+#ifdef HAVE_LANGINFO_TIME
+
+#define GET_AMPM(d) ((i_cal_time_get_hour (d) < 12) ? \
+ nl_langinfo (AM_STR) : \
+ nl_langinfo (PM_STR))
+#define GET_AMPM_IS_LOCALE TRUE
+
+#define PREFERRED_DATE_TIME_FMT nl_langinfo (D_T_FMT)
+#define PREFERRED_DATE_FMT nl_langinfo (D_FMT)
+#define PREFERRED_TIME_FMT nl_langinfo (T_FMT)
+#define PREFERRED_12HR_TIME_FMT nl_langinfo (T_FMT_AMPM)
+
+static const gint weekday_item[2][7] =
+{
+ { ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7 },
+ { DAY_1, DAY_2, DAY_3, DAY_4, DAY_5, DAY_6, DAY_7 }
+};
+
+static const gint month_item[2][12] =
+{
+ { ABMON_1, ABMON_2, ABMON_3, ABMON_4, ABMON_5, ABMON_6, ABMON_7, ABMON_8, ABMON_9, ABMON_10, ABMON_11, ABMON_12 },
+ { MON_1, MON_2, MON_3, MON_4, MON_5, MON_6, MON_7, MON_8, MON_9, MON_10, MON_11, MON_12 },
+};
+
+#define WEEKDAY_ABBR(d) nl_langinfo (weekday_item[0][i_cal_time_day_of_week (d) - 1])
+#define WEEKDAY_ABBR_IS_LOCALE TRUE
+#define WEEKDAY_FULL(d) nl_langinfo (weekday_item[1][i_cal_time_day_of_week (d) - 1])
+#define WEEKDAY_FULL_IS_LOCALE TRUE
+#define MONTH_ABBR(d) nl_langinfo (month_item[0][i_cal_time_get_month (d) - 1])
+#define MONTH_ABBR_IS_LOCALE TRUE
+#define MONTH_FULL(d) nl_langinfo (month_item[1][i_cal_time_get_month (d) - 1])
+#define MONTH_FULL_IS_LOCALE TRUE
+
+#else
+
+#define GET_AMPM(d) (get_fallback_ampm (i_cal_time_get_hour (d)))
+#define GET_AMPM_IS_LOCALE FALSE
+
+/* 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")
+
+/* Translators: this is the preferred format for expressing the date */
+#define PREFERRED_DATE_FMT C_("GDateTime", "%m/%d/%y")
+
+/* Translators: this is the preferred format for expressing the time */
+#define PREFERRED_TIME_FMT C_("GDateTime", "%H:%M:%S")
+
+/* Translators: this is the preferred format for expressing 12 hour time */
+#define PREFERRED_12HR_TIME_FMT C_("GDateTime", "%I:%M:%S %p")
+
+#define WEEKDAY_ABBR(d) (get_weekday_name_abbr (i_cal_time_day_of_week (d)))
+#define WEEKDAY_ABBR_IS_LOCALE FALSE
+#define WEEKDAY_FULL(d) (get_weekday_name (i_cal_time_day_of_week (d)))
+#define WEEKDAY_FULL_IS_LOCALE FALSE
+/* We don't yet know if nl_langinfo (MON_n) returns standalone or complete-date
+ * format forms but if nl_langinfo (ALTMON_n) is not supported then we will
+ * have to use MONTH_FULL as standalone. The same if nl_langinfo () does not
+ * exist at all. MONTH_ABBR is similar: if nl_langinfo (_NL_ABALTMON_n) is not
+ * supported then we will use MONTH_ABBR as standalone.
+ */
+#define MONTH_ABBR(d) (get_month_name_abbr_standalone (i_cal_time_get_month (d)))
+#define MONTH_ABBR_IS_LOCALE FALSE
+#define MONTH_FULL(d) (get_month_name_standalone (i_cal_time_get_month (d)))
+#define MONTH_FULL_IS_LOCALE FALSE
+
+static const gchar *
+get_month_name_standalone (gint month)
+{
+ switch (month)
+ {
+ case 1:
+ /* Translators: Some languages (Baltic, Slavic, Greek, and some more)
+ * need different grammatical forms of month names depending on whether
+ * they are standalone or in a complete date context, with the day
+ * number. Some other languages may prefer starting with uppercase when
+ * they are standalone and with lowercase when they are in a complete
+ * date context. Here are full month names in a form appropriate when
+ * they are used standalone. If your system is Linux with the glibc
+ * version 2.27 (released Feb 1, 2018) or newer or if it is from the BSD
+ * family (which includes OS X) then you can refer to the date command
+ * line utility and see what the command `date +%OB' produces. Also in
+ * the latest Linux the command `locale alt_mon' in your native locale
+ * produces a complete list of month names almost ready to copy and
+ * paste here. Note that in most of the languages (western European,
+ * non-European) there is no difference between the standalone and
+ * complete date form.
+ */
+ return C_("full month name", "January");
+ case 2:
+ return C_("full month name", "February");
+ case 3:
+ return C_("full month name", "March");
+ case 4:
+ return C_("full month name", "April");
+ case 5:
+ return C_("full month name", "May");
+ case 6:
+ return C_("full month name", "June");
+ case 7:
+ return C_("full month name", "July");
+ case 8:
+ return C_("full month name", "August");
+ case 9:
+ return C_("full month name", "September");
+ case 10:
+ return C_("full month name", "October");
+ case 11:
+ return C_("full month name", "November");
+ case 12:
+ return C_("full month name", "December");
+
+ default:
+ g_warning ("Invalid month number %d", month);
+ }
+
+ return NULL;
+}
+
+static const gchar *
+get_month_name_abbr_standalone (gint month)
+{
+ switch (month)
+ {
+ case 1:
+ /* Translators: Some languages need different grammatical forms of
+ * month names depending on whether they are standalone or in a complete
+ * date context, with the day number. Some may prefer starting with
+ * uppercase when they are standalone and with lowercase when they are
+ * in a full date context. However, as these names are abbreviated
+ * the grammatical difference is visible probably only in Belarusian
+ * and Russian. In other languages there is no difference between
+ * the standalone and complete date form when they are abbreviated.
+ * If your system is Linux with the glibc version 2.27 (released
+ * Feb 1, 2018) or newer then you can refer to the date command line
+ * utility and see what the command `date +%Ob' produces. Also in
+ * the latest Linux the command `locale ab_alt_mon' in your native
+ * locale produces a complete list of month names almost ready to copy
+ * and paste here. Note that this feature is not yet supported by any
+ * other platform. Here are abbreviated month names in a form
+ * appropriate when they are used standalone.
+ */
+ return C_("abbreviated month name", "Jan");
+ case 2:
+ return C_("abbreviated month name", "Feb");
+ case 3:
+ return C_("abbreviated month name", "Mar");
+ case 4:
+ return C_("abbreviated month name", "Apr");
+ case 5:
+ return C_("abbreviated month name", "May");
+ case 6:
+ return C_("abbreviated month name", "Jun");
+ case 7:
+ return C_("abbreviated month name", "Jul");
+ case 8:
+ return C_("abbreviated month name", "Aug");
+ case 9:
+ return C_("abbreviated month name", "Sep");
+ case 10:
+ return C_("abbreviated month name", "Oct");
+ case 11:
+ return C_("abbreviated month name", "Nov");
+ case 12:
+ return C_("abbreviated month name", "Dec");
+
+ default:
+ g_warning ("Invalid month number %d", month);
+ }
+
+ return NULL;
+}
+
+static const gchar *
+get_weekday_name (gint day)
+{
+ switch (day)
+ {
+ case 1:
+ return C_("full weekday name", "Sunday");
+ case 2:
+ return C_("full weekday name", "Monday");
+ case 3:
+ return C_("full weekday name", "Tuesday");
+ case 4:
+ return C_("full weekday name", "Wednesday");
+ case 5:
+ return C_("full weekday name", "Thursday");
+ case 6:
+ return C_("full weekday name", "Friday");
+ case 7:
+ return C_("full weekday name", "Saturday");
+
+ default:
+ g_warning ("Invalid week day number %d", day);
+ }
+
+ return NULL;
+}
+
+static const gchar *
+get_weekday_name_abbr (gint day)
+{
+ switch (day)
+ {
+ case 1:
+ return C_("abbreviated weekday name", "Sun");
+ case 2:
+ return C_("abbreviated weekday name", "Mon");
+ case 3:
+ return C_("abbreviated weekday name", "Tue");
+ case 4:
+ return C_("abbreviated weekday name", "Wed");
+ case 5:
+ return C_("abbreviated weekday name", "Thu");
+ case 6:
+ return C_("abbreviated weekday name", "Fri");
+ case 7:
+ return C_("abbreviated weekday name", "Sat");
+
+ default:
+ g_warning ("Invalid week day number %d", day);
+ }
+
+ return NULL;
+}
+
+#endif /* HAVE_LANGINFO_TIME */
+
+#ifdef HAVE_LANGINFO_ALTMON
+
+/* If nl_langinfo () supports ALTMON_n then MON_n returns full date format
+ * forms and ALTMON_n returns standalone forms.
+ */
+
+#define MONTH_FULL_WITH_DAY(d) MONTH_FULL(d)
+#define MONTH_FULL_WITH_DAY_IS_LOCALE MONTH_FULL_IS_LOCALE
+
+static const gint alt_month_item[12] =
+{
+ ALTMON_1, ALTMON_2, ALTMON_3, ALTMON_4, ALTMON_5, ALTMON_6,
+ ALTMON_7, ALTMON_8, ALTMON_9, ALTMON_10, ALTMON_11, ALTMON_12
+};
+
+#define MONTH_FULL_STANDALONE(d) nl_langinfo (alt_month_item[i_cal_time_get_month (d) - 1])
+#define MONTH_FULL_STANDALONE_IS_LOCALE TRUE
+
+#else
+
+/* If nl_langinfo () does not support ALTMON_n then either MON_n returns
+ * standalone forms or nl_langinfo (MON_n) does not work so we have defined
+ * it as standalone form.
+ */
+
+#define MONTH_FULL_STANDALONE(d) MONTH_FULL(d)
+#define MONTH_FULL_STANDALONE_IS_LOCALE MONTH_FULL_IS_LOCALE
+#define MONTH_FULL_WITH_DAY(d) (get_month_name_with_day (i_cal_time_get_month (d)))
+#define MONTH_FULL_WITH_DAY_IS_LOCALE FALSE
+
+static const gchar *
+get_month_name_with_day (gint month)
+{
+ switch (month)
+ {
+ case 1:
+ /* Translators: Some languages need different grammatical forms of
+ * month names depending on whether they are standalone or in a full
+ * date context, with the day number. Some may prefer starting with
+ * uppercase when they are standalone and with lowercase when they are
+ * in a full date context. Here are full month names in a form
+ * appropriate when they are used in a full date context, with the
+ * day number. If your system is Linux with the glibc version 2.27
+ * (released Feb 1, 2018) or newer or if it is from the BSD family
+ * (which includes OS X) then you can refer to the date command line
+ * utility and see what the command `date +%B' produces. Also in
+ * the latest Linux the command `locale mon' in your native locale
+ * produces a complete list of month names almost ready to copy and
+ * paste here. In older Linux systems due to a bug the result is
+ * incorrect in some languages. Note that in most of the languages
+ * (western European, non-European) there is no difference between the
+ * standalone and complete date form.
+ */
+ return C_("full month name with day", "January");
+ case 2:
+ return C_("full month name with day", "February");
+ case 3:
+ return C_("full month name with day", "March");
+ case 4:
+ return C_("full month name with day", "April");
+ case 5:
+ return C_("full month name with day", "May");
+ case 6:
+ return C_("full month name with day", "June");
+ case 7:
+ return C_("full month name with day", "July");
+ case 8:
+ return C_("full month name with day", "August");
+ case 9:
+ return C_("full month name with day", "September");
+ case 10:
+ return C_("full month name with day", "October");
+ case 11:
+ return C_("full month name with day", "November");
+ case 12:
+ return C_("full month name with day", "December");
+
+ default:
+ g_warning ("Invalid month number %d", month);
+ }
+
+ return NULL;
+}
+
+#endif /* HAVE_LANGINFO_ALTMON */
+
+#ifdef HAVE_LANGINFO_ABALTMON
+
+/* If nl_langinfo () supports _NL_ABALTMON_n then ABMON_n returns full
+ * date format forms and _NL_ABALTMON_n returns standalone forms.
+ */
+
+#define MONTH_ABBR_WITH_DAY(d) MONTH_ABBR(d)
+#define MONTH_ABBR_WITH_DAY_IS_LOCALE MONTH_ABBR_IS_LOCALE
+
+static const gint ab_alt_month_item[12] =
+{
+ _NL_ABALTMON_1, _NL_ABALTMON_2, _NL_ABALTMON_3, _NL_ABALTMON_4,
+ _NL_ABALTMON_5, _NL_ABALTMON_6, _NL_ABALTMON_7, _NL_ABALTMON_8,
+ _NL_ABALTMON_9, _NL_ABALTMON_10, _NL_ABALTMON_11, _NL_ABALTMON_12
+};
+
+#define MONTH_ABBR_STANDALONE(d) nl_langinfo (ab_alt_month_item[i_cal_time_get_month (d) - 1])
+#define MONTH_ABBR_STANDALONE_IS_LOCALE TRUE
+
+#else
+
+/* If nl_langinfo () does not support _NL_ABALTMON_n then either ABMON_n
+ * returns standalone forms or nl_langinfo (ABMON_n) does not work so we
+ * have defined it as standalone form. Now it's time to swap.
+ */
+
+#define MONTH_ABBR_STANDALONE(d) MONTH_ABBR(d)
+#define MONTH_ABBR_STANDALONE_IS_LOCALE MONTH_ABBR_IS_LOCALE
+#define MONTH_ABBR_WITH_DAY(d) (get_month_name_abbr_with_day (i_cal_time_get_month (d)))
+#define MONTH_ABBR_WITH_DAY_IS_LOCALE FALSE
+
+static const gchar *
+get_month_name_abbr_with_day (gint month)
+{
+ switch (month)
+ {
+ case 1:
+ /* Translators: Some languages need different grammatical forms of
+ * month names depending on whether they are standalone or in a full
+ * date context, with the day number. Some may prefer starting with
+ * uppercase when they are standalone and with lowercase when they are
+ * in a full date context. Here are abbreviated month names in a form
+ * appropriate when they are used in a full date context, with the
+ * day number. However, as these names are abbreviated the grammatical
+ * difference is visible probably only in Belarusian and Russian.
+ * In other languages there is no difference between the standalone
+ * and complete date form when they are abbreviated. If your system
+ * is Linux with the glibc version 2.27 (released Feb 1, 2018) or newer
+ * then you can refer to the date command line utility and see what the
+ * command `date +%b' produces. Also in the latest Linux the command
+ * `locale abmon' in your native locale produces a complete list of
+ * month names almost ready to copy and paste here. In other systems
+ * due to a bug the result is incorrect in some languages.
+ */
+ return C_("abbreviated month name with day", "Jan");
+ case 2:
+ return C_("abbreviated month name with day", "Feb");
+ case 3:
+ return C_("abbreviated month name with day", "Mar");
+ case 4:
+ return C_("abbreviated month name with day", "Apr");
+ case 5:
+ return C_("abbreviated month name with day", "May");
+ case 6:
+ return C_("abbreviated month name with day", "Jun");
+ case 7:
+ return C_("abbreviated month name with day", "Jul");
+ case 8:
+ return C_("abbreviated month name with day", "Aug");
+ case 9:
+ return C_("abbreviated month name with day", "Sep");
+ case 10:
+ return C_("abbreviated month name with day", "Oct");
+ case 11:
+ return C_("abbreviated month name with day", "Nov");
+ case 12:
+ return C_("abbreviated month name with day", "Dec");
+
+ default:
+ g_warning ("Invalid month number %d", month);
+ }
+
+ return NULL;
+}
+
+#endif /* HAVE_LANGINFO_ABALTMON */
+
+static gint
+i_cal_time_day_of_week_monday (const ICalTime *tt)
+{
+ gint weekday = i_cal_time_day_of_week (tt);
+ if (weekday != 1)
+ return weekday-1;
+
+ return 7;
+}
+
+/*
+ * i_cal_time_get_week_numbering_year:
+ * @tt: an #ICalTime
+ *
+ * Returns the ISO 8601 week-numbering year in which the week containing
+ * @tt falls.
+ *
+ * This function, taken together with i_cal_time_get_week_of_year() and
+ * i_cal_time_get_day_of_week() can be used to determine the full ISO
+ * week date on which @tt falls.
+ *
+ * This is usually equal to the normal Gregorian year (as returned by
+ * i_cal_time_get_year()), except as detailed below:
+ *
+ * For Thursday, the week-numbering year is always equal to the usual
+ * calendar year. For other days, the number is such that every day
+ * within a complete week (Monday to Sunday) is contained within the
+ * same week-numbering year.
+ *
+ * For Monday, Tuesday and Wednesday occurring near the end of the year,
+ * this may mean that the week-numbering year is one greater than the
+ * calendar year (so that these days have the same week-numbering year
+ * as the Thursday occurring early in the next year).
+ *
+ * For Friday, Saturday and Sunday occurring near the start of the year,
+ * this may mean that the week-numbering year is one less than the
+ * calendar year (so that these days have the same week-numbering year
+ * as the Thursday occurring late in the previous year).
+ *
+ * An equivalent description is that the week-numbering year is equal to
+ * the calendar year containing the majority of the days in the current
+ * week (Monday to Sunday).
+ *
+ * Note that January 1 0001 in the proleptic Gregorian calendar is a
+ * Monday, so this function never returns 0.
+ *
+ * Returns: the ISO 8601 week-numbering year for @tt
+ **/
+static gint
+i_cal_time_get_week_numbering_year (const ICalTime *tt)
+{
+ gint year, month, day, weekday;
+
+ i_cal_time_get_date (tt, &year, &month, &day);
+ /* The calculations are easier with 1=monday … 7=sunday
+ */
+ weekday = i_cal_time_day_of_week_monday (tt);
+
+ /* January 1, 2, 3 might be in the previous year if they occur after
+ * Thursday.
+ *
+ * Jan 1: Friday, Saturday, Sunday => day 1: weekday 5, 6, 7
+ * Jan 2: Saturday, Sunday => day 2: weekday 6, 7
+ * Jan 3: Sunday => day 3: weekday 7
+ *
+ * So we have a special case if (day - weekday) <= -4
+ */
+ if (month == 1 && (day - weekday) <= -4)
+ return year - 1;
+
+ /* December 29, 30, 31 might be in the next year if they occur before
+ * Thursday.
+ *
+ * Dec 31: Monday, Tuesday, Wednesday => day 31: weekday 1, 2, 3
+ * Dec 30: Monday, Tuesday => day 30: weekday 1, 2
+ * Dec 29: Monday => day 29: weekday 1
+ *
+ * So we have a special case if (day - weekday) >= 28
+ */
+ else if (month == 12 && (day - weekday) >= 28)
+ return year + 1;
+
+ else
+ return year;
+}
+
+/*
+ * i_cal_time_get_week_of_year:
+ * @tt: a #ICalTime
+ *
+ * Returns the ISO 8601 week number for the week containing @datetime.
+ * The ISO 8601 week number is the same for every day of the week (from
+ * Moday through Sunday). That can produce some unusual results
+ * (described below).
+ *
+ * The first week of the year is week 1. This is the week that contains
+ * the first Thursday of the year. Equivalently, this is the first week
+ * that has more than 4 of its days falling within the calendar year.
+ *
+ * The value 0 is never returned by this function. Days contained
+ * within a year but occurring before the first ISO 8601 week of that
+ * year are considered as being contained in the last week of the
+ * previous year. Similarly, the final days of a calendar year may be
+ * considered as being part of the first ISO 8601 week of the next year
+ * if 4 or more days of that week are contained within the new year.
+ *
+ * Returns: the ISO 8601 week number for @datetime.
+ */
+static gint
+i_cal_time_get_week_of_year (const ICalTime *tt)
+{
+ gint a, b, c, d, e, f, g, n, s, month, day, year, weeknum;
+
+ g_return_val_if_fail (tt != NULL, 0);
+
+ i_cal_time_get_date (tt, &year, &month, &day);
+
+ if (month <= 2)
+ {
+ a = i_cal_time_get_year (tt) - 1;
+ b = (a / 4) - (a / 100) + (a / 400);
+ c = ((a - 1) / 4) - ((a - 1) / 100) + ((a - 1) / 400);
+ s = b - c;
+ e = 0;
+ f = day - 1 + (31 * (month - 1));
+ }
+ else
+ {
+ a = year;
+ b = (a / 4) - (a / 100) + (a / 400);
+ c = ((a - 1) / 4) - ((a - 1) / 100) + ((a - 1) / 400);
+ s = b - c;
+ e = s + 1;
+ f = day + (((153 * (month - 3)) + 2) / 5) + 58 + s;
+ }
+
+ g = (a + b) % 7;
+ d = (f + g - e) % 7;
+ n = f + 3 - d;
+
+ if (n < 0)
+ weeknum = 53 - ((g - s) / 5);
+ else if (n > 364 + s)
+ weeknum = 1;
+ else
+ weeknum = (n / 7) + 1;
+
+ return weeknum;
+}
+
+/* Format AM/PM indicator if the locale does not have a localized version. */
+static const gchar *
+get_fallback_ampm (gint hour)
+{
+ if (hour < 12)
+ /* Translators: 'before midday' indicator */
+ return C_("GDateTime", "AM");
+ else
+ /* Translators: 'after midday' indicator */
+ return C_("GDateTime", "PM");
+}
+
+static inline gint
+ymd_to_days (gint year,
+ gint month,
+ gint day)
+{
+ gint64 days;
+
+ days = ((gint64) year - 1) * 365 + ((year - 1) / 4) - ((year - 1) / 100)
+ + ((year - 1) / 400);
+
+ days += days_in_year[0][month - 1];
+ if (GREGORIAN_LEAP (year) && month > 2)
+ day++;
+
+ days += day;
+
+ return days;
+}
+
+static gboolean
+format_z (GString *outstr,
+ gint offset,
+ guint colons)
+{
+ gint hours;
+ gint minutes;
+ gint seconds;
+ gchar sign = offset >= 0 ? '+' : '-';
+
+ offset = ABS (offset);
+ hours = offset / 3600;
+ minutes = offset / 60 % 60;
+ seconds = offset % 60;
+
+ switch (colons)
+ {
+ case 0:
+ g_string_append_printf (outstr, "%c%02d%02d",
+ sign,
+ hours,
+ minutes);
+ break;
+
+ case 1:
+ g_string_append_printf (outstr, "%c%02d:%02d",
+ sign,
+ hours,
+ minutes);
+ break;
+
+ case 2:
+ g_string_append_printf (outstr, "%c%02d:%02d:%02d",
+ sign,
+ hours,
+ minutes,
+ seconds);
+ break;
+
+ case 3:
+ g_string_append_printf (outstr, "%c%02d", sign, hours);
+
+ if (minutes != 0 || seconds != 0)
+ {
+ g_string_append_printf (outstr, ":%02d", minutes);
+
+ if (seconds != 0)
+ g_string_append_printf (outstr, ":%02d", seconds);
+ }
+ break;
+
+ default:
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#ifdef HAVE_LANGINFO_OUTDIGIT
+/* Initializes the array with UTF-8 encoded alternate digits suitable for use
+ * in current locale. Returns NULL when current locale does not use alternate
+ * digits or there was an error converting them to UTF-8.
+ */
+static const gchar * const *
+initialize_alt_digits (void)
+{
+ guint i;
+ gsize digit_len;
+ gchar *digit;
+ const gchar *locale_digit;
+#define N_DIGITS 10
+#define MAX_UTF8_ENCODING_LEN 4
+ static gchar buffer[N_DIGITS * (MAX_UTF8_ENCODING_LEN + 1 /* null separator */)];
+#undef N_DIGITS
+#undef MAX_UTF8_ENCODING_LEN
+ gchar *buffer_end = buffer;
+ static const gchar *alt_digits[10];
+
+ for (i = 0; i != 10; ++i)
+ {
+ locale_digit = nl_langinfo (_NL_CTYPE_OUTDIGIT0_MB + i);
+
+ if (g_strcmp0 (locale_digit, "") == 0)
+ return NULL;
+
+ digit = _g_ctype_locale_to_utf8 (locale_digit, -1, NULL, &digit_len, NULL);
+ if (digit == NULL)
+ return NULL;
+
+ g_assert (digit_len < (gsize) (buffer + sizeof (buffer) - buffer_end));
+
+ alt_digits[i] = buffer_end;
+ buffer_end = g_stpcpy (buffer_end, digit);
+ /* skip trailing null byte */
+ buffer_end += 1;
+
+ g_free (digit);
+ }
+
+ return alt_digits;
+}
+#endif /* HAVE_LANGINFO_OUTDIGIT */
+
+static void
+format_number (GString *str,
+ gboolean use_alt_digits,
+ const gchar *pad,
+ gint width,
+ guint32 number)
+{
+ const gchar *ascii_digits[10] = {
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
+ };
+ const gchar * const *digits = ascii_digits;
+ const gchar *tmp[10];
+ gint i = 0;
+
+ g_return_if_fail (width <= 10);
+
+#ifdef HAVE_LANGINFO_OUTDIGIT
+ if (use_alt_digits)
+ {
+ static const gchar * const *alt_digits = NULL;
+ static gsize initialised;
+
+ if G_UNLIKELY (g_once_init_enter (&initialised))
+ {
+ alt_digits = initialize_alt_digits ();
+
+ if (alt_digits == NULL)
+ alt_digits = ascii_digits;
+
+ g_once_init_leave (&initialised, TRUE);
+ }
+
+ digits = alt_digits;
+ }
+#endif /* HAVE_LANGINFO_OUTDIGIT */
+
+ do
+ {
+ tmp[i++] = digits[number % 10];
+ number /= 10;
+ }
+ while (number);
+
+ while (pad && i < width)
+ tmp[i++] = *pad == '0' ? digits[0] : pad;
+
+ /* should really be impossible */
+ g_assert (i <= 10);
+
+ while (i)
+ g_string_append (str, tmp[--i]);
+}
+
+static gboolean
+format_ampm (const ICalTime *tt,
+ GString *outstr,
+ gboolean locale_is_utf8,
+ gboolean uppercase)
+{
+ const gchar *ampm;
+ gchar *tmp = NULL, *ampm_dup;
+
+ ampm = GET_AMPM (tt);
+
+ if (!ampm || ampm[0] == '\0')
+ ampm = get_fallback_ampm (i_cal_time_get_hour (tt));
+
+ if (!locale_is_utf8 && GET_AMPM_IS_LOCALE)
+ {
+ /* This assumes that locale encoding can't have embedded NULs */
+ ampm = tmp = g_locale_to_utf8 (ampm, -1, NULL, NULL, NULL);
+ if (tmp == NULL)
+ return FALSE;
+ }
+ if (uppercase)
+ ampm_dup = g_utf8_strup (ampm, -1);
+ else
+ ampm_dup = g_utf8_strdown (ampm, -1);
+ g_free (tmp);
+
+ g_string_append (outstr, ampm_dup);
+ g_free (ampm_dup);
+
+ return TRUE;
+}
+
+static gboolean i_cal_time_format_utf8 (const ICalTime *tt,
+ const gchar *format,
+ GString *outstr,
+ gboolean locale_is_utf8);
+
+/* i_cal_time_format() subroutine that takes a locale-encoded format
+ * string and produces a UTF-8 encoded date/time string.
+ */
+static gboolean
+i_cal_time_format_locale (const ICalTime *tt,
+ const gchar *locale_format,
+ GString *outstr,
+ gboolean locale_is_utf8)
+{
+ gchar *utf8_format;
+ gboolean success;
+
+ if (locale_is_utf8)
+ return i_cal_time_format_utf8 (tt, locale_format, outstr, locale_is_utf8);
+
+ utf8_format = g_locale_to_utf8 (locale_format, -1, NULL, NULL, NULL);
+ if (utf8_format == NULL)
+ return FALSE;
+
+ success = i_cal_time_format_utf8 (tt, utf8_format, outstr, locale_is_utf8);
+ g_free (utf8_format);
+ return success;
+}
+
+static inline gboolean
+string_append (GString *string,
+ const gchar *s,
+ gboolean s_is_utf8)
+{
+ gchar *utf8;
+ gsize utf8_len;
+
+ if (s_is_utf8)
+ {
+ g_string_append (string, s);
+ }
+ else
+ {
+ utf8 = g_locale_to_utf8 (s, -1, NULL, &utf8_len, NULL);
+ if (utf8 == NULL)
+ return FALSE;
+ g_string_append_len (string, utf8, utf8_len);
+ g_free (utf8);
+ }
+
+ return TRUE;
+}
+
+/* i_cal_time_format() subroutine that takes a UTF-8 encoded format
+ * string and produces a UTF-8 encoded date/time string.
+ */
+static gboolean
+i_cal_time_format_utf8 (const ICalTime *tt,
+ const gchar *utf8_format,
+ GString *outstr,
+ gboolean locale_is_utf8)
+{
+ guint len;
+ guint colons;
+ gunichar c;
+ gboolean alt_digits = FALSE;
+ gboolean pad_set = FALSE;
+ gboolean name_is_utf8;
+ const gchar *pad = "";
+ const gchar *name;
+ const gchar *tz;
+
+ while (*utf8_format)
+ {
+ len = strcspn (utf8_format, "%");
+ if (len)
+ g_string_append_len (outstr, utf8_format, len);
+
+ utf8_format += len;
+ if (!*utf8_format)
+ break;
+
+ g_assert (*utf8_format == '%');
+ utf8_format++;
+ if (!*utf8_format)
+ break;
+
+ colons = 0;
+ alt_digits = FALSE;
+ pad_set = FALSE;
+
+ next_mod:
+ c = g_utf8_get_char (utf8_format);
+ utf8_format = g_utf8_next_char (utf8_format);
+ switch (c)
+ {
+ case 'a':
+ name = WEEKDAY_ABBR (tt);
+ if (g_strcmp0 (name, "") == 0)
+ return FALSE;
+
+ name_is_utf8 = locale_is_utf8 || !WEEKDAY_ABBR_IS_LOCALE;
+
+ if (!string_append (outstr, name, name_is_utf8))
+ return FALSE;
+
+ break;
+ case 'A':
+ name = WEEKDAY_FULL (tt);
+ if (g_strcmp0 (name, "") == 0)
+ return FALSE;
+
+ name_is_utf8 = locale_is_utf8 || !WEEKDAY_FULL_IS_LOCALE;
+
+ if (!string_append (outstr, name, name_is_utf8))
+ return FALSE;
+
+ break;
+ case 'b':
+ name = alt_digits ? MONTH_ABBR_STANDALONE (tt)
+ : MONTH_ABBR_WITH_DAY (tt);
+ if (g_strcmp0 (name, "") == 0)
+ return FALSE;
+
+ name_is_utf8 = locale_is_utf8 ||
+ ((alt_digits && !MONTH_ABBR_STANDALONE_IS_LOCALE) ||
+ (!alt_digits && !MONTH_ABBR_WITH_DAY_IS_LOCALE));
+
+ if (!string_append (outstr, name, name_is_utf8))
+ return FALSE;
+
+ break;
+ case 'B':
+ name = alt_digits ? MONTH_FULL_STANDALONE (tt)
+ : MONTH_FULL_WITH_DAY (tt);
+ if (g_strcmp0 (name, "") == 0)
+ return FALSE;
+
+ name_is_utf8 = locale_is_utf8 ||
+ ((alt_digits && !MONTH_FULL_STANDALONE_IS_LOCALE) ||
+ (!alt_digits && !MONTH_FULL_WITH_DAY_IS_LOCALE));
+
+ if (!string_append (outstr, name, name_is_utf8))
+ return FALSE;
+
+ break;
+ case 'c':
+ {
+ if (g_strcmp0 (PREFERRED_DATE_TIME_FMT, "") == 0)
+ return FALSE;
+ if (!i_cal_time_format_locale (tt, PREFERRED_DATE_TIME_FMT,
+ outstr, locale_is_utf8))
+ return FALSE;
+ }
+ break;
+ case 'C':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_year (tt) / 100);
+ break;
+ case 'd':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_day (tt));
+ break;
+ case 'e':
+ format_number (outstr, alt_digits, pad_set ? pad : " ", 2,
+ i_cal_time_get_day (tt));
+ break;
+ case 'f':
+ // ICalTime doesn't support microsecond precision, return 0
+ g_string_append_printf (outstr, "%06" G_GUINT64_FORMAT, (guint64) 0);
+ break;
+ case 'F':
+ g_string_append_printf (outstr, "%d-%02d-%02d",
+ i_cal_time_get_year (tt),
+ i_cal_time_get_month (tt),
+ i_cal_time_get_day (tt));
+ break;
+ case 'g':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_week_numbering_year (tt) % 100);
+ break;
+ case 'G':
+ format_number (outstr, alt_digits, pad_set ? pad : 0, 0,
+ i_cal_time_get_week_numbering_year (tt));
+ break;
+ case 'h':
+ name = alt_digits ? MONTH_ABBR_STANDALONE (tt)
+ : MONTH_ABBR_WITH_DAY (tt);
+ if (g_strcmp0 (name, "") == 0)
+ return FALSE;
+
+ name_is_utf8 = locale_is_utf8 ||
+ ((alt_digits && !MONTH_ABBR_STANDALONE_IS_LOCALE) ||
+ (!alt_digits && !MONTH_ABBR_WITH_DAY_IS_LOCALE));
+
+ if (!string_append (outstr, name, name_is_utf8))
+ return FALSE;
+
+ break;
+ case 'H':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_hour (tt));
+ break;
+ case 'I':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ (i_cal_time_get_hour (tt) + 11) % 12 + 1);
+ break;
+ case 'j':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 3,
+ i_cal_time_day_of_year (tt));
+ break;
+ case 'k':
+ format_number (outstr, alt_digits, pad_set ? pad : " ", 2,
+ i_cal_time_get_hour (tt));
+ break;
+ case 'l':
+ format_number (outstr, alt_digits, pad_set ? pad : " ", 2,
+ (i_cal_time_get_hour (tt) + 11) % 12 + 1);
+ break;
+ case 'm':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_month (tt));
+ break;
+ case 'M':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_minute (tt));
+ break;
+ case 'n':
+ g_string_append_c (outstr, '\n');
+ break;
+ case 'O':
+ alt_digits = TRUE;
+ goto next_mod;
+ case 'p':
+ if (!format_ampm (tt, outstr, locale_is_utf8, TRUE))
+ return FALSE;
+ break;
+ case 'P':
+ if (!format_ampm (tt, outstr, locale_is_utf8, FALSE))
+ return FALSE;
+ break;
+ case 'r':
+ {
+ if (g_strcmp0 (PREFERRED_12HR_TIME_FMT, "") == 0)
+ return FALSE;
+ if (!i_cal_time_format_locale (tt, PREFERRED_12HR_TIME_FMT,
+ outstr, locale_is_utf8))
+ return FALSE;
+ }
+ break;
+ case 'R':
+ g_string_append_printf (outstr, "%02d:%02d",
+ i_cal_time_get_hour (tt),
+ i_cal_time_get_minute (tt));
+ break;
+ case 's':
+ g_string_append_printf (outstr, "%" G_GINT64_FORMAT, (gint64)i_cal_time_as_timet (tt));
+ break;
+ case 'S':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_second (tt));
+ break;
+ case 't':
+ g_string_append_c (outstr, '\t');
+ break;
+ case 'T':
+ g_string_append_printf (outstr, "%02d:%02d:%02d",
+ i_cal_time_get_hour (tt),
+ i_cal_time_get_minute (tt),
+ i_cal_time_get_second (tt));
+ break;
+ case 'u':
+ format_number (outstr, alt_digits, 0, 0,
+ i_cal_time_day_of_week_monday (tt));
+ break;
+ case 'V':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_week_of_year (tt));
+ break;
+ case 'w':
+ format_number (outstr, alt_digits, 0, 0,
+ i_cal_time_day_of_week (tt) - 1);
+ break;
+ case 'x':
+ {
+ if (g_strcmp0 (PREFERRED_DATE_FMT, "") == 0)
+ return FALSE;
+ if (!i_cal_time_format_locale (tt, PREFERRED_DATE_FMT,
+ outstr, locale_is_utf8))
+ return FALSE;
+ }
+ break;
+ case 'X':
+ {
+ if (g_strcmp0 (PREFERRED_TIME_FMT, "") == 0)
+ return FALSE;
+ if (!i_cal_time_format_locale (tt, PREFERRED_TIME_FMT,
+ outstr, locale_is_utf8))
+ return FALSE;
+ }
+ break;
+ case 'y':
+ format_number (outstr, alt_digits, pad_set ? pad : "0", 2,
+ i_cal_time_get_year (tt) % 100);
+ break;
+ case 'Y':
+ format_number (outstr, alt_digits, 0, 0,
+ i_cal_time_get_year (tt));
+ break;
+ case 'z':
+ {
+ gint offset;
+ offset = i_cal_timezone_get_utc_offset (i_cal_time_get_timezone (tt), (ICalTime *) tt, NULL);
+ if (!format_z (outstr, offset, colons))
+ return FALSE;
+ }
+ break;
+ case 'Z':
+ tz = i_cal_timezone_get_tznames (i_cal_time_get_timezone (tt));
+ if (tz)
+ g_string_append (outstr, tz);
+ else
+ g_string_append (outstr, "UTC");
+ break;
+ case '%':
+ g_string_append_c (outstr, '%');
+ break;
+ case '-':
+ pad_set = TRUE;
+ pad = "";
+ goto next_mod;
+ case '_':
+ pad_set = TRUE;
+ pad = " ";
+ goto next_mod;
+ case '0':
+ pad_set = TRUE;
+ pad = "0";
+ goto next_mod;
+ case ':':
+ /* Colons are only allowed before 'z' */
+ if (*utf8_format && *utf8_format != 'z' && *utf8_format != ':')
+ return FALSE;
+ colons++;
+ goto next_mod;
+ default:
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * i_cal_time_format:
+ * @tt: An #IcalTime
+ * @format: a valid UTF-8 string, containing the format for the
+ * #IcalTime
+ *
+ * Creates a newly allocated string representing the requested @format.
+ *
+ * The format strings understood by this function are a subset of the
+ * strftime() format language as specified by C99. The \%D, \%U and \%W
+ * conversions are not supported, nor is the 'E' modifier. The GNU
+ * extensions \%k, \%l, \%s and \%P are supported, however, as are the
+ * '0', '_' and '-' modifiers. The Python extension \%f is also supported.
+ *
+ * In contrast to strftime(), this function always produces a UTF-8
+ * string, regardless of the current locale. Note that the rendering of
+ * many formats is locale-dependent and may not match the strftime()
+ * output exactly.
+ *
+ * The following format specifiers are supported:
+ *
+ * - \%a: the abbreviated weekday name according to the current locale
+ * - \%A: the full weekday name according to the current locale
+ * - \%b: the abbreviated month name according to the current locale
+ * - \%B: the full month name according to the current locale
+ * - \%c: the preferred date and time representation for the current locale
+ * - \%C: the century number (year/100) as a 2-digit integer (00-99)
+ * - \%d: the day of the month as a decimal number (range 01 to 31)
+ * - \%e: the day of the month as a decimal number (range 1 to 31)
+ * - \%F: equivalent to `%Y-%m-%d` (the ISO 8601 date format)
+ * - \%g: the last two digits of the ISO 8601 week-based year as a
+ * decimal number (00-99). This works well with \%V and \%u.
+ * - \%G: the ISO 8601 week-based year as a decimal number. This works
+ * well with \%V and \%u.
+ * - \%h: equivalent to \%b
+ * - \%H: the hour as a decimal number using a 24-hour clock (range 00 to 23)
+ * - \%I: the hour as a decimal number using a 12-hour clock (range 01 to 12)
+ * - \%j: the day of the year as a decimal number (range 001 to 366)
+ * - \%k: the hour (24-hour clock) as a decimal number (range 0 to 23);
+ * single digits are preceded by a blank
+ * - \%l: the hour (12-hour clock) as a decimal number (range 1 to 12);
+ * single digits are preceded by a blank
+ * - \%m: the month as a decimal number (range 01 to 12)
+ * - \%M: the minute as a decimal number (range 00 to 59)
+ * - \%f: the microsecond as a decimal number (range 000000 to 999999)
+ * - \%p: either "AM" or "PM" according to the given time value, or the
+ * corresponding strings for the current locale. Noon is treated as
+ * "PM" and midnight as "AM". Use of this format specifier is discouraged, as
+ * many locales have no concept of AM/PM formatting. Use \%c or \%X instead.
+ * - \%P: like \%p but lowercase: "am" or "pm" or a corresponding string for
+ * the current locale. Use of this format specifier is discouraged, as
+ * many locales have no concept of AM/PM formatting. Use \%c or \%X instead.
+ * - \%r: the time in a.m. or p.m. notation. Use of this format specifier is
+ * discouraged, as many locales have no concept of AM/PM formatting. Use \%c
+ * or \%X instead.
+ * - \%R: the time in 24-hour notation (\%H:\%M)
+ * - \%s: the number of seconds since the Epoch, that is, since 1970-01-01
+ * 00:00:00 UTC
+ * - \%S: the second as a decimal number (range 00 to 60)
+ * - \%t: a tab character
+ * - \%T: the time in 24-hour notation with seconds (\%H:\%M:\%S)
+ * - \%u: the ISO 8601 standard day of the week as a decimal, range 1 to 7,
+ * Monday being 1. This works well with \%G and \%V.
+ * - \%V: the ISO 8601 standard week number of the current year as a decimal
+ * number, range 01 to 53, where week 1 is the first week that has at
+ * least 4 days in the new year.
+ * This works well with \%G and \%u.
+ * - \%w: the day of the week as a decimal, range 0 to 6, Sunday being 0.
+ * This is not the ISO 8601 standard format -- use \%u instead.
+ * - \%x: the preferred date representation for the current locale without
+ * the time
+ * - \%X: the preferred time representation for the current locale without
+ * the date
+ * - \%y: the year as a decimal number without the century
+ * - \%Y: the year as a decimal number including the century
+ * - \%z: the time zone as an offset from UTC (+hhmm)
+ * - \%:z: the time zone as an offset from UTC (+hh:mm).
+ * This is a gnulib strftime() extension.
+ * - \%::z: the time zone as an offset from UTC (+hh:mm:ss). This is a
+ * gnulib strftime() extension.
+ * - \%:::z: the time zone as an offset from UTC, with : to necessary
+ * precision (e.g., -04, +05:30). This is a gnulib strftime() extension.
+ * - \%Z: the time zone or name or abbreviation
+ * - \%\%: a literal \% character
+ *
+ * Some conversion specifications can be modified by preceding the
+ * conversion specifier by one or more modifier characters. The
+ * following modifiers are supported for many of the numeric
+ * conversions:
+ *
+ * - O: Use alternative numeric symbols, if the current locale supports those.
+ * - _: Pad a numeric result with spaces. This overrides the default padding
+ * for the specifier.
+ * - -: Do not pad a numeric result. This overrides the default padding
+ * for the specifier.
+ * - 0: Pad a numeric result with zeros. This overrides the default padding
+ * for the specifier.
+ *
+ * Additionally, when O is used with B, b, or h, it produces the alternative
+ * form of a month name. The alternative form should be used when the month
+ * name is used without a day number (e.g., standalone). It is required in
+ * some languages (Baltic, Slavic, Greek, and more) due to their grammatical
+ * rules. For other languages there is no difference. \%OB is a GNU and BSD
+ * strftime() extension expected to be added to the future POSIX specification,
+ * \%Ob and \%Oh are GNU strftime() extensions.
+ *
+ * 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
+ * string should be freed with g_free().
+ *
+ * Since: 3.0.12
+ */
+gchar *
+i_cal_time_format (const ICalTime *tt,
+ const gchar *format)
+{
+ GString *outstr;
+ const gchar *charset;
+ /* Avoid conversions from locale (for LC_TIME and not for LC_MESSAGES unless
+ * specified otherwise) charset to UTF-8 if charset is compatible
+ * with UTF-8 already. Check for UTF-8 and synonymous canonical names of
+ * ASCII. */
+ gboolean time_is_utf8_compatible = g_get_charset (&charset) ||
+ g_strcmp0 ("ASCII", charset) == 0 ||
+ g_strcmp0 ("ANSI_X3.4-1968", charset) == 0;
+
+ g_return_val_if_fail (tt != NULL, NULL);
+ g_return_val_if_fail (format != NULL, NULL);
+ g_return_val_if_fail (g_utf8_validate (format, -1, NULL), NULL);
+
+ outstr = g_string_sized_new (strlen (format) * 2);
+
+ if (!i_cal_time_format_utf8 (tt, format, outstr,
+ time_is_utf8_compatible))
+ {
+ g_string_free (outstr, TRUE);
+ return NULL;
+ }
+
+ return g_string_free (outstr, FALSE);
+}
diff --git a/src/libical-glib/i-cal-time-format.h b/src/libical-glib/i-cal-time-format.h
new file mode 100644
index 00000000..d288afa3
--- /dev/null
+++ b/src/libical-glib/i-cal-time-format.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 Collabora, Ltd. (https://collabora.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of version 2.1. of the GNU Lesser General Public License
+ * as published by the Free Software Foundation.
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBICAL_GLIB_H_INSIDE__) && !defined (LIBICAL_GLIB_COMPILATION)
+#error "Only <libical-glib/libical-glib.h> can be included directly."
+#endif
+
+#ifndef I_CAL_TIME_FORMAT_H
+#define I_CAL_TIME_FORMAT_H
+
+#include <libical-glib/i-cal-forward-declarations.h>
+#include <libical-glib/i-cal-time.h>
+
+G_BEGIN_DECLS
+
+LIBICAL_ICAL_EXPORT
+gchar * i_cal_time_format (const ICalTime *tt,
+ const gchar *format);
+
+G_END_DECLS
+
+#endif /* I_CAL_TIME_FORMAT_H */
diff --git a/src/libical-glib/tools/header-header-template b/src/libical-glib/tools/header-header-template
index 9dde6369..89e78a70 100644
--- a/src/libical-glib/tools/header-header-template
+++ b/src/libical-glib/tools/header-header-template
@@ -28,6 +28,8 @@
#define __LIBICAL_GLIB_H_INSIDE__
#include <libical-glib/i-cal-forward-declarations.h>
+#include <libical-glib/i-cal-object.h>
+#include <libical-glib/i-cal-time-format.h>
${allHeaders}
#undef __LIBICAL_GLIB_H_INSIDE__
diff --git a/src/test/libical-glib/CMakeLists.txt b/src/test/libical-glib/CMakeLists.txt
index 4d239dba..3f2aea01 100644
--- a/src/test/libical-glib/CMakeLists.txt
+++ b/src/test/libical-glib/CMakeLists.txt
@@ -9,6 +9,7 @@ list(APPEND TEST_FILES
comprehensive.py
duration.py
error.py
+ format.py
misc.py
parameter.py
period.py
diff --git a/src/test/libical-glib/format.py b/src/test/libical-glib/format.py
new file mode 100755
index 00000000..73f8691d
--- /dev/null
+++ b/src/test/libical-glib/format.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#GI_TYPELIB_PATH=$PREFIX/lib/girepository-1.0/ ./format.py
+
+###############################################################################
+#
+# Copyright 2021 Corentin Noël <corentin.noel@collabora.com>
+# Copyright 2021 Collabora, Ltd. (https://collabora.com)
+#
+# This library is free software; you can redistribute it and/or modify
+# it under the terms of either:
+#
+# The LGPL as published by the Free Software Foundation, version
+# 2.1, available at: https://www.gnu.org/licenses/lgpl-2.1.txt
+#
+# Or:
+#
+# The Mozilla Public License Version 2.0. You may obtain a copy of
+# the License at https://www.mozilla.org/MPL/
+#
+###############################################################################
+
+import gi
+
+gi.require_version('ICalGLib', '3.0')
+
+from gi.repository import ICalGLib
+from gi.repository import GLib
+
+icaltime = ICalGLib.Time.new_from_string("20181224T000000Z")
+glibdatetime = GLib.DateTime.new_from_iso8601("20181224T000000Z")
+
+assert(glibdatetime.to_unix() == icaltime.as_timet());
+
+test_format = "a%a A%A b%b B%B c%c C%C d%d e%e F%F g%g G%G h%h H%H I%I j%j m%m M%M n%n p%p r%r R%R S%S t%t T%T u%u V%V w%w x%x X%X y%y Y%Y z%z Z%Z %%"
+# 127997 is prime, 1315005118 is now
+for t in range(0, 1315005118, 127997):
+ glibdatetime = GLib.DateTime.new_from_unix_utc(t)
+ icaltime = ICalGLib.Time.new_from_timet_with_zone(t, 0, None)
+ assert(glibdatetime.format(test_format) == icaltime.format(test_format))