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.
- checksums.yaml +4 -4
- data/Gemfile +2 -3
- data/README.md +308 -107
- data/Rakefile +6 -10
- data/lib/ffi-icu/break_iterator.rb +24 -5
- data/lib/ffi-icu/collation.rb +20 -15
- data/lib/ffi-icu/currency.rb +123 -0
- data/lib/ffi-icu/duration_formatting.rb +2 -26
- data/lib/ffi-icu/lib/util.rb +1 -1
- data/lib/ffi-icu/lib.rb +82 -24
- data/lib/ffi-icu/list_formatting.rb +52 -0
- data/lib/ffi-icu/locale.rb +1 -0
- data/lib/ffi-icu/message_formatting.rb +217 -0
- data/lib/ffi-icu/normalization.rb +5 -5
- data/lib/ffi-icu/normalizer.rb +11 -12
- data/lib/ffi-icu/number_formatting.rb +44 -3
- data/lib/ffi-icu/time_formatting.rb +12 -12
- data/lib/ffi-icu/transliteration.rb +14 -10
- data/lib/ffi-icu/uchar.rb +23 -0
- data/lib/ffi-icu/version.rb +1 -1
- data/lib/ffi-icu.rb +3 -0
- metadata +4 -1
data/lib/ffi-icu/collation.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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).
|
|
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.
|
|
119
|
-
size = Lib.ucol_getSortKey(@c, ptr,
|
|
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,
|
|
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
|
-
|
|
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(:
|
|
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)
|
data/lib/ffi-icu/lib/util.rb
CHANGED
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
|
-
] +
|
|
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
|
-
|
|
32
|
-
File.expand_path(File.join(path, lib))
|
|
33
|
-
end
|
|
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, :
|
|
357
|
-
:post_context, :
|
|
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
|
-
:
|
|
461
|
-
:
|
|
462
|
-
:
|
|
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
|
data/lib/ffi-icu/locale.rb
CHANGED
|
@@ -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
|