217 lines
5.7 KiB
Python
217 lines
5.7 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):
|
||
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')
|
||
self._lower = value
|
||
|
||
@property
|
||
def upper(self):
|
||
return self._upper
|
||
|
||
@upper.setter
|
||
def upper(self, value):
|
||
if value is None:
|
||
self._upper = float('inf')
|
||
self._upper = value
|
||
|
||
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)
|
||
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):
|
||
"""
|
||
Returns new NumberRange object from normalized number range format.
|
||
|
||
Example ::
|
||
|
||
range = NumberRange('[23, 45]')
|
||
range.lower = 23
|
||
range.upper = 45
|
||
|
||
range = NumberRange('(23, 45]')
|
||
range.lower = 24
|
||
range.upper = 45
|
||
|
||
range = NumberRange('(23, 45)')
|
||
range.lower = 24
|
||
range.upper = 44
|
||
"""
|
||
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] == '('
|
||
if self.lower_inc:
|
||
lower += 1
|
||
|
||
self.upper_inc = value[-1] == ')'
|
||
if self.upper_inc:
|
||
upper -= 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]' % (
|
||
self.lower if self.lower is not None else '',
|
||
self.upper if self.upper is not None 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
|