././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/0000755000076500000240000000000000000000000014152 5ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899592.0 intervals-0.9.0/CHANGES.rst0000644000076500000240000000550600000000000015762 0ustar00konstastaff00000000000000Changelog --------- Here you can see the full list of changes between each intervals release. 0.9.0 (2020-07-16) ^^^^^^^^^^^^^^^^^^ - Fixed interval coercion (#42) - Dropped py27 and py34 support 0.8.1 (2017-12-06) ^^^^^^^^^^^^^^^^^^ - Fixed interval coercion (#41) 0.8.0 (2016-07-10) ^^^^^^^^^^^^^^^^^^ - Added is_connected utility method - Made interval constructor throw IllegalArgument exception for intervals such as (a..a) - Fixed some edge cases with interval intersections - Fixed interval union (#33) 0.7.1 (2016-03-05) ^^^^^^^^^^^^^^^^^^ - Added support for step argument in ``from_string`` factory method - Added better error message when passing string as a first constructor argument 0.7.0 (2016-02-27) ^^^^^^^^^^^^^^^^^^ - Added canonicalize to main module import - Added factory methods for easier interval initialization ``open``, ``closed``, ``open_closed``, ``closed_open``, ``greater_than``, ``at_least``, ``less_than``, ``at_most`` and ``all`` - Renamed ``open`` and ``closed`` properties to ``is_open`` and ``is_closed`` 0.6.0 (2016-01-28) ^^^^^^^^^^^^^^^^^^ - Removed interval constructor string parsing - Added new class method from_string 0.5.0 (2014-03-25) ^^^^^^^^^^^^^^^^^^ - Drop python 2.6 support 0.4.0 (2014-10-30) ^^^^^^^^^^^^^^^^^^ - Made intervals hashable - Added CharacterInterval class 0.3.2 (2014-10-21) ^^^^^^^^^^^^^^^^^^ - Fixed interval datetime type guessing - Fixed code examples in docs - Fixed IntInterval step rounding - Added better test suite (now docs are tested also) 0.3.1 (2014-06-24) ^^^^^^^^^^^^^^^^^^ - Fixed setup.py packages 0.3.0 (2014-03-18) ^^^^^^^^^^^^^^^^^^ - Added ``__bool__`` implementation for all interval types - Added ``__int__`` support for ``IntInterval`` 0.2.4 (2014-02-25) ^^^^^^^^^^^^^^^^^^ - Added step argument to ``AbstractInterval`` constructor 0.2.3 (2014-02-25) ^^^^^^^^^^^^^^^^^^ - Improved hyphen range parsing - Fixed ``glb`` and ``lub`` methods - Added ``inf`` and ``sup`` methods 0.2.2 (2014-02-20) ^^^^^^^^^^^^^^^^^^ - Fixed comparison to ``None`` - Added ``glb`` and ``lub`` methods 0.2.1 (2014-02-17) ^^^^^^^^^^^^^^^^^^ - Fixed ``__lt__`` and ``__le__`` operators (``total_ordering`` was not working) 0.2.0 (2014-01-10) ^^^^^^^^^^^^^^^^^^ - Added improved arithmetics - Added ``centre``, ``radius``, ``discrete`` and ``length`` properties - Added support for custom typed intervals - Added support for discrete intervals - Added support for ``__radd__`` and ``__rsub__`` - Added ``degenerate`` property - Added support for contains - Added ``Interval`` subclasses (``IntInterval``, ``DateInterval``, ``DateTimeInterval``, ``FloatInterval`` and ``DecimalInterval``) - Updated infinity dependency to 0.1.3 0.1.1 (2014-01-09) ^^^^^^^^^^^^^^^^^^ - Added interval ``length`` property 0.1.0 (2014-01-09) ^^^^^^^^^^^^^^^^^^ - Initial public release ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1389041722.0 intervals-0.9.0/LICENSE0000644000076500000240000000272500000000000015165 0ustar00konstastaff00000000000000Copyright (c) 2014, Konsta Vesterinen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1403622431.0 intervals-0.9.0/MANIFEST.in0000644000076500000240000000015700000000000015713 0ustar00konstastaff00000000000000include README.rst include LICENSE include CHANGES.rst recursive-include tests * recursive-exclude tests *.pyc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/PKG-INFO0000644000076500000240000000215700000000000015254 0ustar00konstastaff00000000000000Metadata-Version: 2.1 Name: intervals Version: 0.9.0 Summary: Python tools for handling intervals (ranges of comparable objects). Home-page: https://github.com/kvesteri/intervals Author: Konsta Vesterinen Author-email: konsta@fastmonkeys.com License: BSD Description: intervals --------- Python tools for handling intervals (ranges of comparable objects). Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468169674.0 intervals-0.9.0/README.rst0000644000076500000240000002576600000000000015661 0ustar00konstastaff00000000000000intervals ========= |Build Status| |Version Status| |Downloads| Python tools for handling intervals (ranges of comparable objects). Interval initialization ----------------------- Intervals can be initialized using the class constructor, various factory methods or ``from_string`` class method. The recommended way is to use the factory methods. ========= =================== ================ Notation Definition Factory method ========= =================== ================ (a..b) {x | a < x < b} open [a..b] {x | a <= x <= b} closed (a..b] {x | a < x <= b} open_closed [a..b) {x | a <= x < b} closed_open (a..+∞) {x | x > a} greater_than [a..+∞) {x | x >= a} at_least (-∞..b) {x | x < b} less_than (-∞..b] {x | x <= b} at_most (-∞..+∞) {x} all ========= =================== ================ When both endpoints exist, the upper endpoint may not be less than the lower. The endpoints may be equal only if at least one of the bounds is closed: * [a..a]: a singleton range (contains only one value) * [a..a), (a..a]: empty ranges * (a..a): invalid; an ``IllegalArgument`` exception will be thrown .. code-block:: python >>> from intervals import IntInterval >>> interval = IntInterval.open_closed(1, 5) >>> interval.lower 1 >>> interval.upper 5 >>> interval.upper_inc True >>> interval = IntInterval.all() >>> interval.lower -inf >>> interval.upper inf The first argument of class constructor should define the bounds of the interval. .. code-block:: python >>> from intervals import IntInterval >>> # All integers between 1 and 4 >>> interval = IntInterval([1, 4]) >>> interval.lower 1 >>> interval.upper 4 >>> interval.lower_inc True >>> interval.upper_inc True You can also pass a scalar as the first constructor argument. .. code-block:: python >>> from intervals import IntInterval >>> # All integers between 1 and 4 >>> interval = IntInterval(1) >>> interval.lower 1 >>> interval.upper 1 Initializing an interval from string ------------------------------------ The ``from_string`` method accepts two different formats. 1. Standard string format .. code-block:: python >>> from intervals import IntInterval >>> # All integers between 1 and 4 >>> interval = IntInterval.from_string('[1, 4]') >>> interval.lower 1 >>> interval.upper 4 By using standard string format you can easily initialize half-open intervals. .. code-block:: python >>> from intervals import IntInterval >>> interval = IntInterval.from_string('[1, 4)') >>> interval.lower 1 >>> interval.upper 4 >>> interval.upper_inc False Unbounded intervals are supported as well. .. code-block:: python >>> from intervals import IntInterval >>> interval = IntInterval.from_string('[1, ]') >>> interval.lower 1 >>> interval.upper inf 2. Hyphenized format .. code-block:: python >>> from intervals import IntInterval >>> # All integers between 1 and 4 >>> interval = IntInterval.from_string('1 - 4') >>> interval.lower 1 >>> interval.upper 4 You can also initialize unbounded ranges. .. code-block:: python >>> from intervals import IntInterval >>> interval = IntInterval.from_string('1 - ') >>> interval.lower 1 >>> interval.upper inf Open, half-open and closed intervals ------------------------------------ Intervals can be either open, half-open or closed. Properties ``lower_inc`` and ``upper_inc`` denote whether or not given endpoint is included (open) or not. * An open interval is an interval where both endpoints are open. .. code-block:: python >>> interval = IntInterval((1, 4)) >>> interval.is_open True >>> interval.lower_inc False >>> interval.upper_inc False * Half-open interval has one of the endpoints as open .. code-block:: python >>> from intervals import Interval >>> interval = IntInterval.from_string('[1, 4)') >>> interval.is_open False >>> interval.lower_inc True >>> interval.upper_inc False * Closed interval includes both endpoints .. code-block:: python >>> interval = IntInterval.from_string('[1, 4]') >>> interval.is_closed True >>> interval.lower_inc True >>> interval.upper_inc True Unbounded intervals ------------------- Unbounded intervals are intervals where either one of the bounds is infinite. .. code-block:: python >>> from infinity import inf >>> from intervals import IntInterval >>> interval = IntInterval.closed_open(1, inf) >>> interval = IntInterval.open(-inf, inf) Interval types -------------- Each interval encapsulates a type. Interval is not actually a class. Its a convenient factory that generates ``AbstractInterval`` subclasses. Whenever you call ``Interval()`` the ``IntervalFactory`` tries to guess to best matching interval for given bounds. .. code-block:: python >>> from datetime import date >>> from infinity import inf >>> interval = Interval([1, 4]) >>> interval IntInterval('[1, 4]') >>> interval.type.__name__ 'int' >>> interval = Interval(['a', 'd']) >>> interval CharacterInterval('[a, d]') >>> interval.type.__name__ 'str' >>> interval = Interval([1.5, 4]) >>> interval FloatInterval('[1.5, 4.0]') >>> interval.type == type(5.5) True >>> interval = Interval([date(2000, 1, 1), inf]) >>> interval DateInterval('[2000-01-01,]') >>> interval.type.__name__ 'date' You can also create interval subtypes directly (this is also faster than using ``Interval``). .. code-block:: python >>> from intervals import FloatInterval, IntInterval >>> IntInterval([1, 4]) IntInterval('[1, 4]') >>> FloatInterval((1.4, 2.7)) FloatInterval('(1.4, 2.7)') Currently provided subtypes are: * ``IntInterval`` * ``CharacterInterval`` * ``FloatInterval`` * ``DecimalInterval`` * ``DateInterval`` * ``DateTimeInterval`` Properties ---------- * ``radius`` gives the half-length of an interval .. code-block:: python >>> IntInterval([1, 4]).radius 1.5 * ``length`` gives the length of an interval. .. code-block:: python >>> IntInterval([1, 4]).length 3 * ``centre`` gives the centre (midpoint) of an interval .. code-block:: python >>> IntInterval([-1, 1]).centre 0.0 * Interval :math:`[a, b]` is ``degenerate`` if :math:`a = b` .. code-block:: python >>> IntInterval([1, 1]).degenerate True >>> IntInterval([1, 2]).degenerate False Emptiness --------- An interval is empty if it contains no points: .. code-block:: python >>> IntInterval.from_string('(1, 1]').empty True Data type coercion ------------------ Interval evaluates as ``True`` if its non-empty .. code-block:: python >>> bool(IntInterval([1, 6])) True >>> bool(IntInterval([0, 0])) True >>> bool(IntInterval.from_string('(1, 1]')) False Integer intervals can be coerced to integer if they contain only one point, otherwise passing them to ``int()`` throws a ``TypeError`` .. code-block:: python >>> int(IntInterval([1, 6])) Traceback (most recent call last): ... TypeError: Only intervals containing single point can be coerced to integers >>> int(IntInterval([1, 1])) 1 Operators --------- Operator coercion rules ^^^^^^^^^^^^^^^^^^^^^^^ All the operators and arithmetic functions use special coercion rules. These rules are made for convenience. So for example when you type: .. code-block:: python IntInterval([1, 5]) > IntInterval([3, 3]) Its actually the same as typing: .. code-block:: python IntInterval([1, 5]) > [3, 3] Which is also the same as typing: .. code-block:: python IntInterval([1, 5]) > 3 Comparison operators ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python >>> IntInterval([1, 5]) > IntInterval([0, 3]) True >>> IntInterval([1, 5]) == IntInterval([1, 5]) True >>> IntInterval([2, 3]) in IntInterval([2, 6]) True >>> IntInterval([2, 3]) in IntInterval([2, 3]) True >>> IntInterval([2, 3]) in IntInterval((2, 3)) False Intervals are hashable ^^^^^^^^^^^^^^^^^^^^^^ Intervals are hashed on the same attributes that affect comparison: the values of the upper and lower bounds, ``lower_inc`` and ``upper_inc``, and the ``type`` of the interval. This enables the use of intervals as keys in dict() objects. .. code-block:: python >>> IntInterval([3, 7]) in {IntInterval([3, 7]): 'zero to ten'} True >>> IntInterval([3, 7]) in set([IntInterval([3, 7])]) True >>> IntInterval((3, 7)) in set([IntInterval([3, 7])]) False >>> IntInterval([3, 7]) in set([FloatInterval([3, 7])]) False Discrete intervals ------------------ .. code-block:: python >>> IntInterval([2, 4]) == IntInterval((1, 5)) True Using interval steps ^^^^^^^^^^^^^^^^^^^^ You can assign given interval to use optional ``step`` argument. By default ``IntInterval`` uses ``step=1``. When the interval encounters a value that is not a multiplier of the ``step`` argument it tries to round it to the nearest multiplier of the ``step``. .. code-block:: python >>> from intervals import IntInterval >>> interval = IntInterval([0, 5], step=2) >>> interval.lower 0 >>> interval.upper 6 You can also use steps for ``FloatInterval`` and ``DecimalInterval`` classes. Same rounding rules apply here. .. code-block:: python >>> from intervals import FloatInterval >>> interval = FloatInterval([0.2, 0.8], step=0.5) >>> interval.lower 0.0 >>> interval.upper 1.0 Arithmetics ----------- Arithmetic operators ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python >>> Interval([1, 5]) + Interval([1, 8]) IntInterval('[2, 13]') >>> Interval([1, 4]) - 1 IntInterval('[0, 3]') Intersection: .. code-block:: python >>> Interval([2, 6]) & Interval([3, 8]) IntInterval('[3, 6]') Union: .. code-block:: python >>> Interval([2, 6]) | Interval([3, 8]) IntInterval('[2, 8]') Arithmetic functions ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python >>> interval = IntInterval([1, 3]) >>> # greatest lower bound >>> interval.glb(IntInterval([1, 2])) IntInterval('[1, 2]') >>> # least upper bound >>> interval.lub(IntInterval([1, 2])) IntInterval('[1, 3]') >>> # infimum >>> interval.inf(IntInterval([1, 2])) IntInterval('[1, 2]') >>> # supremum >>> interval.sup(IntInterval([1, 2])) IntInterval('[1, 3]') .. |Build Status| image:: https://travis-ci.org/kvesteri/intervals.png?branch=master :target: https://travis-ci.org/kvesteri/intervals .. |Version Status| image:: https://img.shields.io/pypi/v/intervals.svg :target: https://pypi.python.org/pypi/intervals/ .. |Downloads| image:: https://img.shields.io/pypi/dm/intervals.svg :target: https://pypi.python.org/pypi/intervals/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals/0000755000076500000240000000000000000000000016161 5ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899592.0 intervals-0.9.0/intervals/__init__.py0000644000076500000240000000126100000000000020272 0ustar00konstastaff00000000000000# -*- coding: utf-8 -*- from .exc import IllegalArgument, IntervalException, RangeBoundsException from .interval import ( AbstractInterval, canonicalize, CharacterInterval, DateInterval, DateTimeInterval, DecimalInterval, FloatInterval, Interval, IntervalFactory, IntInterval, NumberInterval ) __all__ = ( 'AbstractInterval', 'CharacterInterval', 'canonicalize', 'DateInterval', 'DateTimeInterval', 'DecimalInterval', 'FloatInterval', 'Interval', 'IntervalException', 'IntervalFactory', 'IntInterval', 'IllegalArgument', 'NumberInterval', 'RangeBoundsException' ) __version__ = '0.9.0' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468174338.0 intervals-0.9.0/intervals/exc.py0000644000076500000240000000071700000000000017317 0ustar00konstastaff00000000000000class IntervalException(Exception): pass class RangeBoundsException(IntervalException): def __init__(self, min_value, max_value): super(RangeBoundsException, self).__init__( 'Min value %s is bigger than max value %s.' % ( min_value, max_value ) ) class IllegalArgument(IntervalException): def __init__(self, message): super(IntervalException, self).__init__(message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899592.0 intervals-0.9.0/intervals/interval.py0000644000076500000240000005161500000000000020367 0ustar00konstastaff00000000000000""" http://grouper.ieee.org/groups/1788/PositionPapers/ARITHYY.pdf http://grouper.ieee.org/groups/1788/PositionPapers/overlapping.pdf https://en.wikipedia.org/wiki/Interval_(mathematics) """ # -*- coding: utf-8 -*- import operator from datetime import date, datetime, timedelta from decimal import Decimal from math import ceil, floor from infinity import inf, is_infinite from .exc import IllegalArgument, IntervalException, RangeBoundsException from .parser import IntervalParser, IntervalStringParser try: string_types = basestring, # Python 2 except NameError: string_types = str, # Python 3 def is_number(number): return isinstance(number, (float, int, Decimal)) def py2round(value): """Round values as in Python 2, for Python 3 compatibility. All x.5 values are rounded away from zero. In Python 3, this has changed to avoid bias: when x is even, rounding is towards zero, when x is odd, rounding is away from zero. Thus, in Python 3, round(2.5) results in 2, round(3.5) is 4. Python 3 also returns an int; Python 2 returns a float. """ if value > 0: return float(floor(float(value)+0.5)) else: return float(ceil(float(value)-0.5)) def canonicalize_lower(interval, inc=True): if not interval.lower_inc and inc: return interval.lower + interval.step, True elif not inc and interval.lower_inc: return interval.lower - interval.step, False else: return interval.lower, interval.lower_inc def canonicalize_upper(interval, inc=False): if not interval.upper_inc and inc: return interval.upper - interval.step, True elif not inc and interval.upper_inc: return interval.upper + interval.step, False else: return interval.upper, interval.upper_inc def canonicalize(interval, lower_inc=True, upper_inc=False): """ Convert equivalent discrete intervals to different representations. """ if not interval.discrete: raise TypeError('Only discrete ranges can be canonicalized') if interval.empty: return interval lower, lower_inc = canonicalize_lower(interval, lower_inc) upper, upper_inc = canonicalize_upper(interval, upper_inc) return interval.__class__( [lower, upper], lower_inc=lower_inc, upper_inc=upper_inc, ) def coerce_interval(func): def wrapper(self, arg): if ( isinstance(arg, list) or isinstance(arg, tuple) or isinstance(arg, self.type) or isinstance(arg, type(self)) or arg == inf or arg == -inf ): try: if arg is not None: arg = type(self)(arg) return func(self, arg) except IntervalException: return NotImplemented try: arg = type(self)(self.type(arg)) except (ValueError, TypeError, OverflowError): pass return func(self, arg) return wrapper class AbstractInterval(object): step = None type = None parser = IntervalParser() def __init__( self, bounds, lower_inc=None, upper_inc=None, step=None ): """ Parse given args and assign lower and upper bound for this number range. 1. Comma separated string argument:: >>> range = IntInterval.from_string('[23, 45]') >>> range.lower 23 >>> range.upper 45 >>> range = IntInterval.from_string('(23, 45]') >>> range.lower_inc False >>> range = IntInterval.from_string('(23, 45)') >>> range.lower_inc False >>> range.upper_inc False 2. Lists and tuples as an argument:: >>> range = IntInterval([23, 45]) >>> range.lower 23 >>> range.upper 45 >>> range.is_closed True >>> range = IntInterval((23, 45)) >>> range.lower 23 >>> range.is_closed False 3. Integer argument:: >>> range = IntInterval(34) >>> range.lower == range.upper == 34 True 4. Object argument:: >>> range = IntInterval(IntInterval((20, 30))) >>> range.lower 20 >>> range.upper 30 """ if isinstance(bounds, string_types): raise TypeError( 'First argument should be a list or tuple. If you wish to ' 'initialize an interval from string, use from_string factory ' 'method.' ) if step is not None: self.step = step self.lower, self.upper, self.lower_inc, self.upper_inc = ( self.parser(bounds, lower_inc, upper_inc) ) if self.lower > self.upper: raise RangeBoundsException( self.lower, self.upper ) if ( self.lower == self.upper and not self.lower_inc and not self.upper_inc ): raise IllegalArgument( 'The bounds may be equal only if at least one of the bounds ' 'is closed.' ) @classmethod def open(cls, lower_bound, upper_bound, **kwargs): return cls( [lower_bound, upper_bound], lower_inc=False, upper_inc=False, **kwargs ) @classmethod def closed(cls, lower_bound, upper_bound, **kwargs): return cls( [lower_bound, upper_bound], lower_inc=True, upper_inc=True, **kwargs ) @classmethod def open_closed(cls, lower_bound, upper_bound, **kwargs): return cls( [lower_bound, upper_bound], lower_inc=False, upper_inc=True, **kwargs ) @classmethod def closed_open(cls, lower_bound, upper_bound, **kwargs): return cls( [lower_bound, upper_bound], lower_inc=True, upper_inc=False, **kwargs ) @classmethod def greater_than(cls, lower_bound, **kwargs): return cls( [lower_bound, inf], lower_inc=False, upper_inc=False, **kwargs ) @classmethod def at_least(cls, lower_bound, **kwargs): return cls( [lower_bound, inf], lower_inc=True, upper_inc=False, **kwargs ) @classmethod def less_than(cls, upper_bound, **kwargs): return cls( [-inf, upper_bound], lower_inc=False, upper_inc=False, **kwargs ) @classmethod def at_most(cls, upper_bound, **kwargs): return cls( [-inf, upper_bound], lower_inc=False, upper_inc=True, **kwargs ) @classmethod def all(cls, **kwargs): return cls( [-inf, inf], lower_inc=False, upper_inc=False, **kwargs ) @classmethod def from_string(cls, bounds_string, **kwargs): return cls( *IntervalStringParser().parse_string(bounds_string), **kwargs ) def copy_args(self, interval): self.lower_inc = interval.lower_inc self.upper_inc = interval.upper_inc self.lower = interval.lower self.upper = interval.upper self.type = interval.type def coerce_value(self, value): if value is None or value == '': return None elif is_infinite(value): return value elif isinstance(value, self.type): return value elif isinstance(value, string_types): return self.coerce_string(value) else: return self.coerce_obj(value) def coerce_string(self, value): return self.type(value) def coerce_obj(self, obj): return self.type(obj) @property def lower(self): return self._lower @lower.setter def lower(self, value): value = self.coerce_value(value) if value is None: self._lower = -inf else: self._lower = self.round_value_by_step(value) @property def upper(self): return self._upper @upper.setter def upper(self, value): value = self.coerce_value(value) if value is None: self._upper = inf else: self._upper = self.round_value_by_step(value) def round_value_by_step(self, value): return value @property def is_open(self): """ Return whether or not this object is an open interval. Examples:: >>> range = Interval.from_string('(23, 45)') >>> range.is_open True >>> range = Interval.from_string('[23, 45]') >>> range.is_open False """ return not self.lower_inc and not self.upper_inc @property def is_closed(self): """ Return whether or not this object is a closed interval. Examples:: >>> range = Interval.from_string('(23, 45)') >>> range.is_closed False >>> range = Interval.from_string('[23, 45]') >>> range.is_closed True """ return self.lower_inc and self.upper_inc def __str__(self): return '%s%s,%s%s' % ( '[' if self.lower_inc else '(', str(self.lower) if not is_infinite(self.lower) else '', ' ' + str(self.upper) if not is_infinite(self.upper) else '', ']' if self.upper_inc else ')' ) def equals(self, other): return ( self.lower == other.lower and self.upper == other.upper and self.lower_inc == other.lower_inc and self.upper_inc == other.upper_inc and self.type == other.type ) @coerce_interval def __eq__(self, other): try: if self.discrete: return canonicalize(self).equals(canonicalize(other)) return self.equals(other) except AttributeError: return NotImplemented def __hash__(self): return ( self.upper, self.lower, self.upper_inc, self.lower_inc, self.type ).__hash__() def __ne__(self, other): return not (self == other) @coerce_interval def __gt__(self, other): return self.lower > other.lower and self.upper > other.upper @coerce_interval def __lt__(self, other): return self.lower < other.lower and self.upper < other.upper @coerce_interval def __ge__(self, other): return self == other or self > other @coerce_interval def __le__(self, other): return self == other or self < other @coerce_interval def __contains__(self, other): lower_op = ( operator.le if self.lower_inc or (not self.lower_inc and not other.lower_inc) else operator.lt ) upper_op = ( operator.ge if self.upper_inc or (not self.upper_inc and not other.upper_inc) else operator.gt ) return ( lower_op(self.lower, other.lower) and upper_op(self.upper, other.upper) ) @property def discrete(self): """ Return whether or not this interval is discrete. """ return self.step is not None @property def length(self): return abs(self.upper - self.lower) @property def radius(self): if self.length == inf: return inf return float(self.length) / 2 @property def degenerate(self): """ An interval is considered degenerate if it only contains one item or if it is empty. """ return self.upper == self.lower @property def empty(self): if self.discrete and not self.degenerate: return ( self.upper - self.lower == self.step and not (self.upper_inc or self.lower_inc) ) return ( self.upper == self.lower and not (self.lower_inc and self.upper_inc) ) def __bool__(self): return not self.empty def __nonzero__(self): return not self.empty @property def centre(self): return float((self.lower + self.upper)) / 2 def __repr__(self): return "%s(%r)" % (self.__class__.__name__, str(self)) @property def hyphenized(self): if not self.discrete: raise TypeError('Only discrete intervals have hyphenized format.') c = canonicalize(self, True, True) if c.lower != c.upper: return '%s -%s' % ( str(c.lower) if not is_infinite(c.lower) else '', ' ' + str(c.upper) if not is_infinite(c.upper) else '' ) return str(c.lower) @coerce_interval def __add__(self, other): """ [a, b] + [c, d] = [a + c, b + d] """ return self.__class__( [ self.lower + other.lower, self.upper + other.upper ], lower_inc=self.lower_inc if self < other else other.lower_inc, upper_inc=self.upper_inc if self > other else other.upper_inc, ) __radd__ = __add__ @coerce_interval def __sub__(self, other): """ Define the substraction operator. [a, b] - [c, d] = [a - d, b - c] """ return self.__class__([ self.lower - other.upper, self.upper - other.lower ]) @coerce_interval def glb(self, other): """ Return the greatest lower bound for given intervals. :param other: AbstractInterval instance """ return self.__class__( [ min(self.lower, other.lower), min(self.upper, other.upper) ], lower_inc=self.lower_inc if self < other else other.lower_inc, upper_inc=self.upper_inc if self > other else other.upper_inc, ) @coerce_interval def lub(self, other): """ Return the least upper bound for given intervals. :param other: AbstractInterval instance """ return self.__class__( [ max(self.lower, other.lower), max(self.upper, other.upper), ], lower_inc=self.lower_inc if self < other else other.lower_inc, upper_inc=self.upper_inc if self > other else other.upper_inc, ) @coerce_interval def inf(self, other): """ Return the infimum of given intervals. This is the same as intersection. :param other: AbstractInterval instance """ return self & other @coerce_interval def sup(self, other): """ Return the supremum of given intervals. This is the same as union. :param other: AbstractInterval instance """ return self | other @coerce_interval def __rsub__(self, other): return self.__class__([ other.lower - self.upper, other.upper - self.lower ]) def __and__(self, other): """ Define the intersection operator """ if not self.is_connected(other): raise IllegalArgument( 'Intersection is only supported for connected intervals.' ) lower = max(self.lower, other.lower) upper = min(self.upper, other.upper) if self.lower < other.lower: lower_inc = other.lower_inc elif self.lower > other.lower: lower_inc = self.lower_inc else: lower_inc = self.lower_inc and other.lower_inc if self.upper > other.upper: upper_inc = other.upper_inc elif self.upper < other.upper: upper_inc = self.upper_inc else: upper_inc = self.upper_inc and other.upper_inc return self.__class__( [lower, upper], lower_inc=lower_inc, upper_inc=upper_inc ) def __or__(self, other): """ Define the union operator """ if not self.is_connected(other): raise IllegalArgument('Union is not continuous.') lower = min(self.lower, other.lower) upper = max(self.upper, other.upper) if self.lower < other.lower: lower_inc = self.lower_inc elif self.lower > other.lower: lower_inc = other.lower_inc else: lower_inc = self.lower_inc or other.lower_inc if self.upper > other.upper: upper_inc = self.upper_inc elif self.upper < other.upper: upper_inc = other.upper_inc else: upper_inc = self.upper_inc or other.upper_inc return self.__class__( [lower, upper], lower_inc=lower_inc, upper_inc=upper_inc ) def is_connected(self, other): """ Returns ``True`` if there exists a (possibly empty) range which is enclosed by both this range and other. Examples: * [1, 3] and [5, 7] are not connected * [5, 7] and [1, 3] are not connected * [2, 4) and [3, 5) are connected, because both enclose [3, 4) * [1, 3) and [3, 5) are connected, because both enclose the empty range [3, 3) * [1, 3) and (3, 5) are not connected """ return self.upper > other.lower and other.upper > self.lower or ( self.upper == other.lower and (self.upper_inc or other.lower_inc) ) or ( self.lower == other.upper and (self.lower_inc or other.upper_inc) ) class NumberInterval(AbstractInterval): rounding_type = Decimal def round_value_by_step(self, value): if self.step and not is_infinite(value): return self.type( self.rounding_type(self.step) * self.rounding_type( py2round( self.rounding_type('1.0') / self.rounding_type(self.step) * self.rounding_type(value) ) ) ) return value class IntInterval(NumberInterval): step = 1 type = int def coerce_obj(self, obj): if isinstance(obj, float) or isinstance(obj, Decimal): if str(int(obj)) != str(obj): raise IntervalException( 'Could not coerce %s to int. Decimal places would ' 'be lost.' ) return int(obj) return obj def __int__(self): if self.empty: raise TypeError('Empty intervals cannot be coerced to integers') if self.lower != self.upper: raise TypeError( 'Only intervals containing single point can be coerced to' ' integers' ) return self.lower class DateInterval(AbstractInterval): step = timedelta(days=1) type = date class DateTimeInterval(AbstractInterval): type = datetime class FloatInterval(NumberInterval): type = float rounding_type = float class DecimalInterval(NumberInterval): type = Decimal def round_value_by_step(self, value): if self.step and not is_infinite(value): return self.type(str( float(self.step) * py2round(1.0 / float(self.step) * float(value)) )) return value class CharacterInterval(AbstractInterval): type = str def coerce_obj(self, obj): if not isinstance(obj, str): raise IntervalException('Type %s is not a string.') return obj class IntervalFactory(object): interval_classes = [ CharacterInterval, IntInterval, FloatInterval, DecimalInterval, DateTimeInterval, DateInterval, ] def __call__(self, bounds, lower_inc=None, upper_inc=None, step=None): for interval_class in self.interval_classes: try: return interval_class( bounds, lower_inc=lower_inc, upper_inc=upper_inc, step=step ) except (IntervalException, TypeError): pass raise IntervalException( 'Could not initialize interval.' ) @classmethod def from_string(self, value): for interval_class in self.interval_classes: try: return interval_class.from_string(value) except (IntervalException, TypeError): pass raise IntervalException( 'Could not initialize interval.' ) Interval = IntervalFactory() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1453997631.0 intervals-0.9.0/intervals/parser.py0000644000076500000240000000512600000000000020033 0ustar00konstastaff00000000000000from .exc import IntervalException try: string_types = basestring, # Python 2 except NameError: string_types = str, # Python 3 strip = lambda a: a.strip() class IntervalStringParser(object): def parse_string(self, value): if ',' not in value: return self.parse_hyphen_range(value) else: return self.parse_bounded_range(value) def parse_bounded_range(self, value): values = value.strip()[1:-1].split(',') lower, upper = map(strip, values) return ( [lower, upper], value[0] == '[', value[-1] == ']' ) def parse_hyphen_range(self, value): """ Parse hyphen ranges such as: 2 - 5, -2 - -1, -3 - 5 """ values = value.strip().split('-') values = list(map(strip, values)) if len(values) == 1: lower = upper = value.strip() elif len(values) == 2: lower, upper = values if lower == '': # Parse range such as '-3' upper = '-' + upper lower = upper else: if len(values) > 4: raise IntervalException( 'Unknown interval format given.' ) values_copy = [] for key, value in enumerate(values): if value != '': try: if values[key - 1] == '': value = '-' + value except IndexError: pass values_copy.append(value) lower, upper = values_copy return [lower, upper], True, True class IntervalParser(object): def parse_object(self, obj): return obj.lower, obj.upper, obj.lower_inc, obj.upper_inc def parse_sequence(self, seq): lower, upper = seq if isinstance(seq, tuple): return lower, upper, False, False else: return lower, upper, True, True def parse_single_value(self, value): return value, value, True, True def __call__(self, bounds, lower_inc=None, upper_inc=None): if isinstance(bounds, (list, tuple)): values = self.parse_sequence(bounds) elif hasattr(bounds, 'lower') and hasattr(bounds, 'upper'): values = self.parse_object(bounds) else: values = self.parse_single_value(bounds) values = list(values) if lower_inc is not None: values[2] = lower_inc if upper_inc is not None: values[3] = upper_inc return values ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/0000755000076500000240000000000000000000000017653 5ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/PKG-INFO0000644000076500000240000000215700000000000020755 0ustar00konstastaff00000000000000Metadata-Version: 2.1 Name: intervals Version: 0.9.0 Summary: Python tools for handling intervals (ranges of comparable objects). Home-page: https://github.com/kvesteri/intervals Author: Konsta Vesterinen Author-email: konsta@fastmonkeys.com License: BSD Description: intervals --------- Python tools for handling intervals (ranges of comparable objects). Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides-Extra: test ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/SOURCES.txt0000644000076500000240000000132500000000000021540 0ustar00konstastaff00000000000000CHANGES.rst LICENSE MANIFEST.in README.rst setup.py intervals/__init__.py intervals/exc.py intervals/interval.py intervals/parser.py intervals.egg-info/PKG-INFO intervals.egg-info/SOURCES.txt intervals.egg-info/dependency_links.txt intervals.egg-info/not-zip-safe intervals.egg-info/requires.txt intervals.egg-info/top_level.txt tests/__init__.py tests/interval/__init__.py tests/interval/test_arithmetic.py tests/interval/test_canonicalize.py tests/interval/test_coercion.py tests/interval/test_factory_methods.py tests/interval/test_initialization.py tests/interval/test_operators.py tests/interval/test_properties.py tests/interval/test_representations.py tests/interval/test_step.py tests/interval/test_utility_methods.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/dependency_links.txt0000644000076500000240000000000100000000000023721 0ustar00konstastaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1389278467.0 intervals-0.9.0/intervals.egg-info/not-zip-safe0000644000076500000240000000000100000000000022101 0ustar00konstastaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/requires.txt0000644000076500000240000000011700000000000022252 0ustar00konstastaff00000000000000infinity>=0.1.3 [test] pytest>=2.2.3 Pygments>=1.2 flake8>=2.4.0 isort>=4.2.2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/intervals.egg-info/top_level.txt0000644000076500000240000000001200000000000022376 0ustar00konstastaff00000000000000intervals ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/setup.cfg0000644000076500000240000000004600000000000015773 0ustar00konstastaff00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899592.0 intervals-0.9.0/setup.py0000644000076500000240000000354300000000000015671 0ustar00konstastaff00000000000000""" intervals --------- Python tools for handling intervals (ranges of comparable objects). """ from setuptools import setup import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) PY3 = sys.version_info[0] == 3 def get_version(): filename = os.path.join(HERE, 'intervals', '__init__.py') with open(filename) as f: contents = f.read() pattern = r"^__version__ = '(.*?)'$" return re.search(pattern, contents, re.MULTILINE).group(1) extras_require = { 'test': [ 'pytest>=2.2.3', 'Pygments>=1.2', 'flake8>=2.4.0', 'isort>=4.2.2', ], } setup( name='intervals', version=get_version(), url='https://github.com/kvesteri/intervals', license='BSD', author='Konsta Vesterinen', author_email='konsta@fastmonkeys.com', description=( 'Python tools for handling intervals (ranges of comparable objects).' ), long_description=__doc__, packages=['intervals'], zip_safe=False, include_package_data=True, platforms='any', dependency_links=[], install_requires=[ 'infinity>=0.1.3', ], extras_require=extras_require, classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/tests/0000755000076500000240000000000000000000000015314 5ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1403622357.0 intervals-0.9.0/tests/__init__.py0000644000076500000240000000000000000000000017413 0ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899612.0 intervals-0.9.0/tests/interval/0000755000076500000240000000000000000000000017140 5ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1413831913.0 intervals-0.9.0/tests/interval/__init__.py0000644000076500000240000000000000000000000021237 0ustar00konstastaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468143627.0 intervals-0.9.0/tests/interval/test_arithmetic.py0000644000076500000240000000423300000000000022704 0ustar00konstastaff00000000000000from pytest import mark from intervals import IntInterval class TestArithmeticOperators(object): @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [2, 5]), (IntInterval([1, 3]), 1, [2, 4]), (1, IntInterval([1, 3]), [2, 4]), ([1, 2], IntInterval([1, 2]), [2, 4]) )) def test_add_operator(self, first, second, result): assert first + second == IntInterval(result) @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [-1, 2]), (IntInterval([1, 3]), 1, [0, 2]), (1, IntInterval([1, 3]), [-2, 0]) )) def test_sub_operator(self, first, second, result): assert first - second == IntInterval(result) def test_isub_operator(self): range_ = IntInterval([1, 3]) range_ -= IntInterval([1, 2]) assert range_ == IntInterval([-1, 2]) def test_iadd_operator(self): range_ = IntInterval([1, 2]) range_ += IntInterval([1, 2]) assert range_ == IntInterval([2, 4]) class TestArithmeticFunctions(object): @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [1, 2]), (IntInterval([-2, 2]), 1, [-2, 1]) )) def test_glb(self, first, second, result): assert first.glb(second) == IntInterval(result) @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [1, 3]), (IntInterval([-2, 2]), 1, [1, 2]) )) def test_lub(self, first, second, result): assert first.lub(second) == IntInterval(result) @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [1, 2]), (IntInterval([-2, 2]), 1, [1, 1]) )) def test_inf(self, first, second, result): assert first.inf(second) == IntInterval(result) @mark.parametrize(('first', 'second', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 2]), [1, 3]), (IntInterval([-2, 2]), 1, [-2, 2]) )) def test_sup(self, first, second, result): assert first.sup(second) == IntInterval(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1456424846.0 intervals-0.9.0/tests/interval/test_canonicalize.py0000644000076500000240000000107200000000000023210 0ustar00konstastaff00000000000000from datetime import date from intervals import canonicalize, DateInterval, IntInterval def test_canonicalize_integer_intervals(): assert str(canonicalize(IntInterval([1, 4]))) == '[1, 5)' assert str(canonicalize( IntInterval((1, 7)), lower_inc=True, upper_inc=True )) == '[2, 6]' assert str(canonicalize( IntInterval([1, 7]), lower_inc=False, upper_inc=True )) == '(0, 7]' def test_canonicalize_date_intervals(): interval = canonicalize(DateInterval([date(2000, 2, 2), date(2000, 2, 6)])) assert interval.upper.day == 7 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899275.0 intervals-0.9.0/tests/interval/test_coercion.py0000644000076500000240000000266100000000000022357 0ustar00konstastaff00000000000000from infinity import inf from pytest import mark, raises from intervals import IntInterval @mark.parametrize(('interval', 'string'), ( ((1, 3), '(1, 3)'), ([1, 1], '[1, 1]'), ([1, inf], '[1,]') )) def test_str_representation(interval, string): assert str(IntInterval(interval)) == string @mark.parametrize( ('number_range', 'empty'), ( (IntInterval((2, 3)), True), (IntInterval([2, 3]), False), (IntInterval([2, 2]), False), (IntInterval.from_string('[2, 2)'), True), (IntInterval.from_string('(2, 2]'), True), (IntInterval.from_string('[2, 3)'), False), (IntInterval((2, 10)), False), ) ) def test_bool(number_range, empty): assert bool(IntInterval(number_range)) != empty @mark.parametrize( ('number_range', 'coerced_value'), ( ([5, 5], 5), ([2, 2], 2), ) ) def test_int_with_single_point_interval(number_range, coerced_value): assert int(IntInterval(number_range)) == coerced_value @mark.parametrize( ('number_range'), ( '[2, 2)', '(2, 2]', ) ) def test_int_with_empty_interval(number_range): with raises(TypeError): int(IntInterval.from_string(number_range)) @mark.parametrize( ('number_range'), ( [2, 4], [2, 5], ) ) def test_int_with_interval_containing_multiple_points(number_range): with raises(TypeError): int(IntInterval(number_range)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1456508149.0 intervals-0.9.0/tests/interval/test_factory_methods.py0000644000076500000240000000452400000000000023750 0ustar00konstastaff00000000000000from infinity import inf from intervals import IntInterval class TestFactoryMethods(object): def test_open(self): interval = IntInterval.open(12, 14, step=2) assert interval.lower == 12 assert interval.upper == 14 assert interval.is_open assert interval.step == 2 def test_closed(self): interval = IntInterval.closed(10, 11, step=2) assert interval.lower == 10 assert interval.upper == 12 assert interval.is_closed assert interval.step == 2 def test_open_closed(self): interval = IntInterval.open_closed(10, 14, step=2) assert interval.lower == 10 assert interval.upper == 14 assert not interval.lower_inc assert interval.upper_inc assert interval.step == 2 def test_closed_open(self): interval = IntInterval.closed_open(10, 14, step=2) assert interval.lower == 10 assert interval.upper == 14 assert interval.lower_inc assert not interval.upper_inc assert interval.step == 2 def test_greater_than(self): interval = IntInterval.greater_than(10, step=2) assert interval.lower == 10 assert interval.upper == inf assert not interval.lower_inc assert not interval.upper_inc assert interval.step == 2 def test_at_least(self): interval = IntInterval.at_least(10, step=2) assert interval.lower == 10 assert interval.upper == inf assert interval.lower_inc assert not interval.upper_inc assert interval.step == 2 def test_less_than(self): interval = IntInterval.less_than(10, step=2) assert interval.lower == -inf assert interval.upper == 10 assert not interval.lower_inc assert not interval.upper_inc assert interval.step == 2 def test_at_most(self): interval = IntInterval.at_most(10, step=2) assert interval.lower == -inf assert interval.upper == 10 assert not interval.lower_inc assert interval.upper_inc assert interval.step == 2 def test_all(self): interval = IntInterval.all(step=2) assert interval.lower == -inf assert interval.upper == inf assert not interval.lower_inc assert not interval.upper_inc assert interval.step == 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468174443.0 intervals-0.9.0/tests/interval/test_initialization.py0000644000076500000240000001453700000000000023612 0ustar00konstastaff00000000000000from datetime import date from decimal import Decimal from infinity import inf from pytest import mark, raises from intervals import ( CharacterInterval, DecimalInterval, FloatInterval, IllegalArgument, Interval, IntInterval, RangeBoundsException ) class TestIntervalInit(object): def test_string_as_constructor_param(self): with raises(TypeError) as e: FloatInterval('(0.2, 0.5)') assert ( 'First argument should be a list or tuple. If you wish to ' 'initialize an interval from string, use from_string factory ' 'method.' ) in str(e) def test_invalid_argument(self): with raises(IllegalArgument) as e: FloatInterval((0, 0)) assert ( 'The bounds may be equal only if at least one of the bounds is ' 'closed.' ) in str(e) def test_floats(self): interval = FloatInterval((0.2, 0.5)) assert interval.lower == 0.2 assert interval.upper == 0.5 assert not interval.lower_inc assert not interval.upper_inc def test_decimals(self): interval = DecimalInterval((Decimal('0.2'), Decimal('0.5'))) assert interval.lower == Decimal('0.2') assert interval.upper == Decimal('0.5') assert not interval.lower_inc assert not interval.upper_inc def test_support_range_object(self): interval = IntInterval(IntInterval((1, 3))) assert interval.lower == 1 assert interval.upper == 3 assert not interval.lower_inc assert not interval.upper_inc def test_supports_strings(self): interval = IntInterval.from_string('1-3') assert interval.lower == 1 assert interval.upper == 3 assert interval.lower_inc assert interval.upper_inc def test_supports_infinity(self): interval = IntInterval((-inf, inf)) assert interval.lower == -inf assert interval.upper == inf assert not interval.lower_inc assert not interval.upper_inc def test_supports_strings_with_spaces(self): interval = IntInterval.from_string('1 - 3') assert interval.lower == 1 assert interval.upper == 3 assert interval.lower_inc assert interval.upper_inc def test_supports_strings_with_bounds(self): interval = IntInterval.from_string('[1, 3]') assert interval.lower == 1 assert interval.upper == 3 assert interval.lower_inc assert interval.upper_inc def test_step_argument_for_from_string(self): interval = IntInterval.from_string('[2,)', step=2) assert interval.lower == 2 assert interval.upper == inf assert interval.step == 2 def test_empty_string_as_upper_bound(self): interval = IntInterval.from_string('[1,)') assert interval.lower == 1 assert interval.upper == inf assert interval.lower_inc assert not interval.upper_inc def test_empty_string_as_lower_bound(self): interval = IntInterval.from_string('[,1)') assert interval.lower == -inf assert interval.upper == 1 assert interval.lower_inc assert not interval.upper_inc def test_supports_exact_ranges_as_strings(self): interval = IntInterval.from_string('3') assert interval.lower == 3 assert interval.upper == 3 assert interval.lower_inc assert interval.upper_inc def test_supports_integers(self): interval = IntInterval(3) assert interval.lower.__class__ == int assert interval.lower == 3 assert interval.upper == 3 assert interval.lower_inc assert interval.upper_inc def test_supports_character_intervals(self): interval = CharacterInterval(('a', 'z')) assert interval.lower == 'a' assert interval.upper == 'z' assert not interval.lower_inc assert not interval.upper_inc def test_supports_characters_from_strings(self): interval = CharacterInterval.from_string('A-Z') assert interval.lower == 'A' assert interval.upper == 'Z' assert interval.lower_inc assert interval.upper_inc def test_supports_characters_with_spaces(self): interval = CharacterInterval.from_string('A - Z') assert interval.lower == 'A' assert interval.upper == 'Z' assert interval.lower_inc assert interval.upper_inc def test_empty_string_as_upper_character_bound(self): interval = CharacterInterval.from_string('[a,)') assert interval.lower == 'a' assert interval.upper == inf assert interval.lower_inc assert not interval.upper_inc def test_empty_string_as_lower_bound_for_char_interval(self): interval = CharacterInterval.from_string('[,a)') assert interval.lower == -inf assert interval.upper == 'a' assert interval.lower_inc assert not interval.upper_inc @mark.parametrize( ('number_range', 'lower', 'upper'), ( ('-2-2', -2, 2), ('-3--2', -3, -2), ('2-3', 2, 3), ('2-', 2, inf), ('-5', -5, -5) ) ) def test_hyphen_format(self, number_range, lower, upper): interval = IntInterval.from_string(number_range) assert interval.lower == lower assert interval.upper == upper @mark.parametrize( ('constructor', 'number_range'), ( (IntInterval, (3, 2)), (IntInterval, [4, 2]), (IntInterval, (float('inf'), 2)), (CharacterInterval, ('c', 'b')), (CharacterInterval, ('d', 'b')), (CharacterInterval, (inf, 'b')), ) ) def test_raises_exception_for_badly_constructed_range( self, constructor, number_range ): with raises(RangeBoundsException): constructor(number_range) class TestTypeGuessing(object): @mark.parametrize( ('number_range', 'type'), ( ((2, 3), int), ([-6, 8], int), (8.5, float), ([Decimal(2), 9], int), ([Decimal('0.5'), 9], float), ([date(2000, 1, 1), inf], date), (('a', 'e'), str), ) ) def test_guesses_types(self, number_range, type): assert Interval(number_range).type == type ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594899592.0 intervals-0.9.0/tests/interval/test_operators.py0000644000076500000240000001733400000000000022577 0ustar00konstastaff00000000000000from datetime import date from infinity import inf from pytest import mark, raises from intervals import DateInterval, FloatInterval, IllegalArgument, IntInterval class TestComparisonOperators(object): @mark.parametrize(('comparison', 'result'), ( (IntInterval([1, 3]) == IntInterval([1, 3]), True), (IntInterval([1, 3]) == IntInterval([1, 4]), False), (IntInterval([inf, inf]) == inf, True), (IntInterval([3, 3]) == 3, True), (IntInterval([3, 3]) == 5, False), (IntInterval([3, 3]) == 'something', False), ( IntInterval([3, 3]) == DateInterval([date(2011, 1, 1), date(2011, 1, 1)]), False ), (IntInterval.from_string('(,)') == None, False), # noqa ( DateInterval(date(2000, 1, 1), date(2001, 1, 1)) == -12312321312312312312123123, False ) )) def test_eq_operator(self, comparison, result): assert comparison is result @mark.parametrize(('comparison', 'result'), ( (IntInterval([1, 3]) != IntInterval([1, 3]), False), (IntInterval([1, 3]) != IntInterval([1, 4]), True), (IntInterval([inf, inf]) != inf, False), (IntInterval([3, 3]) != 3, False), (IntInterval([3, 3]) != 5, True), (IntInterval([3, 3]) != 'something', True), (IntInterval.from_string('(,)') != None, True) # noqa )) def test_ne_operator(self, comparison, result): assert comparison is result @mark.parametrize(('comparison', 'result'), ( (IntInterval([1, 3]) > IntInterval([0, 2]), True), (IntInterval((1, 4)) > 1, False), (IntInterval((1, 6)) > [1, 6], False), (IntInterval((1, 6)) > 0, True) )) def test_gt_operator(self, comparison, result): assert comparison is result @mark.parametrize(('comparison', 'result'), ( (IntInterval([1, 3]) >= IntInterval([0, 2]), True), (IntInterval((1, 4)) >= 1, False), (IntInterval((1, 6)) >= [1, 6], False), (IntInterval((1, 6)) >= 0, True) )) def test_ge_operator(self, comparison, result): assert comparison is result @mark.parametrize(('comparison', 'result'), ( (IntInterval([0, 2]) < IntInterval([1, 3]), True), (IntInterval([2, 3]) < IntInterval([2, 3]), False), (IntInterval([2, 5]) < 6, True), (IntInterval([2, 5]) < 5, False), (IntInterval([2, 5]) < inf, True) )) def test_lt_operator(self, comparison, result): assert comparison is result @mark.parametrize(('comparison', 'result'), ( (IntInterval([0, 2]) <= IntInterval([1, 3]), True), (IntInterval([1, 3]) <= IntInterval([1, 3]), True), (IntInterval([1, 7]) <= 8, True), (IntInterval([1, 6]) <= 5, False), (IntInterval([1, 5]) <= inf, True) )) def test_le_operator(self, comparison, result): assert comparison is result def test_integer_comparison(self): assert IntInterval([2, 2]) <= 3 assert IntInterval([1, 3]) >= 0 assert IntInterval([2, 2]) == 2 assert IntInterval([2, 2]) != 3 @mark.parametrize('value', ( IntInterval([0, 2]), 1, 1.0, (-1, 1), )) def test_contains_operator_for_inclusive_interval(self, value): assert value in IntInterval([-1, 2]) @mark.parametrize('value', ( IntInterval([0, 2]), 2, [-1, 1], )) def test_contains_operator_for_non_inclusive_interval(self, value): assert value not in IntInterval((-1, 2)) @mark.parametrize(('interval1', 'interval2', 'expected'), ( (IntInterval((0, 2)), IntInterval((0, 2)), True), (IntInterval([0, 2]), IntInterval([0, 2]), True), ( IntInterval.from_string('[0, 2)'), IntInterval.from_string('[0, 2)'), True ), ( IntInterval.from_string('(0, 2]'), IntInterval.from_string('(0, 2]'), True ), (IntInterval((0, 2)), IntInterval((1, 2)), False), (IntInterval((0, 2)), IntInterval((0, 1)), False), (IntInterval((0, 2)), IntInterval([0, 1]), False), (IntInterval((0, 2)), FloatInterval((0, 1)), False), )) def test_hash_operator_with_interval_attributes( self, interval1, interval2, expected ): actual = (interval1.__hash__() == interval2.__hash__()) assert actual == expected @mark.parametrize(('contains_check', 'expected'), ( (IntInterval([0, 2]) in {IntInterval([0, 2]): ''}, True), (IntInterval([0, 2]) in {IntInterval((0, 2)): ''}, False), (IntInterval([0, 2]) in set([IntInterval([0, 2])]), True), )) def test_hash_operator_with_collections(self, contains_check, expected): assert contains_check is expected class TestDiscreteRangeComparison(object): @mark.parametrize(('interval', 'interval2'), ( ('[1, 3]', '[1, 4)'), ('(1, 5]', '[2, 5]'), ('(1, 6)', '[2, 5]'), )) def test_eq_operator(self, interval, interval2): assert ( IntInterval.from_string(interval) == IntInterval.from_string(interval2) ) class TestBinaryOperators(object): @mark.parametrize(('interval1', 'interval2', 'result'), ( ('(2, 3]', '[3, 4)', IntInterval([3, 3])), ('(2, 10]', '[3, 40]', IntInterval([3, 10])), ('[0, 0]', '[0, 0]', IntInterval.from_string('[0, 0]')), ('[0, 0]', '(0, 0]', IntInterval.from_string('(0, 0]')), ('[0, 0)', '[0, 0]', IntInterval.from_string('[0, 0)')), ('(2, 2]', '(2, 2]', IntInterval.from_string('(2, 2]')), ('[2, 2)', '[2, 2)', IntInterval.from_string('[2, 2)')), ('(2, 3)', '[3, 4]', IntInterval.from_string('[3, 3)')), ('[2, 3]', '(3, 4)', IntInterval.from_string('(3, 3]')), )) def test_and_operator( self, interval1, interval2, result ): assert ( IntInterval.from_string(interval1) & IntInterval.from_string(interval2) == result ) @mark.parametrize(('interval1', 'interval2'), ( ('[2, 2]', '[5, 7)'), ('(2, 3]', '[4, 40]'), ('(2, 3)', '(3, 4)'), )) def test_and_operator_with_illegal_arguments(self, interval1, interval2): with raises(IllegalArgument): ( IntInterval.from_string(interval1) & IntInterval.from_string(interval2) ) @mark.parametrize(('interval1', 'interval2'), ( ('[2, 2]', '[5, 7)'), ('(2, 3]', '[4, 40]'), ('(2, 3)', '(3, 4)'), )) def test_or_operator_with_illegal_arguments(self, interval1, interval2): with raises(IllegalArgument): ( IntInterval.from_string(interval1) | IntInterval.from_string(interval2) ) @mark.parametrize(('interval1', 'interval2', 'result'), ( ('(2, 3]', '[3, 4)', IntInterval.from_string('(2, 4)')), ('(2, 10]', '[3, 40]', IntInterval.from_string('(2, 40]')), ('[0, 0]', '[0, 0]', IntInterval.from_string('[0, 0]')), ('[0, 0]', '(0, 0]', IntInterval.from_string('[0, 0]')), ('[0, 0)', '[0, 0]', IntInterval.from_string('[0, 0]')), ('(2, 2]', '(2, 2]', IntInterval.from_string('(2, 2]')), ('[2, 2)', '[2, 2)', IntInterval.from_string('[2, 2)')), ('(2, 3)', '[3, 4]', IntInterval.from_string('(2, 4]')), ('[2, 3]', '(3, 4)', IntInterval.from_string('[2, 4)')), )) def test_or_operator( self, interval1, interval2, result ): assert ( IntInterval.from_string(interval1) | IntInterval.from_string(interval2) == result ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468170975.0 intervals-0.9.0/tests/interval/test_properties.py0000644000076500000240000000701300000000000022746 0ustar00konstastaff00000000000000from datetime import date, datetime from decimal import Decimal from infinity import inf from pytest import mark from intervals import ( DateInterval, DateTimeInterval, DecimalInterval, FloatInterval, IntInterval ) class TestIntervalProperties(object): @mark.parametrize( ('number_range', 'length'), ( ([1, 4], 3), ([-1, 1], 2), ((-inf, inf), inf), ((1, inf), inf), ) ) def test_length(self, number_range, length): assert IntInterval(number_range).length == length @mark.parametrize( ('number_range', 'radius'), ( ([1, 4], 1.5), ([-1, 1], 1.0), ([-4, -1], 1.5), ((-inf, inf), inf), ((1, inf), inf), ) ) def test_radius(self, number_range, radius): assert IntInterval(number_range).radius == radius @mark.parametrize( ('number_range', 'centre'), ( ([1, 4], 2.5), ([-1, 1], 0), ([-4, -1], -2.5), ((1, inf), inf), ) ) def test_centre(self, number_range, centre): assert IntInterval(number_range).centre == centre @mark.parametrize( ('interval', 'is_open'), ( (IntInterval((2, 3)), True), (IntInterval.from_string('(2, 5)'), True), (IntInterval.from_string('[3, 4)'), False), (IntInterval.from_string('(4, 5]'), False), (IntInterval.from_string('3 - 4'), False), (IntInterval([4, 5]), False), (IntInterval.from_string('[4, 5]'), False) ) ) def test_is_open(self, interval, is_open): assert interval.is_open == is_open @mark.parametrize( ('interval', 'is_closed'), ( (IntInterval((2, 3)), False), (IntInterval.from_string('(2, 5)'), False), (IntInterval.from_string('[3, 4)'), False), (IntInterval.from_string('(4, 5]'), False), (IntInterval.from_string('3 - 4'), True), (IntInterval([4, 5]), True), (IntInterval.from_string('[4, 5]'), True) ) ) def test_closed(self, interval, is_closed): assert interval.is_closed == is_closed @mark.parametrize( ('interval', 'empty'), ( (IntInterval((2, 3)), True), (IntInterval([2, 3]), False), (IntInterval([2, 2]), False), (IntInterval.from_string('[2, 2)'), True), (IntInterval.from_string('(2, 2]'), True), (IntInterval.from_string('[2, 3)'), False), (IntInterval((2, 10)), False), ) ) def test_empty(self, interval, empty): assert interval.empty == empty @mark.parametrize( ('interval', 'degenerate'), ( (IntInterval((2, 4)), False), (IntInterval.from_string('[2, 2]'), True), (IntInterval.from_string('[0, 0)'), True), ) ) def test_degenerate(self, interval, degenerate): assert interval.degenerate == degenerate @mark.parametrize( ('interval', 'discrete'), ( (IntInterval((2, 3)), True), (IntInterval(5), True), (FloatInterval(3.5), False), (DecimalInterval(Decimal('2.4')), False), (DateTimeInterval(datetime(2002, 1, 1)), False), (DateInterval(date(2002, 1, 1)), True) ) ) def test_discrete(self, interval, discrete): assert interval.discrete == discrete ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1453997202.0 intervals-0.9.0/tests/interval/test_representations.py0000644000076500000240000000057700000000000024007 0ustar00konstastaff00000000000000from pytest import mark from intervals import IntInterval @mark.parametrize(('interval', 'string'), ( (IntInterval((1, 3)), '2'), (IntInterval([1, 1]), '1'), (IntInterval([1, 3]), '1 - 3'), (IntInterval.from_string('(1, 5]'), '2 - 5'), (IntInterval.from_string('[1,]'), '1 -') )) def test_hyphenized(interval, string): assert interval.hyphenized == string ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1453983460.0 intervals-0.9.0/tests/interval/test_step.py0000644000076500000240000000300200000000000021517 0ustar00konstastaff00000000000000from decimal import Decimal from intervals import DecimalInterval, FloatInterval, IntInterval class TestStepWithIntegers(object): def test_integers_with_step(self): interval = IntInterval([1, 2], step=3) assert interval.lower == 0 assert interval.upper == 3 def test_step_rounding(self): interval = IntInterval([1, 5], step=3) assert interval.lower == 0 assert interval.upper == 6 class TestStepWithFloats(object): def test_floats_with_step(self): interval = FloatInterval((0, 0.5), step=0.5) assert interval.lower == 0 assert interval.upper == 0.5 assert not interval.lower_inc assert not interval.upper_inc assert interval.step def test_step_rounding(self): interval = FloatInterval((0.2, 0.8), step=0.5) assert interval.lower == 0 assert interval.upper == 1 class TestStepWithDecimals(object): def test_decimals_with_step(self): interval = DecimalInterval( (Decimal('0'), Decimal('0.5')), step=Decimal('0.5') ) assert interval.lower == 0 assert interval.upper == Decimal('0.5') assert not interval.lower_inc assert not interval.upper_inc assert interval.step == Decimal('0.5') def test_step_rounding(self): interval = DecimalInterval( (Decimal('0.2'), Decimal('0.8')), step=Decimal('0.5') ) assert interval.lower == 0 assert interval.upper == 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1468173318.0 intervals-0.9.0/tests/interval/test_utility_methods.py0000644000076500000240000000177500000000000024011 0ustar00konstastaff00000000000000from pytest import mark from intervals import DecimalInterval, IntInterval @mark.parametrize( ('interval1', 'interval2', 'result'), ( (IntInterval([1, 3]), IntInterval([1, 3]), True), ( DecimalInterval.from_string('[1, 3)'), DecimalInterval.from_string('[3, 4]'), True ), ( DecimalInterval.from_string('[3, 4]'), DecimalInterval.from_string('[1, 3)'), True ), ( DecimalInterval.from_string('[1, 3]'), DecimalInterval.from_string('[4, 5]'), False ), ( DecimalInterval.from_string('[2, 4)'), DecimalInterval.from_string('[0, 1]'), False ), ( DecimalInterval.from_string('[1, 3)'), DecimalInterval.from_string('(3, 4]'), False ) ) ) def test_is_connected(interval1, interval2, result): assert interval1.is_connected(interval2) is result