timefmt-goというGoライブラリを公開してメンテしているのですが、最近タイムゾーン周りの対応が弱いことに気がついて実装していました。タイムゾーンオフセットの表記は +0900
のようにコロンを入れないほうが一般的だと思いますが、RFC3339では +09:00
のようにコロンありの形もvalidです。strftime
では %:z
によりこの形を出力し、strptime
の %z
はコロンありなし両方の形をparseする必要があります。GNU拡張のstrftime
では %::z
を使うと秒まで表記し、%:::z
ではオフセットの精度によっていい感じに表記するようです (分精度なら分までなど)。
$ date +%z +0900 $ date +%:z +09:00 $ date +%::z +09:00:00 $ date +%:::z +09
strptime
の他の言語の挙動を確認しているときに、Python (3.9.2) で思わぬエラーが発生することに気が付きました。
>>> from datetime import datetime >>> datetime.strptime('Z', '%z') datetime.datetime(1900, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) >>> datetime.strptime('z', '%z') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.9/_strptime.py", line 568, in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) File "/usr/local/lib/python3.9/_strptime.py", line 453, in _strptime if z[3] == ':': IndexError: string index out of range
%z
が大文字の Z
を解釈するのは正しいのですが、それ以外のおかしな入力のときは ValueError
となるべきです。
>>> datetime.strptime('x', '%z') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.9/_strptime.py", line 568, in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) File "/usr/local/lib/python3.9/_strptime.py", line 349, in _strptime raise ValueError("time data %r does not match format %r" % ValueError: time data 'x' does not match format '%z'
例外の型が違うということは except ValueError
のようなキャッチをすり抜けてしまうということですね。ざんねんでした。
早速ソースコードを読んでいると、フォーマットを正規表現に変換するときに大文字小文字を無視するオプションを使っていて (%a
がMon
でもMON
でもマッチするように)、UTCを表す Z
が意図せず z
にもマッチしているようでした。
'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|Z)", # 略 def compile(self, format): """Return a compiled re object for the format string.""" return re_compile(self.pattern(format), IGNORECASE) # 略 elif group_key == 'z': z = found_dict['z'] if z == 'Z': gmtoff = 0 else: if z[3] == ':': z = z[:3] + z[4:]
考慮漏れだとわかったので、既存のissueがないことを確かめて新たにチケットを切りました。
bugs.python.org
GitHubではないので面倒かと思ったのですが、OpenIDですぐにログインできて、本名も要求されずとても簡単でした (Linux Foundationとかと比べると遥かに)。
次の日には興味を持った人がパッチを投げてくれました。
月や曜日のマッチングにはignore caseフラグが必要なのでどう修正するのかわからなかったのですが、 (?-i:PATTERN)
で一時的に無視することができるのですね。知らなかった。
github.com
3.9.3では直るんじゃないでしょうか。
世界から一つバグが消えてよかったですね。
Pythonのstrptime
は正規表現で実装されていて結構遅いと思いますが、あまり気にしない文化なのでしょうか。
日時なんて大した長さの文字列ではないからこれで良いのかもしれません (しかも strptime('999', '%H%M%S')
みたいなケースも対応している)。
timefmt
は極力アロケーションを減らし、strconv.Atoi
すら避けてカリカリにチューニングしているので、Pythonのライブラリはだいぶ富豪的だなと思いました。
終わり。