Compare commits

...

24 Commits

Author SHA1 Message Date
kennethreitz 36fc82211f remove bunk 2017-05-23 10:51:03 -07:00
kennethreitz 6eba043582 amazing MayaInterval class, compliments of @dgouldin :) 2017-05-23 10:49:36 -07:00
kennethreitz 23652fe16a new lock file 2017-05-15 20:14:13 -04:00
kennethreitz 36379fc2f2 .org 2017-05-15 20:12:43 -04:00
kennethreitz c7b804985e Update README.rst 2017-05-15 20:10:27 -04:00
kennethreitz e4a928e8f4 new version 2017-05-15 20:09:11 -04:00
kennethreitz 080831519d range 2017-05-15 20:06:35 -04:00
kennethreitz 43705d84d6 test intervals 2017-05-15 20:05:00 -04:00
kennethreitz 6521a7dbc5 maya.intervals 2017-05-15 20:00:56 -04:00
kennethreitz 1796fc1d40 fix failing tests 2017-05-15 15:22:26 -04:00
kennethreitz 450dff000b v0.1.8 2017-02-09 21:29:03 -05:00
kennethreitz 90ba4ded70 fix for rand-day 2017-02-09 21:28:40 -05:00
kennethreitz 2e33ede5ac document add 2017-02-09 21:28:27 -05:00
kennethreitz 6a9661759f fix add/subtract 2017-02-09 21:28:22 -05:00
kennethreitz f46c3792e8 pipfile.lock 2017-02-09 21:23:37 -05:00
kennethreitz c07e51bf4d Update README.rst 2017-02-09 20:54:51 -05:00
kennethreitz 63d5e0ce90 Subtract and add! 2017-02-09 20:43:55 -05:00
kennethreitz 361f7a45ad Merge pull request #51 from sdispater/improve-iso8601-parsing
Improve iso8601 parsing
2017-02-09 19:15:19 -06:00
Sébastien Eustace 09db746027 Updates AUTHORS.rst 2017-02-09 19:40:09 -05:00
Sébastien Eustace 15a447491a Improves ISO8601 parsing 2017-02-09 19:39:18 -05:00
kennethreitz f8d83c0370 Update README.rst 2017-02-09 17:19:19 -05:00
kennethreitz 7c47796ac6 Update README.rst 2017-02-09 17:18:44 -05:00
kennethreitz 9c88519e63 only test 3.6 and 2.7 2017-02-09 17:15:22 -05:00
kennethreitz 50d4558ffa travis 2017-02-09 17:07:03 -05:00
9 changed files with 1014 additions and 32 deletions
+2 -5
View File
@@ -1,12 +1,9 @@
language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
# command to install dependencies
install: pip install -r requirements.txt
install: pip install pipenv; pipenv install --dev
# command to run tests
script: make
script: pipenv run pytest
+1
View File
@@ -20,3 +20,4 @@ In chronological order:
- aaronjeline (`@aaronjeline <https://github.com/aaronjeline>`_)
- jerry2yu (`@jerry2yu <https://github.com/jerry2yu>`_)
- Joshua Li <joshua.r.li.98@gmail.com> (`@JoshuaRLi <https://github.com/JoshuaRLi>`_)
- Sébastien Eustace <sebastien@eustace.io> (`@sdispater <https://github.com/sdispater>`_)
+1 -2
View File
@@ -2,10 +2,9 @@
humanize = "*"
pytz = "*"
dateparser = "*"
iso8601 = "*"
python-dateutil = "*"
"ruamel.yaml" = "*"
tzlocal = "*"
pendulum = ">=1.0"
[dev-packages]
pytest = "*"
Generated
+72
View File
@@ -0,0 +1,72 @@
{
"_meta": {
"hash": {
"sha256": "5617ff73ba51e60721267b24dc01e83f33d2a3692870b60e394b0f75ed2dc313"
},
"requires": {},
"sources": [
{
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"dateparser": {
"version": "==0.6.0"
},
"humanize": {
"version": "==0.5.1"
},
"pendulum": {
"version": "==1.2.0"
},
"python-dateutil": {
"version": "==2.6.0"
},
"pytz": {
"version": "==2017.2"
},
"pytzdata": {
"version": "==2017.2"
},
"regex": {
"version": "==2017.04.29"
},
"ruamel.ordereddict": {
"version": "==0.4.9"
},
"ruamel.yaml": {
"version": "==0.14.12"
},
"six": {
"version": "==1.10.0"
},
"tzlocal": {
"version": "==1.4"
}
},
"develop": {
"appdirs": {
"version": "==1.4.3"
},
"packaging": {
"version": "==16.8"
},
"py": {
"version": "==1.4.33"
},
"pyparsing": {
"version": "==2.2.0"
},
"pytest": {
"version": "==3.0.7"
},
"setuptools": {
"version": "==35.0.2"
},
"six": {
"version": "==1.10.0"
}
}
}
+20 -8
View File
@@ -1,5 +1,5 @@
Maya: Datetime for Humans™
==========================
Maya: Timestamps for Humans™
============================
.. image:: https://img.shields.io/pypi/v/maya.svg
:target: https://pypi.python.org/pypi/maya
@@ -7,7 +7,7 @@ Maya: Datetime for Humans™
.. image:: https://travis-ci.org/kennethreitz/maya.svg?branch=master
:target: https://travis-ci.org/kennethreitz/maya
.. image:: https://img.shields.io/badge/SayThanks.io-☼-1EAEDB.svg
.. image:: https://img.shields.io/badge/SayThanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
@@ -40,11 +40,17 @@ Behold, datetimes for humans!
>>> tomorrow.slang_time()
'23 hours from now'
# Also: MayaDT.from_iso8601(...)
>>> tomorrow.iso8601()
'2016-12-16T15:11:30.263350Z'
'2017-02-10T22:17:01.445418Z'
# Also: MayaDT.from_rfc2822(...)
>>> tomorrow.rfc2822()
'Fri, 16 Dec 2016 20:11:30 -0000'
'Fri, 10 Feb 2017 22:17:01 GMT'
# Also: MayaDT.from_rfc3339(...)
>>> tomorrow.rfc3339()
'2017-02-10T22:17:01.44Z'
>>> tomorrow.datetime()
datetime.datetime(2016, 12, 16, 15, 11, 30, 263350, tzinfo=<UTC>)
@@ -57,14 +63,20 @@ Behold, datetimes for humans!
>>> rand_day = maya.when('2011-02-07', timezone='US/Eastern')
<MayaDT epoch=1297036800.0>
# Note how this is the 6th, not the 7th.
>>> rand_day.day
6
7
>>> rand_day.add(days=10).day
17
# Always.
>>> rand_day.timezone
UTC
# Range of hours in a day:
>>> maya.interval(start=maya.now(), end=maya.now().add(days=1), interval=60*60)
<generator object intervals at 0x105ba5820>
☤ Why is this useful?
---------------------
@@ -84,7 +96,7 @@ Arrow, for example, is a fantastic library, but isn't what I wanted in a datetim
I simply desire a sane API for datetimes that made sense to me for all the things I'd ever want to do—especially when dealing with timezone algebra. Arrow doesn't do all of the things I need (but it does a lot more!). Maya does do exactly what I need.
I think these projects complement each-other, personally. Maya is great for parsing websites. For example- Arrow supports floors and ceilings and spans of dates, which Maya does not at all.
I think these projects complement each-other, personally. Maya is great for parsing websites!
☤ Installing Maya
+305 -10
View File
@@ -11,15 +11,15 @@ warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
from datetime import datetime as Datetime
from datetime import timedelta, datetime as Datetime
import pytz
import humanize
import dateparser
import iso8601
import dateutil.parser
import pendulum
from tzlocal import get_localzone
_EPOCH_START = (1970, 1, 1)
@@ -42,7 +42,6 @@ def validate_class_type_arguments(operator):
return inner
class MayaDT(object):
"""The Maya Datetime object."""
@@ -60,7 +59,6 @@ class MayaDT(object):
"""Return's the datetime's format"""
return format(self.datetime(), *args, **kwargs)
@validate_class_type_arguments('==')
def __eq__(self, maya_dt):
return self._epoch == maya_dt._epoch
@@ -85,6 +83,26 @@ class MayaDT(object):
def __ge__(self, maya_dt):
return self._epoch >= maya_dt._epoch
def __hash__(self):
return hash(self.epoch)
def __add__(self, item):
return self.add(seconds=seconds_or_timedelta(item).total_seconds())
def __radd__(self, item):
return self + item
def __sub__(self, item):
return self.subtract(
seconds=seconds_or_timedelta(item).total_seconds())
def add(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).add(**kwargs))
def subtract(self, **kwargs):
""""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).subtract(**kwargs))
# Timezone Crap
# -------------
@@ -131,8 +149,7 @@ class MayaDT(object):
@classmethod
def from_iso8601(klass, string):
"""Returns MayaDT instance from iso8601 string."""
dt = iso8601.parse_date(string)
return klass.from_datetime(dt)
return parse(string)
@staticmethod
def from_rfc2822(string):
@@ -182,7 +199,7 @@ class MayaDT(object):
def rfc3339(self):
"""Returns an RFC 3339 representation of the MayaDT."""
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4]+"Z"
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + "Z"
# Properties
# ----------
@@ -242,6 +259,262 @@ class MayaDT(object):
return humanize.naturaltime(dt)
def to_utc_offset_naive(dt):
if dt.tzinfo is None:
return dt
return dt.astimezone(pytz.utc).replace(tzinfo=None)
def to_utc_offset_aware(dt):
if dt.tzinfo is not None:
return dt
return pytz.utc.localize(dt)
def to_iso8601(dt):
return to_utc_offset_naive(dt).isoformat() + 'Z'
def end_of_day_midnight(dt):
return dt if dt.time() == time.min else\
(dt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1))
class MayaInterval(object):
"""
A MayaInterval represents a range between two datetimes, inclusive of the start
and exclusive of the end.
"""
def __init__(self, start=None, end=None, duration=None):
try:
# Ensure that proper arguments were passed.
assert any((
(start and end),
(start and duration is not None),
(end and duration is not None),
))
assert not all((start, end, duration is not None))
except AssertionError:
raise ValueError(
'Exactly 2 of start, end, and duration must be specified')
# Convert duration to timedelta if seconds were provided.
duration = seconds_or_timedelta(duration)
if not start:
start = end - duration
if not end:
end = start + duration
if start > end:
raise ValueError('MayaInterval cannot end before it starts')
self.start = start
self.end = end
def __repr__(self):
return '<MayaInterval start={!r0} end={!r1}>'.format(self.start, self.end)
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaInterval."""
return '{0}/{1}'.format(self.start.iso6801, self.end.iso8601)
@classmethod
def from_iso8601(cls, s):
# # Start and end, such as "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z"
# start, end = s.split('/')
# try:
# start = parse(start)
# except pendulum.parsing.exceptions.ParserError:
# start = self._parse_iso8601_duration(start)
# try:
# end = parse(start)
# except pendulum.parsing.exceptions.ParserError as e:
# end = self._parse_iso8601_duration(start)
# # Start and duration, such as "2007-03-01T13:00:00Z/P1Y2M10DT2H30M"
# # Duration and end, such as "P1Y2M10DT2H30M/2008-05-11T15:30:00Z"
raise NotImplentedError()
def __and__(self, i):
return self.intersection(i)
def __or__(self, i):
return self.combine(i)
def __eq__(self, i):
return (
self.start == i.start and
self.end == i.end
)
def __hash__(self):
return hash((self.start, self.end))
def __iter__(self):
yield self.start
yield self.end
def __cmp__(self, i):
return (
cmp(self.start, i.start) or
cmp(self.end, i.end)
)
@property
def duration(self):
return self.timedelta.total_seconds()
@property
def timedelta(self):
return timedelta(seconds=(self.end.epoch - self.start.epoch))
@property
def is_instant(self):
return self.timedelta == timedelta(seconds=0)
def intersects(self, i):
return self & i is not None
@property
def midpoint(self):
return self.start.add(seconds=(self.duration / 2))
def combine(self, i):
"""Returns a combined list of timespans, merged together."""
ii = sorted([self, i])
if self & i or self.is_adjacent(i):
return [
MayaInterval(
ii[0].start,
max(ii[0].end, ii[1].end),
),
]
return ii
def subtract(self, i):
""""Removes the given inerval."""
if not self & i:
return [self]
elif i.contains(self):
return []
ii = []
if self.start < i.start:
ii.append(MayaInterval(self.start, i.start))
if self.end > i.end:
ii.append(MayaInterval(i.end, self.end))
return ii
def split(self, duration, include_remainder=True):
# Convert seconds to timedelta, if appropriate.
duration = seconds_or_timedelta(duration)
assert duration > timedelta(seconds=0), 'cannot call split with a non-positive timedelta'
start = self.start
while start < self.end:
if start + duration <= self.end:
yield MayaInterval(start, start + duration)
elif include_remainder:
yield MayaInterval(start, self.end)
start += duration
def quantize(self, duration, snap_out=False, timezone='UTC'):
"""Returns a quanitzed interval."""
# Convert seconds to timedelta, if appropriate.
duration = seconds_or_timedelta(duration)
timezone = pytz.timezone(timezone)
assert duration > timedelta(seconds=0), 'cannot quantize by non-positive timedelta'
epoch = timezone.localize(Datetime(1970, 1, 1))
seconds = int(duration.total_seconds())
start_seconds = int((self.start.datetime(naive=False) - epoch).total_seconds())
end_seconds = int((self.end.datetime(naive=False) - epoch).total_seconds())
if start_seconds % seconds and not snap_out:
start_seconds += seconds
if end_seconds % seconds and snap_out:
end_seconds += seconds
start_seconds -= start_seconds % seconds
end_seconds -= end_seconds % seconds
if start_seconds > end_seconds:
start_seconds = end_seconds
return MayaInterval(
start=MayaDT.from_datetime(epoch).add(seconds=start_seconds),
end=MayaDT.from_datetime(epoch).add(seconds=end_seconds),
)
def intersection(self, i):
"""Returns the intersection between two intervals."""
start = max(self.start, i.start)
end = min(self.end, i.end)
either_instant = self.is_instant or i.is_instant
instant_overlap = (
self.start == i.start or
start <= end
)
if ((either_instant and instant_overlap) or (start < end)):
return MayaInterval(start, end)
def contains(self, i):
return (
self.start <= i.start and
self.end >= i.end
)
def __contains__(self, item):
if isinstance(item, MayaDT):
return self.contains_dt(item)
return item.contains(self)
def contains_dt(self, dt):
return self.start <= dt < self.end
def is_adjacent(self, i):
return (
self.start == i.end or
self.end == i.start
)
@property
def icalendar(self):
ical_dt_format = '%Y%m%dT%H%M%SZ'
return """
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:{0}
DTEND:{1}
END:VEVENT
END:VCALENDAR
""".format(
self.start.datetime().strftime(ical_dt_format),
self.end.datetime().strftime(ical_dt_format),
).replace(' ', '').strip('\r\n').replace('\n', '\r\n')
@staticmethod
def flatten(ii):
return reduce(lambda reduced, i: (
(reduced[:-1] + i.combine(reduced[-1]))
if reduced else [i]
), sorted(ii), [])
@classmethod
def from_datetime(cls, start_dt=None, end_dt=None, duration=None):
start = MayaDT.from_datetime(start_dt) if start_dt else None
end = MayaDT.from_datetime(end_dt) if end_dt else None
return cls(start=start, end=end, duration=duration)
def now():
@@ -249,6 +522,7 @@ def now():
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
@@ -269,14 +543,35 @@ def when(string, timezone='UTC'):
return MayaDT.from_datetime(dt)
def parse(string, day_first=False):
""""Returns a MayaDT instance for the machine-produced moment specified.
Powered by dateutil. Accepts most known formats. Useful for working with data.
Powered by pendulum. Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
day_first -- if true, the first value (e.g. 01/05/2016) is parsed as day (default: False)
"""
dt = dateutil.parser.parse(string, dayfirst=day_first)
dt = pendulum.parse(string, day_first=day_first)
return MayaDT.from_datetime(dt)
def seconds_or_timedelta(s):
# Convert seconds into timedelta.
if isinstance(s, int):
s = timedelta(seconds=s)
return s
def intervals(start, end, interval):
"""Yields MayaDT objects between the start and end MayaDTs given, at a given interval (seconds or timedelta)."""
interval = seconds_or_timedelta(interval)
current_timestamp = start
while current_timestamp.epoch < end.epoch:
yield current_timestamp
current_timestamp = current_timestamp.add(seconds=interval.seconds)
+4 -5
View File
@@ -28,19 +28,18 @@ required = [
'humanize',
'pytz',
'dateparser',
'iso8601',
'python-dateutil',
'ruamel.yaml',
'tzlocal'
'tzlocal',
'pendulum'
]
setup(
name='maya',
version='0.1.7',
version='0.3.0',
description='Datetimes for Humans.',
long_description=long_description,
author='Kenneth Reitz',
author_email='me@kennethreitz.com',
author_email='me@kennethreitz.org',
url='https://github.com/kennethreitz/maya',
py_modules=['maya'],
install_requires=required,
+71 -2
View File
@@ -1,5 +1,6 @@
import pytest
import copy
from datetime import timedelta
import maya
@@ -18,6 +19,50 @@ def test_iso8601():
assert r == d.iso8601()
def test_parse_iso8601():
string = '20161001T1430.4+05:30'
expected = '2016-10-01T09:00:00.400000Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2016T14'
expected = '2016-01-01T14:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2016-10T14'
expected = '2016-10-01T14:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2012W05'
expected = '2012-01-30T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2012W055'
expected = '2012-02-03T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2012007'
expected = '2012-01-07T00:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
string = '2016-W07T09'
expected = '2016-02-15T09:00:00Z'
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
def test_human_when():
r1 = maya.when('yesterday')
r2 = maya.when('today')
@@ -35,7 +80,7 @@ def test_machine_parse():
def test_dt_tz_translation():
d1 = maya.now().datetime()
d2 = maya.now().datetime(to_timezone='US/Eastern')
d2 = maya.now().datetime(to_timezone='EST')
assert (d1.hour - d2.hour) % 24 == 5
@@ -43,7 +88,7 @@ def test_dt_tz_naive():
d1 = maya.now().datetime(naive=True)
assert d1.tzinfo is None
d2 = maya.now().datetime(to_timezone='US/Eastern', naive=True)
d2 = maya.now().datetime(to_timezone='EST', naive=True)
assert d2.tzinfo is None
assert (d1.hour - d2.hour) % 24 == 5
@@ -158,3 +203,27 @@ def test_comparison_operations():
now > 1
with pytest.raises(TypeError):
now >= 1
def test_intervals():
now = maya.now()
tomorrow = now.add(days=1)
assert len(list(maya.intervals(now, tomorrow, 60*60))) == 24
def test_dunder_add():
now = maya.now()
assert now + 1 == now.add(seconds=1)
assert now + timedelta(seconds=1) == now.add(seconds=1)
def test_dunder_radd():
now = maya.now()
assert now.add(seconds=1) == now + 1
assert now.add(seconds=1) == now + timedelta(seconds=1)
def test_dunder_sub():
now = maya.now()
assert now - 1 == now.subtract(seconds=1)
assert now - timedelta(seconds=1) == now.subtract(seconds=1)
+538
View File
@@ -0,0 +1,538 @@
import random
from datetime import datetime, timedelta
import pytest
import pytz
import maya
Los_Angeles = pytz.timezone('America/Los_Angeles')
New_York = pytz.timezone('America/New_York')
Melbourne = pytz.timezone('Australia/Melbourne')
def test_interval_requires_2_of_start_end_duration():
start = maya.now()
end = start.add(hours=1)
with pytest.raises(ValueError):
maya.MayaInterval(start=start)
with pytest.raises(ValueError):
maya.MayaInterval(end=end)
with pytest.raises(ValueError):
maya.MayaInterval(duration=60)
with pytest.raises(ValueError):
maya.MayaInterval(start=start, end=end, duration=60)
maya.MayaInterval(start=start, end=end)
maya.MayaInterval(start=start, duration=60)
maya.MayaInterval(end=end, duration=60)
def test_interval_requires_end_time_after_or_on_start_time():
maya.MayaInterval(start=maya.now(), duration=0)
with pytest.raises(ValueError):
maya.MayaInterval(start=maya.now(), duration=-1)
def test_interval_init_start_end():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval.start == start
assert interval.end == end
def test_interval_init_start_duration():
start = maya.now()
duration = 1
interval = maya.MayaInterval(start=start, duration=duration)
assert interval.start == start
assert interval.end == start.add(seconds=duration)
def test_interval_init_end_duration():
end = maya.now()
duration = 1
interval = maya.MayaInterval(end=end, duration=duration)
assert interval.end == end
assert interval.start == end.subtract(seconds=duration)
@pytest.mark.parametrize('start_doy1,end_doy1,start_doy2,end_doy2,intersection_doys', (
(0, 2, 1, 3, (1, 2)),
(0, 2, 3, 4, None),
(0, 2, 2, 3, None),
(0, 1, 0, 1, (0, 1)),
(1, 1, 1, 3, (1, 1)),
(1, 1, 1, 1, (1, 1)),
(1, 1, 2, 3, None),
(2, 2, 1, 3, (2, 2)),
(1, 3, 1, 1, (1, 1)),
(2, 3, 1, 1, None),
(1, 3, 2, 2, (2, 2)),
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
'equal',
'instant overlapping start only',
'instant equal',
'instant disjoint',
'instant overlapping',
'instant overlapping start only (left)',
'instant disjoint (left)',
'instant overlapping (left)'
))
def test_interval_intersection(
start_doy1, end_doy1, start_doy2, end_doy2, intersection_doys
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2),
base.add(days=end_doy2),
)
if intersection_doys:
start_doy_intersection, end_doy_intersection = intersection_doys
assert interval1 & interval2 == maya.MayaInterval(
base.add(days=start_doy_intersection),
base.add(days=end_doy_intersection),
)
else:
assert (interval1 & interval2) is None
def test_interval_intersects():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(base, base.add(days=1))
assert interval.intersects(interval)
assert not interval.intersects(maya.MayaInterval(
base.add(days=2),
base.add(days=3),
))
def test_and_operator():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(base, base.add(days=2))
interval2 = maya.MayaInterval(base.add(days=1), base.add(days=3))
assert (
interval1 & interval2 ==
interval2 & interval1 ==
interval1.intersection(interval2)
)
def test_interval_eq_operator():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval == maya.MayaInterval(start=start, end=end)
assert interval != maya.MayaInterval(start=start, end=end.add(days=1))
def test_interval_timedelta():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.timedelta == delta
def test_interval_duration():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.duration == delta.total_seconds()
@pytest.mark.parametrize('start_doy1,end_doy1,start_doy2,end_doy2,expected', (
(0, 2, 1, 3, False),
(0, 2, 3, 4, False),
(0, 2, 2, 3, False),
(0, 1, 0, 1, True),
(0, 3, 1, 2, True),
), ids=(
'overlapping',
'non-overlapping',
'adjacent',
'equal',
'subset',
))
def test_interval_contains(
start_doy1, end_doy1, start_doy2, end_doy2, expected
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(
base.add(days=start_doy1),
base.add(days=end_doy1),
)
interval2 = maya.MayaInterval(
base.add(days=start_doy2),
base.add(days=end_doy2),
)
assert interval1.contains(interval2) is expected
assert (interval1 in interval2) is expected
@pytest.mark.parametrize('start_doy,end_doy,dt_doy,expected', (
(2, 4, 1, False),
(2, 4, 2, True),
(2, 4, 3, True),
(2, 4, 4, False),
(2, 4, 5, False),
), ids=(
'before-start',
'on-start',
'during',
'on-end',
'after-end',
))
def test_interval_in_operator_maya_dt(
start_doy, end_doy, dt_doy, expected
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(
start=base.add(days=start_doy),
end=base.add(days=end_doy),
)
dt = base.add(days=dt_doy)
assert (dt in interval) is expected
def test_interval_hash():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert hash(interval) == hash(maya.MayaInterval(start=start, end=end))
assert hash(interval) != hash(maya.MayaInterval(
start=start, end=end.add(days=1)))
def test_interval_iter():
start = maya.now()
end = start.add(days=1)
assert tuple(maya.MayaInterval(start=start, end=end)) == (start, end)
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 1, 2, 0),
(1, 3, 2, 4, -1),
(2, 4, 1, 3, 1),
(1, 2, 1, 3, -1),
], ids=(
'equal',
'less-than',
'greater-than',
'use-end-time-if-start-time-identical',
))
def test_interval_cmp(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
assert cmp(interval1, interval2) == expected
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 2, 3, [(1, 3)]),
(1, 3, 2, 4, [(1, 4)]),
(1, 2, 3, 4, [(1, 2), (3, 4)]),
(1, 5, 2, 3, [(1, 5)]),
], ids=(
'adjacent',
'overlapping',
'non-overlapping',
'contains',
))
def test_interval_combine(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
expected_intervals = [maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
) for start, end in expected]
assert interval1.combine(interval2) == expected_intervals
assert interval2.combine(interval1) == expected_intervals
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 3, 4, [(1, 2)]),
(1, 2, 2, 4, [(1, 2)]),
(2, 3, 1, 4, []),
(1, 4, 2, 3, [(1, 2), (3, 4)]),
(1, 4, 0, 2, [(2, 4)]),
(1, 4, 3, 5, [(1, 3)]),
(1, 4, 1, 2, [(2, 4)]),
(1, 4, 3, 4, [(1, 3)]),
], ids=(
'non-overlapping',
'adjacent',
'contains',
'splits',
'overlaps-left',
'overlaps-right',
'overlaps-left-identical-start',
'overlaps-right-identical-end',
))
def test_interval_subtract(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
expected_intervals = [maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
) for start, end in expected]
assert interval1.subtract(interval2) == expected_intervals
@pytest.mark.parametrize('start1,end1,start2,end2,expected', [
(1, 2, 2, 3, True),
(2, 3, 1, 2, True),
(1, 3, 2, 3, False),
(2, 3, 4, 5, False),
], ids=(
'adjacent-right',
'adjacent-left',
'overlapping',
'non-overlapping',
))
def test_interval_is_adjacent(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(
start=base.add(days=start1),
end=base.add(days=end1),
)
interval2 = maya.MayaInterval(
start=base.add(days=start2),
end=base.add(days=end2),
)
assert interval1.is_adjacent(interval2) == expected
@pytest.mark.parametrize('start,end,delta,include_remainder,expected', [
(0, 10, 5, False, [(0, 5), (5, 10)]),
(0, 10, 5, True, [(0, 5), (5, 10)]),
(0, 10, 3, False, [(0, 3), (3, 6), (6, 9)]),
(0, 10, 3, True, [(0, 3), (3, 6), (6, 9), (9, 10)]),
(0, 2, 5, False, []),
(0, 2, 5, True, [(0, 2)]),
], ids=(
'even-split',
'even-split-include-partial',
'uneven-split-do-not-include-partial',
'uneven-split-include-partial',
'delta-larger-than-timepsan-do-not-include-partial',
'delta-larger-than-timepsan-include-partial',
))
def test_interval_split(start, end, delta, include_remainder, expected):
base = maya.now()
interval = maya.MayaInterval(
start=base.add(days=start),
end=base.add(days=end),
)
delta = timedelta(days=delta)
expected_intervals = [
maya.MayaInterval(
start=base.add(days=s),
end=base.add(days=e),
) for s, e in expected
]
assert expected_intervals == list(interval.split(
delta, include_remainder=include_remainder))
def test_interval_split_non_positive_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(AssertionError):
list(interval.split(timedelta(seconds=0)))
with pytest.raises(AssertionError):
list(interval.split(timedelta(seconds=-10)))
@pytest.mark.parametrize('start,end,minutes,timezone,snap_out,expected_start,expected_end', [
((5, 12), (8, 48), 30, None, False, (5, 30), (8, 30)),
((5, 12), (8, 48), 30, None, True, (5, 0), (9, 0)),
((5, 15), (9, 0), 15, None, False, (5, 15), (9, 0)),
((5, 15), (9, 0), 15, None, True, (5, 15), (9, 0)),
((6, 50), (9, 15), 60, 'America/New_York', False, (7, 0), (9, 0)),
((6, 50), (9, 15), 60, 'America/New_York', True, (6, 0), (10, 0)),
((6, 20), (6, 50), 60, None, False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, None, True, (6, 0), (7, 0)),
((6, 20), (6, 50), 60, 'America/Chicago', False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, 'America/Chicago', True, (6, 0), (7, 0)),
], ids=(
'normal',
'normal-snap_out',
'already-quantized',
'already-quantized-snap_out',
'with-timezone',
'with-timezone-snap_out',
'too-small',
'too-small-snap_out',
'too-small-with-timezone',
'too-small-with-timezone-snap_out',
))
def test_quantize(start, end, minutes, timezone, snap_out, expected_start, expected_end):
base = maya.MayaDT.from_datetime(datetime(2017, 1, 1))
interval = maya.MayaInterval(
start=base.add(hours=start[0], minutes=start[1]),
end=base.add(hours=end[0], minutes=end[1]),
)
kwargs = {'timezone': timezone} if timezone is not None else {}
quantized_interval = interval.quantize(
timedelta(minutes=minutes),
snap_out=snap_out,
**kwargs
)
assert quantized_interval == maya.MayaInterval(
start=base.add(hours=expected_start[0], minutes=expected_start[1]),
end=base.add(hours=expected_end[0], minutes=expected_end[1]),
)
def test_quantize_invalid_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(AssertionError):
interval.quantize(timedelta(minutes=0))
with pytest.raises(AssertionError):
interval.quantize(timedelta(minutes=-1))
def test_interval_flatten_non_overlapping():
step = 2
max_hour = 20
base = maya.now()
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step - 1),
) for hour in range(0, max_hour, step)]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == sorted(intervals)
def test_interval_flatten_adjacent():
step = 2
max_hour = 20
base = maya.now()
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step),
) for hour in range(0, max_hour, step)]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [maya.MayaInterval(
start=base,
duration=timedelta(hours=max_hour),
)]
def test_interval_flatten_intersecting():
step = 2
max_hour = 20
base = maya.now()
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step, minutes=30),
) for hour in range(0, max_hour, step)]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [maya.MayaInterval(
start=base,
duration=timedelta(hours=max_hour, minutes=30),
)]
def test_interval_flatten_containing():
step = 2
max_hour = 20
base = maya.now()
containing_interval = maya.MayaInterval(
start=base,
end=base.add(hours=max_hour + step),
)
intervals = [maya.MayaInterval(
start=base.add(hours=hour),
duration=timedelta(hours=step - 1),
) for hour in range(2, max_hour, step)]
intervals.append(containing_interval)
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [containing_interval]
def test_interval_from_datetime():
start = maya.now()
duration = timedelta(hours=1)
end = start + duration
interval = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False),
end_dt=end.datetime(naive=False),
)
assert interval.start == start
assert interval.end == end
interval2 = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False),
duration=duration,
)
assert interval2.start == start
assert interval2.end == end
interval3 = maya.MayaInterval.from_datetime(
end_dt=end.datetime(naive=False),
duration=duration,
)
assert interval3.start == start
assert interval3.end == end