nullのある特定期間のバリデーション
仕事でマスク機能を開発することになり、その仕様で大変頭を使ったので備忘録として残しておく。
要件
- マスクを作成する際、公開/終了時間をそれぞれ設定できる
- 終了期間はnull可
- 公開時間 < 終了時間
- 既に設定されている公開期間と重複して設定できない
具体例
マスク①:2021/01/01 10:00~12:00
マスク②:2021/01/02 17:00~
以上2件のマスクが既に設定されていたとする。
ここで新たに
マスク③:2021/01/01 11:00~13:00
を作成しようとすると①の期間と重複する期間があるためバリデーションエラーで作成不可。
また、
マスク④:2021/01/02 18:00~20:00
も、②が17時以降ずっと設定されていることになるためバリデーションエラーで作成不可。
マスク⑤:2021/01/01/09:00~07:00
を作成することはもちろんできない。(どんな世界線だ)
実装
この上記バリデーションの要件をどう実装するか、大変頭を使った。
実際に実装しようとすると分かると思うが、これが本当に難しかった…。
紙に書きまくって「これはダメ」「それもダメ」といろいろ案を出していったがなかなか要件を満たせるコードが出てこない。
一旦単純に実装しようとし、SQLを分けて書いてみたがまぁ汚いクソコードができあがった。(それも残しておけば良かったな)
しかしこのクソコードがいい仕事をした。
ここにリファクタリングを加えることでなんと1行で実装することができた。
それがこちら。(実際のコードは他に条件が追加されているため期間部分のみ抽出)
self.class .where('end_at >= :start_at OR end_at IS NULL', start_at: start_at) .where('start_at <= :end_at OR :end_at IS NULL', end_at: end_at)
できあがったとき「私は天才だ…」と自分に惚れ惚れした。
これは紛うことなき天才だろう。
実際先輩に「スマート!」とお褒めの言葉をいただいた。
やっぱり天才だった。
やってみるといいかもしれないこと
今回の実装で紙に時間軸を書いてOK/NGな期間を可視化するようにしてみた。
この可視化が割と自分の頭の整理に繋がって助かった。
これからも複雑な要件があったときは紙に書き出してみることにしようと思う。
おわり。