{"id":13689265,"url":"https://github.com/AlexandreDecan/portion","last_synced_at":"2025-05-01T23:33:13.842Z","repository":{"id":37619283,"uuid":"127901398","full_name":"AlexandreDecan/portion","owner":"AlexandreDecan","description":"portion, a Python library providing data structure and operations for intervals.","archived":false,"fork":false,"pushed_at":"2024-02-26T08:26:33.000Z","size":427,"stargazers_count":452,"open_issues_count":1,"forks_count":32,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-04-15T00:39:39.090Z","etag":null,"topics":["datastructures","interval","interval-set","operations","python","range"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AlexandreDecan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2018-04-03T12:02:13.000Z","updated_at":"2024-04-09T01:17:30.000Z","dependencies_parsed_at":"2023-01-25T20:46:08.073Z","dependency_job_id":"e76c5051-b925-4916-a44a-16874d086ca3","html_url":"https://github.com/AlexandreDecan/portion","commit_stats":{"total_commits":333,"total_committers":3,"mean_commits":111.0,"dds":0.009009009009009028,"last_synced_commit":"59e05afd13255cda236cfcffc4442af315041855"},"previous_names":["alexandredecan/python-intervals"],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexandreDecan%2Fportion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexandreDecan%2Fportion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexandreDecan%2Fportion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexandreDecan%2Fportion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AlexandreDecan","download_url":"https://codeload.github.com/AlexandreDecan/portion/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224282193,"owners_count":17285786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["datastructures","interval","interval-set","operations","python","range"],"created_at":"2024-08-02T15:01:40.675Z","updated_at":"2024-11-12T13:31:09.975Z","avatar_url":"https://github.com/AlexandreDecan.png","language":"Python","readme":"# portion - data structure and operations for intervals\n\n[![Tests](https://github.com/AlexandreDecan/portion/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/AlexandreDecan/portion/actions/workflows/test.yaml)\n[![Coverage Status](https://coveralls.io/repos/github/AlexandreDecan/portion/badge.svg?branch=master)](https://coveralls.io/github/AlexandreDecan/portion?branch=master)\n[![License](https://badgen.net/pypi/license/portion)](https://github.com/AlexandreDecan/portion/blob/master/LICENSE.txt)\n[![PyPI](https://badgen.net/pypi/v/portion)](https://pypi.org/project/portion)\n[![Commits](https://badgen.net/github/last-commit/AlexandreDecan/portion)](https://github.com/AlexandreDecan/portion/commits/)\n\n\nThe `portion` library provides data structure and operations for intervals in Python.\n\n - Support intervals of any (comparable) objects.\n - Closed or open, finite or (semi-)infinite intervals.\n - Interval sets (union of atomic intervals) are supported.\n - Automatic simplification of intervals.\n - Support comparison, transformation, intersection, union, complement, difference and containment.\n - Provide test for emptiness, atomicity, overlap and adjacency.\n - Discrete iterations on the values of an interval.\n - Dict-like structure to map intervals to data.\n - Import and export intervals to strings and to Python built-in data types.\n - Heavily tested with high code coverage.\n\n\n## Table of contents\n\n  * [Installation](#installation)\n  * [Documentation \u0026 usage](#documentation--usage)\n      * [Interval creation](#interval-creation)\n      * [Interval bounds \u0026 attributes](#interval-bounds--attributes)\n      * [Interval operations](#interval-operations)\n      * [Comparison operators](#comparison-operators)\n      * [Interval transformation](#interval-transformation)\n      * [Discrete iteration](#discrete-iteration)\n      * [Map intervals to data](#map-intervals-to-data)\n      * [Import \u0026 export intervals to strings](#import--export-intervals-to-strings)\n      * [Import \u0026 export intervals to Python built-in data types](#import--export-intervals-to-python-built-in-data-types)\n      * [Specialize \u0026 customize intervals](#specialize--customize-intervals)\n  * [Changelog](#changelog)\n  * [Contributions](#contributions)\n  * [License](#license)\n\n\n## Installation\n\nYou can use `pip` to install it, as usual: `pip install portion`. This will install the latest available version from [PyPI](https://pypi.org/project/portion).\nPre-releases are available from the *master* branch on [GitHub](https://github.com/AlexandreDecan/portion) and can be installed with `pip install git+https://github.com/AlexandreDecan/portion`.\n\nYou can install `portion` and its development environment using `pip install -e .[test]` at the root of this repository. This automatically installs [pytest](https://docs.pytest.org/en/latest/) (for the test suites) and [ruff](https://docs.astral.sh/ruff/) (for code style).\n\n\n## Documentation \u0026 usage\n\n### Interval creation\n\nAssuming this library is imported using `import portion as P`, intervals can be easily created using one of the following helpers:\n\n```python\n\u003e\u003e\u003e P.open(1, 2)\n(1,2)\n\u003e\u003e\u003e P.closed(1, 2)\n[1,2]\n\u003e\u003e\u003e P.openclosed(1, 2)\n(1,2]\n\u003e\u003e\u003e P.closedopen(1, 2)\n[1,2)\n\u003e\u003e\u003e P.singleton(1)\n[1]\n\u003e\u003e\u003e P.empty()\n()\n\n```\n\nThe bounds of an interval can be any arbitrary values, as long as they are comparable:\n\n```python\n\u003e\u003e\u003e P.closed(1.2, 2.4)\n[1.2,2.4]\n\u003e\u003e\u003e P.closed('a', 'z')\n['a','z']\n\u003e\u003e\u003e import datetime\n\u003e\u003e\u003e P.closed(datetime.date(2011, 3, 15), datetime.date(2013, 10, 10))\n[datetime.date(2011, 3, 15),datetime.date(2013, 10, 10)]\n\n```\n\n\nInfinite and semi-infinite intervals are supported using `P.inf` and `-P.inf` as upper or lower bounds.\nThese two objects support comparison with any other object.\nWhen infinities are used as a lower or upper bound, the corresponding boundary is automatically converted to an open one.\n\n```python\n\u003e\u003e\u003e P.inf \u003e 'a', P.inf \u003e 0, P.inf \u003e True\n(True, True, True)\n\u003e\u003e\u003e P.openclosed(-P.inf, 0)\n(-inf,0]\n\u003e\u003e\u003e P.closed(-P.inf, P.inf)  # Automatically converted to an open interval\n(-inf,+inf)\n\n```\n\nIntervals created with this library are `Interval` instances.\nAn `Interval` instance is a disjunction of atomic intervals each representing a single interval (e.g. `[1,2]`).\nIntervals can be iterated to access the underlying atomic intervals, sorted by their lower and upper bounds.\n\n```python\n\u003e\u003e\u003e list(P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))\n[[0,1], (10,11), [20,21]]\n\u003e\u003e\u003e list(P.empty())\n[]\n\n```\n\nNested (sorted) intervals can also be retrieved with a position or a slice:\n\n```python\n\u003e\u003e\u003e (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[0]\n[0,1]\n\u003e\u003e\u003e (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[-2]\n(10,11)\n\u003e\u003e\u003e (P.open(10, 11) | P.closed(0, 1) | P.closed(20, 21))[:2]\n[0,1] | (10,11)\n\n```\n\nFor convenience, intervals are automatically simplified:\n\n```python\n\u003e\u003e\u003e P.closed(0, 2) | P.closed(2, 4)\n[0,4]\n\u003e\u003e\u003e P.closed(1, 2) | P.closed(3, 4) | P.closed(2, 3)\n[1,4]\n\u003e\u003e\u003e P.empty() | P.closed(0, 1)\n[0,1]\n\u003e\u003e\u003e P.closed(1, 2) | P.closed(2, 3) | P.closed(4, 5)\n[1,3] | [4,5]\n\n```\n\nNote that, by default, simplification of discrete intervals is **not** supported by `portion` (but it can be simulated though, see [#24](https://github.com/AlexandreDecan/portion/issues/24#issuecomment-604456362)).\nFor example, combining `[0,1]` with `[2,3]` will **not** result in `[0,3]` even if there is no integer between `1` and `2`.\nRefer to [Specialize \u0026 customize intervals](#specialize--customize-intervals) to see how to create and use specialized discrete intervals.\n\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Interval bounds \u0026 attributes\n\n\nAn `Interval` defines the following properties:\n\n - `i.empty` is `True` if and only if the interval is empty.\n   ```python\n   \u003e\u003e\u003e P.closed(0, 1).empty\n   False\n   \u003e\u003e\u003e P.closed(0, 0).empty\n   False\n   \u003e\u003e\u003e P.openclosed(0, 0).empty\n   True\n   \u003e\u003e\u003e P.empty().empty\n   True\n\n   ```\n\n - `i.atomic` is `True` if and only if the interval is empty or is a disjunction of a single interval.\n   ```python\n   \u003e\u003e\u003e P.empty().atomic\n   True\n   \u003e\u003e\u003e P.closed(0, 2).atomic\n   True\n   \u003e\u003e\u003e (P.closed(0, 1) | P.closed(1, 2)).atomic\n   True\n   \u003e\u003e\u003e (P.closed(0, 1) | P.closed(2, 3)).atomic\n   False\n\n   ```\n\n - `i.enclosure` refers to the smallest atomic interval that includes the current one.\n   ```python\n   \u003e\u003e\u003e (P.closed(0, 1) | P.open(2, 3)).enclosure\n   [0,3)\n\n   ```\n\nThe 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.\nThe `left` and `right` bounds are either `P.CLOSED` or `P.OPEN`.\nBy definition, `P.CLOSED == ~P.OPEN` and vice-versa.\n\n```python\n\u003e\u003e P.CLOSED, P.OPEN\nCLOSED, OPEN\n\u003e\u003e\u003e x = P.closedopen(0, 1)\n\u003e\u003e\u003e x.left, x.lower, x.upper, x.right\n(CLOSED, 0, 1, OPEN)\n\n```\n\nBy convention, empty intervals resolve to `(P.inf, -P.inf)`:\n\n```python\n\u003e\u003e\u003e i = P.empty()\n\u003e\u003e\u003e i.left, i.lower, i.upper, i.right\n(OPEN, +inf, -inf, OPEN)\n\n```\n\n\nIf 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:\n\n```python\n\u003e\u003e\u003e x = P.open(0, 1) | P.closed(3, 4)\n\u003e\u003e\u003e x.left, x.lower, x.upper, x.right\n(OPEN, 0, 4, CLOSED)\n\n```\n\nOne can easily check for some interval properties based on the bounds of an interval:\n\n```python\n\u003e\u003e\u003e x = P.openclosed(-P.inf, 0)\n\u003e\u003e\u003e # Check that interval is left/right closed\n\u003e\u003e\u003e x.left == P.CLOSED, x.right == P.CLOSED\n(False, True)\n\u003e\u003e\u003e # Check that interval is left/right bounded\n\u003e\u003e\u003e x.lower == -P.inf, x.upper == P.inf\n(True, False)\n\u003e\u003e\u003e # Check for singleton\n\u003e\u003e\u003e x.lower == x.upper\nFalse\n\n```\n\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Interval operations\n\n`Interval` instances support the following operations:\n\n - `i.intersection(other)` and `i \u0026 other` return the intersection of two intervals.\n   ```python\n   \u003e\u003e\u003e P.closed(0, 2) \u0026 P.closed(1, 3)\n   [1,2]\n   \u003e\u003e\u003e P.closed(0, 4) \u0026 P.open(2, 3)\n   (2,3)\n   \u003e\u003e\u003e P.closed(0, 2) \u0026 P.closed(2, 3)\n   [2]\n   \u003e\u003e\u003e P.closed(0, 2) \u0026 P.closed(3, 4)\n   ()\n\n   ```\n\n - `i.union(other)` and `i | other` return the union of two intervals.\n   ```python\n   \u003e\u003e\u003e P.closed(0, 1) | P.closed(1, 2)\n   [0,2]\n   \u003e\u003e\u003e P.closed(0, 1) | P.closed(2, 3)\n   [0,1] | [2,3]\n\n   ```\n\n - `i.complement(other)` and `~i` return the complement of the interval.\n   ```python\n   \u003e\u003e\u003e ~P.closed(0, 1)\n   (-inf,0) | (1,+inf)\n   \u003e\u003e\u003e ~(P.open(-P.inf, 0) | P.open(1, P.inf))\n   [0,1]\n   \u003e\u003e\u003e ~P.open(-P.inf, P.inf)\n   ()\n\n   ```\n\n - `i.difference(other)` and `i - other` return the difference between `i` and `other`.\n   ```python\n   \u003e\u003e\u003e P.closed(0,2) - P.closed(1,2)\n   [0,1)\n   \u003e\u003e\u003e P.closed(0, 4) - P.closed(1, 2)\n   [0,1) | (2,4]\n\n   ```\n\n - `i.contains(other)` and `other in i` hold if given item is contained in the interval.\n It supports intervals and arbitrary comparable values.\n   ```python\n   \u003e\u003e\u003e 2 in P.closed(0, 2)\n   True\n   \u003e\u003e\u003e 2 in P.open(0, 2)\n   False\n   \u003e\u003e\u003e P.open(0, 1) in P.closed(0, 2)\n   True\n\n   ```\n\n - `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.\n 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).\n   ```python\n   \u003e\u003e\u003e P.closed(0, 1).adjacent(P.openclosed(1, 2))\n   True\n   \u003e\u003e\u003e P.closed(0, 1).adjacent(P.closed(1, 2))\n   False\n   \u003e\u003e\u003e (P.closed(0, 1) | P.closed(2, 3)).adjacent(P.open(1, 2) | P.open(3, 4))\n   True\n   \u003e\u003e\u003e (P.closed(0, 1) | P.closed(2, 3)).adjacent(P.open(3, 4))\n   False\n   \u003e\u003e\u003e P.closed(0, 1).adjacent(P.open(1, 2) | P.open(3, 4))\n   False\n\n   ```\n\n - `i.overlaps(other)` tests if there is an overlap between two intervals.\n   ```python\n   \u003e\u003e\u003e P.closed(1, 2).overlaps(P.closed(2, 3))\n   True\n   \u003e\u003e\u003e P.closed(1, 2).overlaps(P.open(2, 3))\n   False\n\n   ```\n\nFinally, intervals are hashable as long as their bounds are hashable (and we have defined a hash value for `P.inf` and `-P.inf`).\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Comparison operators\n\nEquality between intervals can be checked with the classical `==` operator:\n\n```python\n\u003e\u003e\u003e P.closed(0, 2) == P.closed(0, 1) | P.closed(1, 2)\nTrue\n\u003e\u003e\u003e P.closed(0, 2) == P.open(0, 2)\nFalse\n\n```\n\nMoreover, intervals are comparable using `\u003e`, `\u003e=`, `\u003c` or `\u003c=`.\nThese comparison operators have a different behaviour than the usual ones.\nFor instance, `a \u003c b` holds if all values in `a` are lower than the minimal value of `b` (i.e., `a` is entirely on the left of the lower bound of `b`).\n\n```python\n\u003e\u003e\u003e P.closed(0, 1) \u003c P.closed(2, 3)\nTrue\n\u003e\u003e\u003e P.closed(0, 1) \u003c P.closed(1, 2)\nFalse\n\n```\n\nSimilarly, `a \u003c= b` if all values in `a` are lower than the maximal value of `b` (i.e., `a` is entirely on the left of the upper bound of `b`).\n\n```python\n\u003e\u003e\u003e P.closed(0, 1) \u003c= P.closed(2, 3)\nTrue\n\u003e\u003e\u003e P.closed(0, 2) \u003c= P.closed(1, 3)\nTrue\n\u003e\u003e\u003e P.closed(0, 3) \u003c= P.closed(1, 2)\nFalse\n\n```\n\nIf an interval needs to be compared against a single value, convert the value to a singleton interval first:\n\n```python\n\u003e\u003e\u003e P.singleton(0) \u003c P.closed(0, 10)\nFalse\n\u003e\u003e\u003e P.singleton(0) \u003c= P.closed(0, 10)\nTrue\n\u003e\u003e\u003e P.singleton(5) \u003c= P.closed(0, 10)\nTrue\n\u003e\u003e\u003e P.closed(0, 1) \u003c P.singleton(2)\nTrue\n\n```\n\nNote that all these semantics differ from classical comparison operators.\nAs a consequence, the empty interval is never `\u003c`, `\u003c=`, `\u003e` nor `\u003e=` than any other interval, and no interval is `\u003c`, `\u003e`, `\u003c=` or `\u003e=` when compared to the empty interval.\n\n```python\n\u003e\u003e\u003e e = P.empty()\n\u003e\u003e\u003e e \u003c e or e \u003e e or e \u003c= e or e \u003e= e\nFalse\n\u003e\u003e\u003e i = P.closed(0, 1)\n\u003e\u003e\u003e e \u003c i or e \u003c= i or e \u003e i or e \u003e= i\nFalse\n\n```\n\nMoreover, some non-empty intervals are also not comparable in the classical sense, as illustrated hereafter:\n\n```python\n\u003e\u003e\u003e a, b = P.closed(0, 4), P.closed(1, 2)\n\u003e\u003e\u003e a \u003c b or a \u003e b\nFalse\n\u003e\u003e\u003e a \u003c= b or a \u003e= b\nFalse\n\u003e\u003e\u003e b \u003c= a and b \u003e= a\nTrue\n\n```\n\nAs a general rule, if `a \u003c b` holds, then `a \u003c= b`, `b \u003e a`, `b \u003e= a`, `not (a \u003e b)`, `not (b \u003c a)`, `not (a \u003e= b)`, and `not (b \u003c= a)` hold.\n\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Interval transformation\n\nIntervals are immutable but provide a `replace` method to create a new interval based on the current one. This method accepts four optional parameters `left`, `lower`, `upper`, and `right`:\n\n```python\n\u003e\u003e\u003e i = P.closed(0, 2)\n\u003e\u003e\u003e i.replace(P.OPEN, -1, 3, P.CLOSED)\n(-1,3]\n\u003e\u003e\u003e i.replace(lower=1, right=P.OPEN)\n[1,2)\n\n```\n\nFunctions can be passed instead of values. If a function is passed, it is called with the current corresponding value.\n\n```python\n\u003e\u003e\u003e P.closed(0, 2).replace(upper=lambda x: 2 * x)\n[0,4]\n\n```\n\nThe provided function won't be called on infinities, unless `ignore_inf` is set to `False`.\n\n```python\n\u003e\u003e\u003e i = P.closedopen(0, P.inf)\n\u003e\u003e\u003e i.replace(upper=lambda x: 10)  # No change, infinity is ignored\n[0,+inf)\n\u003e\u003e\u003e i.replace(upper=lambda x: 10, ignore_inf=False)  # Infinity is not ignored\n[0,10)\n\n```\n\nWhen `replace` is applied on an interval that is not atomic, it is extended and/or restricted such that its enclosure satisfies the new bounds.\n\n```python\n\u003e\u003e\u003e i = P.openclosed(0, 1) | P.closed(5, 10)\n\u003e\u003e\u003e i.replace(P.CLOSED, -1, 8, P.OPEN)\n[-1,1] | [5,8)\n\u003e\u003e\u003e i.replace(lower=4)\n(4,10]\n\n```\n\nTo apply arbitrary transformations on the underlying atomic intervals, intervals expose an `apply` method that acts like `map`.\nThis method accepts a function that will be applied on each of the underlying atomic intervals to perform the desired transformation.\nThe provided function is expected to return either an `Interval`, or a 4-uple `(left, lower, upper, right)`.\n\n```python\n\u003e\u003e\u003e i = P.closed(2, 3) | P.open(4, 5)\n\u003e\u003e\u003e # Increment bound values\n\u003e\u003e\u003e i.apply(lambda x: (x.left, x.lower + 1, x.upper + 1, x.right))\n[3,4] | (5,6)\n\u003e\u003e\u003e # Invert bounds\n\u003e\u003e\u003e i.apply(lambda x: (~x.left, x.lower, x.upper, ~x.right))\n(2,3) | [4,5]\n\n```\n\nThe `apply` method is very powerful when used in combination with `replace`.\nBecause the latter allows functions to be passed as parameters and ignores infinities by default, it can be conveniently used to transform (disjunction of) intervals in presence of infinities.\n\n```python\n\u003e\u003e\u003e i = P.openclosed(-P.inf, 0) | P.closed(3, 4) | P.closedopen(8, P.inf)\n\u003e\u003e\u003e # Increment bound values\n\u003e\u003e\u003e i.apply(lambda x: x.replace(upper=lambda v: v + 1))\n(-inf,1] | [3,5] | [8,+inf)\n\u003e\u003e\u003e # Intervals are still automatically simplified\n\u003e\u003e\u003e i.apply(lambda x: x.replace(lower=lambda v: v * 2))\n(-inf,0] | [16,+inf)\n\u003e\u003e\u003e # Invert bounds\n\u003e\u003e\u003e i.apply(lambda x: x.replace(left=lambda v: ~v, right=lambda v: ~v))\n(-inf,0) | (3,4) | (8,+inf)\n\u003e\u003e\u003e # Replace infinities with -10 and 10\n\u003e\u003e\u003e conv = lambda v: -10 if v == -P.inf else (10 if v == P.inf else v)\n\u003e\u003e\u003e i.apply(lambda x: x.replace(lower=conv, upper=conv, ignore_inf=False))\n(-10,0] | [3,4] | [8,10)\n\n```\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Discrete iteration\n\nThe `iterate` function takes an interval, and returns a generator to iterate over the values of an interval. Obviously, as intervals are continuous, it is required to specify the  `step` between consecutive values. The iteration then starts from the lower bound and ends on the upper one. Only values contained by the interval are returned this way.\n\n```python\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 3), step=1))\n[0, 1, 2, 3]\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 3), step=2))\n[0, 2]\n\u003e\u003e\u003e list(P.iterate(P.open(0, 3), step=2))\n[2]\n\n```\n\nWhen an interval is not atomic, `iterate` consecutively iterates on all underlying atomic intervals, starting from each lower bound and ending on each upper one:\n\n```python\n\u003e\u003e\u003e list(P.iterate(P.singleton(0) | P.singleton(3) | P.singleton(5), step=2))  # Won't be [0]\n[0, 3, 5]\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 2) | P.closed(4, 6), step=3))  # Won't be [0, 6]\n[0, 4]\n\n```\n\nBy default, the iteration always starts on the lower bound of each underlying atomic interval.\nThe `base` parameter can be used to change this behaviour, by specifying how the initial value to start the iteration from must be computed. This parameter accepts a callable that is called with the lower bound of each underlying atomic interval, and that returns the initial value to start the iteration from.\nIt can be helpful to deal with (semi-)infinite intervals, or to *align* the generated values of the iterator:\n\n```python\n\u003e\u003e\u003e # Align on integers\n\u003e\u003e\u003e list(P.iterate(P.closed(0.3, 4.9), step=1, base=int))\n[1, 2, 3, 4]\n\u003e\u003e\u003e # Restrict values of a (semi-)infinite interval\n\u003e\u003e\u003e list(P.iterate(P.openclosed(-P.inf, 2), step=1, base=lambda x: max(0, x)))\n[0, 1, 2]\n\n```\n\nThe `base` parameter can be used to change how `iterate` applies on unions of atomic interval, by specifying a function that returns a single value, as illustrated next:\n\n```python\n\u003e\u003e\u003e base = lambda x: 0\n\u003e\u003e\u003e list(P.iterate(P.singleton(0) | P.singleton(3) | P.singleton(5), step=2, base=base))\n[0]\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 2) | P.closed(4, 6), step=3, base=base))\n[0, 6]\n\n```\n\nNotice that defining `base` such that it returns a single value can be extremely inefficient in terms of performance when the intervals are \"far apart\" each other (i.e., when the *gaps* between atomic intervals are large).\n\nFinally, iteration can be performed in reverse order by specifying `reverse=True`.\n\n```python\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 3), step=-1, reverse=True))  # Mind step=-1\n[3, 2, 1, 0]\n\u003e\u003e\u003e list(P.iterate(P.closed(0, 3), step=-2, reverse=True))  # Mind step=-2\n[3, 1]\n\n```\n\nAgain, this library does not make any assumption about the objects being used in an interval, as long as they are comparable. However, it is not always possible to provide a meaningful value for `step` (e.g., what would be the step between two consecutive characters?). In these cases, a callable can be passed instead of a value.\nThis callable will be called with the current value, and is expected to return the next possible value.\n\n```python\n\u003e\u003e\u003e list(P.iterate(P.closed('a', 'd'), step=lambda d: chr(ord(d) + 1)))\n['a', 'b', 'c', 'd']\n\u003e\u003e\u003e # Since we reversed the order, we changed \"+\" to \"-\" in the lambda.\n\u003e\u003e\u003e list(P.iterate(P.closed('a', 'd'), step=lambda d: chr(ord(d) - 1), reverse=True))\n['d', 'c', 'b', 'a']\n\n```\n\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Map intervals to data\n\nThe library provides an `IntervalDict` class, a `dict`-like data structure to store and query data along with intervals. Any value can be stored in such data structure as long as it supports equality.\n\n\n```python\n\u003e\u003e\u003e d = P.IntervalDict()\n\u003e\u003e\u003e d[P.closed(0, 3)] = 'banana'\n\u003e\u003e\u003e d[4] = 'apple'\n\u003e\u003e\u003e d\n{[0,3]: 'banana', [4]: 'apple'}\n\n```\n\nWhen a value is defined for an interval that overlaps an existing one, it is automatically updated to take the new value into account:\n\n```python\n\u003e\u003e\u003e d[P.closed(2, 4)] = 'orange'\n\u003e\u003e\u003e d\n{[0,2): 'banana', [2,4]: 'orange'}\n\n```\n\nAn `IntervalDict` can be queried using single values or intervals. If a single value is used as a key, its behaviour corresponds to the one of a classical `dict`:\n\n```python\n\u003e\u003e\u003e d[2]\n'orange'\n\u003e\u003e\u003e d[5]  # Key does not exist\nTraceback (most recent call last):\n ...\nKeyError: 5\n\u003e\u003e\u003e d.get(5, default=0)\n0\n\n```\n\nWhen the key is an interval, a new `IntervalDict` containing the values for the specified key is returned:\n\n```python\n\u003e\u003e\u003e d[~P.empty()]  # Get all values, similar to d.copy()\n{[0,2): 'banana', [2,4]: 'orange'}\n\u003e\u003e\u003e d[P.closed(1, 3)]\n{[1,2): 'banana', [2,3]: 'orange'}\n\u003e\u003e\u003e d[P.closed(-2, 1)]\n{[0,1]: 'banana'}\n\u003e\u003e\u003e d[P.closed(-2, -1)]\n{}\n\n```\n\nBy using `.get`, a default value (defaulting to `None`) can be specified.\nThis value is used to \"fill the gaps\" if the queried interval is not completely covered by the `IntervalDict`:\n\n```python\n\u003e\u003e\u003e d.get(P.closed(-2, 1), default='peach')\n{[-2,0): 'peach', [0,1]: 'banana'}\n\u003e\u003e\u003e d.get(P.closed(-2, -1), default='peach')\n{[-2,-1]: 'peach'}\n\u003e\u003e\u003e d.get(P.singleton(1), default='peach')  # Key is covered, default is not used\n{[1]: 'banana'}\n\n```\n\nFor convenience, an `IntervalDict` provides a way to look for specific data values.\nThe `.find` method always returns a (possibly empty) `Interval` instance for which given value is defined:\n\n```python\n\u003e\u003e\u003e d.find('banana')\n[0,2)\n\u003e\u003e\u003e d.find('orange')\n[2,4]\n\u003e\u003e\u003e d.find('carrot')\n()\n\n```\n\nThe active domain of an `IntervalDict` can be retrieved with its `.domain` method.\nThis method always returns a single `Interval` instance, where `.keys` returns a sorted view of disjoint intervals.\n\n```python\n\u003e\u003e\u003e d.domain()\n[0,4]\n\u003e\u003e\u003e list(d.keys())\n[[0,2), [2,4]]\n\u003e\u003e\u003e list(d.values())\n['banana', 'orange']\n\u003e\u003e\u003e list(d.items())\n[([0,2), 'banana'), ([2,4], 'orange')]\n\n```\n\nThe `.keys`, `.values` and `.items` methods return exactly one element for each stored value (i.e., if two intervals share a value, they are merged into a disjunction), as illustrated next.\nSee [#44](https://github.com/AlexandreDecan/portion/issues/44#issuecomment-710199687) to know how to obtain a sorted list of atomic intervals instead.\n\n```python\n\u003e\u003e\u003e d = P.IntervalDict()\n\u003e\u003e\u003e d[P.closed(0, 1)] = d[P.closed(2, 3)] = 'peach'\n\u003e\u003e\u003e list(d.items())\n[([0,1] | [2,3], 'peach')]\n\n```\n\n\nTwo `IntervalDict` instances can be combined using the `.combine` method.\nThis method returns a new `IntervalDict` whose keys and values are taken from the two source `IntervalDict`.\nThe values corresponding to intersecting keys (i.e., when the two instances overlap) are combined using the provided `how` function, while values corresponding to non-intersecting keys are simply copied (i.e., the `how` function is not called for them), as illustrated hereafter:\n\n```python\n\u003e\u003e\u003e d1 = P.IntervalDict({P.closed(0, 2): 'banana'})\n\u003e\u003e\u003e d2 = P.IntervalDict({P.closed(1, 3): 'orange'})\n\u003e\u003e\u003e concat = lambda x, y: x + '/' + y\n\u003e\u003e\u003e d1.combine(d2, how=concat)\n{[0,1): 'banana', [1,2]: 'banana/orange', (2,3]: 'orange'}\n\n```\n\nThe `how` function can also receive the current interval as third parameter, by enabling the `pass_interval` parameter of `.combine`.\nThe `combine` method also accepts a `missing` parameter. When `missing` is set, the `how` function is called even for non-intersecting keys, using the value of `missing` to replace the missing values:\n\n```python\n\u003e\u003e\u003e d1.combine(d2, how=concat, missing='kiwi')\n{[0,1): 'banana/kiwi', [1,2]: 'banana/orange', (2,3]: 'kiwi/orange'}\n\n```\n\nResulting keys always correspond to an outer join. Other joins can be easily simulated by querying the resulting `IntervalDict` as follows:\n\n```python\n\u003e\u003e\u003e d = d1.combine(d2, how=concat)\n\u003e\u003e\u003e d[d1.domain()]  # Left join\n{[0,1): 'banana', [1,2]: 'banana/orange'}\n\u003e\u003e\u003e d[d2.domain()]  # Right join\n{[1,2]: 'banana/orange', (2,3]: 'orange'}\n\u003e\u003e\u003e d[d1.domain() \u0026 d2.domain()]  # Inner join\n{[1,2]: 'banana/orange'}\n\n```\n\nWhile `.combine` accepts a single `IntervalDict`, it can be generalized to support an arbitrary number of `IntervalDicts`, as illustrated in [#95](https://github.com/AlexandreDecan/portion/issues/95#issuecomment-2351435891).\n\nFinally, similarly to a `dict`, an `IntervalDict` also supports `len`, `in` and `del`, and defines `.clear`, `.copy`, `.update`, `.pop`, `.popitem`, and `.setdefault`.\nFor convenience, one can export the content of an `IntervalDict` to a classical Python `dict` using the `as_dict` method. This method accepts an optional `atomic` parameter (whose default is `False`).\nWhen set to `True`, the keys of the resulting `dict` instance are atomic intervals.\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Import \u0026 export intervals to strings\n\nIntervals can be exported to string, either using `repr` (as illustrated above) or with the `to_string` function.\n\n```python\n\u003e\u003e\u003e P.to_string(P.closedopen(0, 1))\n'[0,1)'\n\n```\n\nThe way string representations are built can be easily parametrized using the various parameters supported by `to_string`:\n\n```python\n\u003e\u003e\u003e params = {\n...   'disj': ' or ',\n...   'sep': ' - ',\n...   'left_closed': '\u003c',\n...   'right_closed': '\u003e',\n...   'left_open': '..',\n...   'right_open': '..',\n...   'pinf': '+oo',\n...   'ninf': '-oo',\n...   'conv': lambda v: '\"{}\"'.format(v),\n... }\n\u003e\u003e\u003e x = P.openclosed(0, 1) | P.closed(2, P.inf)\n\u003e\u003e\u003e P.to_string(x, **params)\n'..\"0\" - \"1\"\u003e or \u003c\"2\" - +oo..'\n\n```\n\nSimilarly, intervals can be created from a string using the `from_string` function.\nA conversion function (`conv` parameter) has to be provided to convert a bound (as string) to a value.\n\n```python\n\u003e\u003e\u003e P.from_string('[0, 1]', conv=int) == P.closed(0, 1)\nTrue\n\u003e\u003e\u003e P.from_string('[1.2]', conv=float) == P.singleton(1.2)\nTrue\n\u003e\u003e\u003e converter = lambda s: datetime.datetime.strptime(s, '%Y/%m/%d')\n\u003e\u003e\u003e P.from_string('[2011/03/15, 2013/10/10]', conv=converter)\n[datetime.datetime(2011, 3, 15, 0, 0),datetime.datetime(2013, 10, 10, 0, 0)]\n\n```\n\nSimilarly to `to_string`, function `from_string` can be parametrized to deal with more elaborated inputs.\nNotice that as `from_string` expects regular expression patterns, we need to escape some characters.\n\n```python\n\u003e\u003e\u003e s = '..\"0\" - \"1\"\u003e or \u003c\"2\" - +oo..'\n\u003e\u003e\u003e params = {\n...   'disj': ' or ',\n...   'sep': ' - ',\n...   'left_closed': '\u003c',\n...   'right_closed': '\u003e',\n...   'left_open': r'\\.\\.',  # from_string expects regular expression patterns\n...   'right_open': r'\\.\\.',  # from_string expects regular expression patterns\n...   'pinf': r'\\+oo',  # from_string expects regular expression patterns\n...   'ninf': '-oo',\n...   'conv': lambda v: int(v[1:-1]),\n... }\n\u003e\u003e\u003e P.from_string(s, **params)\n(0,1] | [2,+inf)\n\n```\n\nWhen 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.\n\n```python\n\u003e\u003e\u003e s = '[(0, 1), (2, 3)]'  # Bounds are expected to be tuples\n\u003e\u003e\u003e P.from_string(s, conv=eval, bound=r'\\(.+?\\)')\n[(0, 1),(2, 3)]\n\n```\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Import \u0026 export intervals to Python built-in data types\n\nIntervals can also be exported to a list of 4-uples with `to_data`, e.g., to support JSON serialization.\n`P.CLOSED` and `P.OPEN` are represented by Boolean values `True` (inclusive) and `False` (exclusive).\n\n```python\n\u003e\u003e\u003e P.to_data(P.openclosed(0, 2))\n[(False, 0, 2, True)]\n\n```\n\nThe values used to represent positive and negative infinities can be specified with `pinf` and `ninf`. They default to `float('inf')` and `float('-inf')` respectively.\n\n```python\n\u003e\u003e\u003e x = P.openclosed(0, 1) | P.closedopen(2, P.inf)\n\u003e\u003e\u003e P.to_data(x)\n[(False, 0, 1, True), (True, 2, inf, False)]\n\n```\n\nThe function to convert bounds can be specified with the `conv` parameter.\n\n```python\n\u003e\u003e\u003e x = P.closedopen(datetime.date(2011, 3, 15), datetime.date(2013, 10, 10))\n\u003e\u003e\u003e P.to_data(x, conv=lambda v: (v.year, v.month, v.day))\n[(True, (2011, 3, 15), (2013, 10, 10), False)]\n\n```\n\nIntervals can be imported from such a list of 4-tuples with `from_data`.\nThe same set of parameters can be used to specify how bounds and infinities are converted.\n\n```python\n\u003e\u003e\u003e x = [(True, (2011, 3, 15), (2013, 10, 10), False)]\n\u003e\u003e\u003e P.from_data(x, conv=lambda v: datetime.date(*v))\n[datetime.date(2011, 3, 15),datetime.date(2013, 10, 10))\n\n```\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n### Specialize \u0026 customize intervals\n\n**Disclaimer**: the features explained in this section are still experimental and are subject to backward incompatible changes even in minor or patch updates of `portion`.\n\nThe intervals provided by `portion` already cover a wide range of use cases.\nHowever, in some situations, it might be interesting to specialize or customize these intervals.\nOne typical example would be to support discrete intervals such as intervals of integers.\n\nWhile it is definitely possible to rely on the default intervals provided by `portion` to encode discrete intervals, there are a few edge cases that lead some operations to return unexpected results:\n\n```python\n\u003e\u003e\u003e P.singleton(0) | P.singleton(1)  # Case 1: should be [0,1] for discrete numbers\n[0] | [1]\n\u003e\u003e\u003e P.open(0, 1)  # Case 2: should be empty\n(0,1)\n\u003e\u003e\u003e P.closedopen(0, 1)  # Case 3: should be singleton [0]\n[0,1)\n\n```\n\nThe `portion` library makes its best to ease defining and using subclasses of `Interval` to address these situations. In particular, `Interval` instances always produce new intervals using `self.__class__`, and the class is written in a way that most of its methods can be easily extended.\n\nTo implement a class for intervals of discrete numbers and to cover the three aforementioned cases, we need to change the behaviour of the `Interval._mergeable` class method (to address first case) and of the `Interval.from_atomic` class method (for cases 2 and 3).\nThe former is used to detect whether two atomic intervals can be merged into a single interval, while the latter is used to create atomic intervals.\n\nThankfully, since discrete intervals are expected to be a frequent use case, `portion` provides an `AbstractDiscreteInterval` class that already makes the appropriate changes to these two methods.\nAs indicated by its name, this class cannot be used directly and should be inherited.\nIn particular, one has either to provide a `_step` class attribute to define the step between consecutive discrete values, or to define the `_incr` and `_decr` class methods:\n\n```python\n\u003e\u003e\u003e class IntInterval(P.AbstractDiscreteInterval):\n...     _step = 1\n\n```\n\nThat's all!\nWe can now use this class to manipulate intervals of discrete numbers and see it covers the three problematic cases:\n\n```python\n\u003e\u003e\u003e IntInterval.from_atomic(P.CLOSED, 0, 0, P.CLOSED) | IntInterval.from_atomic(P.CLOSED, 1, 1, P.CLOSED)\n[0,1]\n\u003e\u003e\u003e IntInterval.from_atomic(P.OPEN, 0, 1, P.OPEN)\n()\n\u003e\u003e\u003e IntInterval.from_atomic(P.CLOSED, 0, 1, P.OPEN)\n[0]\n\n```\n\nAs an example of using `_incr` and `_decr`, consider the following `CharInterval` subclass tailored to manipulate intervals of characters:\n\n```python\n\u003e\u003e\u003e class CharInterval(P.AbstractDiscreteInterval):\n...     _incr = lambda v: chr(ord(v) + 1)\n...     _decr = lambda v: chr(ord(v) - 1)\n\u003e\u003e\u003e CharInterval.from_atomic(P.OPEN, 'a', 'z', P.OPEN)\n['b','y']\n\n```\n\nHaving to call `from_atomic` on the subclass to create intervals is quite verbose.\nFor convenience, all the functions that create interval instances accept an additional `klass` parameter to specify the class that creates intervals, circumventing the direct use of the class constructors.\nHowever, having to specify the `klass` parameter in each call to `P.closed` or other helpers that create intervals is still a bit too verbose to be convenient.\nConsequently, `portion` provides a `create_api` function that, given a subclass of `Interval`, returns a dynamically generated module whose API is similar to the one of `portion` but configured to use the subclass instead:\n\n```python\n\u003e\u003e\u003e D = P.create_api(IntInterval)\n\u003e\u003e\u003e D.singleton(0) | D.singleton(1)\n[0,1]\n\u003e\u003e\u003e D.open(0, 1)\n()\n\u003e\u003e\u003e D.closedopen(0, 1)\n[0]\n\n```\n\nThis makes it easy to use our newly defined `IntInterval` subclass while still benefiting from `portion`'s API.\n\nLet's extend our example to support intervals of natural numbers.\nSuch intervals are quite similar to the above ones, except they cannot go over negative values.\nWe can prevent the bounds of an interval to be negative by slightly changing the `from_atomic` class method as follows:\n\n```python\n\u003e\u003e\u003e class NaturalInterval(IntInterval):\n...    @classmethod\n...    def from_atomic(cls, left, lower, upper, right):\n...        return super().from_atomic(\n...            P.CLOSED if lower \u003c 0 else left,\n...            max(0, lower),\n...            upper,\n...            right,\n...        )\n\n```\n\nWe can now define and use the `N` module to check whether our newly defined `NaturalInterval` does the job:\n\n```python\n\u003e\u003e\u003e N = P.create_api(NaturalInterval)\n\u003e\u003e\u003e N.closed(-10, 2)\n[0,2]\n\u003e\u003e\u003e N.open(-10, 2)\n[0,1]\n\u003e\u003e\u003e ~N.empty()\n[0,+inf)\n\n```\n\nKeep in mind that just because `NaturalInterval` has semantics associated with natural numbers does not mean that all possible operations on these intervals strictly comply the semantics.\nThe following examples illustrate some of the cases where additional checks should be implemented to strictly adhere to these semantics:\n\n```python\n\u003e\u003e\u003e N.closed(1.5, 2.5)  # Bounds are not natural numbers\n[1.5,2.5]\n\u003e\u003e\u003e 0.5 in N.closed(0, 1)  # Given value is not a natural number\nTrue\n\u003e\u003e\u003e ~N.singleton(0.5)\n[1.5,+inf)\n\n```\n\n\n\n[\u0026uparrow; back to top](#table-of-contents)\n## Changelog\n\nThis library adheres to a [semantic versioning](https://semver.org) scheme.\nSee [CHANGELOG.md](https://github.com/AlexandreDecan/portion/blob/master/CHANGELOG.md) for the list of changes.\n\n\n\n## Contributions\n\nContributions are very welcome!\nFeel free to report bugs or suggest new features using GitHub issues and/or pull requests.\n\n\n\n## License\n\nDistributed under [LGPLv3 - GNU Lesser General Public License, version 3](https://github.com/AlexandreDecan/portion/blob/master/LICENSE.txt).\n\nYou can refer to this library using:\n\n```\n@software{portion,\n  author = {Decan, Alexandre},\n  title = {portion: Python data structure and operations for intervals},\n  url = {https://github.com/AlexandreDecan/portion},\n}\n```\n\n\n","funding_links":[],"categories":["Python"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAlexandreDecan%2Fportion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAlexandreDecan%2Fportion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAlexandreDecan%2Fportion/lists"}