-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A minimal implementation of quarters so that we can use quarters in i…
…dtags.
- Loading branch information
1 parent
45d4344
commit 269f0ae
Showing
3 changed files
with
306 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
from datetime import date, datetime | ||
import ttcal | ||
import pytest | ||
|
||
|
||
@pytest.fixture | ||
def quarters(): | ||
return [ | ||
ttcal.Quarter(2005, 1), | ||
ttcal.Quarter(), | ||
ttcal.Quarter(2025, 4), | ||
] | ||
|
||
|
||
def test_stringification(quarters): | ||
assert str(quarters[0]) == '1' | ||
|
||
|
||
def test_timetuple(quarters): | ||
assert quarters[0].timetuple() == datetime(2005, 1, 1, 0, 0, 0) | ||
|
||
|
||
def test_range(quarters): | ||
assert len(list(quarters[0].range())) == 90 | ||
|
||
|
||
def test_between_tuple(quarters): | ||
a, b = quarters[0].between_tuple() | ||
assert a < b | ||
|
||
|
||
def test_middle(quarters): | ||
assert quarters[0].middle == (ttcal.Day(2005, 2, 14)) | ||
|
||
|
||
def test_unicode(quarters): | ||
assert repr(quarters[0]) == 'Q(20051)' | ||
assert str(quarters[0]) == '1' | ||
|
||
|
||
def test_month(quarters): | ||
assert quarters[0].Month == ttcal.Month(2005, 1) | ||
|
||
|
||
def test_quarter(quarters): | ||
assert int(quarters[0]) == int(quarters[0].Quarter) | ||
|
||
|
||
def test_hash(quarters): | ||
assert hash(quarters[0]) == hash(ttcal.Quarter(2005, 1)) | ||
|
||
|
||
def test_from_idtag(quarters): | ||
"""Test of the from_idtag method. | ||
""" | ||
assert quarters[0].from_idtag('q20051') == quarters[0] | ||
|
||
|
||
def test_idtag(quarters): | ||
"""Test of the idtag method. | ||
""" | ||
assert quarters[2].idtag() == 'q20254' | ||
|
||
|
||
def test_add(quarters): | ||
"""Test of the __add__ method. | ||
""" | ||
assert quarters[0] + 2 == ttcal.Quarter(2005, 3) | ||
assert 2 + quarters[0] == ttcal.Quarter(2005, 3) | ||
|
||
|
||
def test_sub(quarters): | ||
"""Test of the __sub__ method. | ||
""" | ||
assert quarters[2] - 3 == ttcal.Quarter(2025, 1) | ||
|
||
|
||
def test_prev(quarters): | ||
"""Test of the prev method. | ||
""" | ||
assert quarters[2].prev() == ttcal.Quarter(2025, 3) | ||
|
||
|
||
def test_next(quarters): | ||
"""Test of the next method. | ||
""" | ||
assert quarters[0].next() == ttcal.Quarter(2005, 2) | ||
|
||
|
||
def test_format(quarters): | ||
assert quarters[0].format('q') == '1' | ||
assert quarters[0].format('Q') == '2005Q1' | ||
assert quarters[0].format() == '2005Q1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
""" | ||
quarter class. | ||
""" | ||
import datetime | ||
|
||
from . import Month | ||
from .calfns import chop, rangecmp, rangetuple | ||
from .day import Day | ||
from .year import Year | ||
|
||
|
||
class Quarter: # pylint:disable=too-many-public-methods | ||
"""A single quarter. | ||
""" | ||
def __init__(self, year=None, quarter=None): | ||
super().__init__() | ||
# if quarter is None: | ||
if year is None: | ||
year = datetime.date.today().year | ||
if quarter is None: | ||
quarter = 1 | ||
self.year = year | ||
self.quarter = quarter | ||
self.months = Year(year).quarters()[self.quarter-1] | ||
|
||
def __int__(self): | ||
return self.quarter | ||
|
||
def range(self): | ||
"""Return an iterator for the range of `self`. | ||
""" | ||
return self.dayiter() | ||
|
||
def rangetuple(self): | ||
"""Return a pair of datetime objects containing quarter | ||
(in a half-open interval). | ||
""" | ||
return self.first.datetime(), (self + 1).first.datetime() | ||
|
||
# def __lt__(self, other): | ||
# if isinstance(other, int): | ||
# return self.quarter < other | ||
# othr = rangetuple(other) | ||
# if othr is other: | ||
# return False | ||
# return rangecmp(self.rangetuple(), othr) < 0 | ||
# | ||
# def __le__(self, other): | ||
# if isinstance(other, int): | ||
# return self.quarter <= other | ||
# othr = rangetuple(other) | ||
# if othr is other: | ||
# return False | ||
# return rangecmp(self.rangetuple(), othr) <= 0 | ||
|
||
def __eq__(self, other): | ||
if isinstance(other, int): | ||
return self.quarter == other | ||
othr = rangetuple(other) | ||
if othr is other: | ||
return False | ||
return rangecmp(self.rangetuple(), othr) == 0 | ||
|
||
def __ne__(self, other): | ||
return not self == other | ||
|
||
# def __gt__(self, other): | ||
# if isinstance(other, int): | ||
# return self.quarter > other | ||
# othr = rangetuple(other) | ||
# if othr is other: | ||
# return False | ||
# return rangecmp(self.rangetuple(), othr) > 0 | ||
# | ||
# def __ge__(self, other): | ||
# if isinstance(other, int): | ||
# return self.quarter >= other | ||
# othr = rangetuple(other) | ||
# if othr is other: | ||
# return False | ||
# return rangecmp(self.rangetuple(), othr) >= 0 | ||
|
||
def timetuple(self): | ||
"""Returns a datetime at 00:00:00 on January 1st. | ||
""" | ||
d = datetime.date(*self.first.datetuple()) | ||
t = datetime.time() | ||
return datetime.datetime.combine(d, t) | ||
|
||
@property | ||
def first(self): | ||
# The negative indexing here is due to the fact that the | ||
# first quarter is list element 0 and so on. | ||
return self.Year.quarters()[self.quarter-1][0].first | ||
|
||
@property | ||
def last(self): | ||
return self.Year.quarters()[self.quarter-1][2].last | ||
|
||
def between_tuple(self): # pylint:disable=E0213 | ||
"""Return a tuple of datetimes that is convenient for sql | ||
`between` queries. | ||
""" | ||
return (self.first.datetime(), | ||
(self.last + 1).datetime() - datetime.timedelta(seconds=1)) | ||
|
||
@property | ||
def Year(self): | ||
"""Return the year (for api completeness). | ||
""" | ||
return Year(self.year) | ||
|
||
@property | ||
def Month(self): | ||
"""For orthogonality in the api. | ||
""" | ||
return self.months[0] | ||
|
||
@property | ||
def middle(self): | ||
"""Return the day that splits the date range in half. | ||
""" | ||
middle = (self.first.toordinal() + self.last.toordinal()) // 2 | ||
return Day.fromordinal(middle) | ||
|
||
# def timetuple(self): | ||
# """Create timetuple from datetuple. | ||
# (to interact with datetime objects). | ||
# """ | ||
# d = datetime.date(*self.datetuple()) | ||
# t = datetime.time() | ||
# return datetime.datetime.combine(d, t) | ||
|
||
def __repr__(self): | ||
return f'Q({self.year}{self.quarter})' | ||
|
||
def __str__(self): # pragma: nocover | ||
return str(self.quarter) | ||
|
||
@property | ||
def Quarter(self): | ||
"""Return the quarter (for api completeness). | ||
""" | ||
return self | ||
|
||
@classmethod | ||
def from_idtag(cls, tag): | ||
"""quarter tags have the lower-case letter y + the four digit quarter, | ||
eg. q20081. | ||
""" | ||
y = int(tag[1:5]) | ||
q = int(tag[5]) | ||
return cls(year=y, quarter=q) | ||
|
||
def idtag(self): | ||
"""quarter tags have the lower-case letter y + the four digit quarter, | ||
eg. y2008. | ||
""" | ||
return f'q{self.year}{self.quarter}' | ||
|
||
def __add__(self, n): | ||
"""Add n quarters to self. | ||
""" | ||
return Quarter(self.year, self.quarter + n) | ||
|
||
def __radd__(self, n): | ||
return self + n | ||
|
||
def __sub__(self, n): | ||
return self + (-n) | ||
|
||
# rsub doesn't make sense | ||
|
||
def prev(self): | ||
"""Previous quarter. | ||
""" | ||
return self - 1 | ||
|
||
def next(self): | ||
"""Next quarter. | ||
""" | ||
return self + 1 | ||
|
||
def __hash__(self): | ||
return self.quarter | ||
|
||
def dayiter(self): | ||
"""Yield all days in all months in quarter. | ||
""" | ||
for m in self.months: | ||
yield from m.days() | ||
|
||
def _format(self, fmtchars): | ||
# http://blog.tkbe.org/archive/date-filter-cheat-sheet/ | ||
for ch in fmtchars: | ||
if ch == 'q': | ||
yield str(self.quarter) | ||
elif ch == 'Q': | ||
yield f'{str(self.year)}Q{self.quarter}' | ||
else: | ||
yield ch | ||
|
||
def format(self, fmt=None): | ||
"""Format according to format string. Default format is | ||
four-digit-year and quearter-number. | ||
""" | ||
if fmt is None: | ||
fmt = "Q" | ||
tmp = list(self._format(list(fmt))) | ||
return ''.join(tmp) |