Interval class

class timeboard.Interval(timeboard, bounds, schedule=None)

A sequence of workshifts within the timeboard.

Interval is defined by two positions on the timeline which are the zero-based sequence numbers of the first and the last workshifts of the interval. An interval can contain one or more workshifts; the empty interval is not allowed.

Duty status of the workshifts within the interval is interpreted by the given schedule.

In addition to the methods defined for intervals, you can use interval as a generator that yields the workshifts of the interval, from the first to the last.

Parameters:
timeboard : Timeboard
bounds : tuple(int, int) or tuple(Workshift, Workshift)

The two elements of bounds provide the positions of the first and the last workshifts of the interval within the timeline. The element’s type is either non-negative integer or Workshift.

schedule : _Schedule, optional

If not given, the timeboard’s default schedule is used.

Raises:
VoidIntervalError

If bounds are in the reverse order.

OutOfBoundsError

If any of bounds points outside the timeboard.

See also

Timeboard.get_interval
provides convenient ways to instantiate an interval instead of calling Interval() constructor directly. Moreover, in many cases, you can shortcut a get_interval() call by calling the instance of Timeboard itself.
workshifts
Return the generator that yields workshifts with the specified duty.

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = tb.interval.Interval(clnd, (2,9))
>>> ivl
Interval((2, 9)): 'D' at 2017-10-02 -> 'D' at 2017-10-09 [8]
>>> len(ivl)
8
>>> for ws in ivl: 
...     print (ws.start_time, "     ", ws.label)
2017-10-02 00:00:00          0.0
2017-10-03 00:00:00          1.0
2017-10-04 00:00:00          0.0
2017-10-05 00:00:00          1.0
2017-10-06 00:00:00          0.0
2017-10-07 00:00:00          1.0
2017-10-08 00:00:00          0.0
2017-10-09 00:00:00          1.0    

The following operations consume memory to hold the data for the entire interval.

>>> list(ivl)
[Workshift(2) of 'D' at 2017-10-02,
 Workshift(3) of 'D' at 2017-10-03,
 Workshift(4) of 'D' at 2017-10-04,
 Workshift(5) of 'D' at 2017-10-05,
 Workshift(6) of 'D' at 2017-10-06,
 Workshift(7) of 'D' at 2017-10-07,
 Workshift(8) of 'D' at 2017-10-08,
 Workshift(9) of 'D' at 2017-10-09] 
>>> print(ivl)
Interval((2, 9)): 'D' at 2017-10-02 -> 'D' at 2017-10-09 [8]
<BLANKLINE>
        ws_ref      start  duration        end  label  on_duty
loc                                                           
2   2017-10-02 2017-10-02         1 2017-10-02    0.0    False
3   2017-10-03 2017-10-03         1 2017-10-03    1.0     True
4   2017-10-04 2017-10-04         1 2017-10-04    0.0    False
5   2017-10-05 2017-10-05         1 2017-10-05    1.0     True
6   2017-10-06 2017-10-06         1 2017-10-06    0.0    False
7   2017-10-07 2017-10-07         1 2017-10-07    1.0     True
8   2017-10-08 2017-10-08         1 2017-10-08    0.0    False
9   2017-10-09 2017-10-09         1 2017-10-09    1.0     True        
Attributes:
start_time : Timestamp

When the first workshift of the interval starts.

end_time : Timestamp

When the last workshift of the interval ends.

length : int

Number of workshifts in the interval. You can also call len() function for an interval which returns the same value.

schedule : _Schedule

Schedule used by interval’s methods unless explicitly redefined in the method call. Use name attribute of schedule to review its identity.

__div__(self, other)

x / y is the same as x.what_portion_of(y)

__mul__(self, other)

x * y is the same as x.overlap(y)

count(self, duty='on', schedule=None)

Return the count of interval’s workshifts with the specified duty.

Parameters:
duty : {'on', 'off', 'any'} , optional (default 'on')

Specify the duty of workshifts to be counted. If duty='on', off-duty workshifts are ignored, and vice versa. If duty='any', all workshifts are counted whatever the duty.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
int >=0

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = clnd(('02 Oct 2017', '08 Oct 2017'))
>>> ivl.count()
3
>>> ivl.count(duty='off')
4
>>> ivl.count(duty='any')
7
count_periods(self, freq, duty='on', schedule=None)

Return how many calendar periods fit into the interval.

The interval is sliced into calendar periods of the specified frequency and then each slice of the interval is compared to its corresponding period duty-wise: the workshift count in the interval’s slice is divided by the total count of workshifts in the period containing this slice; only workshifts with the specified duty are counted.

The quotients for each period are summed to produce the return value of the method.

If a period does not contain workshifts of the required duty, it contributes zero to the returned value.

Regardless of the period frequency, the method returns 0.0 if there are no workshifts with the specified duty in the interval.

Parameters:
freq : str

pandas-compatible frequency of calendar periods to be counted (i.e. 'M' for month). pandas-native business periods (i.e. ‘BM’), as well as periods with multipliers (i.e. ‘3M’), are not applicable.

duty : {'on', 'off', 'any'} , optional (default 'on')

Specify the duty of workshifts to be accounted for.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
float
Raises:
OutOfBoundsError

If the calendar period containing the first or the last workshift of the interval (subject to duty) extends outside the timeboard.

UnacceptablePeriodError

If freq is not valid for this method or is shorter than (some) workshifts in the interval.

Examples

>>> clnd = tb.Timeboard('H', '01 Oct 2017', '08 Oct 2017 23:59', 
...                     layout=[0,1])
>>> ivl = clnd(('01 Oct 2017 11:00', '02 Oct 2017 23:59'))

Interval ivl spans two days: it contains 13 of 24 workshifts of October 1 and all 24 workshifts of October 2.

>>> ivl.count_periods('D', duty='any')
1.5416666666666665
>>> 13/24 + 24/24
1.5416666666666665

The timeboard’s layout defines that all workshifts taking place on even hours are off duty, and those on odd hours are on duty. The first workshift of the interval (01 October 11:00 - 11:59) is on duty. Hence, the interval contains 7 of 12 on-duty workshifts of October 1, and all 12 on-duty workshifts of October 2.

>>> ivl.count_periods('D')
1.5833333333333335
>>> 7/12 + 12/12
1.5833333333333335

The interval contains 6 of 12 off-duty workshifts of October 1, and all 12 off-duty workshifts of October 2.

>>> ivl.count_periods('D', duty='off')
1.5
>>> 6/12 + 12/12
1.5

Note that we cannot count how many weeks are in this interval. The workshifts of the interval belong to the weeks of Sep 25 - Oct 1 and Oct 2 - Oct 8. The first of these extends beyond the timeboard. We may not guess what layout could be applied to the workshifts of Sep 25 - Sep 30 if the week were included in the timeboard entirely. We are not authorized to extrapolate the existing layout outside the timeboard. Moreover, for some complex layouts, any attempt at extrapolation would be ambiguous.

>>> ivl.count_periods('W')
OutOfBoundsError                     Traceback (most recent call last)
  ...
OutOfBoundsError: The 1st bound of interval or period referenced by 
`2017-09-25/2017-10-01` is outside Timeboard of 'H': 2017-10-01 00:00 
-> 2017-10-03 23:00
first(self, duty='on', schedule=None)

Return the first workshift with the specified duty in the interval.

Same as nth(0, duty, schedule)

See also

nth
Return n-th workshift with the specified duty in the interval.

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = clnd(('02 Oct 2017', '08 Oct 2017'))
>>> ivl.first()
Workshift(3) of 'D' at 2017-10-03
>>> ivl.first(duty='off')
Workshift(2) of 'D' at 2017-10-02
>>> ivl.first(duty='any')
Workshift(2) of 'D' at 2017-10-02
last(self, duty='on', schedule=None)

Return the last workshift with the specified duty in the interval.

Same as nth(-1, duty, schedule)

See also

nth
Return n-th workshift with the specified duty in the interval.

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = clnd(('02 Oct 2017', '08 Oct 2017'))
>>> ivl.last()
Workshift(7) of 'D' at 2017-10-07
>>> ivl.last(duty='off')
Workshift(8) of 'D' at 2017-10-08
>>> ivl.last(duty='any')
Workshift(8) of 'D' at 2017-10-08
nth(self, n, duty='on', schedule=None)

Return n-th workshift with the specified duty in the interval.

Parameters:
n : int

Zero-based sequence number of the workshift with the specified duty within the interval. Negative values count from the end toward the beginning of the interval (n=-1 returns the last workshift with the specified duty).

duty : {'on', 'off’, 'any’} , optional (default 'on')

Duty of workshifts to be counted. If duty='on', off-duty workshifts are ignored, and vice versa. If duty='any', all workshifts are counted whatever the duty.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
Workshift
Raises:
OutOfBoundsError

If the requested workshift does not exist within the interval.

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = clnd(('02 Oct 2017', '08 Oct 2017'))
>>> ivl.nth(1)
Workshift(5) of 'D' at 2017-10-05
>>> ivl.nth(1, duty='off')
Workshift(4) of 'D' at 2017-10-04
>>> ivl.nth(1, duty='any')
Workshift(3) of 'D' at 2017-10-03
>>> ivl.nth(10)
-----------------------------------------------------------------------
OutOfBoundsError                      Traceback (most recent call last)
  ...
OutOfBoundsError: Interval((2, 8)): 'D' at 2017-10-02 -> 'D' at 
2017-10-08 [7] contains not enough on-duty workshifts for n=10  
overlap(self, other, schedule=None)

Create the interval that is the intersection of two intervals.

Parameters:
other : Interval
schedule : _Schedule, optional

The schedule to be attached to the created interval. If schedule is not given, self’s schedule is used. The schedule attached to other is ignored in any case.

Returns:
Interval

The intersection of self and other intervals.

Notes

If the intervals do not overlap, a special object representing a void interval is returned. You can call the same methods on a void interval as on a normal interval. Note that start_time and end_time properties of a void interval return the null value (pandas.NaT).

You can also use * (multiplication) operator with two intervals to obtain their intersection. The resulting interval will get the schedule of the first operand.

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = tb.interval.Interval(clnd, (2,9))
>>> other = tb.interval.Interval(clnd, (8,11))
>>> ivl.overlap(other)
Interval((8, 9)): 'D' at 2017-10-08 -> 'D' at 2017-10-09 [2]

The use of * operator:

>>> ivl * other
Interval((8, 9)): 'D' at 2017-10-08 -> 'D' at 2017-10-09 [2]

The case of no overlap:

>>> other = tb.interval.Interval(clnd, (10,11))
>>> void_ivl = ivl.overlap(other)
>>> void_ivl
Interval ((10, 9)): void [0]
>>> len(void_ivl)
0
>>> void_ivl.count()
0
>>> list(void_ivl)
[]
>>> void_ivl.start_time
NaT
>>> void_ivl.first(duty='any')
-----------------------------------------------------------------------
OutOfBoundsError                      Traceback (most recent call last)
  ...
OutOfBoundsError: No workshifts in void interval Interval ((10, 9)): 
void [0]
to_dataframe(self)

Convert interval into pandas.Dataframe.

Each workshift is represented as a row. The dataframe has the following columns:

Column Explanation
‘loc’ zero-based position of the workshift on the timeline
‘workshift’ the reference time of the workshift
‘start’ the start time of the workshift
‘end’ the start time of the workshift
‘duration’ the number of base units in the workshift
‘label’ workshift’s label
Returns:
pandas.DataFrame
total_duration(self, duty='on', schedule=None)

Return the total duration of workshifts with the specified duty.

Parameters:
duty : {'on', 'off', 'any'} , optional (default 'on')

Specify the duty of workshifts whose durations are counted. If duty='on', off-duty workshifts are ignored, and vice versa. If duty='any', durations of all workshifts are counted whatever the duty.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
int

Total count of base units in the workshifts with the specified duty.

Examples

>>> org = tb.Organizer(marks = ['03 Oct 2017', '07 Oct 2017', 
...                             '09 Oct 2017'], 
...                    structure=[0,1]) 
>>> clnd = tb.Timeboard('D', '30 Sep 2017', '11 Oct 2017', 
...                     layout=org)
>>> ivl = clnd()
>>> print (ivl)
Interval((0, 3)): 3x'D' at 2017-09-30 -> 3x'D' at 2017-10-09 [4]
<BLANKLINE>
        ws_ref      start  duration        end  label  on_duty
loc                                                           
0   2017-09-30 2017-09-30         3 2017-10-02    0.0    False
1   2017-10-03 2017-10-03         4 2017-10-06    1.0     True
2   2017-10-07 2017-10-07         2 2017-10-08    0.0    False
3   2017-10-09 2017-10-09         3 2017-10-11    1.0     True
>>> ivl.total_duration()
7
>>> ivl.total_duration(duty='off')
5
>>> ivl.total_duration(duty='any')
12
what_portion_of(self, other, duty='on', schedule=None)

What portion of the other interval this interval takes up.

The method returns the ratio of the workshift count in the intersection of the two intervals to the workshift count in the other interval. Only workshifts with the specified duty are counted.

If the two intervals do not overlap or their intersection contains no workshifts with the specified duty, zero is returned.

Parameters:
other : Interval
duty : {'on', 'off', 'any'} , optional (default 'on')

Specify the duty of workshifts to be accounted for.

schedule : _Schedule, optional

If schedule is not given, self’s schedule is used. The schedule attached to other is ignored in any case.

Returns:
float

Notes

You can also use / (division) operator. x / y is the same as x.what_portion_of(y)

Examples

>>> clnd = tb.Timeboard('D', '02 Oct 2017', '15 Oct 2017',
...                     layout=[1, 1, 1, 1, 1, 0, 0])
>>> week1 = clnd('02 Oct 2017', period='W')

week1 contains five working days and two days off.

>>> ivl = clnd(('05 Oct 2017', '07 Oct 2017'))

ivl takes up days Thursday through Saturday of week1 (two working days and one day off).

>>> ivl.what_portion_of(week1)
0.4
>>> 2 / 5 # working days
0.4
>>> ivl.what_portion_of(week1, duty='off')
0.5
>>> 1 / 2 # days off
0.5
>>> ivl.what_portion_of(week1, duty='any')
0.42857142857142855
>>> 3 / 7 # all days
0.42857142857142855

Using / (division) operator calls what_portion_of() with the default parameter values (so, the duty is ‘on’):

>>> ivl / week1
0.4

ivl and week2 do not overlap:

>>> week2 = clnd('09 Oct 2017', period='W')
>>> ivl.what_portion_of(week2, duty='any')
0.0

decade includes the entire week1 and extends beyond.

>>> decade = clnd(('02 Oct 2017', '11 Oct 2017'))
>>> decade.what_portion_of(week1)
1.0
>>> weekend = clnd(('07 Oct 2017', '08 Oct 2017'))

All days of weekend are also the days of week1 but they are not working days, so:

>>> weekend.what_portion_of(week1)
0.0

However, weekend contains all off-duty days of week1:

>>> weekend.what_portion_of(week1, duty='off')
1.0
workshifts(self, duty='on', schedule=None)

Return the generator that yields workshifts with the specified duty.

The workshifts are yielded in order from the first to the last.

Parameters:
duty : {'on', 'off’, 'any’} , optional (default 'on')

Duty of the workshifts to be yielded. If duty='on', off-duty workshifts are skipped, and vice versa. If duty='any', every workshift in the interval is yielded.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
generator

Examples

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '15 Oct 2017', layout=[0,1])
>>> ivl = tb.interval.Interval(clnd, (2,9))
>>> ivl
Interval((2, 9)): 'D' at 2017-10-02 -> 'D' at 2017-10-09 [8]
>>> for ws in ivl.workshifts(): 
...     print (ws.start_time, " ", ws.label)    
2017-10-03 00:00:00      1
2017-10-05 00:00:00      1
2017-10-07 00:00:00      1
2017-10-09 00:00:00      1
>>> list(ivl.workshifts(duty='off'))
[Workshift(2) of 'D' at 2017-10-02,
 Workshift(4) of 'D' at 2017-10-04,
 Workshift(6) of 'D' at 2017-10-06,
 Workshift(8) of 'D' at 2017-10-08]
worktime(self, duty='on', schedule=None)

Return the total work time of workshifts with the specified duty.

The source for work time values is determined by Timeboard.worktime_source.

Parameters:
duty : {'on', 'off', 'any'} , optional (default 'on')

Specify the duty of workshifts whose work times are counted. If duty='on', off-duty workshifts are ignored, and vice versa. If duty='any', work times of all workshifts are counted whatever the duty.

schedule : _Schedule, optional

If schedule is not given, the interval’s schedule is used.

Returns:
int or float

The sum of work times of the workshifts with the specified duty or zero if the interval does not contain workshifts with this duty.

Raises:
TypeError

If `worktime_source`=’labels’ but the labels could be summed up to a number.

Examples

By default, workshift’s work time equals to workshift’s duration:

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '11 Oct 2017', 
...                     layout=[4, 8, 4, 8],
...                     default_selector = lambda label: label>4)
>>> ivl = tb.Interval(clnd, (1, 3))
>>> print (ivl)
Interval((1, 3)): 'D' at 2017-10-01 -> 'D' at 2017-10-03 [3]
<BLANKLINE>
        ws_ref      start  duration        end  label  on_duty
loc                                                           
1   2017-10-01 2017-10-01         1 2017-10-01    8.0     True
2   2017-10-02 2017-10-02         1 2017-10-02    4.0    False
3   2017-10-03 2017-10-03         1 2017-10-03    8.0     True
>>> ivl.worktime()
2
>>> ivl.worktime(duty='off')
1
>>> ivl.worktime(duty='any')
3

In the example below, the work time is taken from the labels:

>>> clnd = tb.Timeboard('D', '30 Sep 2017', '11 Oct 2017', 
...                     layout=[4, 8, 4, 8],
...                     default_selector = lambda label: label>4,
...                     worktime_source = 'labels')
>>> ivl = tb.Interval(clnd, (1, 3))
>>> ivl.worktime()
16.0
>>> ivl.worktime(duty='off')
4.0
>>> ivl.worktime(duty='any')
20.0