ffi-icu 0.6.1 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f219be994f4a69799cf8b848c0909786476face806e223c89f8f4b455ef9b85
4
- data.tar.gz: 562ceb9c82764d4251566107cadaabe7d0f63c69f012a0771c6ebf2d75677252
3
+ metadata.gz: 6ffb113b9b98bd46355e3d6750fc1026d7732d06a8575bf2022aa073e72c532e
4
+ data.tar.gz: 43b5a202de5a08790c2a257a8a05f9867df2a5b42624cab157dbf20eaa85eb37
5
5
  SHA512:
6
- metadata.gz: ca6afe1397a8879f89303ed2b9866caaa906d0b4271b4fb75f8eb8a51e8e9a482f8b398072f13a85e5d790e9b261a7ab3178dda2c2a35a5dceb575fd798b5679
7
- data.tar.gz: 9f722bd3b1248aeac053d74230ce9c98a9cde2d1e925a86284179880d969ec7d1fe59dbabf579306e9f63101bb013198ba6c637e4aa7e561a48b7995451e4411
6
+ metadata.gz: 63fefe8619f8bfbad7ead9ac1ada9471074246b1a2d7c4af6cdb899250f464a07c8596bb55e836f44a128d1ce077b939404b2ad85c612298cc65a66bc1fe92f8
7
+ data.tar.gz: 06e98c3af40a060bbbd2a19a9e5b95fcea49d1d75316a1f2ea639d91d88721886fe079ad3e4952ac8750cdfbf62e5dc4550b0893c3583c1896764c46d1c3fd46
data/README.md CHANGED
@@ -93,6 +93,28 @@ curf = ICU::NumberFormatting.create('en-US', :currency)
93
93
  curf.format(1234.56, 'USD') #=> "$1,234.56"
94
94
  ```
95
95
 
96
+ ## Message Formatting
97
+
98
+ Message formatting supports ICU MessageFormat patterns using numbered arguments:
99
+
100
+ ```ruby
101
+ formatter = ICU::MessageFormatting.create(
102
+ '{0, plural, one {# item} other {# items}}',
103
+ locale: 'en-US'
104
+ )
105
+
106
+ formatter.format([2]) #=> "2 items"
107
+ ```
108
+
109
+ Patterns can also be validated without formatting a message:
110
+
111
+ ```ruby
112
+ ICU::MessageFormatting.validate('{0, select, male {He} female {She} other {They}}', locale: 'en-US')
113
+ #=> true
114
+ ```
115
+
116
+ The current API uses ICU's C MessageFormat API and therefore resolves numbered arguments only. Named arguments are not resolved by this API, and rich-text tags require a separate compatibility layer. Supported Ruby argument types are `String`, `Integer`, `Float`, and `BigDecimal`.
117
+
96
118
  ## Time Formatting/Parsing
97
119
 
98
120
  Examples:
@@ -187,6 +209,26 @@ Example:
187
209
  ICU::Transliteration.transliterate('Traditional-Simplified', '沈從文') # => "沈从文"
188
210
  ```
189
211
 
212
+ ## List Formatting
213
+
214
+ Join items with locale-aware separators and conjunctions:
215
+
216
+ ```ruby
217
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'en', style: :and)
218
+ # => "Alice, Bob, and Jane"
219
+
220
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'fr', style: :and)
221
+ # => "Alice, Bob et Jane"
222
+ ```
223
+
224
+ Supported styles are:
225
+
226
+ - `:and`: the default
227
+ - `:or`
228
+ - `:unit`: ICU's units list type
229
+
230
+ Only ICU's wide width is exposed. Styles must be symbols, and items must be strings. The Ruby/FFI layer converts Ruby strings to ICU's UTF-16 `UChar` values before calling ICU. List formatting requires ICU 67 or newer.
231
+
190
232
  ## Locale
191
233
 
192
234
  Examples:
@@ -197,7 +239,10 @@ locale.display_country('en-US') #=> "United States"
197
239
  locale.display_language('es') #=> "inglés"
198
240
  locale.display_name('es') #=> "inglés (Estados Unidos)"
199
241
  locale.display_name_with_context('en-US', [:length_short]) #=> "English (US)"
200
- locale.display_name_with_context('en-US', [:length_long]) #=> "English (United States)"
242
+ locale.display_name_with_context('en-US', [:length_full]) #=> "English (United States)"
243
+
244
+ dari = ICU::Locale.new('fa-AF')
245
+ dari.display_name_with_context('en', [:dialect_names]) #=> "Dari"
201
246
  ```
202
247
 
203
248
  [icu]: https://github.com/unicode-org/icu
@@ -20,16 +20,20 @@ module ICU
20
20
  end
21
21
 
22
22
  def text=(str)
23
- @text = str
23
+ text_pointer = UCharPointer.from_utf8(str)
24
24
 
25
25
  Lib.check_error do |err|
26
- Lib.ubrk_setText(@iterator, UCharPointer.from_string(str), str.size, err)
26
+ Lib.ubrk_setText(@iterator, text_pointer, text_pointer.length_in_uchars, err)
27
27
  end
28
+
29
+ @text = str
30
+ @text_pointer = text_pointer
28
31
  end
29
32
 
30
33
  def each
31
34
  return to_enum(:each) unless block_given?
32
35
 
36
+ # Positional methods expose ICU UTF-16 code-unit offsets.
33
37
  int = first
34
38
 
35
39
  while int != DONE
@@ -43,11 +47,13 @@ module ICU
43
47
  def each_substring
44
48
  return to_enum(:each_substring) unless block_given?
45
49
 
46
- # each_char needed for 1.8, where String#[] works on bytes, not characters
47
50
  chars = text.each_char.to_a
48
- low = first
51
+ utf16_to_ruby = ruby_offsets_for(chars)
52
+
53
+ low = utf16_to_ruby[first]
49
54
 
50
55
  while (high = self.next) != DONE
56
+ high = utf16_to_ruby[high]
51
57
  yield(chars[low...high].join)
52
58
  low = high
53
59
  end
@@ -55,6 +61,19 @@ module ICU
55
61
  self
56
62
  end
57
63
 
64
+ def ruby_offsets_for(chars)
65
+ offsets = [0]
66
+ utf16_offset = 0
67
+
68
+ chars.each_with_index do |char, ruby_index|
69
+ utf16_offset += char.encode(Encoding::UTF_16LE).bytesize / UCharPointer::TYPE_SIZE
70
+ offsets[utf16_offset] = ruby_index + 1
71
+ end
72
+
73
+ offsets
74
+ end
75
+ private :ruby_offsets_for
76
+
58
77
  def substrings
59
78
  each_substring.to_a
60
79
  end
@@ -64,7 +83,7 @@ module ICU
64
83
  end
65
84
 
66
85
  def previous
67
- Lib.ubrk_next(@iterator)
86
+ Lib.ubrk_previous(@iterator)
68
87
  end
69
88
 
70
89
  def first
@@ -72,21 +72,15 @@ module ICU
72
72
  end
73
73
 
74
74
  def compare(a, b)
75
- Lib.ucol_strcoll(
76
- @c,
77
- UCharPointer.from_string(a), a.size,
78
- UCharPointer.from_string(b), b.size
79
- )
75
+ Lib.ucol_strcoll(@c, *collation_arguments(a, b))
80
76
  end
81
77
 
82
78
  def greater?(a, b)
83
- Lib.ucol_greater(@c, UCharPointer.from_string(a), a.size,
84
- UCharPointer.from_string(b), b.size)
79
+ Lib.ucol_greater(@c, *collation_arguments(a, b))
85
80
  end
86
81
 
87
82
  def greater_or_equal?(a, b)
88
- Lib.ucol_greaterOrEqual(@c, UCharPointer.from_string(a), a.size,
89
- UCharPointer.from_string(b), b.size)
83
+ Lib.ucol_greaterOrEqual(@c, *collation_arguments(a, b))
90
84
  end
91
85
 
92
86
  def equal?(*args)
@@ -96,8 +90,7 @@ module ICU
96
90
 
97
91
  a, b = args
98
92
 
99
- Lib.ucol_equal(@c, UCharPointer.from_string(a), a.size,
100
- UCharPointer.from_string(b), b.size)
93
+ Lib.ucol_equal(@c, *collation_arguments(a, b))
101
94
  end
102
95
 
103
96
  def collate(sortable)
@@ -110,15 +103,16 @@ module ICU
110
103
  @rules ||= begin
111
104
  length = FFI::MemoryPointer.new(:int)
112
105
  ptr = Lib.ucol_getRules(@c, length)
113
- ptr.read_array_of_uint16(length.read_int).pack('U*')
106
+ ptr.read_array_of_uint16(length.read_int).
107
+ pack('v*').force_encoding('UTF-16LE').encode('UTF-8')
114
108
  end
115
109
  end
116
110
 
117
111
  def collation_key(string)
118
- ptr = UCharPointer.from_string(string)
119
- size = Lib.ucol_getSortKey(@c, ptr, string.size, nil, 0)
112
+ ptr = UCharPointer.from_utf8(string)
113
+ size = Lib.ucol_getSortKey(@c, ptr, ptr.length_in_uchars, nil, 0)
120
114
  buffer = FFI::MemoryPointer.new(:char, size)
121
- Lib.ucol_getSortKey(@c, ptr, string.size, buffer, size)
115
+ Lib.ucol_getSortKey(@c, ptr, ptr.length_in_uchars, buffer, size)
122
116
  buffer.read_bytes(size - 1)
123
117
  end
124
118
 
@@ -134,6 +128,17 @@ module ICU
134
128
  end
135
129
  end
136
130
 
131
+ def collation_arguments(left, right)
132
+ left_text = UCharPointer.from_utf8(left)
133
+ right_text = UCharPointer.from_utf8(right)
134
+
135
+ [
136
+ left_text, left_text.length_in_uchars,
137
+ right_text, right_text.length_in_uchars
138
+ ]
139
+ end
140
+ private :collation_arguments
141
+
137
142
  # create friendly named methods for setting attributes
138
143
  ATTRIBUTES.each_key do |attribute|
139
144
  class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -0,0 +1,123 @@
1
+ module ICU
2
+ class Currency
3
+ CURRENCY_TYPE_COMMON = 1
4
+ CURRENCY_TYPE_UNCOMMON = 2
5
+ CURRENCY_TYPE_DEPRECATED = 4
6
+ CURRENCY_TYPE_NON_DEPRECATED = 8
7
+ CURRENCY_TYPE_ALL = (2**31) - 1
8
+
9
+ class << self
10
+ # Returns the default currency code for a locale, e.g. "USD" for "en_US"
11
+ def for_locale(locale)
12
+ ptr = UCharPointer.new(4)
13
+ length = Lib.check_error do |status|
14
+ Lib.ucurr_forLocale(locale.to_s, ptr, 4, status)
15
+ end
16
+ ptr.utf8_string(length)
17
+ end
18
+
19
+ # Returns an array of ISO 4217 currency code strings.
20
+ # currency_type: :all (default), :common, :uncommon, :deprecated, :non_deprecated
21
+ def available(currency_type = :all)
22
+ flag = case currency_type
23
+ when :common then CURRENCY_TYPE_COMMON
24
+ when :uncommon then CURRENCY_TYPE_UNCOMMON
25
+ when :deprecated then CURRENCY_TYPE_DEPRECATED
26
+ when :non_deprecated then CURRENCY_TYPE_NON_DEPRECATED
27
+ else CURRENCY_TYPE_ALL
28
+ end
29
+
30
+ enum_ptr = Lib.check_error { |status| Lib.ucurr_openISOCurrencies(flag, status) }
31
+ begin
32
+ Lib.enum_ptr_to_array(enum_ptr)
33
+ ensure
34
+ Lib.uenum_close(enum_ptr)
35
+ end
36
+ end
37
+
38
+ # Convenience: ICU::Currency.symbol('USD', 'en_US') => "$"
39
+ def symbol(code, locale)
40
+ new(code).symbol(locale)
41
+ end
42
+
43
+ # Convenience: ICU::Currency.name('USD', 'en') => "US Dollar"
44
+ def name(code, locale)
45
+ new(code).name(locale)
46
+ end
47
+ end
48
+
49
+ attr_reader :code
50
+
51
+ def initialize(code)
52
+ @code = code.to_s.upcase
53
+ end
54
+
55
+ # Currency symbol for the given locale, e.g. "$", "€"
56
+ def symbol(locale)
57
+ get_name(locale, :symbol_name)
58
+ end
59
+
60
+ # Narrow (shortest unambiguous) symbol. Requires ICU >= 61.
61
+ def narrow_symbol(locale)
62
+ raise('narrow_symbol requires ICU >= 61') if Lib.version.to_a.first < 61
63
+
64
+ get_name(locale, :narrow_symbol_name)
65
+ end
66
+
67
+ # Long display name for the given locale, e.g. "US Dollar"
68
+ def name(locale)
69
+ get_name(locale, :long_name)
70
+ end
71
+
72
+ # Plural form of the currency name.
73
+ # plural_count: "zero", "one", "two", "few", "many", "other"
74
+ def plural_name(locale, plural_count = 'other')
75
+ raise('plural_name requires ICU >= 4.2') unless Lib.respond_to?(:ucurr_getPluralName)
76
+
77
+ is_choice = FFI::MemoryPointer.new(:uint8)
78
+ len = FFI::MemoryPointer.new(:int32_t)
79
+
80
+ ptr = Lib.check_error do |status|
81
+ Lib.ucurr_getPluralName(currency_uchar, locale.to_s, is_choice, plural_count.to_s, len, status)
82
+ end
83
+ ptr.read_array_of_uint16(len.read_int32).pack('U*')
84
+ end
85
+
86
+ # Number of decimal digits used when displaying this currency.
87
+ # usage: :standard (default) or :cash
88
+ def fraction_digits(usage = :standard)
89
+ if usage == :cash && Lib.respond_to?(:ucurr_getDefaultFractionDigitsForUsage)
90
+ Lib.check_error { |status| Lib.ucurr_getDefaultFractionDigitsForUsage(currency_uchar, :cash, status) }
91
+ else
92
+ Lib.check_error { |status| Lib.ucurr_getDefaultFractionDigits(currency_uchar, status) }
93
+ end
94
+ end
95
+
96
+ # ISO 4217 numeric code, e.g. 840 for USD. Requires ICU >= 49.
97
+ def numeric_code
98
+ raise('numeric_code requires ICU >= 49') unless Lib.respond_to?(:ucurr_getNumericCode)
99
+
100
+ Lib.ucurr_getNumericCode(currency_uchar)
101
+ end
102
+
103
+ def to_s
104
+ @code
105
+ end
106
+
107
+ private
108
+
109
+ def currency_uchar
110
+ UCharPointer.from_utf8(@code, 4)
111
+ end
112
+
113
+ def get_name(locale, style)
114
+ is_choice = FFI::MemoryPointer.new(:uint8)
115
+ len = FFI::MemoryPointer.new(:int32_t)
116
+
117
+ ptr = Lib.check_error do |status|
118
+ Lib.ucurr_getName(currency_uchar, locale.to_s, style, is_choice, len, status)
119
+ end
120
+ ptr.read_array_of_uint16(len.read_int32).pack('U*')
121
+ end
122
+ end
123
+ end
@@ -264,7 +264,7 @@ module ICU
264
264
 
265
265
  def number_formatter(skeleton)
266
266
  @number_formatters[skeleton] ||= begin
267
- skeleton_uchar = UCharPointer.from_string(skeleton)
267
+ skeleton_uchar = UCharPointer.from_utf8(skeleton)
268
268
  FFI::AutoPointer.new(
269
269
  Lib.check_error do |error|
270
270
  Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars,
@@ -291,7 +291,7 @@ module ICU
291
291
  end
292
292
 
293
293
  def format_list(values)
294
- value_uchars = values.map(&UCharPointer.method(:from_string))
294
+ value_uchars = values.map(&UCharPointer.method(:from_utf8))
295
295
  value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size)
296
296
  value_uchars_array.put_array_of_pointer(0, value_uchars)
297
297
  value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size)
@@ -32,7 +32,7 @@ module ICU
32
32
 
33
33
  def self.read_uchar_buffer(length, &)
34
34
  buf, len = read_uchar_buffer_as_ptr_impl(length, &)
35
- buf.string(len)
35
+ buf.utf8_string(len)
36
36
  end
37
37
 
38
38
  def self.read_uchar_buffer_as_ptr(length, &)
data/lib/ffi-icu/lib.rb CHANGED
@@ -19,18 +19,23 @@ module ICU
19
19
  elsif FFI::Platform::IS_WINDOWS
20
20
  ENV['PATH'].split(File::PATH_SEPARATOR)
21
21
  else
22
+ multiarch_paths = Dir['/usr/lib/*-linux-gnu']
23
+ multiarch_paths.sort_by! do |path|
24
+ File.basename(path).start_with?("#{FFI::Platform::ARCH}-") ? 0 : 1
25
+ end
26
+
22
27
  [
23
28
  '/usr/local/{lib64,lib}',
24
29
  '/opt/local/{lib64,lib}',
25
30
  '/usr/{lib64,lib}'
26
- ] + Dir['/usr/lib/*-linux-gnu'] # for Debian Multiarch http://wiki.debian.org/Multiarch
31
+ ] + multiarch_paths # for Debian Multiarch http://wiki.debian.org/Multiarch
27
32
  end
28
33
  end
29
34
 
30
35
  def self.find_lib(lib)
31
- Dir.glob(search_paths.map do |path|
32
- File.expand_path(File.join(path, lib))
33
- end).first
36
+ search_paths.lazy.filter_map do |path|
37
+ Dir.glob(File.expand_path(File.join(path, lib))).first
38
+ end.first
34
39
  end
35
40
 
36
41
  def self.load_icu
@@ -120,6 +125,7 @@ module ICU
120
125
 
121
126
  def self.check_error
122
127
  ptr = FFI::MemoryPointer.new(:int)
128
+ ptr.write_int(0)
123
129
  ret = yield(ptr)
124
130
  error_code = ptr.read_int
125
131
 
@@ -203,6 +209,10 @@ module ICU
203
209
 
204
210
  typedef VersionInfo, :version
205
211
 
212
+ attach_function :u_getVersion, "u_getVersion#{suffix}", [:version], :void
213
+ attach_function :u_versionToString, "u_versionToString#{suffix}", [:version, :pointer], :void
214
+ version ||= self.version.to_s
215
+
206
216
  attach_function :u_errorName, "u_errorName#{suffix}", [:int], :string
207
217
  attach_function :uenum_count, "uenum_count#{suffix}", [:pointer, :pointer], :int
208
218
  attach_function :uenum_close, "uenum_close#{suffix}", [:pointer], :void
@@ -210,9 +220,6 @@ module ICU
210
220
  attach_function :u_charsToUChars, "u_charsToUChars#{suffix}", [:string, :pointer, :int32_t], :void
211
221
  attach_function :u_UCharsToChars, "u_UCharsToChars#{suffix}", [:pointer, :string, :int32_t], :void
212
222
 
213
- attach_function :u_getVersion, "u_getVersion#{suffix}", [:version], :void
214
- attach_function :u_versionToString, "u_versionToString#{suffix}", [:version, :pointer], :void
215
-
216
223
  #
217
224
  # Locale
218
225
  #
@@ -345,16 +352,51 @@ module ICU
345
352
  attach_function :ucol_getAttribute, "ucol_getAttribute#{suffix}", [:pointer, :int, :pointer], :int
346
353
  attach_function :ucol_setAttribute, "ucol_setAttribute#{suffix}", [:pointer, :int, :int, :pointer], :void
347
354
 
355
+ # Currency
356
+ #
357
+ # https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/ucurr_8h.html
358
+ #
359
+
360
+ enum :ucurr_name_style, [
361
+ :symbol_name, 0, # "$"
362
+ :long_name, 1, # "US Dollar"
363
+ :narrow_symbol_name, 4, # shortest unambiguous symbol (ICU >= 61)
364
+ :formal_symbol_name, 5, # banking context (ICU >= 68)
365
+ :variant_symbol_name, 6 # regional variant (ICU >= 68)
366
+ ]
367
+
368
+ enum :ucurrency_usage, [
369
+ :standard, 0, # UCURR_USAGE_STANDARD
370
+ :cash, 1 # UCURR_USAGE_CASH
371
+ ]
372
+
373
+ attach_function :ucurr_getName, "ucurr_getName#{suffix}",
374
+ [:pointer, :string, :ucurr_name_style, :pointer, :pointer, :pointer], :pointer
375
+ attach_optional_function :ucurr_getPluralName, "ucurr_getPluralName#{suffix}",
376
+ [:pointer, :string, :pointer, :string, :pointer, :pointer], :pointer
377
+ attach_function :ucurr_forLocale, "ucurr_forLocale#{suffix}",
378
+ [:string, :pointer, :int32_t, :pointer], :int32_t
379
+ attach_function :ucurr_openISOCurrencies, "ucurr_openISOCurrencies#{suffix}",
380
+ [:uint32, :pointer], :pointer
381
+ attach_function :ucurr_getDefaultFractionDigits, "ucurr_getDefaultFractionDigits#{suffix}",
382
+ [:pointer, :pointer], :int32_t
383
+ attach_optional_function :ucurr_getDefaultFractionDigitsForUsage, "ucurr_getDefaultFractionDigitsForUsage#{suffix}",
384
+ [:pointer, :ucurrency_usage, :pointer], :int32_t
385
+ attach_optional_function :ucurr_getNumericCode, "ucurr_getNumericCode#{suffix}",
386
+ [:pointer], :int32_t
387
+
348
388
  # Transliteration
349
389
  #
350
390
  # http://icu-project.org/apiref/icu4c/utrans_8h.html
351
391
  #
352
392
 
353
393
  class UParseError < FFI::Struct
394
+ U_PARSE_CONTEXT_LEN = 16
395
+
354
396
  layout :line, :int32_t,
355
397
  :offset, :int32_t,
356
- :pre_context, :pointer,
357
- :post_context, :pointer
398
+ :pre_context, [:uint16, U_PARSE_CONTEXT_LEN],
399
+ :post_context, [:uint16, U_PARSE_CONTEXT_LEN]
358
400
 
359
401
  def to_s
360
402
  format('#<%<class>s:%<hash>x line: %<line>d offset: %<offset>d',
@@ -362,6 +404,17 @@ module ICU
362
404
  end
363
405
  end
364
406
 
407
+ # MessageFormat
408
+ #
409
+ # https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/umsg_8h.html
410
+ #
411
+
412
+ attach_function :umsg_open, "umsg_open#{suffix}",
413
+ [:pointer, :int32_t, :string, :pointer, :pointer], :pointer
414
+ attach_function :umsg_close, "umsg_close#{suffix}", [:pointer], :void
415
+ attach_function :umsg_format, "umsg_format#{suffix}",
416
+ [:pointer, :pointer, :int32_t, :pointer, :varargs], :int32_t
417
+
365
418
  class UTransPosition < FFI::Struct
366
419
  layout :context_start, :int32_t,
367
420
  :context_limit, :int32_t,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ICU
4
+ module ListFormatting
5
+ VALID_STYLES = [:and, :or, :unit].freeze
6
+
7
+ STYLES_TO_TYPES = {
8
+ and: :and,
9
+ or: :or,
10
+ unit: :units
11
+ }.freeze
12
+
13
+ def self.format(items, locale:, style: :and)
14
+ raise(ArgumentError, "Unknown style #{style}") unless VALID_STYLES.include?(style)
15
+ raise(ArgumentError, 'items must be strings') unless items.all?(String)
16
+
17
+ return '' if items.empty?
18
+
19
+ unless Lib.respond_to?(:ulistfmt_openForType) &&
20
+ Lib.respond_to?(:ulistfmt_close) &&
21
+ Lib.respond_to?(:ulistfmt_format)
22
+ raise('ICU::ListFormatting requires ICU >= 67')
23
+ end
24
+
25
+ formatter = Lib.check_error do |error|
26
+ Lib.ulistfmt_openForType(locale, STYLES_TO_TYPES.fetch(style), :wide, error)
27
+ end
28
+
29
+ begin
30
+ value_uchars = items.map { |item| UCharPointer.from_utf8(item) }
31
+ value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size)
32
+ value_uchars_array.put_array_of_pointer(0, value_uchars)
33
+ value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size)
34
+ value_lengths_array.put_array_of_int32(0, value_uchars.map(&:length_in_uchars))
35
+
36
+ Lib::Util.read_uchar_buffer(0) do |buffer, error|
37
+ Lib.ulistfmt_format(
38
+ formatter,
39
+ value_uchars_array,
40
+ value_lengths_array,
41
+ value_uchars.size,
42
+ buffer,
43
+ buffer.length_in_uchars,
44
+ error
45
+ )
46
+ end
47
+ ensure
48
+ Lib.ulistfmt_close(formatter)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -45,6 +45,7 @@ module ICU
45
45
  attr_reader :id
46
46
 
47
47
  DISPLAY_CONTEXT = {
48
+ dialect_names: 1, # UDISPCTX_DIALECT_NAMES = (UDISPCTX_TYPE_DIALECT_HANDLING<<8) + 1
48
49
  length_full: 512, # UDISPCTX_LENGTH_FULL = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 0
49
50
  length_short: 513 # UDISPCTX_LENGTH_SHORT = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 1
50
51
  }.freeze
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module ICU
6
+ module MessageFormatting
7
+ class MessageFormatError < ICU::Error
8
+ end
9
+
10
+ class InvalidPatternError < MessageFormatError
11
+ attr_reader :locale, :pattern, :parse_error
12
+
13
+ def initialize(message, locale:, pattern:, parse_error:)
14
+ super(message)
15
+ @locale = locale
16
+ @pattern = pattern
17
+ @parse_error = parse_error
18
+ end
19
+
20
+ def offset
21
+ parse_error[:offset]
22
+ end
23
+
24
+ def line
25
+ parse_error[:line]
26
+ end
27
+ end
28
+
29
+ def self.create(pattern, locale:)
30
+ Formatter.new(pattern, locale:)
31
+ end
32
+
33
+ def self.format(pattern, locale:, arguments:)
34
+ create(pattern, locale:).format(arguments)
35
+ end
36
+
37
+ # rubocop:disable-next Naming/PredicateMethod
38
+ def self.validate(pattern, locale:)
39
+ create(pattern, locale:)
40
+ true
41
+ end
42
+
43
+ def self.valid?(pattern, locale:)
44
+ validate(pattern, locale:)
45
+ rescue InvalidPatternError
46
+ false
47
+ end
48
+
49
+ class Formatter
50
+ attr_reader :locale, :pattern
51
+
52
+ def initialize(pattern, locale:)
53
+ @pattern = String(pattern)
54
+ @locale = String(locale)
55
+ @pattern_pointer = UCharPointer.from_utf8(@pattern)
56
+ @formatter = open_formatter
57
+ end
58
+
59
+ def format(arguments = [])
60
+ validate_argument_count!(arguments)
61
+ typed_arguments = typed_arguments_for(arguments)
62
+ validate_argument_types!(arguments)
63
+
64
+ result = Lib::Util.read_uchar_buffer_as_ptr(0) do |result, status|
65
+ Lib.umsg_format(
66
+ @formatter,
67
+ result,
68
+ result.length_in_uchars,
69
+ status,
70
+ *typed_arguments
71
+ )
72
+ end
73
+ result.utf8_string
74
+ rescue ICU::Error => e
75
+ raise MessageFormatError, "could not format message: #{e.message}"
76
+ end
77
+
78
+ private
79
+
80
+ def open_formatter
81
+ parse_error = Lib::UParseError.new
82
+ status = FFI::MemoryPointer.new(:int32_t)
83
+ status.write_int32(0)
84
+ pointer = Lib.umsg_open(
85
+ @pattern_pointer,
86
+ @pattern_pointer.length_in_uchars,
87
+ @locale,
88
+ parse_error,
89
+ status
90
+ )
91
+
92
+ error_code = status.read_int32
93
+ return FFI::AutoPointer.new(pointer, Lib.method(:umsg_close)) unless error_code.positive?
94
+
95
+ raise InvalidPatternError.new(
96
+ "invalid message pattern: #{Lib.u_errorName(error_code)} " \
97
+ "at line #{parse_error[:line]}, offset #{parse_error[:offset]}",
98
+ locale: @locale,
99
+ pattern: @pattern,
100
+ parse_error: parse_error
101
+ )
102
+ end
103
+
104
+ def typed_arguments_for(arguments)
105
+ unless arguments.is_a?(Array)
106
+ raise ArgumentError, 'MessageFormat arguments must be provided as an Array of numbered values'
107
+ end
108
+
109
+ arguments.flat_map do |argument|
110
+ case argument
111
+ when String
112
+ [:pointer, null_terminated_uchar_pointer(argument)]
113
+ when Integer, Float, BigDecimal
114
+ [:double, argument.to_f]
115
+ else
116
+ raise ArgumentError, "unsupported MessageFormat argument type: #{argument.class}"
117
+ end
118
+ end
119
+ end
120
+
121
+ def validate_argument_count!(arguments)
122
+ unless arguments.is_a?(Array)
123
+ raise ArgumentError, 'MessageFormat arguments must be provided as an Array of numbered values'
124
+ end
125
+
126
+ highest_argument = argument_types.keys.max
127
+ required_count = highest_argument ? highest_argument + 1 : 0
128
+ return if arguments.length >= required_count
129
+
130
+ raise ArgumentError, "MessageFormat pattern requires at least #{required_count} numbered arguments"
131
+ end
132
+
133
+ # Maps each numbered argument in the pattern to its ICU type keyword
134
+ # (e.g. "plural", "select", "number") or nil for a simple argument.
135
+ def argument_types
136
+ types = {}
137
+ quoted = false
138
+ chars = @pattern.each_char.to_a
139
+ index = 0
140
+
141
+ while index < chars.length
142
+ if chars[index] == "'"
143
+ if chars[index + 1] == "'"
144
+ index += 2
145
+ next
146
+ elsif quoted
147
+ quoted = false
148
+ elsif ['{', '}', '#'].include?(chars[index + 1])
149
+ quoted = true
150
+ end
151
+ elsif chars[index] == '{' && !quoted
152
+ index += 1
153
+ index += 1 while index < chars.length && chars[index].match?(/\s/)
154
+ start = index
155
+ index += 1 while index < chars.length && chars[index].match?(/\d/)
156
+ next if index == start
157
+
158
+ arg_number = chars[start...index].join.to_i
159
+ index += 1 while index < chars.length && chars[index].match?(/\s/)
160
+
161
+ if chars[index] == '}'
162
+ types[arg_number] = nil unless types.key?(arg_number)
163
+ elsif chars[index] == ','
164
+ index += 1
165
+ index += 1 while index < chars.length && chars[index].match?(/\s/)
166
+ type_start = index
167
+ index += 1 while index < chars.length && chars[index].match?(/[A-Za-z]/)
168
+ type = chars[type_start...index].join.downcase
169
+ types[arg_number] = (type.empty? ? nil : type) unless types.key?(arg_number)
170
+ end
171
+ end
172
+ index += 1
173
+ end
174
+
175
+ types
176
+ end
177
+
178
+ def validate_argument_types!(arguments)
179
+ types = argument_types
180
+ arguments.each_with_index do |argument, index|
181
+ next unless types.key?(index)
182
+
183
+ case expected_kind_for(types[index])
184
+ when :string
185
+ unless argument.is_a?(String)
186
+ raise ArgumentError,
187
+ "MessageFormat argument #{index} expects a String, got #{argument.class}"
188
+ end
189
+ when :number
190
+ unless [Integer, Float, BigDecimal].any? { |klass| argument.is_a?(klass) }
191
+ raise ArgumentError,
192
+ "MessageFormat argument #{index} expects a numeric value, got #{argument.class}"
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # ICU reads each argument slot from the vararg list according to the
199
+ # pattern: simple and select arguments read a UChar*, while number,
200
+ # date, time, choice, plural, and selectordinal read a double.
201
+ def expected_kind_for(type)
202
+ case type
203
+ when 'number', 'date', 'time', 'choice', 'plural', 'selectordinal' then :number
204
+ else :string
205
+ end
206
+ end
207
+
208
+ def null_terminated_uchar_pointer(string)
209
+ pointer = UCharPointer.from_utf8(string)
210
+ terminated = UCharPointer.new(pointer.length_in_uchars + 1)
211
+ terminated.put_bytes(0, pointer.get_bytes(0, pointer.size))
212
+ terminated.put_uint16(pointer.size, 0)
213
+ terminated
214
+ end
215
+ end
216
+ end
217
+ end
@@ -3,28 +3,28 @@
3
3
  module ICU
4
4
  module Normalization
5
5
  def self.normalize(input, mode = :default)
6
- input_length = input.size
7
6
  needed_length = out_length = options = 0
8
- in_ptr = UCharPointer.from_string(input)
7
+ input_pointer = UCharPointer.from_utf8(input)
8
+ input_length = input_pointer.length_in_uchars
9
9
  out_ptr = UCharPointer.new(out_length)
10
10
 
11
11
  retried = false
12
12
 
13
13
  begin
14
14
  Lib.check_error do |error|
15
- needed_length = Lib.unorm_normalize(in_ptr, input_length, mode, options, out_ptr, out_length, error)
15
+ needed_length = Lib.unorm_normalize(input_pointer, input_length, mode, options, out_ptr, out_length, error)
16
16
  end
17
17
  rescue BufferOverflowError
18
18
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
19
19
 
20
- out_ptr = out_ptr.resized_to(needed_length)
21
20
  out_length = needed_length + 1
21
+ out_ptr = out_ptr.resized_to(out_length)
22
22
 
23
23
  retried = true
24
24
  retry
25
25
  end
26
26
 
27
- out_ptr.string
27
+ out_ptr.utf8_string(needed_length)
28
28
  end
29
29
  end
30
30
  end
@@ -11,39 +11,38 @@ module ICU
11
11
  end
12
12
 
13
13
  def normalize(input)
14
- input_length = input.size
15
- in_ptr = UCharPointer.from_string(input)
14
+ input_pointer = UCharPointer.from_utf8(input)
15
+ input_length = input_pointer.length_in_uchars
16
16
  needed_length = capacity = 0
17
17
  out_ptr = UCharPointer.new(needed_length)
18
18
 
19
19
  retried = false
20
20
  begin
21
21
  Lib.check_error do |error|
22
- needed_length = Lib.unorm2_normalize(@instance, in_ptr, input_length, out_ptr, capacity, error)
22
+ needed_length = Lib.unorm2_normalize(@instance, input_pointer, input_length, out_ptr, capacity, error)
23
23
  end
24
24
  rescue BufferOverflowError
25
25
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
26
26
 
27
- capacity = needed_length
28
- out_ptr = out_ptr.resized_to(needed_length)
27
+ capacity = needed_length + 1
28
+ out_ptr = out_ptr.resized_to(capacity)
29
29
 
30
30
  retried = true
31
31
  retry
32
32
  end
33
33
 
34
- out_ptr.string
34
+ out_ptr.utf8_string(needed_length)
35
35
  end
36
36
 
37
- def normailzed?(input)
38
- input_length = input.size
39
- in_ptr = UCharPointer.from_string(input)
37
+ def normalized?(input)
38
+ input_pointer = UCharPointer.from_utf8(input)
39
+ input_length = input_pointer.length_in_uchars
40
40
 
41
41
  Lib.check_error do |error|
42
- Lib.unorm2_isNormalized(@instance, in_ptr, input_length, error)
42
+ Lib.unorm2_isNormalized(@instance, input_pointer, input_length, error)
43
43
  end
44
-
45
- result
46
44
  end
45
+ alias normailzed? normalized?
47
46
 
48
47
  def is_normalized?(input) # rubocop:disable Naming/PredicatePrefix
49
48
  Warning.warn('is_normalized? is deprecated and will be removed after v0.7. Please use normalized? instead.')
@@ -97,7 +97,7 @@ module ICU
97
97
  end
98
98
  end
99
99
  end
100
- out_ptr.string(needed_length)
100
+ out_ptr.utf8_string(needed_length)
101
101
  rescue BufferOverflowError
102
102
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
103
103
 
@@ -136,10 +136,10 @@ module ICU
136
136
 
137
137
  begin
138
138
  Lib.check_error do |error|
139
- needed_length = Lib.unum_format_currency(@f, number, UCharPointer.from_string(currency, 4), out_ptr,
139
+ needed_length = Lib.unum_format_currency(@f, number, UCharPointer.from_utf8(currency, 4), out_ptr,
140
140
  needed_length, nil, error)
141
141
  end
142
- out_ptr.string
142
+ out_ptr.utf8_string
143
143
  rescue BufferOverflowError
144
144
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
145
145
 
@@ -91,8 +91,8 @@ module ICU
91
91
  pattern_ptr = FFI::MemoryPointer.new(4)
92
92
 
93
93
  if time_zone_str
94
- time_zone = UCharPointer.from_string(time_zone_str)
95
- tz_len = time_zone_str.size
94
+ time_zone = UCharPointer.from_utf8(time_zone_str)
95
+ tz_len = time_zone.length_in_uchars
96
96
  else
97
97
  Lib.check_error do |error|
98
98
  i_len = 150
@@ -142,8 +142,8 @@ module ICU
142
142
  end
143
143
 
144
144
  def parse(str)
145
- str_u = UCharPointer.from_string(str)
146
- str_l = str.size
145
+ str_u = UCharPointer.from_utf8(str)
146
+ str_l = str_u.length_in_uchars
147
147
  Lib.check_error do |error|
148
148
  ret = Lib.udat_parse(@f, str_u, str_l, nil, error)
149
149
  Time.at(ret / 1000.0)
@@ -170,7 +170,7 @@ module ICU
170
170
  end
171
171
  end
172
172
 
173
- out_ptr.string
173
+ out_ptr.utf8_string
174
174
  rescue BufferOverflowError
175
175
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
176
176
 
@@ -209,7 +209,7 @@ module ICU
209
209
  needed_length = Lib.udat_toPattern(@f, localized, out_ptr, needed_length, error)
210
210
  end
211
211
 
212
- out_ptr.string
212
+ out_ptr.utf8_string
213
213
  rescue BufferOverflowError
214
214
  raise(BufferOverflowError, "needed: #{needed_length}") if retried
215
215
 
@@ -228,8 +228,8 @@ module ICU
228
228
  end
229
229
 
230
230
  def skeleton_format(skeleton_pattern_str, locale)
231
- skeleton_pattern_ptr = UCharPointer.from_string(skeleton_pattern_str)
232
- skeleton_pattern_len = skeleton_pattern_str.size
231
+ skeleton_pattern_ptr = UCharPointer.from_utf8(skeleton_pattern_str)
232
+ skeleton_pattern_len = skeleton_pattern_ptr.length_in_uchars
233
233
 
234
234
  needed_length = 0
235
235
  pattern_ptr = UCharPointer.new(needed_length)
@@ -272,7 +272,7 @@ module ICU
272
272
  return unless @hour_cycle
273
273
 
274
274
  # Get the current pattern and convert to a skeleton
275
- skeleton_str = pattern_to_skeleton_uchar(current_pattern_as_uchar).string
275
+ skeleton_str = pattern_to_skeleton_uchar(current_pattern_as_uchar).utf8_string
276
276
 
277
277
  # Manipulate the skeleton to make it work with the correct hour cycle.
278
278
  skeleton_str.gsub!(/[hHkKjJ]/, HOUR_CYCLE_SYMS[@hour_cycle])
@@ -286,7 +286,7 @@ module ICU
286
286
  end
287
287
 
288
288
  # Convert the skeleton back to a pattern
289
- new_pattern_str = skeleton_to_pattern_uchar(UCharPointer.from_string(skeleton_str)).string
289
+ new_pattern_str = skeleton_to_pattern_uchar(UCharPointer.from_utf8(skeleton_str)).utf8_string
290
290
 
291
291
  # We also need to manipulate the _pattern_, a little bit, because (according to Firefox source):
292
292
  #
@@ -345,8 +345,8 @@ module ICU
345
345
  end
346
346
 
347
347
  def set_date_format_impl(localized, pattern_str)
348
- pattern = UCharPointer.from_string(pattern_str)
349
- pattern_len = pattern_str.size
348
+ pattern = UCharPointer.from_utf8(pattern_str)
349
+ pattern_len = pattern.length_in_uchars
350
350
 
351
351
  Lib.check_error do |_error|
352
352
  Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
@@ -26,15 +26,17 @@ module ICU
26
26
  rules_length = 0
27
27
 
28
28
  if rules
29
- rules_length = rules.size + 1
30
- rules = UCharPointer.from_string(rules)
29
+ rules = UCharPointer.from_utf8(rules)
30
+ rules_length = rules.length_in_uchars
31
31
  end
32
32
 
33
+ id = UCharPointer.from_utf8(id)
34
+
33
35
  parse_error = Lib::UParseError.new
34
36
  begin
35
37
  Lib.check_error do |status|
36
- ptr = Lib.utrans_openU(UCharPointer.from_string(id), id.size, direction, rules, rules_length,
37
- @parse_error, status)
38
+ ptr = Lib.utrans_openU(id, id.length_in_uchars, direction, rules, rules_length,
39
+ parse_error, status)
38
40
  @tr = FFI::AutoPointer.new(ptr, Lib.method(:utrans_close))
39
41
  end
40
42
  rescue ICU::Error => e
@@ -45,9 +47,11 @@ module ICU
45
47
  def transliterate(from)
46
48
  # this is a bit unpleasant
47
49
 
48
- unicode_size = from.unpack('U*').size
49
- capacity = unicode_size + 1
50
- buf = UCharPointer.from_string(from, capacity)
50
+ input = UCharPointer.from_utf8(from)
51
+ input_length_in_uchars = input.length_in_uchars
52
+ capacity = input_length_in_uchars + 1
53
+ buf = UCharPointer.new(capacity)
54
+ buf.put_bytes(0, input.get_bytes(0, input.size))
51
55
  limit = FFI::MemoryPointer.new(:int32)
52
56
  text_length = FFI::MemoryPointer.new(:int32)
53
57
 
@@ -56,7 +60,7 @@ module ICU
56
60
  begin
57
61
  # resets to original size on retry
58
62
  [limit, text_length].each do |ptr|
59
- ptr.put_int32(0, unicode_size)
63
+ ptr.put_int32(0, input_length_in_uchars)
60
64
  end
61
65
 
62
66
  Lib.check_error do |error|
@@ -73,13 +77,13 @@ module ICU
73
77
  # create a new buffer with more capacity instead of resizing,
74
78
  # since the old buffer now has result data
75
79
  buf.free
76
- buf = UCharPointer.from_string(from, capacity)
80
+ buf = UCharPointer.from_utf8(from, capacity)
77
81
 
78
82
  retried = true
79
83
  retry
80
84
  end
81
85
 
82
- buf.string(text_length.get_int32(0))
86
+ buf.utf8_string(text_length.get_int32(0))
83
87
  end
84
88
  end
85
89
  end
data/lib/ffi-icu/uchar.rb CHANGED
@@ -22,6 +22,22 @@ module ICU
22
22
  ptr
23
23
  end
24
24
 
25
+ def self.from_utf8(str, capacity = nil)
26
+ chars = str.encode(Encoding::UTF_16LE).unpack('v*')
27
+
28
+ if capacity
29
+ raise(ArgumentError, "capacity is too small for string of #{chars.size} UChars") if capacity < chars.size
30
+
31
+ ptr = new(capacity)
32
+ else
33
+ ptr = new(chars.size)
34
+ end
35
+
36
+ ptr.write_array_of_uint16(chars)
37
+
38
+ ptr
39
+ end
40
+
25
41
  def initialize(size)
26
42
  super(UCHAR_TYPE, size)
27
43
  end
@@ -42,6 +58,13 @@ module ICU
42
58
  wstring.pack('U*')
43
59
  end
44
60
 
61
+ def utf8_string(length = nil)
62
+ length ||= size / TYPE_SIZE
63
+
64
+ wstring = read_array_of_uint16(length).pack('v*')
65
+ wstring.force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8)
66
+ end
67
+
45
68
  def length_in_uchars
46
69
  size / type_size
47
70
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ICU
4
- VERSION = '0.6.1'
4
+ VERSION = '0.7.0'
5
5
  end
data/lib/ffi-icu.rb CHANGED
@@ -25,11 +25,14 @@ require 'ffi-icu/lib/util'
25
25
  require 'ffi-icu/uchar'
26
26
  require 'ffi-icu/chardet'
27
27
  require 'ffi-icu/collation'
28
+ require 'ffi-icu/currency'
28
29
  require 'ffi-icu/locale'
29
30
  require 'ffi-icu/transliteration'
30
31
  require 'ffi-icu/normalization'
31
32
  require 'ffi-icu/normalizer'
32
33
  require 'ffi-icu/break_iterator'
33
34
  require 'ffi-icu/number_formatting'
35
+ require 'ffi-icu/message_formatting'
34
36
  require 'ffi-icu/time_formatting'
35
37
  require 'ffi-icu/duration_formatting'
38
+ require 'ffi-icu/list_formatting'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-icu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erick Guan
@@ -76,10 +76,13 @@ files:
76
76
  - lib/ffi-icu/break_iterator.rb
77
77
  - lib/ffi-icu/chardet.rb
78
78
  - lib/ffi-icu/collation.rb
79
+ - lib/ffi-icu/currency.rb
79
80
  - lib/ffi-icu/duration_formatting.rb
80
81
  - lib/ffi-icu/lib.rb
81
82
  - lib/ffi-icu/lib/util.rb
83
+ - lib/ffi-icu/list_formatting.rb
82
84
  - lib/ffi-icu/locale.rb
85
+ - lib/ffi-icu/message_formatting.rb
83
86
  - lib/ffi-icu/normalization.rb
84
87
  - lib/ffi-icu/normalizer.rb
85
88
  - lib/ffi-icu/number_formatting.rb