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でコケますね。