Convertir une "Generalized Time" string en datetime
En ce moment, je bosse beaucoup avec un Active Directory, logiciel
Microsoft utilisant Ldap. Si on met de côté les erreurs abstraites
incompréhensible, l'obligation d'envoyer des string sur la plupart des
champs (l'unicode serait quand même vachement plus cool) et j'en passe,
c'est sympa.
La plupart des champs temporels de l'AD utilise le type "Long Int", et
contrairement à ce qu'on pourrait croire, ce n'est pas un timestamp.
C'est un filetime. Le filetime est la représentation du nombre
d'intervals de 100 nano-secondes depuis le 1er Janvier 1601 (UTC).
Pourquoi faire simple quand on peut faire ça.
Passe encore, j'ai trouvé un petit module qui permet de convertir tout
ça chez Reliably
Broken.
Mais comme le disent nos chers conseillers SFR, "Et c'est pas fini !" :
non content d'utiliser une représentation de datetime exotique, ils en
utilisent une autre : le GeneralizedTime String.
C'est une chaîne de caractères qui concatène l'année (sur 4 chiffres),
le mois (sur 2), le jour (sur 2), l'heure (sur 2) et optionnelement les
minutes, les secondes, les microsecondes (sur 3 chiffres) et un
indicateur pour l'offset heures/minutes par rapport à l'UTC, l'UTC, ou
rien du tout. Plus d'info sur
l'ASN.1
Concrètement, ça ressemble à ça :
20150131143554.230 # datetime(2015, 01, 31, 14, 35, 554, 230)
20150131143554.230Z # datetime(2015, 01, 31, 14, 35, 554, 230, tzinfo=<UTC>)
20150131143554.230+0300 # datetime(2015, 01, 31, 11, 35, 554, 230, tzinfo=<UTC>)
C'est relativement simple à parser, mais n'ayant rien trouvé pour
convertir ça en datetime sur le grand internet mondial, voici une petite
tool function pour le faire.
""" Tool function to convert Generalized Time string
to Python datetime object
"""
import datetime
import pytz
def gt_to_dt(gt):
""" Convert GeneralizedTime string to python datetime object
>>> gt_to_dt("20150131143554.230")
datetime.datetime(2015, 1, 31, 14, 35, 54, 230)
>>> gt_to_dt("20150131143554.230Z")
datetime.datetime(2015, 1, 31, 14, 35, 54, 230, tzinfo=<UTC>)
>>> gt_to_dt("20150131143554.230+0300")
datetime.datetime(2015, 1, 31, 11, 35, 54, 230, tzinfo=<UTC>)
"""
# check UTC and offset from local time
utc = False
if "Z" in gt.upper():
utc = True
gt = gt[:-1]
if gt[-5] in ['+', '-']:
# offsets are given from local time to UTC, so substract the offset to get UTC time
hour_offset, min_offset = -int(gt[-5] + gt[-4:-2]), -int(gt[-5] + gt[-2:])
utc = True
gt = gt[:-5]
else:
hour_offset, min_offset = 0, 0
# microseconds are optionnals
if "." in gt:
microsecond = int(gt[gt.index('.') + 1:])
gt = gt[:gt.index('.')]
else:
microsecond = 0
# seconds and minutes are optionnals too
if len(gt) == 14:
year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:10]), int(gt[10:12]), int(gt[12:])
hours += hour_offset
minutes += min_offset
elif len(gt) == 12:
year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:10]), int(gt[10:]), 0
hours += hour_offset
minutes += min_offset
elif len(gt) == 10:
year, month, day, hours, minutes, sec = int(gt[:4]), int(gt[4:6]), int(gt[6:8]), int(gt[8:]), 0, 0
hours += hour_offset
minutes += min_offset
else:
# can't be a generalized time
raise ValueError('This is not a generalized time string')
# construct aware or naive datetime
if utc:
dt = datetime.datetime(year, month, day, hours, minutes, sec, microsecond, tzinfo=pytz.UTC)
else:
dt = datetime.datetime(year, month, day, hours, minutes, sec, microsecond)
# done !
return dt