Ranges
A utility for working with ranges
The basic building block of the module is the Range
class.
Usage
A few examples:
from xocto.ranges import Range, RangeBoundaries
> > > Range(0, 2, boundaries=RangeBoundaries.EXCLUSIVE_INCLUSIVE)
> > > <Range: (0,2]>
> > > 0 in Range(0, 2)
> > > True
> > > 2 in Range(0, 2)
> > > False
> > > date(2020, 1, 1) in Range(date(2020, 1, 2), date(2020, 1, 5))
> > > False
> > > sorted([Range(1, 4), Range(0, 5)])
> > > [<Range: [0,5)>, <Range: [1,4)>]
See xocto.ranges for more details, including examples and in depth technical details.
API Reference
- class xocto.ranges.FiniteDateRange(start: date, end: date)[source]
Bases:
FiniteRange
[date
]This subclass is a helper for a common usecase for ranges - representing finite intervals of whole days.
- boundaries: RangeBoundaries
- property days: int
Return the number of days between the start and end of the range.
- end: T
- intersection(other: Range[date]) FiniteDateRange | None [source]
Intersections with finite ranges will always be finite.
- start: T
- union(other: Range[date]) FiniteDateRange | None [source]
Unions between two FiniteDateRanges should produce a FiniteDateRange.
- class xocto.ranges.FiniteDatetimeRange(start: datetime, end: datetime)[source]
Bases:
FiniteRange
[datetime
]This subclass is a helper for a common usecase for ranges - representing finite intervals of time.
- boundaries: RangeBoundaries
- property days: int
Return the number of days between the start and end of the range.
- end: T
- intersection(other: Range[datetime]) FiniteDatetimeRange | None [source]
Intersections with finite ranges will always be finite.
- property seconds: int
Return the number of seconds between the start and end of the range.
- start: T
- class xocto.ranges.FiniteRange(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]
Bases:
Range
[T
]A FiniteRange represents a range that MUST have finite endpoints (i.e. they cannot be None).
This is mostly to add type checking, as we can specify that functions require a finite range and then skip checking if the endpoints are None.
- boundaries: RangeBoundaries
- end: T
- intersection(other: Range[T]) FiniteRange[T] | None [source]
Intersections with finite ranges will always be finite.
- start: T
- class xocto.ranges.FiniteRangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]
Bases:
RangeSet
[T
]This subclass is useful when dealing with finite intervals as we can offer stronger guarantees than we can get with normal RangeSets - mostly around intersections being finite etc.
- intersection(other: Range[T] | RangeSet[T]) FiniteRangeSet[T] [source]
Return the intersection of this RangeSet and the other Range or RangeSet.
- pop() FiniteRange[T] [source]
Remove and return an arbitrary set element.
Raises KeyError if the set is empty.
- class xocto.ranges.HalfFiniteRange(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]
Bases:
Range
[T
]This is also for type-checking, but represents a very common range type in Kraken (possibly the most common).
Specifically, ranges that MUST have a finite inclusive left endpoint and a (possibly infinite) exclusive right endpoint.
However LeftFiniteInclusiveRightExclusiveRange is a bit of a mouthful so we’re going with HalfFiniteRange.
- boundaries: RangeBoundaries
- end
- intersection(other: Range[T]) HalfFiniteRange[T] | None [source]
Intersections with half finite ranges will always be half finite.
- start: T
- class xocto.ranges.HalfFiniteRangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]
Bases:
RangeSet
[T
],Generic
[T
]This subclass is useful when dealing with half-finite intervals as we can offer stronger guarantees than we can get with normal RangeSets - mostly around intersections being half-finite etc.
- intersection(other: Range[T] | RangeSet[T]) HalfFiniteRangeSet[T] [source]
Return the intersection of this RangeSet and the other Range or RangeSet.
- pop() HalfFiniteRange[T] [source]
Remove and return an arbitrary set element.
Raises KeyError if the set is empty.
- class xocto.ranges.Range(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]
Bases:
Generic
[T
]The basic concept of an range of some sort of comparable item, specified by its endpoints and boundaries (whether the endpoints are inclusive or exclusive). This class provides some useful helpers for working with ranges.
Usage:
Ranges are declared by specifying their endpoints and boundaries. If the boundaries is omitted, then they are inclusive-exclusive by default
>>> Range(0, 2, boundaries=RangeBoundaries.EXCLUSIVE_INCLUSIVE) <Range: (0,2]> >>> Range(0, 2, boundaries="[]") <Range: [0,2]> >>> r = Range(0, 2) >>> print(f"{r}") "[0,2)"
If an endpoint is set as None, then that means that the range is effectively infinite. Infinite ranges must have exclusive bounds for the infinite ends. We provide the continuum to the continnum helper to get an unbounded range.
>>> Range(0, None) <Range: [0, None) >>> Range(0, None, boundaries="[]") # Invalid >>> Range.continuum() # Helper function <Range: (None, None)>
Ranges can be declared for any comparable type
>>> int_erval: Range[int] = Range(0, 2) >>> date_erval: Range[date] = Range(date(2020, 1, 1), date(2020, 6, 6)) >>> string_erval: Range[str] = Range("ardvark", "zebra") # Uses lexical ordering
Ranges are themselves comparable. Two ranges are ordered by their start, with their end used to break ties
>>> sorted([Range(1, 4), Range(0, 5)]) [<Range: [0,5)>, <Range: [1,4)>] >>> sorted([Range(1, 2), Range(None, 2)]) [<Range: [None,2)>, <Range: [1,2)>] >>> sorted([Range(3, 5), Range(3, 4)]) [<Range: [3,4)>, <Range: [4,5)>] >>> sorted([Range(0, 2, boundaries=b) for b in RangeBoundaries]) [<Range: [0,2)>, <Range: [0,2]>, <Range: (0,2)>, <Range: (0,2]>]
The in operator is provided to check if an item of the range type is within the the range
>>> 0 in Range(0, 2) True >>> 2 in Range(0, 2) False >>> date(2020, 1, 1) in Range(date(2020, 1, 2), date(2020, 1, 5)) False
The intersection function (which is aliased to the and (&) operator) will return the overlap of two ranges, or None if they are disjoint
>>> Range(0, 2).intersection(Range(1, 4)) <Range: [1,2)> >>> Range(1, 2) & Range(3, 4) None
- The is_disjoint function will tell you if two ranges are disjoint
>>> Range(0, 2).is_disjoint(Range(3, 5)) True >>> Range(0, 2).is_disjoint(Range(2, 5)) # Since the default is not right-inclusive True
The union function (which is aliased to the or (|) operator), will return a range which covers two overlapping (or touching) ranges, or None if they are disjoint.
>>> Range(0, 2).union(Range(1, 3)) <Range: [0,3)> >>> Range(0, 2) | Range(2, 4) <Range: [0,4)> >>> Range(0, 2) | Range(3, 4) None >>> Range(0, 2) | Range(2, 4, boundaries="(]") None >>> Range(0, 2, boundaries="[]") | Range(3, 4) # Since Range doesn't understand that 2 and 3 are adjacent. None
The difference function (which is aliased to the subtraction (-) operator), will return a range which contains the bit of this range which is not covered by the other range, or a rangeset which contains the bits of this range which are not covered (or None if the other range covers this one).
>>> Range(0, 4) - Range(2, 4) <Range: [0,2)> >>> Range(0, 4) - Range(2, 3) <RangeSet: {[0,2), [3,4)}> >>> Range(0, 4) - Range(0, 5) None
- boundaries: RangeBoundaries
- difference(other: Range[T]) Range[T] | RangeSet[T] | None [source]
Return a range or rangeset consisting of the bits of this range that do not intersect the other range (or None if this range is covered by the other range).
- end
- intersection(other: Range[T]) Range[T] | None [source]
Return the intersection of the two ranges if it exists, or None if they are disjoint.
- start
- class xocto.ranges.RangeBoundaries(value)[source]
Bases:
Enum
An enumeration.
- EXCLUSIVE_EXCLUSIVE = '()'
- EXCLUSIVE_INCLUSIVE = '(]'
- INCLUSIVE_EXCLUSIVE = '[)'
- INCLUSIVE_INCLUSIVE = '[]'
- classmethod from_bounds(left_exclusive: bool, right_exclusive: bool) RangeBoundaries [source]
Convenience method to get the relevant boundary type by specifiying the exclusivity of each end.
- class xocto.ranges.RangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]
Bases:
Generic
[T
]A RangeSet represents an ordered set of disjoint ranges. It is constructed from an iterable of Ranges (which can be omited to create an empty RangeSet):
>>> RangeSet() # empty RangeSet <RangeSet: {}> >>> rs = RangeSet([Range(0, 1), Range(2, 4)]) # Single iterable of ranges >>> print(f"{rs}") "{[0,1), [2, 4)}"
Overlapping Ranges are condensed when they are added to a set:
>>> RangeSet([Range(0, 3), Range(2, 4)]) == RangeSet([Range(0, 4)]) True
- complement() RangeSet[T] [source]
Get a rangeset representing the ranges between the ranges in this rangeset and infinite left and right bounds.
- contains_item(item: T) bool [source]
Check if an item is contained by any Range within this RangeSet.
- contains_range(range: Range[T]) bool [source]
Check a Range is fully contained within another Range within this RanegSet.
- difference(other: RangeSet[T]) RangeSet[T] [source]
Return a rangeset consisting of the bits of this rangeset that do not intersect the other rangeset.
- discard(item: Range[T]) None [source]
Discarding a range from a range set is equivalent to “cutting away” all the intersections of that range with the rangeset.
- intersection(other: Range[T] | RangeSet[T]) RangeSet[T] [source]
Return the intersection of this RangeSet and the other Range or RangeSet.
- is_disjoint(other: RangeSet[T]) bool [source]
Check whether this RangeSet is disjoint from the other one.
- xocto.ranges.any_overlapping(ranges: Iterable[Range[T]]) bool [source]
Return true if any of the passed Ranges are overlapping.
- xocto.ranges.as_finite_datetime_periods(periods: Iterable[HalfFiniteRange[datetime] | Range[datetime]]) Sequence[FiniteDatetimeRange] [source]
Casts the given date/time periods as finite periods.
This is useful when working with potentially infinite ranges that are known to be finite e.g. due to intersection with a finite range.
- Raises:
ValueError: If one or more periods is not finite.
- xocto.ranges.get_finite_datetime_ranges_from_timestamps(finite_datetime_range: FiniteRange[datetime], timestamps: Iterable[datetime]) Sequence[FiniteDatetimeRange] [source]
Given a datetime range and some timestamps, cut that period into multiple points whenever one of the timestamps falls within it.
Sorts and deduplicates the timestamps first.
Example:
- Input:
period: (“2021-09-01 00:00:00”, “2021-10-01 00:00:00”)
timestamps: [“2021-09-10 00:00:00”, “2021-09-16 00:00:00”, “2021-09-23 00:00:00”]
- Return:
- [
(“2021-09-01 00:00:00”, “2021-09-10 00:00:00”), (“2021-09-10 00:00:00”, “2021-09-16 00:00:00”), (“2021-09-16 00:00:00”, “2021-09-23 00:00:00”), (“2021-09-23 00:00:00”, “2021-10-01 00:00:00”),
]