Skip to content

Commit 269f0ae

Browse files
committed
A minimal implementation of quarters so that we can use quarters in idtags.
1 parent 45d4344 commit 269f0ae

File tree

3 files changed

+306
-1
lines changed

3 files changed

+306
-1
lines changed

Diff for: tests/test_ttcal_quarter.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from datetime import date, datetime
2+
import ttcal
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def quarters():
8+
return [
9+
ttcal.Quarter(2005, 1),
10+
ttcal.Quarter(),
11+
ttcal.Quarter(2025, 4),
12+
]
13+
14+
15+
def test_stringification(quarters):
16+
assert str(quarters[0]) == '1'
17+
18+
19+
def test_timetuple(quarters):
20+
assert quarters[0].timetuple() == datetime(2005, 1, 1, 0, 0, 0)
21+
22+
23+
def test_range(quarters):
24+
assert len(list(quarters[0].range())) == 90
25+
26+
27+
def test_between_tuple(quarters):
28+
a, b = quarters[0].between_tuple()
29+
assert a < b
30+
31+
32+
def test_middle(quarters):
33+
assert quarters[0].middle == (ttcal.Day(2005, 2, 14))
34+
35+
36+
def test_unicode(quarters):
37+
assert repr(quarters[0]) == 'Q(20051)'
38+
assert str(quarters[0]) == '1'
39+
40+
41+
def test_month(quarters):
42+
assert quarters[0].Month == ttcal.Month(2005, 1)
43+
44+
45+
def test_quarter(quarters):
46+
assert int(quarters[0]) == int(quarters[0].Quarter)
47+
48+
49+
def test_hash(quarters):
50+
assert hash(quarters[0]) == hash(ttcal.Quarter(2005, 1))
51+
52+
53+
def test_from_idtag(quarters):
54+
"""Test of the from_idtag method.
55+
"""
56+
assert quarters[0].from_idtag('q20051') == quarters[0]
57+
58+
59+
def test_idtag(quarters):
60+
"""Test of the idtag method.
61+
"""
62+
assert quarters[2].idtag() == 'q20254'
63+
64+
65+
def test_add(quarters):
66+
"""Test of the __add__ method.
67+
"""
68+
assert quarters[0] + 2 == ttcal.Quarter(2005, 3)
69+
assert 2 + quarters[0] == ttcal.Quarter(2005, 3)
70+
71+
72+
def test_sub(quarters):
73+
"""Test of the __sub__ method.
74+
"""
75+
assert quarters[2] - 3 == ttcal.Quarter(2025, 1)
76+
77+
78+
def test_prev(quarters):
79+
"""Test of the prev method.
80+
"""
81+
assert quarters[2].prev() == ttcal.Quarter(2025, 3)
82+
83+
84+
def test_next(quarters):
85+
"""Test of the next method.
86+
"""
87+
assert quarters[0].next() == ttcal.Quarter(2005, 2)
88+
89+
90+
def test_format(quarters):
91+
assert quarters[0].format('q') == '1'
92+
assert quarters[0].format('Q') == '2005Q1'
93+
assert quarters[0].format() == '2005Q1'

Diff for: ttcal/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88
from .month import Month
99
from .week import Week
1010
from .year import Year
11+
from .quarter import Quarter
1112

1213

1314
def from_idtag(idtag):
1415
"""Return a class from idtag.
1516
"""
1617
assert len(idtag) > 1
17-
assert idtag[0] in 'wdmy'
18+
assert idtag[0] in 'wdmqy'
1819

1920
return {
2021
'w': Week,
2122
'd': Day,
2223
'm': Month,
24+
'q': Quarter,
2325
'y': Year,
2426
}[idtag[0]].from_idtag(idtag)

Diff for: ttcal/quarter.py

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""
2+
quarter class.
3+
"""
4+
import datetime
5+
6+
from . import Month
7+
from .calfns import chop, rangecmp, rangetuple
8+
from .day import Day
9+
from .year import Year
10+
11+
12+
class Quarter: # pylint:disable=too-many-public-methods
13+
"""A single quarter.
14+
"""
15+
def __init__(self, year=None, quarter=None):
16+
super().__init__()
17+
# if quarter is None:
18+
if year is None:
19+
year = datetime.date.today().year
20+
if quarter is None:
21+
quarter = 1
22+
self.year = year
23+
self.quarter = quarter
24+
self.months = Year(year).quarters()[self.quarter-1]
25+
26+
def __int__(self):
27+
return self.quarter
28+
29+
def range(self):
30+
"""Return an iterator for the range of `self`.
31+
"""
32+
return self.dayiter()
33+
34+
def rangetuple(self):
35+
"""Return a pair of datetime objects containing quarter
36+
(in a half-open interval).
37+
"""
38+
return self.first.datetime(), (self + 1).first.datetime()
39+
40+
# def __lt__(self, other):
41+
# if isinstance(other, int):
42+
# return self.quarter < other
43+
# othr = rangetuple(other)
44+
# if othr is other:
45+
# return False
46+
# return rangecmp(self.rangetuple(), othr) < 0
47+
#
48+
# def __le__(self, other):
49+
# if isinstance(other, int):
50+
# return self.quarter <= other
51+
# othr = rangetuple(other)
52+
# if othr is other:
53+
# return False
54+
# return rangecmp(self.rangetuple(), othr) <= 0
55+
56+
def __eq__(self, other):
57+
if isinstance(other, int):
58+
return self.quarter == other
59+
othr = rangetuple(other)
60+
if othr is other:
61+
return False
62+
return rangecmp(self.rangetuple(), othr) == 0
63+
64+
def __ne__(self, other):
65+
return not self == other
66+
67+
# def __gt__(self, other):
68+
# if isinstance(other, int):
69+
# return self.quarter > other
70+
# othr = rangetuple(other)
71+
# if othr is other:
72+
# return False
73+
# return rangecmp(self.rangetuple(), othr) > 0
74+
#
75+
# def __ge__(self, other):
76+
# if isinstance(other, int):
77+
# return self.quarter >= other
78+
# othr = rangetuple(other)
79+
# if othr is other:
80+
# return False
81+
# return rangecmp(self.rangetuple(), othr) >= 0
82+
83+
def timetuple(self):
84+
"""Returns a datetime at 00:00:00 on January 1st.
85+
"""
86+
d = datetime.date(*self.first.datetuple())
87+
t = datetime.time()
88+
return datetime.datetime.combine(d, t)
89+
90+
@property
91+
def first(self):
92+
# The negative indexing here is due to the fact that the
93+
# first quarter is list element 0 and so on.
94+
return self.Year.quarters()[self.quarter-1][0].first
95+
96+
@property
97+
def last(self):
98+
return self.Year.quarters()[self.quarter-1][2].last
99+
100+
def between_tuple(self): # pylint:disable=E0213
101+
"""Return a tuple of datetimes that is convenient for sql
102+
`between` queries.
103+
"""
104+
return (self.first.datetime(),
105+
(self.last + 1).datetime() - datetime.timedelta(seconds=1))
106+
107+
@property
108+
def Year(self):
109+
"""Return the year (for api completeness).
110+
"""
111+
return Year(self.year)
112+
113+
@property
114+
def Month(self):
115+
"""For orthogonality in the api.
116+
"""
117+
return self.months[0]
118+
119+
@property
120+
def middle(self):
121+
"""Return the day that splits the date range in half.
122+
"""
123+
middle = (self.first.toordinal() + self.last.toordinal()) // 2
124+
return Day.fromordinal(middle)
125+
126+
# def timetuple(self):
127+
# """Create timetuple from datetuple.
128+
# (to interact with datetime objects).
129+
# """
130+
# d = datetime.date(*self.datetuple())
131+
# t = datetime.time()
132+
# return datetime.datetime.combine(d, t)
133+
134+
def __repr__(self):
135+
return f'Q({self.year}{self.quarter})'
136+
137+
def __str__(self): # pragma: nocover
138+
return str(self.quarter)
139+
140+
@property
141+
def Quarter(self):
142+
"""Return the quarter (for api completeness).
143+
"""
144+
return self
145+
146+
@classmethod
147+
def from_idtag(cls, tag):
148+
"""quarter tags have the lower-case letter y + the four digit quarter,
149+
eg. q20081.
150+
"""
151+
y = int(tag[1:5])
152+
q = int(tag[5])
153+
return cls(year=y, quarter=q)
154+
155+
def idtag(self):
156+
"""quarter tags have the lower-case letter y + the four digit quarter,
157+
eg. y2008.
158+
"""
159+
return f'q{self.year}{self.quarter}'
160+
161+
def __add__(self, n):
162+
"""Add n quarters to self.
163+
"""
164+
return Quarter(self.year, self.quarter + n)
165+
166+
def __radd__(self, n):
167+
return self + n
168+
169+
def __sub__(self, n):
170+
return self + (-n)
171+
172+
# rsub doesn't make sense
173+
174+
def prev(self):
175+
"""Previous quarter.
176+
"""
177+
return self - 1
178+
179+
def next(self):
180+
"""Next quarter.
181+
"""
182+
return self + 1
183+
184+
def __hash__(self):
185+
return self.quarter
186+
187+
def dayiter(self):
188+
"""Yield all days in all months in quarter.
189+
"""
190+
for m in self.months:
191+
yield from m.days()
192+
193+
def _format(self, fmtchars):
194+
# http://blog.tkbe.org/archive/date-filter-cheat-sheet/
195+
for ch in fmtchars:
196+
if ch == 'q':
197+
yield str(self.quarter)
198+
elif ch == 'Q':
199+
yield f'{str(self.year)}Q{self.quarter}'
200+
else:
201+
yield ch
202+
203+
def format(self, fmt=None):
204+
"""Format according to format string. Default format is
205+
four-digit-year and quearter-number.
206+
"""
207+
if fmt is None:
208+
fmt = "Q"
209+
tmp = list(self._format(list(fmt)))
210+
return ''.join(tmp)

0 commit comments

Comments
 (0)