2008-09-21 11:21:30 -04:00
|
|
|
|
# encoding: utf-8
|
2009-11-09 01:31:54 -05:00
|
|
|
|
require 'active_support/core_ext/string/access'
|
2009-05-18 18:01:13 -04:00
|
|
|
|
require 'active_support/core_ext/string/behavior'
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
|
|
|
|
module ActiveSupport #:nodoc:
|
|
|
|
|
module Multibyte #:nodoc:
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Chars enables you to work transparently with UTF-8 encoding in the Ruby String class without having extensive
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an
|
|
|
|
|
# encoding safe manner. All the normal String methods are also implemented on the proxy.
|
|
|
|
|
#
|
|
|
|
|
# String methods are proxied through the Chars object, and can be accessed through the +mb_chars+ method. Methods
|
|
|
|
|
# which would normally return a String object now return a Chars object so methods can be chained.
|
|
|
|
|
#
|
2008-09-21 11:23:37 -04:00
|
|
|
|
# "The Perfect String ".mb_chars.downcase.strip.normalize #=> "the perfect string"
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made.
|
|
|
|
|
# If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them.
|
|
|
|
|
#
|
2008-09-21 11:23:37 -04:00
|
|
|
|
# bad.explicit_checking_method "T".mb_chars.downcase.to_s
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# The default Chars implementation assumes that the encoding of the string is UTF-8, if you want to handle different
|
2010-04-07 15:21:41 -04:00
|
|
|
|
# encodings you can write your own multibyte string handler and configure it through
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# ActiveSupport::Multibyte.proxy_class.
|
|
|
|
|
#
|
|
|
|
|
# class CharsForUTF32
|
|
|
|
|
# def size
|
|
|
|
|
# @wrapped_string.size / 4
|
|
|
|
|
# end
|
|
|
|
|
#
|
|
|
|
|
# def self.accepts?(string)
|
|
|
|
|
# string.length % 4 == 0
|
|
|
|
|
# end
|
|
|
|
|
# end
|
|
|
|
|
#
|
|
|
|
|
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
|
|
|
|
|
class Chars
|
|
|
|
|
attr_reader :wrapped_string
|
|
|
|
|
alias to_s wrapped_string
|
|
|
|
|
alias to_str wrapped_string
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
if RUBY_VERSION >= "1.9"
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Creates a new Chars instance by wrapping _string_.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
def initialize(string)
|
|
|
|
|
@wrapped_string = string
|
|
|
|
|
@wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
|
|
|
|
|
end
|
|
|
|
|
else
|
2008-09-21 11:28:05 -04:00
|
|
|
|
def initialize(string) #:nodoc:
|
2008-09-21 11:21:30 -04:00
|
|
|
|
@wrapped_string = string
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Forward all undefined methods to the wrapped string.
|
|
|
|
|
def method_missing(method, *args, &block)
|
|
|
|
|
if method.to_s =~ /!$/
|
|
|
|
|
@wrapped_string.__send__(method, *args, &block)
|
|
|
|
|
self
|
|
|
|
|
else
|
2008-09-21 11:29:22 -04:00
|
|
|
|
result = @wrapped_string.__send__(method, *args, &block)
|
|
|
|
|
result.kind_of?(String) ? chars(result) : result
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
end
|
2008-09-21 11:29:22 -04:00
|
|
|
|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# Returns +true+ if _obj_ responds to the given method. Private methods are included in the search
|
|
|
|
|
# only if the optional second parameter evaluates to +true+.
|
|
|
|
|
def respond_to?(method, include_private=false)
|
|
|
|
|
super || @wrapped_string.respond_to?(method, include_private) || false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Enable more predictable duck-typing on String-like classes. See Object#acts_like?.
|
|
|
|
|
def acts_like_string?
|
|
|
|
|
true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise.
|
|
|
|
|
def self.consumes?(string)
|
|
|
|
|
# Unpack is a little bit faster than regular expressions.
|
|
|
|
|
string.unpack('U*')
|
|
|
|
|
true
|
|
|
|
|
rescue ArgumentError
|
2008-05-31 17:54:17 -04:00
|
|
|
|
false
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
2008-05-31 17:54:17 -04:00
|
|
|
|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
include Comparable
|
2008-09-11 16:38:20 -04:00
|
|
|
|
|
2010-06-13 19:58:36 -04:00
|
|
|
|
# Returns <tt>-1</tt>, <tt>0</tt> or <tt>+1</tt> depending on whether the Chars object is to be sorted before,
|
|
|
|
|
# equal or after the object on the right side of the operation. It accepts any object that implements +to_s+.
|
|
|
|
|
# See <tt>String#<=></tt> for more details.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'é'.mb_chars <=> 'ü'.mb_chars #=> -1
|
|
|
|
|
def <=>(other)
|
|
|
|
|
@wrapped_string <=> other.to_s
|
|
|
|
|
end
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
if RUBY_VERSION < "1.9"
|
|
|
|
|
# Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns
|
|
|
|
|
# +false+ otherwise.
|
|
|
|
|
def self.wants?(string)
|
|
|
|
|
$KCODE == 'UTF8' && consumes?(string)
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Returns a new Chars object containing the _other_ object concatenated to the string.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# ('Café'.mb_chars + ' périferôl').to_s #=> "Café périferôl"
|
|
|
|
|
def +(other)
|
2010-05-18 15:19:53 -04:00
|
|
|
|
chars(@wrapped_string + other)
|
2010-05-10 09:46:37 -04:00
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café périferôl'.mb_chars =~ /ô/ #=> 12
|
|
|
|
|
def =~(other)
|
|
|
|
|
translate_offset(@wrapped_string =~ other)
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Inserts the passed string at specified codepoint offsets.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café'.mb_chars.insert(4, ' périferôl').to_s #=> "Café périferôl"
|
|
|
|
|
def insert(offset, fragment)
|
|
|
|
|
unpacked = Unicode.u_unpack(@wrapped_string)
|
|
|
|
|
unless offset > unpacked.length
|
|
|
|
|
@wrapped_string.replace(
|
|
|
|
|
Unicode.u_unpack(@wrapped_string).insert(offset, *Unicode.u_unpack(fragment)).pack('U*')
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
raise IndexError, "index #{offset} out of string"
|
|
|
|
|
end
|
|
|
|
|
self
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Returns +true+ if contained string contains _other_. Returns +false+ otherwise.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café'.mb_chars.include?('é') #=> true
|
|
|
|
|
def include?(other)
|
|
|
|
|
# We have to redefine this method because Enumerable defines it.
|
|
|
|
|
@wrapped_string.include?(other)
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café périferôl'.mb_chars.index('ô') #=> 12
|
|
|
|
|
# 'Café périferôl'.mb_chars.index(/\w/u) #=> 0
|
|
|
|
|
def index(needle, offset=0)
|
|
|
|
|
wrapped_offset = first(offset).wrapped_string.length
|
|
|
|
|
index = @wrapped_string.index(needle, wrapped_offset)
|
|
|
|
|
index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Returns the position _needle_ in the string, counting in
|
|
|
|
|
# codepoints, searching backward from _offset_ or the end of the
|
|
|
|
|
# string. Returns +nil+ if _needle_ isn't found.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café périferôl'.mb_chars.rindex('é') #=> 6
|
|
|
|
|
# 'Café périferôl'.mb_chars.rindex(/\w/u) #=> 13
|
|
|
|
|
def rindex(needle, offset=nil)
|
|
|
|
|
offset ||= length
|
|
|
|
|
wrapped_offset = first(offset).wrapped_string.length
|
|
|
|
|
index = @wrapped_string.rindex(needle, wrapped_offset)
|
|
|
|
|
index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Returns the number of codepoints in the string
|
|
|
|
|
def size
|
|
|
|
|
Unicode.u_unpack(@wrapped_string).size
|
|
|
|
|
end
|
|
|
|
|
alias_method :length, :size
|
|
|
|
|
|
|
|
|
|
# Strips entire range of Unicode whitespace from the right of the string.
|
|
|
|
|
def rstrip
|
|
|
|
|
chars(@wrapped_string.gsub(Unicode::TRAILERS_PAT, ''))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Strips entire range of Unicode whitespace from the left of the string.
|
|
|
|
|
def lstrip
|
|
|
|
|
chars(@wrapped_string.gsub(Unicode::LEADERS_PAT, ''))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Strips entire range of Unicode whitespace from the right and left of the string.
|
|
|
|
|
def strip
|
|
|
|
|
rstrip.lstrip
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Returns the codepoint of the first character in the string.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'こんにちは'.mb_chars.ord #=> 12371
|
|
|
|
|
def ord
|
|
|
|
|
Unicode.u_unpack(@wrapped_string)[0]
|
|
|
|
|
end
|
|
|
|
|
|
2010-06-25 22:55:25 -04:00
|
|
|
|
# Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.rjust(8).to_s
|
|
|
|
|
# #=> " ¾ cup"
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace
|
|
|
|
|
# #=> " ¾ cup"
|
|
|
|
|
def rjust(integer, padstr=' ')
|
|
|
|
|
justify(integer, :right, padstr)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.rjust(8).to_s
|
|
|
|
|
# #=> "¾ cup "
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.rjust(8, " ").to_s # Use non-breaking whitespace
|
|
|
|
|
# #=> "¾ cup "
|
|
|
|
|
def ljust(integer, padstr=' ')
|
|
|
|
|
justify(integer, :left, padstr)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.center(8).to_s
|
|
|
|
|
# #=> " ¾ cup "
|
|
|
|
|
#
|
|
|
|
|
# "¾ cup".mb_chars.center(8, " ").to_s # Use non-breaking whitespace
|
|
|
|
|
# #=> " ¾ cup "
|
|
|
|
|
def center(integer, padstr=' ')
|
|
|
|
|
justify(integer, :center, padstr)
|
|
|
|
|
end
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
else
|
|
|
|
|
def =~(other)
|
|
|
|
|
@wrapped_string =~ other
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# Works just like <tt>String#split</tt>, with the exception that the items in the resulting list are Chars
|
|
|
|
|
# instances instead of String. This makes chaining methods easier.
|
2009-08-09 21:57:25 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } #=> ["CAF", " P", "RIFERÔL"]
|
|
|
|
|
def split(*args)
|
|
|
|
|
@wrapped_string.split(*args).map { |i| i.mb_chars }
|
2009-08-09 21:57:25 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Like <tt>String#[]=</tt>, except instead of byte offsets you specify character offsets.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
#
|
|
|
|
|
# s = "Müller"
|
2008-09-21 11:23:37 -04:00
|
|
|
|
# s.mb_chars[2] = "e" # Replace character with offset 2
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# s
|
|
|
|
|
# #=> "Müeler"
|
|
|
|
|
#
|
|
|
|
|
# s = "Müller"
|
2008-09-21 11:23:37 -04:00
|
|
|
|
# s.mb_chars[1, 2] = "ö" # Replace 2 characters at character offset 1
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# s
|
|
|
|
|
# #=> "Möler"
|
|
|
|
|
def []=(*args)
|
|
|
|
|
replace_by = args.pop
|
|
|
|
|
# Indexed replace with regular expressions already works
|
|
|
|
|
if args.first.is_a?(Regexp)
|
|
|
|
|
@wrapped_string[*args] = replace_by
|
|
|
|
|
else
|
2010-05-10 09:46:37 -04:00
|
|
|
|
result = Unicode.u_unpack(@wrapped_string)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
if args[0].is_a?(Fixnum)
|
|
|
|
|
raise IndexError, "index #{args[0]} out of string" if args[0] >= result.length
|
|
|
|
|
min = args[0]
|
|
|
|
|
max = args[1].nil? ? min : (min + args[1] - 1)
|
|
|
|
|
range = Range.new(min, max)
|
|
|
|
|
replace_by = [replace_by].pack('U') if replace_by.is_a?(Fixnum)
|
|
|
|
|
elsif args.first.is_a?(Range)
|
|
|
|
|
raise RangeError, "#{args[0]} out of range" if args[0].min >= result.length
|
|
|
|
|
range = args[0]
|
2006-10-03 19:45:32 -04:00
|
|
|
|
else
|
2008-09-21 11:21:30 -04:00
|
|
|
|
needle = args[0].to_s
|
|
|
|
|
min = index(needle)
|
2010-05-10 09:46:37 -04:00
|
|
|
|
max = min + Unicode.u_unpack(needle).length - 1
|
2008-09-21 11:21:30 -04:00
|
|
|
|
range = Range.new(min, max)
|
2006-10-03 19:45:32 -04:00
|
|
|
|
end
|
2010-05-10 09:46:37 -04:00
|
|
|
|
result[range] = Unicode.u_unpack(replace_by)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
@wrapped_string.replace(result.pack('U*'))
|
2006-10-03 19:45:32 -04:00
|
|
|
|
end
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Reverses all characters in the string.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'Café'.mb_chars.reverse.to_s #=> 'éfaC'
|
|
|
|
|
def reverse
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.g_unpack(@wrapped_string).reverse.flatten.pack('U*'))
|
2006-10-03 19:45:32 -04:00
|
|
|
|
end
|
2009-08-31 15:16:22 -04:00
|
|
|
|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that
|
|
|
|
|
# character.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
2008-10-21 13:33:40 -04:00
|
|
|
|
# 'こんにちは'.mb_chars.slice(2..3).to_s #=> "にち"
|
2008-09-21 11:21:30 -04:00
|
|
|
|
def slice(*args)
|
|
|
|
|
if args.size > 2
|
|
|
|
|
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native
|
|
|
|
|
elsif (args.size == 2 && !(args.first.is_a?(Numeric) || args.first.is_a?(Regexp)))
|
|
|
|
|
raise TypeError, "cannot convert #{args.first.class} into Integer" # Do as if we were native
|
|
|
|
|
elsif (args.size == 2 && !args[1].is_a?(Numeric))
|
|
|
|
|
raise TypeError, "cannot convert #{args[1].class} into Integer" # Do as if we were native
|
|
|
|
|
elsif args[0].kind_of? Range
|
2010-05-10 09:46:37 -04:00
|
|
|
|
cps = Unicode.u_unpack(@wrapped_string).slice(*args)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
result = cps.nil? ? nil : cps.pack('U*')
|
|
|
|
|
elsif args[0].kind_of? Regexp
|
|
|
|
|
result = @wrapped_string.slice(*args)
|
|
|
|
|
elsif args.size == 1 && args[0].kind_of?(Numeric)
|
2010-05-10 09:46:37 -04:00
|
|
|
|
character = Unicode.u_unpack(@wrapped_string)[args[0]]
|
2008-09-21 11:21:30 -04:00
|
|
|
|
result = character.nil? ? nil : [character].pack('U')
|
|
|
|
|
else
|
2010-05-10 09:46:37 -04:00
|
|
|
|
result = Unicode.u_unpack(@wrapped_string).slice(*args).pack('U*')
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
result.nil? ? nil : chars(result)
|
|
|
|
|
end
|
|
|
|
|
alias_method :[], :slice
|
|
|
|
|
|
2008-12-30 08:16:15 -05:00
|
|
|
|
# Like <tt>String#slice!</tt>, except instead of byte offsets you specify character offsets.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# s = 'こんにちは'
|
|
|
|
|
# s.mb_chars.slice!(2..3).to_s #=> "にち"
|
|
|
|
|
# s #=> "こんは"
|
|
|
|
|
def slice!(*args)
|
|
|
|
|
slice = self[*args]
|
|
|
|
|
self[*args] = ''
|
|
|
|
|
slice
|
|
|
|
|
end
|
|
|
|
|
|
2009-11-01 11:18:27 -05:00
|
|
|
|
# Limit the byte size of the string to a number of bytes without breaking characters. Usable
|
|
|
|
|
# when the storage for a string is limited for some reason.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# s = 'こんにちは'
|
|
|
|
|
# s.mb_chars.limit(7) #=> "こに"
|
|
|
|
|
def limit(limit)
|
|
|
|
|
slice(0...translate_offset(limit))
|
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Convert characters in the string to uppercase.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
2010-01-08 07:29:24 -05:00
|
|
|
|
# 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s #=> "LAURENT, OÙ SONT LES TESTS ?"
|
2008-09-21 11:21:30 -04:00
|
|
|
|
def upcase
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.apply_mapping @wrapped_string, :uppercase_mapping)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Convert characters in the string to lowercase.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s #=> "věda a výzkum"
|
|
|
|
|
def downcase
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.apply_mapping @wrapped_string, :lowercase_mapping)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Converts the first character to uppercase and the remainder to lowercase.
|
2008-09-21 11:21:30 -04:00
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'über'.mb_chars.capitalize.to_s #=> "Über"
|
|
|
|
|
def capitalize
|
2008-09-21 11:25:36 -04:00
|
|
|
|
(slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2010-05-28 15:36:22 -04:00
|
|
|
|
# Capitalizes the first letter of every word, when possible.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró"
|
|
|
|
|
# "日本語".mb_chars.titleize # => "日本語"
|
|
|
|
|
def titleize
|
|
|
|
|
chars(downcase.to_s.gsub(/\b('?[\S])/u) { Unicode.apply_mapping $1, :uppercase_mapping })
|
|
|
|
|
end
|
|
|
|
|
alias_method :titlecase, :titleize
|
|
|
|
|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
# Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
|
|
|
|
|
# passing strings to databases and validations.
|
|
|
|
|
#
|
|
|
|
|
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
|
|
|
|
|
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
|
2010-05-10 09:46:37 -04:00
|
|
|
|
# ActiveSupport::Multibyte::Unicode.default_normalization_form
|
|
|
|
|
def normalize(form = nil)
|
|
|
|
|
chars(Unicode.normalize(@wrapped_string, form))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Performs canonical decomposition on all the characters.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'é'.length #=> 2
|
|
|
|
|
# 'é'.mb_chars.decompose.to_s.length #=> 3
|
|
|
|
|
def decompose
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.decompose_codepoints(:canonical, Unicode.u_unpack(@wrapped_string)).pack('U*'))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Performs composition on all the characters.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'é'.length #=> 3
|
|
|
|
|
# 'é'.mb_chars.compose.to_s.length #=> 2
|
|
|
|
|
def compose
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.compose_codepoints(Unicode.u_unpack(@wrapped_string)).pack('U*'))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Returns the number of grapheme clusters in the string.
|
|
|
|
|
#
|
|
|
|
|
# Example:
|
|
|
|
|
# 'क्षि'.mb_chars.length #=> 4
|
|
|
|
|
# 'क्षि'.mb_chars.g_length #=> 3
|
|
|
|
|
def g_length
|
2010-05-10 09:46:37 -04:00
|
|
|
|
Unicode.g_unpack(@wrapped_string).length
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string.
|
2010-04-07 15:21:41 -04:00
|
|
|
|
#
|
|
|
|
|
# Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1.
|
|
|
|
|
def tidy_bytes(force = false)
|
2010-05-10 09:46:37 -04:00
|
|
|
|
chars(Unicode.tidy_bytes(@wrapped_string, force))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-12-30 08:16:15 -05:00
|
|
|
|
%w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
define_method("#{method}!") do |*args|
|
|
|
|
|
unless args.nil?
|
|
|
|
|
@wrapped_string = send(method, *args).to_s
|
|
|
|
|
else
|
|
|
|
|
@wrapped_string = send(method).to_s
|
|
|
|
|
end
|
|
|
|
|
self
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
protected
|
2010-04-07 15:21:41 -04:00
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
def translate_offset(byte_offset) #:nodoc:
|
2008-09-21 11:21:30 -04:00
|
|
|
|
return nil if byte_offset.nil?
|
|
|
|
|
return 0 if @wrapped_string == ''
|
2010-04-07 15:21:41 -04:00
|
|
|
|
|
2009-11-02 15:34:07 -05:00
|
|
|
|
if @wrapped_string.respond_to?(:force_encoding)
|
|
|
|
|
@wrapped_string = @wrapped_string.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
|
|
|
end
|
2010-04-07 15:21:41 -04:00
|
|
|
|
|
2008-09-21 11:21:30 -04:00
|
|
|
|
begin
|
2009-11-01 11:18:27 -05:00
|
|
|
|
@wrapped_string[0...byte_offset].unpack('U*').length
|
|
|
|
|
rescue ArgumentError => e
|
|
|
|
|
byte_offset -= 1
|
|
|
|
|
retry
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
def justify(integer, way, padstr=' ') #:nodoc:
|
2008-09-21 11:21:30 -04:00
|
|
|
|
raise ArgumentError, "zero width padding" if padstr.length == 0
|
|
|
|
|
padsize = integer - size
|
|
|
|
|
padsize = padsize > 0 ? padsize : 0
|
|
|
|
|
case way
|
|
|
|
|
when :right
|
2010-05-10 09:46:37 -04:00
|
|
|
|
result = @wrapped_string.dup.insert(0, padding(padsize, padstr))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
when :left
|
2010-05-10 09:46:37 -04:00
|
|
|
|
result = @wrapped_string.dup.insert(-1, padding(padsize, padstr))
|
2008-09-21 11:21:30 -04:00
|
|
|
|
when :center
|
2010-05-10 09:46:37 -04:00
|
|
|
|
lpad = padding((padsize / 2.0).floor, padstr)
|
|
|
|
|
rpad = padding((padsize / 2.0).ceil, padstr)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
result = @wrapped_string.dup.insert(0, lpad).insert(-1, rpad)
|
|
|
|
|
end
|
|
|
|
|
chars(result)
|
|
|
|
|
end
|
|
|
|
|
|
2010-05-10 09:46:37 -04:00
|
|
|
|
def padding(padsize, padstr=' ') #:nodoc:
|
|
|
|
|
if padsize != 0
|
|
|
|
|
chars(padstr * ((padsize / Unicode.u_unpack(padstr).size) + 1)).slice(0, padsize)
|
|
|
|
|
else
|
|
|
|
|
''
|
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
|
def chars(string) #:nodoc:
|
|
|
|
|
self.class.new(string)
|
2008-09-21 11:21:30 -04:00
|
|
|
|
end
|
|
|
|
|
end
|
2006-10-03 19:45:32 -04:00
|
|
|
|
end
|
2008-10-21 13:33:40 -04:00
|
|
|
|
end
|