はじめに

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

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

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

$ cabal repl -b time==1.10

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

基本的には Data.Timeimport して使います。

import Data.Time

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

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

rio を利用している場合

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

import RIO.Time

Data.Time.LocalTime

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

getZonedTime

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

> :t getZonedTime
getZonedTime :: IO ZonedTime

> getZonedTime
2020-06-20 13:18:40.677811323 JST

getCurrentTimeZone

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

> :t getCurrentTimeZone
getCurrentTimeZone :: IO TimeZone

> getCurrentTimeZone
JST

zonedTimeToUTC

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

> :t zonedTimeToUTC
zonedTimeToUTC :: ZonedTime -> UTCTime

> zonedTimeToUTC <$> getZonedTime
2020-06-20 04:20:14.529514141 UTC

utcToZonedTime

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

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

> utcToZonedTime <$> getCurrentTimeZone <*> getCurrentTime
2020-06-20 13:20:28.011749783 JST

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

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

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

addUTCTime :: NominalDiffTime -> UTCTime -> UTCTime

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

nominalDay :: NominalDiffTime
nominalDay = 86400

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

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

> t1 = addUTCTime nominalDay . zonedTimeToUTC <$> getZonedTime
> getZonedTime
2020-06-20 13:22:26.700694373 JST

> utcToZonedTime <$> getCurrentTimeZone <*> t1
2020-06-21 13:22:33.553973172 JST

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

> t2 = addUTCTime (60 * 60) . zonedTimeToUTC <$> getZonedTime
> getZonedTime
2020-06-20 13:22:58.351335073 JST

> t2
2020-06-20 05:23:04.594425732 UTC

> utcToZonedTime <$> getCurrentTimeZone <*> t2
2020-06-20 14:23:12.834203921 JST

上手くいってますね!

Data.Time.LocalTime

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

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

formatTime

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

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

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

formatTime :: TimeLocale -> String -> ZonedTime -> String
formatTime :: TimeLocale -> String -> UTCTime   -> String
formatTime :: TimeLocale -> String -> Day       -> String

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

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

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

> formatTime defaultTimeLocale "" <$> getZonedTime
""

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

> formatTime defaultTimeLocale "%D" <$> getZonedTime
"06/20/20"

> formatTime defaultTimeLocale "%F" <$> getZonedTime
"2020-06-20"

> formatTime defaultTimeLocale "%x" <$> getZonedTime
"06/20/20"

> formatTime defaultTimeLocale "%Y/%m/%d-%T" <$> getZonedTime
"2020/06/20-13:26:44"

> formatTime defaultTimeLocale rfc822DateFormat <$> getZonedTime
"Sat, 20 Jun 2020 13:26:50 JST"

Data.Time.Format.ISO8601

ISO8601 の書式は Data.Time.Format.ISO8601 モジュールの iso8601Show を利用します。

> iso8601Show <$> getZonedTime
"2020-06-20T13:31:08.7048868+09:00"

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

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

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

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

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

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

具体例

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

> formatTime defaultTimeLocale "%F" <$> getZonedTime
"2020-06-20"

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

> parseTimeM True defaultTimeLocale "%F" "2020-06-20" :: IO ZonedTime
2020-06-20 00:00:00 +0000

> parseTimeM True defaultTimeLocale "%F" "2020-06-20" :: Maybe ZonedTime
Just 2020-06-20 00:00:00 +0000

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

> parseTimeM True defaultTimeLocale "%F" " 2020-06-20 " :: IO ZonedTime
2020-06-20 00:00:00 +0000

> parseTimeM False defaultTimeLocale "%F" " 2020-06-20 " :: IO ZonedTime
*** Exception: user error (parseTimeM: no parse of " 2020-06-20 ")

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

> parseTimeM False defaultTimeLocale "%x" " 2020-06-20 " :: IO ZonedTime
*** Exception: user error (parseTimeM: no parse of " 2020-06-20 ")

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

> parseTimeM True defaultTimeLocale "%F" "2020-06-20" :: IO Day
2020-06-20

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

> d = parseTimeM True defaultTimeLocale "%F" "2020-06-20" :: IO Day

> addDays 1 <$> d
2020-06-21

> addDays 35 <$> d
2020-07-25

まとめ

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

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

以上です。

おまけ

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

> getZonedTime
2020-06-20 13:41:37.314698155 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