diff options
Diffstat (limited to 'pendulum/_extensions')
-rw-r--r-- | pendulum/_extensions/_helpers.c | 1861 | ||||
-rw-r--r-- | pendulum/_extensions/_helpers.pyi | 31 | ||||
-rw-r--r-- | pendulum/_extensions/helpers.py | 722 |
3 files changed, 1326 insertions, 1288 deletions
diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c index aa92ae5..a3114d9 100644 --- a/pendulum/_extensions/_helpers.c +++ b/pendulum/_extensions/_helpers.c @@ -1,930 +1,931 @@ -/* ------------------------------------------------------------------------- */
-
-#include <Python.h>
-#include <datetime.h>
-#include <structmember.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-/* ------------------------------------------------------------------------- */
-
-#define EPOCH_YEAR 1970
-
-#define DAYS_PER_N_YEAR 365
-#define DAYS_PER_L_YEAR 366
-
-#define USECS_PER_SEC 1000000
-
-#define SECS_PER_MIN 60
-#define SECS_PER_HOUR (60 * SECS_PER_MIN)
-#define SECS_PER_DAY (SECS_PER_HOUR * 24)
-
-// 400-year chunks always have 146097 days (20871 weeks).
-#define DAYS_PER_400_YEARS 146097L
-#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY)
-
-// The number of seconds in an aligned 100-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int64_t SECS_PER_100_YEARS[2] = {
- (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in an aligned 4-year chunk, for those that
-// do not begin with a leap year and those that do respectively.
-const int32_t SECS_PER_4_YEARS[2] = {
- (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY,
- (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY};
-
-// The number of seconds in non-leap and leap years respectively.
-const int32_t SECS_PER_YEAR[2] = {
- DAYS_PER_N_YEAR * SECS_PER_DAY,
- DAYS_PER_L_YEAR *SECS_PER_DAY};
-
-#define MONTHS_PER_YEAR 12
-
-// The month lengths in non-leap and leap years respectively.
-const int32_t DAYS_PER_MONTHS[2][13] = {
- {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
- {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
-
-// The day offsets of the beginning of each (1-based) month in non-leap
-// and leap years respectively.
-// For example, in a leap year there are 335 days before December.
-const int32_t MONTHS_OFFSETS[2][14] = {
- {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
- {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
-
-const int DAY_OF_WEEK_TABLE[12] = {
- 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
-
-#define TM_SUNDAY 0
-#define TM_MONDAY 1
-#define TM_TUESDAY 2
-#define TM_WEDNESDAY 3
-#define TM_THURSDAY 4
-#define TM_FRIDAY 5
-#define TM_SATURDAY 6
-
-#define TM_JANUARY 0
-#define TM_FEBRUARY 1
-#define TM_MARCH 2
-#define TM_APRIL 3
-#define TM_MAY 4
-#define TM_JUNE 5
-#define TM_JULY 6
-#define TM_AUGUST 7
-#define TM_SEPTEMBER 8
-#define TM_OCTOBER 9
-#define TM_NOVEMBER 10
-#define TM_DECEMBER 11
-
-/* ------------------------------------------------------------------------- */
-
-int _p(int y)
-{
- return y + y / 4 - y / 100 + y / 400;
-}
-
-int _is_leap(int year)
-{
- return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-}
-
-int _is_long_year(int year)
-{
- return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3);
-}
-
-int _week_day(int year, int month, int day)
-{
- int y;
- int w;
-
- y = year - (month < 3);
-
- w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7;
-
- if (!w)
- {
- w = 7;
- }
-
- return w;
-}
-
-int _days_in_year(int year)
-{
- if (_is_leap(year))
- {
- return DAYS_PER_L_YEAR;
- }
-
- return DAYS_PER_N_YEAR;
-}
-
-int _day_number(int year, int month, int day)
-{
- month = (month + 9) % 12;
- year = year - month / 10;
-
- return (
- 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1));
-}
-
-int _get_offset(PyObject *dt)
-{
- PyObject *tzinfo;
- PyObject *offset;
-
- tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
- if (tzinfo != Py_None)
- {
- offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt);
-
- return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset);
- }
-
- return 0;
-}
-
-int _has_tzinfo(PyObject *dt)
-{
- return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo;
-}
-
-char *_get_tz_name(PyObject *dt)
-{
- PyObject *tzinfo;
- char *tz = "";
-
- tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo;
-
- if (tzinfo != Py_None)
- {
- if (PyObject_HasAttrString(tzinfo, "name"))
- {
- // Pendulum timezone
- tz = (char *)PyUnicode_AsUTF8(
- PyObject_GetAttrString(tzinfo, "name"));
- }
- else if (PyObject_HasAttrString(tzinfo, "zone"))
- {
- // pytz timezone
- tz = (char *)PyUnicode_AsUTF8(
- PyObject_GetAttrString(tzinfo, "zone"));
- }
- }
-
- return tz;
-}
-
-/* ------------------------ Custom Types ------------------------------- */
-
-/*
- * class Diff():
- */
-typedef struct
-{
- PyObject_HEAD int years;
- int months;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
- int total_days;
-} Diff;
-
-/*
- * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days):
- * self.years = years
- * # ...
-*/
-static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs)
-{
- int years;
- int months;
- int days;
- int hours;
- int minutes;
- int seconds;
- int microseconds;
- int total_days;
-
- if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days))
- return -1;
-
- self->years = years;
- self->months = months;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
- self->total_days = total_days;
-
- return 0;
-}
-
-/*
- * def __repr__(self):
- * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format(
- * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds
- * )
- */
-static PyObject *Diff_repr(Diff *self)
-{
- char repr[82] = {0};
-
- sprintf(
- repr,
- "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds",
- self->years,
- self->months,
- self->days,
- self->hours,
- self->minutes,
- self->seconds,
- self->microseconds);
-
- return PyUnicode_FromString(repr);
-}
-
-/*
- * Instantiate new Diff_type object
- * Skip overhead of calling PyObject_New and PyObject_Init.
- * Directly allocate object.
- */
-static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type)
-{
- Diff *self = (Diff *)(type->tp_alloc(type, 0));
-
- if (self != NULL)
- {
- self->years = years;
- self->months = months;
- self->days = days;
- self->hours = hours;
- self->minutes = minutes;
- self->seconds = seconds;
- self->microseconds = microseconds;
- self->total_days = total_days;
- }
-
- return (PyObject *)self;
-}
-
-/*
- * Class member / class attributes
- */
-static PyMemberDef Diff_members[] = {
- {"years", T_INT, offsetof(Diff, years), 0, "years in diff"},
- {"months", T_INT, offsetof(Diff, months), 0, "months in diff"},
- {"days", T_INT, offsetof(Diff, days), 0, "days in diff"},
- {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"},
- {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"},
- {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"},
- {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"},
- {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"},
- {NULL}};
-
-static PyTypeObject Diff_type = {
- PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */
- sizeof(Diff), /* tp_basicsize */
- 0, /* tp_itemsize */
- 0, /* tp_dealloc */
- 0, /* tp_print */
- 0, /* tp_getattr */
- 0, /* tp_setattr */
- 0, /* tp_as_async */
- (reprfunc)Diff_repr, /* tp_repr */
- 0, /* tp_as_number */
- 0, /* tp_as_sequence */
- 0, /* tp_as_mapping */
- 0, /* tp_hash */
- 0, /* tp_call */
- (reprfunc)Diff_repr, /* tp_str */
- 0, /* tp_getattro */
- 0, /* tp_setattro */
- 0, /* tp_as_buffer */
- Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
- "Precise difference between two datetime objects", /* tp_doc */
-};
-
-#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type)
-
-/* -------------------------- Functions --------------------------*/
-
-PyObject *is_leap(PyObject *self, PyObject *args)
-{
- PyObject *leap;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- leap = PyBool_FromLong(_is_leap(year));
-
- return leap;
-}
-
-PyObject *is_long_year(PyObject *self, PyObject *args)
-{
- PyObject *is_long;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- is_long = PyBool_FromLong(_is_long_year(year));
-
- return is_long;
-}
-
-PyObject *week_day(PyObject *self, PyObject *args)
-{
- PyObject *wd;
- int year;
- int month;
- int day;
-
- if (!PyArg_ParseTuple(args, "iii", &year, &month, &day))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- wd = PyLong_FromLong(_week_day(year, month, day));
-
- return wd;
-}
-
-PyObject *days_in_year(PyObject *self, PyObject *args)
-{
- PyObject *ndays;
- int year;
-
- if (!PyArg_ParseTuple(args, "i", &year))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- ndays = PyLong_FromLong(_days_in_year(year));
-
- return ndays;
-}
-
-PyObject *timestamp(PyObject *self, PyObject *args)
-{
- int64_t result;
- PyObject *dt;
-
- if (!PyArg_ParseTuple(args, "O", &dt))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- int year = (double)PyDateTime_GET_YEAR(dt);
- int month = PyDateTime_GET_MONTH(dt);
- int day = PyDateTime_GET_DAY(dt);
- int hour = PyDateTime_DATE_GET_HOUR(dt);
- int minute = PyDateTime_DATE_GET_MINUTE(dt);
- int second = PyDateTime_DATE_GET_SECOND(dt);
-
- result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month];
- result += (int)floor((double)(year - 1968) / 4);
- result -= (year - 1900) / 100;
- result += (year - 1600) / 400;
-
- if (_is_leap(year) && month < 3)
- {
- result -= 1;
- }
-
- result += day - 1;
- result *= 24;
- result += hour;
- result *= 60;
- result += minute;
- result *= 60;
- result += second;
-
- return PyLong_FromSsize_t(result);
-}
-
-PyObject *local_time(PyObject *self, PyObject *args)
-{
- double unix_time;
- int32_t utc_offset;
- int32_t year;
- int32_t microsecond;
- int64_t seconds;
- int32_t leap_year;
- int64_t sec_per_100years;
- int64_t sec_per_4years;
- int32_t sec_per_year;
- int32_t month;
- int32_t day;
- int32_t month_offset;
- int32_t hour;
- int32_t minute;
- int32_t second;
-
- if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- year = EPOCH_YEAR;
- seconds = (int64_t)floor(unix_time);
-
- // Shift to a base year that is 400-year aligned.
- if (seconds >= 0)
- {
- seconds -= 10957L * SECS_PER_DAY;
- year += 30; // == 2000;
- }
- else
- {
- seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY;
- year -= 370; // == 1600;
- }
-
- seconds += utc_offset;
-
- // Handle years in chunks of 400/100/4/1
- year += 400 * (seconds / SECS_PER_400_YEARS);
- seconds %= SECS_PER_400_YEARS;
- if (seconds < 0)
- {
- seconds += SECS_PER_400_YEARS;
- year -= 400;
- }
-
- leap_year = 1; // 4-century aligned
-
- sec_per_100years = SECS_PER_100_YEARS[leap_year];
-
- while (seconds >= sec_per_100years)
- {
- seconds -= sec_per_100years;
- year += 100;
- leap_year = 0; // 1-century, non 4-century aligned
- sec_per_100years = SECS_PER_100_YEARS[leap_year];
- }
-
- sec_per_4years = SECS_PER_4_YEARS[leap_year];
- while (seconds >= sec_per_4years)
- {
- seconds -= sec_per_4years;
- year += 4;
- leap_year = 1; // 4-year, non century aligned
- sec_per_4years = SECS_PER_4_YEARS[leap_year];
- }
-
- sec_per_year = SECS_PER_YEAR[leap_year];
- while (seconds >= sec_per_year)
- {
- seconds -= sec_per_year;
- year += 1;
- leap_year = 0; // non 4-year aligned
- sec_per_year = SECS_PER_YEAR[leap_year];
- }
-
- // Handle months and days
- month = TM_DECEMBER + 1;
- day = seconds / SECS_PER_DAY + 1;
- seconds %= SECS_PER_DAY;
- while (month != TM_JANUARY + 1)
- {
- month_offset = MONTHS_OFFSETS[leap_year][month];
- if (day > month_offset)
- {
- day -= month_offset;
- break;
- }
-
- month -= 1;
- }
-
- // Handle hours, minutes and seconds
- hour = seconds / SECS_PER_HOUR;
- seconds %= SECS_PER_HOUR;
- minute = seconds / SECS_PER_MIN;
- second = seconds % SECS_PER_MIN;
-
- return Py_BuildValue("NNNNNNN",
- PyLong_FromLong(year),
- PyLong_FromLong(month),
- PyLong_FromLong(day),
- PyLong_FromLong(hour),
- PyLong_FromLong(minute),
- PyLong_FromLong(second),
- PyLong_FromLong(microsecond));
-}
-
-// Calculate a precise difference between two datetimes.
-PyObject *precise_diff(PyObject *self, PyObject *args)
-{
- PyObject *dt1;
- PyObject *dt2;
-
- if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2))
- {
- PyErr_SetString(
- PyExc_ValueError, "Invalid parameters");
- return NULL;
- }
-
- int year_diff = 0;
- int month_diff = 0;
- int day_diff = 0;
- int hour_diff = 0;
- int minute_diff = 0;
- int second_diff = 0;
- int microsecond_diff = 0;
- int sign = 1;
- int year;
- int month;
- int leap;
- int days_in_last_month;
- int days_in_month;
- int dt1_year = PyDateTime_GET_YEAR(dt1);
- int dt2_year = PyDateTime_GET_YEAR(dt2);
- int dt1_month = PyDateTime_GET_MONTH(dt1);
- int dt2_month = PyDateTime_GET_MONTH(dt2);
- int dt1_day = PyDateTime_GET_DAY(dt1);
- int dt2_day = PyDateTime_GET_DAY(dt2);
- int dt1_hour = 0;
- int dt2_hour = 0;
- int dt1_minute = 0;
- int dt2_minute = 0;
- int dt1_second = 0;
- int dt2_second = 0;
- int dt1_microsecond = 0;
- int dt2_microsecond = 0;
- int dt1_total_seconds = 0;
- int dt2_total_seconds = 0;
- int dt1_offset = 0;
- int dt2_offset = 0;
- int dt1_is_datetime = PyDateTime_Check(dt1);
- int dt2_is_datetime = PyDateTime_Check(dt2);
- char *tz1 = "";
- char *tz2 = "";
- int in_same_tz = 0;
- int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
-
- // If both dates are datetimes, we check
- // If we are in the same timezone
- if (dt1_is_datetime && dt2_is_datetime)
- {
- if (_has_tzinfo(dt1))
- {
- tz1 = _get_tz_name(dt1);
- dt1_offset = _get_offset(dt1);
- }
-
- if (_has_tzinfo(dt2))
- {
- tz2 = _get_tz_name(dt2);
- dt2_offset = _get_offset(dt2);
- }
-
- in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1);
- }
-
- // If we have datetimes (and not only dates)
- // we get the information we need
- if (dt1_is_datetime)
- {
- dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
- dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
- dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
- dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
-
- if ((!in_same_tz && dt1_offset != 0) || total_days == 0)
- {
- dt1_hour -= dt1_offset / SECS_PER_HOUR;
- dt1_offset %= SECS_PER_HOUR;
- dt1_minute -= dt1_offset / SECS_PER_MIN;
- dt1_offset %= SECS_PER_MIN;
- dt1_second -= dt1_offset;
-
- if (dt1_second < 0)
- {
- dt1_second += 60;
- dt1_minute -= 1;
- }
- else if (dt1_second > 60)
- {
- dt1_second -= 60;
- dt1_minute += 1;
- }
-
- if (dt1_minute < 0)
- {
- dt1_minute += 60;
- dt1_hour -= 1;
- }
- else if (dt1_minute > 60)
- {
- dt1_minute -= 60;
- dt1_hour += 1;
- }
-
- if (dt1_hour < 0)
- {
- dt1_hour += 24;
- dt1_day -= 1;
- }
- else if (dt1_hour > 24)
- {
- dt1_hour -= 24;
- dt1_day += 1;
- }
- }
-
- dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second);
- }
-
- if (dt2_is_datetime)
- {
- dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
- dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
- dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
- dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
-
- if ((!in_same_tz && dt2_offset != 0) || total_days == 0)
- {
- dt2_hour -= dt2_offset / SECS_PER_HOUR;
- dt2_offset %= SECS_PER_HOUR;
- dt2_minute -= dt2_offset / SECS_PER_MIN;
- dt2_offset %= SECS_PER_MIN;
- dt2_second -= dt2_offset;
-
- if (dt2_second < 0)
- {
- dt2_second += 60;
- dt2_minute -= 1;
- }
- else if (dt2_second > 60)
- {
- dt2_second -= 60;
- dt2_minute += 1;
- }
-
- if (dt2_minute < 0)
- {
- dt2_minute += 60;
- dt2_hour -= 1;
- }
- else if (dt2_minute > 60)
- {
- dt2_minute -= 60;
- dt2_hour += 1;
- }
-
- if (dt2_hour < 0)
- {
- dt2_hour += 24;
- dt2_day -= 1;
- }
- else if (dt2_hour > 24)
- {
- dt2_hour -= 24;
- dt2_day += 1;
- }
- }
-
- dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second);
- }
-
- // Direct comparison between two datetimes does not work
- // so we need to check by properties
- int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond));
-
- if (dt1_gt_dt2)
- {
- PyObject *temp;
- temp = dt1;
- dt1 = dt2;
- dt2 = temp;
- sign = -1;
-
- // Retrieving properties
- dt1_year = PyDateTime_GET_YEAR(dt1);
- dt2_year = PyDateTime_GET_YEAR(dt2);
- dt1_month = PyDateTime_GET_MONTH(dt1);
- dt2_month = PyDateTime_GET_MONTH(dt2);
- dt1_day = PyDateTime_GET_DAY(dt1);
- dt2_day = PyDateTime_GET_DAY(dt2);
-
- if (dt2_is_datetime)
- {
- dt1_hour = PyDateTime_DATE_GET_HOUR(dt1);
- dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1);
- dt1_second = PyDateTime_DATE_GET_SECOND(dt1);
- dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1);
- }
-
- if (dt1_is_datetime)
- {
- dt2_hour = PyDateTime_DATE_GET_HOUR(dt2);
- dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2);
- dt2_second = PyDateTime_DATE_GET_SECOND(dt2);
- dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2);
- }
-
- total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day));
- }
-
- year_diff = dt2_year - dt1_year;
- month_diff = dt2_month - dt1_month;
- day_diff = dt2_day - dt1_day;
- hour_diff = dt2_hour - dt1_hour;
- minute_diff = dt2_minute - dt1_minute;
- second_diff = dt2_second - dt1_second;
- microsecond_diff = dt2_microsecond - dt1_microsecond;
-
- if (microsecond_diff < 0)
- {
- microsecond_diff += 1e6;
- second_diff -= 1;
- }
-
- if (second_diff < 0)
- {
- second_diff += 60;
- minute_diff -= 1;
- }
-
- if (minute_diff < 0)
- {
- minute_diff += 60;
- hour_diff -= 1;
- }
-
- if (hour_diff < 0)
- {
- hour_diff += 24;
- day_diff -= 1;
- }
-
- if (day_diff < 0)
- {
- // If we have a difference in days,
- // we have to check if they represent months
- year = dt2_year;
- month = dt2_month;
-
- if (month == 1)
- {
- month = 12;
- year -= 1;
- }
- else
- {
- month -= 1;
- }
-
- leap = _is_leap(year);
-
- days_in_last_month = DAYS_PER_MONTHS[leap][month];
- days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month];
-
- if (day_diff < days_in_month - days_in_last_month)
- {
- // We don't have a full month, we calculate days
- if (days_in_last_month < dt1_day)
- {
- day_diff += dt1_day;
- }
- else
- {
- day_diff += days_in_last_month;
- }
- }
- else if (day_diff == days_in_month - days_in_last_month)
- {
- // We have exactly a full month
- // We remove the days difference
- // and add one to the months difference
- day_diff = 0;
- month_diff += 1;
- }
- else
- {
- // We have a full month
- day_diff += days_in_last_month;
- }
-
- month_diff -= 1;
- }
-
- if (month_diff < 0)
- {
- month_diff += 12;
- year_diff -= 1;
- }
-
- return new_diff(
- year_diff * sign,
- month_diff * sign,
- day_diff * sign,
- hour_diff * sign,
- minute_diff * sign,
- second_diff * sign,
- microsecond_diff * sign,
- total_days * sign);
-}
-
-/* ------------------------------------------------------------------------- */
-
-static PyMethodDef helpers_methods[] = {
- {"is_leap",
- (PyCFunction)is_leap,
- METH_VARARGS,
- PyDoc_STR("Checks if a year is a leap year.")},
- {"is_long_year",
- (PyCFunction)is_long_year,
- METH_VARARGS,
- PyDoc_STR("Checks if a year is a long year.")},
- {"week_day",
- (PyCFunction)week_day,
- METH_VARARGS,
- PyDoc_STR("Returns the weekday number.")},
- {"days_in_year",
- (PyCFunction)days_in_year,
- METH_VARARGS,
- PyDoc_STR("Returns the number of days in the given year.")},
- {"timestamp",
- (PyCFunction)timestamp,
- METH_VARARGS,
- PyDoc_STR("Returns the timestamp of the given datetime.")},
- {"local_time",
- (PyCFunction)local_time,
- METH_VARARGS,
- PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")},
- {"precise_diff",
- (PyCFunction)precise_diff,
- METH_VARARGS,
- PyDoc_STR("Calculate a precise difference between two datetimes.")},
- {NULL}};
-
-/* ------------------------------------------------------------------------- */
-
-static struct PyModuleDef moduledef = {
- PyModuleDef_HEAD_INIT,
- "_helpers",
- NULL,
- -1,
- helpers_methods,
- NULL,
- NULL,
- NULL,
- NULL,
-};
-
-PyMODINIT_FUNC
-PyInit__helpers(void)
-{
- PyObject *module;
-
- PyDateTime_IMPORT;
-
- module = PyModule_Create(&moduledef);
-
- if (module == NULL)
- return NULL;
-
- // Diff declaration
- Diff_type.tp_new = PyType_GenericNew;
- Diff_type.tp_members = Diff_members;
- Diff_type.tp_init = (initproc)Diff_init;
-
- if (PyType_Ready(&Diff_type) < 0)
- return NULL;
-
- PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type);
-
- return module;
-}
+/* ------------------------------------------------------------------------- */ + +#include <Python.h> +#include <datetime.h> +#include <structmember.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +/* ------------------------------------------------------------------------- */ + +#define EPOCH_YEAR 1970 + +#define DAYS_PER_N_YEAR 365 +#define DAYS_PER_L_YEAR 366 + +#define USECS_PER_SEC 1000000 + +#define SECS_PER_MIN 60 +#define SECS_PER_HOUR (60 * SECS_PER_MIN) +#define SECS_PER_DAY (SECS_PER_HOUR * 24) + +// 400-year chunks always have 146097 days (20871 weeks). +#define DAYS_PER_400_YEARS 146097L +#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) + +// The number of seconds in an aligned 100-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int64_t SECS_PER_100_YEARS[2] = { + (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY}; + +// The number of seconds in an aligned 4-year chunk, for those that +// do not begin with a leap year and those that do respectively. +const int32_t SECS_PER_4_YEARS[2] = { + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY}; + +// The number of seconds in non-leap and leap years respectively. +const int32_t SECS_PER_YEAR[2] = { + DAYS_PER_N_YEAR * SECS_PER_DAY, + DAYS_PER_L_YEAR *SECS_PER_DAY}; + +#define MONTHS_PER_YEAR 12 + +// The month lengths in non-leap and leap years respectively. +const int32_t DAYS_PER_MONTHS[2][13] = { + {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; + +// The day offsets of the beginning of each (1-based) month in non-leap +// and leap years respectively. +// For example, in a leap year there are 335 days before December. +const int32_t MONTHS_OFFSETS[2][14] = { + {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, + {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}}; + +const int DAY_OF_WEEK_TABLE[12] = { + 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + +#define TM_SUNDAY 0 +#define TM_MONDAY 1 +#define TM_TUESDAY 2 +#define TM_WEDNESDAY 3 +#define TM_THURSDAY 4 +#define TM_FRIDAY 5 +#define TM_SATURDAY 6 + +#define TM_JANUARY 0 +#define TM_FEBRUARY 1 +#define TM_MARCH 2 +#define TM_APRIL 3 +#define TM_MAY 4 +#define TM_JUNE 5 +#define TM_JULY 6 +#define TM_AUGUST 7 +#define TM_SEPTEMBER 8 +#define TM_OCTOBER 9 +#define TM_NOVEMBER 10 +#define TM_DECEMBER 11 + +/* ------------------------------------------------------------------------- */ + +int _p(int y) +{ + return y + y / 4 - y / 100 + y / 400; +} + +int _is_leap(int year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +int _is_long_year(int year) +{ + return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3); +} + +int _week_day(int year, int month, int day) +{ + int y; + int w; + + y = year - (month < 3); + + w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; + + if (!w) + { + w = 7; + } + + return w; +} + +int _days_in_year(int year) +{ + if (_is_leap(year)) + { + return DAYS_PER_L_YEAR; + } + + return DAYS_PER_N_YEAR; +} + +int _day_number(int year, int month, int day) +{ + month = (month + 9) % 12; + year = year - month / 10; + + return ( + 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1)); +} + +int _get_offset(PyObject *dt) +{ + PyObject *tzinfo; + PyObject *offset; + + tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; + + if (tzinfo != Py_None) + { + offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt); + + return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset); + } + + return 0; +} + +int _has_tzinfo(PyObject *dt) +{ + return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo; +} + +char *_get_tz_name(PyObject *dt) +{ + PyObject *tzinfo; + char *tz = ""; + + tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; + + if (tzinfo != Py_None) + { + if (PyObject_HasAttrString(tzinfo, "key")) + { + // zoneinfo timezone + tz = (char *)PyUnicode_AsUTF8( + PyObject_GetAttrString(tzinfo, "name")); + } + else if (PyObject_HasAttrString(tzinfo, "name")) + { + // Pendulum timezone + tz = (char *)PyUnicode_AsUTF8( + PyObject_GetAttrString(tzinfo, "name")); + } + else if (PyObject_HasAttrString(tzinfo, "zone")) + { + // pytz timezone + tz = (char *)PyUnicode_AsUTF8( + PyObject_GetAttrString(tzinfo, "zone")); + } + } + + return tz; +} + +/* ------------------------ Custom Types ------------------------------- */ + +/* + * class Diff(): + */ +typedef struct +{ + PyObject_HEAD int years; + int months; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + int total_days; +} Diff; + +/* + * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days): + * self.years = years + * # ... +*/ +static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs) +{ + int years; + int months; + int days; + int hours; + int minutes; + int seconds; + int microseconds; + int total_days; + + if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days)) + return -1; + + self->years = years; + self->months = months; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + self->total_days = total_days; + + return 0; +} + +/* + * def __repr__(self): + * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( + * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds + * ) + */ +static PyObject *Diff_repr(Diff *self) +{ + return PyUnicode_FromFormat( + "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds", + self->years, + self->months, + self->days, + self->hours, + self->minutes, + self->seconds, + self->microseconds); +} + +/* + * Instantiate new Diff_type object + * Skip overhead of calling PyObject_New and PyObject_Init. + * Directly allocate object. + */ +static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type) +{ + Diff *self = (Diff *)(type->tp_alloc(type, 0)); + + if (self != NULL) + { + self->years = years; + self->months = months; + self->days = days; + self->hours = hours; + self->minutes = minutes; + self->seconds = seconds; + self->microseconds = microseconds; + self->total_days = total_days; + } + + return (PyObject *)self; +} + +/* + * Class member / class attributes + */ +static PyMemberDef Diff_members[] = { + {"years", T_INT, offsetof(Diff, years), 0, "years in diff"}, + {"months", T_INT, offsetof(Diff, months), 0, "months in diff"}, + {"days", T_INT, offsetof(Diff, days), 0, "days in diff"}, + {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"}, + {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"}, + {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"}, + {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"}, + {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"}, + {NULL}}; + +static PyTypeObject Diff_type = { + PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */ + sizeof(Diff), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_as_async */ + (reprfunc)Diff_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)Diff_repr, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Precise difference between two datetime objects", /* tp_doc */ +}; + +#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type) + +/* -------------------------- Functions --------------------------*/ + +PyObject *is_leap(PyObject *self, PyObject *args) +{ + PyObject *leap; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + leap = PyBool_FromLong(_is_leap(year)); + + return leap; +} + +PyObject *is_long_year(PyObject *self, PyObject *args) +{ + PyObject *is_long; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + is_long = PyBool_FromLong(_is_long_year(year)); + + return is_long; +} + +PyObject *week_day(PyObject *self, PyObject *args) +{ + PyObject *wd; + int year; + int month; + int day; + + if (!PyArg_ParseTuple(args, "iii", &year, &month, &day)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + wd = PyLong_FromLong(_week_day(year, month, day)); + + return wd; +} + +PyObject *days_in_year(PyObject *self, PyObject *args) +{ + PyObject *ndays; + int year; + + if (!PyArg_ParseTuple(args, "i", &year)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + ndays = PyLong_FromLong(_days_in_year(year)); + + return ndays; +} + +PyObject *timestamp(PyObject *self, PyObject *args) +{ + int64_t result; + PyObject *dt; + + if (!PyArg_ParseTuple(args, "O", &dt)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + int year = (double)PyDateTime_GET_YEAR(dt); + int month = PyDateTime_GET_MONTH(dt); + int day = PyDateTime_GET_DAY(dt); + int hour = PyDateTime_DATE_GET_HOUR(dt); + int minute = PyDateTime_DATE_GET_MINUTE(dt); + int second = PyDateTime_DATE_GET_SECOND(dt); + + result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month]; + result += (int)floor((double)(year - 1968) / 4); + result -= (year - 1900) / 100; + result += (year - 1600) / 400; + + if (_is_leap(year) && month < 3) + { + result -= 1; + } + + result += day - 1; + result *= 24; + result += hour; + result *= 60; + result += minute; + result *= 60; + result += second; + + return PyLong_FromSsize_t(result); +} + +PyObject *local_time(PyObject *self, PyObject *args) +{ + double unix_time; + int32_t utc_offset; + int32_t year; + int32_t microsecond; + int64_t seconds; + int32_t leap_year; + int64_t sec_per_100years; + int64_t sec_per_4years; + int32_t sec_per_year; + int32_t month; + int32_t day; + int32_t month_offset; + int32_t hour; + int32_t minute; + int32_t second; + + if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + year = EPOCH_YEAR; + seconds = (int64_t)floor(unix_time); + + // Shift to a base year that is 400-year aligned. + if (seconds >= 0) + { + seconds -= 10957L * SECS_PER_DAY; + year += 30; // == 2000; + } + else + { + seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY; + year -= 370; // == 1600; + } + + seconds += utc_offset; + + // Handle years in chunks of 400/100/4/1 + year += 400 * (seconds / SECS_PER_400_YEARS); + seconds %= SECS_PER_400_YEARS; + if (seconds < 0) + { + seconds += SECS_PER_400_YEARS; + year -= 400; + } + + leap_year = 1; // 4-century aligned + + sec_per_100years = SECS_PER_100_YEARS[leap_year]; + + while (seconds >= sec_per_100years) + { + seconds -= sec_per_100years; + year += 100; + leap_year = 0; // 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year]; + } + + sec_per_4years = SECS_PER_4_YEARS[leap_year]; + while (seconds >= sec_per_4years) + { + seconds -= sec_per_4years; + year += 4; + leap_year = 1; // 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year]; + } + + sec_per_year = SECS_PER_YEAR[leap_year]; + while (seconds >= sec_per_year) + { + seconds -= sec_per_year; + year += 1; + leap_year = 0; // non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year]; + } + + // Handle months and days + month = TM_DECEMBER + 1; + day = seconds / SECS_PER_DAY + 1; + seconds %= SECS_PER_DAY; + while (month != TM_JANUARY + 1) + { + month_offset = MONTHS_OFFSETS[leap_year][month]; + if (day > month_offset) + { + day -= month_offset; + break; + } + + month -= 1; + } + + // Handle hours, minutes and seconds + hour = seconds / SECS_PER_HOUR; + seconds %= SECS_PER_HOUR; + minute = seconds / SECS_PER_MIN; + second = seconds % SECS_PER_MIN; + + return Py_BuildValue("NNNNNNN", + PyLong_FromLong(year), + PyLong_FromLong(month), + PyLong_FromLong(day), + PyLong_FromLong(hour), + PyLong_FromLong(minute), + PyLong_FromLong(second), + PyLong_FromLong(microsecond)); +} + +// Calculate a precise difference between two datetimes. +PyObject *precise_diff(PyObject *self, PyObject *args) +{ + PyObject *dt1; + PyObject *dt2; + + if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2)) + { + PyErr_SetString( + PyExc_ValueError, "Invalid parameters"); + return NULL; + } + + int year_diff = 0; + int month_diff = 0; + int day_diff = 0; + int hour_diff = 0; + int minute_diff = 0; + int second_diff = 0; + int microsecond_diff = 0; + int sign = 1; + int year; + int month; + int leap; + int days_in_last_month; + int days_in_month; + int dt1_year = PyDateTime_GET_YEAR(dt1); + int dt2_year = PyDateTime_GET_YEAR(dt2); + int dt1_month = PyDateTime_GET_MONTH(dt1); + int dt2_month = PyDateTime_GET_MONTH(dt2); + int dt1_day = PyDateTime_GET_DAY(dt1); + int dt2_day = PyDateTime_GET_DAY(dt2); + int dt1_hour = 0; + int dt2_hour = 0; + int dt1_minute = 0; + int dt2_minute = 0; + int dt1_second = 0; + int dt2_second = 0; + int dt1_microsecond = 0; + int dt2_microsecond = 0; + int dt1_total_seconds = 0; + int dt2_total_seconds = 0; + int dt1_offset = 0; + int dt2_offset = 0; + int dt1_is_datetime = PyDateTime_Check(dt1); + int dt2_is_datetime = PyDateTime_Check(dt2); + char *tz1 = ""; + char *tz2 = ""; + int in_same_tz = 0; + int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); + + // If both dates are datetimes, we check + // If we are in the same timezone + if (dt1_is_datetime && dt2_is_datetime) + { + if (_has_tzinfo(dt1)) + { + tz1 = _get_tz_name(dt1); + dt1_offset = _get_offset(dt1); + } + + if (_has_tzinfo(dt2)) + { + tz2 = _get_tz_name(dt2); + dt2_offset = _get_offset(dt2); + } + + in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1); + } + + // If we have datetimes (and not only dates) + // we get the information we need + if (dt1_is_datetime) + { + dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); + dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); + dt1_second = PyDateTime_DATE_GET_SECOND(dt1); + dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); + + if ((!in_same_tz && dt1_offset != 0) || total_days == 0) + { + dt1_hour -= dt1_offset / SECS_PER_HOUR; + dt1_offset %= SECS_PER_HOUR; + dt1_minute -= dt1_offset / SECS_PER_MIN; + dt1_offset %= SECS_PER_MIN; + dt1_second -= dt1_offset; + + if (dt1_second < 0) + { + dt1_second += 60; + dt1_minute -= 1; + } + else if (dt1_second > 60) + { + dt1_second -= 60; + dt1_minute += 1; + } + + if (dt1_minute < 0) + { + dt1_minute += 60; + dt1_hour -= 1; + } + else if (dt1_minute > 60) + { + dt1_minute -= 60; + dt1_hour += 1; + } + + if (dt1_hour < 0) + { + dt1_hour += 24; + dt1_day -= 1; + } + else if (dt1_hour > 24) + { + dt1_hour -= 24; + dt1_day += 1; + } + } + + dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second); + } + + if (dt2_is_datetime) + { + dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); + dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); + dt2_second = PyDateTime_DATE_GET_SECOND(dt2); + dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); + + if ((!in_same_tz && dt2_offset != 0) || total_days == 0) + { + dt2_hour -= dt2_offset / SECS_PER_HOUR; + dt2_offset %= SECS_PER_HOUR; + dt2_minute -= dt2_offset / SECS_PER_MIN; + dt2_offset %= SECS_PER_MIN; + dt2_second -= dt2_offset; + + if (dt2_second < 0) + { + dt2_second += 60; + dt2_minute -= 1; + } + else if (dt2_second > 60) + { + dt2_second -= 60; + dt2_minute += 1; + } + + if (dt2_minute < 0) + { + dt2_minute += 60; + dt2_hour -= 1; + } + else if (dt2_minute > 60) + { + dt2_minute -= 60; + dt2_hour += 1; + } + + if (dt2_hour < 0) + { + dt2_hour += 24; + dt2_day -= 1; + } + else if (dt2_hour > 24) + { + dt2_hour -= 24; + dt2_day += 1; + } + } + + dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second); + } + + // Direct comparison between two datetimes does not work + // so we need to check by properties + int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond)); + + if (dt1_gt_dt2) + { + PyObject *temp; + temp = dt1; + dt1 = dt2; + dt2 = temp; + sign = -1; + + // Retrieving properties + dt1_year = PyDateTime_GET_YEAR(dt1); + dt2_year = PyDateTime_GET_YEAR(dt2); + dt1_month = PyDateTime_GET_MONTH(dt1); + dt2_month = PyDateTime_GET_MONTH(dt2); + dt1_day = PyDateTime_GET_DAY(dt1); + dt2_day = PyDateTime_GET_DAY(dt2); + + if (dt2_is_datetime) + { + dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); + dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); + dt1_second = PyDateTime_DATE_GET_SECOND(dt1); + dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); + } + + if (dt1_is_datetime) + { + dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); + dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); + dt2_second = PyDateTime_DATE_GET_SECOND(dt2); + dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); + } + + total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); + } + + year_diff = dt2_year - dt1_year; + month_diff = dt2_month - dt1_month; + day_diff = dt2_day - dt1_day; + hour_diff = dt2_hour - dt1_hour; + minute_diff = dt2_minute - dt1_minute; + second_diff = dt2_second - dt1_second; + microsecond_diff = dt2_microsecond - dt1_microsecond; + + if (microsecond_diff < 0) + { + microsecond_diff += 1e6; + second_diff -= 1; + } + + if (second_diff < 0) + { + second_diff += 60; + minute_diff -= 1; + } + + if (minute_diff < 0) + { + minute_diff += 60; + hour_diff -= 1; + } + + if (hour_diff < 0) + { + hour_diff += 24; + day_diff -= 1; + } + + if (day_diff < 0) + { + // If we have a difference in days, + // we have to check if they represent months + year = dt2_year; + month = dt2_month; + + if (month == 1) + { + month = 12; + year -= 1; + } + else + { + month -= 1; + } + + leap = _is_leap(year); + + days_in_last_month = DAYS_PER_MONTHS[leap][month]; + days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month]; + + if (day_diff < days_in_month - days_in_last_month) + { + // We don't have a full month, we calculate days + if (days_in_last_month < dt1_day) + { + day_diff += dt1_day; + } + else + { + day_diff += days_in_last_month; + } + } + else if (day_diff == days_in_month - days_in_last_month) + { + // We have exactly a full month + // We remove the days difference + // and add one to the months difference + day_diff = 0; + month_diff += 1; + } + else + { + // We have a full month + day_diff += days_in_last_month; + } + + month_diff -= 1; + } + + if (month_diff < 0) + { + month_diff += 12; + year_diff -= 1; + } + + return new_diff( + year_diff * sign, + month_diff * sign, + day_diff * sign, + hour_diff * sign, + minute_diff * sign, + second_diff * sign, + microsecond_diff * sign, + total_days * sign); +} + +/* ------------------------------------------------------------------------- */ + +static PyMethodDef helpers_methods[] = { + {"is_leap", + (PyCFunction)is_leap, + METH_VARARGS, + PyDoc_STR("Checks if a year is a leap year.")}, + {"is_long_year", + (PyCFunction)is_long_year, + METH_VARARGS, + PyDoc_STR("Checks if a year is a long year.")}, + {"week_day", + (PyCFunction)week_day, + METH_VARARGS, + PyDoc_STR("Returns the weekday number.")}, + {"days_in_year", + (PyCFunction)days_in_year, + METH_VARARGS, + PyDoc_STR("Returns the number of days in the given year.")}, + {"timestamp", + (PyCFunction)timestamp, + METH_VARARGS, + PyDoc_STR("Returns the timestamp of the given datetime.")}, + {"local_time", + (PyCFunction)local_time, + METH_VARARGS, + PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")}, + {"precise_diff", + (PyCFunction)precise_diff, + METH_VARARGS, + PyDoc_STR("Calculate a precise difference between two datetimes.")}, + {NULL}}; + +/* ------------------------------------------------------------------------- */ + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_helpers", + NULL, + -1, + helpers_methods, + NULL, + NULL, + NULL, + NULL, +}; + +PyMODINIT_FUNC +PyInit__helpers(void) +{ + PyObject *module; + + PyDateTime_IMPORT; + + module = PyModule_Create(&moduledef); + + if (module == NULL) + return NULL; + + // Diff declaration + Diff_type.tp_new = PyType_GenericNew; + Diff_type.tp_members = Diff_members; + Diff_type.tp_init = (initproc)Diff_init; + + if (PyType_Ready(&Diff_type) < 0) + return NULL; + + PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type); + + return module; +} diff --git a/pendulum/_extensions/_helpers.pyi b/pendulum/_extensions/_helpers.pyi new file mode 100644 index 0000000..99a5397 --- /dev/null +++ b/pendulum/_extensions/_helpers.pyi @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections import namedtuple +from datetime import date +from datetime import datetime + +def days_in_year(year: int) -> int: ... +def is_leap(year: int) -> bool: ... +def is_long_year(year: int) -> bool: ... +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: ... + +class PreciseDiff( + namedtuple( + "PreciseDiff", + "years months days " "hours minutes seconds microseconds " "total_days", + ) +): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + +def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... +def timestamp(dt: datetime) -> int: ... +def week_day(year: int, month: int, day: int) -> int: ... diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py index 16d078c..01066a3 100644 --- a/pendulum/_extensions/helpers.py +++ b/pendulum/_extensions/helpers.py @@ -1,358 +1,364 @@ -import datetime
-import math
-import typing
-
-from collections import namedtuple
-
-from ..constants import DAY_OF_WEEK_TABLE
-from ..constants import DAYS_PER_L_YEAR
-from ..constants import DAYS_PER_MONTHS
-from ..constants import DAYS_PER_N_YEAR
-from ..constants import EPOCH_YEAR
-from ..constants import MONTHS_OFFSETS
-from ..constants import SECS_PER_4_YEARS
-from ..constants import SECS_PER_100_YEARS
-from ..constants import SECS_PER_400_YEARS
-from ..constants import SECS_PER_DAY
-from ..constants import SECS_PER_HOUR
-from ..constants import SECS_PER_MIN
-from ..constants import SECS_PER_YEAR
-from ..constants import TM_DECEMBER
-from ..constants import TM_JANUARY
-
-
-class PreciseDiff(
- namedtuple(
- "PreciseDiff",
- "years months days " "hours minutes seconds microseconds " "total_days",
- )
-):
- def __repr__(self):
- return (
- "{years} years "
- "{months} months "
- "{days} days "
- "{hours} hours "
- "{minutes} minutes "
- "{seconds} seconds "
- "{microseconds} microseconds"
- ).format(
- years=self.years,
- months=self.months,
- days=self.days,
- hours=self.hours,
- minutes=self.minutes,
- seconds=self.seconds,
- microseconds=self.microseconds,
- )
-
-
-def is_leap(year): # type: (int) -> bool
- return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
-
-
-def is_long_year(year): # type: (int) -> bool
- def p(y):
- return y + y // 4 - y // 100 + y // 400
-
- return p(year) % 7 == 4 or p(year - 1) % 7 == 3
-
-
-def week_day(year, month, day): # type: (int, int, int) -> int
- if month < 3:
- year -= 1
-
- w = (
- year
- + year // 4
- - year // 100
- + year // 400
- + DAY_OF_WEEK_TABLE[month - 1]
- + day
- ) % 7
-
- if not w:
- w = 7
-
- return w
-
-
-def days_in_year(year): # type: (int) -> int
- if is_leap(year):
- return DAYS_PER_L_YEAR
-
- return DAYS_PER_N_YEAR
-
-
-def timestamp(dt): # type: (datetime.datetime) -> int
- year = dt.year
-
- result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month]
- result += (year - 1968) // 4
- result -= (year - 1900) // 100
- result += (year - 1600) // 400
-
- if is_leap(year) and dt.month < 3:
- result -= 1
-
- result += dt.day - 1
- result *= 24
- result += dt.hour
- result *= 60
- result += dt.minute
- result *= 60
- result += dt.second
-
- return result
-
-
-def local_time(
- unix_time, utc_offset, microseconds
-): # type: (int, int, int) -> typing.Tuple[int, int, int, int, int, int, int]
- """
- Returns a UNIX time as a broken down time
- for a particular transition type.
-
- :type unix_time: int
- :type utc_offset: int
- :type microseconds: int
-
- :rtype: tuple
- """
- year = EPOCH_YEAR
- seconds = int(math.floor(unix_time))
-
- # Shift to a base year that is 400-year aligned.
- if seconds >= 0:
- seconds -= 10957 * SECS_PER_DAY
- year += 30 # == 2000
- else:
- seconds += (146097 - 10957) * SECS_PER_DAY
- year -= 370 # == 1600
-
- seconds += utc_offset
-
- # Handle years in chunks of 400/100/4/1
- year += 400 * (seconds // SECS_PER_400_YEARS)
- seconds %= SECS_PER_400_YEARS
- if seconds < 0:
- seconds += SECS_PER_400_YEARS
- year -= 400
-
- leap_year = 1 # 4-century aligned
-
- sec_per_100years = SECS_PER_100_YEARS[leap_year]
- while seconds >= sec_per_100years:
- seconds -= sec_per_100years
- year += 100
- leap_year = 0 # 1-century, non 4-century aligned
- sec_per_100years = SECS_PER_100_YEARS[leap_year]
-
- sec_per_4years = SECS_PER_4_YEARS[leap_year]
- while seconds >= sec_per_4years:
- seconds -= sec_per_4years
- year += 4
- leap_year = 1 # 4-year, non century aligned
- sec_per_4years = SECS_PER_4_YEARS[leap_year]
-
- sec_per_year = SECS_PER_YEAR[leap_year]
- while seconds >= sec_per_year:
- seconds -= sec_per_year
- year += 1
- leap_year = 0 # non 4-year aligned
- sec_per_year = SECS_PER_YEAR[leap_year]
-
- # Handle months and days
- month = TM_DECEMBER + 1
- day = seconds // SECS_PER_DAY + 1
- seconds %= SECS_PER_DAY
- while month != TM_JANUARY + 1:
- month_offset = MONTHS_OFFSETS[leap_year][month]
- if day > month_offset:
- day -= month_offset
- break
-
- month -= 1
-
- # Handle hours, minutes, seconds and microseconds
- hour = seconds // SECS_PER_HOUR
- seconds %= SECS_PER_HOUR
- minute = seconds // SECS_PER_MIN
- second = seconds % SECS_PER_MIN
-
- return (year, month, day, hour, minute, second, microseconds)
-
-
-def precise_diff(
- d1, d2
-): # type: (typing.Union[datetime.datetime, datetime.date], typing.Union[datetime.datetime, datetime.date]) -> PreciseDiff
- """
- Calculate a precise difference between two datetimes.
-
- :param d1: The first datetime
- :type d1: datetime.datetime or datetime.date
-
- :param d2: The second datetime
- :type d2: datetime.datetime or datetime.date
-
- :rtype: PreciseDiff
- """
- sign = 1
-
- if d1 == d2:
- return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
-
- tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None
- tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None
-
- if (
- tzinfo1 is None
- and tzinfo2 is not None
- or tzinfo2 is None
- and tzinfo1 is not None
- ):
- raise ValueError(
- "Comparison between naive and aware datetimes is not supported"
- )
-
- if d1 > d2:
- d1, d2 = d2, d1
- sign = -1
-
- d_diff = 0
- hour_diff = 0
- min_diff = 0
- sec_diff = 0
- mic_diff = 0
- total_days = _day_number(d2.year, d2.month, d2.day) - _day_number(
- d1.year, d1.month, d1.day
- )
- in_same_tz = False
- tz1 = None
- tz2 = None
-
- # Trying to figure out the timezone names
- # If we can't find them, we assume different timezones
- if tzinfo1 and tzinfo2:
- if hasattr(tzinfo1, "name"):
- # Pendulum timezone
- tz1 = tzinfo1.name
- elif hasattr(tzinfo1, "zone"):
- # pytz timezone
- tz1 = tzinfo1.zone
-
- if hasattr(tzinfo2, "name"):
- tz2 = tzinfo2.name
- elif hasattr(tzinfo2, "zone"):
- tz2 = tzinfo2.zone
-
- in_same_tz = tz1 == tz2 and tz1 is not None
-
- if isinstance(d2, datetime.datetime):
- if isinstance(d1, datetime.datetime):
- # If we are not in the same timezone
- # we need to adjust
- #
- # We also need to adjust if we do not
- # have variable-length units
- if not in_same_tz or total_days == 0:
- offset1 = d1.utcoffset()
- offset2 = d2.utcoffset()
-
- if offset1:
- d1 = d1 - offset1
-
- if offset2:
- d2 = d2 - offset2
-
- hour_diff = d2.hour - d1.hour
- min_diff = d2.minute - d1.minute
- sec_diff = d2.second - d1.second
- mic_diff = d2.microsecond - d1.microsecond
- else:
- hour_diff = d2.hour
- min_diff = d2.minute
- sec_diff = d2.second
- mic_diff = d2.microsecond
-
- if mic_diff < 0:
- mic_diff += 1000000
- sec_diff -= 1
-
- if sec_diff < 0:
- sec_diff += 60
- min_diff -= 1
-
- if min_diff < 0:
- min_diff += 60
- hour_diff -= 1
-
- if hour_diff < 0:
- hour_diff += 24
- d_diff -= 1
-
- y_diff = d2.year - d1.year
- m_diff = d2.month - d1.month
- d_diff += d2.day - d1.day
-
- if d_diff < 0:
- year = d2.year
- month = d2.month
-
- if month == 1:
- month = 12
- year -= 1
- else:
- month -= 1
-
- leap = int(is_leap(year))
-
- days_in_last_month = DAYS_PER_MONTHS[leap][month]
- days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month]
-
- if d_diff < days_in_month - days_in_last_month:
- # We don't have a full month, we calculate days
- if days_in_last_month < d1.day:
- d_diff += d1.day
- else:
- d_diff += days_in_last_month
- elif d_diff == days_in_month - days_in_last_month:
- # We have exactly a full month
- # We remove the days difference
- # and add one to the months difference
- d_diff = 0
- m_diff += 1
- else:
- # We have a full month
- d_diff += days_in_last_month
-
- m_diff -= 1
-
- if m_diff < 0:
- m_diff += 12
- y_diff -= 1
-
- return PreciseDiff(
- sign * y_diff,
- sign * m_diff,
- sign * d_diff,
- sign * hour_diff,
- sign * min_diff,
- sign * sec_diff,
- sign * mic_diff,
- sign * total_days,
- )
-
-
-def _day_number(year, month, day): # type: (int, int, int) -> int
- month = (month + 9) % 12
- year = year - month // 10
-
- return (
- 365 * year
- + year // 4
- - year // 100
- + year // 400
- + (month * 306 + 5) // 10
- + (day - 1)
- )
+from __future__ import annotations + +import datetime +import math + +from collections import namedtuple +from typing import cast + +from pendulum.constants import DAY_OF_WEEK_TABLE +from pendulum.constants import DAYS_PER_L_YEAR +from pendulum.constants import DAYS_PER_MONTHS +from pendulum.constants import DAYS_PER_N_YEAR +from pendulum.constants import EPOCH_YEAR +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECS_PER_4_YEARS +from pendulum.constants import SECS_PER_100_YEARS +from pendulum.constants import SECS_PER_400_YEARS +from pendulum.constants import SECS_PER_DAY +from pendulum.constants import SECS_PER_HOUR +from pendulum.constants import SECS_PER_MIN +from pendulum.constants import SECS_PER_YEAR +from pendulum.constants import TM_DECEMBER +from pendulum.constants import TM_JANUARY +from pendulum.tz.timezone import Timezone +from pendulum.utils._compat import zoneinfo + + +class PreciseDiff( + namedtuple( + "PreciseDiff", + "years months days " "hours minutes seconds microseconds " "total_days", + ) +): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + + def __repr__(self) -> str: + return ( + f"{self.years} years " + f"{self.months} months " + f"{self.days} days " + f"{self.hours} hours " + f"{self.minutes} minutes " + f"{self.seconds} seconds " + f"{self.microseconds} microseconds" + ) + + +def is_leap(year: int) -> bool: + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def is_long_year(year: int) -> bool: + def p(y: int) -> int: + return y + y // 4 - y // 100 + y // 400 + + return p(year) % 7 == 4 or p(year - 1) % 7 == 3 + + +def week_day(year: int, month: int, day: int) -> int: + if month < 3: + year -= 1 + + w = ( + year + + year // 4 + - year // 100 + + year // 400 + + DAY_OF_WEEK_TABLE[month - 1] + + day + ) % 7 + + if not w: + w = 7 + + return w + + +def days_in_year(year: int) -> int: + if is_leap(year): + return DAYS_PER_L_YEAR + + return DAYS_PER_N_YEAR + + +def timestamp(dt: datetime.datetime) -> int: + year = dt.year + + result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month] + result += (year - 1968) // 4 + result -= (year - 1900) // 100 + result += (year - 1600) // 400 + + if is_leap(year) and dt.month < 3: + result -= 1 + + result += dt.day - 1 + result *= 24 + result += dt.hour + result *= 60 + result += dt.minute + result *= 60 + result += dt.second + + return result + + +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: + """ + Returns a UNIX time as a broken-down time + for a particular transition type. + """ + year = EPOCH_YEAR + seconds = int(math.floor(unix_time)) + + # Shift to a base year that is 400-year aligned. + if seconds >= 0: + seconds -= 10957 * SECS_PER_DAY + year += 30 # == 2000 + else: + seconds += (146097 - 10957) * SECS_PER_DAY + year -= 370 # == 1600 + + seconds += utc_offset + + # Handle years in chunks of 400/100/4/1 + year += 400 * (seconds // SECS_PER_400_YEARS) + seconds %= SECS_PER_400_YEARS + if seconds < 0: + seconds += SECS_PER_400_YEARS + year -= 400 + + leap_year = 1 # 4-century aligned + + sec_per_100years = SECS_PER_100_YEARS[leap_year] + while seconds >= sec_per_100years: + seconds -= sec_per_100years + year += 100 + leap_year = 0 # 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year] + + sec_per_4years = SECS_PER_4_YEARS[leap_year] + while seconds >= sec_per_4years: + seconds -= sec_per_4years + year += 4 + leap_year = 1 # 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year] + + sec_per_year = SECS_PER_YEAR[leap_year] + while seconds >= sec_per_year: + seconds -= sec_per_year + year += 1 + leap_year = 0 # non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year] + + # Handle months and days + month = TM_DECEMBER + 1 + day = seconds // SECS_PER_DAY + 1 + seconds %= SECS_PER_DAY + while month != TM_JANUARY + 1: + month_offset = MONTHS_OFFSETS[leap_year][month] + if day > month_offset: + day -= month_offset + break + + month -= 1 + + # Handle hours, minutes, seconds and microseconds + hour = seconds // SECS_PER_HOUR + seconds %= SECS_PER_HOUR + minute = seconds // SECS_PER_MIN + second = seconds % SECS_PER_MIN + + return year, month, day, hour, minute, second, microseconds + + +def precise_diff( + d1: datetime.datetime | datetime.date, d2: datetime.datetime | datetime.date +) -> PreciseDiff: + """ + Calculate a precise difference between two datetimes. + + :param d1: The first datetime + :param d2: The second datetime + """ + sign = 1 + + if d1 == d2: + return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) + + tzinfo1: datetime.tzinfo | None = ( + d1.tzinfo if isinstance(d1, datetime.datetime) else None + ) + tzinfo2: datetime.tzinfo | None = ( + d2.tzinfo if isinstance(d2, datetime.datetime) else None + ) + + if ( + tzinfo1 is None + and tzinfo2 is not None + or tzinfo2 is None + and tzinfo1 is not None + ): + raise ValueError( + "Comparison between naive and aware datetimes is not supported" + ) + + if d1 > d2: + d1, d2 = d2, d1 + sign = -1 + + d_diff = 0 + hour_diff = 0 + min_diff = 0 + sec_diff = 0 + mic_diff = 0 + total_days = _day_number(d2.year, d2.month, d2.day) - _day_number( + d1.year, d1.month, d1.day + ) + in_same_tz = False + tz1 = None + tz2 = None + + # Trying to figure out the timezone names + # If we can't find them, we assume different timezones + if tzinfo1 and tzinfo2: + tz1 = _get_tzinfo_name(tzinfo1) + tz2 = _get_tzinfo_name(tzinfo2) + + in_same_tz = tz1 == tz2 and tz1 is not None + + if isinstance(d2, datetime.datetime): + if isinstance(d1, datetime.datetime): + # If we are not in the same timezone + # we need to adjust + # + # We also need to adjust if we do not + # have variable-length units + if not in_same_tz or total_days == 0: + offset1 = d1.utcoffset() + offset2 = d2.utcoffset() + + if offset1: + d1 = d1 - offset1 + + if offset2: + d2 = d2 - offset2 + + hour_diff = d2.hour - d1.hour + min_diff = d2.minute - d1.minute + sec_diff = d2.second - d1.second + mic_diff = d2.microsecond - d1.microsecond + else: + hour_diff = d2.hour + min_diff = d2.minute + sec_diff = d2.second + mic_diff = d2.microsecond + + if mic_diff < 0: + mic_diff += 1000000 + sec_diff -= 1 + + if sec_diff < 0: + sec_diff += 60 + min_diff -= 1 + + if min_diff < 0: + min_diff += 60 + hour_diff -= 1 + + if hour_diff < 0: + hour_diff += 24 + d_diff -= 1 + + y_diff = d2.year - d1.year + m_diff = d2.month - d1.month + d_diff += d2.day - d1.day + + if d_diff < 0: + year = d2.year + month = d2.month + + if month == 1: + month = 12 + year -= 1 + else: + month -= 1 + + leap = int(is_leap(year)) + + days_in_last_month = DAYS_PER_MONTHS[leap][month] + days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month] + + if d_diff < days_in_month - days_in_last_month: + # We don't have a full month, we calculate days + if days_in_last_month < d1.day: + d_diff += d1.day + else: + d_diff += days_in_last_month + elif d_diff == days_in_month - days_in_last_month: + # We have exactly a full month + # We remove the days difference + # and add one to the months difference + d_diff = 0 + m_diff += 1 + else: + # We have a full month + d_diff += days_in_last_month + + m_diff -= 1 + + if m_diff < 0: + m_diff += 12 + y_diff -= 1 + + return PreciseDiff( + sign * y_diff, + sign * m_diff, + sign * d_diff, + sign * hour_diff, + sign * min_diff, + sign * sec_diff, + sign * mic_diff, + sign * total_days, + ) + + +def _day_number(year: int, month: int, day: int) -> int: + month = (month + 9) % 12 + year = year - month // 10 + + return ( + 365 * year + + year // 4 + - year // 100 + + year // 400 + + (month * 306 + 5) // 10 + + (day - 1) + ) + + +def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None: + if tzinfo is None: + return None + + if hasattr(tzinfo, "key"): + # zoneinfo timezone + return cast(str, cast(zoneinfo.ZoneInfo, tzinfo).key) + elif hasattr(tzinfo, "name"): + # Pendulum timezone + return cast(Timezone, tzinfo).name + elif hasattr(tzinfo, "zone"): + # pytz timezone + return tzinfo.zone # type: ignore + + return None |