🛠️

最近の ActiveSupport (7.1.1) の便利機能

2023/11/09に公開

フロントエンドの荒波から生還したところ、しばらく見てなかった ActiveSupport がまた便利になっていたので、気づいた点を簡単にまとめた。昔からあったけど知らなかっただけなのもたぶんある。

EnvironmentInquirer#local?

ActiveSupport::EnvironmentInquirer.new("development").local?  # => true

あっちこっちで if Rails.env.development? || Rails.env.test? を書いている自分向け。if Rails.env.local? だけで済むようになる。

BroadcastLogger

logger = ActiveSupport::BroadcastLogger.new
logger.broadcast_to(Logger.new("a.log"))
logger.broadcast_to(Logger.new("b.log"))
logger.info("foo")

一箇所から複数の logger に投げる。

Object#with

ブロック内で一時的にメンバを変更する。

class User
  attr_accessor :name
end
user = User.new
user.name = "alice"
user.with(name: "bob") do
  user.name  # => "bob"
end
user.name    # => "alice"

public なメソッド name= に反応できないといけない。

Pathname#existence

ファイルが存在しなければ nil を返すので、

file = Pathname("file.txt")
if file.exist?
  file.delete
end

は、

Pathname("file.txt").existence&.delete

と書ける。

Pathname("file.txt").delete rescue nil

と書くのに抵抗がある場合の代替に使いたい。

Pathname#blank?

Pathname(" ").blank?  # => false

ファイル名には空白も含まれるのだから空白だけのファイル名は空ではないということのようだ。

Range#include?

(1..4).include?(2..3)  # => true

引数が Range の場合に賢くなる。

数値の範囲の意味合いを持つ cover? なら元々 true なので include? ではなく cover? を使えばいいんじゃないかという気はする。

Regexp#multiline?

//m.multiline?  # => true

m オプションをつけていたかすぐわかる。

ActiveSupport がない場合は、

//m.options.allbits?(Regexp::MULTILINE)  # => true

と書く。

SecureRandom.base36

["0", "O", "I", "l"] を除いたランダム文字列を作る。

SecureRandom.base36  # => "zw6e66f3b6xkyt5d"
SecureRandom.base58  # => "WH7QUHj9cc7BS4xG"

base58 のほうは大文字を含む。

日本人が ツ と シ や ソ と ン を見分けられるように英語圏の人たちは 0 と O や I と l を平然と見分けられるのだと思っていたが、そうでもないらしい。

String#upcase_first

"aBc".upcase_first    # => "ABc"
"ABc".downcase_first  # => "aBc"

capitalize と似ているけど変更するのは先頭の文字のみ。

ERB::Util.html_escape_once

ERB::Util.html_escape_once("&")      # => "&"
ERB::Util.html_escape_once("&")  # => "&"

何度呼んでも & がエスケープされ続けない。

delegate が速くなっていた

class C1
  def self.x = nil
  delegate :x, to: :class
end

class C2
  delegate :x, to: :class
  def self.x = nil
end

c1 = C1.new
c2 = C2.new
"%.2f ms" % Benchmark.ms { 1000000.times { c1.x } }  # => "84.50 ms"
"%.2f ms" % Benchmark.ms { 1000000.times { c2.x } }  # => "120.13 ms"

引数がないかつ delegate した時点でメソッドを見つけられる場合は速くなる。

TimeWithZone#next_day?

Time.now.next_day?  # => false
Time.now.prev_day?  # => false

tomorrow? と yesterday? のエイリアス。英語になると明日と昨日がわからなくなる自分向け。

ActiveSupport::Duration#inspect

ActiveSupport::Duration.build(10000)  # => 2 hours, 46 minutes, and 40 seconds

さくっと人間向け表記に変換する。

ActiveSupport::Duration#parts

ActiveSupport::Duration.build(10000).parts  # => {:hours=>2, :minutes=>46, :seconds=>40}

自力で表記したいとき用。

expires_in をブロック内で変更できるようになっていた

cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
cache.fetch("foo") do |name, options|
  name                # => "foo"
  options.expires_in  # => 5 minutes
  options.expires_in = 1.minutes
  true
end

キャッシュ対象によって expires_in が動的に変わる場合にブロック内で最初に1回設定するだけでよくなる。

Enumerable#maximum

c = Data.define(:foo)
a = [c.new(5), c.new(6)]

としたとき

a.collect(&:foo).max  # => 6

が、

a.maximum(:foo)  # => 6

と書ける。minimum もある。

Enumerable#index_by

c = Data.define(:foo)
a = [c.new(5), c.new(6)]

としたとき

a.inject({}) { |a, e| a.merge(e.foo => e) }  # => {5=>#<data foo=5>, 6=>#<data foo=6>}

が、

a.index_by(&:foo)  # => {5=>#<data foo=5>, 6=>#<data foo=6>}

と書ける。

Enumerable#index_with

[:a, :b].inject({}) { |a, e| a.merge(e => 1) }       # => {:a=>1, :b=>1}
[:a, :b].inject({}) { |a, e| a.merge(e => e.next) }  # => {:a=>:b, :b=>:c}

が、

[:a, :b].index_with(1)       # => {:a=>1, :b=>1}
[:a, :b].index_with(&:next)  # => {:a=>:b, :b=>:c}

と書ける。

Enumerable#sole

[:a].sole                # => :a
[:a, :b].sole rescue $!  # => #<Enumerable::SoleItemExpectedError: multiple items found>

1つしかないところから1つ参照するのを保証する。

Object#instance_values

class C
  attr_accessor :a, :b
end
c = C.new
c.a = 1
c.b = 2
c.instance_values  # => {"a"=>1, "b"=>2}

ActiveRecord の attributes のような機能。

Object#presence_in

1.presence_in([1, 2])  # => 1
1.presence_in([2, 3])  # => nil

in? なら self で偽なら nil を返す。presence_in? ではないので注意。

応用すると x が 1 か 2 でなければ 1 としたいときなど簡単に書ける。

x = 3
x = x.presence_in([1, 2]) || 1
x  # => 1

Discussion