nutools/lib/nulib/python/nulib/dates.py

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