/* gdatetime.c * * Copyright (C) 2009-2010 Christian Hergert * Copyright (C) 2010 Thiago Santos * Copyright (C) 2010 Emmanuele Bassi * Copyright © 2010 Codethink Limited * * 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 * licence, or (at your option) any later version. * * This 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 . * * Authors: Christian Hergert * Thiago Santos * Emmanuele Bassi * Ryan Lortie * Robert Ancell */ /* Algorithms within this file are based on the Calendar FAQ by * Claus Tondering. It can be found at * http://www.tondering.dk/claus/cal/calendar29.txt * * Copyright and disclaimer * ------------------------ * This document is Copyright (C) 2008 by Claus Tondering. * E-mail: claus@tondering.dk. (Please include the word * "calendar" in the subject line.) * The document may be freely distributed, provided this * copyright notice is included and no money is charged for * the document. * * This document is provided "as is". No warranties are made as * to its correctness. */ /* Prologue {{{1 */ #include "config.h" /* langinfo.h in glibc 2.27 defines ALTMON_* only if _GNU_SOURCE is defined. */ #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif #include #include #ifdef HAVE_LANGINFO_TIME #include #endif #include "gdatetime.h" #include "gslice.h" #include "gatomic.h" #include "gcharset.h" #include "gconvert.h" #include "gfileutils.h" #include "ghash.h" #include "gmain.h" #include "gmappedfile.h" #include "gstrfuncs.h" #include "gtestutils.h" #include "gthread.h" #include "gtimezone.h" #include "glibintl.h" #ifndef G_OS_WIN32 #include #include #endif /* !G_OS_WIN32 */ /** * SECTION:date-time * @title: GDateTime * @short_description: a structure representing Date and Time * @see_also: #GTimeZone * * #GDateTime is a structure that combines a Gregorian date and time * into a single structure. It provides many conversion and methods to * manipulate dates and times. Time precision is provided down to * microseconds and the time can range (proleptically) from 0001-01-01 * 00:00:00 to 9999-12-31 23:59:59.999999. #GDateTime follows POSIX * time in the sense that it is oblivious to leap seconds. * * #GDateTime is an immutable object; once it has been created it cannot * be modified further. All modifiers will create a new #GDateTime. * Nearly all such functions can fail due to the date or time going out * of range, in which case %NULL will be returned. * * #GDateTime is reference counted: the reference count is increased by calling * g_date_time_ref() and decreased by calling g_date_time_unref(). When the * reference count drops to 0, the resources allocated by the #GDateTime * structure are released. * * Many parts of the API may produce non-obvious results. As an * example, adding two months to January 31st will yield March 31st * whereas adding one month and then one month again will yield either * March 28th or March 29th. Also note that adding 24 hours is not * always the same as adding one day (since days containing daylight * savings time transitions are either 23 or 25 hours in length). * * #GDateTime is available since GLib 2.26. */ struct _GDateTime { /* Microsecond timekeeping within Day */ guint64 usec; /* TimeZone information */ GTimeZone *tz; gint interval; /* 1 is 0001-01-01 in Proleptic Gregorian */ gint32 days; volatile gint ref_count; }; /* Time conversion {{{1 */ #define UNIX_EPOCH_START 719163 #define INSTANT_TO_UNIX(instant) \ ((instant)/USEC_PER_SECOND - UNIX_EPOCH_START * SEC_PER_DAY) #define UNIX_TO_INSTANT(unix) \ (((gint64) (unix) + UNIX_EPOCH_START * SEC_PER_DAY) * USEC_PER_SECOND) #define UNIX_TO_INSTANT_IS_VALID(unix) \ ((gint64) (unix) <= INSTANT_TO_UNIX (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) ((g_date_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_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7, ABDAY_1 }, { DAY_2, DAY_3, DAY_4, DAY_5, DAY_6, DAY_7, DAY_1 } }; 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][g_date_time_get_day_of_week (d) - 1]) #define WEEKDAY_ABBR_IS_LOCALE TRUE #define WEEKDAY_FULL(d) nl_langinfo (weekday_item[1][g_date_time_get_day_of_week (d) - 1]) #define WEEKDAY_FULL_IS_LOCALE TRUE #define MONTH_ABBR(d) nl_langinfo (month_item[0][g_date_time_get_month (d) - 1]) #define MONTH_ABBR_IS_LOCALE TRUE #define MONTH_FULL(d) nl_langinfo (month_item[1][g_date_time_get_month (d) - 1]) #define MONTH_FULL_IS_LOCALE TRUE #else #define GET_AMPM(d) (get_fallback_ampm (g_date_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 (g_date_time_get_day_of_week (d))) #define WEEKDAY_ABBR_IS_LOCALE FALSE #define WEEKDAY_FULL(d) (get_weekday_name (g_date_time_get_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 (g_date_time_get_month (d))) #define MONTH_ABBR_IS_LOCALE FALSE #define MONTH_FULL(d) (get_month_name_standalone (g_date_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", "Monday"); case 2: return C_("full weekday name", "Tuesday"); case 3: return C_("full weekday name", "Wednesday"); case 4: return C_("full weekday name", "Thursday"); case 5: return C_("full weekday name", "Friday"); case 6: return C_("full weekday name", "Saturday"); case 7: return C_("full weekday name", "Sunday"); 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", "Mon"); case 2: return C_("abbreviated weekday name", "Tue"); case 3: return C_("abbreviated weekday name", "Wed"); case 4: return C_("abbreviated weekday name", "Thu"); case 5: return C_("abbreviated weekday name", "Fri"); case 6: return C_("abbreviated weekday name", "Sat"); case 7: return C_("abbreviated weekday name", "Sun"); 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[g_date_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 (g_date_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[g_date_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 (g_date_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 */ /* 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 = (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 void g_date_time_get_week_number (GDateTime *datetime, gint *week_number, gint *day_of_week, gint *day_of_year) { gint a, b, c, d, e, f, g, n, s, month, day, year; g_date_time_get_ymd (datetime, &year, &month, &day); if (month <= 2) { a = g_date_time_get_year (datetime) - 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 (week_number) { if (n < 0) *week_number = 53 - ((g - s) / 5); else if (n > 364 + s) *week_number = 1; else *week_number = (n / 7) + 1; } if (day_of_week) *day_of_week = d + 1; if (day_of_year) *day_of_year = f + 1; } /* Lifecycle {{{1 */ static GDateTime * g_date_time_alloc (GTimeZone *tz) { GDateTime *datetime; datetime = g_slice_new0 (GDateTime); datetime->tz = g_time_zone_ref (tz); datetime->ref_count = 1; return datetime; } /** * g_date_time_ref: * @datetime: a #GDateTime * * Atomically increments the reference count of @datetime by one. * * Returns: the #GDateTime with the reference count increased * * Since: 2.26 */ GDateTime * g_date_time_ref (GDateTime *datetime) { g_return_val_if_fail (datetime != NULL, NULL); g_return_val_if_fail (datetime->ref_count > 0, NULL); g_atomic_int_inc (&datetime->ref_count); return datetime; } /** * g_date_time_unref: * @datetime: a #GDateTime * * Atomically decrements the reference count of @datetime by one. * * When the reference count reaches zero, the resources allocated by * @datetime are freed * * Since: 2.26 */ void g_date_time_unref (GDateTime *datetime) { g_return_if_fail (datetime != NULL); g_return_if_fail (datetime->ref_count > 0); if (g_atomic_int_dec_and_test (&datetime->ref_count)) { g_time_zone_unref (datetime->tz); g_slice_free (GDateTime, datetime); } } /* Internal state transformers {{{1 */ /*< internal > * g_date_time_to_instant: * @datetime: a #GDateTime * * Convert a @datetime into an instant. * * An instant is a number that uniquely describes a particular * microsecond in time, taking time zone considerations into account. * (ie: "03:00 -0400" is the same instant as "02:00 -0500"). * * An instant is always positive but we use a signed return value to * avoid troubles with C. */ static gint64 g_date_time_to_instant (GDateTime *datetime) { gint64 offset; offset = g_time_zone_get_offset (datetime->tz, datetime->interval); offset *= USEC_PER_SECOND; return datetime->days * USEC_PER_DAY + datetime->usec - offset; } /*< internal > * g_date_time_from_instant: * @tz: a #GTimeZone * @instant: a instant in time * * Creates a #GDateTime from a time zone and an instant. * * This might fail if the time ends up being out of range. */ static GDateTime * g_date_time_from_instant (GTimeZone *tz, gint64 instant) { GDateTime *datetime; gint64 offset; if (instant < 0 || instant > G_GINT64_CONSTANT (1000000000000000000)) return NULL; datetime = g_date_time_alloc (tz); datetime->interval = g_time_zone_find_interval (tz, G_TIME_TYPE_UNIVERSAL, INSTANT_TO_UNIX (instant)); offset = g_time_zone_get_offset (datetime->tz, datetime->interval); offset *= USEC_PER_SECOND; instant += offset; datetime->days = instant / USEC_PER_DAY; datetime->usec = instant % USEC_PER_DAY; if (datetime->days < 1 || 3652059 < datetime->days) { g_date_time_unref (datetime); datetime = NULL; } return datetime; } /*< internal > * g_date_time_deal_with_date_change: * @datetime: a #GDateTime * * This function should be called whenever the date changes by adding * days, months or years. It does three things. * * First, we ensure that the date falls between 0001-01-01 and * 9999-12-31 and return %FALSE if it does not. * * Next we update the ->interval field. * * Finally, we ensure that the resulting date and time pair exists (by * ensuring that our time zone has an interval containing it) and * adjusting as required. For example, if we have the time 02:30:00 on * March 13 2010 in Toronto and we add 1 day to it, we would end up with * 2:30am on March 14th, which doesn't exist. In that case, we bump the * time up to 3:00am. */ static gboolean g_date_time_deal_with_date_change (GDateTime *datetime) { GTimeType was_dst; gint64 full_time; gint64 usec; if (datetime->days < 1 || datetime->days > 3652059) return FALSE; was_dst = g_time_zone_is_dst (datetime->tz, datetime->interval); full_time = datetime->days * USEC_PER_DAY + datetime->usec; usec = full_time % USEC_PER_SECOND; full_time /= USEC_PER_SECOND; full_time -= UNIX_EPOCH_START * SEC_PER_DAY; datetime->interval = g_time_zone_adjust_time (datetime->tz, was_dst, &full_time); full_time += UNIX_EPOCH_START * SEC_PER_DAY; full_time *= USEC_PER_SECOND; full_time += usec; datetime->days = full_time / USEC_PER_DAY; datetime->usec = full_time % USEC_PER_DAY; /* maybe daylight time caused us to shift to a different day, * but it definitely didn't push us into a different year */ return TRUE; } static GDateTime * g_date_time_replace_days (GDateTime *datetime, gint days) { GDateTime *new; new = g_date_time_alloc (datetime->tz); new->interval = datetime->interval; new->usec = datetime->usec; new->days = days; if (!g_date_time_deal_with_date_change (new)) { g_date_time_unref (new); new = NULL; } return new; } /* now/unix/timeval Constructors {{{1 */ /*< internal > * g_date_time_new_from_timeval: * @tz: a #GTimeZone * @tv: a #GTimeVal * * Creates a #GDateTime corresponding to the given #GTimeVal @tv in the * given time zone @tz. * * The time contained in a #GTimeVal is always stored in the form of * seconds elapsed since 1970-01-01 00:00:00 UTC, regardless of the * given time zone. * * This call can fail (returning %NULL) if @tv represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ static GDateTime * g_date_time_new_from_timeval (GTimeZone *tz, const GTimeVal *tv) { if ((gint64) tv->tv_sec > G_MAXINT64 - 1 || !UNIX_TO_INSTANT_IS_VALID ((gint64) tv->tv_sec + 1)) return NULL; return g_date_time_from_instant (tz, tv->tv_usec + UNIX_TO_INSTANT (tv->tv_sec)); } /*< internal > * g_date_time_new_from_unix: * @tz: a #GTimeZone * @t: the Unix time * * Creates a #GDateTime corresponding to the given Unix time @t in the * given time zone @tz. * * Unix time is the number of seconds that have elapsed since 1970-01-01 * 00:00:00 UTC, regardless of the time zone given. * * This call can fail (returning %NULL) if @t represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ static GDateTime * g_date_time_new_from_unix (GTimeZone *tz, gint64 secs) { if (!UNIX_TO_INSTANT_IS_VALID (secs)) return NULL; return g_date_time_from_instant (tz, UNIX_TO_INSTANT (secs)); } /** * g_date_time_new_now: * @tz: a #GTimeZone * * Creates a #GDateTime corresponding to this exact instant in the given * time zone @tz. The time is as accurate as the system allows, to a * maximum accuracy of 1 microsecond. * * This function will always succeed unless the system clock is set to * truly insane values (or unless GLib is still being used after the * year 9999). * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_now (GTimeZone *tz) { GTimeVal tv; g_get_current_time (&tv); return g_date_time_new_from_timeval (tz, &tv); } /** * g_date_time_new_now_local: * * Creates a #GDateTime corresponding to this exact instant in the local * time zone. * * This is equivalent to calling g_date_time_new_now() with the time * zone returned by g_time_zone_new_local(). * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_now_local (void) { GDateTime *datetime; GTimeZone *local; local = g_time_zone_new_local (); datetime = g_date_time_new_now (local); g_time_zone_unref (local); return datetime; } /** * g_date_time_new_now_utc: * * Creates a #GDateTime corresponding to this exact instant in UTC. * * This is equivalent to calling g_date_time_new_now() with the time * zone returned by g_time_zone_new_utc(). * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_now_utc (void) { GDateTime *datetime; GTimeZone *utc; utc = g_time_zone_new_utc (); datetime = g_date_time_new_now (utc); g_time_zone_unref (utc); return datetime; } /** * g_date_time_new_from_unix_local: * @t: the Unix time * * Creates a #GDateTime corresponding to the given Unix time @t in the * local time zone. * * Unix time is the number of seconds that have elapsed since 1970-01-01 * 00:00:00 UTC, regardless of the local time offset. * * This call can fail (returning %NULL) if @t represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_from_unix_local (gint64 t) { GDateTime *datetime; GTimeZone *local; local = g_time_zone_new_local (); datetime = g_date_time_new_from_unix (local, t); g_time_zone_unref (local); return datetime; } /** * g_date_time_new_from_unix_utc: * @t: the Unix time * * Creates a #GDateTime corresponding to the given Unix time @t in UTC. * * Unix time is the number of seconds that have elapsed since 1970-01-01 * 00:00:00 UTC. * * This call can fail (returning %NULL) if @t represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_from_unix_utc (gint64 t) { GDateTime *datetime; GTimeZone *utc; utc = g_time_zone_new_utc (); datetime = g_date_time_new_from_unix (utc, t); g_time_zone_unref (utc); return datetime; } /** * g_date_time_new_from_timeval_local: * @tv: a #GTimeVal * * Creates a #GDateTime corresponding to the given #GTimeVal @tv in the * local time zone. * * The time contained in a #GTimeVal is always stored in the form of * seconds elapsed since 1970-01-01 00:00:00 UTC, regardless of the * local time offset. * * This call can fail (returning %NULL) if @tv represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_from_timeval_local (const GTimeVal *tv) { GDateTime *datetime; GTimeZone *local; local = g_time_zone_new_local (); datetime = g_date_time_new_from_timeval (local, tv); g_time_zone_unref (local); return datetime; } /** * g_date_time_new_from_timeval_utc: * @tv: a #GTimeVal * * Creates a #GDateTime corresponding to the given #GTimeVal @tv in UTC. * * The time contained in a #GTimeVal is always stored in the form of * seconds elapsed since 1970-01-01 00:00:00 UTC. * * This call can fail (returning %NULL) if @tv represents a time outside * of the supported range of #GDateTime. * * You should release the return value by calling g_date_time_unref() * when you are done with it. * * Returns: a new #GDateTime, or %NULL * * Since: 2.26 **/ GDateTime * g_date_time_new_from_timeval_utc (const GTimeVal *tv) { GDateTime *datetime; GTimeZone *utc; utc = g_time_zone_new_utc (); datetime = g_date_time_new_from_timeval (utc, tv); g_time_zone_unref (utc); return datetime; } /* Parse integers in the form d (week days), dd (hours etc), ddd (ordinal days) or dddd (years) */ static gboolean get_iso8601_int (const gchar *text, gsize length, gint *value) { gint i, v = 0; if (length < 1 || length > 4) return FALSE; for (i = 0; i < length; i++) { const gchar c = text[i]; if (c < '0' || c > '9') return FALSE; v = v * 10 + (c - '0'); } *value = v; return TRUE; } /* Parse seconds in the form ss or ss.sss (variable length decimal) */ static gboolean get_iso8601_seconds (const gchar *text, gsize length, gdouble *value) { gint i; gdouble divisor = 1, v = 0; if (length < 2) return FALSE; for (i = 0; i < 2; i++) { const gchar c = text[i]; if (c < '0' || c > '9') return FALSE; v = v * 10 + (c - '0'); } if (length > 2 && !(text[i] == '.' || text[i] == ',')) return FALSE; i++; if (i == length) return FALSE; for (; i < length; i++) { const gchar c = text[i]; if (c < '0' || c > '9') return FALSE; v = v * 10 + (c - '0'); divisor *= 10; } *value = v / divisor; return TRUE; } static GDateTime * g_date_time_new_ordinal (GTimeZone *tz, gint year, gint ordinal_day, gint hour, gint minute, gdouble seconds) { GDateTime *dt; if (ordinal_day < 1 || ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365)) return NULL; dt = g_date_time_new (tz, year, 1, 1, hour, minute, seconds); dt->days += ordinal_day - 1; return dt; } static GDateTime * g_date_time_new_week (GTimeZone *tz, gint year, gint week, gint week_day, gint hour, gint minute, gdouble seconds) { gint64 p; gint max_week, jan4_week_day, ordinal_day; GDateTime *dt; p = (year * 365 + (year / 4) - (year / 100) + (year / 400)) % 7; max_week = p == 4 ? 53 : 52; if (week < 1 || week > max_week || week_day < 1 || week_day > 7) return NULL; dt = g_date_time_new (tz, year, 1, 4, 0, 0, 0); g_date_time_get_week_number (dt, NULL, &jan4_week_day, NULL); ordinal_day = (week * 7) + week_day - (jan4_week_day + 3); if (ordinal_day < 0) { year--; ordinal_day += GREGORIAN_LEAP (year) ? 366 : 365; } else if (ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365)) { ordinal_day -= (GREGORIAN_LEAP (year) ? 366 : 365); year++; } return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds); } static GDateTime * parse_iso8601_date (const gchar *text, gsize length, gint hour, gint minute, gdouble seconds, GTimeZone *tz) { /* YYYY-MM-DD */ if (length == 10 && text[4] == '-' && text[7] == '-') { int year, month, day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 5, 2, &month) || !get_iso8601_int (text + 8, 2, &day)) return NULL; return g_date_time_new (tz, year, month, day, hour, minute, seconds); } /* YYYY-DDD */ else if (length == 8 && text[4] == '-') { gint year, ordinal_day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 5, 3, &ordinal_day)) return NULL; return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds); } /* YYYY-Www-D */ else if (length == 10 && text[4] == '-' && text[5] == 'W' && text[8] == '-') { gint year, week, week_day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 6, 2, &week) || !get_iso8601_int (text + 9, 1, &week_day)) return NULL; return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds); } /* YYYYWwwD */ else if (length == 8 && text[4] == 'W') { gint year, week, week_day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 5, 2, &week) || !get_iso8601_int (text + 7, 1, &week_day)) return NULL; return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds); } /* YYYYMMDD */ else if (length == 8) { int year, month, day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 4, 2, &month) || !get_iso8601_int (text + 6, 2, &day)) return NULL; return g_date_time_new (tz, year, month, day, hour, minute, seconds); } /* YYYYDDD */ else if (length == 7) { gint year, ordinal_day; if (!get_iso8601_int (text, 4, &year) || !get_iso8601_int (text + 4, 3, &ordinal_day)) return NULL; return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds); } else return FALSE; } static GTimeZone * parse_iso8601_timezone (const gchar *text, gsize length, gssize *tz_offset) { gint i, tz_length, offset_sign = 1, offset_hours, offset_minutes; GTimeZone *tz; /* UTC uses Z suffix */ if (length > 0 && text[length - 1] == 'Z') { *tz_offset = length - 1; return g_time_zone_new_utc (); } /* Look for '+' or '-' of offset */ for (i = length - 1; i >= 0; i--) if (text[i] == '+' || text[i] == '-') { offset_sign = text[i] == '-' ? -1 : 1; break; } if (i < 0) return NULL; tz_length = length - i; /* +hh:mm or -hh:mm */ if (tz_length == 6 && text[i+3] == ':') { if (!get_iso8601_int (text + i + 1, 2, &offset_hours) || !get_iso8601_int (text + i + 4, 2, &offset_minutes)) return NULL; } /* +hhmm or -hhmm */ else if (tz_length == 5) { if (!get_iso8601_int (text + i + 1, 2, &offset_hours) || !get_iso8601_int (text + i + 3, 2, &offset_minutes)) return NULL; } /* +hh or -hh */ else if (tz_length == 3) { if (!get_iso8601_int (text + i + 1, 2, &offset_hours)) return NULL; offset_minutes = 0; } else return NULL; *tz_offset = i; tz = g_time_zone_new (text + i); /* Double-check that the GTimeZone matches our interpretation of the timezone. * Failure would indicate a bug either here of in the GTimeZone code. */ g_assert (g_time_zone_get_offset (tz, 0) == offset_sign * (offset_hours * 3600 + offset_minutes * 60)); return tz; } static gboolean parse_iso8601_time (const gchar *text, gsize length, gint *hour, gint *minute, gdouble *seconds, GTimeZone **tz) { gssize tz_offset = -1; /* Check for timezone suffix */ *tz = parse_iso8601_timezone (text, length, &tz_offset); if (tz_offset >= 0) length = tz_offset; /* hh:mm:ss(.sss) */ if (length >= 8 && text[2] == ':' && text[5] == ':') { return get_iso8601_int (text, 2, hour) && get_iso8601_int (text + 3, 2, minute) && get_iso8601_seconds (text + 6, length - 6, seconds); } /* hhmmss(.sss) */ else if (length >= 6) { return get_iso8601_int (text, 2, hour) && get_iso8601_int (text + 2, 2, minute) && get_iso8601_seconds (text + 4, length - 4, seconds); } else return FALSE; } /** * g_date_time_new_from_iso8601: * @text: an ISO 8601 formatted time string. * @default_tz: (nullable): a #GTimeZone to use if the text doesn't contain a * timezone, or %NULL. * * Creates a #GDateTime corresponding to the given * [ISO 8601 formatted string](https://en.wikipedia.org/wiki/ISO_8601) * @text. ISO 8601 strings of the form