- Ruby / Rails関連
[Rails 5] ActiveSupport::NumberHelperとActionView::Helpers::NumberHelperの数値フォーマットメソッド
こんにちは、hachi8833です。
少々空いていましたが、久しぶりのActiveSupport探訪シリーズではNumberHelperを見てみます。
ActiveSupport::NumberHelper
クラス
ActiveSupport::NumberHelper
には以下の数値フォーマット変換メソッドがあります。
#number_to_currency
- 数値を通貨形式に変換。単位のほかにロケールで指定もできる。
例:number_to_currency(1234567890.506, locale: :fr)
# => "1 234 567 890,51 €" #number_to_delimited
- 数値を桁区切り表示に変換。ロケール指定や正規表現も使える。
例:number_to_delimited(12345678, delimiter: '.')
# => "12.345.678" #number_to_human
- 数値をmillionなどの単位に丸めて表示。有効桁数なども指定できる。
例:number_to_human(1234567, precision: 4, significant: false)
# => "1.2346 Million" #number_to_human_size
#number_to_human
と似ているが、ファイルサイズの単位表示に向いている。有効桁数なども指定できる。
例:number_to_human_size(1234567, precision: 2)
# => "1.2 MB"#number_to_percentage
- 数値をパーセント単位で表示。有効桁数なども指定できる。
例:number_to_percentage('98')
# => "98.000%" #number_to_phone
- 数値を電話番号に変換。国番号や内線番号、桁区切りも指定できる。有効桁数指定や正規表現も使える。
例:number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
# => "+1.123.555.1234 x 1343" #number_to_rounded
- 数値を丸める。有効桁数やロケールも指定できる。
例:number_to_rounded(389.32314, precision: 4, significant: true)
# => "389.3"
ActionView::Helpers::NumberHelper
とどう違うのか?
ActionViewにもActionView::Helpers::NumberHelper
というヘルパークラスがあります。実は先にこっちを知ってから「あれ?同じものがActiveSupportにあったような?」と気づいたのでした。
ActionView::Helpers::NumberHelper
の数値フォーマット変換メソッドですが、何だかActiveSupportのと微妙に違っています。
以下については両者共通のようです。
#number_to_currency
- 数値を通貨形式に変換。
#number_to_human
- 数値をmillionなどの単位に丸めて表示。
#number_to_human_size
#number_to_human
と似ているが、ファイルサイズの単位表示に向いている。#number_to_percentage
- 数値をパーセント単位で表示。
#number_to_phone
- 数値を電話番号に変換。
こちらには#number_to_delimited
と#number_to_rounded
はなく、代わりに以下があります。
#number_with_delimiter
- 数値を桁区切り表示に変換。ロケールや正規表現も使える。
例:number_with_delimiter(98765432.98, delimiter: " ", separator: ",")
# => 98 765 432,98 #number_with_precision
- 数値を丸める。有効桁数やロケールも指定できる。
例:number_with_precision(13, precision: 5, significant: true)
# => 13.000
ActiveSupport::NumberHelper
とActionView::Helpers::NumberHelper
のコード
こうなってくると、両者のコードは実は共通ではないかという予測が立ちます。
active_support/number_helper.rb
まずはactive_support/number_helper.rbから。コメントは取っ払ってあります。
# active_support/number_helper.rb
module ActiveSupport
module NumberHelper
extend ActiveSupport::Autoload
eager_autoload do
autoload :NumberConverter
autoload :RoundingHelper
autoload :NumberToRoundedConverter
autoload :NumberToDelimitedConverter
autoload :NumberToHumanConverter
autoload :NumberToHumanSizeConverter
autoload :NumberToPhoneConverter
autoload :NumberToCurrencyConverter
autoload :NumberToPercentageConverter
end
extend self
def number_to_phone(number, options = {})
NumberToPhoneConverter.convert(number, options)
end
def number_to_currency(number, options = {})
NumberToCurrencyConverter.convert(number, options)
end
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
def number_to_delimited(number, options = {})
NumberToDelimitedConverter.convert(number, options)
end
def number_to_rounded(number, options = {})
NumberToRoundedConverter.convert(number, options)
end
def number_to_human_size(number, options = {})
NumberToHumanSizeConverter.convert(number, options)
end
def number_to_human(number, options = {})
NumberToHumanConverter.convert(number, options)
end
end
end
内容はというと、NumberToPhoneConverter
などにそのまま投げているシンプルなつくりです。投げる相手は activesupport/lib/active_support/number_helper/の下にあります。
せっかくなのでその中からnumber_to_currency_converter.rbを開いてみました。
# active_support/number_helper/number_to_currency_converter.rb
require "active_support/core_ext/numeric/inquiry"
module ActiveSupport
module NumberHelper
class NumberToCurrencyConverter < NumberConverter # :nodoc:
self.namespace = :currency
def convert
number = self.number.to_s.strip
format = options[:format]
if number.to_f.negative?
format = options[:negative_format]
number = absolute_value(number)
end
rounded_number = NumberToRoundedConverter.convert(number, options)
format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, options[:unit])
end
private
def absolute_value(number)
number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, "")
end
def options
@options ||= begin
defaults = default_format_options.merge(i18n_opts)
# フォーマットオプションが渡された場合はマイナス表示をオーバーライド
defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
defaults.merge!(opts)
end
end
def i18n_opts
# 固有のマイナス記号がない場合は国際的なマイナス記号にする
i18n = i18n_format_options
i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
i18n
end
end
end
i18nオプションのうち、マイナス記号の処理はここで行われています。そしてそれ以外のi18nオプションの処理は、同じディレクトリのnumber_converter.rbで行っていました。以下は該当箇所のみピックアップしています。
# active_support/number_helper/number_converter.rb
...
private
...
def i18n_format_options
locale = opts[:locale]
options = I18n.translate(:'number.format', locale: locale, default: {}).dup
if namespace
options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
end
options
end
def translate_number_value_with_default(key, i18n_options = {})
I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
end
def translate_in_locale(key, i18n_options = {})
translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options))
end
def default_value(key)
key.split(".").reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
end
def valid_float?
Float(number)
rescue ArgumentError, TypeError
false
end
end
...
最終的に、ActiveSupportがインクルードしているI18n gemにi18n処理を投げています。ロケールなどの処理もi18nで行われています。長い旅路ですね。
action_view/helpers/number_helper.rb
続いてaction_view/helpers/number_helper.rbです。
# action_view/helpers/number_helper.rb
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/string/output_safety"
require "active_support/number_helper"
module ActionView
module Helpers #:nodoc:
module NumberHelper
class InvalidNumberError < StandardError
attr_accessor :number
def initialize(number)
@number = number
end
end
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
parse_float(number, true) if options.delete(:raise)
ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
end
def number_to_currency(number, options = {})
delegate_number_helper_method(:number_to_currency, number, options)
end
def number_to_percentage(number, options = {})
delegate_number_helper_method(:number_to_percentage, number, options)
end
def number_with_delimiter(number, options = {})
delegate_number_helper_method(:number_to_delimited, number, options)
end
def number_with_precision(number, options = {})
delegate_number_helper_method(:number_to_rounded, number, options)
end
def number_to_human_size(number, options = {})
delegate_number_helper_method(:number_to_human_size, number, options)
end
def number_to_human(number, options = {})
delegate_number_helper_method(:number_to_human, number, options)
end
private
def delegate_number_helper_method(method, number, options)
return unless number
options = escape_unsafe_options(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.public_send(method, number, options)
}
end
def escape_unsafe_options(options)
options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
options
end
def escape_units(units)
Hash[units.map do |k, v|
[k, ERB::Util.html_escape(v)]
end]
end
def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
valid_float = valid_float?(number)
raise InvalidNumberError, number if raise_on_invalid && !valid_float
formatted_number = yield
if valid_float || number.html_safe?
formatted_number.html_safe
else
formatted_number
end
end
def valid_float?(number)
!parse_float(number, false).nil?
end
def parse_float(number, raise_error)
Float(number)
rescue ArgumentError, TypeError
raise InvalidNumberError, number if raise_error
end
end
end
end
予想どおり、require "active_support/number_helper"
でActiveSupport::NumberHelperを読み込んでいます。
delegate_number_helper_method
というprivateメソッドがあり、ここで以下の処理が追加されています。
- 数値チェック:
return unless number
- ビュー向けにエスケープ:
#escape_unsafe_options
- ビュー向けに単位をエスケープ:
#escape_units
#html_safe
にしてエスケープの繰り返しを避ける:#wrap_with_output_safety_handling
- floatが有効かどうかチェック:
#parse_float
#html_safe
「については以下の記事をご覧ください。
まとめ
ActiveSupport::NumberHelper
は、WebやHTMLに限定されない汎用性の高い変換を行います。I18nライブラリによるi18n対応もここで行っています。ActionView::Helpers::NumberHelper
は上のコードを基にしながら、ビュー向けに適切なエスケープ処理を追加しています。
したがって、RailsのビューではActionViewの数値ヘルパーを使うべきです。お間違いのないよう。
関連記事
[Rails5] Active Support Core Extensionsのマルチバイト系メソッド: String#mb_charsとis_utf8?