SkillAgentSearch skills...

Portion

portion, a Python library providing data structure and operations for intervals.

Install / Use

/learn @AlexandreDecan/Portion
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<div align="center">

Logo

portion - data structure and operations for intervals

License Tests Coverage Status PyPI Last commit

</div>

The portion library provides data structure and operations for intervals in Python. In particular, it provides the following features:

  • Support intervals of any (comparable) objects;
  • Closed or open, finite or (semi-)infinite intervals;
  • Interval sets (union of atomic intervals) are supported;
  • Automatic simplification of intervals;
  • Support comparison, transformation, intersection, union, complement, difference and containment;
  • Provide test for emptiness, atomicity, overlap and adjacency;
  • Discrete iterations on the values of an interval;
  • Dict-like structure to map intervals to data;
  • Import and export intervals to strings and to Python built-in data types;
  • Heavily tested with high code coverage (regardless of what it means);
  • Mainly developed by a stubborn but enthusiastic Pythonista!

Table of contents

Installation

You can use pip to install it, as usual: pip install portion. This will install the latest available version from PyPI. Pre-releases are available from the master branch on GitHub and can be installed with pip install git+https://github.com/AlexandreDecan/portion (but don't trust pre-releases!).

You can install portion and its development environment using pip install --group dev at the root of this repository. This automatically installs pytest (for the test suites) and ruff (for code style).

Documentation & usage

Interval creation

Assuming this library is imported using import portion as P, intervals can be easily created using one of the following helpers:

>>> P.open(1, 2)
(1,2)
>>> P.closed(1, 2)
[1,2]
>>> P.openclosed(1, 2)
(1,2]
>>> P.closedopen(1, 2)
[1,2)
>>> P.singleton(1)
[1]
>>> P.empty()
()

The bounds of an interval can be any arbitrary values, as long as they are comparable:

>>> P.closed(1.2, 2.4)
[1.2,2.4]
>>> P.closed('a', 'z')
['a','z']
>>> import datetime
>>> P.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 P.inf and -P.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.

>>> P.inf > 'a', P.inf > 0, P.inf > True
(True, True, True)
>>> P.openclosed(-P.inf, 0)
(-inf,0]
>>> P.closed(-P.inf, P.inf)  # Automatically converted to an open interval
(-inf,+inf)

Intervals created with this library are Interval instances. An Interval instance is a disjunction of atomic intervals each representing a single interval (e.g. [1,2]). Intervals can be iterated to access the underlying atomic intervals, sorted by their lower and upper bounds.

>>> list(P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))
[[0,1], (10,11), [20,21]]
>>> list(P.empty())
[]

Nested (sorted) intervals can also be retrieved with a position or a slice:

>>> (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[0]
[0,1]
>>> (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[-2]
(10,11)
>>> (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[:2]
[0,1] | (10,11)

For convenience, intervals are automatically simplified:

>>> P.closed(0, 2) | P.closed(2, 4)
[0,4]
>>> P.closed(1, 2) | P.closed(3, 4) | P.closed(2, 3)
[1,4]
>>> P.empty() | P.closed(0, 1)
[0,1]
>>> P.closed(1, 2) | P.closed(2, 3) | P.closed(4, 5)
[1,3] | [4,5]

Note that, by default, simplification of discrete intervals is not supported by portion (but it can be simulated though, see #24). For example, combining [0,1] with [2,3] will not result in [0,3] even if there is no integer between 1 and 2. Refer to Specialize & customize intervals to see how to create and use specialized discrete intervals.

↑ back to top

Interval bounds & attributes

An Interval defines the following properties:

  • i.empty is True if and only if the interval is empty.

    >>> P.closed(0, 1).empty
    False
    >>> P.closed(0, 0).empty
    False
    >>> P.openclosed(0, 0).empty
    True
    >>> P.empty().empty
    True
    
    
  • i.atomic is True if and only if the interval is empty or is a disjunction of a single interval.

    >>> P.empty().atomic
    True
    >>> P.closed(0, 2).atomic
    True
    >>> (P.closed(0, 1) | P.closed(1, 2)).atomic
    True
    >>> (P.closed(0, 1) | P.closed(2, 3)).atomic
    False
    
    
  • i.enclosure refers to the smallest atomic interval that includes the current one.

    >>> (P.closed(0, 1) | P.open(2, 3)).enclosure
    [0,3)
    
    

The left and right boundaries, and the lower and upper bounds of an interval can be respectively accessed with its left, right, lower and upper attributes. The left and right bounds are either P.CLOSED or P.OPEN. By definition, P.CLOSED == ~P.OPEN and vice-versa.

>> P.CLOSED, P.OPEN
CLOSED, OPEN
>>> x = P.closedopen(0, 1)
>>> x.left, x.lower, x.upper, x.right
(CLOSED, 0, 1, OPEN)

By convention, empty intervals resolve to (P.inf, -P.inf):

>>> i = P.empty()
>>> i.left, i.lower, i.upper, i.right
(OPEN, +inf, -inf, OPEN)

If the interval is not atomic, then left and lower refer to the lower bound of its enclosure, while right and upper refer to the upper bound of its enclosure:

>>> x = P.open(0, 1) | P.closed(3, 4)
>>> x.left, x.lower, x.upper, x.right
(OPEN, 0, 4, CLOSED)

One can easily check for some interval properties based on the bounds of an interval:

>>> x = P.openclosed(-P.inf, 0)
>>> # Check that interval is left/right closed
>>> x.left == P.CLOSED, x.right == P.CLOSED
(False, True)
>>> # Check that interval is left/right bounded
>>> x.lower == -P.inf, x.upper == P.inf
(True, False)
>>> # Check for singleton
>>> x.lower == x.upper
False

↑ back to top

Interval operations

Interval instances support the following operations:

  • i.intersection(other) and i & other return the intersection of two intervals.

    >>> P.closed(0, 2) & P.closed(1, 3)
    [1,2]
    >>> P.closed(0, 4) & P.open(2, 3)
    (2,3)
    >>> P.closed(0, 2) & P.closed(2, 3)
    [2]
    >>> P.closed(0, 2) & P.closed(3, 4)
    ()
    
    
  • i.union(other) and i | other return the union of two intervals.

    >>> P.closed(0, 1) | P.closed(1, 2)
    [0,2]
    >>> P.closed(0, 1) | P.closed(2, 3)
    [0,1] | [2,3]
    
    
  • i.complement(other) and ~i return the complement of the interval.

    >>> ~P.closed(0, 1)
    (-inf,0) | (1,+inf)
    >>> ~(P.open(-P.inf, 0) | P.open(1, P.inf))
    [0,1]
    >>> ~P.open(-P.inf, P.inf)
    ()
    
    
  • i.difference(other) and i - other return the difference between i and other.

    >>> P.closed(0,2) - P.closed(1,2)
    [0,1)
    >>> P.closed(0, 4) - P.closed(1, 2)
    [0,1) | (2,4]
    
    
  • i.contains(other) and other in i hold if given item is contained in the interval. It supports intervals and arbitrary comparable values.

    >>> 2 in P.closed(0, 2)
    True
    >>> 2 in P.open(0, 2)
    False
    >>> P.open(0, 1) in P.closed(0, 2)
    True
    
    
  • i.adjacent(other) tests if the two intervals are adjacent, i.e., if they do not overlap and their union form a single atomic interval. While this definition corresponds to the usual notion of adjacency for atomic intervals, it has stronger requirements for non-atomic ones since it requires all underlying atomic intervals to be adjacent (i.e. that one interval fills the gaps between the atomic intervals of the other one).

    >>> P.closed(0, 1).adjacent(P.openclosed(1, 2))
    True
    >>> P.closed(0, 1).adjacent(P.closed(1, 2))
    False
    >>> (P.closed(0, 1) | P.closed(2, 3)).adjacent(P.open(1, 2) | P.open(3, 4))
    True
    >>> (P.closed(0, 1) | P.closed(2, 3)).adjacent(P.open(3,
    
View on GitHub
GitHub Stars519
CategoryDevelopment
Updated10d ago
Forks39

Languages

Python

Security Score

100/100

Audited on Mar 24, 2026

No findings