はじめに

Haskell で時間や日付を扱う際に良く利用されるのは time パッケージです。

このパッケージが使いやすいかどうかは人それぞれですが、使い方を知っておくと便利なのでよく使いそうな関数を簡単に解説しようと思います。

これからの例は以下のコマンドを実行していると仮定して話を進めます。

$ stack repl --package time --resolver lts-12.9
$ import Data.Time

Time パッケージのモジュール構造

基本的には Data.Time を import して使います。

Data.Time は以下のモジュールを再エクスポートしています。

モジュール名 用途
Data.Time.Calendar 日付
Data.Time.Clock 全然使わないので良くわからない
Data.Time.LocalTime 日本の現在時刻を取得など
Data.Time.Format 出力の整形

rio を利用している場合

rio を利用している場合は RIO.Time を import します。

Data.Time.LocalTime

現在時刻を取得する場合にこのモジュールを使います。現在時刻を取得したいからと言って getCurrentTime を利用すると日本時間にならないので注意してください。

getZonedTime

システムのタイムゾーンに応じた現在時刻を返します。

> :t getZonedTime
getZonedTime :: IO ZonedTime

> getZonedTime
2018-09-17 13:41:05.512522063 JST

getCurrentTimeZone

システムのタイムゾーンを取得します。このタイムゾーンに基づいて getZonedTime が計算されます。

> :t getCurrentTimeZone
getCurrentTimeZone :: IO TimeZone

> getCurrentTimeZone
JST

zonedTimeToUTC

ZonedTimeUTCTime に変換するために使います。

> :t zonedTimeToUTC
zonedTimeToUTC :: ZonedTime -> UTCTime

> zonedTimeToUTC <$> getZonedTime
2018-09-17 04:41:27.907476307 UTC

utcToZonedTime

zonedTimeToUTC の逆で UTCTimeZonedTime に変換する関数です。タイムゾーンのための引数を余分に取ります。

> :t utcToZonedTime
utcToZonedTime :: TimeZone -> UTCTime -> ZonedTime

> utcToZonedTime <$> getCurrentTimeZone <*> getCurrentTime
2018-09-17 13:41:37.955641567 JST

1日後の時間を計算するには?

ここで、取得した時間の1日後を計算してみましょう。

そのためには Data.Time.Clock で定義されている addUTCTime を使います。

第一引数に NominalDiffTime という謎の型を取りますが、nominalDay の実装を見れば 60 * 60 * 24 っぽいことがわかるので、そんな感じで値を作ります。

ちなみに、上記の実装でなぜ NominalDiffTime の値になるかと言うと、NominalDiffTimeNum クラスのインスタンスになっているため、自動的に fromInteger が呼ばれて変換されるという仕組みです。

実際に試してみましょう。1日後を計算してみます。

> t = addUTCTime nominalDay . zonedTimeToUTC <$> getZonedTime
2018-09-17 10:32:56.880362453 UTC

> getZonedTime
2018-09-17 13:49:09.279378323 JST

> utcToZonedTime <$> getCurrentTimeZone <*> t
2018-09-18 13:49:16.211737218 JST

同様に1時間後も計算してみましょう。

> t = addUTCTime (60 * 60) . zonedTimeToUTC <$> getZonedTime

> getZonedTime
2018-09-17 13:49:33.169797528 JST

> t
2018-09-17 05:49:36.757498845 UTC

> utcToZonedTime <$> getCurrentTimeZone <*> t
2018-09-17 14:49:40.930944714 JST

上手くいってますね!

Data.Time.LocalTime

時刻の取得・計算ができたら、あとは整形して出力するだけです!

Data.Time.LocalTime モジュールの関数を使って出力を整形してみましょう!

formatTime

formatTime 関数の使い方がわかれば、任意の形式で出力できるようになります。

> :t formatTime
formatTime :: FormatTime t => TimeLocale -> String -> t -> String

ここで FormatTime ttUTCTimeZonedTimeDay などの型が使えます。

型に応じて第三引数が変わるということです。

実際に使えばすぐに慣れます。(第一引数の値は defaultTimeLocale を指定しておけば良いのですが、自分でカスタマイズしたものを使うこともあります)

第二引数がフォーマット文字列なので、空文字列を与えれば当然結果も空になります。

> formatTime defaultTimeLocale "" <$> getZonedTime
""

フォーマットの指定方法については haddock を参照してください。

> formatTime defaultTimeLocale "%D" <$> getZonedTime
"09/17/18"

> formatTime defaultTimeLocale "%F" <$> getZonedTime
"2018-09-17"

> formatTime defaultTimeLocale "%x" <$> getZonedTime
"09/17/18"

> formatTime defaultTimeLocale "%Y/%m/%d-%T" <$> getZonedTime
"2018/09/17-13:52:21"

> formatTime defaultTimeLocale rfc822DateFormat <$> getZonedTime
"Sun, 16 Sep 2018 19:53:10 JST"

> formatTime defaultTimeLocale (iso8601DateFormat Nothing) <$> getZonedTime
"2018-09-16"

文字列をパーズして ZonedTime や Day の値を作る

ここまでは現在時刻を元に時刻の計算や出力結果の整形を行いました。

しかし、実際のプログラムでは文字列をパーズして ZonedTimeDay の値に変換したいこともあるでしょう。そのような場合は parseTimeM を使うと便利です。

> :t parseTimeM
parseTimeM
  :: (Monad m, ParseTime t) =>
     Bool -> TimeLocale -> String -> String -> m t

型がわかりづらいですが、具体的にはこんな型で利用することができます。

  • 第一引数は 空白 を許容するかどうかのフラグです (True だと空白OK)
  • 第二引数は気にせず defaultTimeLocale を指定しておきましょう
  • 第三引数は パーズで利用するフォーマット を指定します
  • 第四引数は 入力の文字列 です

具体例

実際にいくつか使ってみましょう。以下の通り %FYYYY-MM-DD の書式になります。

> formatTime defaultTimeLocale "%F" <$> getZonedTime
"2018-09-17"

モナドを IOMaybe などに変化させた基本的な例。

> parseTimeM True defaultTimeLocale "%F" "2018-09-17" :: IO ZonedTime
2018-09-17 00:00:00 +0000

> parseTimeM True defaultTimeLocale "%F" "2018-09-17" :: Maybe ZonedTime
Just 2018-09-17 00:00:00 +0000

第一引数を変化させて、入力文字列の空白の有無について確認する例。

> parseTimeM True defaultTimeLocale "%F" " 2018-09-17 " :: IO ZonedTime
2018-09-17 00:00:00 +0000

> parseTimeM False defaultTimeLocale "%F" " 2018-09-17 " :: IO ZonedTime
*** Exception: user error (parseTimeM: no parse of "2018-09-17 ")

入力文字列とパーズの書式がマッチしない例

> parseTimeM False defaultTimeLocale "%x" " 2018-09-17 " :: IO ZonedTime
*** Exception: user error (parseTimeM: no parse of " 2018-09-17 ")

Day 型の値をとしてパーズする例

> parseTimeM True defaultTimeLocale "%F" "2018-09-17" :: IO Day
2018-09-17

このようにして日付を取得できれば、今回は説明していませんが Data.Time.CalendaraddDays 関数などを使って日付の計算を行うこともできるようになります。

> d = parseTimeM True defaultTimeLocale "%F" "2018-09-17" :: IO Day

> addDays 1 <$> d
2018-09-18

> addDays 35 <$> d
2018-10-22

まとめ

  • time パッケージを使うと時刻や日付の計算ができる
  • 現在の日本時間を取得した場合は getCurrentTime ではなく、getZonedTime を使う
  • 整形には formatTime を使う
  • 文字列から ZonedTimeDay に変換する際は parseTimeM を使う

Haskell入門の 7.7 日付・時刻を扱う にも3ページほど time パッケージの解説があるので、気になる人はそちらも確認してみると良いかもしれません。

以上です。

おまけ

getZonedTime に対して formatTime defaultTimeLocale <フォーマット文字> の対応表です。

> getZonedTime
2018-09-17 14:44:52.052040178 JST
文字 出力結果
%-z +900
%_z + 900
%0z +0900
%^z +0900
%#z +0900
%8z +00000900
%_12z + 900
%% %
%t \t
%n \n
%z +0900
%Z JST
%c Mon Sep 17 14:39:34 JST 2018
%R 14:39
%T 14:40:12
%X 14:40:31
%r 02:40:55 PM
%P pm
%p PM
%H 14
%k 14
%I 02
%l 2
%M 43
%S 49
%q 903244678000
%Q .28084722
%s 1537163079
%D 09/17/18
%F 2018-09-17
%x 09/17/18
%Y 2018
%y 18
%C 20
%B September
%b Sep
%h Sep
%m 09
%d 17
%e 17
%j 260
%f 20
%V 38
%u 1
%a Mon
%A Monday
%U 37
%w 1
%W 38