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)