918 lines
34 KiB
Python
918 lines
34 KiB
Python
# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
|
|
|
"""Des fonctions pour gérer les dates.
|
|
"""
|
|
|
|
__all__ = ('pydate', 'pydatetime',
|
|
'datef',
|
|
'YEAR_DATEF', 'ISO_DATEF', 'NUM_DATEF',
|
|
'FR_DATEF', 'FRHM_DATEF', 'FRTS_DATEF',
|
|
'TW_DATEF', 'NUMTS_DATEF',
|
|
'Date', 'isdate', 'isanydate',
|
|
'parse_date', 'ensure_date',
|
|
'rfc2822',
|
|
'DateSpec', 'DateSpecs',
|
|
)
|
|
|
|
import re
|
|
import time as time_mod
|
|
from time import time, localtime, gmtime, asctime
|
|
from datetime import date as pydate, datetime as pydatetime, timedelta
|
|
|
|
from .base import isstr, isnum, isseq
|
|
from .uio import _s, _u
|
|
|
|
dateformat_map = {'%Y': '%(y)04i',
|
|
'%m': '%(m)02i',
|
|
'%d': '%(d)02i',
|
|
'%H': '%(H)02i',
|
|
'%M': '%(M)02i',
|
|
'%S': '%(S)02i',
|
|
}
|
|
YEAR_DATEF = '%Y'
|
|
ISO_DATEF = '%Y-%m-%d'
|
|
NUM_DATEF = '%Y%m%d'
|
|
FR_DATEF = '%d/%m/%Y'
|
|
FRHM_DATEF = '%d/%m/%Y-%H:%M'
|
|
FRTS_DATEF = '%d/%m/%Y-%H:%M:%S'
|
|
TW_DATEF = '%Y%m%d%H%M'
|
|
NUMTS_DATEF = '%Y%m%d%H%M%S'
|
|
|
|
def datef(format=None, t=None):
|
|
"""Retourner la date avec le format indiqué.
|
|
|
|
On peut utiliser les formats suivants:
|
|
|
|
%Y année (4 digits)
|
|
%m mois (2 digits)
|
|
%d jour (2 digits)
|
|
%H heure (2 digits)
|
|
%M minutes (2 digits)
|
|
%S secondes (2 digits)
|
|
"""
|
|
if format is None: format = FR_DATEF
|
|
if t is None: t = time()
|
|
|
|
y, m, d, H, M, S, W, J, dst = localtime(t)
|
|
for fr, to in dateformat_map.items():
|
|
format = format.replace(fr, to)
|
|
return format % locals()
|
|
|
|
def _fix_year(year, thisyear=None):
|
|
if year < 100:
|
|
if thisyear is None: thisyear = Date().year
|
|
year = year + thisyear - thisyear % 100
|
|
elif year < 1000:
|
|
if thisyear is None: thisyear = Date().year
|
|
year = year + thisyear - thisyear % 1000
|
|
return year
|
|
def _isleap(year):
|
|
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
|
def _fix_month(year, month):
|
|
month -= 1
|
|
while month > 11:
|
|
month -= 12
|
|
year += 1
|
|
while month < 0:
|
|
month += 12
|
|
year -= 1
|
|
month += 1
|
|
return year, month
|
|
MONTHDAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
def _monthdays(year, month, offset=0):
|
|
year, month = _fix_month(year, month + offset)
|
|
if month == 2 and _isleap(year): leapday = 1
|
|
else: leapday = 0
|
|
return MONTHDAYS[month] + leapday
|
|
def _fix_day(year, month, day):
|
|
# on assume que month est déjà "fixé"
|
|
day -= 1
|
|
while day > _monthdays(year, month) - 1:
|
|
day -= _monthdays(year, month)
|
|
year, month = _fix_month(year, month + 1)
|
|
while day < 0:
|
|
year, month = _fix_month(year, month - 1)
|
|
day += _monthdays(year, month)
|
|
day += 1
|
|
return year, month, day
|
|
def _fix_date(day, month, year):
|
|
year, month = _fix_month(year, month)
|
|
year, month, day = _fix_day(year, month, day)
|
|
return day, month, year
|
|
|
|
MONTHNAMES = [u"Janvier", u"Février", u"Mars", u"Avril", u"Mai", u"Juin",
|
|
u"Juillet", u"Août", u"Septembre", u"Octobre", u"Novembre", u"Décembre",
|
|
]
|
|
MONTHNAMES3 = [u"Jan", u"Fév", u"Mar", u"Avr", u"Mai", u"Jun",
|
|
u"Jul", u"Aoû", u"Sep", u"Oct", u"Nov", u"Déc",
|
|
]
|
|
MONTHNAMES1 = [u"J", u"F", u"M", u"A", u"M", u"J",
|
|
u"J", u"A", u"S", u"O", u"N", u"D",
|
|
]
|
|
|
|
class Date(object):
|
|
"""Un wrapper pour 'datetime.date'.
|
|
|
|
Attention! Cet objet est mutable, il ne faut donc pas l'utiliser comme clé
|
|
dans un dictionnaire.
|
|
"""
|
|
_d = None
|
|
|
|
def __init__(self, day=None, month=None, year=None, t=None):
|
|
"""Initialiser l'objet.
|
|
|
|
Dans l'ordre, les champs considérés sont:
|
|
- day si c'est une instance de Date ou datetime.date
|
|
- t le nombre de secondes depuis l'epoch, comme retourné par
|
|
time.time(). Cette valeur est fusionnée avec les valeurs numériques
|
|
day, month, year.
|
|
"""
|
|
if day is not None and not isnum(day) and month is None and year is None and t is None:
|
|
if isinstance(day, pydatetime): day = day.date()
|
|
if isinstance(day, pydate): self._d = day
|
|
elif isinstance(day, Date): self._d = day._d
|
|
if self._d is None:
|
|
if t is None: t = time()
|
|
y, m, d = localtime(t)[:3]
|
|
if year is None: year = y
|
|
if month is None: month = m
|
|
if day is None: day = d
|
|
day, month, year = _fix_date(day, month, year)
|
|
self._d = pydate(year, month, day)
|
|
|
|
date = property(lambda self: self._d)
|
|
year = property(lambda self: self._d.year)
|
|
month = property(lambda self: self._d.month)
|
|
day = property(lambda self: self._d.day)
|
|
|
|
# nombre de jours du mois
|
|
monthdays = property(lambda self: MONTHDAYS[self.month])
|
|
|
|
def weekday(self):
|
|
"""Retourner le jour de la semaine, de 0 (lundi) à 6 (dimanche)
|
|
"""
|
|
return self._d.weekday()
|
|
def isoweekday(self):
|
|
"""Retourner le jour de la semaine, de 1 (lundi) à 7 (dimanche)
|
|
"""
|
|
return self._d.isoweekday()
|
|
def is_today(self):
|
|
"""Tester si cette date est le jour d'aujourd'hui
|
|
"""
|
|
now = self.__class__()._d
|
|
date = self._d
|
|
return now.year == date.year and now.month == date.month and now.day == date.day
|
|
|
|
def calday(self, show_month=False, show_year=False):
|
|
"""Retourner 'day' si day != 1 and not show_month and not show_year,
|
|
'day/month' si month != 1 and not show_year,
|
|
'day/month/year' sinon
|
|
"""
|
|
day, month, year = self.day, self.month, self.year
|
|
if day != 1 and not show_month and not show_year: return _u(day)
|
|
elif month != 1 and not show_year: return u"%i/%i" % (day, month)
|
|
else: return u"%i/%i/%i" % (day, month, year)
|
|
|
|
def monthname(self, format=None):
|
|
"""Obtenir le nom du mois.
|
|
Si format est dans (1, 't', 'tiny'), retourner le nom sur 1 lettre.
|
|
Si format est dans (3, 's', 'small'), retourner le nom sur 3 lettres.
|
|
Sinon, retourner le nom complet.
|
|
"""
|
|
if format in (1, 't', 'tiny'): names = MONTHNAMES1
|
|
elif format in (3, 's', 'small'): names = MONTHNAMES3
|
|
else: names = MONTHNAMES
|
|
return names[self.month - 1]
|
|
|
|
__monthname1 = lambda self: self.monthname(1)
|
|
__monthname3 = lambda self: self.monthname(3)
|
|
FORMAT_MAP = {'%Y': '%(y)04i', '%m': '%(m)02i', '%d': '%(d)02i',
|
|
'%H': '%(H)02i', '%M': '%(M)02i', '%S': '%(S)02i',
|
|
'%1m': __monthname1, '%3m': __monthname3, '%fm': monthname,
|
|
'%C': calday,
|
|
}
|
|
def format(self, format=None):
|
|
"""Formater la date pour affichage.
|
|
|
|
Les champs valides sont %Y, %m, %d qui correspondent à la date de cet
|
|
objet, %H, %M, %S qui valent toujours 0, et %1m, %3m, %fm, %C, qui
|
|
correspondent respectivement à self.monthname(1), self.monthname(3),
|
|
self.monthname(), self.calday().
|
|
"""
|
|
if format is None: format = FR_DATEF
|
|
y, m, d, H, M, S = self.year, self.month, self.day, 0, 0, 0
|
|
for fr, to in self.FORMAT_MAP.items():
|
|
if callable(to): to = to(self)
|
|
format = format.replace(fr, to)
|
|
return format % locals()
|
|
|
|
def set(self, day=None, month=None, year=None):
|
|
kw = {}
|
|
for name, value in [('day', day), ('month', month), ('year', year)]:
|
|
if value is not None: kw[name] = value
|
|
self._d = self._d.replace(**kw)
|
|
return self
|
|
|
|
def set_weekday(self, weekday=0):
|
|
if self.weekday() != weekday:
|
|
day = self.day + weekday - self.weekday()
|
|
self.set(*_fix_date(day, self.month, self.year))
|
|
return self
|
|
|
|
def set_isoweekday(self, isoweekday=1):
|
|
if self.isoweekday() != isoweekday:
|
|
day = self.day + isoweekday - self.isoweekday()
|
|
self.set(*_fix_date(day, self.month, self.year))
|
|
return self
|
|
|
|
def __repr__(self):
|
|
return '%s(%i, %i, %i)' % (self.__class__.__name__, self.year, self.month, self.day)
|
|
def __str__(self):
|
|
return '%02i/%02i/%04i' % (self.day, self.month, self.year)
|
|
def __unicode__(self):
|
|
return u'%02i/%02i/%04i' % (self.day, self.month, self.year)
|
|
|
|
def __eq__(self, other): return self._d == self._date(other, False)
|
|
def __ne__(self, other): return self._d != self._date(other, False)
|
|
def __lt__(self, other):
|
|
if other is None: return False
|
|
else: return self._d < self._date(other)
|
|
def __le__(self, other):
|
|
if other is None: return False
|
|
else: return self._d <= self._date(other)
|
|
def __gt__(self, other):
|
|
if other is None: return True
|
|
else: return self._d > self._date(other)
|
|
def __ge__(self, other):
|
|
if other is None: return True
|
|
else: return self._d >= self._date(other)
|
|
def __cmp__(self, other):
|
|
if other is None: return 1
|
|
else: return cmp(self._d, self._date(other))
|
|
def __hash__(self): return hash(self._d)
|
|
|
|
def _date(self, d, required=True):
|
|
"""Retourner l'instance de datetime.date correspondant à l'objet d.
|
|
"""
|
|
if isinstance(d, pydate): return d
|
|
elif isinstance(d, pydatetime): return d.date()
|
|
elif isinstance(d, Date): return d._d
|
|
elif required: raise ValueError("Expected datetime.date or Date instance, got %s" % repr(d))
|
|
else: return None
|
|
|
|
def _delta(self, td):
|
|
"""Retourner l'instance de datetime.timedelta correspondant à l'objet td
|
|
"""
|
|
if isinstance(td, timedelta): return td
|
|
elif isnum(td): return timedelta(td)
|
|
else: raise ValueError("Expected number or datetime.delta instance got %s" % repr(td))
|
|
|
|
def _new(cls, d=None, t=None):
|
|
"""Constructeur. d est une instance de Date ou datetime.date. t est un
|
|
nombre de secondes depuis l'epoch.
|
|
"""
|
|
if d is not None:
|
|
if isinstance(d, pydate): return cls(d.day, d.month, d.year)
|
|
elif isinstance(d, pydatetime): return cls(d.day, d.month, d.year)
|
|
elif isinstance(d, Date): return cls(d.day, d.month, d.year)
|
|
else: raise ValueError("Expected datetime.date or Date instance, got %s" % repr(d))
|
|
elif t is not None: return cls(t=t)
|
|
else: return cls()
|
|
_new = classmethod(_new)
|
|
|
|
def copy(self):
|
|
"""Retourner une nouvelle instance, copie de cet objet
|
|
"""
|
|
return self._new(self._d)
|
|
|
|
def replace(self, day=None, month=None, year=None):
|
|
"""Retourner une nouvelle instance avec les champs spécifiés modifiés.
|
|
"""
|
|
kw = {}
|
|
for name, value in [('day', day), ('month', month), ('year', year)]:
|
|
if value is not None: kw[name] = value
|
|
return self._new(self._d.replace(**kw))
|
|
|
|
def __add__(self, other): return self._new(self._d + self._delta(other))
|
|
__radd__ = __add__
|
|
def add(self, days=1): return self + days
|
|
|
|
def __sub__(self, other): return self._new(self._d - self._delta(other))
|
|
__rsub__ = __sub__
|
|
def sub(self, days=1): return self - days
|
|
|
|
def diff(self, other):
|
|
"""Retourner le nombre de jours de différences entre cette date et other
|
|
"""
|
|
delta = self._d - self._date(other)
|
|
return delta.days
|
|
|
|
def __fix_weekday(self, date):
|
|
"""Si date est après jeudi, retourner le début de la semaine
|
|
suivante, sinon retourner le début de la semaine courante.
|
|
"""
|
|
date = date.copy()
|
|
if date.weekday() > 3:
|
|
date = date.set_weekday(0)
|
|
date += 7
|
|
else:
|
|
date.set_weekday(0)
|
|
return date
|
|
|
|
def get_monthweeks(self, complete=True, only_debut=None):
|
|
"""Retourner une liste de dates (debut, fin) correspondant aux débuts
|
|
et aux fins des semaine du mois de cet objet.
|
|
|
|
Si only_debut==True, ne retourner que la liste de valeurs debut au lieu
|
|
des tuples (debut, fin). Par défaut only_debut==complete
|
|
|
|
Si complete==True, on ne retourne que des semaines complètes: les dates
|
|
au début et à la fin du mois sont corrigées pour inclure les jours du
|
|
mois précédent et du mois suivant s'il y a au moins 4 jours dans le mois
|
|
courant.
|
|
|
|
Sinon, les semaines du début et de la fin du mois peuvent être tronquées
|
|
et ne contiennent que les jours du mois.
|
|
"""
|
|
if only_debut is None: only_debut = complete
|
|
|
|
first = self.copy().set(1)
|
|
monthdays = first.monthdays
|
|
last = first + monthdays
|
|
weeks = []
|
|
if complete:
|
|
first = self.__fix_weekday(first)
|
|
last = self.__fix_weekday(last)
|
|
debut = first
|
|
while debut < last:
|
|
fin = debut + 6
|
|
if only_debut: weeks.append(debut)
|
|
else: weeks.append((debut, fin))
|
|
debut = fin + 1
|
|
else:
|
|
last -= 1
|
|
debut = first
|
|
while debut <= last:
|
|
fin = debut.copy().set_weekday(6)
|
|
if fin > last: fin = last
|
|
if only_debut: weeks.append(debut)
|
|
else: weeks.append((debut, fin))
|
|
debut = fin + 1
|
|
return weeks
|
|
|
|
def isdate(d):
|
|
"""Tester si d est une instance de Date
|
|
"""
|
|
return isinstance(d, Date)
|
|
def isanydate(d):
|
|
"""Tester si d est une instance de Date, datetime.date ou datetime.datetime
|
|
"""
|
|
return isinstance(d, Date) or isinstance(d, pydate) or isinstance(d, pydatetime)
|
|
|
|
RE_DATE_FR = re.compile(r'(\d+)(?:/(\d+)(?:/(\d+))?)?$')
|
|
RE_DATE_ISO = re.compile(r'(\d+)-(\d+)-(\d+)$')
|
|
def parse_date(s):
|
|
"""Parser une chaine et retourner une instance de Date
|
|
"""
|
|
mof = RE_DATE_FR.match(s)
|
|
moi = RE_DATE_ISO.match(s)
|
|
if mof is not None:
|
|
year = mof.group(3)
|
|
month = mof.group(2)
|
|
day = mof.group(1)
|
|
elif moi is not None:
|
|
year = moi.group(1)
|
|
month = moi.group(2)
|
|
day = moi.group(3)
|
|
else:
|
|
raise ValueError("Invalid date format: %s" % _s(s))
|
|
if year is not None: year = _fix_year(int(year))
|
|
if month is not None: month = int(month)
|
|
if day is not None: day = int(day)
|
|
return Date(day, month, year)
|
|
|
|
def ensure_date(d):
|
|
"""Retourner une instance de Date, ou None si d==None.
|
|
|
|
d peut être une intance de datetime.date, Date ou une chaine.
|
|
"""
|
|
if d is None: return None
|
|
elif isinstance(d, Date): return d
|
|
elif isinstance(d, pydate): return Date._new(d)
|
|
elif isinstance(d, pydatetime): return Date._new(d)
|
|
if not isstr(d): d = _s(d)
|
|
return parse_date(d)
|
|
|
|
def _tzname():
|
|
tz = time_mod.timezone
|
|
if tz > 0: s = "-"
|
|
else: s = "+"
|
|
tz = abs(tz) / 60
|
|
h = tz / 60
|
|
m = tz % 60
|
|
return "%s%02i%02i" % (s, h, m)
|
|
|
|
def rfc2822(time=None, gmt=True):
|
|
"""Retourner la date au format rfc 2822.
|
|
|
|
time est une date au format de time.time()
|
|
"""
|
|
if time is None: time = time_mod.time()
|
|
if gmt:
|
|
time = gmtime(time)
|
|
tzname = "+0000"
|
|
else:
|
|
time = localtime(time)
|
|
tzname = _tzname()
|
|
return "%s %s" % (asctime(time), tzname)
|
|
|
|
class _DateSpecConstants:
|
|
"""Constantes utilisées par les classes DateSpec et ses filles
|
|
"""
|
|
|
|
# Contrainte
|
|
C = r'(?:!(w|n)(\d+))'
|
|
C_COUNT = 2 # nombre de groupes pour l'expression régulière C
|
|
C_OP = 0 # numéro relatif du groupe pour la valeur OP
|
|
C_WD = 1 # numéro relatif du groupe pour la valeur WEEKDAY
|
|
|
|
# Spécification
|
|
I = r'(\d+)'
|
|
I_COUNT = 1 # nombre de groupes pour l'expression régulière I
|
|
I_VALUE = 0 # numéro relatif du groupe pour la valeur VALUE
|
|
|
|
R = r'(?:(\d+)(?:\s*-\s*(\d+))?)' # Range
|
|
R_COUNT = 2 # nombre de groupes pour l'expression régulière R
|
|
R_FROM = 0 # numéro relatif du groupe pour la valeur FROM
|
|
R_TO = 1 # numéro relatif du groupe pour la valeur TO
|
|
|
|
AOR = r'(?:(\*)|%s)' % R # AnyOrRange
|
|
AOR_COUNT = 1 + R_COUNT # nombre de groupes pour l'expression régulière AOR
|
|
AOR_R_POS = 1 # position du premier groupe de l'expression R dans AOR
|
|
AOR_ANY = 0
|
|
AOR_FROM = AOR_R_POS + R_FROM # numéro relatif du groupe pour la valeur FROM
|
|
AOR_TO = AOR_R_POS + R_TO # numéro relatif du groupe pour la valeur TO
|
|
|
|
S = r'(?:\+%s|w%s|%s)(?:\s*/\s*%s(?:\s*/\s*%s)?)?' % (I, R, AOR, AOR, AOR)
|
|
S_COUNT = I_COUNT + R_COUNT + 3 * AOR_COUNT # nombre de groupes pour l'expression régulière S
|
|
S_I_POS = 0 # position du premier groupe de l'expression I dans S
|
|
S_R_POS = S_I_POS + I_COUNT # position du premier groupe de l'expression R dans S
|
|
S_DAOR_POS = S_R_POS + R_COUNT # position du premier groupe de l'expression DAOR dans S
|
|
S_MAOR_POS = S_DAOR_POS + AOR_COUNT # position du premier groupe de l'expression DAOR dans S
|
|
S_YAOR_POS = S_MAOR_POS + AOR_COUNT # position du premier groupe de l'expression DAOR dans S
|
|
S_OFFSET = S_I_POS + I_VALUE # numéro relatif du groupe pour la valeur OFFSET
|
|
S_WD_FROM = S_R_POS + R_FROM # numéro relatif du groupe pour la valeur FROM de WD
|
|
S_WD_TO = S_R_POS + R_TO # numéro relatif du groupe pour la valeur TO de WD
|
|
S_D_ANY = S_DAOR_POS + AOR_ANY # numéro relatif du groupe pour la valeur ANY de D
|
|
S_D_FROM = S_DAOR_POS + AOR_FROM # numéro relatif du groupe pour la valeur FROM de D
|
|
S_D_TO = S_DAOR_POS + AOR_TO # numéro relatif du groupe pour la valeur TO de D
|
|
S_M_ANY = S_MAOR_POS + AOR_ANY # numéro relatif du groupe pour la valeur ANY de M
|
|
S_M_FROM = S_MAOR_POS + AOR_FROM # numéro relatif du groupe pour la valeur FROM de M
|
|
S_M_TO = S_MAOR_POS + AOR_TO # numéro relatif du groupe pour la valeur TO de M
|
|
S_Y_ANY = S_YAOR_POS + AOR_ANY # numéro relatif du groupe pour la valeur ANY de Y
|
|
S_Y_FROM = S_YAOR_POS + AOR_FROM # numéro relatif du groupe pour la valeur FROM de Y
|
|
S_Y_TO = S_YAOR_POS + AOR_TO # numéro relatif du groupe pour la valeur TO de Y
|
|
|
|
RE_SPEC = re.compile(r'(?:(?:%s)|(?:%s))$' % (C, S))
|
|
# offsets des positions des groupes dans l'expression RE_SPEC
|
|
SPEC_C_POS = 0
|
|
SPEC_S_POS = SPEC_C_POS + C_COUNT
|
|
# position des groupes dans l'expression RE_SPEC
|
|
SPEC_C_OFF = 1 + SPEC_C_POS
|
|
CONS_OP = SPEC_C_OFF + C_OP
|
|
CONS_WD = SPEC_C_OFF + C_WD
|
|
SPEC_S_OFF = 1 + SPEC_S_POS
|
|
SPEC_OFFSET = SPEC_S_OFF + S_OFFSET
|
|
SPEC_WD_FROM = SPEC_S_OFF + S_WD_FROM
|
|
SPEC_WD_TO = SPEC_S_OFF + S_WD_TO
|
|
SPEC_D_ANY = SPEC_S_OFF + S_D_ANY
|
|
SPEC_D_FROM = SPEC_S_OFF + S_D_FROM
|
|
SPEC_D_TO = SPEC_S_OFF + S_D_TO
|
|
SPEC_M_ANY = SPEC_S_OFF + S_M_ANY
|
|
SPEC_M_FROM = SPEC_S_OFF + S_M_FROM
|
|
SPEC_M_TO = SPEC_S_OFF + S_M_TO
|
|
SPEC_Y_ANY = SPEC_S_OFF + S_Y_ANY
|
|
SPEC_Y_FROM = SPEC_S_OFF + S_Y_FROM
|
|
SPEC_Y_TO = SPEC_S_OFF + S_Y_TO
|
|
|
|
def _range(f, t=None):
|
|
f = int(f)
|
|
if t is None: t = f
|
|
else: t = int(t)
|
|
if t < f: t, f = f, t
|
|
return (f, t)
|
|
_range = staticmethod(_range)
|
|
def _isw(vs): return vs == '*'
|
|
_isw = staticmethod(_isw)
|
|
def _isr(vs): return isseq(vs)
|
|
_isr = staticmethod(_isr)
|
|
def _matches(cls, vs, v):
|
|
if cls._isw(vs): return True
|
|
elif cls._isr(vs): return v >= vs[0] and v <= vs[1]
|
|
else: raise ValueError("Invalid format: %s" % _s(vs))
|
|
_matches = classmethod(_matches)
|
|
def _tostr(cls, vs):
|
|
if cls._isw(vs):
|
|
return "*"
|
|
elif cls._isr(vs):
|
|
if vs[0] == vs[1]: return "%i" % vs[0]
|
|
else: return "%i-%i" % vs
|
|
else: raise ValueError("Invalid format: %s" % _s(vs))
|
|
_tostr = classmethod(_tostr)
|
|
def _check_range(cls, name, vs, min, max):
|
|
if (min is not None and (vs[0] < min or vs[1] < min)) or \
|
|
(max is not None and (vs[0] > max or vs[1] > max)):
|
|
if min is None: min = u"-INF"
|
|
else: min = str(min)
|
|
if max is None: max = u"+INF"
|
|
else: max = str(max)
|
|
raise ValueError("%s values must be in the [%s, %s] range, got %s" % (name, min, max, cls._tostr(vs)))
|
|
_check_range = classmethod(_check_range)
|
|
def _check_value(cls, name, v, min, max):
|
|
if (min is not None and v < min) or (max is not None and v > max):
|
|
if min is None: min = u"-INF"
|
|
else: min = str(min)
|
|
if max is None: max = u"+INF"
|
|
else: max = str(max)
|
|
raise ValueError("%s value must be in the [%s, %s] range, got %i" % (name, min, max, v))
|
|
_check_value = classmethod(_check_value)
|
|
|
|
class DateSpec(_DateSpecConstants):
|
|
"""Une spécification de dates de la forme D[/M[/Y]], ou une spécification
|
|
de contrainte de date de la forme !W.
|
|
|
|
- D peut prendre l'une des formes suivantes:
|
|
- soit des jours du moins sous la forme *, DAY ou FROM-TO.
|
|
- soit des jours de la semaine sous la forme "w"WEEKDAY ou "w"FROM-TO
|
|
avec 1=Lundi, ..., 7=Dimanche
|
|
- soit une expression relative de la forme "+"DAYS, qui représente
|
|
DAYS jours après une date de référence.
|
|
- M représente des mois sous la forme *, MONTH ou FROM-TO.
|
|
- Y représente des années sous la forme *, YEAR ou FROM-TO.
|
|
- W représente des jours de la semaine sous la forme "w"WEEKDAY ou
|
|
"n"WEEKDAY avec 1=Lundi, ..., 7=Dimanche
|
|
|
|
Exemples:
|
|
|
|
w1-5
|
|
Les jours de la semaine
|
|
15/1-6
|
|
Les 15 des mois de janvier à juin
|
|
*/1
|
|
N'importe quel jour du mois de janvier
|
|
!w4
|
|
Spécifier que le jour DOIT être un Jeudi.
|
|
!n4
|
|
Spécifier que le jour DOIT être le Jeudi *suivant* la date de référence
|
|
"""
|
|
|
|
class Strategy(_DateSpecConstants):
|
|
def matches(self, date):
|
|
u"""Tester si la date correspond à cette spécification de date
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def fix(self, date, now=None, refdate=None):
|
|
u"""Corriger date, refdate étant la date de référence
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def is_obsolete(self, now=None):
|
|
u"""Tester si cette spécification de date est obsolète, c'est à
|
|
dire si elle désigne une date passée.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
class ConstraintStrategy(Strategy):
|
|
"""Une contrainte de date:
|
|
|
|
"!wWEEKDAY" signifie que le jour DOIT être celui spécifié, en restant
|
|
dans la semaine en cours.
|
|
|
|
"!nWEEKDAY" signifie que le jour DOIT être celui spécifié, mais en
|
|
prenant toujours une date future. Il est alors possible de passer sur
|
|
la semaine suivante pour arriver au bon jour.
|
|
"""
|
|
_op = None # op: w ou n
|
|
_ws = None # weekdays
|
|
|
|
def __init__(self, mo):
|
|
self._op = mo.group(self.CONS_OP)
|
|
ws = mo.group(self.CONS_WD)
|
|
if ws is not None: self._ws = self._range(ws)
|
|
if self._ws is not None:
|
|
self._check_range("WEEKDAYS", self._ws, 0, 7)
|
|
|
|
def __str__(self):
|
|
s = "!"
|
|
if self._ws is not None:
|
|
s += self._op
|
|
s += self._tostr(self._ws)
|
|
return s
|
|
|
|
def matches(self, date):
|
|
return True
|
|
|
|
def fix(self, date, now=None, refdate=None):
|
|
date = ensure_date(date)
|
|
expected_wd = self._ws[0]
|
|
actual_wd = date.isoweekday()
|
|
if expected_wd != actual_wd:
|
|
date += expected_wd - actual_wd
|
|
if self._op == 'n' and actual_wd > expected_wd:
|
|
date += 7
|
|
return date
|
|
|
|
def is_obsolete(self, now=None):
|
|
return False
|
|
|
|
class DateStrategy(Strategy):
|
|
"""Une spécification de date
|
|
"""
|
|
_offset = None # offset
|
|
_ws = None # weekdays
|
|
_ds = None # days
|
|
_ms = None # months
|
|
_ys = None # years
|
|
|
|
def __init__(self, mo):
|
|
# offset
|
|
o = mo.group(self.SPEC_OFFSET)
|
|
if o is None: pass
|
|
else: self._offset = self._range(o)[0]
|
|
if self._offset is not None:
|
|
self._check_value("OFFSET", self._offset, 1, None)
|
|
# weekdays
|
|
wf, wt = mo.group(self.SPEC_WD_FROM), mo.group(self.SPEC_WD_TO)
|
|
if wf is None and wt is None: pass
|
|
elif wt is not None: self._ws = self._range(wf, wt)
|
|
else: self._ws = self._range(wf)
|
|
if self._ws is not None:
|
|
self._check_range("WEEKDAYS", self._ws, 0, 7)
|
|
# days
|
|
dw, df, dt = mo.group(self.SPEC_D_ANY), mo.group(self.SPEC_D_FROM), mo.group(self.SPEC_D_TO)
|
|
if dw is None and df is None and dt is None: pass
|
|
elif dw is not None: self._ds = '*'
|
|
elif dt is not None: self._ds = self._range(df, dt)
|
|
else: self._ds = self._range(df)
|
|
# months
|
|
mw, mf, mt = mo.group(self.SPEC_M_ANY), mo.group(self.SPEC_M_FROM), mo.group(self.SPEC_M_TO)
|
|
if mw is None and mf is None and mt is None: self._ms = '*'
|
|
elif mw is not None: self._ms = '*'
|
|
elif mt is not None: self._ms = self._range(mf, mt)
|
|
else: self._ms = self._range(mf)
|
|
# years
|
|
yw, yf, yt = mo.group(self.SPEC_Y_ANY), mo.group(self.SPEC_Y_FROM), mo.group(self.SPEC_Y_TO)
|
|
if yw is None and yf is None and yt is None: self._ys = '*'
|
|
elif yw is not None: self._ys = '*'
|
|
elif yt is not None: self._ys = self._range(yf, yt)
|
|
else: self._ys = self._range(yf)
|
|
if self._isr(self._ys):
|
|
self._ys = map(_fix_year, self._ys)
|
|
|
|
def __str__(self):
|
|
s = ""
|
|
if self._offset is not None:
|
|
s += "+%i" % self._offset
|
|
if self._ws is not None:
|
|
s += "w"
|
|
s += self._tostr(self._ws)
|
|
elif self._ds is not None:
|
|
s += self._tostr(self._ds)
|
|
s += "/"
|
|
s += self._tostr(self._ms)
|
|
s += "/"
|
|
s += self._tostr(self._ys)
|
|
return s
|
|
|
|
def fill_ranges(self, yrs = None, mrs = None, drs = None, wrs = None):
|
|
if yrs is None: yrs = []
|
|
yrs.append(self._ys)
|
|
if mrs is None: mrs = []
|
|
mrs.append(self._ms)
|
|
if self._ws is not None:
|
|
if wrs is None: wrs = []
|
|
wrs.append(self._ws)
|
|
elif self._ds is not None:
|
|
if drs is None: drs = []
|
|
drs.append(self._ds)
|
|
return yrs, mrs, drs, wrs
|
|
|
|
def matches(self, date):
|
|
date = ensure_date(date)
|
|
# tester l'année
|
|
if not self._matches(self._ys, date.year): return False
|
|
# tester le mois
|
|
if not self._matches(self._ms, date.month): return False
|
|
# tester weekday ou day
|
|
if self._ws is not None:
|
|
if not self._matches(self._ws, date.isoweekday()): return False
|
|
elif self._ds is not None:
|
|
if not self._matches(self._ds, date.day): return False
|
|
return True
|
|
|
|
def fix(self, date, now=None, refdate=None):
|
|
if self._offset is not None:
|
|
if now is None: now = Date()
|
|
if refdate is None: refdate = now
|
|
date = refdate + self._offset
|
|
return date
|
|
|
|
def is_obsolete(self, now=None):
|
|
if self._offset is not None: return False
|
|
elif self._ws is not None: return False
|
|
elif self._isw(self._ds): return False
|
|
elif self._isw(self._ms): return False
|
|
elif self._isw(self._ys): return False
|
|
if now is None: now = Date()
|
|
y = now.year; ys = self._ys
|
|
if y > ys[0] and y > ys[1]: return True
|
|
elif y < ys[0] and y < ys[1]: return False
|
|
m = now.month; ms = self._ms
|
|
if m > ms[0] and m > ms[1]: return True
|
|
elif m < ms[0] and m < ms[1]: return False
|
|
d = now.day; ds = self._ds
|
|
if d > ds[0] and d > ds[1]: return True
|
|
return False
|
|
|
|
_strategy = None
|
|
strategy = property(lambda self: self._strategy)
|
|
|
|
def is_constraint_spec(self):
|
|
"""Retourner True s'il s'agit d'une spécification de contrainte de date
|
|
"""
|
|
return isinstance(self._strategy, self.ConstraintStrategy)
|
|
def is_date_spec(self):
|
|
"""Retourner True s'il s'agit d'une spécification de date
|
|
"""
|
|
return isinstance(self._strategy, self.DateStrategy)
|
|
|
|
def __init__(self, spec):
|
|
mo = self.RE_SPEC.match(spec)
|
|
if mo is None:
|
|
raise ValueError("Invalid DateSpec format: %s" % _s(spec))
|
|
|
|
if mo.group(self.CONS_WD) is None: strategy = self.DateStrategy(mo)
|
|
else: strategy = self.ConstraintStrategy(mo)
|
|
self._strategy = strategy
|
|
|
|
def __str__(self):
|
|
return self._strategy.__str__()
|
|
|
|
def __repr__(self):
|
|
return "%s(\"%s\")" % (self.__class__.__name__, self)
|
|
|
|
def matches(self, date):
|
|
return self._strategy.matches(date)
|
|
|
|
def fix(self, date, now=None, refdate=None):
|
|
return self._strategy.fix(date, now, refdate)
|
|
|
|
def matches_fix(self, date, now=None, refdate=None):
|
|
if self.matches(date): return True, self.fix(date, now, refdate)
|
|
else: return False, date
|
|
|
|
def is_obsolete(self):
|
|
return self._strategy.is_obsolete()
|
|
|
|
class DateSpecs:
|
|
"""Une suite de spécifications de date, séparées par des virgules.
|
|
|
|
Attention! l'ordre est important, car les calculs et l'évaluation des
|
|
contraintes se fait dans l'ordre des spécifications.
|
|
"""
|
|
RE_COMMA = re.compile(r'\s*,\s*')
|
|
|
|
_specs = None
|
|
def __constraint_specs(self):
|
|
return [spec for spec in self._specs if spec.is_constraint_spec()]
|
|
def __date_specs(self):
|
|
return [spec for spec in self._specs if spec.is_date_spec()]
|
|
|
|
def __init__(self, specs):
|
|
specs = _s(specs).strip()
|
|
self._specs = [DateSpec(spec) for spec in self.RE_COMMA.split(specs)]
|
|
|
|
def __str__(self):
|
|
return ",".join([str(spec) for spec in self._specs])
|
|
|
|
def __repr__(self):
|
|
return "%s(\"%s\")" % (self.__class__.__name__, self)
|
|
|
|
def matches(self, date):
|
|
for spec in self._specs:
|
|
if spec.matches(date): return True
|
|
return False
|
|
|
|
def matches_fix(self, date, now=None, refdate=None):
|
|
if now is None: now = Date()
|
|
if refdate is None: refdate = now
|
|
for spec in self.__date_specs():
|
|
if spec.matches(date):
|
|
for spec in self._specs:
|
|
date = spec.fix(date, now, refdate)
|
|
return True, date
|
|
return False, date
|
|
|
|
_now = None
|
|
_refdate = None
|
|
_candidates = None
|
|
|
|
def _reset_candidates(self):
|
|
self._now = None
|
|
self._refdate = None
|
|
self._candidates = None
|
|
|
|
def _get_candidates(self, now=None, refdate=None):
|
|
if now is None: now = Date()
|
|
if refdate is None: refdate = now
|
|
if self._candidates is not None and \
|
|
now == self._now and refdate == self._refdate:
|
|
return self._candidates
|
|
|
|
isw = DateSpec._isw
|
|
# Enumérer les candidats de weekdays, days, months, years
|
|
yrs = None
|
|
mrs = None
|
|
drs = None
|
|
wrs = None
|
|
for spec in self.__date_specs():
|
|
yrs, mrs, drs, wrs = spec.strategy.fill_ranges(yrs, mrs, drs, wrs)
|
|
# Calculer les dates candidates
|
|
# ...years
|
|
candidates = {}
|
|
if yrs is None: yrs = ['*']
|
|
for ys in yrs:
|
|
if ys == '*':
|
|
candidates[now.year] = {}
|
|
candidates[now.year + 1] = {}
|
|
else:
|
|
for y in range(ys[0], ys[1] + 1):
|
|
candidates[y] = {}
|
|
years = candidates.keys()
|
|
# ...months
|
|
for year in years:
|
|
if mrs is None: mrs = ['*']
|
|
for ms in mrs:
|
|
if ms == '*':
|
|
candidates[year][now.month] = {}
|
|
candidates[year][now.month + 1] = {}
|
|
else:
|
|
for m in range(ms[0], ms[1] + 1):
|
|
candidates[year][m] = {}
|
|
# ...weekdays or days
|
|
for year in years:
|
|
for month in candidates[year].keys():
|
|
monthdays = range(1, _monthdays(year, month) + 1)
|
|
#candidates[year][month]['ws'] = None
|
|
candidates[year][month]['ds'] = None
|
|
if wrs is not None:
|
|
# si on précise des jours de semaine,
|
|
# inclure tous les jours du mois
|
|
#ws = []
|
|
#for wr in wrs:
|
|
# ws.extend(range(wr[0], wr[1] + 1))
|
|
#candidates[year][month]['ws'] = ws
|
|
candidates[year][month]['ds'] = monthdays
|
|
elif drs is not None:
|
|
ds = []
|
|
for dr in drs:
|
|
if isw(dr): ds.extend(monthdays)
|
|
else: ds.extend(range(dr[0], dr[1] + 1))
|
|
candidates[year][month]['ds'] = ds
|
|
else:
|
|
# ni weekdays, ni days, prendre tous les jours du mois
|
|
# à configurer ci-dessous quand on saura quel mois prendre
|
|
candidates[year][month]['ds'] = monthdays
|
|
# fin
|
|
self._now = now
|
|
self._refdate = refdate
|
|
self._candidates = candidates
|
|
return candidates
|
|
|
|
def get_next_date(self, now=None, refdate=None):
|
|
if now is None: now = Date()
|
|
if refdate is None: refdate = now
|
|
candidates = self._get_candidates(now, refdate)
|
|
for year in [year for year in sorted(candidates.keys())
|
|
if year >= now.year]:
|
|
for month in [month for month in sorted(candidates[year].keys())
|
|
if Date(0, month + 1, year) >= now]:
|
|
days = [day for day in candidates[year][month]['ds']
|
|
if Date(day, month, year) > now]
|
|
#weekdays = candidates[year][month]['ws']
|
|
for day in days:
|
|
next = Date(day, month, year)
|
|
matches, next = self.matches_fix(next, now, refdate)
|
|
if matches: return next
|
|
return None
|
|
|
|
def remove_obsoletes(self):
|
|
specs = [spec for spec in self._specs if not spec.is_obsolete()]
|
|
if len(specs) != len(self._specs):
|
|
self._specs = specs
|
|
self._reset_candidates()
|
|
return True
|
|
else:
|
|
return False
|