Crystal言語の紹介と、Crystalで日報ファイル作成スクリプトを（再）実装した話

## はじめに

最近個人的に注目しているプログラミング言語に[Crystal](https://ja.crystal-lang.org/)があります。
そのCrystalが今年になって[^windows]、~~ようやく~~Windows (x86-64)向けのコンパイラが（まだunsupported扱いですが）公式から[リリース](https://github.com/crystal-lang/crystal/releases)されるようになりました。

[^windows]: [2022/01/06リリースの1.3.0](https://github.com/crystal-lang/crystal/releases/tag/1.3.0)から

筆者は普段の業務でWindowsを使用しており、業務で度々使用する各種便利スクリプトをPythonで書いていました。
なので、これを機にそれらのPythonスクリプトをCrystalで書き直してみたくなりました。

そこでまず手始めに、毎日使用している日報ファイル作成スクリプトをCrystalで（再）実装してみることにしました。

## Crystalとは

Crystalの特徴~~という名の個人的な注目ポイント~~です。

- Rubyの強い影響を受けた、読みやすく書きやすい構文を持つスクリプト言語
- ただしRubyと違い、静的型付き言語
- 優秀な型推論があるため、あまり型を意識することなくコーディングが可能
- 型安全・null安全[^null-safety]
- バックエンドにLLVMを用いており、高速なネイティブコードにコンパイル可能
- シングルバイナリを生成できるため、配布や実行環境の構築がとても簡単
- 標準ライブラリが充実しており、かゆいところに手が届く

[^null-safety]: null安全はモダンな言語なら当然かもしれませんが

## 今回実装したいスクリプトの動作

1. テンプレートファイル`template.md`を読み込む
1. テンプレートファイルの中の特定の文字列を今日の日付や曜日に変換する
1. それを指定のパス`./年/月/daily_年月日.md`(e.g. `./2022/03/daily_20220304.md`）に保存する
1. 生成された`.md`ファイルを、関連付けられたテキストエディタで開く

## 実際の実装

### 環境

実行環境はWindows 10 Pro (64 bit)、
使用したCrystalのバージョンは

```
Crystal 1.3.2 [932f193ae] (2022-01-18)

LLVM: 10.0.0
Default target: x86_64-pc-windows-msvc
```

です。

### スクリプト本体

```crystal:open_daily_report.cr
require "file_utils"
require "json"

def get_day_of_week_name(time : Time, config : JSON::Any) : String
  case time.day_of_week
  in Time::DayOfWeek::Monday    then config["dayOfWeek"]["monday"].to_s
  in Time::DayOfWeek::Tuesday   then config["dayOfWeek"]["tuesday"].to_s
  in Time::DayOfWeek::Wednesday then config["dayOfWeek"]["wednesday"].to_s
  in Time::DayOfWeek::Thursday  then config["dayOfWeek"]["thursday"].to_s
  in Time::DayOfWeek::Friday    then config["dayOfWeek"]["friday"].to_s
  in Time::DayOfWeek::Saturday  then config["dayOfWeek"]["saturday"].to_s
  in Time::DayOfWeek::Sunday    then config["dayOfWeek"]["sunday"].to_s
  end
end

def format(str : String, time : Time, config : JSON::Any) : String
  time.to_s(str.gsub("%-a", get_day_of_week_name(time, config)))
end

config = File.open("config.json") do |f|
  JSON.parse(f)
end

current_time = Time.local
template_file = Path[config["templateFile"].to_s]

dir_name = Path[format(config["outputDir"].to_s, current_time, config)]
file_name = Path[dir_name, format(config["outputFile"].to_s, current_time, config)]

unless File.exists?(file_name)
  FileUtils.mkdir_p(dir_name)
  File.open(file_name, "w") do |f|
    f.puts format(File.read(template_file), current_time, config)
  end
end
Process.new("cmd.exe", ["/c", "start", file_name.expand.to_s])
exit
```

### 設定ファイル

```json:config.json
{
    "templateFile": "template.md",
    "outputDir": "./%Y/%m",
    "outputFile": "daily_%Y%m%d.md",
    "dayOfWeek": {
        "monday": "月",
        "tuesday": "火",
        "wednesday": "水",
        "thursday": "木",
        "friday": "金",
        "saturday": "土",
        "sunday": "日"
    }
}
```

### テンプレートファイル

```md:template.md
# %Y年%m月%d日（%-a）日報

## 今日やったこと

- 

## 明日やること

- 

## 連絡事項とか

- 

```

### 使い方

上記の3ファイルを同一のディレクトリに保存してやり、PowerShell等のお好きなシェルで

```powershell
crystal run open_daily_report.cr
```

として実行してあげる[^path]と、設定されたパスに下記のような日報ファイルが生成され、`.md`が関連付けられたテキストエディタで開かれます。

[^path]: コンパイラへの`PATH`は既に通してあるものとします。

```md:./2022/03/daily_20220304.md
# 2022年03月04日（金）日報
（略）
```

後は開かれたテキストエディタ上で日報を書いて、決められた提出先に提出するだけです。

#### 使用できるフォーマット／ディレクティブについて

設定ファイルやテンプレートには、`%Y`、`%m`、`%d`など[`Time::Format`で定められたディレクティブ](https://crystal-lang.org/api/1.3.2/Time/Format.html)（指示文字列）を使用できます。
Crystalのディレクティブは[Cのそれ](https://docs.microsoft.com/ja-jp/cpp/c-runtime-library/reference/strftime-wcsftime-strftime-l-wcsftime-l?view=msvc-170#generic-text-routine-mappings)や[Pythonのそれ](https://docs.python.org/ja/3/library/time.html#time.strftime)とほぼ互換性があるようですが、一部独自拡張があるようです。

また、Crystalでは現状で曜日等にロケールを使用できない（i.e. 日月火……みたいな日本語の曜日名を使用できない）ようだったので、`%-a`という独自拡張を作り、それを設定ファイルで設定した曜日名に置換するようにしました。

:::note warn
既知の不具合

現時点（Crystal 1.3.2）において、Windows環境では`Time.local`が常にUTCでの時刻を返すようです。
そのため、本スクリプトもUTCでの日時が適用されます。
例えばタイムゾーンが日本に設定されたWindows環境で上記のテンプレートを使用した場合、08:59以前はディレクティブが前日の日付に置換されます[^jst]。

[^jst]: 筆者は業務が終了する夕方～夜のタイミングで日報を書き始めることが多いので、この不具合も特に困ることはありませんでした。ただ、9時より前に日報を書き始める方（今日やった業務と所要時間をリアルタイムで記録したい方など）は困るかもしれません。
:::

### シングルバイナリにビルドする

想定どおり動作することが確かめられたので、実行ファイルを生成してみましょう。

お好みのシェルで

```powershell
crystal build open_daily_report.cr --release
```

としてやると、同じディレクトリに`open_dialy_report.exe`が生成されます。
次回からはこのexeを実行するだけで、日報ファイルが生成されるようになります。

また、他のPC等に配布する際も、このexeとテンプレート、設定ファイルの3つを配るだけでOKになりました。

自分が書いたスクリプトを他人に使ってもらうのに、いちいち「あらかじめ○○っていう言語の処理系をインストールしてパスを通しておいてください」みたいな事前準備が要らなくなるのは嬉しいですね。

## おわりに

これだけ短いスクリプトを書いただけでも、Crystalは

- 簡単に習得できる
- 型安全・null安全なコードをサクッと書ける
- 強力な標準ライブラリによって目的を簡単に実現できる
- 高速なネイティブコードを出力・実行可能

と、とても便利なLightweight[^lightweight] Languageだということを実感できました。

[^lightweight]: 「お手軽」という意味でも「軽量」という意味でもLightweightですね。

一方で、現状では

- 特にWindows環境ではまだ不具合も多い
- まだまだ公式のリファレンス以外に情報が少ない（日本語・英語とも）
- 公式のリファレンスも情報が少なかったりする（特に標準ライブラリ周り）ので、結局ソースコードから実装を読み解く羽目になることもある

といった課題も感じました。

Crystalの今後のさらなる発展と普及に期待しつつ、他にも色々とスクリプトを書いてみようと思いました。

## 参考文献

- [crystal-lang/crystal: The Crystal Programming Language](https://github.com/crystal-lang/crystal)
- [言語リファレンス](https://ja.crystal-lang.org/reference/syntax_and_semantics/index.html)
- [標準ライブラリAPI](https://crystal-lang.org/api/1.3.2/)