チェシャ猫の消滅定理

数学にプログラミング、素敵なもの何もかも。

elm/time の使い方

はじめに

先日、Elm v0.19 がリリースされました。公式ライブラリのリポジトリelm-lang から elm に変更され、その中身も大きく再構成されています。

本記事では、これらの変更のうち特に時刻や日付の扱いに関する部分について、新しい API の使い方を含めて簡単に解説します。

v0.18 における時刻の扱い

v0.18 では、時刻を扱う機能は標準パッケージ elm-lang/core の中で提供されていました。時刻を扱う Time モジュールと日付を扱う Date モジュールで、それぞれデータ型や関数が定義されているのが特徴です。

なお、旧バージョンのライブラリは現在 Elm Packages の検索にはヒットしない ので、中身を確認するためには直接 URL にアクセスする必要があります。

旧 Time モジュール

Time - core 5.1.1

時刻を扱う Time 型を提供します。Time 型の実体は Float 型のエイリアスで、Unix Epoch からの経過ミリ秒数を表します。

メインの関数は現在時刻を取得する now と指定した時間間隔で Msg を送出する every です。その他、every と組み合わせて使用する単位として second : Timeminute : Time が定義されており、例えば毎秒何かを行う Subscription は次のように書けます。

type Msg =
    DoSomethingAt Time.Time

subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every Time.second DoSomethingAt

旧 Date モジュール

Date - core 5.1.1

日付を扱う Date 型を提供します。また Time 型との変換用の関数もこちらで定義されています。

その他、紛らわしいですが Date モジュールでも now 関数がエクスポートされており、こちらは現在の日付を取得します。

特徴的なのは、Time 型と Date 型の変換においてタイムゾーンを指定する機能がないことです。したがって API 定義上は

toTime : Date -> Time
fromTime : Time -> Date

の変換は純粋な関数に見えますが、実際にはシステムのタイムゾーンに依存する副作用を持っていることになります。

v0.19 における時刻の扱い

さて、今回新しくなったバージョンでは、旧 TimeDate に相当する機能が再編成されてひとつのモジュール Time になり、さらに別ライブラリ elm/time として切り出されました。

新 Time モジュール

Time - time 1.0.0

大きな変更点は、Unix 時間を表す Posix 型に加えて、タイムゾーンを表す Zone 型が陽に導入されたことです。

現在時刻を取得するには今まで通り now で、現在のタイムゾーンを取得するには here を使用します。例えば初期化の際、両者を同時に取得する Cmd は以下のように書けます。

type Msg
    = SetSystemTime (Time.Zone, Time,Posix)

setSystemTime : Cmd Msg
setSystemTime =
    Task.perform SetSystemTime <| Task.map2 Tuple.pair Time.here Time.now

Date モジュールにあった日付への変換も Time の中にまとめられました。Unix 時間が同じでも実際の日付はタイムゾーンに依存するため、変換には Zone 型が必要になっているのが分かります。新しい変換関数名には toXXX で統一されており、Day 型は Weekday 型に、dayOfWeektoWeekday に変更されました。

toYear : Zone -> Posix -> Int
toMonth : Zone -> Posix -> Month
toWeekday : Zone -> Posix -> Weekday
toHour : Zone -> Posix -> Int
toMinute : Zone -> Posix -> Int
toSecond : Zone -> Posix -> Int
toMillis : Zone -> Posix -> Int

また、旧 Time モジュールにあった時間の単位 minutesecond は外されました。ミリ秒単位で直接指定する必要があります。

サンプル:デジタル時計

以上をまとめると、Elm v0.19 対応の簡単なデジタル時計は次のように実装することができます。

  • 初期化時に setSystemTime で現在のタイムゾーンと時刻を取得
  • それ以後 1000 ミリ秒ごとに setCurrentTime で現在時刻を更新
  • 表示の際は toHourtoMinuteタイムゾーン依存の時刻に変換

という流れになっています。

module Main exposing (main)

import Browser
import Html exposing (..)
import Task
import Time


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


type alias Model =
    { zone : Time.Zone
    , posix : Time.Posix
    }


type Msg
    = SetSystemTime ( Time.Zone, Time.Posix )
    | SetCurrentTime Time.Posix


init : () -> ( Model, Cmd Msg )
init _ =
    ( { zone = Time.utc, posix = Time.millisToPosix 0 }, setSystemTime )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetSystemTime ( zone, time ) ->
            ( { zone = zone, posix = time }, Cmd.none )

        SetCurrentTime time ->
            ( { model | posix = time }, Cmd.none )


view : Model -> Html Msg
view model =
    let
        h =
            Time.toHour model.zone model.posix

        m =
            Time.toMinute model.zone model.posix
    in
    div [] [ text <| String.fromInt h ++ ":" ++ String.fromInt m ]


setSystemTime : Cmd Msg
setSystemTime =
    Task.perform SetSystemTime <| Task.map2 Tuple.pair Time.here Time.now


subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every 1000 SetCurrentTime

Time.Extra モジュール

ところで、実際に時刻を扱うアプリを書いてみると、elm/time はかなり非力であることがわかります。特に以下のようなケースは問題になりそうです。

  • 時刻 + タイムゾーンから Unix 時間に変換できない(例:特定の日付と現在時刻を比較したい)
  • 時刻の和や差が取れない(例:ちょうど 1 か月後の日付が欲しい)
  • 時刻を丸めることができない(例:次に 00 秒になるタイミングで Msg を発生させたい)

このような問題を解決するために、justinmimbs/time-extra が使用できます。

time-extra 1.0.1

Time.Extra モジュールでは旧 Date 型に代わるものとして Parts 型を定義しており、Zone 型を組み合わせることで各タイムゾーンにおけるその時刻の Unix 時間を得ることができます。

partsToPosix : Zone -> Parts -> Posix

-- UTC における 2018/09/26 11:23.45.00
time1 : Posix
time1 =
    partsToPosix utc <| Parts 2018 Sep 26 11 23 45 0

また、旧 Time モジュールの minutesecond や代わる時間単位として Interval 型を定義しており、これを使って「1 か月後の日付」や「次に 00 秒ちょうどになる時刻」が取得できるようになっています。

add : Interval -> Int -> Zone -> Posix -> Posix
ceiling : Interval -> Zone -> Posix -> Posix

-- UTC における 2018/09/26 11:23.45.00 の 1 か月後
time2 : Posix
time2 =
    add Month 1 utc <| partsToPosix utc <| Parts 2018 Sep 26 11 23 45 0

-- UTC における 2018/09/26 11:23.45.00 以降、最初に 00 秒になる瞬間
time3 : Posix
time3 =
    ceiling Minute utc <| partsToPosix utc <| Parts 2018 Sep 26 11 23 45 0

まとめ

本記事では Elm v0.19 で刷新された elm/time について、旧バージョンとの違いや使い方について簡単に解説しました。

  • 時刻パッケージは elm-lang/core から elm/time に移動
  • 原則 Posix を操作し、通常の時刻表示に変換した時は Zone と合わせる
  • 時刻を操作するには justinmimbs/time-extra ライブラリが使える

ところで、今回紹介した新しい API を使ってちょっと面白いサンプルアプリを作ってみたので、GIF アニメにしたものを貼っておきます。

https://raw.githubusercontent.com/y-taka-23/elm-clockclock24/master/demo.gif

元ネタは Humans since 1982 の作品 ClockClock 24 です。ソースコードは以下にあるので elm/time の使い方の参考に。

github.com

ちなみにこのサンプル、実際には elm/time ではなくそれ以外の部分の実装のほうが大変だったのですが、それはまた別の話。