From 0066c2d2e7c93be6c7dc73765727e36539645bbc Mon Sep 17 00:00:00 2001 From: Lorenzo Cappelletti Date: Sun, 26 Sep 2021 21:59:50 +0200 Subject: [PATCH 1/4] docs/library: Add new module `datetime`. Signed-off-by: Lorenzo Cappelletti --- docs/library/datetime.rst | 696 ++++++++++++++++++++++++++++++++++++++ docs/library/index.rst | 1 + 2 files changed, 697 insertions(+) create mode 100644 docs/library/datetime.rst diff --git a/docs/library/datetime.rst b/docs/library/datetime.rst new file mode 100644 index 0000000000..a69acd05d3 --- /dev/null +++ b/docs/library/datetime.rst @@ -0,0 +1,696 @@ +:mod:`datetime` -- basic date and time types +============================================ + +.. module:: datetime + :synopsis: basic date and time types + +|see_cpython_module| :mod:`python:datetime`. + +This module supplies classes for manipulating dates, times, and deltas. + +:class:`datetime` objects may be categorized as “aware” or “naive” +depending on whether or not they include timezone information. +An aware object can locate itself relative to other aware objects. An +*aware* object represents a specific moment in time that is not open to +interpretation. + +A *naive* object does not contain enough information to unambiguously +locate itself relative to other :class:`datetime` objects. Whether a naive +object represents Coordinated Universal Time (UTC), local time, or time +in some other timezone is purely up to the program, just like it is up +to the program whether a particular number represents metres, miles, or +mass. Naive objects are easy to understand and to work with, at the cost +of ignoring some aspects of reality. + +For applications requiring aware objects, :class:`datetime` objects have +an optional time zone information attribute, *tzinfo*, that can be set to +an instance of a :class:`timezone` class. These objects capture +information about the offset from UTC time and the time zone name. + +The following classes are provided: + +* :class:`timedelta` +* :class:`timezone` +* :class:`datetime` + +Python's classes `date() +`_ and `time() +`_ +are mimicked with a class :class:`datetime` set to midnight, and a class +:class:`timedelta` in the range [0-24h). + + +:class:`timedelta` Objects +-------------------------- + +A :class:`timedelta` object represents a duration, the difference between two +dates or times. With respect to the Python module +`datetime `_, +this implementation is constrained as follows: + + * Minimum resolution is *1 second*, instead of *1 microsecond*. + * Masimum delta spans over ±24855 days (±2\ :sup:`31` seconds or ±68 years) + instead of ±999999999 days. + + +Class attributes +^^^^^^^^^^^^^^^^ + +.. attribute:: timedelta.MINYEAR + + The year of :attr:`timedelta.min`, i.e. + ``timedelta.min.tuple()[1] // (365*24*60*60) == -68``. + + +.. attribute:: timedelta.MAXYEAR + + The year of :attr:`timedelta.max`, i.e. + ``timedelta.max.tuple()[1] // (365*24*60*60) == 68``. + + +.. attribute:: timedelta.min + + The most negative :class:`timedelta` object, ``timedelta(-2**31)``. + + +.. attribute:: timedelta.max + + The most positive :class:`timedelta` object, ``timedelta(2**31 - 1)``. + + +.. attribute:: timedelta.resolution + + The smallest possible difference between non-equal :class:`timedelta` + objects, ``timedelta(seconds=1)``. + + +Class methods +^^^^^^^^^^^^^ + +.. class:: timedelta(hours=0, minutes=0, seconds=0, days=0, weeks=0) + +All arguments are optional and default to ``0``. Arguments may be integers +or floats, and may be positive or negative. Only seconds are stored +internally. Arguments are converted to those units: + + * A minute is converted to 60 seconds. + * An hour is converted to 3600 seconds. + * A week is converted to 7 days. + +If no argument is a float, the conversion and normalization processes are +exact (no information is lost). + + +.. method:: timedelta.total_seconds() + + Return the total number of seconds contained in the duration. + + +.. method:: timedelta.__add__(other) + + Return a :class:`timedelta` which represents the sum of two durations. + + +.. method:: timedelta.__sub__(other) + + Return a :class:`timedelta` which represents the difference between + two durations. + + +.. method:: timedelta.__mul__(other) + + Return a delta multiplied by an integer or float. The result is rounded to + the nearest second using round-half-to-even. + + +.. method:: timedelta.__truediv__(other) + + When *other* is a float or an integer, returns a delta divided by *other*. + The result is rounded to the nearest multiple of timedelta.resolution + using round-half-to-even. + + When *other* is a delta, division of overall duration by interval unit + *other*. Returns a float object. + + +.. method:: timedelta.__floordiv__(other) + + The floor is computed and the remainder (if any) is thrown away. When + *other* is a delta, an integer is returned. + + +.. method:: timedelta.__mod__(other) + + The remainder is computed as a :class:`timedelta` object. + + +.. method:: timedelta.__divmod__(other) + + Computes the quotient and the remainder: ``q = td1.__floordiv__(td2)`` and + ``r = td1.__mod__(td2)``. ``q`` is an integer and ``r`` is a :class:`timedelta` + object. + + +.. method:: timedelta.__neg__() + + Equivalent to ``td1.__mul__(-1)``. + + +.. method:: timedelta.__eq__(other) + + Equivalent to ``td1.total_seconds() == td2.total_seconds()``. + + +.. method:: timedelta.__le__(other) + + Equivalent to ``td1.total_seconds() <= td2.total_seconds()``. + + +.. method:: timedelta.__lt__(other) + + Equivalent to ``td1.total_seconds() < td2.total_seconds()``. + + +.. method:: timedelta.__ge__(other) + + Equivalent to ``td1.total_seconds() >= td2.total_seconds()``. + + +.. method:: timedelta.__gt__(other) + + Equivalent to ``td1.total_seconds() > td2.total_seconds()``. + + +.. method:: timedelta.__bool__() + + Return ``False`` when duration is ``0``. + + +.. method:: timedelta.__abs__() + + Return a positive delta. + + +.. method:: timedelta.isoformat() + + This method mimics Python's `isoformat() + `_ + for *time* objects by returning a string in the format ``HH:MM:SS``, where + ``HH``, ``MM``, and ``SS`` are two digits of the time delta's hours, + minutes and seconds, respectively, since midnight. This is only if value + is within the range [0-24h). + + For other values, it returns the format ``±Dd HH:MM:SS``, where ``±`` is + the sign of the delta and ``D`` its number of days. This is *not* ISO + compliant, but provides a complete representation. + + +.. method:: timedelta.tuple(sign_pos='') + + Return the tuple ``(sign, days, hours, minutes, seconds)``, where ``sign`` is + ``-`` if delta is negative, *sign_pos* otherwise. + + +Examples of usage +^^^^^^^^^^^^^^^^^ + +An example of normalization:: + + import datetime.timedelta + + # Components of another_year add up to exactly 365 days + year = timedelta(days=365) + another_year = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600) + print(year == another_year) # True + print(year.total_seconds()) # 31536000 + + +Examples of timedelta arithmetic:: + + import datetime.timedelta + + year = timedelta(days=365) + ten_years = year * 10 + print(ten_years) # 3650d 00:00:00 + nine_years = ten_years - year + print(nine_years) # 3285d 00:00:00 + three_years = nine_years // 3 + print(three_years) # 1095d 00:00:00 + + +:class:`timezone` Objects +------------------------- + +The :class:`timezone` class represents a timezone defined by a fixed +offset from UTC. Define a subclass of :class:`timezone` to capture +information about a particular time zone. + +An instance of :class:`timezone` can be passed to the constructors for +:class:`datetime`. The latter objects view their attributes as being in +local time, and the :class:`timezone` object supports methods revealing +offset of local time from UTC, the name of the time zone, and DST offset, +all relative to a date-time object passed to them. + + +Methods to customize +^^^^^^^^^^^^^^^^^^^^ + +A subclass of :class:`timezone` may need to override the following methods. +Exactly which methods are needed depends on the uses made of aware +:class:`datetime` objects. If in doubt, simply implement all of them. + + +.. method:: timezone.utcoffset(dt) + + Return offset of local time from UTC, as a :class:`timedelta` object + that is positive east of UTC. If local time is west of UTC, this should + be negative. + + This represents the *total* offset from UTC; for example, if a + :class:`timezone` object represents both time zone and DST adjustments, + :meth:`timezone.utcoffset` should return their sum. If the UTC offset + isn’t known, return ``None``. Else the value returned must be a + :class:`timedelta` object strictly between ``timedelta(hours=-24)`` and + ``timedelta(hours=24)`` (the magnitude of the offset must be less than one + day). Most implementations of :meth:`timezone.utcoffset` will probably + look like one of these two: + + return CONSTANT # fixed-offset class + return CONSTANT + self.dst(dt) # daylight-aware class + + If :meth:`timezone.utcoffset` does not return ``None``, :meth:`timezone.dst` + should not return None either. + + The default implementation of :meth:`timezone.utcoffset` returns the sum + of time zone and DST adjustments, if available. + +.. method:: timezone.dst(dt) + + Return the daylight saving time (DST) adjustment, as a :class:`timedelta` + object or ``None`` if DST information isn’t known. + + Return ``timedelta(0)`` if DST is not in effect. If DST is in effect, return + the offset as a :class:`timedelta` object (see :meth:`timezone.utcoffset` + for details). Note that DST offset, if applicable, has already been added + to the UTC offset returned by :meth:`timezone.utcoffset`, so there’s no + need to consult :meth:`timezone.dst` unless you’re interested in obtaining + DST info separately. + + Most implementations of :meth:`timezone.dst` will probably look like one + of these two:: + + def dst(self, dt): + # a fixed-offset class: doesn't account for DST + return timedelta(0) + + or:: + + def dst(self, dt): + # Code to set dston and dstoff to the time zone's DST + # transition times based on the input *dt*'s year, and + # expressed in standard local time. + + dt_ = dt.replace(tzinfo=None) + if dt_ >= dston and dt_ < dstoff: + return timedelta(hours=1) + else: + return timedelta(0) + + The default implementation of :meth:`timezone.dst` returns ``None``. + + +.. method:: timezone.tzname(dt) + + Return the time zone name corresponding to the :class:`datetime` object + *dt*, as a string. Nothing about string names is defined by the + :class:`datetime` module, and there’s no requirement that it mean anything + in particular. For example, “GMT”, “UTC”, “-500”, “-5:00”, “EDT”, + “US/Eastern”, “America/New York” are all valid replies. Return ``None`` if + a string name isn’t known. Note that this is a method rather than a fixed + string primarily because some :class:`timezone` subclasses will wish to + return different names depending on the specific value of *dt* passed, + especially if the :class:`timezone` class is accounting for daylight time. + + The default implementation of :meth:`timezone.tzname` returns the fixed + value specified when the :class:`timezone` instance is constructed. + If *name* is not provided in the constructor, the name returned by + ``tzname()`` is generated from the value of the ``offset`` as follows. + If *offset* is ``timedelta(0)``, the name is “UTC”, otherwise it returns + the string provided by :meth:`timezone.isoformat` method. + +These methods are called by a :class:`datetime` object, in response to their +methods of the same names. A :class:`datetime` object passes *self* as *dt* +argument. + + +Class attributes +^^^^^^^^^^^^^^^^ + +.. attribute:: timezone.utc + + The UTC timezone, ``timezone(timedelta(0))``. + + +Class methods +^^^^^^^^^^^^^ + +.. class:: timezone(offset, name=None) + + The *offset* argument must be specified as a :class:`timedelta` + object representing the difference between the local time and UTC. + It must be strictly between ``timedelta(hours=-24)`` and + ``timedelta(hours=24)``, otherwise :exc:`ValueError` is raised. + + The *name* argument is optional. If specified it must be a string + that will be used as the value returned by the :meth:`datetime.tzname` + method. + + +.. method:: timezone.isoformat(dt) + + Return a string in the format ``UTC±HH:MM``, where ``±`` is the sign of + *offset* from UTC, ``HH`` and ``MM`` are two digits of offset's hours and + offset's minutes respectively. If *offset* is ``timedelta(0)``, “UTC” + is returned. + + If *utc* is ``False``, this method always returns ``±HH:MM``. + + *dt* is needed in determining the right offset; it can be ``None``. + + +Examples of usage +^^^^^^^^^^^^^^^^^ + +`Central European Time `_ +(CET), used in most parts of Europe and a few North African countries, is a +standard time which is 1 hour ahead of Coordinated Universal Time (UTC). +As of 2011, all member states of the European Union observe summer time; +those that during the winter use CET use Central European Summer Time (CEST) +(or: UTC+02:00, daylight saving time) in summer (from last Sunday of March +to last Sunday of October). :: + + import datetime + + class Cet(datetime.timezone): + def __init__(self): + super().__init__(datetime.timedelta(hours=1), "CET") + + def dst(self, dt): + return datetime.timedelta(hours=1) if self.isdst(dt) else datetime.timedelta(0) + + def tzname(self, dt): + return 'CEST' if self.isdst(dt) else 'CET' + + def isdst(self, dt): + if dt is None: + return False + year, month, day, hour, minute, second, tz = dt.tuple() + if not 2000 <= year < 2100: + raise ValueError + if 3 < month < 10: + return True + if month == 3: + beg = 31 - (5*year//4 + 4) % 7 # last Sunday of March + if day < beg: return False + if day > beg: return True + return hour >= 3 + if month == 10: + end = 31 - (5*year//4 + 1) % 7 # last Sunday of October + if day < end: return True + if day > end: return False + return hour < 3 + return False + + tz = Cet() + print(tz.isoformat(datetime(2011, 1, 1))) # UTC+01:00 + print(tz.tzname (datetime(2011, 1, 1))) # CET + print(tz.isoformat(datetime(2011, 8, 1))) # UTC+02:00 + print(tz.tzname (datetime(2011, 8, 1))) # CEST + +The two formulas above are a simplification of the general +`Gauss's algorithm (disparate variation) +`_ +for calculating the week day of any given date:: + + def week_day (year, month, day, verbose=False): + if month <= 2: + raise ValueError + + Y = year + y = Y%100 + c = Y//100 + m = month - 2 + d = day + W = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat') + + k1 = d + int(2.6*m - 0.2) - 507*c//4 + k = k1 % 7 + w = (5*year//4 + k) % 7 + print(f'w(year) = (5*year//4 + {k}) % 7 # {100*c} <= year < {(c+1)*100}') + + if verbose: + print() + print(f'year = {year}, month = {month}, day = {day}') + print( 'w(y) = (d + int(2.6*m - 0.2) + y + y//4 + c//4 - 2*c) % 7') + print( 'w(y) = (d + int(2.6*m - 0.2) + 5*y//4 - 7*c//4) % 7') + print() + print( 'w(year) = (d + int(2.6*m - 0.2) + 5*year//4 - 5*c*100//4 - 7*c//4) % 7') + print( 'w(year) = (d + int(2.6*m - 0.2) + 5*year//4 - 507*c//4) % 7') + print(f'w(year) = ({d} + int(2.6*{m} - 0.2) + 5*year//4 - 507*{c}//4) % 7') + print(f'w(year) = (5*year//4 + ({k1}) % 7) % 7') + print(f'w(year) = (5*year//4 + {k}) % 7') + print(f'w({year}) = {w} ({W[w]})') + +which produces the following output for March and October:: + + >>> week_day(2000, 3, 31) + w(year) = (5*year//4 + 4) % 7 # 2000 <= year < 2100 + >>> week_day(2000, 10, 31) + w(year) = (5*year//4 + 1) % 7 # 2000 <= year < 2100 + + +:class:`datetime` Objects +------------------------- + +A :class:`datetime` object is a single object containing all the information +for specifying an absolute date and time point. + +:class:`datetime` assumes the current Gregorian calendar extended in both +directions, past and future. January 1 of year 1 is called day number 1, +January 2 of year 1 is called day number 2, and so on. + +:class:`datetime` assumes there are exactly 3600*24 seconds in every day and +subject to adjustment via a :class:`timezone` object. + + +Constructors +^^^^^^^^^^^^ + +.. class:: datetime(self, year, month, day, hour=0, minute=0, second=0, tzinfo=None) + + The *year*, *month* and *day* arguments are required. *tzinfo* may be + ``None``, or an instance of a :class:`timezone` class. The remaining + arguments must be integers in the following ranges: + + * ``MINYEAR <= year <= MAXYEAR``, + * ``1 <= month <= 12``, + * ``1 <= day <= number of days in the given month and year``, + * ``0 <= hour < 24``, + * ``0 <= minute < 60``, + * ``0 <= second < 60``, + + If an argument outside those ranges is given, :exc:`ValueError` is raised. + + +.. function:: fromisoformat(date_string) + + Return a :class:`datetime` corresponding to a *date_string* in the format + emitted by :meth:`datetime.isoformat`. + + Specifically, this function supports strings in the format:: + + YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] + + where ``*`` can match any single character. + + +.. function:: fromordinal(n) + + Return the :class:`datetime` corresponding to the proleptic Gregorian + ordinal, where January 1 of year 1 has ordinal 1. :exc:`ValueError` is + raised unless ``1 <= ordinal <= datetime.max.toordinal()``. The hour, + minute and second of the result are all 0, and *tzinfo* is ``None``. + + +.. function:: combine(date, time, tzinfo) + + Return a new :class:`datetime` object whose date components are equal to + the given *date* object’s (see :meth:`datetime.date`), and whose time + components are equal to the given *time* object’s (see + :meth:`datetime.time`). If the *tzinfo* argument is provided, its value + is used to set the *tzinfo* attribute of the result, otherwise the + *tzinfo* attribute of the *date* argument is used. + + +Class attributes +^^^^^^^^^^^^^^^^ + +.. attribute:: datetime.MINYEAR + + The smallest year number allowed in a :class:`datetime` object. + :attr:`datetime.MINYEAR` is ``1``. + + +.. attribute:: datetime.MAXYEAR + + The largest year number allowed in a :class:`datetime` object. + :attr:`datetime.MAXYEAR` is ``9999``. + + +.. attribute:: datetime.timezone + + The object passed as the *tzinfo* argument to the :class:`datetime` + constructor, or ``None`` if none was passed. + + +Class methods +^^^^^^^^^^^^^ + +.. method:: datetime.__add__(other) + + In the expression ``datetime2 = datetime1.__add__(timedelta)``, ``datetime2`` + is a duration of ``timedelta`` removed from ``datetime1``, moving forward + in time if ``timedelta > 0``, or backward if ``timedelta < 0``. The result + has the same :class:`timezone` attribute as the input ``datetime1``, and + ``datetime2 - datetime1 == timedelta`` after. + + Note that no time zone adjustments are done even if the input is an aware + object. + + +.. method:: datetime.__sub__(other) + + If *other* is an instance of :class:`timedelta`, the expression + ``datetime2 = datetime1.__sub__(timedelta)`` computes the ``datetime2`` such + that ``datetime2 + timedelta == datetime1``. As for addition, the result has + the same :class:`timezone` attribute as the input ``datetime1``, and no time + zone adjustments are done even if the input is aware. + + If *other* is an instance of :class:`datetime`, subtraction + ``timedelta = datetime2.__sub__(datetime1)`` is defined only if both operands + are naive, or if both are aware. If one is aware and the other is naive, + :exc:`TypeError` is raised. + + If both are naive, or both are aware and have the same :class:`timezone` + attribute, the :class:`timezone` attributes are ignored, and the result + is a :class:`timedelta` object *t* such that ``datetime2 + t == datetime1``. + No time zone adjustments are done in this case. + + If both are aware and have different :class:`timezone` attributes, ``a-b`` + acts as if *a* and *b* were first converted to naive UTC datetimes first. + + +.. method:: datetime.__lt__(other) + + Equivalent to ``dt1.toordinal() < dt2.toordinal()``. + + +.. method:: datetime.__le__(other) + + Equivalent to ``dt1.toordinal() <= dt2.toordinal()``. + + +.. method:: datetime.__eq__(other) + + Equivalent to ``dt1.toordinal() == dt2.toordinal()``. + + +.. method:: datetime.__ge__(other) + + Equivalent to ``dt1.toordinal() >= dt2.toordinal()``. + + +.. method:: datetime.__gt__(other) + + Equivalent to ``dt1.toordinal() > dt2.toordinal()``. + + +.. method:: datetime.utcoffset() + + If *tzinfo* is ``None``, returns ``None``, else returns a + :class:`timedelta` object with magnitude less than one day. + + +.. method:: datetime.replace(year=None, month=None, day=None, hour=None, minute=None, second=None, tzinfo=True) + + Return a :class:`datetime` with the same attributes, except for those + attributes given new values by whichever keyword arguments are specified. + Note that ``tzinfo=None`` can be specified to create a naive + :class:`datetime` from an aware :class:`datetime` with no conversion of + date and time data. + + +.. method:: datetime.astimezone(tz) + + Return a :class:`datetime` object with new *tzinfo* attribute + *tz*, adjusting the date and time data so the result is the same UTC time + as *self*, but in *tz*’s local time. *self* must be aware. + + If you merely want to attach a :class:`timezone` object *tz* to a + :class:`datetime` *dt* without adjustment of date and time data, use + ``dt.replace(tzinfo=tz)``. If you merely want to remove the :class:`timezone` + object from an aware :class:`datetime` *dt* without conversion of date and + time data, use ``dt.replace(tzinfo=None)``. + + +.. method:: datetime.isoformat(sep='T') + + Return a string representing the date and time in ISO 8601 format + ``YYYY-MM-DDTHH:MM:SS``. If :meth:`datetime.utcoffset` does not return + ``None``, a string is appended, giving the UTC offset: + ``YYYY-MM-DDTHH:MM:SS+HH:MM``. + + +.. method:: datetime.date() + + Return a :class:`datetime` instance whose date and time zone components + are equal to the input object and time is set to midnight. + + +.. method:: datetime.time() + + Return a :class:`timedelta` instance whose time components are equal to + the input object. + + +.. method:: datetime.toordinal() + + Return the proleptic Gregorian ordinal of the date. + + +.. method:: datetime.isoweekday() + + Return the day of the week as an integer, where Monday is 1 and Sunday + is 7. For example, ``date(2002, 12, 4).isoweekday() == 3``, a Wednesday. + + +.. method:: datetime.tuple() + + Return the tuple ``(year, month, day, hour, minute, second, tzinfo)``. + + +Examples of usage +^^^^^^^^^^^^^^^^^ + +Examples of working with :class:`datetime` objects:: + + from datetime import timedelta, timezone, datetime, fromisoformat + + print(datetime(2005, 7, 14, 12, 30)) # 2005-07-14 12:30:00 + dt = fromisoformat('2006-11-21 16:30+01:00') + print(dt.add(timedelta(hours=23))) # 2006-11-22 15:30:00+01:00 + tz1 = timezone(timedelta(hours=4, minutes=30)) + print(tz1) # UTC+04:30 + dt = datetime(1900, 11, 21, 3, 30, tzinfo=tz1) + print(dt) # 1900-11-21 03:30:00+04:30 + print(dt.astimezone(timezone.utc)) # 1900-11-20 23:00:00+00:00 diff --git a/docs/library/index.rst b/docs/library/index.rst index 2b1d6b9651..3134a7ff0a 100644 --- a/docs/library/index.rst +++ b/docs/library/index.rst @@ -57,6 +57,7 @@ library. builtins.rst cmath.rst collections.rst + datetime.rst errno.rst gc.rst hashlib.rst From 337a42bf3a32a7030d6d61f6366d24b8f4fe97ad Mon Sep 17 00:00:00 2001 From: Lorenzo Cappelletti Date: Tue, 26 Oct 2021 22:42:36 +0200 Subject: [PATCH 2/4] docs/library/datetime.rst: Add EPOCH. Signed-off-by: Lorenzo Cappelletti --- docs/library/datetime.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/library/datetime.rst b/docs/library/datetime.rst index a69acd05d3..41f5485ff4 100644 --- a/docs/library/datetime.rst +++ b/docs/library/datetime.rst @@ -547,6 +547,12 @@ Class attributes :attr:`datetime.MAXYEAR` is ``9999``. +.. attribute:: datetime.EPOCH + + :class:`datetime` object representing the time epoch of 2000-01-01 00:00:00 + UTC (same as :mod:`time`'s epoch). + + .. attribute:: datetime.timezone The object passed as the *tzinfo* argument to the :class:`datetime` From c2d152727e4c5a8abd74c956a979c0c6e154e771 Mon Sep 17 00:00:00 2001 From: Lorenzo Cappelletti Date: Tue, 2 Nov 2021 21:21:57 +0100 Subject: [PATCH 3/4] docs/library/datetime.rst: Nanosecond resolution. Signed-off-by: Lorenzo Cappelletti --- docs/library/datetime.rst | 50 +++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/library/datetime.rst b/docs/library/datetime.rst index 41f5485ff4..eae3f6d70a 100644 --- a/docs/library/datetime.rst +++ b/docs/library/datetime.rst @@ -48,9 +48,9 @@ dates or times. With respect to the Python module `datetime `_, this implementation is constrained as follows: - * Minimum resolution is *1 second*, instead of *1 microsecond*. - * Masimum delta spans over ±24855 days (±2\ :sup:`31` seconds or ±68 years) - instead of ±999999999 days. + * Minimum resolution is *1 nanosecond*, instead of *1 microsecond*. + * Masimum delta spans over ±106751 days (±2\ :sup:`63` nanoseconds or + ±292 years) instead of ±999999999 days. Class attributes @@ -59,40 +59,50 @@ Class attributes .. attribute:: timedelta.MINYEAR The year of :attr:`timedelta.min`, i.e. - ``timedelta.min.tuple()[1] // (365*24*60*60) == -68``. + ``timedelta.min.tuple()[1] // (365 * 24 * 60 * 60 * 10**9) == -292``. .. attribute:: timedelta.MAXYEAR The year of :attr:`timedelta.max`, i.e. - ``timedelta.max.tuple()[1] // (365*24*60*60) == 68``. + ``timedelta.max.tuple()[1] // (365 * 24 * 60 * 60 * 10**9) == 292``. .. attribute:: timedelta.min - The most negative :class:`timedelta` object, ``timedelta(-2**31)``. + The most negative :class:`timedelta` object, + ``timedelta(nanoseconds=-2**63)``. .. attribute:: timedelta.max - The most positive :class:`timedelta` object, ``timedelta(2**31 - 1)``. + The most positive :class:`timedelta` object, + ``timedelta(nanoseconds=2**63 - 1)``. .. attribute:: timedelta.resolution The smallest possible difference between non-equal :class:`timedelta` - objects, ``timedelta(seconds=1)``. + objects, ``timedelta(nanoseconds=1)``. + + +.. attribute:: timedelta.nanoseconds + + The internal time delta representation as 64-bit integer. Class methods ^^^^^^^^^^^^^ -.. class:: timedelta(hours=0, minutes=0, seconds=0, days=0, weeks=0) +.. class:: timedelta(hours=0, minutes=0, seconds=0, days=0, weeks=0, + milliseconds=0, microseconds=0, nanoseconds=0) All arguments are optional and default to ``0``. Arguments may be integers -or floats, and may be positive or negative. Only seconds are stored +or floats, and may be positive or negative. Only nanoseconds are stored internally. Arguments are converted to those units: + * A microsecond is converted to 1000 nanoseconds. + * A millisecond is converted to 10\ :sup:`6` nanoseconds. * A minute is converted to 60 seconds. * An hour is converted to 3600 seconds. * A week is converted to 7 days. @@ -103,7 +113,7 @@ exact (no information is lost). .. method:: timedelta.total_seconds() - Return the total number of seconds contained in the duration. + Return a float representing the total number of seconds contained in the duration. .. method:: timedelta.__add__(other) @@ -120,7 +130,7 @@ exact (no information is lost). .. method:: timedelta.__mul__(other) Return a delta multiplied by an integer or float. The result is rounded to - the nearest second using round-half-to-even. + the nearest nanosecond using round-half-to-even. .. method:: timedelta.__truediv__(other) @@ -204,11 +214,14 @@ exact (no information is lost). the sign of the delta and ``D`` its number of days. This is *not* ISO compliant, but provides a complete representation. + If the fractional part of :meth:`timedelta.total_seconds()` is not 0, + ``.ffffff`` is appended. + .. method:: timedelta.tuple(sign_pos='') - Return the tuple ``(sign, days, hours, minutes, seconds)``, where ``sign`` is - ``-`` if delta is negative, *sign_pos* otherwise. + Return the tuple ``(sign, days, hours, minutes, seconds, nanoseconds)``, + where ``sign`` is ``-`` if delta is negative, *sign_pos* otherwise. Examples of usage @@ -222,7 +235,7 @@ An example of normalization:: year = timedelta(days=365) another_year = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600) print(year == another_year) # True - print(year.total_seconds()) # 31536000 + print(year.total_seconds()) # 31536000.0 Examples of timedelta arithmetic:: @@ -486,7 +499,7 @@ subject to adjustment via a :class:`timezone` object. Constructors ^^^^^^^^^^^^ -.. class:: datetime(self, year, month, day, hour=0, minute=0, second=0, tzinfo=None) +.. class:: datetime(self, year, month, day, hour=0, minute=0, second=0, nanosecond=0, tzinfo=None) The *year*, *month* and *day* arguments are required. *tzinfo* may be ``None``, or an instance of a :class:`timezone` class. The remaining @@ -498,6 +511,7 @@ Constructors * ``0 <= hour < 24``, * ``0 <= minute < 60``, * ``0 <= second < 60``, + * ``0 <= nanosecond < 999_999_999``, If an argument outside those ranges is given, :exc:`ValueError` is raised. @@ -627,7 +641,7 @@ Class methods :class:`timedelta` object with magnitude less than one day. -.. method:: datetime.replace(year=None, month=None, day=None, hour=None, minute=None, second=None, tzinfo=True) +.. method:: datetime.replace(year=None, month=None, day=None, hour=None, minute=None, second=None, nanosecond=None, tzinfo=True) Return a :class:`datetime` with the same attributes, except for those attributes given new values by whichever keyword arguments are specified. @@ -682,7 +696,7 @@ Class methods .. method:: datetime.tuple() - Return the tuple ``(year, month, day, hour, minute, second, tzinfo)``. + Return the tuple ``(year, month, day, hour, minute, second, nanosecond, tzinfo)``. Examples of usage From 0b3bb2fda4537c5c05c636563fe02bf05f3e23ef Mon Sep 17 00:00:00 2001 From: Lorenzo Cappelletti Date: Wed, 6 Oct 2021 12:48:43 +0200 Subject: [PATCH 4/4] docs/library: Add new module `suntime`. Signed-off-by: Lorenzo Cappelletti --- docs/library/index.rst | 1 + docs/library/suntime.rst | 325 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 docs/library/suntime.rst diff --git a/docs/library/index.rst b/docs/library/index.rst index 3134a7ff0a..379337f7d5 100644 --- a/docs/library/index.rst +++ b/docs/library/index.rst @@ -72,6 +72,7 @@ library. socket.rst ssl.rst struct.rst + suntime.rst sys.rst time.rst uasyncio.rst diff --git a/docs/library/suntime.rst b/docs/library/suntime.rst new file mode 100644 index 0000000000..b593fe263e --- /dev/null +++ b/docs/library/suntime.rst @@ -0,0 +1,325 @@ +:mod:`suntime` -- sunrise and sunset time +========================================= + +.. module:: suntime + :synopsis: sunrise and sunset time + +This library provides an approximated calculation of sunrise and sunset time. +It contains two classes, :class:`Sundatetime` and :class:`Suntime`, which +differ from the input and output data types. + + +Class :class:`Sundatetime` +-------------------------- + +The following class makes use of module :mod:`datetime` for expressing input +date and output time. + +.. class:: Sundatetime(latitude, longitude, altitude=0) + + Arguments *latitude* and *longitude* are floats representing the + coordinates of a place on Earth. *altitude* is an integer number for + observer's elevation in meters. + +.. method:: Sundatetime.calc_sunrise_sunset(date) + + Calculate the sunrise and sunset for the given date. *date* must be an + *aware* `datetime.datetime` object in the range [2000-01-01; 2100-01-01). + Time information is ignored, whereas time zone ``tzinfo`` is used to + provide meaningful output. The results are cached in :data:`sunrise` and + :data:`sunset` . + +.. method:: Sundatetime.is_daytime(now) + Sundatetime.is_nighttime(now) + + Argument *now* is an *aware* `datetime.datetime` object representing a + point in time. A boolean value is returned whether Sun is above/below the + horizon or not, ``None`` when data are meaningless. + +.. method:: Sundatetime.is_sunrise(now) + Sundatetime.is_sunset(now) + + Argument *now* is an *aware* `datetime.datetime` object representing a + point in time. A boolean value is returned whether *now* matches + sunrise/sunset time or not, ``None`` when data are meaningless. + +The following instance variables are accessible: + +.. data:: Sundatetime.latitude + Sundatetime.longitude + + Float numbers for coordinates on Earth. + +.. data:: Sundatetime.altitude + + Elevation in meters for observations above the sea horizon. It corrects + for both apparent dip and terrestrial refraction. + +.. data:: Sundatetime.sunrise + Sundatetime.sunset + + They hold ``None`` when an instance is created, an *aware* + `datetime.datetime` after :meth:`calc_sunrise_sunset` is called. + +.. note:: + :data:`sunrise` may occur before 00:00 and :data:`sunset` after 23:59 + on calculated *date*. See :ref:`unexpected-results`. + + +Class :class:`Suntime` +---------------------- + +The following class makes use of plain integers for expressing input date and +output time. + +.. class:: Suntime(latitude, longitude, altitude=0, timezone=0) + + Arguments *latitude* and *longitude* are floats representing the + coordinates of a place on Earth. *altitude* is an integer number for + observer's elevation in meters. *timezone* is an integer holding the + timezone offset from UTC in minutes. The results are cached in + :data:`sunrise` and :data:`sunset`. + +.. method:: Suntime.calc_sunrise_sunset(year, month, day, dst=0) + + Calculate the sunrise and sunset for the given year, month and day. + *year* must be in the range [2000; 2100). *dst* is an integer holding the + offset in minute (usually 60) that accounts for Daylight Saving Time. + +.. method:: Suntime.is_daytime(now) + Suntime.is_nighttime(now) + + Argument *now* is an integer holding the number of minutes since midnight. + A boolean value is returned whether Sun is above/below the horizon or not, + ``None`` when data are meaningless. + +.. method:: Suntime.is_sunrise(now) + Suntime.is_sunset(now) + + Argument *now* is an an integer holding the number of minutes since midnight. + A boolean value is returned whether *now* matches sunrise/sunset time or not, + ``None`` when data are meaningless. + +The following instance variables are accessible: + +.. data:: Suntime.latitude + Suntime.longitude + + Float numbers for coordinates on Earth. + +.. data:: Suntime.altitude + + Elevation in meters for observations above the sea horizon. It corrects + for both apparent dip and terrestrial refraction. + +.. data:: Suntime.sunrise + Suntime.sunset + + It holds ``None`` when an instance is created, an integer for the + difference in minutes since 00:00 after :meth:`calc_sunrise_sunset` + is called. + +.. note:: + ``sunrise`` may be negative and ``sunset`` may be greater than 1439 + (24 hours). See :ref:`unexpected-results`. + +.. _unexpected-results: + +Unexpected results +------------------ + +:class:`Sundatetime` may return unexpected results: :data:`Sundatetime.sunrise` +may come before 00:00 and/or :data:`Sundatetime.sunset` may come after 23:59. +Similarly, :class:`Suntime` may return a negative :data:`Suntime.sunrise` +and/or a :data:`Suntime.sunset` greater or equal to 1440 (24 hours). + +Assuming ``date`` is the current date and ``now`` is the current time, +the conditions which may lead to unexpected results are: + +* *incorrect time zone*: *date*'s time zone is not consistent with provided + *longitude*. Suitable values for *timezone* and *dst* should be provided. + See example #3 (Novosibirsk) below. + +* *Sun is up all day*: close to the poles, Sun never sets in summer/winter + time. For this dates, ``is_daytime()`` holds true for the whole ``date``. + Note that the following holds true: ``sunrise ≤ now < sunset``. + +* *Sun is down all day*: close to the poles, Sun never raises in summer/winter + time. For this dates, ``is_nighttime()`` holds true for the whole ``date``. + Note that the following holds false: ``sunrise < sunset``. + +* *Sun sets after midnight*: close to the poles, Sun may sets after + 23:59. In this case, ``is_daytime()`` and ``is_nighttime()`` behave as + expected. The following condition is true: ``sunrise ≤ now ≤ 23:59 < sunset``. + + +Examples of usage +----------------- + +A typical use case is:: + + import datetime, suntime, time + + class Cet(datetime.timezone): + # See `datetime` documentation + + # initialization + CET = Cet() + Rome = suntime.Sundatetime(42.5966460, 12.4360233) + Rome.calc_sunrise_sunset(datetime.datetime(2000, 1, 1, tzinfo=CET)) + + # main loop (every minute or more) + now = datetime.datetime(*time.localtime()[:5], tzinfo=CET) + if (now.date() > Rome.sunset.date()): + Rome.calc_sunrise_sunset(now) + Rome.is_daytime(now) + +The following script shows sunrise and sunset time for several places and dates:: + + # place: latitude longitude + pl1 = ( 42.5966460, 12.4360233) # Rome + pl2 = ( 51.1627938,-122.9593616) # Vancouver + pl3 = (-33.9252192, 18.4240762) # CapeTown + pl4 = ( 55.1574890, 82.8547661) # Novosibirsk + pl5 = ( 78.6560170, 16.3447384) # Pyramiden + pl6 = pl5 + pl7 = (-77.7817838, 166.4561470) # McMurdo + pl8 = pl7 + + # date: YY MM DD sunrise sunset + dt1 = (2000, 1, 1) # 7:37 16:49 - https://www.timeanddate.com/sun/italy/rome?month=1&year=2000 + dt2 = (2014, 10, 3) # 7:15 18:46 - https://www.timeanddate.com/sun/canada/vancouver?month=10&year=2014 + dt3 = (2016, 12, 21) # 5:32 19:57 - https://www.timeanddate.com/sun/south-africa/cape-town?month=12&year=2016 + dt4 = (2021, 4, 24) # 6:04 20:50 - https://www.timeanddate.com/sun/russia/novosibirsk?month=4&year=2021 + dt5 = (2040, 8, 25) # up all day - https://www.timeanddate.com/sun/@2729216?month=8&year=2033 + dt6 = (2040, 8, 26) # 00:09 + # 1:45 23:41 - https://www.timeanddate.com/sun/@2729216?month=8&year=2040 + dt7 = (2033, 8, 10) # down all day - https://www.timeanddate.com/sun/antarctica/mcmurdo?month=8&year=2033 + dt8 = (2033, 10, 21) # 3:00 24:13 - https://www.timeanddate.com/sun/antarctica/mcmurdo?month=10&year=2033 + + # timezone offsets and DSTs (in hours) + tz1 = ( 1, 0) + tz2 = (-8, 1) + tz3 = ( 2, 0) + tz4 = ( 0, 0) # wrong; it generates negative hour because actual timezone is (7, 0) + tz5 = ( 1, 1) + tz6 = ( 1, 1) + tz7 = (13,-1) + tz8 = (13, 0) + +The following snippet of code makes use of class :class:`Sundatetime`:: + + from suntime import Sundatetime + from datetime import datetime, timedelta, timezone + + class Tz(timezone): + def __init__(self, hours, dst=0): + super().__init__(timedelta(hours=hours)) + self._dst = dst + + def dst(self, dt): + return timedelta(hours=self._dst) if self.isdst(dt) else timedelta(0) + + def isdst(self, dt): + return self._dst != 0 + + print('Rome:') + sd1 = Sundatetime(*pl1) + sd1.calc_sunrise_sunset(datetime(*dt1, tzinfo=Tz(*tz1))) + print('>', sd1.sunrise) # 2000-01-01 07:40:00+01:00 + print('>', sd1.sunset ) # 2000-01-01 16:47:00+01:00 + + print('Vancouver:') + sd2 = Sundatetime(*pl2) + sd2.calc_sunrise_sunset(datetime(*dt2, tzinfo=Tz(*tz2))) + print('>', sd2.sunrise) # 2014-10-03 07:16:00-08:00 + print('>', sd2.sunset ) # 2014-10-03 18:46:00-08:00 + + print('Cape Town:') + sd3 = Sundatetime(*pl3) + sd3.calc_sunrise_sunset(datetime(*dt3, tzinfo=Tz(*tz3))) + print('>', sd3.sunrise) # 2016-12-21 05:32:00+02:00 + print('>', sd3.sunset ) # 2016-12-21 19:57:00+02:00 + + print('Novosibirsk:') + sd4 = Sundatetime(*pl4) + sd4.calc_sunrise_sunset(datetime(*dt4, tzinfo=Tz(*tz4))) + print('>', sd4.sunrise) # 2021-04-23 23:04:00+00:00 + print('>', sd4.sunset ) # 2021-04-24 13:49:00+00:00 + + print('Pyramiden:') + sd5 = Sundatetime(*pl5) + sd5.calc_sunrise_sunset(datetime(*dt5, tzinfo=Tz(*tz5))) + print('>', sd5.sunrise) # 2040-08-24 12:57:00+02:00 + print('>', sd5.sunset ) # 2040-08-26 12:57:00+02:00 + + print('Pyramiden:') + sd6 = Sundatetime(*pl6) + sd6.calc_sunrise_sunset(datetime(*dt6, tzinfo=Tz(*tz6))) + print('>', sd6.sunrise) # 2040-08-26 01:35:00+02:00 + print('>', sd6.sunset ) # 2040-08-27 00:18:00+02:00 + + print('McMurdo:') + sd7 = Sundatetime(*pl7) + sd7.calc_sunrise_sunset(datetime(*dt7, tzinfo=Tz(*tz7))) + print('>', sd7.sunrise) # 2033-08-11 13:00:00+12:00 + print('>', sd7.sunset ) # 2033-08-09 13:00:00+12:00 + + print('McMurdo:') + sd8 = Sundatetime(*pl8) + sd8.calc_sunrise_sunset(datetime(*dt8, tzinfo=Tz(*tz8))) + print('>', sd8.sunrise) # 2033-10-21 03:06:00+13:00 + print('>', sd8.sunset ) # 2033-10-22 00:12:00+13:00 + +If :mod:`datetime` module is not available, the same input data can feed class +:class:`Suntime`:: + + from suntime import Suntime + + st1 = Suntime(*pl1, timezone=tz1[0]*60) + st1.calc_sunrise_sunset(*dt1, dst=tz1[1]*60) + print('Rome:') + print('>', divmod(st1.sunrise, 60)) # (7, 40) + print('>', divmod(st1.sunset , 60)) # (16, 47) + + st2 = Suntime(*pl2, timezone=tz2[0]*60) + st2.calc_sunrise_sunset(*dt2, dst=tz2[1]*60) + print('Vancouver:') + print('>', divmod(st2.sunrise, 60)) # (7, 16) + print('>', divmod(st2.sunset , 60)) # (18, 46) + + st3 = Suntime(*pl3, timezone=tz3[0]*60) + st3.calc_sunrise_sunset(*dt3, dst=tz3[1]*60) + print('Cape Town:') + print('>', divmod(st3.sunrise, 60)) # (5, 32) + print('>', divmod(st3.sunset , 60)) # (19, 57) + + st4 = Suntime(*pl4, timezone=tz4[0]*60) + st4.calc_sunrise_sunset(*dt4, dst=tz4[1]*60) + print('Novosibirsk:') + print('>', divmod(st4.sunrise, 60)) # (-1, 4) + print('>', divmod(st4.sunset , 60)) # (13, 49) + + st5 = Suntime(*pl5, timezone=tz5[0]*60) + st5.calc_sunrise_sunset(*dt5, dst=tz5[1]*60) + print('Pyramiden:') + print('>', divmod(st5.sunrise, 60)) # (-12, 57) + print('>', divmod(st5.sunset , 60)) # (36, 57) + + st6 = Suntime(*pl6, timezone=tz6[0]*60) + st6.calc_sunrise_sunset(*dt6, dst=tz6[1]*60) + print('Pyramiden:') + print('>', divmod(st6.sunrise, 60)) # (1, 35) + print('>', divmod(st6.sunset , 60)) # (24, 18) + + st7 = Suntime(*pl7, timezone=tz7[0]*60) + st7.calc_sunrise_sunset(*dt7, dst=tz7[1]*60) + print('McMurdo:') + print('>', divmod(st7.sunrise, 60)) # (37, 0) + print('>', divmod(st7.sunset , 60)) # (-11, 0) + + st8 = Suntime(*pl8, timezone=tz8[0]*60) + st8.calc_sunrise_sunset(*dt8, dst=tz8[1]*60) + print('McMurdo:') + print('>', divmod(st8.sunrise, 60)) # (3, 6) + print('>', divmod(st8.sunset , 60)) # (24, 12)