Skip to main content

Interval arithmetic for Python

Project description

Interval arithmetic for Python

Travis Coverage Status PyPI

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) or x & 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) or x | 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) or x - other return the difference between x and other.

    >>> 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) or other in x return True if given item is contained in the interval. Support Interval, 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 a permissive parameter which defaults to False. If True, 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 to x.enclosure() but returns an AtomicInterval instead of an Interval object.

  • x.is_atomic() evaluates to True 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, and right 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 and I.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 and to_data (#6).
  • Add examples for arbitrary interval transformations.

1.6.0 (2018-08-29)

  • Add support for customized infinity representation in to_string and from_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 and AtomicInterval 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__ accepts Interval instances in addition to AtomicInterval 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 support Interval from AtomicInterval instead of having a dedicated piece of code in AtomicInterval.
  • 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 underlying AtomicInterval objects.

1.1.0 (2018-04-04)

  • Both AtomicInterval and Interval 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 an Interval are sorted by lower and upper bounds.

1.0.4 (2018-04-03)

  • All operations of AtomicInterval (except overlaps) accept Interval.
  • Raise TypeError instead of ValueError if type is not supported (coherent with NotImplemented).

1.0.3 (2018-04-03)

  • Initial working release on PyPi.

1.0.0 (2018-04-03)

  • Initial release.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python-intervals-1.8.0.tar.gz (23.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_intervals-1.8.0-py2.py3-none-any.whl (16.5 kB view details)

Uploaded Python 2Python 3

File details

Details for the file python-intervals-1.8.0.tar.gz.

File metadata

  • Download URL: python-intervals-1.8.0.tar.gz
  • Upload date:
  • Size: 23.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.12.1 pkginfo/1.4.2 requests/2.21.0 setuptools/40.6.3 requests-toolbelt/0.8.0 tqdm/4.28.1 CPython/3.5.6

File hashes

Hashes for python-intervals-1.8.0.tar.gz
Algorithm Hash digest
SHA256 b55406c44136ca03a9f49d82b466e30824cb7c501614989b23809442d01ebcb0
MD5 1ce27bbb107f2b3e2b9d7cdbb3cde2fd
BLAKE2b-256 71cd4b15456e84a08814bea3082344cb823521ec22b00394b773eb905541c062

See more details on using hashes here.

File details

Details for the file python_intervals-1.8.0-py2.py3-none-any.whl.

File metadata

  • Download URL: python_intervals-1.8.0-py2.py3-none-any.whl
  • Upload date:
  • Size: 16.5 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.12.1 pkginfo/1.4.2 requests/2.21.0 setuptools/40.6.3 requests-toolbelt/0.8.0 tqdm/4.28.1 CPython/3.5.6

File hashes

Hashes for python_intervals-1.8.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 1d3de06b01517548b4a8d5137dfba5c2514780ec9b99778d5ed7920ee10ee22b
MD5 a1d047a2328e2ebbc04b785cd801671f
BLAKE2b-256 c670dc592dbb3512216aec9a1e35ec51691bd8ac41a550fc4b1e2b9fb937d17c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page