Browse Source

datetimex.Duration: support minutes

Fabian Peter Hammerle 7 years ago
parent
commit
c4eb12e87a
4 changed files with 64 additions and 8 deletions
  1. 14 7
      ioex/datetimeex.py
  2. 1 1
      setup.py
  3. 40 0
      tests/datetimeex/test_duration.py
  4. 9 0
      tests/datetimeex/test_duration_yaml.py

+ 14 - 7
ioex/datetimeex.py

@@ -36,22 +36,25 @@ class Duration(object):
 
     yaml_tag = u'!duration'
 
-    iso_format = r'P((?P<y>\d+)Y)?((?P<d>\d+)D)?'
+    iso_format = r'P((?P<y>\d+)Y)?((?P<d>\d+)D)?' \
+        + r'(T(?P<min>\d+)M)?'
 
     years = ioex.classex.AttributeDescriptor('_years', types=(int,), min=0)
     days = ioex.classex.AttributeDescriptor('_days', types=(int,), min=0)
+    minutes = ioex.classex.AttributeDescriptor('_minutes', types=(int,), min=0)
 
-    def __init__(self, years=0, days=0):
+    def __init__(self, years=0, days=0, minutes=0):
         self.years = years
         self.days = days
+        self.minutes = minutes
 
     @property
     def isoformat(self):
         iso_str = re.sub(
             r'(?<!\d)0.',
             '',
-            'P{}Y{}D'.format(self.years, self.days),
-        )
+            'P{}Y{}DT{}M'.format(self.years, self.days, self.minutes),
+        ).rstrip('T')
         return 'P0Y' if iso_str == 'P' else iso_str
 
     @classmethod
@@ -68,12 +71,14 @@ class Duration(object):
             return cls(
                 years=attr['y'],
                 days=attr['d'],
+                minutes=attr['min'],
             )
 
     def __eq__(self, other):
         return (type(self) == type(other)
                 and self.years == other.years
-                and self.days == other.days)
+                and self.days == other.days
+                and self.minutes == other.minutes)
 
     def __radd__(self, dt):
         if not isinstance(dt, datetime.datetime):
@@ -82,6 +87,7 @@ class Duration(object):
             return dt + dateutil.relativedelta.relativedelta(
                 years=self.years,
                 days=self.days,
+                minutes=self.minutes,
             )
 
     @classmethod
@@ -99,6 +105,7 @@ class Duration(object):
             mapping={k: v for k, v in {
                 'years': duration.years,
                 'days': duration.days,
+                'minutes': duration.minutes,
             }.items() if v != 0},
         )
 
@@ -154,8 +161,8 @@ class Period(object):
         else:
             attr = match.groupdict()
             return cls(
-                start = dateutil.parser.parse(attr['start']),
-                end = dateutil.parser.parse(attr['end']),
+                start=dateutil.parser.parse(attr['start']),
+                end=dateutil.parser.parse(attr['end']),
             )
 
     def __eq__(self, other):

+ 1 - 1
setup.py

@@ -5,7 +5,7 @@ import glob
 setup(
     name = 'ioex',
     packages = ['ioex'],
-    version = '0.17.0',
+    version = '0.18.0',
     description = 'extension for python\'s build-in input / output interface',
     author = 'Fabian Peter Hammerle',
     author_email = 'fabian.hammerle@gmail.com',

+ 40 - 0
tests/datetimeex/test_duration.py

@@ -11,7 +11,9 @@ import pytz
     {'years': 13},
     {'days': 0},
     {'days': 13},
+    {'minutes': 7},
     {'years': 1, 'days': 3},
+    {'years': 1, 'days': 3, 'minutes': 5},
 ])
 def test_init(init_kwargs):
     d = Duration(**init_kwargs)
@@ -26,6 +28,7 @@ def test_init_default():
     d = Duration()
     assert 0 == d.years
     assert 0 == d.days
+    assert 0 == d.minutes
 
 
 @pytest.mark.parametrize(('init_kwargs', 'exception_type'), [
@@ -33,6 +36,8 @@ def test_init_default():
     [{'years': '1'}, TypeError],
     [{'days': -2}, ValueError],
     [{'days': '1'}, TypeError],
+    [{'minutes': -2}, ValueError],
+    [{'minutes': '1'}, TypeError],
 ])
 def test_init_fail(init_kwargs, exception_type):
     with pytest.raises(exception_type):
@@ -79,13 +84,36 @@ def test_set_days_fail(days, exception_type):
         d.days = days
 
 
+@pytest.mark.parametrize(('minutes'), [
+    0,
+    13,
+])
+def test_set_minutes(minutes):
+    d = Duration()
+    d.minutes = minutes
+    assert d.minutes == minutes
+
+
+@pytest.mark.parametrize(('minutes', 'exception_type'), [
+    [-2, ValueError],
+    ['1', TypeError],
+])
+def test_set_minutes_fail(minutes, exception_type):
+    d = Duration()
+    with pytest.raises(exception_type):
+        d.minutes = minutes
+
+
 @pytest.mark.parametrize(('init_params', 'iso'), [
     [{'years': 0}, 'P0Y'],
     [{'years': 30}, 'P30Y'],
     [{'years': 3}, 'P3Y'],
     [{'days': 30}, 'P30D'],
     [{'days': 3}, 'P3D'],
+    [{'minutes': 3}, 'PT3M'],
     [{'years': 10, 'days': 30}, 'P10Y30D'],
+    [{'days': 30, 'minutes': 50}, 'P30DT50M'],
+    [{'years': 10, 'days': 30, 'minutes': 50}, 'P10Y30DT50M'],
 ])
 def test_get_isoformat(init_params, iso):
     d = Duration(**init_params)
@@ -98,7 +126,10 @@ def test_get_isoformat(init_params, iso):
     [Duration(years=3), 'P3Y'],
     [Duration(days=30), 'P30D'],
     [Duration(days=3), 'P3D'],
+    [Duration(minutes=3), 'PT3M'],
     [Duration(years=10, days=30), 'P10Y30D'],
+    [Duration(days=30, minutes=50), 'P30DT50M'],
+    [Duration(years=10, days=30, minutes=50), 'P10Y30DT50M'],
 ])
 def test_from_iso(expected, source_iso):
     d = Duration.from_iso(source_iso)
@@ -107,6 +138,7 @@ def test_from_iso(expected, source_iso):
 
 @pytest.mark.parametrize(('source_iso'), [
     'Q0Y',
+    'P10M20M',
     '2017-05-19T20:02:22+02:00',
 ])
 def test_from_iso_fail(source_iso):
@@ -120,7 +152,10 @@ def test_from_iso_fail(source_iso):
     [Duration(years=3), Duration(years=3)],
     [Duration(days=0), Duration(days=0)],
     [Duration(days=3), Duration(days=3)],
+    [Duration(minutes=3), Duration(minutes=3)],
     [Duration(years=1, days=3), Duration(years=1, days=3)],
+    [Duration(years=1, days=3, minutes=5),
+     Duration(years=1, days=3, minutes=5)],
 ])
 def test_eq(a, b):
     assert a == b
@@ -143,6 +178,11 @@ def test_eq(a, b):
         Duration(years=1, days=6),
         datetime.datetime(2017, 3, 6, 21, 7, 1),
     ],
+    [
+        datetime.datetime(2016, 2, 29, 21, 7, 1),
+        Duration(years=1, days=6, minutes=18),
+        datetime.datetime(2017, 3, 6, 21, 25, 1),
+    ],
     [
         pytz.timezone('Europe/Vienna').localize(
             datetime.datetime(2016, 2, 29, 21, 7, 1),

+ 9 - 0
tests/datetimeex/test_duration_yaml.py

@@ -12,7 +12,11 @@ yaml = pytest.importorskip('yaml')
     [Duration(years=0),  '!duration\n{}'],
     [Duration(years=0),  '!duration {}'],
     [Duration(days=32), '!duration\ndays: 32'],
+    [Duration(minutes=0), '!duration\nminutes: 0'],
+    [Duration(minutes=32), '!duration\nminutes: 32'],
     [Duration(years=1, days=3), '!duration\nyears: 1\ndays: 3'],
+    [Duration(years=1, days=3, minutes=5),
+     '!duration\nyears: 1\ndays: 3\nminutes: 5'],
 ])
 def test_from_yaml(expected_duration, yaml_string, loader):
     class TestLoader(loader):
@@ -40,7 +44,12 @@ def test_from_yaml_tag(expected_duration, yaml_string, tag, loader):
     [Duration(years=0),  '!duration {}\n'],
     [Duration(years=32), '!duration\nyears: 32\n'],
     [Duration(days=32), '!duration\ndays: 32\n'],
+    [Duration(minutes=32), '!duration\nminutes: 32\n'],
     [Duration(years=1, days=2), '!duration\ndays: 2\nyears: 1\n'],
+    [Duration(days=2, minutes=0), '!duration\ndays: 2\n'],
+    [Duration(days=2, minutes=5), '!duration\ndays: 2\nminutes: 5\n'],
+    [Duration(years=1, days=2, minutes=5),
+     '!duration\ndays: 2\nminutes: 5\nyears: 1\n'],
 ])
 def test_to_yaml(duration, yaml_string, dumper):
     class TestDumper(dumper):