[Elm] AlertmanagerにDatePickerを導入した話

Web UIを作る時に何かと需要があるdate picker
最近ElmでDatePicker触れる機会があったので、PurometheusのAlertmanagerにもdatepicker入った方が良くない？
と思って入れてみた。

# ElmのDatePicker

DatePickerについては
[Elmで日付を扱うならとりあえずこれ、なパッケージたち。](https://qiita.com/tmKentauros/items/0680bb98e305e885196a)
この記事が分かりやすくまとまってたので、参考にさせてもらった。

話の前段として、Elmで自由にカスタマイズできるDatePickerは少ない。というよりほぼ無い。
cssがelm-cssで埋め込まれていたり、入力にいinput fieldしか使えなかったり、、
既存のシステムに組み込むにはかなり怠いこと間違いなし。


### CurrySoftware/elm-datepicker

https://package.elm-lang.org/packages/CurrySoftware/elm-datepicker/latest
色々調べて一番良さそうだったのがこれ。
見た目が何よりもシンプル。そしてカスタマイズがしやすい。
↓な感じで、AttributesやFormatを自分で定義できるからほぼ思い通りのDatePickerが表示できるのが魅力。

```elm

type alias Settings =
    { placeholder : String
    , classNamespace : String
    , containerClassList : List ( String, Bool )
    , inputClassList : List ( String, Bool )
    , inputName : Maybe String
    , inputId : Maybe String
    , inputAttributes : List (Attribute Msg)
    , isDisabled : Date -> Bool
    , parser : String -> Result String Date
    , dateFormatter : Date -> String
    , dayFormatter : Weekday -> String
    , monthFormatter : Month -> String
    , yearFormatter : Int -> String
    , cellFormatter : String -> Html Msg
    , firstDayOfWeek : Weekday
    , changeYear : YearRange
    }
```

割と重要なのが、Posixで全部管理されていること。
[Elmで日付を扱う0.19編](https://qiita.com/miyamo_madoka/items/ae83fe3a43eac432434e)
の記事にあるように最近は標準のdateパッケージが無い。
ので、できるだけ出所不明のパッケージに依存しないという意味で最高。

基本的には[elm/time](https://package.elm-lang.org/packages/elm/time/latest)と[time-extra](https://package.elm-lang.org/packages/justinmimbs/time-extra/latest/Time.Extra)で事足りるけど、いろんなdateパッケージが出てて何使って良いのやらって感じ。
Elmerの人たちはDateパッケージの導入嫌うよね。独自のDate型に依存するくらいなら、時間は全部Posixで管理しようということなのだろう（と思う。この辺は正直分からない。）


というわけで 「CurrySoftware/elm-datepicker」をalertmanagerに入れてPR投げてみる。
https://github.com/prometheus/alertmanager/pull/2262

![82245921-c946a280-997e-11ea-89a6-d2ad73c65059.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/0c39c580-8c0f-e5b2-ba06-f6025488f646.gif)


が、速攻でTimePickerもクレとのレスがついた
> I guess it would be good to support time too but probably it isn't that easy (at least the component supports only dates)?

Time Pickerってそんなに必要じゃなく無い？
個人的には時間まできたら手入力で良いと思ってる。

が、あった方が便利なのは間違いないので、今度はDateTimePickerを探す

# ElmのDateTimePicker
DateTimePickerとなるとグッと選択肢が狭まる。
これが最高！というものは無いが、良さそうなのはいくつか。
### mercurymedia/elm-datetime-picker
最初に[w0rm](https://github.com/w0rm)にお勧めされたのがこれ
https://package.elm-lang.org/packages/mercurymedia/elm-datetime-picker/latest/
DateTimePickerとしては申し分ない作り。最高だね。これ使おう。
と思ったが、
**Dateを選択するとtimeが0クリアされる仕様**
人によってどう思うか違うところだが、せっかく設定した時間が日付変更で0クリアされるのは個人的には頂けない。

他のも探してみる。

### PanagiotisGeorgiadis/elm-datepicker

次点で良さそうなのが
[PanagiotisGeorgiadis/elm-datepicker](https://package.elm-lang.org/packages/PanagiotisGeorgiadis/elm-datepicker/latest/)
見た目もカッコいいし、duration pickerがついてるからAlertmanagerにはあってるかな。
「せっかくなのでこれ使って、duration pickerにしよう」ということになったが、１点致命的な欠陥が。。。
**Duration Pickerでは、開始日/終了日に同じ日付が選べない仕様**
これは仕様なのか？と疑いたくなる様な挙動。Alertmanager的にはこれはNG。

色々あって、最終的には自分で作った方が早くね？となったのでした。

# DateTimePickerを作る
コードはここ
https://github.com/prometheus/alertmanager/pull/2262/files
で以上なのだが、
意外と簡単にできたのでメモ。

## DatePicker
肝になるのが下のアップデート文。DatePickerに関してはほぼこれが全て

```elm

-- UPDATE
....
        MouseOverDay time ->
            { dateTimePicker | mouseOverDay = Just time }

        ClearMouseOverDay ->
            { dateTimePicker | mouseOverDay = Nothing }

        OnClickDay ->
            let
                addDateTime_ : Posix -> Maybe Posix -> Posix
                addDateTime_ date maybeTime =
                    case maybeTime of
                        Just time ->
                            floorDate date
                                |> Time.posixToMillis
                                |> (\d ->
                                        trimTime time
                                            |> Time.posixToMillis
                                            |> (\t -> d + t)
                                   )
                                |> Time.millisToPosix

                        Nothing ->
                            floorDate date

                updateTime_ : Maybe Posix -> Maybe Posix -> Maybe Posix
                updateTime_ maybeDate maybeTime =
                    case maybeDate of
                        Just date ->
                            Just <| addDateTime_ date maybeTime

                        Nothing ->
                            maybeTime

                ( startDate, endDate ) =
                    case dateTimePicker.mouseOverDay of
                        Just m ->
                            case ( dateTimePicker.startDate, dateTimePicker.endDate ) of
                                ( Nothing, Nothing ) ->
                                    ( Just m
                                    , Nothing
                                    )

                                ( Just start, Nothing ) ->
                                    case
                                        compare (floorDate m |> Time.posixToMillis)
                                            (floorDate start |> Time.posixToMillis)
                                    of
                                        LT ->
                                            ( Just m
                                            , Just start
                                            )

                                        _ ->
                                            ( Just start
                                            , Just m
                                            )

                                ( Nothing, Just end ) ->
                                    ( Just m
                                    , Just end
                                    )

                                ( Just start, Just end ) ->
                                    ( Just m
                                    , Nothing
                                    )

                        _ ->
                            ( dateTimePicker.startDate
                            , dateTimePicker.endDate
                            )
            in
            { dateTimePicker
                | startDate = startDate
                , endDate = endDate
                , startTime = updateTime_ startDate dateTimePicker.startTime
                , endTime = updateTime_ endDate dateTimePicker.endTime
            }

-- VIEW

-- カレンダー１マス分のviewを抜粋
        [ div
            [ class ("date front" ++ mouseoverClass ++ startClass ++ endClass ++ thisMonthClass)
            , onMouseOver <| MouseOverDay day
            , onClick OnClickDay
            ]
            [ text (Time.toDay utc day |> String.fromInt) ]
        ]
```

`MouseOverDay time`でマウスが置かれているマス（日付）をモデル`mouseOverDay`に記録する。
`OnClickDay`でクリックされた日付（mouseOverDayに設定された日付）をpick済みの日付としてセットする（startDate, endDate）。

あとは、Duration Pickerなので、クリックした２つの日付比べて、startDateにするかendDateにするかを長々と判定している。

## TimePicker
TimePicker側はDateと明確に分けるためにstartTime,endTimeの入れ物を定義している。

```elm

-- UPDATE
....
        SetInputTime startOrEnd inputHourOrMinute num ->
            let
                limit_ : Int -> Int -> Int
                limit_ limit n =
                    if n < 0 then
                        0

                    else
                        modBy limit n

                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
                updateHourOrMinute_ ihom s =
                    case ihom of
                        InputHour ->
                            updateHour (limit_ 24 num) s

                        InputMinute ->
                            updateMinute (limit_ 60 num) s

                ( startTime, endTime ) =
                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
            in
            { dateTimePicker | startTime = startTime, endTime = endTime }

        IncrementTime startOrEnd inputHourOrMinute num ->
            let
                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
                updateHourOrMinute_ ihom s =
                    let
                        compare_ : Posix -> Posix
                        compare_ a =
                            if
                                (floorDate s |> Time.posixToMillis)
                                    == (floorDate a |> Time.posixToMillis)
                            then
                                a

                            else
                                s
                    in
                    case ihom of
                        InputHour ->
                            addHour num s
                                |> compare_

                        InputMinute ->
                            addMinute num s
                                |> compare_

                ( startTime, endTime ) =
                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
            in
            { dateTimePicker | startTime = startTime, endTime = endTime }

-- VIEW
        , div [ class "minute" ]
            [ button
                [ class "up-button d-flex-center"
                , onClick <| IncrementTime startOrEnd InputMinute 1
                ]
                [ i
                    [ class "fa fa-angle-up" ]
                    []
                ]
            , input
                [ on "blur" <| Decode.map (SetInputTime startOrEnd InputMinute) targetValueIntParse
                , value
                    (case startOrEnd of
                        Start ->
                            case dateTimePicker.startTime of
                                Just t ->
                                    Time.toMinute utc t |> String.fromInt

                                Nothing ->
                                    "0"

                        End ->
                            case dateTimePicker.endTime of
                                Just t ->
                                    Time.toMinute utc t |> String.fromInt

                                Nothing ->
                                    "0"
                    )
                , maxlength 2
                , class "view"
                ]
                []
```

ボタンクリックで時間をインクリメントしたい場合は``IncrementTime``を、テキスト入力で時間を指定したい場合は``SetInputTime``を、
という形で若干こだわったので長い。
全部Posixで管理するためには、Time.floorとかTime.posixToMillisとか頻繁に使ってDateとTimeの足算をしてあげないといけない。
この辺りは無駄にコードが長くなるので、全て別ファイル`Utils.elm`にまとめた。

というわけで完成品
![87542666-5176c900-c6de-11ea-9a7b-64b11e2acf48.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/65a7bf63-425c-8045-4ec9-8053c47cbbbe.gif)


色々とコードを直してもらったが無事にマージされた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/7c21de3f-dfd0-65bc-f3d0-7f5da8f383c8.png)

# 所感
ElmのDateTimePickerは外部ライブラリが多くあり、それなりに使えそう。自由にカスタマイズできるものは少ない。
どれも若干クセがあるので、選定の前に要件をはっきりさせておかないと路頭に迷う。
（↑はElmに限った話では無いが、どのライブラリもサクッと導入できる物ではないので、トライ＆エラーで選ぶと労力が半端ない）

ElmでDatePickerを作るのはかなり簡単。気に入ったライブラリがなければ、自分で作るのは選択肢の１つだと思う。
DatePickerSDKとかDatePickerHelperとかが欲しい気分