Set algebra over intervals and singleton values
Project description
set-algebra
set-algebra is a Python library for working with mathematical sets built from intervals and singleton values.
It provides four core concepts:
InfinityandNegativeInfinityEndpointIntervalSet
The library is designed for cases where you need interval-aware set logic rather than plain Python hash sets.
It supports open and closed bounds, unbounded intervals, singleton points, membership checks, complements, unions, intersections, differences, and symmetric differences.
It can also be useful for schedule calculations, such as finding overlapping meeting times between multiple people.
All code examples in this README are automatically extracted and tested to ensure they stay correct.
What the library does
set-algebra models subsets of an ordered axis.
That axis is usually numeric, and the string parser is numeric-focused, but the core Endpoint, Interval, and Set classes can also work with other comparable values when you construct them directly instead of parsing from strings.
Examples of supported representations:
- a single point:
{3} - a closed interval:
[1, 5] - an open interval:
(1, 5) - an unbounded interval:
(-inf, 10] - a mixed set:
[1, 2], {3}, (5, inf)
Installation
pip install set-algebra
Quick start
>>> from set_algebra import Interval, Set
>>> percentage = Interval('[0, 100]')
>>> 50 in percentage
True
>>> 200 in percentage
False
>>> negative = Interval('(-inf, 0)')
>>> -1 in negative
True
>>> 0 in negative
False
>>> s = Set('[1, 2], {3}')
>>> 1.5 in s
True
>>> 2 in s
True
>>> 3 in s
True
>>> 4 in s
False
>>> 100 in s
False
>>> s |= Set('[5, inf)')
>>> 100 in s
True
Public API
Infinity, NegativeInfinity, inf, neg_inf
Special objects representing positive and negative infinity.
>>> from set_algebra import inf, neg_inf
>>> inf > 10000000000
True
>>> neg_inf < -1.e20
True
>>> -inf == neg_inf
True
>>> -inf == -inf
True
Use unary minus on the library objects:
>>> from set_algebra import inf, neg_inf
>>> -inf is neg_inf
True
>>> -neg_inf is inf
True
Use is_finite(x) to test whether a value is neither infinity nor NaN:
>>> from set_algebra import inf, is_finite
>>> is_finite(inf)
False
>>> is_finite(float('inf'))
False
>>> is_finite(999)
True
Endpoint
Represents one boundary point of an interval.
Four endpoint forms are supported:
[1left-closed(1left-open1]right-closed1)right-open
Construction styles:
>>> from set_algebra import Endpoint
>>> Endpoint('[1')
Endpoint('[1')
>>> Endpoint('3)')
Endpoint('3)')
>>> Endpoint('(-inf')
Endpoint('(-inf')
>>> Endpoint(1, '[')
Endpoint('[1')
>>> Endpoint(3, ')')
Endpoint('3)')
Endpoints can be compared with scalars and with other endpoints.
>>> from set_algebra import Endpoint
>>> Endpoint('[1') <= 1
True
>>> Endpoint('(1') <= 1
False
>>> Endpoint('1]') == 1
True
>>> Endpoint('1)') == 1
False
Bitwise inversion flips side and openness while keeping the same value:
>>> from set_algebra import Endpoint
>>> ~Endpoint('[7')
Endpoint('7)')
>>> ~Endpoint('3]')
Endpoint('(3')
Use are_bounding(e1, e2) to check whether two opposite-side endpoints touch with no gap between them.
>>> from set_algebra import are_bounding, Endpoint
>>> are_bounding( Endpoint('7)'), Endpoint('[7') )
True
>>> are_bounding( Endpoint('7)'), Endpoint('(7') )
False
Interval
Represents a single interval with two endpoints, a and b.
Construction styles:
>>> from set_algebra import Interval, Endpoint
>>> Interval('[0, 1]')
Interval('[0, 1]')
>>> Interval(1, 2, '[)')
Interval('[1, 2)')
>>> Interval(Endpoint('(-1'), Endpoint('1]'))
Interval('(-1, 1]')
Interval Membership
- scalar in interval
- endpoint in interval
- interval in interval
>>> from set_algebra import Interval
>>> real = Interval('(-inf, inf)')
>>> 999 in real
True
>>> percentage = Interval('[0, 100]')
>>> 50 in percentage
True
>>> 200 in percentage
False
>>> small = Interval('[10, 20]')
>>> small in percentage
True
Useful attributes and methods
a,b: left and right endpointsnotation: normalized interval notationis_degenerate: whether both endpoint values are equalcopy()
A predefined unbounded interval is also available:
>>> from set_algebra import unbounded
>>> unbounded
Interval('(-inf, inf)')
Set
Represents a set as an ordered sequence of non-overlapping pieces, where each piece is either:
- an
Interval - a singleton scalar value
Construction styles
>>> from set_algebra import Set, Interval
>>> Set()
Set([])
>>> Set('[1, 2], {3}, [5, inf)')
Set([Interval('[1, 2]'), 3, Interval('[5, inf)')])
>>> Set([Interval('[1, 2]'), 3, Interval('[5, inf)')])
Set([Interval('[1, 2]'), 3, Interval('[5, inf)')])
>>> Set(Set('[1, 2], {3}'))
Set([Interval('[1, 2]'), 3])
>>> Set('[1, 2], {3}')
Set([Interval('[1, 2]'), 3])
The notation parser for Set expects pieces in ascending order with gaps between them. Adjacent or overlapping pieces must be represented in their merged form.
Valid:
Set('[1, 2], {4}, [6, 7]')
Invalid because there is no gap between pieces:
Set('[1, 2], {2}')
Set('[1, 2], (2, 3)')
Set Membership
>>> from set_algebra import Set, Interval
>>> s = Set('[1, 2], {3}, [5, 10)')
>>> 1 in s
True
>>> 3 in s
True
>>> 4 in s
False
>>> Interval('[6, 7]') in s
True
Boolean value
>>> from set_algebra import Set
>>> bool(Set())
False
>>> bool(Set('{10}'))
True
Set operations
Operator forms require another Set.
>>> from set_algebra import Set
>>> a = Set('[1, 5]')
>>> b = Set('[4, 10]')
>>> a | b # union
Set([Interval('[1, 10]')])
>>> a & b # intersection
Set([Interval('[4, 5]')])
>>> a - b # difference
Set([Interval('[1, 4)')])
>>> a ^ b # symmetric difference
Set([Interval('[1, 4)'), Interval('(5, 10]')])
>>> ~a # complement relative to (-inf, inf)
Set([Interval('(-inf, 1)'), Interval('(5, inf)')])
Method forms are more permissive and also accept iterables of intervals and/or scalars:
>>> from set_algebra import Set, Interval
>>> a = Set('[1, 5]')
>>> a.union([6, Interval('[10, 12]')])
Set([Interval('[1, 5]'), 6, Interval('[10, 12]')])
>>> a.intersection([Interval('[3, 4]')])
Set([Interval('[3, 4]')])
>>> a.difference([2, 3])
Set([Interval('[1, 2)'), Interval('(2, 3)'), Interval('(3, 5]')])
In-place mutating operators
Set supports mutating operators that modify the object in place:
|=: union update&=: intersection update-=: difference update^=: symmetric difference update
>>> from set_algebra import Set
>>> s = Set('[0, 5)')
>>> s |= Set('[10, 20]')
>>> s.notation
'[0, 5), [10, 20]'
>>> s &= Set('[3, 12]')
>>> s.notation
'[3, 5), [10, 12]'
>>> s -= Set('{4}, [11, 20]')
>>> s.notation
'[3, 4), (4, 5), [10, 11)'
>>> s ^= Set('[0, 3], {10}')
>>> s.notation
'[0, 3), (3, 4), (4, 5), (10, 11)'
Supported mutating methods
add(x)remove(x)clear()update(*others)intersection_update(*others)difference_update(*others)symmetric_difference_update(*others)
add(x)
>>> from set_algebra import Set, Interval
>>> s = Set('[0, 5]')
>>> s.add(10)
>>> s.notation
'[0, 5], {10}'
>>> s.add(Interval('[7, 9]'))
>>> s.notation
'[0, 5], [7, 9], {10}'
remove(x)
>>> from set_algebra import Set
>>> s = Set('[0, 10]')
>>> s.remove(5)
>>> s.notation
'[0, 5), (5, 10]'
Unlike Python built-in set.remove(x), Set.remove(x) does not raise an exception when x is not present:
>>> from set_algebra import Set
>>> s = Set('[0, 5]')
>>> s.remove(10)
>>> s.notation
'[0, 5]'
Because removal is already tolerant of missing values, there is no separate discard() method.
clear()
>>> from set_algebra import Set
>>> s = Set('[0, 5], {10}')
>>> bool(s)
True
>>> s.clear()
>>> bool(s)
False
>>> s.notation
''
update(*others)
>>> from set_algebra import Set, Interval
>>> s = Set('[0, 5]')
>>> s.update(
... Set('[10, 20]'),
... [30, Interval('[40, 50]')],
... )
>>> s.notation
'[0, 5], [10, 20], {30}, [40, 50]'
intersection_update(*others)
>>> from set_algebra import Set, Interval
>>> s = Set('[0, 20]')
>>> s.intersection_update(
... Set('[5, 15]'),
... [Interval('[10, 25]')],
... )
>>> s.notation
'[10, 15]'
Supported comparison operators
a == ba != ba > ba >= ba < ba <= b
Equality and inequality
>>> from set_algebra import Set
>>> Set('[0, 5]') == Set('[0, 5]')
True
>>> Set('[0, 5]') != Set('[0, 10]')
True
>>> Set('[0, 5]') == Set('[0, 5], {10}')
False
Superset and proper superset
>>> from set_algebra import Set
>>> Set('[0, 10]') >= Set('[2, 4]')
True
>>> Set('[0, 10]') >= Set('[2, 12]')
False
>>> Set('[0, 10]') > Set('[2, 4]')
True
>>> Set('[0, 10]') > Set('[0, 10]')
False
Subset and proper subset
>>> from set_algebra import Set
>>> Set('[2, 4]') <= Set('[0, 10]')
True
>>> Set('[2, 12]') <= Set('[0, 10]')
False
>>> Set('[2, 4]') < Set('[0, 10]')
True
>>> Set('[0, 10]') < Set('[0, 10]')
False
Two nonempty disjoint sets are neither subsets nor supersets of each other:
>>> from set_algebra import Set
>>> a = Set('[0, 5]')
>>> b = Set('[10, 20]')
>>> a < b
False
>>> a > b
False
>>> a == b
False
Supported query methods
search(x)issubset(other)issuperset(other)isdisjoint(other)copy()
search(x)
>>> from set_algebra import Set, Interval
>>> s = Set('[0, 5], {10}, [20, 30]')
>>> s.search(3)
(0, Interval('[0, 5]'))
>>> s.search(10)
(1, 10)
>>> s.search(15)
(2, None)
>>> s.search(25)
(2, Interval('[20, 30]'))
issubset(other)
>>> from set_algebra import Set, Interval
>>> Set('[2, 4]').issubset(Set('[0, 10]'))
True
>>> Set('[2, 12]').issubset(Set('[0, 10]'))
False
>>> Set('[2, 4]').issubset([Interval('[0, 10]')])
True
issuperset(other)
>>> from set_algebra import Set, Interval
>>> Set('[0, 10]').issuperset(Set('[2, 4]'))
True
>>> Set('[0, 10]').issuperset(Set('[2, 12]'))
False
>>> Set('[0, 10]').issuperset([Interval('[2, 4]')])
True
isdisjoint(other)
>>> from set_algebra import Set
>>> Set('[0, 5]').isdisjoint(Set('[10, 20]'))
True
>>> Set('[0, 5]').isdisjoint(Set('[3, 10]'))
False
>>> Set('{5}').isdisjoint(Set('(5, 10]'))
True
copy()
>>> from set_algebra import Set
>>> s1 = Set('[0, 5]')
>>> s2 = s1.copy()
>>> s1 == s2
True
>>> s1 is s2
False
>>> s2.add(10)
>>> s1.notation
'[0, 5]'
>>> s2.notation
'[0, 5], {10}'
Parser helpers
The parser module provides helpers used by notation-based constructors:
parse_value()parse_bound()parse_endpoint_notation()
These parse numeric strings, bounds, infinities, and endpoint notation.
Important behavior notes
String parsing is numeric-oriented
Notation strings such as "[1, 2]" or "(-inf, 5]" parse values as:
intfloatinfneg_inf
If you need intervals over non-numeric comparable values, construct Endpoint and Interval directly:
>>> from set_algebra import Endpoint, Interval
>>> left = Endpoint('p', '[')
>>> right = Endpoint('z', ']')
>>> letters = Interval(left, right)
>>> 'r' in letters
True
>>> 'sA' in letters # because 'p' <= 'sA' <= 'z'
True
Degenerate intervals
An interval whose endpoint values are equal is considered degenerate.
For Set, only the closed form [a, a] contributes a point. Other degenerate bound combinations are treated as empty when added or removed from a set.
Set stores normalized pieces
When you add overlapping or touching pieces, Set merges them into the minimal normalized representation.
>>> from set_algebra import Set, Interval
>>> s = Set()
>>> s.add(Interval('[1, 2)'))
>>> s.add(2)
>>> s.add(Interval('(2, 3]'))
>>> s.notation
'[1, 3]'
Complement is relative to the whole axis
The complement operator ~ means complement within (-inf, inf), not relative to some custom universe.
Examples
Interval containment
>>> from set_algebra import Interval
>>> outer = Interval('[0, 100]')
>>> inner = Interval('[20, 30]')
>>> inner in outer
True
Mixed set construction
from set_algebra import Set
s = Set('[1, 2], {4}, (10, 20]')
print(s.notation)
# [1, 2], {4}, (10, 20]
Union and difference
from set_algebra import Set
A = Set('[1, 5], {10}')
B = Set('[4, 8]')
print((A | B).notation)
# [1, 8], {10}
print((A - B).notation)
# [1, 4), {10}
Complement
from set_algebra import Set
s = Set('[0, 1], {3}')
print((~s).notation)
# (-inf, 0), (1, 3), (3, inf)
Scheduling and Availability
Alice can meet on 2026-04-12 from 10:00 to 14:00, and on 2026-04-14 from 13:00 to 15:00.
Bob can meet on 2026-04-12 from 12:00 to 16:00.
Find the time when they are both available.
>>> from datetime import date, datetime, time
>>> from set_algebra import Interval, Set
>>> apr12 = date(2026, 4, 12)
>>> apr14 = date(2026, 4, 14)
>>> alice = Set([
... Interval(
... datetime.combine(apr12, time(10, 0)),
... datetime.combine(apr12, time(14, 0)),
... '[]',
... ),
... Interval(
... datetime.combine(apr14, time(13, 0)),
... datetime.combine(apr14, time(15, 0)),
... '[]',
... ),
... ])
>>> bob = Set([
... Interval(
... datetime.combine(apr12, time(12, 0)),
... datetime.combine(apr12, time(16, 0)),
... '[]',
... ),
... ])
>>> common = alice & bob
>>> common.notation
'[2026-04-12 12:00:00, 2026-04-12 14:00:00]'
Exported names
The package exports the following names from set_algebra.__init__:
Endpoint
are_bounding
Infinity
NegativeInfinity
is_finite
inf
neg_inf
Interval
is_interval
is_scalar
unbounded
Set
License
MIT
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file set_algebra-0.4.0.tar.gz.
File metadata
- Download URL: set_algebra-0.4.0.tar.gz
- Upload date:
- Size: 37.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43c8a740993d94c46d3b80994ba9e7b93d70811ed516b6710f8bd214b5a7c197
|
|
| MD5 |
75f8953a195c75c8f832c81fa3f7073d
|
|
| BLAKE2b-256 |
69b7cbecbbd54915dd5eee043f051c1f539b1875021bb1869f5c32a7f6df63d2
|
File details
Details for the file set_algebra-0.4.0-py3-none-any.whl.
File metadata
- Download URL: set_algebra-0.4.0-py3-none-any.whl
- Upload date:
- Size: 18.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f67498007d4bf76382d213de7e9e94eee178b10098044c0f09bf65a57472f66b
|
|
| MD5 |
d343f2a69d5ddd28e43b7e2214bcf67b
|
|
| BLAKE2b-256 |
e94db3b48752d9647ec4e2c8149fd82ae9f80a1121d135b0d2dd2ebef8229db1
|