diff --git a/firanka/series/__init__.py b/firanka/series/__init__.py index af701b5648a646f4b155f05180b23a6dd3aa78d0..2d6882191b3d5aad9c8b216aac6383ffffb69a30 100644 --- a/firanka/series/__init__.py +++ b/firanka/series/__init__.py @@ -5,14 +5,39 @@ import logging logger = logging.getLogger(__name__) +from .exceptions import OutOfRangeError, EmptyDomainError + + class DataSeries(object): """ Finite mapping from x: REAL => object """ - def __init__(self, data=None): - self.data = data or [] + def __init__(self, data, domain_end=None): + self.data = data + + if domain_end is None: + try: + self.domain_end = data[-1][0] + except IndexError: + self.domain_end = None + else: + self.domain_end = domain_end + + @property + def domain(self): + try: + start = self.data[0][0] + stop = self.domain_end + assert start <= stop + return start, stop + except IndexError: + return EmptyDomainError + + def __contains__(self, index): + start, stop = self.domain + return start <= index <= stop def length(self): """ @@ -20,6 +45,18 @@ class DataSeries(object): :return: float """ try: - return self.data[-1] - self.data[0] + start, stop = self.domain + + return stop-start except IndexError: return 0.0 + except TypeError: + return 0.0 # domain_end is None + + def __getitem__(self, index): + if index not in self: + raise OutOfRangeError('index not within domain', index) + + for k, v in self.data: + if k <= index: + return v diff --git a/firanka/series/exceptions.py b/firanka/series/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..fd588a8578fbf08cd4464a4755eb0545d6bc57a5 --- /dev/null +++ b/firanka/series/exceptions.py @@ -0,0 +1,18 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division +import six +import logging + +logger = logging.getLogger(__name__) + + +class FirankaException(Exception): + pass + + +class OutOfRangeError(FirankaException): + pass + + +class EmptyDomainError(FirankaException): + pass \ No newline at end of file diff --git a/firanka/series/range.py b/firanka/series/range.py new file mode 100644 index 0000000000000000000000000000000000000000..bf60693dd4dcf08729cbcabf1c5ec01dc1798b81 --- /dev/null +++ b/firanka/series/range.py @@ -0,0 +1,116 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division +import six +import logging +import re +from satella.coding import for_argument + +logger = logging.getLogger(__name__) + + +class Range(object): + """ + Range of real numbers + """ + def __init__(self, *args): + if len(args) == 1: + rs, = args + assert rs.startswith('<') or rs.startswith('(') + assert rs.endswith('>') or rs.endswith(')') + + lend_inclusive = rs[0] == '<' + rend_inclusive = rs[-1] == '>' + + rs = rs[1:-1] + start, stop = map(float, rs.split(';')) + elif isinstance(args[0], Range): + start = args[0].range + stop = args[0].stop + lend_inclusive = args[0].lend_inclusive + rend_inclusive = args[0].rend_inclusive + else: + start, stop, lend_inclusive, rend_inclusive = args + + self.start = start + self.stop = stop + self.lend_inclusive = lend_inclusive + self.rend_inclusive = rend_inclusive + + def __contains__(self, x): + if x == self.start: + return self.lend_inclusive + + if x == self.stop: + return self.rend_inclusive + + return self.start < x < self.stop + + def is_empty(self): + return (self.start == self.stop) and (not self.lend_inclusive) and ( + not self.rend_inclusive) + + def __len__(self): + return self.stop - self.start + + def __repr__(self): + return str(self) + return 'Range(%s, %s, %s, %s)' % (repr(self.start), repr(self.stop), repr(self.lend_inclusive), repr(self.rend_inclusive)) + + def __bool__(self): + """True if not empty""" + return not self.is_empty() + + def __str__(self): + return '%s%s;%s%s' % ( + '<' if self.lend_inclusive else '(', + self.start, + self.stop, + '>' if self.rend_inclusive else ')', + ) + + def intersection(self, y): + if not isinstance(y, Range): y = Range(y) + + x = self + + # Check for intersection being impossible + if (x.stop < y.start) or (x.start > y.stop) or \ + (x.stop == y.start and not x.rend_inclusive and not y.lend_inclusive) or \ + (x.start == x.stop and not x.lend_inclusive and not y.rend_inclusive): + return EMPTY_RANGE + + # Check for range extension + if (x.start == y.stop) and (x.lend_inclusive or y.lend_inclusive): + return Range(y.start, x.stop, y.lend_inclusive, x.rend_inclusive) + + if (x.start == y.stop) and (x.lend_inclusive or y.lend_inclusive): + return Range(y.start, x.stop, y.lend_inclusive, x.rend_inclusive) + + + if x.start == y.start: + start = x.start + lend_inclusive = x.lend_inclusive or y.lend_inclusive + else: + p = x if x.start > y.start else y + start = p.start + lend_inclusive = p.lend_inclusive + + if x.stop == y.stop: + stop = x.stop + rend_inclusive = x.rend_inclusive or y.rend_inclusive + else: + p = x if x.stop < y.stop else y + stop = p.stop + rend_inclusive = p.rend_inclusive + + return Range(start, stop, lend_inclusive, rend_inclusive) + + def __eq__(self, other): + if not isinstance(other, Range): other = Range(other) + return self.start == other.start and self.stop == other.stop and self.lend_inclusive == other.lend_inclusive and self.rend_inclusive == other.rend_inclusive + + def __hash__(self): + return hash(self.start) ^ hash(self.stop) + + +EMPTY_RANGE = Range(0, 0, False, False) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ffe2fce498955b628014618b28c6bcf152466a4a..51695eebe71a5ec2a8c4182d21b47ede8728f1bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ six +satella diff --git a/setup.py b/setup.py index 7861f14eedd0de69e5b1d8b8635cfd2df327b7d2..ca77cdecdba9f030770e6248a99bce40a8170bc5 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,6 @@ setup( version=__version__, packages=find_packages(exclude=['tests.*', 'tests']), tests_require=["nose", 'coverage>=4.0,<4.4'], - install_requires=['six'], + install_requires=open('requirements.txt', 'r').readlines(), test_suite='nose.collector', ) diff --git a/tests/test_series/test_range.py b/tests/test_series/test_range.py new file mode 100644 index 0000000000000000000000000000000000000000..7c542602486a300c58b7eed95f40d4188e6e0af0 --- /dev/null +++ b/tests/test_series/test_range.py @@ -0,0 +1,16 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division +import six +import unittest +from firanka.series.range import Range + +class TestRange(unittest.TestCase): + def test_intersection(self): + + self.assertFalse(Range(-10, -1, True, True).intersection('<2;3>')) + self.assertFalse(Range(-10, -1, True, False).intersection('(-1;3>')) + self.assertEquals(Range('<-10;-1)').intersection('<-1;1>'), '<-1;1>') + + + def test_str(self): + self.assertEqual(str(Range(-1, 1, True, True)), '<-1;1>') \ No newline at end of file diff --git a/tests/test_series/test_series.py b/tests/test_series/test_series.py deleted file mode 100644 index 833ce84996eee8db3c32b649628ffda4d8d7d227..0000000000000000000000000000000000000000 --- a/tests/test_series/test_series.py +++ /dev/null @@ -1,15 +0,0 @@ -# coding=UTF-8 -from __future__ import print_function, absolute_import, division -import six -import unittest -from firanka.series import DataSeries - - -class TestSeries(unittest.TestCase): - def test_ds(self): - - ds = DataSeries() - self.assertAlmostEqual(ds.length(), 0.0) - - ds = DataSeries([[0,1], [10,2]]) - self.assertAlmostEqual(ds.length(), 10.0)