Files
deb-python-sqlalchemy-utils/sqlalchemy_utils/primitives/number_range.py
2014-01-05 21:35:33 +02:00

310 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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