読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

UTCTimeをEqで検査する

UTCTimeが関わるテストをQuickCheckで書こうとしてハマった。

まずUTCTimeをランダム生成するために、UTCTimeをArbitraryのインスタンスにしようとした。
UTCTimeはDayとDiffTimeの組になっていて、DayとDiffTimeはIntegerから作れるし、IntegerはArbitraryのインスタンスなので、UTCTimeのインスタンス化はこんな風に書ける。

instance Arbitrary Day where
    arbitrary = ModifiedJulianDay <$> arbitrary

instance Arbitrary DiffTime where
    arbitrary = secondsToDiffTime <$> arbitrary

instance Arbitrary UTCTime where
    arbitrary = UTCTime <$> arbitrary <*> arbitrary

まあ別にDayとDiffTimeまでインスタンス化する必要は必ずしも無いんですけど。

で、UTCTimeに対してTextとの間で相互に変換する関数があるとする。こういうの。

fromText :: Text -> Maybe UTCTime
toText :: UTCTime -> Text

fromTextは一応Maybeにしておきます。

これをQuickCheckでテストする。

main :: IO ()
main = hspec $ describe "test"
    prop "from/to text" prop_text

prop_text :: Text -> Bool
prop_text a = fromText (toText a) == Just a

このテストが通らないわけだ。
なぜか、prop_textの比較に使われているUTCTimeをshowで表示してみても全く同じなのに、(==)がFalseを返す。なんでや。

よく見るとDayとDiffTimeはIntegerから生成しているので、負の数の場合があり得る。
Dayの場合は0が1858-11-17になってるだけなのでいいんだけど、DiffTimeはUTCTimeの中では0-86400の間で、1日の中の経過秒数を表している。(最後の1秒はうるう秒らしい)
ただし、特にUTCTimeやDiffTimeを作る時にそういう値の制約がかかっているわけでもないので、0-86400の範囲外の整数値からも普通にDiffTimeやUTCTimeを作ることができてしまう。いやDiffTimeの時はいいんだけどUTCTimeを作る時は制約かけてくれよ。

その結果どうなるかというと、こういう感じになる。

Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)
1858-11-17 00:00:00 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 1) (secondsToDiffTime $ -86400)
1858-11-17 00:00:00 UTC

で、UTCTimeのEqのインスタンス化がこうなので

instance Eq UTCTime where
	(UTCTime da ta) == (UTCTime db tb) = (da == db) && (ta == tb)

まあ、showの結果が一致しててもこれじゃequalにならないよね。うん。
どうでもいいけどこれだったらderiving Eqでもよかったんじゃないかな。

ということなので、DiffTimeのArbitrary化はこうするとテストが通るようになる。

instance Arbitrary DiffTime where
    arbitrary = secondsToDiffTime <$> choose (0, 60*60*24 - 1)

"-1"は念のため。なくても一応動くんですけどね。

あと、実はDiffTimeは負の値にするとさっきの例のように日付が変わるんですが、増やすと日付が変わらずに時刻だけループする。けども1周しかループしない。

Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86400)
1858-11-17 23:59:60 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86402)
1858-11-17 23:59:62 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86500)
1858-11-17 23:59:160 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 172800)
1858-11-17 23:59:86460 UTC

なんというか、UTCTimeのデータにDiffTimeを使いまわさない方が良かったんじゃないかなぁ。

DiffTimeに大きな値を突っ込んでも一応UTCTimeは作れるんですが、テストで使うと当たり前ですけどparseでコケますね。