DateTimeの型同士の比較

AzureでTwitterBotを作ってて、この問題をうまく理解できてなくて数ヶ月間填まってたので、備忘録的に。
日付・時間を格納するDateTime型。これの比較時の罠。(知ってる人は知ってると思うけど)
結論から言うと、Date/DateTime型はミリ秒まで格納してるので、単純に型同士で比較すると、条件が成立しない場合がある。
特に数秒ごとに時間を判定する処理とか。たとえばBotとか、定期的に処理するサービスとか。

以下コード(ConsoleAppで実験)

  • ただ単純にDateTimeで比較した場合。
    Sub DateTimeSample01()

        '10秒後の時間を比較対象にする
        Dim TargetTime As DateTime = Now.AddSeconds(10)

        Do

            Console.WriteLine("CurrentTime:" & Now)
            Console.WriteLine("TargetTime :" & TargetTime)

            'DateTime同士(正確にはDateTimeとDate)で比較
            If TargetTime = Now Then
                Console.WriteLine("-- TargetTime=Now --")
                Console.ReadLine()
                Exit Do
            End If

            '1秒待つ
            System.Threading.Thread.Sleep(1000)

        Loop

    End Sub

結果:

CurrentTime:2012/12/22 0:04:00
TargetTime :2012/12/22 0:04:03
CurrentTime:2012/12/22 0:04:01
TargetTime :2012/12/22 0:04:03
CurrentTime:2012/12/22 0:04:02
TargetTime :2012/12/22 0:04:03
CurrentTime:2012/12/22 0:04:03 ←
TargetTime :2012/12/22 0:04:03 ←
CurrentTime:2012/12/22 0:04:04
TargetTime :2012/12/22 0:04:03
CurrentTime:2012/12/22 0:04:05
TargetTime :2012/12/22 0:04:03

時間を過ぎても -- TargetTime=Now --の表示がされないはず。そして永遠と条件が成立するまでループが続いていく。

  • 実はミリ秒も格納してる証拠

ToStringの書式指定子に、Fを指定するとミリ秒まで出してくれます。FFFFFFFで秒以下の7桁まで表示。(カスタム DateTime 書式指定文字列 | Microsoft Docs

    Sub DateTimeSample02()

        '10秒後の時間を比較対象にする
        Dim TargetTime As DateTime = Now.AddSeconds(10)

        Do

            Console.WriteLine("CurrentTime:" & Now.ToString("yyyy/MM/dd HH:mm:ss.FFFFFFF"))
            Console.WriteLine("TargetTime :" & TargetTime.ToString("yyyy/MM/dd HH:mm:ss.FFFFFFF"))

            'DateTime同士(正確にはDateTimeとDate)で比較
            If TargetTime = Now Then
                Console.WriteLine("-- TargetTime=Now --")
                Console.ReadLine()
                Exit Do
            End If

            '1秒待つ
            System.Threading.Thread.Sleep(1000)

        Loop

    End Sub

結果:

CurrentTime:2012/12/22 00:06:48.0012582
TargetTime :2012/12/22 00:06:50.9918573
CurrentTime:2012/12/22 00:06:49.0023155
TargetTime :2012/12/22 00:06:50.9918573
CurrentTime:2012/12/22 00:06:50.0033728 ←
TargetTime :2012/12/22 00:06:50.9918573 ←
CurrentTime:2012/12/22 00:06:51.00443
TargetTime :2012/12/22 00:06:50.9918573
CurrentTime:2012/12/22 00:06:52.0054873
TargetTime :2012/12/22 00:06:50.9918573

コンソールにミリ秒が表示され、System.Threading.Thread.Sleep(1000)で1秒待ったとしても、完全に1秒じゃなくてミリ秒単位でズレが出てます。

  • 解決方法はこちら
    Sub DateTimeSample03()

        '10秒後の時間を比較対象にする
        Dim TargetTime As DateTime = Now.AddSeconds(10)

        Do

            Console.WriteLine("CurrentTime:" & Now.ToString("yyyy/MM/dd HH:mm:ss.FFFFFFF"))
            Console.WriteLine("TargetTime :" & TargetTime.ToString("yyyy/MM/dd HH:mm:ss.FFFFFFF"))

            'フォーマットを指定して比較
            If TargetTime.ToString("HH:mm:ss") = Now.ToString("HH:mm:ss") Then
                Console.WriteLine("-- TargetTime=Now --")
                Console.ReadLine()
                Exit Do
            End If

            '1秒待つ
            System.Threading.Thread.Sleep(1000)

        Loop

    End Sub

結果(あえてミリ秒まで表示させてます):

CurrentTime:2012/12/22 00:08:33.9413177
TargetTime :2012/12/22 00:08:34.9318024
CurrentTime:2012/12/22 00:08:34.9423749
TargetTime :2012/12/22 00:08:34.9318024
-- TargetTime=Now --

ToStringでフォーマットを指定し、その上で比較すれば大丈夫だよ。という話でした。