RailsのTimezoneのparseメソッドを使用する前に正規表現でチェックすべき

この記事は、[リンクバルアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/linkbal)の24日目の記事です。

# はじめに
改めまして技術部のダットです！:tangerine:
[machicon JAPAN](https://machicon.jp/)という自社のメインサービスの開発をしています。
開発するとき、よく`RailsのActiveSupport::Timezoneのparse`メソッドを使っています。データベースに保存している日付の価値はUTC（_協定世界時_）ですから、ユーザーに日付を表示させるとき、そのユーザーのタイムゾーンの日付に変換する必要があります。ですが、`Timezoneのparse`を使用するとき、何を注意すべきか、この記事を通じて説明します。:hand::hand:

# Timezoneのparse、正規表現は何か？
`Timezoneのparse`を使用すると、指定したタイムゾーンに日付を変換できます。
```config/application.rb
# RailsアプリケーションのデフォルトのタイムゾーンをJST+9とする
config.time_zone = 'Tokyo'
# Time型のカラムはDBもアプリもUTCで扱う
# Rails 5.1からはJST+9として扱うことも出来る
config.active_record.time_zone_aware_types = [:datetime]
```
```console
pry(main)> Time.zone.parse('2022-12-24 12:00:00')
=> Sat, 24 Dec 2022 12:00:00 JST +09:00
```
記事のタイトルに`正規表現でチェックすべき`がありますが、`正規表現`はなんなんですか。
正規表現は文字列のパターンを記述するための言語です。この言語で記述されたパターンも正規表現と呼ばれます。英語なら`RegularExpression`で普通に省略にすると`Regexp`だと呼ばれています。
正規表現を利用すると、文字列が指定したパターンを含んでいるかどうかを判定できます。その故に、日付のフォマットをバリデーションできるようになります。
```app/models/user.rb
# YYYY-MM-DDのフォマットを設定する
BIRTHDATE_REGEXP = /^\d{4}-\d{2}-\d{2}$/
```
正規表現の具体的はこの記事に書いてありませんが、もし深く把握したい方がいれば[このURL](https://docs.ruby-lang.org/ja/latest/class/Regexp.html)が参考になるかもしれません。

# どうして日付を渡す前にバリデーションが必要か？
`Time.zone.parse`に正しくない日付を渡した時には、nilになったり例外が起きたりします。データベースから直接に日付の値をパースしたら全てRails側に任せてもいいです。なぜかと言うと、インプットの価値を管理できますから。
```console
pry(main)> Time.zone.parse('hoge')
=> nil

pry(main)> Time.zone.parse('2022/99/99')
ArgumentError: argument out of range
```
しかし、管理画面で[csvファイル](https://www.pc-master.jp/sousa/csv.html)のアップロード機能を使えば、第三者から値を挿入する場合もあるではないでしょうか。その時、渡す前に日時のフォマットをチェックしないと意外の不具合が発生する可能性があります。
```console
pry(main)> Time.zone.parse('monooxide')
=> Sat, 24 Dec 2022 00:00:00 JST +09:00 # 適当な文字列を渡したが、現在の日を返せる場合もある

pry(main)> Time.zone.parse('2022-010-08') # 2022年10月8日を渡したつもりが、10月の文字に余裕な0を入れてしまった
=> Wed, 10 Aug 2022 00:00:00 JST +09:00 # 例外が起きないで2022年8月10日の間違った日時を渡してしまった
```

# どうやって日時のフォマットをバリデーションするか？
上記の意外な不具合を防ぐために、日時をパースする前にフォマットをバリデーションする必要があります。
日付のフォーマットを正規表現でチェックして日付の値をRails側に任せても良いです。なぜなら、正規表現で日付の値までチェックしたらロジックはすごく複雑になってしまいます（_閏年かどうかチェックなど_）。
正規表現を作成するとき、`Regexp.union`を使って複数なパータンを組み合わせられます。例を挙げます。
- 正規表現を作成します
```console
pry(main)> hyphen_regexp = /^\d{4}-\d{2}-\d{2}$/
=> /^\d{4}-\d{2}-\d{2}$/ # YYYY-MM-DDのフォマットを許可するパータン

pry(main)> slash_regexp = /^\d{4}\/\d{2}\/\d{2}$/ 
=> /^\d{4}\/\d{2}\/\d{2}$/ # YYYY/MM/DDのフォマットを許可するパータン

pry(main)> BIRTHDATE_REGEXP = Regexp.union(hyphen_regexp, slash_regexp)　
=> /(?-mix:^\d{4}-\d{2}-\d{2}$)|(?-mix:^\d{4}\/\d{2}\/\d{2}$)/ # YYYY-MM-DDかYYYY/MM/DDのフォマットを許可するパータン
```
- 作成された正規表現で`match?`メソッドを使って日付をバリデーションします
```console
pry(main)> '2022-12-24'.match?(BIRTHDATE_REGEXP)
=> true

pry(main)> '2022/12/24'.match?(BIRTHDATE_REGEXP)
=> true

pry(main)> 'monooxide'.match?(BIRTHDATE_REGEXP)
=> false

pry(main)> '2022-010-08'.match?(BIRTHDATE_REGEXP)
=> false
```

## おわりに
parseメソッドを使って簡単に日付を渡せますが、利用するとき日付のフォマットを注意すべきです。
日付を限らず、値を渡す前にインプットのフォマットをちゃんと確認しないと意外な不具合が発生する場合もあります。これを心掛けて欲しいです。
ここまで読んでいただ、きありがとうございました。
この記事は皆さんに役立つ知識であれば幸いです。:pray:
# 参考
- http://blog.willnet.in/entry/2015/06/12/063731
- https://docs.ruby-lang.org/ja/latest/class/Regexp.html
- https://www.pc-master.jp/sousa/csv.html