cardinal_pythonlib.interval


Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

This file is part of cardinal_pythonlib.

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


Time interval classes and related functions.

class cardinal_pythonlib.interval.Interval(start: datetime.datetime, end: datetime.datetime)[source]

Object representing a time interval, with start and end objects that are normally datetime.datetime objects (though with care, a subset of some methods are possible with datetime.date objects; caveat emptor, and some methods will crash).

Does not handle open-ended intervals (−∞, +∞) or null intervals.

There’s probably an existing class for this…

Creates the interval.

component_on_date(date: datetime.date) → Optional[cardinal_pythonlib.interval.Interval][source]

Returns the part of this interval that falls on the date given, or None if the interval doesn’t have any part during that date.

contains(time: datetime.datetime, inclusive: bool = True) → bool[source]

Does the interval contain a momentary time?

Parameters:
  • time – the datetime.datetime to check
  • inclusive – use inclusive rather than exclusive range checks?
contiguous(other: cardinal_pythonlib.interval.Interval) → bool[source]

Does this interval overlap or touch the other?

copy() → cardinal_pythonlib.interval.Interval[source]

Returns a copy of the interval.

cut(times: Union[datetime.datetime, List[datetime.datetime]]) → List[cardinal_pythonlib.interval.Interval][source]

Returns a list of intervals produced by using times (a list of datetime.datetime objects, or a single such object) as a set of knives to slice this interval.

day_night_duration(daybreak: datetime.time = datetime.time(7, 0), nightfall: datetime.time = datetime.time(19, 0)) → Tuple[datetime.timedelta, datetime.timedelta][source]

Returns a (day, night) tuple of datetime.timedelta objects giving the duration of this interval that falls into day and night respectively.

static dayspan(startdate: datetime.date, enddate: datetime.date, include_end: bool = True) → Optional[cardinal_pythonlib.interval.Interval][source]

Returns an Interval representing the date range given, from midnight at the start of the first day to midnight at the end of the last (i.e. at the start of the next day after the last), or if include_end is False, 24h before that.

If the parameters are invalid, returns None.

static daytime(date: datetime.date, daybreak: datetime.time = datetime.time(7, 0), nightfall: datetime.time = datetime.time(19, 0)) → cardinal_pythonlib.interval.Interval[source]

Returns an Interval representing daytime on the date given.

duration() → datetime.timedelta[source]

Returns a datetime.timedelta object representing the duration of this interval.

duration_in(units: str) → float[source]

Returns the duration of this interval in the specified units, as per convert_duration().

duration_outside_uk_normal_working_hours(starttime: datetime.time = datetime.time(7, 0), endtime: datetime.time = datetime.time(19, 0), weekdays_only: bool = False, weekends_only: bool = False) → datetime.timedelta[source]

Returns a duration (a datetime.timedelta object) representing the number of hours outside normal working hours.

This is not simply a subset of day_night_duration(), because weekends are treated differently (they are always out of hours).

The options allow the calculation of components on weekdays or weekends only.

intersection(other: cardinal_pythonlib.interval.Interval) → Optional[cardinal_pythonlib.interval.Interval][source]

Returns an Interval representing the intersection of this and the other, or None if they don’t overlap.

n_weekends() → int[source]

Returns the number of weekends that this interval covers. Includes partial weekends.

overlaps(other: cardinal_pythonlib.interval.Interval) → bool[source]

Does this interval overlap the other?

Overlap:

S--------S     S---S            S---S
  O---O          O---O        O---O

Simpler method of testing is for non-overlap!

S---S              S---S
    O---O      O---O
saturdays_of_weekends() → Set[datetime.date][source]

Returns the dates of all Saturdays that are part of weekends that this interval covers (each Saturday representing a unique identifier for that weekend). The Saturday itself isn’t necessarily the part of the weekend that the interval covers!

union(other: cardinal_pythonlib.interval.Interval) → cardinal_pythonlib.interval.Interval[source]

Returns an interval spanning the extent of this and the other.

static wholeday(date: datetime.date) → cardinal_pythonlib.interval.Interval[source]

Returns an Interval covering the date given (midnight at the start of that day to midnight at the start of the next day).

within(other: cardinal_pythonlib.interval.Interval, inclusive: bool = True) → bool[source]

Is this interval contained within the other?

Parameters:
  • other – the Interval to check
  • inclusive – use inclusive rather than exclusive range checks?
class cardinal_pythonlib.interval.IntervalList(intervals: List[cardinal_pythonlib.interval.Interval] = None, no_overlap: bool = True, no_contiguous: bool = True)[source]

Object representing a list of Intervals. Maintains an internally sorted state (by interval start time).

Creates the IntervalList.

Parameters:
  • intervals – optional list of Interval objects to incorporate into the IntervalList
  • no_overlap – merge intervals that overlap (now and on subsequent addition)?
  • no_contiguous – if no_overlap is set, merge intervals that are contiguous too?
add(interval: cardinal_pythonlib.interval.Interval) → None[source]

Adds an interval to the list. If self.no_overlap is True, as is the default, it will merge any overlapping intervals thus created.

any_contiguous() → bool[source]

Are any of the intervals contiguous?

any_overlap() → bool[source]

Do any of the intervals overlap?

copy(no_overlap: bool = None, no_contiguous: bool = None) → cardinal_pythonlib.interval.IntervalList[source]

Makes and returns a copy of the IntervalList. The no_overlap/no_contiguous parameters can be changed.

Parameters:
  • no_overlap – merge intervals that overlap (now and on subsequent addition)?
  • no_contiguous – if no_overlap is set, merge intervals that are contiguous too?
cumulative_before_during_after(start: datetime.datetime, when: datetime.datetime) → Tuple[datetime.timedelta, datetime.timedelta, datetime.timedelta][source]

For a given time, when, returns the cumulative time

  • after start but before self begins, prior to when;
  • after start and during intervals represented by self, prior to when;
  • after start and after at least one interval represented by self has finished, and not within any intervals represented by self, and prior to when.
Parameters:
  • start – the start time of interest (e.g. before self begins)
  • when – the time of interest
Returns:

before, during, after

Return type:

tuple

Illustration

start:      S
self:           X---X       X---X       X---X       X---X

when:                                           W

before:     ----
during:         -----       -----       -----
after:               -------     -------     ----
cumulative_gaps_to(when: datetime.datetime) → datetime.timedelta[source]

Return the cumulative time within our gaps, up to when.

cumulative_time_to(when: datetime.datetime) → datetime.timedelta[source]

Returns the cumulative time contained in our intervals up to the specified time point.

duration_outside_nwh(starttime: datetime.time = datetime.time(7, 0), endtime: datetime.time = datetime.time(19, 0)) → datetime.timedelta[source]

Returns the total duration outside normal working hours, i.e. evenings/nights, weekends (and Bank Holidays).

durations() → List[datetime.timedelta][source]

Returns a list of datetime.timedelta objects representing the durations of each interval in our list.

end_date() → Optional[datetime.date][source]

Returns the end date of the set of intervals, or None if empty.

end_datetime() → Optional[datetime.datetime][source]

Returns the end date of the set of intervals, or None if empty.

extent() → Optional[cardinal_pythonlib.interval.Interval][source]

Returns an Interval running from the earliest start of an interval in this list to the latest end. Returns None if we are empty.

first_interval_ending(end: datetime.datetime) → Optional[cardinal_pythonlib.interval.Interval][source]

Returns our first interval that ends with the end parameter, or None.

first_interval_starting(start: datetime.datetime) → Optional[cardinal_pythonlib.interval.Interval][source]

Returns our first interval that starts with the start parameter, or None.

gap_subset(interval: cardinal_pythonlib.interval.Interval, flexibility: int = 2) → cardinal_pythonlib.interval.IntervalList[source]

Returns an IntervalList that’s a subset of this one, only containing gaps between intervals that meet the interval criterion.

See subset() for the meaning of parameters.

gaps() → cardinal_pythonlib.interval.IntervalList[source]

Returns all the gaps between intervals, as an IntervalList.

get_overlaps() → cardinal_pythonlib.interval.IntervalList[source]

Returns an IntervalList containing intervals representing periods of overlap between intervals in this one.

is_empty() → bool[source]

Do we have no intervals?

list() → List[cardinal_pythonlib.interval.Interval][source]

Returns the contained list of Interval objects.

longest_duration() → Optional[datetime.timedelta][source]

Returns the duration of the longest interval, or None if none.

longest_interval() → Optional[cardinal_pythonlib.interval.Interval][source]

Returns the longest interval, or None if none.

max_consecutive_days() → Optional[Tuple[int, cardinal_pythonlib.interval.Interval]][source]

The length of the longest sequence of days in which all days include an interval.

Returns:(longest_length, longest_interval) where longest_interval is a Interval containing the start and end date of the longest span – or None if we contain no intervals.
Return type:tuple
n_weekends() → int[source]

Returns the number of weekends that the intervals collectively touch (where “touching a weekend” means “including time on a Saturday or a Sunday”).

remove_overlap(also_remove_contiguous: bool = False) → None[source]

Merges any overlapping intervals.

Parameters:also_remove_contiguous – treat contiguous (as well as overlapping) intervals as worthy of merging?
shortest_duration() → Optional[datetime.timedelta][source]

Returns the duration of the longest interval, or None if none.

shortest_gap() → Optional[cardinal_pythonlib.interval.Interval][source]

Find the shortest gap between intervals, or None if none.

shortest_gap_duration() → Optional[datetime.timedelta][source]

Find the duration of the shortest gap between intervals, or None if none.

shortest_interval() → Optional[cardinal_pythonlib.interval.Interval][source]

Returns the shortest interval, or None if none.

start_date() → Optional[datetime.date][source]

Returns the start date of the set of intervals, or None if empty.

start_datetime() → Optional[datetime.datetime][source]

Returns the start date of the set of intervals, or None if empty.

subset(interval: cardinal_pythonlib.interval.Interval, flexibility: int = 2) → cardinal_pythonlib.interval.IntervalList[source]

Returns an IntervalList that’s a subset of this one, only containing intervals that meet the “interval” parameter criterion. What “meet” means is defined by the flexibility parameter.

flexibility == 0: permits only wholly contained intervals:

interval:
            I----------------I

intervals in self that will/won't be returned:

    N---N  N---N   Y---Y   N---N   N---N
        N---N                N---N

flexibility == 1: permits overlapping intervals as well:

        I----------------I

N---N  Y---Y   Y---Y   Y---Y   N---N
    N---N                N---N

flexibility == 2: permits adjoining intervals as well:

        I----------------I

N---N  Y---Y   Y---Y   Y---Y   N---N
    Y---Y                Y---Y
sufficient_gaps(every_n_days: int, requiredgaps: List[datetime.timedelta], flexibility: int = 2) → Tuple[bool, Optional[cardinal_pythonlib.interval.Interval]][source]

Are gaps present sufficiently often? For example:

every_n_days=21
requiredgaps=[
    datetime.timedelta(hours=62),
    datetime.timedelta(hours=48),
]

… means “is there at least one 62-hour gap and one (separate) 48-hour gap in every possible 21-day sequence within the IntervalList?

  • If flexibility == 0: gaps must be WHOLLY WITHIN the interval.
  • If flexibility == 1: gaps may OVERLAP the edges of the interval.
  • If flexibility == 2: gaps may ABUT the edges of the interval.

Returns (True, None) or (False, first_failure_interval).

time_afterwards_preceding(when: datetime.datetime) → Optional[datetime.timedelta][source]

Returns the time after our last interval, but before when. If self is an empty list, returns None.

total_duration() → datetime.timedelta[source]

Returns a datetime.timedelta object with the total sum of durations. If there is overlap, time will be double-counted, so beware!

cardinal_pythonlib.interval.convert_duration(duration: datetime.timedelta, units: str) → Optional[float][source]

Convert a datetime.timedelta object – a duration – into other units. Possible units:

s, sec, seconds m, min, minutes h, hr, hours d, days w, weeks y, years
cardinal_pythonlib.interval.formatdt(date: datetime.date, include_time: bool = True) → str[source]

Formats a datetime.date to ISO-8601 basic format, to minute accuracy with no timezone (or, if include_time is False, omit the time).

cardinal_pythonlib.interval.is_normal_uk_working_day(date: datetime.date) → bool[source]

Is the specified date (a datetime.date object) a normal working day, i.e. not a weekend or a bank holiday?

cardinal_pythonlib.interval.is_saturday(date: datetime.date) → bool[source]

Is the specified date (a datetime.date object) a Saturday?

cardinal_pythonlib.interval.is_sunday(date: datetime.date) → bool[source]

Is the specified date (a datetime.date object) a Sunday?

cardinal_pythonlib.interval.is_uk_bank_holiday(date: datetime.date) → bool[source]

Is the specified date (a datetime.date object) a UK bank holiday?

Uses the BANK_HOLIDAYS list.

cardinal_pythonlib.interval.is_weekend(date: datetime.date) → bool[source]

Is the specified date (a datetime.date object) a weekend?