v1.17の「Duration and shift/2 functions」を触ってみる

本記事は、「Elixirのアドベントカレンダー2024」の15日目の記事です。

https://qiita.com/advent-calendar/2024

Elixirのv1.18がもうすぐ出てきそうというタイミングで、久々にローカル環境のElixir環境をみたら、v1.14で止まっていました。
直近で触っていたのは**Livebook**だったため、Elixir本体を直接触っていませんでした。そのため、随分とバージョンに差があいてしまっていました。とほほ...

さて、遅れてしまったものを嘆いていても仕方ない。
気持ちを切り替えて、新しいバージョンをインストール...と思ったら、少し前にマシンのCPUがAppleシリコン製に変わっていたこともあり、asdfも失敗。
幸いなことに、先人達がAppleシリコン製のインストール方法を残してくださっていますが、手っ取り早く動かしておきたかったので、今回はdockerを頼ることにしました。

## Elixir v1.17-otp-27をDocker経由でインストール

Elixirの公式イメージを確認。
- https://hub.docker.com/_/elixir

2024年12月15日時点では、1.17.3系が最新。
v1.17系であればいいが、optは最新の27系がいいなと思い、「**1.17-otp-27**」にする。

ひとまず、iexで起動すればいいかなと、以下を実施。

```
docker run -it --rm elixir:1.17-otp-27
```

実行結果はこちら。

```
$docker run -it --rm elixir:1.17-otp-27
Unable to find image 'elixir:1.17-otp-27' locally
1.17-otp-27: Pulling from library/elixir
82312fccb35f: Pull complete
4ac722d9cf93: Pull complete
261351ed796d: Pull complete
a9d319298afc: Pull complete
e094548c0cea: Pull complete
d1a116ea1e30: Pull complete
40d63b8a7c51: Pull complete
94b3c35d2121: Pull complete
Digest: sha256:197b5e195e8413cf898548d71d3023ab8b074f9e5b7ac90c60d37804a9515da1
Status: Downloaded newer image for elixir:1.17-otp-27
Erlang/OTP 27 [erts-15.1.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Interactive Elixir (1.17.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
```

良かった。起動した。

## Duration and shift/2 functionsを試す

さて、起動したのはいいけど、何しようかな...とリリースノートを見ていると、「**Adding Duration and shift/2 functions**」の文字が。

そういえば、直近お仕事で「起点日からNヶ月後の開始日／終了日を算出する」という小さなロジックを書いてたっけ...と思い出し、興味を持ちました。

なので、リハビリがてら、ここをちょっと触ってみることに。

では、早速。
シジルで、NativeData型を定義

```elixir
iex> now = ~N[2024-12-15 23:21:11]
~N[2024-12-15 23:21:11]
```

サンプルに従い、「shift/2」を試してみる。

- 「Date.shift/2」の場合、当然「時刻」部分はカットされる
- 「Time.shift/2」の場合、当然「日付」部分はカットされる
- 「NaiveDateTime.shift/2」の場合、日時は残ったまま


```elixir
iex> now |> Date.shift(month: 1)
~D[2025-01-15]

iex> now |> Time.shift(hour: 1)
~T[00:21:11]

iex> now |> NaiveDateTime.shift(hour: 1)
~N[2024-12-16 00:21:11]
```

年月日、時分秒はそれぞれ指定できる。
もちろん、Dateでは「年月日」のみ、Timeでは「時分秒」しか指定できない。
指定すると、怒れる。

```elixir
iex> now |> NaiveDateTime.shift(year: 2)
~N[2026-12-15 23:21:11]

iex> now |> NaiveDateTime.shift(month: 2)
~N[2025-02-15 23:21:11]

iex> now |> NaiveDateTime.shift(day: 2)
~N[2024-12-17 23:21:11]

iex> now |> NaiveDateTime.shift(hour: 2)
~N[2024-12-16 01:21:11]

iex> now |> NaiveDateTime.shift(minute: 2)
~N[2024-12-15 23:23:11]

iex> now |> NaiveDateTime.shift(second: 2)
~N[2024-12-15 23:21:13]

## Dateの場合

iex> now |> Date.shift(year: 2)
~D[2026-12-15]

iex> now |> Date.shift(month: 2)
~D[2025-02-15]

iex> now |> Date.shift(day: 2)
~D[2024-12-17]

iex> now |> Date.shift(hour: 2)
** (ArgumentError) unsupported unit :hour. Expected :year, :month, :week, :day
    (elixir 1.17.3) lib/calendar/date.ex:819: Date.validate_duration_unit!/1
    (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir 1.17.3) lib/calendar/date.ex:813: Date.__duration__!/1
    (elixir 1.17.3) lib/calendar/date.ex:802: Date.shift/2
    iex:63: (file)

## Timeの場合

iex> now |> Time.shift(hour: 2)
~T[01:21:11]

iex> now |> Time.shift(minute: 2)
~T[23:23:11]

iex> now |> Time.shift(second: 2)
~T[23:21:13]

iex> now |> Time.shift(day: 2)
** (ArgumentError) unsupported unit :day. Expected :hour, :minute, :second, :microsecond
    (elixir 1.17.3) lib/calendar/time.ex:617: Time.validate_duration_unit!/1
    (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2
    (elixir 1.17.3) lib/calendar/time.ex:602: Time.__duration__!/1
    (elixir 1.17.3) lib/calendar/time.ex:584: Time.shift/2
    iex:67: (file)

```

複数の設定も可能

```elixir
iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2)
~N[2028-12-19 01:21:11]
```

マイクロ秒も指定できる。が、指定の仕方は、他の場合と異なり、値と有効桁数の指定になっている

```elixir
iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 6})
~N[2028-12-19 01:21:11.000500]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 5})
~N[2028-12-19 01:21:11.00050]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 4})
~N[2028-12-19 01:21:11.0005]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 3})
~N[2028-12-19 01:21:11.000]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 2})
~N[2028-12-19 01:21:11.00]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 1})
~N[2028-12-19 01:21:11.0]

iex> now |> NaiveDateTime.shift(year: 4, day: 3, hour: 2, microsecond: {500, 0})
~N[2028-12-19 01:21:11]
```


0指定や逆算もできるようです。

```elixir
iex> now |> NaiveDateTime.shift(day: 0)
~N[2024-12-15 23:21:11]

iex> now |> NaiveDateTime.shift(day: -2)
~N[2024-12-13 23:21:11]
```

解説にもあるように、shift/2を2回実施するのと、shift/2で2つ分を一気にずらるのとでは、進み方が違います。
ただ、これについては、影響を受けるのは、「月末＋年または月のshift」という気がしています。

影響を受けない場合（どちらも変わらない）

```elixir
iex> now |> NaiveDateTime.shift(month: 1) |> NaiveDateTime.shift(month: 1)
~N[2025-02-15 23:21:11]

iex> now |> NaiveDateTime.shift(month: 2)
~N[2025-02-15 23:21:11]
```

影響を受ける場合

```elixir
iex> ~N[2024-01-31 23:21:11] |> NaiveDateTime.shift(month: 1) |> NaiveDateTime.shift(month: 1) |> NaiveDateTime.shift(month: 1)
~N[2024-04-29 23:21:11]

iex> ~N[2024-01-31 23:21:11] |> NaiveDateTime.shift(month: 3)
~N[2024-04-30 23:21:11]


iex> ~N[2024-02-29 23:21:11] |> NaiveDateTime.shift(year: 1) |> NaiveDateTime.shift(year: 1) |> NaiveDateTime.shift(year: 1) |> NaiveDateTime.shift(year: 1)
~N[2028-02-28 23:21:11]

iex> ~N[2024-02-29 23:21:11] |> NaiveDateTime.shift(year: 4)
~N[2028-02-29 23:21:11]
```

タイムゾーン周りの設定もあるようだけど、そこはまたの機会に。