Package: portion
portion - data structure and operations for intervalsTheportionlibrary provides data structure and operations for intervals in Python 3.7+.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.Table of contentsInstallationDocumentation & usageInterval creationInterval bounds & attributesInterval operationsComparison operatorsInterval transformationDiscrete iterationMap intervals to dataImport & export intervals to stringsImport & export intervals to Python built-in data typesSpecialize & customize intervalsChangelogContributionsLicenseInstallationYou can usepipto install it, as usual:pip install portion. This will install the latest available version fromPyPI.
Pre-releases are available from themasterbranch onGitHuband can be installed withpip install git+https://github.com/AlexandreDecan/portion.
Note thatportionis also available onconda-forge.You can installportionand its development environment usingpip install -e .[test]at the root of this repository. This automatically installspytest(for the test suites) andblack(for code formatting).Documentation & usageInterval creationAssuming this library is imported usingimport 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']>>>importdatetime>>>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 usingP.infand-P.infas 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 areIntervalinstances.
AnIntervalinstance 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 isnotsupported byportion(but it can be simulated though, see#24).
For example, combining[0,1]with[2,3]willnotresult in[0,3]even if there is
no integer between1and2.
Refer toSpecialize & customize intervalsto see how to create and use specialized discrete intervals.↑ back to topInterval bounds & attributesAnIntervaldefines the following properties:i.emptyisTrueif and only if the interval is empty.>>>P.closed(0,1).emptyFalse>>>P.closed(0,0).emptyFalse>>>P.openclosed(0,0).emptyTrue>>>P.empty().emptyTruei.atomicisTrueif and only if the interval is empty or is a disjunction of a single interval.>>>P.empty().atomicTrue>>>P.closed(0,2).atomicTrue>>>(P.closed(0,1)|P.closed(1,2)).atomicTrue>>>(P.closed(0,1)|P.closed(2,3)).atomicFalsei.enclosurerefers 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 itsleft,right,lowerandupperattributes.
Theleftandrightbounds are eitherP.CLOSEDorP.OPEN.
By definition,P.CLOSED == ~P.OPENand vice-versa.>>P.CLOSED,P.OPENCLOSED,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, thenleftandlowerrefer to the lower bound of its enclosure,
whilerightandupperrefer 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.upperFalse↑ back to topInterval operationsIntervalinstances support the following operations:i.intersection(other)andi & otherreturn 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)andi | otherreturn 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~ireturn 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)andi - otherreturn the difference betweeniandother.>>>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)andother in ihold if given item is contained in the interval.
It supports intervals and arbitrary comparable values.>>>2inP.closed(0,2)True>>>2inP.open(0,2)False>>>P.open(0,1)inP.closed(0,2)Truei.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,4))False>>>P.closed(0,1).adjacent(P.open(1,2)|P.open(3,4))Falsei.overlaps(other)tests if there is an overlap between two intervals.>>>P.closed(1,2).overlaps(P.closed(2,3))True>>>P.closed(1,2).overlaps(P.open(2,3))FalseFinally, intervals are hashable as long as their bounds are hashable (and we have defined a hash value forP.infand-P.inf).↑ back to topComparison operatorsEquality between intervals can be checked with the classical==operator:>>>P.closed(0,2)==P.closed(0,1)|P.closed(1,2)True>>>P.closed(0,2)==P.open(0,2)FalseMoreover, intervals are comparable using>,>=,<or<=.
These comparison operators have a different behaviour than the usual ones.
For instance,a < bholds if all values inaare lower than the minimal value ofb(i.e.,ais
entirely on the left of the lower bound ofb).>>>P.closed(0,1)<P.closed(2,3)True>>>P.closed(0,1)<P.closed(1,2)FalseSimilarly,a <= bif all values inaare lower than the maximal value ofb(i.e.,ais
entirely on the left of the upper bound ofb).>>>P.closed(0,1)<=P.closed(2,3)True>>>P.closed(0,2)<=P.closed(1,3)True>>>P.closed(0,3)<=P.closed(1,2)FalseIf an interval needs to be compared against a single value, convert the value to a singleton interval first:>>>P.singleton(0)<P.closed(0,10)False>>>P.singleton(0)<=P.closed(0,10)True>>>P.singleton(5)<=P.closed(0,10)True>>>P.closed(0,1)<P.singleton(2)TrueNote that all these semantics differ from classical comparison operators.
As a consequence, the empty interval is never<,<=,>nor>=than any other interval, and
no interval is<,>,<=or>=when compared to the empty interval.>>>e=P.empty()>>>e<eore>eore<=eore>=eFalse>>>i=P.closed(0,1)>>>e<iore<=iore>iore>=iFalseMoreover, some non-empty intervals are also not comparable in the classical sense, as illustrated hereafter:>>>a,b=P.closed(0,4),P.closed(1,2)>>>a<bora>bFalse>>>a<=bora>=bFalse>>>b<=aandb>=aTrueAs a general rule, ifa < bholds, thena <= b,b > a,b >= a,not (a > b),not (b < a),not (a >= b), andnot (b <= a)hold.↑ back to topInterval transformationIntervals are immutable but provide areplacemethod to create a new interval based on the
current one. This method accepts four optional parametersleft,lower,upper, andright:>>>i=P.closed(0,2)>>>i.replace(P.OPEN,-1,3,P.CLOSED)(-1,3]>>>i.replace(lower=1,right=P.OPEN)[1,2)Functions can be passed instead of values. If a function is passed, it is called with the current corresponding
value.>>>P.closed(0,2).replace(upper=lambdax:2*x)[0,4]The provided function won't be called on infinities, unlessignore_infis set toFalse.>>>i=P.closedopen(0,P.inf)>>>i.replace(upper=lambdax:10)# No change, infinity is ignored[0,+inf)>>>i.replace(upper=lambdax:10,ignore_inf=False)# Infinity is not ignored[0,10)Whenreplaceis applied on an interval that is not atomic, it is extended and/or restricted such that
its enclosure satisfies the new bounds.>>>i=P.openclosed(0,1)|P.closed(5,10)>>>i.replace(P.CLOSED,-1,8,P.OPEN)[-1,1]|[5,8)>>>i.replace(lower=4)(4,10]To apply arbitrary transformations on the underlying atomic intervals, intervals expose anapplymethod that acts likemap.
This method accepts a function that will be applied on each of the underlying atomic intervals to perform the desired transformation.
The provided function is expected to return either anInterval, or a 4-uple(left, lower, upper, right).>>>i=P.closed(2,3)|P.open(4,5)>>># Increment bound values>>>i.apply(lambdax:(x.left,x.lower+1,x.upper+1,x.right))[3,4]|(5,6)>>># Invert bounds>>>i.apply(lambdax:(~x.left,x.lower,x.upper,~x.right))(2,3)|[4,5]Theapplymethod is very powerful when used in combination withreplace.
Because 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.>>>i=P.openclosed(-P.inf,0)|P.closed(3,4)|P.closedopen(8,P.inf)>>># Increment bound values>>>i.apply(lambdax:x.replace(upper=lambdav:v+1))(-inf,1]|[3,5]|[8,+inf)>>># Intervals are still automatically simplified>>>i.apply(lambdax:x.replace(lower=lambdav:v*2))(-inf,0]|[16,+inf)>>># Invert bounds>>>i.apply(lambdax:x.replace(left=lambdav:~v,right=lambdav:~v))(-inf,0)|(3,4)|(8,+inf)>>># Replace infinities with -10 and 10>>>conv=lambdav:-10ifv==-P.infelse(10ifv==P.infelsev)>>>i.apply(lambdax:x.replace(lower=conv,upper=conv,ignore_inf=False))(-10,0]|[3,4]|[8,10)↑ back to topDiscrete iterationTheiteratefunction 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 thestepbetween 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.>>>list(P.iterate(P.closed(0,3),step=1))[0,1,2,3]>>>list(P.iterate(P.closed(0,3),step=2))[0,2]>>>list(P.iterate(P.open(0,3),step=2))[2]When an interval is not atomic,iterateconsecutively iterates on all underlying atomic
intervals, starting from each lower bound and ending on each upper one:>>>list(P.iterate(P.singleton(0)|P.singleton(3)|P.singleton(5),step=2))# Won't be [0][0,3,5]>>>list(P.iterate(P.closed(0,2)|P.closed(4,6),step=3))# Won't be [0, 6][0,4]By default, the iteration always starts on the lower bound of each underlying atomic interval.
Thebaseparameter 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.
It can be helpful to deal with (semi-)infinite intervals, or toalignthe generated values of the iterator:>>># Align on integers>>>list(P.iterate(P.closed(0.3,4.9),step=1,base=int))[1,2,3,4]>>># Restrict values of a (semi-)infinite interval>>>list(P.iterate(P.openclosed(-P.inf,2),step=1,base=lambdax:max(0,x)))[0,1,2]Thebaseparameter can be used to change howiterateapplies on unions of atomic interval, by
specifying a function that returns a single value, as illustrated next:>>>base=lambdax:0>>>list(P.iterate(P.singleton(0)|P.singleton(3)|P.singleton(5),step=2,base=base))[0]>>>list(P.iterate(P.closed(0,2)|P.closed(4,6),step=3,base=base))[0,6]Notice that definingbasesuch 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 thegapsbetween
atomic intervals are large).Finally, iteration can be performed in reverse order by specifyingreverse=True.>>>list(P.iterate(P.closed(0,3),step=-1,reverse=True))# Mind step=-1[3,2,1,0]>>>list(P.iterate(P.closed(0,3),step=-2,reverse=True))# Mind step=-2[3,1]Again, 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 forstep(e.g., what would
be the step between two consecutive characters?). In these cases, a callable can be passed instead of a value.
This callable will be called with the current value, and is expected to return the next possible value.>>>list(P.iterate(P.closed('a','d'),step=lambdad:chr(ord(d)+1)))['a','b','c','d']>>># Since we reversed the order, we changed "+" to "-" in the lambda.>>>list(P.iterate(P.closed('a','d'),step=lambdad:chr(ord(d)-1),reverse=True))['d','c','b','a']↑ back to topMap intervals to dataThe library provides anIntervalDictclass, adict-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.>>>d=P.IntervalDict()>>>d[P.closed(0,3)]='banana'>>>d[4]='apple'>>>d{[0,3]:'banana',[4]:'apple'}When a value is defined for an interval that overlaps an existing one, it is automatically updated
to take the new value into account:>>>d[P.closed(2,4)]='orange'>>>d{[0,2):'banana',[2,4]:'orange'}AnIntervalDictcan be queried using single values or intervals. If a single value is used as a
key, its behaviour corresponds to the one of a classicaldict:>>>d[2]'orange'>>>d[5]# Key does not existTraceback(mostrecentcalllast):...KeyError:5>>>d.get(5,default=0)0When the key is an interval, a newIntervalDictcontaining the values
for the specified key is returned:>>>d[~P.empty()]# Get all values, similar to d.copy(){[0,2):'banana',[2,4]:'orange'}>>>d[P.closed(1,3)]{[1,2):'banana',[2,3]:'orange'}>>>d[P.closed(-2,1)]{[0,1]:'banana'}>>>d[P.closed(-2,-1)]{}By using.get, a default value (defaulting toNone) can be specified.
This value is used to "fill the gaps" if the queried interval is not completely
covered by theIntervalDict:>>>d.get(P.closed(-2,1),default='peach'){[-2,0):'peach',[0,1]:'banana'}>>>d.get(P.closed(-2,-1),default='peach'){[-2,-1]:'peach'}>>>d.get(P.singleton(1),default='peach')# Key is covered, default is not used{[1]:'banana'}For convenience, anIntervalDictprovides a way to look for specific data values.
The.findmethod always returns a (possibly empty)Intervalinstance for which given
value is defined:>>>d.find('banana')[0,2)>>>d.find('orange')[2,4]>>>d.find('carrot')()The active domain of anIntervalDictcan be retrieved with its.domainmethod.
This method always returns a singleIntervalinstance, where.keysreturns a sorted view of disjoint intervals.>>>d.domain()[0,4]>>>list(d.keys())[[0,2),[2,4]]>>>list(d.values())['banana','orange']>>>list(d.items())[([0,2),'banana'),([2,4],'orange')]The.keys,.valuesand.itemsmethods 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.
See#44to know how to obtained a sorted list of atomic intervals instead.>>>d=P.IntervalDict()>>>d[P.closed(0,1)]=d[P.closed(2,3)]='peach'>>>list(d.items())[([0,1]|[2,3],'peach')]TwoIntervalDictinstances can be combined together using the.combinemethod.
This method returns a newIntervalDictwhose keys and values are taken from the two
sourceIntervalDict. Values corresponding to non-intersecting keys are simply copied,
while values corresponding to intersecting keys are combined together using the provided
function, as illustrated hereafter:>>>d1=P.IntervalDict({P.closed(0,2):'banana'})>>>d2=P.IntervalDict({P.closed(1,3):'orange'})>>>concat=lambdax,y:x+'/'+y>>>d1.combine(d2,how=concat){[0,1):'banana',[1,2]:'banana/orange',(2,3]:'orange'}Resulting keys always correspond to an outer join. Other joins can be easily simulated
by querying the resultingIntervalDictas follows:>>>d=d1.combine(d2,how=concat)>>>d[d1.domain()]# Left join{[0,1):'banana',[1,2]:'banana/orange'}>>>d[d2.domain()]# Right join{[1,2]:'banana/orange',(2,3]:'orange'}>>>d[d1.domain()&d2.domain()]# Inner join{[1,2]:'banana/orange'}Finally, similarly to adict, anIntervalDictalso supportslen,inanddel, and defines.clear,.copy,.update,.pop,.popitem, and.setdefault.
For convenience, one can export the content of anIntervalDictto a classical Pythondictusing
theas_dictmethod. This method accepts an optionalatomicparameter (whose default isFalse). When set toTrue, the keys of the resultingdictinstance are atomic intervals.↑ back to topImport & export intervals to stringsIntervals can be exported to string, either usingrepr(as illustrated above) or with theto_stringfunction.>>>P.to_string(P.closedopen(0,1))'[0,1)'The way string representations are built can be easily parametrized using the various parameters supported byto_string:>>>params={...'disj':' or ',...'sep':' - ',...'left_closed':'<',...'right_closed':'>',...'left_open':'..',...'right_open':'..',...'pinf':'+oo',...'ninf':'-oo',...'conv':lambdav:'"{}"'.format(v),...}>>>x=P.openclosed(0,1)|P.closed(2,P.inf)>>>P.to_string(x,**params)'.."0" - "1"> or <"2" - +oo..'Similarly, intervals can be created from a string using thefrom_stringfunction.
A conversion function (convparameter) has to be provided to convert a bound (as string) to a value.>>>P.from_string('[0, 1]',conv=int)==P.closed(0,1)True>>>P.from_string('[1.2]',conv=float)==P.singleton(1.2)True>>>converter=lambdas:datetime.datetime.strptime(s,'%Y/%m/%d')>>>P.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 toto_string, functionfrom_stringcan be parametrized to deal with more elaborated inputs.
Notice that asfrom_stringexpects 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':lambdav:int(v[1:-1]),...}>>>P.from_string(s,**params)(0,1]|[2,+inf)When a bound contains a comma or has a representation that cannot be automatically parsed withfrom_string,
theboundparameter 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>>>P.from_string(s,conv=eval,bound=r'\(.+?\)')[(0,1),(2,3)]↑ back to topImport & export intervals to Python built-in data typesIntervals can also be exported to a list of 4-uples withto_data, e.g., to support JSON serialization.P.CLOSEDandP.OPENare represented by Boolean valuesTrue(inclusive) andFalse(exclusive).>>>P.to_data(P.openclosed(0,2))[(False,0,2,True)]The values used to represent positive and negative infinities can be specified withpinfandninf. They default tofloat('inf')andfloat('-inf')respectively.>>>x=P.openclosed(0,1)|P.closedopen(2,P.inf)>>>P.to_data(x)[(False,0,1,True),(True,2,inf,False)]The function to convert bounds can be specified with theconvparameter.>>>x=P.closedopen(datetime.date(2011,3,15),datetime.date(2013,10,10))>>>P.to_data(x,conv=lambdav:(v.year,v.month,v.day))[(True,(2011,3,15),(2013,10,10),False)]Intervals can be imported from such a list of 4-tuples withfrom_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)]>>>P.from_data(x,conv=lambdav:datetime.date(*v))[datetime.date(2011,3,15),datetime.date(2013,10,10))↑ back to topSpecialize & customize intervalsDisclaimer: the features explained in this section are still experimental and are subject to backward incompatible changes even in minor or patch updates ofportion.The intervals provided byportionalready cover a wide range of use cases.
However, in some situations, it might be interesting to specialize or customize these intervals.
One typical example would be to support discrete intervals such as intervals of integers.While it is definitely possible to rely on the default intervals provided byportionto encode discrete
intervals, there are a few edge cases that lead some operations to return unexpected results:>>>P.singleton(0)|P.singleton(1)# Case 1: should be [0,1] for discrete numbers[0]|[1]>>>P.open(0,1)# Case 2: should be empty(0,1)>>>P.closedopen(0,1)# Case 3: should be singleton [0][0,1)Theportionlibrary makes its best to ease defining and using subclasses ofIntervalto address
these situations. In particular,Intervalinstances always produce new intervals usingself.__class__, and the class is written in a way that most of its methods can be easily extended.To implement a class for intervals of discrete numbers and to cover the three aforementioned cases, we need to change the behaviour of theInterval._mergeableclass method (to address first case) and of theInterval.from_atomicclass method (for cases 2 and 3).
The 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.Thankfully, since discrete intervals are expected to be a frequent use case,portionprovides anAbstractDiscreteIntervalclass that already makes the appropriate changes to these two methods.
As indicated by its name, this class cannot be used directly and should be inherited.
In particular, one has either to provide a_stepclass attribute to define the step between consecutive discrete values, or to define the_incrand_decrclass methods:>>>classIntInterval(P.AbstractDiscreteInterval):..._step=1That's all!
We can now use this class to manipulate intervals of discrete numbers and see it covers the three problematic cases:>>>IntInterval.from_atomic(P.CLOSED,0,0,P.CLOSED)|IntInterval.from_atomic(P.CLOSED,1,1,P.CLOSED)[0,1]>>>IntInterval.from_atomic(P.OPEN,0,1,P.OPEN)()>>>IntInterval.from_atomic(P.CLOSED,0,1,P.OPEN)[0]As an example of using_incrand_decr, consider the followingCharIntervalsubclass tailored to manipulate intervals of characters:>>>classCharInterval(P.AbstractDiscreteInterval):..._incr=lambdav:chr(ord(v)+1)..._decr=lambdav:chr(ord(v)-1)>>>CharInterval.from_atomic(P.OPEN,'a','z',P.OPEN)['b','y']Having to callfrom_atomicon the subclass to create intervals is quite verbose.
For convenience, all the functions that create interval instances accept an additionalklassparameter to specify the class that creates intervals, circumventing the direct use of the class constructors.
However, having to specify theklassparameter in each call toP.closedor other helpers that create intervals is still a bit too verbose to be convenient.
Consequently,portionprovides acreate_apifunction that, given a subclass ofInterval, returns a dynamically generated module whose API is similar to the one ofportionbut configured to use the subclass instead:>>>D=P.create_api(IntInterval)>>>D.singleton(0)|D.singleton(1)[0,1]>>>D.open(0,1)()>>>D.closedopen(0,1)[0]This makes it easy to use our newly definedIntIntervalsubclass while still benefiting fromportion's API.Let's extend our example to support intervals of natural numbers.
Such intervals are quite similar to the above ones, except they cannot go over negative values.
We can prevent the bounds of an interval to be negative by slightly changing thefrom_atomicclass method as follows:>>>classNaturalInterval(IntInterval):...@classmethod...deffrom_atomic(cls,left,lower,upper,right):...returnsuper().from_atomic(...P.CLOSEDiflower<0elseleft,...max(0,lower),...upper,...right,...)We can now define and use theNmodule to check whether our newly definedNaturalIntervaldoes the job:>>>N=P.create_api(NaturalInterval)>>>N.closed(-10,2)[0,2]>>>N.open(-10,2)[0,1]>>>~N.empty()[0,+inf)Keep in mind that just becauseNaturalIntervalhas semantics associated with natural numbers does not mean that all possible operations on these intervals strictly comply the semantics.
The following examples illustrate some of the cases where additional checks should be implemented to strictly adhere to these semantics:>>>N.closed(1.5,2.5)# Bounds are not natural numbers[1.5,2.5]>>>0.5inN.closed(0,1)# Given value is not a natural numberTrue>>>~N.singleton(0.5)[1.5,+inf)↑ back to topChangelogThis library adheres to asemantic versioningscheme.
SeeCHANGELOG.mdfor the list of changes.ContributionsContributions are very welcome!
Feel free to report bugs or suggest new features using GitHub issues and/or pull requests.LicenseDistributed underLGPLv3 - GNU Lesser General Public License, version 3.You can refer to this library using:@software{portion,
  author = {Decan, Alexandre},
  title = {portion: Python data structure and operations for intervals},
  url = {https://github.com/AlexandreDecan/portion},
}