Skip to content

Commit

Permalink
Re-add 1.9 string interpolation syntax backport
Browse files Browse the repository at this point in the history
ActiveSupport relies on it. Should be moved to ActiveSupport instead.
  • Loading branch information
Sven Fuchs committed Nov 18, 2010
1 parent b893eb8 commit 674b59a
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 0 deletions.
96 changes: 96 additions & 0 deletions lib/i18n/core_ext/string/interpolate.rb
@@ -0,0 +1,96 @@
=begin
heavily based on Masao Mutoh's gettext String interpolation extension
http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
Copyright (C) 2005-2009 Masao Mutoh
You may redistribute it and/or modify it under the same license terms as Ruby.
=end

begin
raise ArgumentError if ("a %{x}" % {:x=>'b'}) != 'a b'
rescue ArgumentError
# KeyError is raised by String#% when the string contains a named placeholder
# that is not contained in the given arguments hash. Ruby 1.9 includes and
# raises this exception natively. We define it to mimic Ruby 1.9's behaviour
# in Ruby 1.8.x
class KeyError < IndexError
def initialize(message = nil)
super(message || "key not found")
end
end unless defined?(KeyError)

# Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError.
#
# String#% method which accept "named argument". The translator can know
# the meaning of the msgids using "named argument" instead of %s/%d style.
class String
# For older ruby versions, such as ruby-1.8.5
alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'}
alias :interpolate_without_ruby_19_syntax :% # :nodoc:

INTERPOLATION_PATTERN = Regexp.union(
/%\{(\w+)\}/, # matches placeholders like "%{foo}"
/%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
)

INTERPOLATION_PATTERN_WITH_ESCAPE = Regexp.union(
/%%/,
INTERPOLATION_PATTERN
)

# % uses self (i.e. the String) as a format specification and returns the
# result of applying it to the given arguments. In other words it interpolates
# the given arguments to the string according to the formats the string
# defines.
#
# There are three ways to use it:
#
# * Using a single argument or Array of arguments.
#
# This is the default behaviour of the String class. See Kernel#sprintf for
# more details about the format string.
#
# Example:
#
# "%d %s" % [1, "message"]
# # => "1 message"
#
# * Using a Hash as an argument and unformatted, named placeholders.
#
# When you pass a Hash as an argument and specify placeholders with %{foo}
# it will interpret the hash values as named arguments.
#
# Example:
#
# "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"}
# # => "Masao Mutoh"
#
# * Using a Hash as an argument and formatted, named placeholders.
#
# When you pass a Hash as an argument and specify placeholders with %<foo>d
# it will interpret the hash values as named arguments and format the value
# according to the formatting instruction appended to the closing >.
#
# Example:
#
# "%<integer>d, %<float>.1f" % { :integer => 10, :float => 43.4 }
# # => "10, 43.3"
def %(args)
if args.kind_of?(Hash)
dup.gsub(INTERPOLATION_PATTERN_WITH_ESCAPE) do |match|
if match == '%%'
'%'
else
key = ($1 || $2).to_sym
raise KeyError unless args.has_key?(key)
$3 ? sprintf("%#{$3}", args[key]) : args[key]
end
end
elsif self =~ INTERPOLATION_PATTERN
raise ArgumentError.new('one hash required')
else
result = gsub(/%([{<])/, '%%\1')
result.send :'interpolate_without_ruby_19_syntax', args
end
end
end
end
99 changes: 99 additions & 0 deletions test/core_ext/string/interpolate_test.rb
@@ -0,0 +1,99 @@
require 'test_helper'

# thanks to Masao's String extensions these should work the same in
# Ruby 1.8 (patched) and Ruby 1.9 (native)
# some tests taken from Masao's tests
# http://github.com/mutoh/gettext/blob/edbbe1fa8238fa12c7f26f2418403015f0270e47/test/test_string.rb

class I18nCoreExtStringInterpolationTest < Test::Unit::TestCase
test "String interpolates a single argument" do
assert_equal "Masao", "%s" % "Masao"
end

test "String interpolates an array argument" do
assert_equal "1 message", "%d %s" % [1, 'message']
end

test "String interpolates a hash argument w/ named placeholders" do
assert_equal "Masao Mutoh", "%{first} %{last}" % { :first => 'Masao', :last => 'Mutoh' }
end

test "String interpolates a hash argument w/ named placeholders (reverse order)" do
assert_equal "Mutoh, Masao", "%{last}, %{first}" % { :first => 'Masao', :last => 'Mutoh' }
end

test "String interpolates named placeholders with sprintf syntax" do
assert_equal "10, 43.4", "%<integer>d, %<float>.1f" % {:integer => 10, :float => 43.4}
end

test "String interpolates named placeholders with sprintf syntax, does not recurse" do
assert_equal "%<not_translated>s", "%{msg}" % { :msg => '%<not_translated>s', :not_translated => 'should not happen' }
end

test "String interpolation does not replace anything when no placeholders are given" do
assert_equal("aaa", "aaa" % {:num => 1})
assert_equal("bbb", "bbb" % [1])
end

test "String interpolation sprintf behaviour equals Ruby 1.9 behaviour" do
assert_equal("1", "%<num>d" % {:num => 1})
assert_equal("0b1", "%<num>#b" % {:num => 1})
assert_equal("foo", "%<msg>s" % {:msg => "foo"})
assert_equal("1.000000", "%<num>f" % {:num => 1.0})
assert_equal(" 1", "%<num>3.0f" % {:num => 1.0})
assert_equal("100.00", "%<num>2.2f" % {:num => 100.0})
assert_equal("0x64", "%<num>#x" % {:num => 100.0})
assert_raise(ArgumentError) { "%<num>,d" % {:num => 100} }
assert_raise(ArgumentError) { "%<num>/d" % {:num => 100} }
end

test "String interpolation old-style sprintf still works" do
assert_equal("foo 1.000000", "%s %f" % ["foo", 1.0])
end

test "String interpolation raises an ArgumentError when the string has extra placeholders (Array)" do
assert_raise(ArgumentError) do # Ruby 1.9 msg: "too few arguments"
"%s %s" % %w(Masao)
end
end

test "String interpolation raises a KeyError when the string has extra placeholders (Hash)" do
assert_raise(KeyError) do # Ruby 1.9 msg: "key not found"
"%{first} %{last}" % { :first => 'Masao' }
end
end

test "String interpolation does not raise when passed extra values (Array)" do
assert_nothing_raised do
assert_equal "Masao", "%s" % %w(Masao Mutoh)
end
end

test "String interpolation does not raise when passed extra values (Hash)" do
assert_nothing_raised do
assert_equal "Masao Mutoh", "%{first} %{last}" % { :first => 'Masao', :last => 'Mutoh', :salutation => 'Mr.' }
end
end

test "% acts as escape character in String interpolation" do
assert_equal "%{first}", "%%{first}" % { :first => 'Masao' }
assert_equal("% 1", "%% %<num>d" % {:num => 1.0})
assert_equal("%{num} %<num>d", "%%{num} %%<num>d" % {:num => 1})
end

test "% can be used in Ruby's own sprintf behavior" do
assert_equal "70%", "%d%%" % 70
assert_equal "70-100%", "%d-%d%%" % [70, 100]
assert_equal "+2.30%", "%+.2f%%" % 2.3
end

def test_sprintf_mix_unformatted_and_formatted_named_placeholders
assert_equal("foo 1.000000", "%{name} %<num>f" % {:name => "foo", :num => 1.0})
end

def test_string_interpolation_raises_an_argument_error_when_mixing_named_and_unnamed_placeholders
assert_raise(ArgumentError) { "%{name} %f" % [1.0] }
assert_raise(ArgumentError) { "%{name} %f" % [1.0, 2.0] }
end
end

0 comments on commit 674b59a

Please sign in to comment.