Interval arithmetic for Python
Project description
Interval arithmetic for Python
This library provides interval arithmetic for Python 2.7+ and Python 3.4+.
Features
- Support intervals of any (comparable) objects.
- Closed or open, finite or (semi-)infinite intervals.
- Atomic intervals and interval sets are supported.
- Automatic simplification of intervals.
- Support iteration, comparison, transformation, intersection, union, complement, difference and containment.
- Import and export intervals to strings and to Python built-in data types.
Installation
You can use pip
to install it, as usual: pip install python-intervals
.
This will install the latest available version from PyPI. Prereleases are available from the master branch on GitHub.
For convenience, the library is contained within a single Python file, and can thus be easily integrated in other projects without the need for an explicit dependency (hint: don't do that!).
Documentation & usage
Interval creation
Assuming this library is imported using import intervals as I
, intervals can be easily created using one of the
following helpers:
>>> I.open(1, 2)
(1,2)
>>> I.closed(1, 2)
[1,2]
>>> I.openclosed(1, 2)
(1,2]
>>> I.closedopen(1, 2)
[1,2)
>>> I.singleton(1)
[1]
>>> I.empty()
()
Intervals created with this library are Interval
instances.
An Interval
object is a disjunction of atomic intervals that represent single intervals (e.g. [1,2]
) corresponding to AtomicInterval
instances.
Except when atomic intervals are explicitly created or retrieved, only Interval
instances are exposed.
The bounds of an interval can be any arbitrary values, as long as they are comparable:
>>> I.closed(1.2, 2.4)
[1.2,2.4]
>>> I.closed('a', 'z')
['a','z']
>>> import datetime
>>> I.closed(datetime.date(2011, 3, 15), datetime.date(2013, 10, 10))
[datetime.date(2011, 3, 15),datetime.date(2013, 10, 10)]
Infinite and semi-infinite intervals are supported using I.inf
and -I.inf
as upper or lower bounds.
These two objects support comparison with any other object.
When infinities are used as a lower or upper bound, the corresponding boundary is automatically converted to an open one.
>>> I.inf > 'a', I.inf > 0, I.inf > True
(True, True, True)
>>> I.openclosed(-I.inf, 0)
(-inf,0]
>>> I.closed(-I.inf, I.inf) # Automatically converted to an open interval
(-inf,+inf)
Empty intervals always resolve to (I.inf, -I.inf)
, regardless of the provided bounds:
>>> I.empty() == I.open(I.inf, -I.inf)
True
>>> I.closed(4, 3) == I.open(I.inf, -I.inf)
True
>>> I.openclosed('a', 'a') == I.open(I.inf, -I.inf)
True
For convenience, intervals are automatically simplified:
>>> I.closed(0, 2) | I.closed(2, 4)
[0,4]
>>> I.closed(1, 2) | I.closed(3, 4) | I.closed(2, 3)
[1,4]
>>> I.empty() | I.closed(0, 1)
[0,1]
>>> I.closed(1, 2) | I.closed(2, 3) | I.closed(4, 5)
[1,3] | [4,5]
Note that discrete intervals are not supported, e.g., combining [0,1]
with [2,3]
will not result
in [0,3]
even if there is no integer between 1
and 2
.
Arithmetic operations
Both Interval
and AtomicInterval
support following interval operations:
-
x.is_empty()
tests if the interval is empty.>>> I.closed(0, 1).is_empty() False >>> I.closed(0, 0).is_empty() False >>> I.openclosed(0, 0).is_empty() True >>> I.empty().is_empty() True
-
x.intersection(other)
orx & other
return the intersection of two intervals.>>> I.closed(0, 2) & I.closed(1, 3) [1,2] >>> I.closed(0, 4) & I.open(2, 3) (2,3) >>> I.closed(0, 2) & I.closed(2, 3) [2] >>> I.closed(0, 2) & I.closed(3, 4) ()
-
x.union(other)
orx | other
return the union of two intervals.>>> I.closed(0, 1) | I.closed(1, 2) [0,2] >>> I.closed(0, 1) | I.closed(2, 3) [0,1] | [2,3]
-
x.complement(other)
or~x
return the complement of the interval.>>> ~I.closed(0, 1) (-inf,0) | (1,+inf) >>> ~(I.open(-I.inf, 0) | I.open(1, I.inf)) [0,1] >>> ~I.open(-I.inf, I.inf) ()
-
x.difference(other)
orx - other
return the difference betweenx
andother
.>>> I.closed(0,2) - I.closed(1,2) [0,1) >>> I.closed(0, 4) - I.closed(1, 2) [0,1) | (2,4]
-
x.contains(other)
orother in x
return True if given item is contained in the interval. SupportInterval
,AtomicInterval
and arbitrary comparable values.>>> 2 in I.closed(0, 2) True >>> 2 in I.open(0, 2) False >>> I.open(0, 1) in I.closed(0, 2) True
-
x.overlaps(other)
tests if there is an overlap between two intervals. This method accepts apermissive
parameter which defaults toFalse
. IfTrue
, it considers that [1, 2) and [2, 3] have an overlap on 2 (but not [1, 2) and (2, 3]).>>> I.closed(1, 2).overlaps(I.closed(2, 3)) True >>> I.closed(1, 2).overlaps(I.open(2, 3)) False >>> I.closed(1, 2).overlaps(I.open(2, 3), permissive=True) True
The following methods are only available for Interval
instances:
-
x.enclosure()
returns the smallest interval that includes the current one.>>> (I.closed(0, 1) | I.closed(2, 3)).enclosure() [0,3]
-
x.to_atomic()
is equivalent tox.enclosure()
but returns anAtomicInterval
instead of anInterval
object. -
x.is_atomic()
evaluates toTrue
if interval is composed of a single (possibly empty) atomic interval.>>> I.closed(0, 2).is_atomic() True >>> (I.closed(0, 1) | I.closed(1, 2)).is_atomic() True >>> (I.closed(0, 1) | I.closed(2, 3)).is_atomic() False
Bounds of an interval
The left and right boundaries, and the lower and upper bounds of an AtomicInterval
can be respectively accessed
with its left
, right
, lower
and upper
attributes.
The left
and right
bounds are either I.CLOSED
(True
) or I.OPEN
(False
).
>> I.CLOSED, I.OPEN
True, False
>>> x = I.closedopen(0, 1).to_atomic()
>>> x.left, x.lower, x.upper, x.right
(True, 0, 1, False)
Similarly, the bounds of an Interval
instance can be accessed with its left
, right
,
lower
and upper
attributes. In that case, left
and lower
refer to the lower bound of its enclosure,
while right
and upper
refer to the upper bound of its enclosure:
>>> x = I.open(0, 1) | I.closed(3, 4)
>>> x.left, x.lower, x.upper, x.right
(False, 0, 4, True)
One can easily check for some interval properties based on the bounds of an interval:
>>> x = I.openclosed(-I.inf, 0)
>>> # Check that interval is left/right closed
>>> x.left == I.CLOSED, x.right == I.CLOSED
(False, True)
>>> # Check that interval is left/right bounded
>>> x.lower == -I.inf, x.upper == I.inf
(True, False)
>>> # Check for singleton
>>> x.lower == x.upper
False
Both Interval
and AtomicInterval
instances are immutable but provide a replace
method that
can be used to create a new instance based on the current one. This method accepts four optional
parameters left
, lower
, upper
, and right
:
>>> i = I.closed(0, 2).to_atomic()
>>> i.replace(I.OPEN, -1, 3, I.CLOSED)
(-1,3]
>>> i.replace(lower=1, right=I.OPEN)
[1,2)
Functions can be passed instead of values. If a function is passed, it is called with the current corresponding
value except if the corresponding bound is an infinity and parameter ignore_inf
if set to False
.
>>> I.closed(0, 2).replace(upper=lambda x: 2 * x)
[0,4]
>>> i = I.closedopen(0, I.inf)
>>> i.replace(upper=lambda x: 10) # No change, infinity is ignored
[0,+inf)
>>> i.replace(upper=lambda x: 10, ignore_inf=False) # Infinity is not ignored
[0,10)
When replace
is applied on an Interval
that is not atomic, it is extended and/or restricted such that
its enclosure satisfies the new bounds.
>>> i = I.openclosed(0, 1) | I.closed(5, 10)
>>> i.replace(I.CLOSED, -1, 8, I.OPEN)
[-1,1] | [5,8)
>>> i.replace(lower=4)
(4,10]
Interval transformation
To apply an arbitrary transformation on an interval, Interval
instances expose an apply
method.
This method accepts a function that will be applied on each of the underlying atomic intervals to perform the desired transformation.
The function is expected to return an AtomicInterval
, an Interval
or a 4-uple (left, lower, upper, right)
.
>>> i = I.closed(2, 3) | I.open(4, 5)
>>> # Increment bound values
>>> i.apply(lambda x: (x.left, x.lower + 1, x.upper + 1, x.right))
[3,4] | (5,6)
>>> # Invert bounds
>>> i.apply(lambda x: (not x.left, x.lower, x.upper, not x.right))
(2,3) | [4,5]
The apply
method is very powerful when used in combination with replace
.
Because the latter allows functions to be passed as parameters and can ignore infinities, it can be
conveniently used to transform intervals in presence of infinities.
>>> i = I.openclosed(-I.inf, 0) | I.closed(3, 4) | I.closedopen(8, I.inf)
>>> # Increment bound values
>>> i.apply(lambda x: x.replace(upper=lambda v: v + 1))
(-inf,1] | [3,5] | [8,+inf)
>>> # Intervals are still automatically simplified
>>> i.apply(lambda x: x.replace(lower=lambda v: v * 2))
(-inf,0] | [16,+inf)
>>> # Invert bounds
>>> i.apply(lambda x: x.replace(left=lambda v: not v, right=lambda v: not v))
(-inf,0) | (3,4) | (8,+inf)
>>> # Replace infinities with -10 and 10
>>> conv = lambda v: -10 if v == -I.inf else (10 if v == I.inf else v)
>>> i.apply(lambda x: x.replace(lower=conv, upper=conv, ignore_inf=False))
(-10,0] | [3,4] | [8,10)
Iteration & indexing
Intervals can be iterated to access the underlying AtomicInterval
objects, sorted by their lower and upper bounds.
>>> list(I.open(2, 3) | I.closed(0, 1) | I.closed(21, 24))
[[0,1], (2,3), [21,24]]
The AtomicInterval
objects of an Interval
can also be accessed using their indexes:
>>> (I.open(2, 3) | I.closed(0, 1) | I.closed(21, 24))[0]
[0,1]
>>> (I.open(2, 3) | I.closed(0, 1) | I.closed(21, 24))[-2]
(2,3)
Comparison operators
Equality between intervals can be checked with the classical ==
operator:
>>> I.closed(0, 2) == I.closed(0, 1) | I.closed(1, 2)
True
>>> I.closed(0, 2) == I.closed(0, 2).to_atomic()
True
Moreover, both Interval
and AtomicInterval
are comparable using e.g. >
, >=
, <
or <=
.
These comparison operators have a different behaviour than the usual one.
For instance, a < b
holds if a
is entirely on the left of the lower bound of b
and a > b
holds if a
is entirely
on the right of the upper bound of b
.
>>> I.closed(0, 1) < I.closed(2, 3)
True
>>> I.closed(0, 1) < I.closed(1, 2)
False
Similarly, a <= b
holds if a
is entirely on the left of the upper bound of b
, and a >= b
holds if a
is entirely on the right of the lower bound of b
.
>>> I.closed(0, 1) <= I.closed(2, 3)
True
>>> I.closed(0, 2) <= I.closed(1, 3)
True
>>> I.closed(0, 3) <= I.closed(1, 2)
False
Intervals can also be compared with single values. If i
is an interval and x
a value, then
x < i
holds if x
is on the left of the lower bound of i
and x <= i
holds if x
is on the
left of the upper bound of i
. This behaviour is similar to the one that could be obtained by first
converting x
to a singleton interval.
>>> 5 < I.closed(0, 10)
False
>>> 5 <= I.closed(0, 10)
True
>>> I.closed(0, 10) < 5
False
>>> I.closed(0, 10) <= 5
True
Note that these semantics differ from classical comparison operators. As a consequence, some intervals are never comparable in the classical sense, as illustrated hereafter:
>>> I.closed(0, 4) <= I.closed(1, 2) or I.closed(0, 4) >= I.closed(1, 2)
False
>>> I.closed(0, 4) < I.closed(1, 2) or I.closed(0, 4) > I.closed(1, 2)
False
>>> I.empty() < I.empty()
True
Import & export intervals to strings
Intervals can be exported to string, either using repr
(as illustrated above) or with the to_string
function.
>>> I.to_string(I.closedopen(0, 1))
'[0,1)'
This function accepts both Interval
and AtomicInterval
instances.
The way string representations are built can be easily parametrized using the various parameters supported by
to_string
:
>>> params = {
... 'disj': ' or ',
... 'sep': ' - ',
... 'left_closed': '<',
... 'right_closed': '>',
... 'left_open': '..',
... 'right_open': '..',
... 'pinf': '+oo',
... 'ninf': '-oo',
... 'conv': lambda v: '"{}"'.format(v),
... }
>>> x = I.openclosed(0, 1) | I.closed(2, I.inf)
>>> I.to_string(x, **params)
'.."0" - "1"> or <"2" - +oo..'
Similarly, intervals can be created from a string using the from_string
function.
A conversion function (conv
parameter) has to be provided to convert a bound (as string) to a value.
>>> I.from_string('[0, 1]', conv=int) == I.closed(0, 1)
True
>>> I.from_string('[1.2]', conv=float) == I.singleton(1.2)
True
>>> converter = lambda s: datetime.datetime.strptime(s, '%Y/%m/%d')
>>> I.from_string('[2011/03/15, 2013/10/10]', conv=converter)
[datetime.datetime(2011, 3, 15, 0, 0),datetime.datetime(2013, 10, 10, 0, 0)]
Similarly to to_string
, function from_string
can be parametrized to deal with more elaborated inputs.
Notice that as from_string
expects regular expression patterns, we need to escape some characters.
>>> s = '.."0" - "1"> or <"2" - +oo..'
>>> params = {
... 'disj': ' or ',
... 'sep': ' - ',
... 'left_closed': '<',
... 'right_closed': '>',
... 'left_open': r'\.\.', # from_string expects regular expression patterns
... 'right_open': r'\.\.', # from_string expects regular expression patterns
... 'pinf': r'\+oo', # from_string expects regular expression patterns
... 'ninf': '-oo',
... 'conv': lambda v: int(v[1:-1]),
... }
>>> I.from_string(s, **params)
(0,1] | [2,+inf)
When a bound contains a comma or has a representation that cannot be automatically parsed with from_string
,
the bound
parameter can be used to specify the regular expression that should be used to match its representation.
>>> s = '[(0, 1), (2, 3)]' # Bounds are expected to be tuples
>>> I.from_string(s, conv=eval, bound=r'\(.+?\)')
[(0, 1),(2, 3)]
Import & export intervals to Python built-in data types
Intervals can also be exported to a list of 4-uples with to_data
, e.g., to support JSON serialization.
>>> x = I.openclosed(0, 1) | I.closedopen(2, I.inf)
>>> I.to_data(x)
[(False, 0, 1, True), (True, 2, inf, False)]
The function that is used to convert bounds can be specified with the conv
parameter.
The values that must be used to represent positive and negative infinities can be specified with
pinf
and ninf
. They default to float('inf')
and float('-inf')
respectively.
>>> x = I.closed(datetime.date(2011, 3, 15), datetime.date(2013, 10, 10))
>>> I.to_data(x, conv=lambda v: (v.year, v.month, v.day))
[(True, (2011, 3, 15), (2013, 10, 10), True)]
Intervals can be imported from such a list of 4-uples with from_data
.
The same set of parameters can be used to specify how bounds and infinities are converted.
>>> x = [(True, (2011, 3, 15), (2013, 10, 10), False)]
>>> I.from_data(x, conv=lambda v: datetime.date(*v))
[datetime.date(2011, 3, 15),datetime.date(2013, 10, 10))
Contributions
Contributions are very welcome! Feel free to report bugs or suggest new features using GitHub issues and/or pull requests.
Licence
Distributed under LGPLv3 - GNU Lesser General Public License, version 3.
Changelog
This library adheres to a semantic versioning scheme.
1.8.0 (2018-12-15)
- Intervals have a
left
,lower
,upper
, andright
attribute that refer to its enclosure. - Intervals have a
replace
method to create new intervals based on the current one. This method accepts both values and functions. - Intervals have an
apply
method to apply a function on the underlying atomic intervals. - Intervals can be compared with single values as well.
I.empty()
returns the same instance to save memory.- Infinities are singleton objects.
- Set
len(I.empty()) = 1
andI.empty()[0] == I.empty().to_atomic()
for consistency.
1.7.0 (2018-12-06)
- Import from and export to Python built-in data types (a list of 4-uples) with
from_data
andto_data
(#6). - Add examples for arbitrary interval transformations.
1.6.0 (2018-08-29)
- Add support for customized infinity representation in
to_string
andfrom_string
(#3).
1.5.4 (2018-07-29)
- Fix
.overlaps
(#2).
1.5.3 (2018-06-21)
- Fix invalid
repr
for atomic singleton intervals.
1.5.2 (2018-06-15)
- Fix invalid comparisons when both
Interval
andAtomicInterval
are compared.
1.5.1 (2018-04-25)
- Fix #1 by making empty intervals always resolving to
(I.inf, -I.inf)
.
1.5.0 (2018-04-17)
Interval.__init__
acceptsInterval
instances in addition toAtomicInterval
ones.
1.4.0 (2018-04-17)
- Function
I.to_string
to export an interval to a string, with many options to customize the representation. - Function
I.from_string
to create an interval from a string, with many options to customize the parsing.
1.3.2 (2018-04-13)
- Support for Python 2.7.
1.3.1 (2018-04-12)
- Define
__slots__
to lower memory usage, and to speed up attribute access. - Define
Interval.__rand__
(and other magic methods) to supportInterval
fromAtomicInterval
instead of having a dedicated piece of code inAtomicInterval
. - Fix
__all__
. - More tests to cover all comparisons.
1.3.0 (2018-04-04)
- Meaningful
<=
and>=
comparisons for intervals.
1.2.0 (2018-04-04)
Interval
supports indexing to retrieve the underlyingAtomicInterval
objects.
1.1.0 (2018-04-04)
- Both
AtomicInterval
andInterval
are fully comparable. - Add
singleton(x)
to create a singleton interval [x]. - Add
empty()
to create an empty interval. - Add
Interval.enclosure()
that returns the smallest interval that includes the current one. - Interval simplification is in O(n) instead of O(n*m).
AtomicInterval
objects in anInterval
are sorted by lower and upper bounds.
1.0.4 (2018-04-03)
- All operations of
AtomicInterval
(except overlaps) acceptInterval
. - Raise
TypeError
instead ofValueError
if type is not supported (coherent withNotImplemented
).
1.0.3 (2018-04-03)
- Initial working release on PyPi.
1.0.0 (2018-04-03)
- Initial release.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for python_intervals-1.8.0-py2.py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1d3de06b01517548b4a8d5137dfba5c2514780ec9b99778d5ed7920ee10ee22b |
|
MD5 | a1d047a2328e2ebbc04b785cd801671f |
|
BLAKE2b-256 | c670dc592dbb3512216aec9a1e35ec51691bd8ac41a550fc4b1e2b9fb937d17c |