========================== Time, Calendar and periods ========================== This section is a dive into three of the time constructs available from Shyft: the time type, the calendar, and the time period. ̨ It is necessary to understand the underlying time representation, and how to manipulate it, to be able to both display and work with time series data correctly, and to set up and perfom computations. ----- **Contents:** .. contents:: :local: :depth: 2 ----- To start of we need to import the time series library of Shyft, we put it into an alias `sts`. .. code-block:: python import shyft.time_series as sts Shyft time ---------- Shyft considers time as UTC+0 internally, is is stored and used as a number in the Shyft core and it can represent time at the microsecond resolution. In the Python API time is exposed through the ``time`` class. Objects of this class is not that interesting, their main purpose is so the C++ implementation of Shyft can speak with the Python API correctly. In addition to this this class helps with parsing and stringifying UTC+0 time as well. When using the ``time`` class is that values or objects of this class can both represent a concrete date/time, or a delta or offset without any particular meaning as a point in time. It is simply a value able to be interpreted in multiple ways. We will mostly ignore the sub-second resolution of time. ----- The first way to initialize a time value is by passing it a number of seconds since the epoch. The epoch is the origin of the time system, Shyft uses the regular epoch: 1970-01-01T00:00:00. .. code-block:: python time_point = sts.time(1559563200) time_point # output: time(1559563200) We can stringify the time value we just made. This will give us a time string following ISO8601: .. code-block:: python str(time_point) # output: '2019-06-03T12:00:00Z' We can also pass the ISO8601 string back to the time class to construct a moment as specified in the string. **Note** that Shyft only parses a subset of ISO8601 so many perfectly valid time strings produces errors, and it does not even parse every time string it produces itself. .. code-block:: python # mostly iso8601 time strings. sts.time('2019-06-03T12:00:00.000000Z') # output: time(1559563200) An example of a ISO8601 that does not work is if we add the UTC-offset .. code-block:: python try: sts.time('2019-06-03T11:00:00.000000+01') except RuntimeError as e: print(e) # output: Needs format 'YYYY-MM-DDThh:mm:ssZ': got 2019-06-03T11:00:00.000000+01 Since time is a simple number we can work with it using simple arithmetics. We can add or subtract to it using ``+`` and ``-``, multiply and divide it using ``*`` and ``/``. When doing arithmetics with time we can use regular numbers, the result will be a ``time`` object if at least one ``time`` object is involved in the computation. Numbers will be interpreted as seconds, while any fractional parts of the numbers will be interpreted as sub-seconds down to microseconds. The next cell add one hour, 3600 seconds, to the time point we defined in the start of this section: .. code-block:: python str(time_point + 3600) # output: '2019-06-03T13:00:00Z' To get the current time we can use ``utctime_now()`` that we imported earlier. This function returns a time point representing the time at which the function was called: .. code-block:: python str(sts.utctime_now()) # output: '2023-08-22T08:10:47.037387Z' The three time constants we imported represent respectivly the least and largest time representable, and invalid time. The minimum and maximum time constructs are mostly usefull as default values, and for creating time periods spanning "all" of time. Invalid time may be returned by some functions to represent that whatever operation it were to do did not work, and some Shyft constructs initialize to it by default. .. code-block:: python sts.min_utctime # output: time.min sts.max_utctime # output: time.max sts.no_utctime # output: time.undefined ----- Calendars --------- Since Shyft represent time solely as UTC+0 we need some construct that is able to work with localized time. This is the purpose of the calendar construct. Initializing calendars ^^^^^^^^^^^^^^^^^^^^^^ To construct a Shyft calendar we need to provide the time zone specification it should use. If we do not specify any time zone it defaults to UTC+0. .. code-block:: python utc = sts.Calendar() utc_p5 = sts.Calendar('UTC+5') utc_m2 = sts.Calendar('UTC-2') oslo = sts.Calendar('Europe/Oslo') We can list all the available specification by calling the function `region_id_list` on the calendar: .. code-block:: python list(sts.Calendar.region_id_list()) Construct localized time ^^^^^^^^^^^^^^^^^^^^^^^^ Calendars can construct time points by using the ``time`` function on them. This function can not accept ISO8601 string, it instead accepts numbers specifying the time point. The only required argument is the year, but it can accept as many as seven values, in order: year, month, day, hour, minute, second, microsecond. As long as we specify the year, we can specify as few or many of the other, but we always have to specify all values in the order listed. .. code-block:: python utc.time(2019) # output: time(1546300800) .. code-block:: python utc.time(2019, 5, 31, 13, 52, 30) # output: time(1559310750) To demonstrate that calendars produce localized time, take a look at the two following cells. Notice that the same time on the same day is specified, but the results are not equal: .. code-block:: python utc.time(2019, 5, 29, 12) # output: time(1559131200) .. code-block:: python oslo.time(2019, 5, 29, 12) # output: time(1559124000) Stringifying local time ^^^^^^^^^^^^^^^^^^^^^^^ Calendars have a ``to_string`` function this function produces ISO8601 strings with offset like the calendar. **Note** that Shyft (at the time of writing) cannot parse these strings again, as it only parses ISO8601 string where both the hour and minute of the offset is specified, but it writes strings with only the hour. .. code-block:: python # stringify now using the local calendar oslo.to_string(sts.utctime_now()) # output: '2023-08-22T10:26:53.434103+02' Trim time ^^^^^^^^^ Often when given an arbitrary time point we can trim it to move it back to a time point with some specific meaning like the beggining of the hour or week containing the time point. Take a look at the end of the calendar section to see the list of time constants available for trimming. Trimming a time point is done using calendar semantics, so you can get different results by using different calendars. .. code-block:: python # get the current time, then trim it to the start of the current hour now = sts.utctime_now() now_hour = oslo.trim(now, sts.Calendar.HOUR) oslo.to_string(now_hour) # output: '2023-08-22T10:00:00+02' .. code-block:: python # get the current time, then trim it to the start of the current hour now = sts.utctime_now() now_week = oslo.trim(now, sts.Calendar.WEEK) oslo.to_string(now_week) # output: '2023-08-21T00:00:00+02' Add to time ^^^^^^^^^^^ If you want to add some offset to a time point that is not a specific length, like a month which could be 28, 29, 30, or 31 days, you should use the ``add`` function which handles these offsets easily. The predefined offsets available for add is listed at the end of the calendar section. The three next cells demonstrates the add function, first we retreive the current time and trim it to the start of the current month. The next two cells first add two months without calendar sematics and then with calendar semantics. Notice how you get diffenet results. .. code-block:: python # get the current time, then trim it to the start of the current hour now = sts.utctime_now() now_month = oslo.trim(now, sts.Calendar.MONTH) oslo.to_string(now_month) # output: '2023-08-01T00:00:00+02' .. code-block:: python # add two months without calendar semantics two_months = now_month + 2*sts.Calendar.MONTH oslo.to_string(two_months) # output: '2023-09-30T00:00:00+02' .. code-block:: python # add two months with calendar semantics two_months = oslo.add(now_month, sts.Calendar.MONTH, 2) oslo.to_string(two_months) # output: '2023-10-01T00:00:00+02' Diff moments ^^^^^^^^^^^^ Using a calendar we can also get the number of times we can subdivide a timespan. Some timespans work with calendar semantics, like a month, or a year which can be off differing length. The predefined time spans that have calendar semantics is listed at the end of the calendar section. When using the ``diff_units`` function we specify first the start and end time points, then the delta to subdivide with. The function does not return any remainder, and only return the whole number of times the delta subdivied the period. You need check for a remainder manually if required. .. code-block:: python # define start, end, and delta t0 = utc.time(2018, 5, 5) tN = utc.time(2018, 9, 17) dt = sts.Calendar.WEEK # get the number of times dt subdivides [t0, tN> n_weeks = utc.diff_units(t0, tN, dt) n_weeks # output: 19 Notice that the timespan we choose did not subdivide fully into weeks. If we add the number of weeks to the start point we do not get fully to the end point: .. code-block:: python # add the computed number of weeks to the start point utc.to_string(t0 + n_weeks*dt) # output: '2018-09-15T00:00:00Z' Time constants ^^^^^^^^^^^^^^ The predefined time constants in Shyft is listed below. These are the ones for wich calendar semantics work. - ``Calendar.SECOND``: One second - ``Calendar.MINUTE``: One minute, 60 seconds - ``Calendar.HOUR``: One hour, 3600 seconds - ``Calendar.WEEK``: Defines a semantic week - ``Calendar.DAY``: Defines a semantic day - ``Calendar.QUARTER``: Defines a semantic quarter, 3 months - ``Calendar.MONTH``: Defines a semantic month - ``Calendar.YEAR``: Defines a semantic year ----- Time periods ------------ Shyft has a time period construct to represent a pair of time points representing a timespan. The type exposed through the Shyft Python API is the ``UtcPeriod`` construct. The time period is represented as a half-open interval: The start value is included in the interval, while the end value is not included. The two values of the period are ``time`` values, and thus represented as UTC+0. Initialization ^^^^^^^^^^^^^^ A ``UtcPeriod`` is initialized from two ``time`` values or two numbers representing time: .. code-block:: python sts.UtcPeriod(utc.time(2018), utc.time(2019)) # output: [2018-01-01T00:00:00Z,2019-01-01T00:00:00Z> .. code-block:: python sts.UtcPeriod(1514764800, 1546300800) # output: [2018-01-01T00:00:00Z,2019-01-01T00:00:00Z> List of functions ^^^^^^^^^^^^^^^^^ ``UtcPeriod`` has several built-in functions and properties. * start * end * timespan * valid * to_string * trim * intersection * overlaps * contains * diff_units Start & end ^^^^^^^^^^^ ``start`` and ``end`` are properites which simply returns the start and end time points of the period. .. code-block:: python period = sts.UtcPeriod(utc.time(2018), utc.time(2019)) period.start, period.end # output: (time(1514764800), time(1546300800)) Timespan ^^^^^^^^ ``timespan`` returns the $\Delta t$ in seconds .. code-block:: python period.timespan() # output: time(31536000) Valid ^^^^^ `valid` returns a boolean if the period is valid or not. .. code-block:: python period.valid() # output: True invalid_period = sts.UtcPeriod(utc.time(2019), utc.time(2018)) invalid_period.valid() # output: False To string ^^^^^^^^^ ``to_string`` returns the period start and end in ISO8601 format with the half-open interval symbols as a string. .. code-block:: python period.to_string() # output: '[2018-01-01T00:00:00Z,2019-01-01T00:00:00Z>' Trimming periods ^^^^^^^^^^^^^^^^ As with time values, time periods can also be trimmed to multiples of specific time deltas. When you have a ``UtcPeriod`` object you call the ``trim`` function on it to do this. The time deltas are the same ones listed at the end of the calendar section. When trimming time periods we have two modes to choose between. We can trim outwards to a multiple of the delta, or inwards to a multiple of the delta. This is specified as the third argument to the function. If it is left unspecified the function defaults to trimming inwards. The first argument to the function is the calendar to use when trimming, the second argument is the delta to trim to, and the optional third argument is the trim policy. .. code-block:: python period = sts.UtcPeriod(utc.time(2017, 6), utc.time(2019, 6)) period.trim(utc, sts.Calendar.YEAR, sts.trim_policy.TRIM_IN) # output: [2018-01-01T00:00:00Z,2019-01-01T00:00:00Z> .. code-block:: python period = sts.UtcPeriod(utc.time(2017, 6), utc.time(2019, 6)) period.trim(utc, sts.Calendar.YEAR, sts.trim_policy.TRIM_OUT) # output: [2017-01-01T00:00:00Z,2020-01-01T00:00:00Z> Intersection & overlap ^^^^^^^^^^^^^^^^^^^^^^ The time period construct have the functions to determine overlap between periods, and compute the intersection between two periods. To demonstrate we define three periods, one for the first three month of the year, one for the middle three, and one for the last three: .. code-block:: python period_1 = sts.UtcPeriod(utc.time(2018, 1), utc.time(2018, 7)) period_2 = sts.UtcPeriod(utc.time(2018, 4), utc.time(2018, 10)) period_3 = sts.UtcPeriod(utc.time(2018, 7), utc.time(2019, 1)) To determine if they overlap we can call the `overlaps` function on one of the periods, and pass the other as argument: .. code-block:: python period_1.overlaps(period_2) # output: True .. code-block:: python period_1.overlaps(period_3) # output: False The intersection is not implemented as a function in the ``UtcPeriod`` structure, it is a free function. This is why we had to implement it separatly. To compute the intersection we pass the ``intersection`` function both the periods to compute intersection for. We get a special period out if the periods does not overlap. .. code-block:: python sts.intersection(period_1, period_2) # output: [2018-04-01T00:00:00Z,2018-07-01T00:00:00Z> sts.intersection(period_1, period_3) # output: [not-valid-period> Moment in period ^^^^^^^^^^^^^^^^ To check if a time point is inside a period we can call the ``contains`` function on the period with the time point as argument. .. code-block:: python period = sts.UtcPeriod(utc.time(2018), utc.time(2019)) The ``contains`` function accepts the time point, and returns a boolean signifying whether the point is within the period or not: .. code-block:: python moment = utc.time(2017, 6) period.contains(moment) # output: False .. code-block:: python moment = utc.time(2018, 6) period.contains(moment) # output: True Diff a period ^^^^^^^^^^^^^ To determine how many times a period can be subdivided by a delta, we do not need to unpack the points defining the period and use ``Calendar.diff_units``. The ``UtcPeriod`` construct have a ``diff_units`` function accepting a calendar and the delta. Otherwise this function works just like the ``Calendar.diff_units``. To demonstrate we define a period of some length: .. code-block:: python # define a period period = sts.UtcPeriod(utc.time(2018, 3, 15, 17), utc.time(2018, 10, 2, 4)) By using ``UtcPeriod.diff_units`` on the period with ``Calendar.DAY`` as the delta, we get the number of whole days in the period: .. code-block:: python # determine the number of whole days the period can be subdivided into period.diff_units(utc, sts.Calendar.DAY) # output: 200 Or we can determine the number of minutes the period can be subdivided into: .. code-block:: python # determine the number of whole minutes the period can be subdivided into period.diff_units(utc, sts.Calendar.MINUTE) # output: 288660