Source code for dk.ttcal.day

# -*- coding: utf-8 -*-
"""Date (single day) operations.
"""

import calendar
import datetime
import re
import six
from dk.fstr import fstr
from dk.ttcal.calfns import rangecmp, rangetuple
from .duration import Duration


[docs]class Day(datetime.date): """A calendar date. """ day_name = u'''mandag tirsdag onsdag torsdag fredag lørdag søndag'''.split() day_code = "M U W H F A S".split() def __reduce__(self): return Day, (self.year, self.month, self.day) def __int__(self): return self.toordinal() @classmethod
[docs] def from_idtag(cls, tag): """Return Day from idtag. """ if len(tag) == 9: # d2008022002 y, m, d = map(int, fstr(tag).split(1, 5, 7)[1:]) return cls(y, m, d, membermonth=m) else: # d2008022002 y, m, d, b = map(int, fstr(tag).split(1, 5, 7, 9)[1:]) return cls(y, m, d, membermonth=b)
@classmethod
[docs] def parse(cls, strval): """Parse date value from a string. Allowed syntax include :: yyyy-mm-dd, yyyy-m-dd, yyyy-mm-d, yyyy-m-d dd-mm-yyyy, etc. dd/mm/yyyy, ... dd.mm.yyyy, ... ddmmyyyy """ if not strval or not strval.strip(): # strval is None or contains only spaces return None datere = re.compile(r""" (?:\s*) (?P<isodate> (?P<iso_yr>[12]\d{3}) (?P<sep>[-\./\s]) (?P<iso_mnth>0[1-9]|1[012]|[1-9]) (?P=sep) (?P<iso_day>3[01]|[12]\d|0[1-9]|[1-9])) |(?P<dmy> (?P<dmy_day>3[01]|[12]\d|0[1-9]|\d) (?P<dmy_sep>[-\./\s]) (?P<dmy_mnth>0[1-9]|1[012]|\d) (?P=dmy_sep) (?P<dmy_yr>[12]\d{3})) |(?P<nsp> (?P<nsp_day>3[01]|[12]\d|0[1-9]) (?P<nsp_mnth>0[1-9]|1[012]) (?P<nsp_yr>[12]\d{3})) |(?P<isonsp> (?P<isonsp_yr>20[1-5]\d) (?P<isonsp_mnth>0[1-9]|1[012]) (?P<isonsp_day>3[01]|[12]\d|0[1-9])) |(?P<two> (?P<two_day>3[01]|[12]\d|0[1-9]|\d) (?P<two_sep>[\./\s]) (?P<two_mnth>0[1-9]|1[012]|\d) (?P=two_sep) (?P<two_yr>[1-9]\d)) (?:\s*) """, re.VERBOSE) m = datere.match(strval) if not m: raise ValueError("Cannot parse %r as date." % strval) prefix = '' g = m.groupdict() if g['isodate']: prefix = 'iso' elif g['dmy']: prefix = 'dmy' elif g['nsp']: prefix = 'nsp' elif g['isonsp']: prefix = 'isonsp' elif g['two']: prefix = 'two' day, month, year = [int(g['%s_%s' % (prefix, val)]) for val in ['day', 'mnth', 'yr']] if year < 13: raise ValueError("Cannot parse %r as date." % strval) if year < 100: year += 2000 return cls(year, month, day)
def __new__(cls, *args, **kw): if len(args) == 3: y, m, d = args elif len(args) == 1: t = args[0] y, m, d = t.year, t.month, t.day elif len(args) == 0: t = datetime.date.today() y, m, d = t.year, t.month, t.day else: raise TypeError('incorrect number of arguments') obj = super(Day, cls).__new__(cls, y, m, d) obj.membermonth = kw.get('membermonth', obj.month) return obj @staticmethod
[docs] def get_day_name(daynum, length=None): """Return dayname for daynum. """ if length is None: return Day.day_name[daynum] else: return Day.day_name[daynum][:length]
[docs] def range(self): """Return an iterator for the range of `self`. """ return Days(self.first, self.last)
[docs] def rangetuple(self): return self.datetime(), (self+1).datetime()
[docs] def between_tuple(self): """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) def __hash__(self): return hash('%04s%02s%02s' % (self.year, self.month, self.day)) def __repr__(self): return '%d-%d-%d-%d' % (self.year, self.month, self.day, self.membermonth) def __unicode__(self): return u'%04d-%02d-%02d' % (self.year, self.month, self.day) def __str__(self): # pragma:nocover if six.PY2: return self.__unicode__().encode('u8') elif six.PY3: return self.__unicode__()
[docs] def datetime(self, hour=0, minute=0, second=0): """Extend `self` to datetime. """ return datetime.datetime(self.year, self.month, self.day, hour, minute, second)
[docs] def date(self): """Excplicitly convert to datetime.date. """ return datetime.date(self.year, self.month, self.day)
[docs] def datetuple(self): """Return year, month, day. """ return self.year, self.month, self.day
def __add__(self, n): return Day.fromordinal(self.toordinal() + n) # make first and last properties, because # self.first = self.last = self creates too many cycles :-) @property def first(self): """Define self == self.first for polymorphic usage with other classes. """ return self @property def last(self): """Define self == self.last for polymorphic usage with other classes. """ return self
[docs] def next(self): """Return Tomorrow (for use in templates). """ return self + 1
[docs] def prev(self): """Return Yesterday (for use in templates). """ return self - 1
def __sub__(self, x): """Return number of days between Days or Day n days ago. """ if isinstance(x, Day): return self.toordinal() - x.toordinal() elif isinstance(x, Duration): return Day.fromordinal(self.toordinal() - x.days) elif isinstance(x, six.integer_types): return Day.fromordinal(self.toordinal() - x) else: raise ValueError('Wrong operands for subtraction: %s and %s' % (type(self), type(x))) @property def dayname(self): """The semi-localized name of self. """ return self.day_name[self.weekday] @property def code(self): """One letter code representing the dayname. """ return self.day_code[self.weekday] @property def weeknum(self): """Return the isoweek of `self`. """ return self.isocalendar()[1] @property def isoyear(self): """Return the `isoyear` of `self`. """ return self.isocalendar()[0] # week, Month, and Year, are added later (don't uncomment them here, since # that leads to nasty circular dependencies. # # @property # def week(self): # """Return a Week object representing the week `self` belongs to. # """ # from .week import Week # return Week.weeknum(self.weeknum, self.isoyear) # @property # def Month(self): # """Return a Month object representing the month `self` belongs to. # """ # from .month import Month # return Month(self.year, self.month) # @property # def Year(self): # """Return a Year object representing the year `self` belongs to. # """ # from .year import Year # return Year(self.year) @property def display(self): """Return the 'class' of self. """ res = set() if self.today and (self.membermonth == self.month): res.add('today') if self.in_month: res.add('month') else: res.add('noday') if self.weekend: res.add('weekend') if hasattr(self, 'mark'): res.add(self.mark) return ' '.join(res) @property def idtag(self): """Return the idtag for `self`: dyyyymmddmm. """ return 'd%d%02d%02d%02d' % (self.year, self.month, self.day, self.membermonth) @property def today(self): """True if self is today. """ return self.compare(datetime.date.today()) == 'day' @property def weekday(self): """True if self is a weekday. """ return calendar.weekday(self.year, self.month, self.day) @property def weekend(self): """True if self is Saturday or Sunday. """ return 5 <= self.weekday <= 6 @property def special(self): """True if the database has an entry for this date (sets special_hours). """ return False @property def in_month(self): """True iff the day is in its month. """ return self.month == self.membermonth
[docs] def compare(self, other): """Return how similar self is to other, i.e. the smallest factor they have in common ('day', 'month', or 'year'). Returns None if the Days are in different years. """ if not hasattr(other, 'year'): None if self.year == other.year: if self.month == other.month: if self.day == other.day: return 'day' else: return 'month' else: return 'year' else: return None
def _format(self, fmtchars): # http://blog.tkbe.org/archive/date-filter-cheat-sheet/ simplefmt = { 'y': lambda: str(self.year)[-2:], 'Y': lambda: str(self.year), 'W': lambda: str(self.weeknum), 'w': lambda: str(self.weekday), 'n': lambda: str(self.month), 'm': lambda: '%02d' % self.month, 'b': lambda: self.Month.format('b'), 'M': lambda: self.Month.format('M'), 'N': lambda: self.Month.format('N'), 'F': lambda: self.Month.format('F'), 'j': lambda: str(self.day), 'd': lambda: '%02d' % self.day, 'D': lambda: self.dayname[:3], 'l': lambda: self.dayname, 'z': lambda: str(int(self) - int(Day(self.year, 1, 1))), } ch = "" for ch in fmtchars: yield simplefmt.get(ch, lambda: ch)()
[docs] def format(self, fmt=None): """Emulate Django's date filter. """ if fmt is None: # pylint:disable=C0301 # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-DATE_FORMAT fmt = "N j, Y" tmp = list(self._format(list(fmt))) return ''.join(tmp)
[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)
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]class Today(Day): """Special subclass for today's date. """ def __new__(cls, *args, **kw): t = datetime.date.today() y, m, d = t.year, t.month, t.day obj = super(Today, cls).__new__(cls, y, m, d) obj.membermonth = obj.month return obj today = True
[docs]class Days(list): """A contigous set of days. """ def __init__(self, start, end, start_week=False): super(Days, self).__init__() assert start <= end if start_week: start = start - start.weekday # set to monday for i in range(start.toordinal(), end.toordinal() + 1): self.append(Day.fromordinal(i)) @property def first(self): """1st day """ return self[0] @property def last(self): """last day """ return self[-1]
[docs] def range(self): """Return an iterator for the range of `self`. """ return Days(self.first, self.last)
[docs] def between_tuple(self): """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)
# 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)