ffi-icu 0.6.1 → 0.8.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.
@@ -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
@@ -262,36 +262,12 @@ module ICU
262
262
  result.string
263
263
  end
264
264
 
265
- def number_formatter(skeleton)
266
- @number_formatters[skeleton] ||= begin
267
- skeleton_uchar = UCharPointer.from_string(skeleton)
268
- FFI::AutoPointer.new(
269
- Lib.check_error do |error|
270
- Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars,
271
- @locale, error)
272
- end,
273
- Lib.method(:unumf_close)
274
- )
275
- end
276
- end
277
-
278
265
  def format_number(value, skeleton)
279
- formatter = number_formatter(skeleton)
280
- result = FFI::AutoPointer.new(
281
- Lib.check_error { |error| Lib.unumf_openResult(error) },
282
- Lib.method(:unumf_closeResult)
283
- )
284
- value_str = value.to_s
285
- Lib.check_error do |error|
286
- Lib.unumf_formatDecimal(formatter, value_str, value_str.size, result, error)
287
- end
288
- Lib::Util.read_uchar_buffer(0) do |buf, error|
289
- Lib.unumf_resultToString(result, buf, buf.length_in_uchars, error)
290
- end
266
+ (@number_formatters[skeleton] ||= NumberFormatting::SkeletonFormatter.new(@locale, skeleton)).format(value)
291
267
  end
292
268
 
293
269
  def format_list(values)
294
- value_uchars = values.map(&UCharPointer.method(:from_string))
270
+ value_uchars = values.map(&UCharPointer.method(:from_utf8))
295
271
  value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size)
296
272
  value_uchars_array.put_array_of_pointer(0, value_uchars)
297
273
  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,
@@ -444,22 +497,27 @@ module ICU
444
497
  attach_function :ubrk_following, "ubrk_following#{suffix}", [:pointer, :int32_t], :int32_t
445
498
  attach_function :ubrk_isBoundary, "ubrk_isBoundary#{suffix}", [:pointer, :int32_t], :int32_t
446
499
 
500
+ # UNUM_FORMAT_STYLE_COUNT is deprecated: "The numeric value may change over time, see ICU ticket #12420."
447
501
  enum :number_format_style, [
448
- :pattern_decimal,
449
- :decimal,
450
- :currency,
451
- :percent,
452
- :scientific,
453
- :spellout,
454
- :ordinal,
455
- :duration,
456
- :numbering_system,
457
- :pattern_rule_based,
458
- :currency_iso,
459
- :currency_plural,
460
- :format_style_count,
461
- :default,
462
- :ignore
502
+ :pattern_decimal, 0,
503
+ :decimal, 1,
504
+ :currency, 2,
505
+ :percent, 3,
506
+ :scientific, 4,
507
+ :spellout, 5,
508
+ :ordinal, 6,
509
+ :duration, 7,
510
+ :numbering_system, 8,
511
+ :pattern_rule_based, 9,
512
+ :currency_iso, 10,
513
+ :currency_plural, 11,
514
+ :currency_accounting, 12,
515
+ :cash_currency, 13,
516
+ :decimal_compact_short, 14,
517
+ :decimal_compact_long, 15,
518
+ :currency_standard, 16,
519
+ :default, 1, # UNUM_DEFAULT = UNUM_DECIMAL
520
+ :ignore, 0 # UNUM_IGNORE = UNUM_PATTERN_DECIMAL
463
521
  ]
464
522
  enum :number_format_attribute, [
465
523
  :parse_int_only, :grouping_used, :decimal_always_show, :max_integer_digits, :min_integer_digits, :integer_digits,
@@ -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
+ # Raises on invalid patterns; use valid? for a predicate.
38
+ def self.validate(pattern, locale:) # rubocop:disable Naming/PredicateMethod
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