core_ext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/core_ext/array/access.rb +76 -0
  4. data/lib/core_ext/array/conversions.rb +211 -0
  5. data/lib/core_ext/array/extract_options.rb +29 -0
  6. data/lib/core_ext/array/grouping.rb +116 -0
  7. data/lib/core_ext/array/inquiry.rb +17 -0
  8. data/lib/core_ext/array/prepend_and_append.rb +7 -0
  9. data/lib/core_ext/array/wrap.rb +46 -0
  10. data/lib/core_ext/array.rb +7 -0
  11. data/lib/core_ext/array_inquirer.rb +44 -0
  12. data/lib/core_ext/benchmark.rb +14 -0
  13. data/lib/core_ext/benchmarkable.rb +49 -0
  14. data/lib/core_ext/big_decimal/conversions.rb +14 -0
  15. data/lib/core_ext/big_decimal.rb +1 -0
  16. data/lib/core_ext/builder.rb +6 -0
  17. data/lib/core_ext/callbacks.rb +770 -0
  18. data/lib/core_ext/class/attribute.rb +128 -0
  19. data/lib/core_ext/class/attribute_accessors.rb +4 -0
  20. data/lib/core_ext/class/subclasses.rb +42 -0
  21. data/lib/core_ext/class.rb +2 -0
  22. data/lib/core_ext/concern.rb +142 -0
  23. data/lib/core_ext/configurable.rb +148 -0
  24. data/lib/core_ext/date/acts_like.rb +8 -0
  25. data/lib/core_ext/date/blank.rb +12 -0
  26. data/lib/core_ext/date/calculations.rb +143 -0
  27. data/lib/core_ext/date/conversions.rb +93 -0
  28. data/lib/core_ext/date/zones.rb +6 -0
  29. data/lib/core_ext/date.rb +5 -0
  30. data/lib/core_ext/date_and_time/calculations.rb +328 -0
  31. data/lib/core_ext/date_and_time/zones.rb +40 -0
  32. data/lib/core_ext/date_time/acts_like.rb +14 -0
  33. data/lib/core_ext/date_time/blank.rb +12 -0
  34. data/lib/core_ext/date_time/calculations.rb +177 -0
  35. data/lib/core_ext/date_time/conversions.rb +104 -0
  36. data/lib/core_ext/date_time/zones.rb +6 -0
  37. data/lib/core_ext/date_time.rb +5 -0
  38. data/lib/core_ext/deprecation/behaviors.rb +86 -0
  39. data/lib/core_ext/deprecation/instance_delegator.rb +24 -0
  40. data/lib/core_ext/deprecation/method_wrappers.rb +70 -0
  41. data/lib/core_ext/deprecation/proxy_wrappers.rb +149 -0
  42. data/lib/core_ext/deprecation/reporting.rb +105 -0
  43. data/lib/core_ext/deprecation.rb +43 -0
  44. data/lib/core_ext/digest/uuid.rb +51 -0
  45. data/lib/core_ext/duration.rb +157 -0
  46. data/lib/core_ext/enumerable.rb +106 -0
  47. data/lib/core_ext/file/atomic.rb +68 -0
  48. data/lib/core_ext/file.rb +1 -0
  49. data/lib/core_ext/hash/compact.rb +20 -0
  50. data/lib/core_ext/hash/conversions.rb +261 -0
  51. data/lib/core_ext/hash/deep_merge.rb +38 -0
  52. data/lib/core_ext/hash/except.rb +22 -0
  53. data/lib/core_ext/hash/indifferent_access.rb +23 -0
  54. data/lib/core_ext/hash/keys.rb +170 -0
  55. data/lib/core_ext/hash/reverse_merge.rb +22 -0
  56. data/lib/core_ext/hash/slice.rb +48 -0
  57. data/lib/core_ext/hash/transform_values.rb +29 -0
  58. data/lib/core_ext/hash.rb +9 -0
  59. data/lib/core_ext/hash_with_indifferent_access.rb +298 -0
  60. data/lib/core_ext/inflections.rb +70 -0
  61. data/lib/core_ext/inflector/inflections.rb +244 -0
  62. data/lib/core_ext/inflector/methods.rb +381 -0
  63. data/lib/core_ext/inflector/transliterate.rb +112 -0
  64. data/lib/core_ext/inflector.rb +7 -0
  65. data/lib/core_ext/integer/inflections.rb +29 -0
  66. data/lib/core_ext/integer/multiple.rb +10 -0
  67. data/lib/core_ext/integer/time.rb +29 -0
  68. data/lib/core_ext/integer.rb +3 -0
  69. data/lib/core_ext/json/decoding.rb +67 -0
  70. data/lib/core_ext/json/encoding.rb +127 -0
  71. data/lib/core_ext/json.rb +2 -0
  72. data/lib/core_ext/kernel/agnostics.rb +11 -0
  73. data/lib/core_ext/kernel/concern.rb +10 -0
  74. data/lib/core_ext/kernel/reporting.rb +41 -0
  75. data/lib/core_ext/kernel/singleton_class.rb +6 -0
  76. data/lib/core_ext/kernel.rb +4 -0
  77. data/lib/core_ext/load_error.rb +30 -0
  78. data/lib/core_ext/logger.rb +57 -0
  79. data/lib/core_ext/logger_silence.rb +24 -0
  80. data/lib/core_ext/marshal.rb +19 -0
  81. data/lib/core_ext/module/aliasing.rb +74 -0
  82. data/lib/core_ext/module/anonymous.rb +28 -0
  83. data/lib/core_ext/module/attr_internal.rb +36 -0
  84. data/lib/core_ext/module/attribute_accessors.rb +212 -0
  85. data/lib/core_ext/module/concerning.rb +135 -0
  86. data/lib/core_ext/module/delegation.rb +218 -0
  87. data/lib/core_ext/module/deprecation.rb +23 -0
  88. data/lib/core_ext/module/introspection.rb +62 -0
  89. data/lib/core_ext/module/method_transplanting.rb +3 -0
  90. data/lib/core_ext/module/qualified_const.rb +52 -0
  91. data/lib/core_ext/module/reachable.rb +8 -0
  92. data/lib/core_ext/module/remove_method.rb +35 -0
  93. data/lib/core_ext/module.rb +11 -0
  94. data/lib/core_ext/multibyte/chars.rb +231 -0
  95. data/lib/core_ext/multibyte/unicode.rb +388 -0
  96. data/lib/core_ext/multibyte.rb +21 -0
  97. data/lib/core_ext/name_error.rb +31 -0
  98. data/lib/core_ext/numeric/bytes.rb +64 -0
  99. data/lib/core_ext/numeric/conversions.rb +132 -0
  100. data/lib/core_ext/numeric/inquiry.rb +26 -0
  101. data/lib/core_ext/numeric/time.rb +74 -0
  102. data/lib/core_ext/numeric.rb +4 -0
  103. data/lib/core_ext/object/acts_like.rb +10 -0
  104. data/lib/core_ext/object/blank.rb +140 -0
  105. data/lib/core_ext/object/conversions.rb +4 -0
  106. data/lib/core_ext/object/deep_dup.rb +53 -0
  107. data/lib/core_ext/object/duplicable.rb +98 -0
  108. data/lib/core_ext/object/inclusion.rb +27 -0
  109. data/lib/core_ext/object/instance_variables.rb +28 -0
  110. data/lib/core_ext/object/json.rb +199 -0
  111. data/lib/core_ext/object/to_param.rb +1 -0
  112. data/lib/core_ext/object/to_query.rb +84 -0
  113. data/lib/core_ext/object/try.rb +146 -0
  114. data/lib/core_ext/object/with_options.rb +69 -0
  115. data/lib/core_ext/object.rb +14 -0
  116. data/lib/core_ext/option_merger.rb +25 -0
  117. data/lib/core_ext/ordered_hash.rb +48 -0
  118. data/lib/core_ext/ordered_options.rb +81 -0
  119. data/lib/core_ext/range/conversions.rb +34 -0
  120. data/lib/core_ext/range/each.rb +21 -0
  121. data/lib/core_ext/range/include_range.rb +23 -0
  122. data/lib/core_ext/range/overlaps.rb +8 -0
  123. data/lib/core_ext/range.rb +4 -0
  124. data/lib/core_ext/regexp.rb +5 -0
  125. data/lib/core_ext/rescuable.rb +119 -0
  126. data/lib/core_ext/securerandom.rb +23 -0
  127. data/lib/core_ext/security_utils.rb +20 -0
  128. data/lib/core_ext/string/access.rb +104 -0
  129. data/lib/core_ext/string/behavior.rb +6 -0
  130. data/lib/core_ext/string/conversions.rb +56 -0
  131. data/lib/core_ext/string/exclude.rb +11 -0
  132. data/lib/core_ext/string/filters.rb +102 -0
  133. data/lib/core_ext/string/indent.rb +43 -0
  134. data/lib/core_ext/string/inflections.rb +235 -0
  135. data/lib/core_ext/string/inquiry.rb +13 -0
  136. data/lib/core_ext/string/multibyte.rb +53 -0
  137. data/lib/core_ext/string/output_safety.rb +261 -0
  138. data/lib/core_ext/string/starts_ends_with.rb +4 -0
  139. data/lib/core_ext/string/strip.rb +23 -0
  140. data/lib/core_ext/string/zones.rb +14 -0
  141. data/lib/core_ext/string.rb +13 -0
  142. data/lib/core_ext/string_inquirer.rb +26 -0
  143. data/lib/core_ext/tagged_logging.rb +78 -0
  144. data/lib/core_ext/test_case.rb +88 -0
  145. data/lib/core_ext/testing/assertions.rb +99 -0
  146. data/lib/core_ext/testing/autorun.rb +12 -0
  147. data/lib/core_ext/testing/composite_filter.rb +54 -0
  148. data/lib/core_ext/testing/constant_lookup.rb +50 -0
  149. data/lib/core_ext/testing/declarative.rb +26 -0
  150. data/lib/core_ext/testing/deprecation.rb +36 -0
  151. data/lib/core_ext/testing/file_fixtures.rb +34 -0
  152. data/lib/core_ext/testing/isolation.rb +115 -0
  153. data/lib/core_ext/testing/method_call_assertions.rb +41 -0
  154. data/lib/core_ext/testing/setup_and_teardown.rb +50 -0
  155. data/lib/core_ext/testing/stream.rb +42 -0
  156. data/lib/core_ext/testing/tagged_logging.rb +25 -0
  157. data/lib/core_ext/testing/time_helpers.rb +134 -0
  158. data/lib/core_ext/time/acts_like.rb +8 -0
  159. data/lib/core_ext/time/calculations.rb +284 -0
  160. data/lib/core_ext/time/conversions.rb +66 -0
  161. data/lib/core_ext/time/zones.rb +95 -0
  162. data/lib/core_ext/time.rb +20 -0
  163. data/lib/core_ext/time_with_zone.rb +503 -0
  164. data/lib/core_ext/time_zone.rb +464 -0
  165. data/lib/core_ext/uri.rb +25 -0
  166. data/lib/core_ext/version.rb +3 -0
  167. data/lib/core_ext/xml_mini/jdom.rb +181 -0
  168. data/lib/core_ext/xml_mini/libxml.rb +79 -0
  169. data/lib/core_ext/xml_mini/libxmlsax.rb +85 -0
  170. data/lib/core_ext/xml_mini/nokogiri.rb +83 -0
  171. data/lib/core_ext/xml_mini/nokogirisax.rb +87 -0
  172. data/lib/core_ext/xml_mini/rexml.rb +130 -0
  173. data/lib/core_ext/xml_mini.rb +194 -0
  174. data/lib/core_ext.rb +3 -0
  175. metadata +310 -0
@@ -0,0 +1,62 @@
1
+ require 'core_ext/inflector'
2
+
3
+ class Module
4
+ # Returns the name of the module containing this one.
5
+ #
6
+ # M::N.parent_name # => "M"
7
+ def parent_name
8
+ if defined? @parent_name
9
+ @parent_name
10
+ else
11
+ @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
12
+ end
13
+ end
14
+
15
+ # Returns the module which contains this one according to its name.
16
+ #
17
+ # module M
18
+ # module N
19
+ # end
20
+ # end
21
+ # X = M::N
22
+ #
23
+ # M::N.parent # => M
24
+ # X.parent # => M
25
+ #
26
+ # The parent of top-level and anonymous modules is Object.
27
+ #
28
+ # M.parent # => Object
29
+ # Module.new.parent # => Object
30
+ def parent
31
+ parent_name ? CoreExt::Inflector.constantize(parent_name) : Object
32
+ end
33
+
34
+ # Returns all the parents of this module according to its name, ordered from
35
+ # nested outwards. The receiver is not contained within the result.
36
+ #
37
+ # module M
38
+ # module N
39
+ # end
40
+ # end
41
+ # X = M::N
42
+ #
43
+ # M.parents # => [Object]
44
+ # M::N.parents # => [M, Object]
45
+ # X.parents # => [M, Object]
46
+ def parents
47
+ parents = []
48
+ if parent_name
49
+ parts = parent_name.split('::')
50
+ until parts.empty?
51
+ parents << CoreExt::Inflector.constantize(parts * '::')
52
+ parts.pop
53
+ end
54
+ end
55
+ parents << Object unless parents.include? Object
56
+ parents
57
+ end
58
+
59
+ def local_constants #:nodoc:
60
+ constants(false)
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ require 'core_ext/deprecation'
2
+
3
+ CoreExt::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.")
@@ -0,0 +1,52 @@
1
+ require 'core_ext/string/inflections'
2
+
3
+ #--
4
+ # Allows code reuse in the methods below without polluting Module.
5
+ #++
6
+ module QualifiedConstUtils
7
+ def self.raise_if_absolute(path)
8
+ raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/
9
+ end
10
+
11
+ def self.names(path)
12
+ path.split('::')
13
+ end
14
+ end
15
+
16
+ ##
17
+ # Extends the API for constants to be able to deal with qualified names. Arguments
18
+ # are assumed to be relative to the receiver.
19
+ #
20
+ #--
21
+ # Qualified names are required to be relative because we are extending existing
22
+ # methods that expect constant names, ie, relative paths of length 1. For example,
23
+ # Object.const_get('::String') raises NameError and so does qualified_const_get.
24
+ #++
25
+ class Module
26
+ def qualified_const_defined?(path, search_parents=true)
27
+ QualifiedConstUtils.raise_if_absolute(path)
28
+
29
+ QualifiedConstUtils.names(path).inject(self) do |mod, name|
30
+ return unless mod.const_defined?(name, search_parents)
31
+ mod.const_get(name)
32
+ end
33
+ return true
34
+ end
35
+
36
+ def qualified_const_get(path)
37
+ QualifiedConstUtils.raise_if_absolute(path)
38
+
39
+ QualifiedConstUtils.names(path).inject(self) do |mod, name|
40
+ mod.const_get(name)
41
+ end
42
+ end
43
+
44
+ def qualified_const_set(path, value)
45
+ QualifiedConstUtils.raise_if_absolute(path)
46
+
47
+ const_name = path.demodulize
48
+ mod_name = path.deconstantize
49
+ mod = mod_name.empty? ? self : qualified_const_get(mod_name)
50
+ mod.const_set(const_name, value)
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ require 'core_ext/module/anonymous'
2
+ require 'core_ext/string/inflections'
3
+
4
+ class Module
5
+ def reachable? #:nodoc:
6
+ !anonymous? && name.safe_constantize.equal?(self)
7
+ end
8
+ end
@@ -0,0 +1,35 @@
1
+ class Module
2
+ # Removes the named method, if it exists.
3
+ def remove_possible_method(method)
4
+ if method_defined?(method) || private_method_defined?(method)
5
+ undef_method(method)
6
+ end
7
+ end
8
+
9
+ # Removes the named singleton method, if it exists.
10
+ def remove_possible_singleton_method(method)
11
+ singleton_class.instance_eval do
12
+ remove_possible_method(method)
13
+ end
14
+ end
15
+
16
+ # Replaces the existing method definition, if there is one, with the passed
17
+ # block as its body.
18
+ def redefine_method(method, &block)
19
+ visibility = method_visibility(method)
20
+ remove_possible_method(method)
21
+ define_method(method, &block)
22
+ send(visibility, method)
23
+ end
24
+
25
+ def method_visibility(method) # :nodoc:
26
+ case
27
+ when private_method_defined?(method)
28
+ :private
29
+ when protected_method_defined?(method)
30
+ :protected
31
+ else
32
+ :public
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ require 'core_ext/module/aliasing'
2
+ require 'core_ext/module/introspection'
3
+ require 'core_ext/module/anonymous'
4
+ require 'core_ext/module/reachable'
5
+ require 'core_ext/module/attribute_accessors'
6
+ require 'core_ext/module/attr_internal'
7
+ require 'core_ext/module/concerning'
8
+ require 'core_ext/module/delegation'
9
+ require 'core_ext/module/deprecation'
10
+ require 'core_ext/module/remove_method'
11
+ require 'core_ext/module/qualified_const'
@@ -0,0 +1,231 @@
1
+ require 'core_ext/json'
2
+ require 'core_ext/string/access'
3
+ require 'core_ext/string/behavior'
4
+ require 'core_ext/module/delegation'
5
+
6
+ module CoreExt #:nodoc:
7
+ module Multibyte #:nodoc:
8
+ # Chars enables you to work transparently with UTF-8 encoding in the Ruby
9
+ # String class without having extensive knowledge about the encoding. A
10
+ # Chars object accepts a string upon initialization and proxies String
11
+ # methods in an encoding safe manner. All the normal String methods are also
12
+ # implemented on the proxy.
13
+ #
14
+ # String methods are proxied through the Chars object, and can be accessed
15
+ # through the +mb_chars+ method. Methods which would normally return a
16
+ # String object now return a Chars object so methods can be chained.
17
+ #
18
+ # 'The Perfect String '.mb_chars.downcase.strip.normalize # => "the perfect string"
19
+ #
20
+ # Chars objects are perfectly interchangeable with String objects as long as
21
+ # no explicit class checks are made. If certain methods do explicitly check
22
+ # the class, call +to_s+ before you pass chars objects to them.
23
+ #
24
+ # bad.explicit_checking_method 'T'.mb_chars.downcase.to_s
25
+ #
26
+ # The default Chars implementation assumes that the encoding of the string
27
+ # is UTF-8, if you want to handle different encodings you can write your own
28
+ # multibyte string handler and configure it through
29
+ # CoreExt::Multibyte.proxy_class.
30
+ #
31
+ # class CharsForUTF32
32
+ # def size
33
+ # @wrapped_string.size / 4
34
+ # end
35
+ #
36
+ # def self.accepts?(string)
37
+ # string.length % 4 == 0
38
+ # end
39
+ # end
40
+ #
41
+ # CoreExt::Multibyte.proxy_class = CharsForUTF32
42
+ class Chars
43
+ include Comparable
44
+ attr_reader :wrapped_string
45
+ alias to_s wrapped_string
46
+ alias to_str wrapped_string
47
+
48
+ delegate :<=>, :=~, :acts_like_string?, :to => :wrapped_string
49
+
50
+ # Creates a new Chars instance by wrapping _string_.
51
+ def initialize(string)
52
+ @wrapped_string = string
53
+ @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
54
+ end
55
+
56
+ # Forward all undefined methods to the wrapped string.
57
+ def method_missing(method, *args, &block)
58
+ result = @wrapped_string.__send__(method, *args, &block)
59
+ if method.to_s =~ /!$/
60
+ self if result
61
+ else
62
+ result.kind_of?(String) ? chars(result) : result
63
+ end
64
+ end
65
+
66
+ # Returns +true+ if _obj_ responds to the given method. Private methods
67
+ # are included in the search only if the optional second parameter
68
+ # evaluates to +true+.
69
+ def respond_to_missing?(method, include_private)
70
+ @wrapped_string.respond_to?(method, include_private)
71
+ end
72
+
73
+ # Returns +true+ when the proxy class can handle the string. Returns
74
+ # +false+ otherwise.
75
+ def self.consumes?(string)
76
+ string.encoding == Encoding::UTF_8
77
+ end
78
+
79
+ # Works just like <tt>String#split</tt>, with the exception that the items
80
+ # in the resulting list are Chars instances instead of String. This makes
81
+ # chaining methods easier.
82
+ #
83
+ # 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
84
+ def split(*args)
85
+ @wrapped_string.split(*args).map { |i| self.class.new(i) }
86
+ end
87
+
88
+ # Works like <tt>String#slice!</tt>, but returns an instance of
89
+ # Chars, or nil if the string was not modified. The string will not be
90
+ # modified if the range given is out of bounds
91
+ #
92
+ # string = 'Welcome'
93
+ # string.mb_chars.slice!(3) # => #<CoreExt::Multibyte::Chars:0x000000038109b8 @wrapped_string="c">
94
+ # string # => 'Welome'
95
+ # string.mb_chars.slice!(0..3) # => #<CoreExt::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo">
96
+ # string # => 'me'
97
+ def slice!(*args)
98
+ string_sliced = @wrapped_string.slice!(*args)
99
+ if string_sliced
100
+ chars(string_sliced)
101
+ end
102
+ end
103
+
104
+ # Reverses all characters in the string.
105
+ #
106
+ # 'Café'.mb_chars.reverse.to_s # => 'éfaC'
107
+ def reverse
108
+ chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack('U*'))
109
+ end
110
+
111
+ # Limits the byte size of the string to a number of bytes without breaking
112
+ # characters. Usable when the storage for a string is limited for some
113
+ # reason.
114
+ #
115
+ # 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
116
+ def limit(limit)
117
+ slice(0...translate_offset(limit))
118
+ end
119
+
120
+ # Converts characters in the string to uppercase.
121
+ #
122
+ # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
123
+ def upcase
124
+ chars Unicode.upcase(@wrapped_string)
125
+ end
126
+
127
+ # Converts characters in the string to lowercase.
128
+ #
129
+ # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
130
+ def downcase
131
+ chars Unicode.downcase(@wrapped_string)
132
+ end
133
+
134
+ # Converts characters in the string to the opposite case.
135
+ #
136
+ # 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN"
137
+ def swapcase
138
+ chars Unicode.swapcase(@wrapped_string)
139
+ end
140
+
141
+ # Converts the first character to uppercase and the remainder to lowercase.
142
+ #
143
+ # 'über'.mb_chars.capitalize.to_s # => "Über"
144
+ def capitalize
145
+ (slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
146
+ end
147
+
148
+ # Capitalizes the first letter of every word, when possible.
149
+ #
150
+ # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró"
151
+ # "日本語".mb_chars.titleize # => "日本語"
152
+ def titleize
153
+ chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1)})
154
+ end
155
+ alias_method :titlecase, :titleize
156
+
157
+ # Returns the KC normalization of the string by default. NFKC is
158
+ # considered the best normalization form for passing strings to databases
159
+ # and validations.
160
+ #
161
+ # * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
162
+ # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
163
+ # CoreExt::Multibyte::Unicode.default_normalization_form
164
+ def normalize(form = nil)
165
+ chars(Unicode.normalize(@wrapped_string, form))
166
+ end
167
+
168
+ # Performs canonical decomposition on all the characters.
169
+ #
170
+ # 'é'.length # => 2
171
+ # 'é'.mb_chars.decompose.to_s.length # => 3
172
+ def decompose
173
+ chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack('U*'))
174
+ end
175
+
176
+ # Performs composition on all the characters.
177
+ #
178
+ # 'é'.length # => 3
179
+ # 'é'.mb_chars.compose.to_s.length # => 2
180
+ def compose
181
+ chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack('U*'))
182
+ end
183
+
184
+ # Returns the number of grapheme clusters in the string.
185
+ #
186
+ # 'क्षि'.mb_chars.length # => 4
187
+ # 'क्षि'.mb_chars.grapheme_length # => 3
188
+ def grapheme_length
189
+ Unicode.unpack_graphemes(@wrapped_string).length
190
+ end
191
+
192
+ # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
193
+ # resulting in a valid UTF-8 string.
194
+ #
195
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's
196
+ # encoding is entirely CP1252 or ISO-8859-1.
197
+ def tidy_bytes(force = false)
198
+ chars(Unicode.tidy_bytes(@wrapped_string, force))
199
+ end
200
+
201
+ def as_json(options = nil) #:nodoc:
202
+ to_s.as_json(options)
203
+ end
204
+
205
+ %w(capitalize downcase reverse tidy_bytes upcase).each do |method|
206
+ define_method("#{method}!") do |*args|
207
+ @wrapped_string = send(method, *args).to_s
208
+ self
209
+ end
210
+ end
211
+
212
+ protected
213
+
214
+ def translate_offset(byte_offset) #:nodoc:
215
+ return nil if byte_offset.nil?
216
+ return 0 if @wrapped_string == ''
217
+
218
+ begin
219
+ @wrapped_string.byteslice(0...byte_offset).unpack('U*').length
220
+ rescue ArgumentError
221
+ byte_offset -= 1
222
+ retry
223
+ end
224
+ end
225
+
226
+ def chars(string) #:nodoc:
227
+ self.class.new(string)
228
+ end
229
+ end
230
+ end
231
+ end