pax_global_header00006660000000000000000000000064140675625370014531gustar00rootroot0000000000000052 comment=60d6f00cbbd11e30dc1a0f67e57321638676a1f3 intervals-0.9.2/000077500000000000000000000000001406756253700135505ustar00rootroot00000000000000intervals-0.9.2/.gitignore000066400000000000000000000004731406756253700155440ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject intervals-0.9.2/.isort.cfg000066400000000000000000000001631406756253700154470ustar00rootroot00000000000000[settings] known_first_party=intervals line_length=79 multi_line_output=3 not_skip=__init__.py order_by_type=false intervals-0.9.2/.travis.yml000066400000000000000000000006731406756253700156670ustar00rootroot00000000000000language: python sudo: required dist: xenial python: - 3.5 - 3.6 - 3.7 - 3.8 - pypy - pypy3 install: - pip install -e ".[test]" script: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then py.test --doctest-modules --doctest-glob="*.rst" --ignore setup.py; else isort --recursive --diff intervals tests && isort --recursive --check-only intervals tests flake8 intervals tests py.test; fi intervals-0.9.2/CHANGES.rst000066400000000000000000000057731406756253700153660ustar00rootroot00000000000000Changelog --------- Here you can see the full list of changes between each intervals release. 0.9.2 (2021-06-02) ^^^^^^^^^^^^^^^^^^ - Make invalid value coercions throw ValueCoercionException 0.9.1 (2020-12-31) ^^^^^^^^^^^^^^^^^^ - Fixed discrete interval length (#53) 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 intervals-0.9.2/LICENSE000066400000000000000000000027251406756253700145630ustar00rootroot00000000000000Copyright (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. intervals-0.9.2/MANIFEST.in000066400000000000000000000001571406756253700153110ustar00rootroot00000000000000include README.rst include LICENSE include CHANGES.rst recursive-include tests * recursive-exclude tests *.pyc intervals-0.9.2/README.rst000066400000000000000000000257661406756253700152570ustar00rootroot00000000000000intervals ========= |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/ intervals-0.9.2/intervals/000077500000000000000000000000001406756253700155575ustar00rootroot00000000000000intervals-0.9.2/intervals/__init__.py000066400000000000000000000012611406756253700176700ustar00rootroot00000000000000# -*- 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.2' intervals-0.9.2/intervals/exc.py000066400000000000000000000012551406756253700167130ustar00rootroot00000000000000class 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) class ValueCoercionException(IntervalException): def __init__(self, message=None): if message is None: message = 'Could not coerce value.' super(IntervalException, self).__init__(message) intervals-0.9.2/intervals/interval.py000066400000000000000000000526221406756253700177640ustar00rootroot00000000000000""" 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, InvalidOperation from math import ceil, floor from infinity import inf, is_infinite from .exc import ( IllegalArgument, IntervalException, RangeBoundsException, ValueCoercionException ) from .parser import IntervalParser, IntervalStringParser 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, str): 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, str): return self.coerce_string(value) else: return self.coerce_obj(value) def coerce_string(self, value): try: return self.type(value) except (ValueError, TypeError): raise ValueCoercionException() def coerce_obj(self, obj): try: return self.type(obj) except (ValueError, TypeError): raise ValueCoercionException() @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): if self.discrete: if not self: return 0 if not self.lower_inc or not self.upper_inc: return canonicalize(self, lower_inc=True, upper_inc=True).length 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, Decimal)) and str(int(obj)) != str(obj): raise IntervalException( 'Could not coerce %s to int. Decimal places would ' 'be lost.' ) super().coerce_obj(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 def coerce_string(self, value): try: return self.type(value) except (InvalidOperation, TypeError): raise ValueCoercionException('Could not coerce given value to decimal.') class CharacterInterval(AbstractInterval): type = str def coerce_obj(self, obj): if not isinstance(obj, str): raise ValueCoercionException('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() intervals-0.9.2/intervals/parser.py000066400000000000000000000047561406756253700174410ustar00rootroot00000000000000from .exc import IntervalException 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 intervals-0.9.2/setup.py000066400000000000000000000036241406756253700152670ustar00rootroot00000000000000""" 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 :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) intervals-0.9.2/tests/000077500000000000000000000000001406756253700147125ustar00rootroot00000000000000intervals-0.9.2/tests/__init__.py000066400000000000000000000000001406756253700170110ustar00rootroot00000000000000intervals-0.9.2/tests/interval/000077500000000000000000000000001406756253700165365ustar00rootroot00000000000000intervals-0.9.2/tests/interval/__init__.py000066400000000000000000000000001406756253700206350ustar00rootroot00000000000000intervals-0.9.2/tests/interval/test_arithmetic.py000066400000000000000000000042331406756253700223020ustar00rootroot00000000000000from 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) intervals-0.9.2/tests/interval/test_canonicalize.py000066400000000000000000000010721406756253700226060ustar00rootroot00000000000000from 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 intervals-0.9.2/tests/interval/test_coercion.py000066400000000000000000000026611406756253700217550ustar00rootroot00000000000000from 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)) intervals-0.9.2/tests/interval/test_factory_methods.py000066400000000000000000000045241406756253700233460ustar00rootroot00000000000000from 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 intervals-0.9.2/tests/interval/test_initialization.py000066400000000000000000000155401406756253700232030ustar00rootroot00000000000000from datetime import date from decimal import Decimal from infinity import inf from pytest import mark, raises from intervals import ( CharacterInterval, DecimalInterval, FloatInterval, IllegalArgument, Interval, IntervalException, 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 @mark.parametrize('value', ( 'bogus', {}, [] ) ) def test_invalid_decimals(self, value): with raises(IntervalException): interval = DecimalInterval((value, '1')) @mark.parametrize('value', ( 'bogus', {}, [] ) ) def test_invalid_integers(self, value): with raises(IntervalException): interval = IntInterval((value, '1')) 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 intervals-0.9.2/tests/interval/test_operators.py000066400000000000000000000173341406756253700221750ustar00rootroot00000000000000from 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 ) intervals-0.9.2/tests/interval/test_properties.py000066400000000000000000000073651406756253700223560ustar00rootroot00000000000000from 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( ('interval', 'length'), ( (IntInterval([1, 4]), 3), (IntInterval([-1, 1]), 2), (IntInterval([-inf, inf]), inf), (IntInterval([1, inf]), inf), (IntInterval.from_string('(0, 3)'), 1), (IntInterval.from_string('[0, 3)'), 2), (IntInterval.from_string('(0, 2)'), 0), (IntInterval.from_string('(0, 1)'), 0) ) ) def test_length(self, interval, length): assert interval.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 intervals-0.9.2/tests/interval/test_representations.py000066400000000000000000000005771406756253700234050ustar00rootroot00000000000000from 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 intervals-0.9.2/tests/interval/test_step.py000066400000000000000000000030021406756253700211150ustar00rootroot00000000000000from 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 intervals-0.9.2/tests/interval/test_utility_methods.py000066400000000000000000000017751406756253700234070ustar00rootroot00000000000000from 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 intervals-0.9.2/tox.ini000066400000000000000000000002511406756253700150610ustar00rootroot00000000000000[tox] envlist = py35,py36,py37,pypy [testenv] deps = infinity pygments pytest commands = py.test --doctest-modules --doctest-glob="*.rst" --ignore setup.py