310 lines
7.5 KiB
Python
310 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
from collections import Iterable
|
||
from decimal import Decimal
|
||
try:
|
||
from functools import total_ordering
|
||
except ImportError:
|
||
from total_ordering import total_ordering
|
||
|
||
import six
|
||
|
||
|
||
class NumberRangeException(Exception):
|
||
pass
|
||
|
||
|
||
class RangeBoundsException(NumberRangeException):
|
||
def __init__(self, min_value, max_value):
|
||
self.message = 'Min value %s is bigger than max value %s.' % (
|
||
min_value,
|
||
max_value
|
||
)
|
||
|
||
|
||
def is_number(number):
|
||
return isinstance(number, (float, int, Decimal))
|
||
|
||
|
||
def parse_number(number):
|
||
if number is None or number == '':
|
||
return None
|
||
elif is_number(number):
|
||
return number
|
||
else:
|
||
return int(number)
|
||
|
||
|
||
@total_ordering
|
||
class NumberRange(object):
|
||
def __init__(self, *args):
|
||
"""
|
||
Parses given args and assigns lower and upper bound for this number
|
||
range.
|
||
|
||
1. Comma separated string argument
|
||
|
||
::
|
||
|
||
|
||
>>> range = NumberRange('[23, 45]')
|
||
>>> range.lower
|
||
23
|
||
>>> range.upper
|
||
45
|
||
|
||
|
||
>>> range = NumberRange('(23, 45]')
|
||
>>> range.lower_inc
|
||
False
|
||
|
||
>>> range = NumberRange('(23, 45)')
|
||
>>> range.lower_inc
|
||
False
|
||
>>> range.upper_inc
|
||
False
|
||
|
||
2. Sequence of arguments
|
||
|
||
::
|
||
|
||
|
||
>>> range = NumberRange(23, 45)
|
||
>>> range.lower
|
||
23
|
||
>>> range.upper
|
||
45
|
||
|
||
|
||
3. Lists and tuples as an argument
|
||
|
||
::
|
||
|
||
|
||
>>> range = NumberRange([23, 45])
|
||
>>> range.lower
|
||
23
|
||
>>> range.upper
|
||
45
|
||
>>> range.closed
|
||
True
|
||
|
||
|
||
>>> range = NumberRange((23, 45))
|
||
>>> range.lower
|
||
23
|
||
>>> range.closed
|
||
False
|
||
|
||
4. Integer argument
|
||
|
||
::
|
||
|
||
|
||
>>> range = NumberRange(34)
|
||
>>> range.lower == range.upper == 34
|
||
True
|
||
|
||
|
||
5. Object argument
|
||
|
||
::
|
||
|
||
>>> range = NumberRange(NumberRange(20, 30))
|
||
>>> range.lower
|
||
20
|
||
>>> range.upper
|
||
30
|
||
|
||
"""
|
||
if len(args) > 2:
|
||
raise NumberRangeException(
|
||
'NumberRange takes at most two arguments'
|
||
)
|
||
elif len(args) == 2:
|
||
self.parse_sequence(args)
|
||
else:
|
||
arg, = args
|
||
if isinstance(arg, six.integer_types):
|
||
self.parse_integer(arg)
|
||
elif isinstance(arg, six.string_types):
|
||
self.parse_string(arg)
|
||
elif isinstance(arg, Iterable):
|
||
self.parse_sequence(arg)
|
||
elif hasattr(arg, 'lower') and hasattr(arg, 'upper'):
|
||
self.parse_object(arg)
|
||
|
||
if self.lower > self.upper:
|
||
raise RangeBoundsException(self.lower, self.upper)
|
||
|
||
@property
|
||
def lower(self):
|
||
return self._lower
|
||
|
||
@lower.setter
|
||
def lower(self, value):
|
||
if value is None:
|
||
self._lower = -float('inf')
|
||
else:
|
||
self._lower = value
|
||
|
||
@property
|
||
def upper(self):
|
||
return self._upper
|
||
|
||
@upper.setter
|
||
def upper(self, value):
|
||
if value is None:
|
||
self._upper = float('inf')
|
||
else:
|
||
self._upper = value
|
||
|
||
@property
|
||
def open(self):
|
||
"""
|
||
Returns whether or not this object is an open interval.
|
||
|
||
::
|
||
|
||
range = NumberRange('(23, 45)')
|
||
range.open # True
|
||
|
||
range = NumberRange('[23, 45]')
|
||
range.open # False
|
||
"""
|
||
return not self.lower_inc and not self.upper_inc
|
||
|
||
@property
|
||
def closed(self):
|
||
"""
|
||
Returns whether or not this object is a closed interval.
|
||
|
||
::
|
||
|
||
range = NumberRange('(23, 45)')
|
||
range.closed # False
|
||
|
||
range = NumberRange('[23, 45]')
|
||
range.closed # True
|
||
"""
|
||
return self.lower_inc and self.upper_inc
|
||
|
||
def parse_object(self, obj):
|
||
self.lower = obj.lower
|
||
self.upper = obj.upper
|
||
self.lower_inc = obj.lower_inc
|
||
self.upper_inc = obj.upper_inc
|
||
|
||
def parse_string(self, value):
|
||
if ',' not in value:
|
||
self.parse_hyphen_range(value)
|
||
else:
|
||
self.parse_bounded_range(value)
|
||
|
||
def parse_sequence(self, seq):
|
||
lower, upper = seq
|
||
self.lower = parse_number(lower)
|
||
self.upper = parse_number(upper)
|
||
if isinstance(seq, tuple):
|
||
self.lower_inc = self.upper_inc = False
|
||
else:
|
||
self.lower_inc = self.upper_inc = True
|
||
|
||
def parse_integer(self, value):
|
||
self.lower = self.upper = value
|
||
self.lower_inc = self.upper_inc = True
|
||
|
||
def parse_bounded_range(self, value):
|
||
values = value.strip()[1:-1].split(',')
|
||
try:
|
||
lower, upper = map(
|
||
lambda a: parse_number(a.strip()), values
|
||
)
|
||
except ValueError as e:
|
||
raise NumberRangeException(e.message)
|
||
|
||
self.lower_inc = value[0] == '['
|
||
self.upper_inc = value[-1] == ']'
|
||
self.lower = lower
|
||
self.upper = upper
|
||
|
||
def parse_hyphen_range(self, value):
|
||
values = value.split('-')
|
||
if len(values) == 1:
|
||
self.lower = self.upper = parse_number(value.strip())
|
||
else:
|
||
try:
|
||
self.lower, self.upper = map(
|
||
lambda a: parse_number(a.strip()), values
|
||
)
|
||
except ValueError as e:
|
||
raise NumberRangeException(str(e))
|
||
self.lower_inc = self.upper_inc = True
|
||
|
||
@property
|
||
def normalized(self):
|
||
return '%s%s, %s%s' % (
|
||
'[' if self.lower_inc else '(',
|
||
self.lower if self.lower != -float('inf') else '',
|
||
self.upper if self.upper != float('inf') else '',
|
||
']' if self.upper_inc else ')'
|
||
)
|
||
|
||
def __eq__(self, other):
|
||
if isinstance(other, six.integer_types):
|
||
return self.lower == other == self.upper
|
||
try:
|
||
return (
|
||
self.lower == other.lower and
|
||
self.upper == other.upper
|
||
)
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __ne__(self, other):
|
||
return not (self == other)
|
||
|
||
def __gt__(self, other):
|
||
if isinstance(other, six.integer_types):
|
||
return self.lower > other and self.upper > other
|
||
|
||
try:
|
||
return self.lower > other.lower and self.upper > other.upper
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __repr__(self):
|
||
return 'NumberRange(%r, %r)' % (self.lower, self.upper)
|
||
|
||
def __str__(self):
|
||
if self.lower != self.upper:
|
||
return '%s - %s' % (self.lower, self.upper)
|
||
return str(self.lower)
|
||
|
||
def __add__(self, other):
|
||
"""
|
||
[a, b] + [c, d] = [a + c, b + d]
|
||
"""
|
||
try:
|
||
return NumberRange(
|
||
self.lower + other.lower,
|
||
self.upper + other.upper
|
||
)
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __sub__(self, other):
|
||
"""
|
||
Defines the substraction operator.
|
||
|
||
As defined in wikipedia:
|
||
|
||
[a, b] − [c, d] = [a − d, b − c]
|
||
"""
|
||
try:
|
||
return NumberRange(
|
||
self.lower - other.upper,
|
||
self.upper - other.lower
|
||
)
|
||
except AttributeError:
|
||
return NotImplemented
|