結論
-
*_since
、*_ago
は使うな - 代わりに
from_now/ago/after/before
+/-
を適切に使おう
はじめに
Rails便利ですね。
個人的にRailsの中ではActiveRecordが最高のツールですが、ActiveSupportも変態拡張が多くて大好きです。
current_user.authorized_at <= 1.day.ago # 1.day.before(Time.now)
とか気持ちいいですね。
あるPR
user.expires_on = Date.today.months_since(6)
これを見たときにすっと思考に入ってきますか?
僕はこれが未来を指すのか過去を指すのか即座に判断できません。
英語の例文を考えてみましょう。
The user's permission will expire after 6 months.
どこにもsinceは現れません。
The user's permission has expired since 1 month ago.
なので私は、PRで months_since
を見ると
user.expires_on = 6.months.after
# 他にもこんな書き方ができるよ!
# user.expires_on = Date.today + 6.months
# user.expires_on = 6.months.after(Date.today) # 丁寧な英語っぽく
# user.expires_on = 6.months.from_now.to_date
という指摘をすることがあります。
何にこだわっているのか
コードを書くときには読み手に負担が少ないことが大事です。
Railsガイドにも *_since(ago)
の記載があり、Rails開発者の中ではメジャーな存在となっていると思いますが、読み心地が極端に悪いと僕は考えています。
すべてのコードが英語としてリーダブルであることを求める必要はありませんが、別の書き方で読みやすくなるならそちらで記述した方がいいでしょう。
sinceのもたらす語感
英語のsinceは、起点を示してそこから何かが継続しているイメージを想起させます。
引用元: https://www.english-speaking.jp/difference-between-from-and-since/
しかしmonths_since(6)
と出てくるとその感覚がバカになるのです。1
念のために原点を確認してみた
古いCHANGELOGを見ると、すでにその姿を見つけられました。1.0.0のころの記述ですね。
* Added Time::Calculations to ask for things like Time.now.tomorrow, Time.now.yesterday, Time.now.months_ago(4) #580 [DP|Flurin]. Examples:
"Later today" => now.in(3.hours),
"Tomorrow morning" => now.tomorrow.change(:hour => 9),
"Tomorrow afternoon" => now.tomorrow.change(:hour => 14),
"In a couple of days" => now.tomorrow.tomorrow.change(:hour => 9),
"Next monday" => now.next_week.change(:hour => 9),
"In a month" => now.next_month.change(:hour => 9),
"In 6 months" => now.months_since(6).change(:hour => 9),
"In a year" => now.in(1.year).change(:hour => 9)
正直なところ、since
の使い方に関して6.months.since(user.created_at)
とかの例が出てくるかなと思ってましたが拍子抜けです。
非ネイティブだからこその無駄なこだわりかもしれません。
since/ago
について
プレフィクスを伴わないsince/ago
は、加算(減算)のシンタックスシュガーです。
どこかに起点を置いてそこからの経過を取得するという場合には有用なメソッドです。
ただ、やっぱりTime
拡張のsince/ago
は使いどころが見つかりません。
Duration#since/ago
は使い勝手がいいので使い方を間違わずに使っていきたいと考えます。
また、それぞれ after/from_now
before/until
というエイリアスを持っているので、文脈に合わせてメソッドを選びたいですね。
追記
@ktroutner さんの指摘から、since
の自然な使い方は難しそうです。
エイリアスのafter/before/from_now
、引数を伴わないago
をうまく使っていくのが良さそうです。
結局*_since
を使うか?
先述のように1.0当初から*_since
があり現在の使い方を想定されていたとはいえ、私はやはり可読性を損なう記述に関しては書き換えを推奨していきます。
6.months.from_now # 6ヶ月後 追記:6.month.after よりも自然 / thx @ktroutner さん
1.month.after(Time.new(2020,1,31)) # 2020/1/31の1ヶ月後: ちゃんと2020/2/29が作れる
Time.new(2020,1,31) + 1.month # 2020/1/31に1ヶ月足す: ちゃんと2020/2/29が作れる
# 追記:atがあることでやはり語感を損なうとのこと / thx @ktroutner さん
2.weeks.since(user.created_at) # ユーザの作成日から2週間: 無料期間とか作るときこんな形だとその間というのがわかりやすい
などが結果を損なわず、文意もわかりやすいものだと考えます。
逆に推奨しない書き方
Time.now.months_since(6) # 本稿の主題。months_sinceは語感からずれる
Time.now.months_ago(6) # sinceがagoに変わっただけで本質は同じ。
Time.now.since(6.month) # 同様にsinceにDurationを持ってきて加算するのは気持ち悪い。
Time.now.ago(6.month) # これもagoの語感と語順が気持ち悪い
Time.now.since(user.created_at) # 機能的にはこんなことができるけどもはや意味不明
-
念のために社内の英語ネイティブスピーカー(非エンジニア)にも確認してみたところ、同様の感覚で答えてくれました。 ↩