# -*- coding: utf-8 -*-
import re
import calendar
import datetime
from .day import Day, Days
from .week import Week
from .calfns import chop, rangecmp, rangetuple
[docs]class Month(object):
"""A calendar month.
"""
month_name = ['', 'Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November',
'Desember']
year = None
month = None
@classmethod
[docs] def from_idtag(cls, tag):
"""Parse idtag into `class`:Month.
"""
# m20082
y = int(tag[1:5])
m = int(tag[5:])
return cls(year=y, month=m)
@classmethod
[docs] def from_date(cls, d):
"""Create a Month from the date ``d``.
"""
return cls(year=d.year, month=d.month)
[docs] def rangetuple(self):
return self.first.datetime(), (self.last + 1).datetime()
@classmethod
[docs] def parse(cls, txt):
"""Parse a textual representation into a Month object.
Format YYYY-MM?
"""
if not txt:
return None
mnth_matcher = re.compile(r"""
(?P<year>\d{4})-?(?P<month>\d{1,2})
""", re.VERBOSE)
m = mnth_matcher.match(txt)
if not m:
raise ValueError(
(u"Ugyldig format, må være åååå-mm, ikke %r." % txt).encode('u8'))
mnth_groups = m.groupdict()
return cls(int(mnth_groups["year"]), int(mnth_groups["month"]))
def __init__(self, year=None, month=None, date=None):
super(Month, self).__init__()
if date is not None:
self.year = date.year
self.month = date.month
elif year is month is date is None:
td = datetime.date.today()
self.year = td.year
self.month = td.month
else:
assert None not in (year, month)
self.year = year
self.month = month
if not (1 <= self.month <= 12):
raise ValueError("Month must be in 1..12.")
self.calendar = calendar.Calendar()
self.name = self.month_name[self.month]
self.short_name = self.name[:3]
# self.short_name = calendar.month_abbr[self.month]
self.weeks = [Week(days, self.month) for days in self._weeks()]
# self.day = 1
def __call__(self, daynum=None):
"""Return the given Day for this year.
Usage::
return ttcal.Year().december(23)
"""
if daynum is None: # pragma:nocover
return self # for when django tries to do value = value() *sigh*
return Day(self.year, self.month, daynum)
def __reduce__(self):
"""Deepcopy helper.
"""
return Month, (self.year, self.month)
def __unicode__(self):
return u"%04d-%02d" % (self.year, self.month)
# @property
# def Year(self):
# """Return a Year object for the year-part of this month.
# """
# return Year(self.year)
@property
def Month(self):
return self
def __hash__(self):
return self.year * 100 + self.month
# def __eq__(self, other):
# noinspection PyBroadException
# try:
# return self.year == other.year and self.month == other.month
# except:
# return False
def __len__(self):
_, n = calendar.monthrange(self.year, self.month)
return n
[docs] def datetuple(self):
"""First date in month.
"""
return self.year, self.month, 1
def __lt__(self, other):
othr = rangetuple(other)
if othr is other:
return False
return rangecmp(self.rangetuple(), othr) < 0
def __le__(self, other):
othr = rangetuple(other)
if othr is other:
return False
return rangecmp(self.rangetuple(), othr) <= 0
def __eq__(self, other):
othr = rangetuple(other)
if othr is other:
return False
return rangecmp(self.rangetuple(), othr) == 0
def __gt__(self, other):
othr = rangetuple(other)
if othr is other:
return False
return rangecmp(self.rangetuple(), othr) > 0
def __ge__(self, other):
othr = rangetuple(other)
if othr is other:
return False
return rangecmp(self.rangetuple(), othr) >= 0
[docs] def numdays(self): # for use in template
"""The number of days in the month.
"""
return len(self)
def __add__(self, n):
"""Add n months to self.
"""
me = self.year * 12 + (self.month - 1)
me += n
q, r = divmod(me, 12)
return Month(q, r + 1)
def __radd__(self, n):
return self + n
def __sub__(self, n):
if isinstance(n, Month):
first, last = min(self, n), max(self, n)
ydiff = last.year - first.year
mdiff = last.month - first.month
res = 12 * ydiff + mdiff
if self > n:
return res
return -res
return self + (-n)
# rsub doesn't make sense
def __repr__(self):
return 'Month(%s, %s)' % (self.year, self.month)
def __str__(self):
return '%04d-%02d' % (self.year, self.month)
# NOTE: Django's query engine calls both __call__ and __iter__ on values
# that are passed in, and uses the return values instead of the value
# itself (i.e. with the implementation below, the queryset would get
# a list of Week objects instead of a Month object).
# NB: W:\srv\venv\dev\Lib\site-packages\django\db\models\sql\where.py
# NB: temp comment
# NB: if is_iterator(value):
# NB: # Consume any generators immediately, so that we can determine
# NB: # emptiness and transform any non-empty values correctly.
# NB: value = list(value)
# def __iter__(self):
# return iter(self.weeks)
[docs] def dayiter(self):
for wk in iter(self.weeks):
for day in wk:
yield day
[docs] def days(self):
"""Return a list of days (`class`:ttcal.Day) in this month.
"""
res = []
for wk in iter(self.weeks):
for day in wk:
if day.month == self.month:
res.append(day) # yield day
return res
[docs] def idtag(self):
"""Return a text representation that is parsable by the from_idtag
function (above), and is useable as part of an url.
"""
return 'm%d%d' % (self.year, self.month)
@property
def daycount(self):
"""The number of days in this month (as an int).
"""
n = calendar.mdays[self.month]
if self.month == 2 and calendar.isleap(self.year):
n += 1
return n
[docs] def prev(self):
"""Previous month.
"""
return self - 1
[docs] def next(self):
"""Next month.
"""
return self + 1
@property
def first(self):
"""First day in month.
"""
return Day(self.year, self.month, 1)
@property
def last(self):
"""Last day in month.
"""
return Day(self.year, self.month, self.daycount)
def _weeks(self):
c = self.calendar
return chop(c.itermonthdates(self.year, self.month), 7)
def __contains__(self, date):
return self.year == date.year and self.month == date.month
def __getitem__(self, day):
for wk in self.weeks:
for d in wk:
if d.compare(day) == 'day':
return d
raise KeyError
[docs] def mark(self, d, value='mark', method='replace'):
try:
day = self[d]
if method == 'replace':
day.mark = value
elif method == 'append':
if hasattr(day, 'mark'):
day.mark += value
else:
day.mark = value
except KeyError: # pragma:nocover
pass
[docs] def marked_days(self):
for wk in self.weeks:
for d in wk:
if hasattr(d, 'mark'):
yield d
def _format(self, fmtchars):
# http://blog.tkbe.org/archive/date-filter-cheat-sheet/
for ch in fmtchars:
if ch == 'y':
yield str(self.year)[-2:]
elif ch == 'Y':
yield str(self.year)
elif ch == 'n':
yield str(self.month)
elif ch == 'm':
yield '%02d' % self.month
elif ch == 'b':
yield self.name[:3].lower()
elif ch == 'M':
yield self.name[:3]
elif ch == 'N':
# should be AP style, but doesn't make sense outside US.
yield self.name[:3]
elif ch == 'F':
yield self.name
else:
yield ch
[docs] def range(self):
"""Return an iterator for the range of `self`.
"""
# if hasattr(self, 'dayiter'):
# return self.dayiter()
return Days(self.first, self.last)
[docs] 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 middle(self):
"""Return the day that splits the date range in half.
"""
middle = (self.first.toordinal() + self.last.toordinal()) // 2
return Day.fromordinal(middle)
[docs] 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)
# noinspection PyPep8Naming
def _Month(self):
"""Return a Month object representing the month `self` belongs to.
"""
return Month(self.year, self.month)
Day.Month = property(_Month)