Repository: crsmithdev/arrow
Pull request #386 (2016-12-05 03:57:12)

implementation of interval function
https://github.com/crsmithdev/arrow/issues/362

From the issue above, implemented an interval function that takes in frame, start, end, tz, and also interval value to give interval on specific time range. (i.e. : frame = 'hour' with interval =2 will give tuples with 2 hour difference within the range)




Patch:
diff --git a/arrow/arrow.py b/arrow/arrow.py
index cda5536..89f7890 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -312,6 +312,49 @@ class Arrow(object):
         _range = cls.range(frame, start, end, tz, limit)
         return [r.span(frame) for r in _range]
 
+    @classmethod
+    def interval(cls, frame, start, end, interval=1, tz=None):
+        ''' Returns an array of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
+        representing a series of intervals between two inputs.
+
+        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
+        :param start: A datetime expression, the start of the range.
+        :param end: (optional) A datetime expression, the end of the range.
+        :param interval: (optional) Time interval for the given time frame.
+        :param tz: (optional) A timezone expression.  Defaults to UTC.
+        
+        Supported frame values: year, quarter, month, week, day, hour, minute, second
+
+        Recognized datetime expressions:
+
+            - An :class:`Arrow <arrow.arrow.Arrow>` object.
+            - A ``datetime`` object.
+
+        Recognized timezone expressions:
+
+            - A ``tzinfo`` object.
+            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
+            - A ``str`` in ISO-8601 style, as in '+07:00'.
+            - A ``str``, one of the following:  'local', 'utc', 'UTC'.
+
+        Usage:
+
+            >>> start = datetime(2013, 5, 5, 12, 30)
+            >>> end = datetime(2013, 5, 5, 17, 15)
+            >>> for r in arrow.Arrow.interval('hour', start, end, 2):
+            ...     print r
+            ...
+            (<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>)
+            (<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)
+            (<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:0]>)
+        '''
+        if interval < 1:
+            raise ValueError("interval has to be a positive integer")
+
+        spanRange = cls.span_range(frame,start,end,tz)
+
+        bound = (len(spanRange) // interval) * interval
+        return [ (spanRange[i][0],spanRange[i+ interval - 1][1]) for i in range(0,bound, interval) ]
 
     # representations
 
diff --git a/arrow/parser.py b/arrow/parser.py
index e4f368a..f3ed56c 100644
--- a/arrow/parser.py
+++ b/arrow/parser.py
@@ -5,12 +5,6 @@ from __future__ import unicode_literals
 from datetime import datetime
 from dateutil import tz
 import re
-
-try:
-    from functools import lru_cache
-except ImportError:  # pragma: no cover
-    from backports.functools_lru_cache import lru_cache  # pragma: no cover
-
 from arrow import locales
 
 
@@ -56,7 +50,7 @@ class DateTimeParser(object):
     MARKERS = ['YYYY', 'MM', 'DD']
     SEPARATORS = ['-', '/', '.']
 
-    def __init__(self, locale='en_us', cache_size=0):
+    def __init__(self, locale='en_us'):
 
         self.locale = locales.get_locale(locale)
         self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
@@ -68,7 +62,7 @@ class DateTimeParser(object):
             'dddd': self._choice_re(self.locale.day_names[1:], re.IGNORECASE),
             'ddd': self._choice_re(self.locale.day_abbreviations[1:],
                                    re.IGNORECASE),
-            'd': re.compile(r"[1-7]"),
+            'd' : re.compile("[1-7]"),
             'a': self._choice_re(
                 (self.locale.meridians['am'], self.locale.meridians['pm'])
             ),
@@ -76,9 +70,6 @@ class DateTimeParser(object):
             # ensure backwards compatibility of this token
             'A': self._choice_re(self.locale.meridians.values())
         })
-        if cache_size > 0:
-            self._generate_pattern_re =\
-                lru_cache(maxsize=cache_size)(self._generate_pattern_re)
 
     def parse_iso(self, string):
 
@@ -107,8 +98,8 @@ class DateTimeParser(object):
             # using various separators: -, /, .
             l = len(self.MARKERS)
             formats = [separator.join(self.MARKERS[:l-i])
-                       for i in range(l)
-                       for separator in self.SEPARATORS]
+                        for i in range(l)
+                        for separator in self.SEPARATORS]
 
         if has_time and has_tz:
             formats = [f + 'Z' for f in formats]
@@ -118,7 +109,10 @@ class DateTimeParser(object):
 
         return self._parse_multiformat(string, formats)
 
-    def _generate_pattern_re(self, fmt):
+    def parse(self, string, fmt):
+
+        if isinstance(fmt, list):
+            return self._parse_multiformat(string, fmt)
 
         # fmt is a string of tokens like 'YYYY-MM-DD'
         # we construct a new string by replacing each
@@ -128,7 +122,7 @@ class DateTimeParser(object):
         offset = 0
 
         # Extract the bracketed expressions to be reinserted later.
-        escaped_fmt = re.sub(self._ESCAPE_RE, "#", fmt)
+        escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt)
         # Any number of S is the same as one.
         escaped_fmt = re.sub('S+', 'S', escaped_fmt)
         escaped_data = re.findall(self._ESCAPE_RE, fmt)
@@ -160,21 +154,11 @@ class DateTimeParser(object):
             if i < len(b):
                 final_fmt_pattern += b[i][1:-1]
 
-        return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE)
-
-    def parse(self, string, fmt):
-
-        if isinstance(fmt, list):
-            return self._parse_multiformat(string, fmt)
-
-        fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt)
-
-        match = fmt_pattern_re.search(string)
+        match = re.search(final_fmt_pattern, string, flags=re.IGNORECASE)
         if match is None:
-            raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''
-                              .format(fmt_pattern_re.pattern, string))
+            raise ParserError('Failed to match \'{0}\' when parsing \'{1}\''.format(final_fmt_pattern, string))
         parts = {}
-        for token in fmt_tokens:
+        for token in tokens:
             if token == 'Do':
                 value = match.group('value')
             else:
diff --git a/requirements.txt b/requirements.txt
index a187d5a..eb3f983 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
-python-dateutil==2.6.0
+python-dateutil==2.1
 nose==1.3.0
 nose-cov==1.6
-chai==1.1.1
+chai==0.4.8
 sphinx==1.3.5
 simplejson==3.6.5
-backports.functools_lru_cache==1.2.1


Test patch:
diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py
index 65022f9..6cf6ddd 100644
--- a/tests/arrow_tests.py
+++ b/tests/arrow_tests.py
@@ -923,6 +923,23 @@ class ArrowSpanRangeTests(Chai):
             assertEqual(f.tzinfo, tz.gettz('US/Central'))
             assertEqual(c.tzinfo, tz.gettz('US/Central'))
 
+class ArrowIntervalTests(Chai):
+
+    def test_incorrect_input(self):
+        correct = True
+        try:
+            result = arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15),0)
+        except:
+            correct = False
+
+        assertEqual(correct,False)
+
+    def test_correct(self):
+        result = arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15),2)
+
+        assertEqual(result,[(arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999)),
+            (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999)),
+            (arrow.Arrow(2013, 5, 5, 16), arrow.Arrow(2013, 5, 5, 17, 59, 59, 999999))])
 
 class ArrowSpanTests(Chai):
 
diff --git a/tests/parser_tests.py b/tests/parser_tests.py
index 7682df8..67cf32f 100644
--- a/tests/parser_tests.py
+++ b/tests/parser_tests.py
@@ -59,44 +59,6 @@ class DateTimeParserTests(Chai):
         self.parser._parse_token('a', 'p..m', parts)
         assertEqual(parts, {})
 
-    def test_parser_no_caching(self):
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(100)
-        self.parser = parser.DateTimeParser(cache_size=0)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-
-    def test_parser_1_line_caching(self):
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1)
-        self.parser = parser.DateTimeParser(cache_size=1)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-        self.parser._generate_pattern_re('fmt_b')
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-
-    def test_parser_multiple_line_caching(self):
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(1)
-        self.parser = parser.DateTimeParser(cache_size=2)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_b').times(1)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
-        self.parser._generate_pattern_re('fmt_b')
-
-        expect(parser.DateTimeParser, '_generate_pattern_re').args('fmt_a').times(0)
-        for _ in range(100):
-            self.parser._generate_pattern_re('fmt_a')
 
 
 class DateTimeParserParseTests(Chai):