python(3.7)のtimedelta
で負になるような値を作ろうとすると、困惑することがあります。その謎を追ってみました。
timedeltaで正・負それぞれつくってみる
正になるパターン
In [1]: import datetime
In [2]: n = datetime.datetime.now()
# 現在時刻 - (現在時刻1秒前) = 1秒
In [3]: t = n - (n - datetime.timedelta(seconds=1))
In [4]: t
Out[4]: datetime.timedelta(seconds=1)
特に何の違和感もないですね。
負になるパターン
# 現在時刻 - (現在時刻1秒後) = -1秒
In [5]: t = n - (n + datetime.timedelta(seconds=1))
In [6]: t
Out[6]: datetime.timedelta(days=-1, seconds=86399)
!?
直感ではtimedelta(seconds=-1)
とか出てきそうなもんですが、なんだかデカイ値がでてきました。しかしこの結果、冷静に考えてみれば、
- 1日前かつ、
- 86399秒後(1日は86400秒)
なので、「1秒前(-1秒)」と同じになります。
なぜか?
ドキュメントよく見ると書いてあるんですよね、これ。
さらに、値が一意に表されるように days, seconds, microseconds が以下のように正規化されます
・0 <= microseconds < 1000000
・0 <= seconds < 3600*24 (一日中の秒数)
・-999999999 <= days <= 999999999
つまり、値の一意性を保つためにsecondsとmicrosecondsの値を正の値に限っています。 (あと、上の単位にまとめられるは場合はまとめるもしている)
どういうこと?
なぜこのような設計にしたか、という具体的な議論や文書は見つかりませんでした。
普通に考えられる点としては、比較の実装が単純になるということでしょうか。
例えば、seconds
にマイナス値を許す場合、以下は同じであると判定しなければいけません。
timedelta(seconds=-1)
timedelta(days=-1, seconds=86399)
こうなると、timedeltaオブジェクトの比較時に毎回計算が必要になってしまいそうです。しかし、一意性があるならば、それぞれ3つの数値フィールドがすべて同じであれば同一であるという簡単な実装が行えます。
実際、timedeltaの実装(ぽいもの)を見てみるとそのような感じでした。
以下、cpythonレポジトリから抜粋
# 抜粋
static int
delta_cmp(PyObject *self, PyObject *other)
{
int diff = GET_TD_DAYS(self) - GET_TD_DAYS(other);
if (diff == 0) {
diff = GET_TD_SECONDS(self) - GET_TD_SECONDS(other);
if (diff == 0)
diff = GET_TD_MICROSECONDS(self) -
GET_TD_MICROSECONDS(other);
}
return diff;
}
すごく単純な計算ですよね。ちなみに正規化の計算自体はこちらでやってそうでした。
ちなみに
コンストラクタは負の値を受け入れることができます。内部で先ほどの正規化関数により値が操作されています。
In [14]: datetime.timedelta(seconds=-1)
Out[14]: datetime.timedelta(days=-1, seconds=86399)
注意点
これ、つまりはtimedelta
オブジェクトのseconds
, microsenconds
フィールドの単体での利用は注意が必要ということです。
In [15]: datetime.timedelta(seconds=-1).seconds
Out[15]: 86399
ここで得たいのは-1
なはず。total_seconds
メソッドを使えば期待した結果が得られます。(floatですけど)
In [16]: datetime.timedelta(seconds=-1).total_seconds()
Out[16]: -1.0
total_microseconds
の実装はないようでした。
現場からは以上です。