ffi-icu 0.7.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ffb113b9b98bd46355e3d6750fc1026d7732d06a8575bf2022aa073e72c532e
4
- data.tar.gz: 43b5a202de5a08790c2a257a8a05f9867df2a5b42624cab157dbf20eaa85eb37
3
+ metadata.gz: 0f0256350b6cfd290356bcab5c7e501dcc55959f2a995dac452bb43756050788
4
+ data.tar.gz: 4fe1bd3bf83133f012f6d71d07ec997ceb9abae835e8b7be4bed6d8a7ea27bb1
5
5
  SHA512:
6
- metadata.gz: 63fefe8619f8bfbad7ead9ac1ada9471074246b1a2d7c4af6cdb899250f464a07c8596bb55e836f44a128d1ce077b939404b2ad85c612298cc65a66bc1fe92f8
7
- data.tar.gz: 06e98c3af40a060bbbd2a19a9e5b95fcea49d1d75316a1f2ea639d91d88721886fe079ad3e4952ac8750cdfbf62e5dc4550b0893c3583c1896764c46d1c3fd46
6
+ metadata.gz: 8560b297f442ceb1548dc9214e7875cc050f2ca60bf9b9d7c8cc35ea1c2afcebbab52f139a294e610d6272049c2ca8de58fff1a9c8b42efded0088f7e6bf7a97
7
+ data.tar.gz: 18fd3d1b2c0f83ba1bc246381e41e72a77d8db495db2d1a0690b82c4d2cff7f00dbae88ab66f786d2d83c9445a925c1ddea2e6f7e6b235410344ac82a9a54fec
data/Gemfile CHANGED
@@ -3,11 +3,10 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
+ gem 'minitest', '~> 6.0'
7
+ gem 'minitest-mock', '~> 5.27'
6
8
  gem 'rake', '>= 12.3.3'
7
- gem 'rspec', '~> 3.13'
8
9
  gem 'rubocop', '~> 1.86'
9
10
  gem 'rubocop-minitest', '~> 0.39'
10
11
  gem 'rubocop-packaging', '~> 0.6'
11
- gem 'rubocop-rspec', '~> 3.9'
12
- gem 'rubocop-rspec_rails', '~> 2.32'
13
12
  end
data/README.md CHANGED
@@ -1,248 +1,404 @@
1
1
  # ffi-icu
2
2
 
3
- FFI wrappers for [International Components for Unicode (ICU)][icu].
4
- ICU provides comprehensive localization and security features.
5
- Majority personal computing devices, server operating systems and web browsers use ICU.
6
- ICU builds on top of Unicode's Common Locale Data Repository (CLDR).
3
+ Ruby FFI wrappers for [International Components for Unicode (ICU)][icu].
4
+ ICU provides Unicode text processing, character encoding detection, and
5
+ locale-aware formatting. Its locale data comes from the
6
+ [Common Locale Data Repository (CLDR)][cldr].
7
7
 
8
- ## Gem
8
+ ## Installation
9
9
 
10
- [Rubygem](http://rubygems.org/gems/ffi-icu "ffi-icu")
10
+ ffi-icu requires Ruby 3.2 or newer and the ICU shared libraries.
11
+ On Linux, use the ICU packages supplied by a major distribution, such as Debian,
12
+ Ubuntu, or Fedora. Install ICU from your distribution's supported repositories
13
+ before installing the gem.
11
14
 
15
+ ### Debian and Ubuntu
16
+
17
+ ```sh
18
+ sudo apt update
19
+ sudo apt install libicu-dev
12
20
  ```
21
+
22
+ The `libicu-dev` package installs the matching ICU runtime libraries as dependencies.
23
+ See the official [Debian package][debian-icu] and [Ubuntu package][ubuntu-icu] pages.
24
+
25
+ ### Fedora
26
+
27
+ ```sh
28
+ sudo dnf install libicu
29
+ ```
30
+
31
+ See the official [Fedora libicu package][fedora-icu] page.
32
+
33
+ ### Install the gem
34
+
35
+ ```sh
13
36
  gem install ffi-icu
14
37
  ```
15
38
 
16
- ## Dependencies
39
+ The gem is published on [RubyGems][rubygems]. Load it before running the examples:
17
40
 
18
- ICU.
41
+ ```ruby
42
+ require('ffi-icu')
43
+ ```
44
+
45
+ ### ICU in a custom directory
19
46
 
20
- If you get messages that the library or functions are not found, you can set some environment variables to tell ffi-icu where to find it, e.g.:
47
+ ffi-icu normally discovers the distribution's ICU libraries automatically.
48
+ If you need to override discovery, set `FFI_ICU_LIB` to the directory containing
49
+ the shared libraries before loading the gem:
21
50
 
22
51
  ```sh
23
- $ export FFI_ICU_LIB="icui18n.so"
24
- $ export FFI_ICU_VERSION_SUFFIX="_3_8"
25
- $ ruby -r ffi-icu program.rb
52
+ export FFI_ICU_LIB=/opt/icu/lib
53
+ ruby my_script.rb
26
54
  ```
27
55
 
28
- # Features
56
+ The library detects ICU's versioned function names automatically.
57
+
58
+ ## Features
59
+
60
+ Localized output, including punctuation and spaces, can vary with the installed
61
+ ICU and CLDR versions. Duration and list formatting require ICU 67 or newer.
29
62
 
30
- ## Character Encoding Detection
63
+ ### Character Encoding Detection
31
64
 
32
- Examples:
65
+ Detect an encoding and inspect the confidence score:
33
66
 
34
67
  ```ruby
35
- match = ICU::CharDet.detect(str)
68
+ match = ICU::CharDet.detect('æåø')
36
69
  match.name # => "UTF-8"
37
70
  match.confidence # => 80
38
71
  ```
39
72
 
40
- or
73
+ Reuse a detector for multiple inputs:
41
74
 
42
75
  ```ruby
43
76
  detector = ICU::CharDet::Detector.new
44
- detector.detect(str) => #<struct ICU::CharDet::Detector::Match ...>
77
+ detector.detect('æåø').name # => "UTF-8"
78
+ detector.detect_all('foo bar').is_a?(Array) # => true
45
79
  ```
46
80
 
47
- * speed
81
+ ### Locale-Aware Collation
48
82
 
49
- ## Locale Sensitive Collation
83
+ Sort strings using a locale's collation rules:
50
84
 
51
- Examples:
85
+ ```ruby
86
+ ICU::Collation.collate('nb', ['å', 'æ', 'ø']) # => ["æ", "ø", "å"]
87
+ ```
88
+
89
+ Reuse a collator to compare and sort strings:
52
90
 
53
91
  ```ruby
54
- ICU::Collation.collate("nb", %w[å æ ø]) == %w[æ ø å] #=> true
92
+ collator = ICU::Collation::Collator.new('nb')
93
+ collator.compare('a', 'b') # => -1
94
+ collator.greater?('z', 'a') # => true
95
+ collator.collate(['å', 'æ', 'ø']) # => ["æ", "ø", "å"]
55
96
  ```
56
97
 
57
- or
98
+ ### Text Boundary Analysis
99
+
100
+ Find word boundaries or extract the substrings between them:
58
101
 
59
102
  ```ruby
60
- collator = ICU::Collation::Collator.new("nb")
61
- collator.compare("a", "b") #=> -1
62
- collator.greater?("z", "a") #=> true
63
- collator.collate(%w[å æ ø]) #=> ["æ", "ø", "å"]
103
+ iterator = ICU::BreakIterator.new(:word, 'en_US')
104
+ iterator.text = 'This is a sentence.'
105
+ iterator.to_a # => [0, 4, 5, 7, 8, 9, 10, 18, 19]
106
+ iterator.substrings # => ["This", " ", "is", " ", "a", " ", "sentence", "."]
64
107
  ```
65
108
 
66
- ## Text Boundary Analysis
109
+ Boundary offsets use UTF-16 code units. Use `substrings` to extract text without
110
+ converting those offsets to Ruby string indexes.
111
+
112
+ ### Number and Currency Formatting
67
113
 
68
- Examples:
114
+ Format numbers, percentages, and currency amounts:
69
115
 
70
116
  ```ruby
71
- iterator = ICU::BreakIterator.new(:word, "en_US")
72
- iterator.text = "This is a sentence."
73
- iterator.to_a #=> [0, 4, 5, 7, 8, 9, 10, 18, 19]
117
+ ICU::NumberFormatting.format_number('en', 1_000) # => "1,000"
118
+ ICU::NumberFormatting.format_number('de-DE', 1_234.56) # => "1.234,56"
119
+ ICU::NumberFormatting.format_currency('en', 123.45, 'USD') # => "$123.45"
120
+ ICU::NumberFormatting.format_percent('en', 0.53) # => "53%"
121
+ ICU::NumberFormatting.spell('en_US', 1_000) # => "one thousand"
122
+ ICU::NumberFormatting.format_number('en', BigDecimal('10000.123')) # => "10,000.123"
74
123
  ```
75
124
 
76
- ## Number/Currency Formatting
125
+ Reuse a formatter for repeated calls. The French Canadian example uses a
126
+ non-breaking space (`\u00A0`) as its thousands separator:
77
127
 
78
- Examples:
128
+ ```ruby
129
+ number_formatter = ICU::NumberFormatting.create('fr-CA')
130
+ number_formatter.format(1_000) # => "1\u00A0000"
131
+ number_formatter.format(1_000.123) # => "1\u00A0000,123"
132
+
133
+ currency_formatter = ICU::NumberFormatting.create('en-US', :currency)
134
+ currency_formatter.format(1_234.56, 'USD') # => "$1,234.56"
135
+ ```
136
+
137
+ For anything the presets above do not cover, pass an
138
+ [ICU number skeleton](https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html)
139
+ (ICU >= 62):
79
140
 
80
141
  ```ruby
81
- # class method interface
82
- ICU::NumberFormatting.format_number("en", 1_000) #=> "1,000"
83
- ICU::NumberFormatting.format_number("de-DE", 1234.56) #=> "1.234,56"
84
- ICU::NumberFormatting.format_currency("en", 123.45, 'USD') #=> "$123.45"
85
- ICU::NumberFormatting.format_percent("en", 0.53, 'USD') #=> "53%"
86
- ICU::NumberFormatting.spell("en_US", 1_000) #=> "one thousand"
142
+ ICU::NumberFormatting.format_with_skeleton('en', 1_234, 'compact-short') # => "1.2K"
143
+ ICU::NumberFormatting.format_with_skeleton('en', 0.1234, 'percent scale/100 .0') # => "12.3%"
144
+
145
+ formatter = ICU::NumberFormatting.create_with_skeleton('en', 'precision-integer')
146
+ formatter.format(999.6) # => "1,000"
147
+ ```
148
+
149
+ ### Currency Metadata
87
150
 
88
- # reusable formatting objects
89
- numf = ICU::NumberFormatting.create('fr-CA')
90
- numf.format(1000) #=> "1 000"
151
+ Look up ISO 4217 codes, localized names, symbols, and decimal digits:
91
152
 
92
- curf = ICU::NumberFormatting.create('en-US', :currency)
93
- curf.format(1234.56, 'USD') #=> "$1,234.56"
153
+ ```ruby
154
+ ICU::Currency.for_locale('en_US') # => "USD"
155
+ ICU::Currency.for_locale('de_DE') # => "EUR"
156
+ ICU::Currency.available(:common).include?('USD') # => true
157
+
158
+ usd = ICU::Currency.new('usd')
159
+ usd.code # => "USD"
160
+ usd.symbol('en_US') # => "$"
161
+ usd.name('en') # => "US Dollar"
162
+ usd.plural_name('en', 'other') # => "US dollars"
163
+ usd.fraction_digits # => 2
164
+ usd.fraction_digits(:cash) # => 2
165
+ usd.numeric_code # => 840
166
+
167
+ ICU::Currency.new('JPY').fraction_digits # => 0
168
+ ICU::Currency.symbol('EUR', 'en_US') # => "€"
94
169
  ```
95
170
 
96
- ## Message Formatting
171
+ `narrow_symbol(locale)` requires ICU 61 or newer. `numeric_code` requires ICU 49 or newer.
97
172
 
98
- Message formatting supports ICU MessageFormat patterns using numbered arguments:
173
+ ### Message Formatting
174
+
175
+ Use numbered arguments for ICU MessageFormat patterns, including plurals and selects:
99
176
 
100
177
  ```ruby
101
178
  formatter = ICU::MessageFormatting.create(
102
179
  '{0, plural, one {# item} other {# items}}',
103
180
  locale: 'en-US'
104
181
  )
182
+ formatter.format([1]) # => "1 item"
183
+ formatter.format([2]) # => "2 items"
105
184
 
106
- formatter.format([2]) #=> "2 items"
185
+ ICU::MessageFormatting.format('Hello, {0}!', locale: 'en-US', arguments: ['Ada'])
186
+ # => "Hello, Ada!"
107
187
  ```
108
188
 
109
- Patterns can also be validated without formatting a message:
189
+ Validate a pattern before formatting:
110
190
 
111
191
  ```ruby
112
- ICU::MessageFormatting.validate('{0, select, male {He} female {She} other {They}}', locale: 'en-US')
113
- #=> true
192
+ pattern = '{0, select, male {He} female {She} other {They}}'
193
+ ICU::MessageFormatting.validate(pattern, locale: 'en-US') # => true
194
+ ICU::MessageFormatting.valid?('{0, plural', locale: 'en-US') # => false
114
195
  ```
115
196
 
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`.
197
+ `validate` raises `ICU::MessageFormatting::InvalidPatternError` for an invalid
198
+ pattern. The error exposes the pattern, locale, line, and offset. Use `valid?`
199
+ when you want a boolean result.
117
200
 
118
- ## Time Formatting/Parsing
201
+ Arguments must be an array of numbered values. Supported types are `String`,
202
+ `Integer`, `Float`, and `BigDecimal`. Use strings for simple and select arguments,
203
+ and numbers for numeric arguments. Named arguments and rich-text tags are not supported.
119
204
 
120
- Examples:
205
+ ### Time Formatting and Parsing
121
206
 
122
- ```ruby
123
- # class method interface
124
- f = ICU::TimeFormatting.format(Time.mktime(2015, 11, 12, 15, 21, 16), {:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :short, :time => :short})
125
- f #=> "12.11.15 15:21"
207
+ Set the time zone explicitly for predictable output:
126
208
 
127
- # reusable formatting objects
128
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :long, :time => :none)
129
- formatter.format(Time.now) #=> "25. února 2015"
209
+ ```ruby
210
+ time = Time.utc(2015, 11, 12, 14, 21)
211
+ ICU::TimeFormatting.format(
212
+ time,
213
+ locale: 'cs_CZ', zone: 'Europe/Prague', date: :short, time: :short
214
+ )
215
+ # => "12.11.15 15:21"
130
216
  ```
131
217
 
218
+ Reuse a formatter for formatting and parsing:
219
+
132
220
  ```ruby
133
- # reusable formatting objects
134
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :long, :time => :none)
135
- formatter.parse("25. února 2015") #=> Wed Feb 25 00:00:00 +0100 2015
221
+ formatter = ICU::TimeFormatting.create(
222
+ locale: 'cs_CZ', zone: 'Europe/Prague', date: :long, time: :none
223
+ )
224
+ formatter.format(Time.utc(2015, 2, 25)) # => "25. února 2015"
225
+ formatter.parse('25. února 2015').utc.to_s # => "2015-02-24 23:00:00 UTC"
136
226
  ```
137
227
 
138
- For skeleton formatting, visit the [Unicode date field symbol table](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table) page to help find the pattern characters to use.
228
+ Use a skeleton to request date fields and let ICU choose the locale's pattern.
229
+ See the [Unicode date field symbol table][date-fields] for supported symbols:
139
230
 
140
231
  ```ruby
141
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :date => :pattern, :time => :pattern, :skeleton => 'MMMMY')
142
- formatter.format(Time.now) #=> "únor 2015"
143
-
144
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :date => :pattern, :time => :pattern, :skeleton => 'Y')
145
- formatter.format(Time.now) #=> "2015"
232
+ formatter = ICU::TimeFormatting.create(
233
+ locale: 'cs_CZ', zone: 'UTC', date: :pattern, time: :pattern, skeleton: 'MMMMy'
234
+ )
235
+ formatter.format(Time.utc(2015, 2, 25)) # => "únor 2015"
146
236
  ```
147
237
 
148
- ## Duration Formatting
238
+ Choose an hour cycle with `h11`, `h12`, `h23`, or `h24`, or use `:locale` for the
239
+ locale's default:
149
240
 
150
241
  ```ruby
151
- # What the various styles look like
152
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
153
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8 hours, 40 minutes, 35 seconds"
242
+ ICU::TimeFormatting.format(
243
+ Time.utc(2021, 4, 1, 12, 5),
244
+ locale: 'en_AU', zone: 'UTC', date: :none, time: :short, hour_cycle: 'h23'
245
+ )
246
+ # => "12:05"
247
+ ```
154
248
 
155
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :short)
156
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8 hrs, 40 mins, 35 secs"
249
+ ### Duration Formatting
157
250
 
158
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :narrow)
159
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8h 40min. 35s."
160
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
161
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8:40:35"
251
+ Duration formatting requires ICU 67 or newer. Choose `:long`, `:short`, `:narrow`,
252
+ or `:digital`:
162
253
 
163
- # How digital & non-digital formats deal with units > hours
164
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :narrow)
165
- formatter.format({days: 2, hours: 8, minutes: 40, seconds: 35}) #=> "2d 8h 40min. 35s."
166
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
167
- formatter.format({days: 2, hours: 8, minutes: 40, seconds: 35}) #=> "2d 8:40:35"
254
+ ```ruby
255
+ fields = { hours: 8, minutes: 40, seconds: 35 }
256
+ ICU::DurationFormatting.format(fields, locale: 'en-AU', style: :long)
257
+ # => "8 hours, 40 minutes, 35 seconds"
258
+ ICU::DurationFormatting.format(fields, locale: 'en-AU', style: :short)
259
+ # => "8 hrs, 40 mins, 35 secs"
260
+ ICU::DurationFormatting.format(fields, locale: 'en-AU', style: :narrow)
261
+ # => "8h 40m 35s"
262
+ ICU::DurationFormatting.format(fields, locale: 'en-AU', style: :digital)
263
+ # => "8:40:35"
264
+ ```
168
265
 
169
- # Missing or zero parts are omitted
170
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
171
- formatter.format({days: 2, minutes: 40, seconds:0}) #=> "2 days, 40 minutes"
266
+ Reuse a formatter when the locale and style stay the same. Larger units remain
267
+ outside the digital clock, and missing or zero fields are omitted:
172
268
 
269
+ ```ruby
173
270
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
174
- formatter.format({hours: 2, minutes: 40}) #=> "2:40"
271
+ formatter.format({ days: 2, hours: 8, minutes: 40, seconds: 35 }) # => "2d 8:40:35"
272
+ formatter.format({ hours: 2, minutes: 40 }) # => "2:40"
273
+ formatter.format({ minutes: 40, seconds: 7 }) # => "40:07"
175
274
 
176
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
177
- formatter.format({minutes: 40, seconds: 7}) #=> "40:07"
275
+ formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
276
+ formatter.format({ days: 2, minutes: 40, seconds: 0 }) # => "2 days, 40 minutes"
277
+ ```
178
278
 
179
- # Sub-second parts are folded into seconds for digital display
180
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
181
- formatter.format({hours: 5, minutes: 7, seconds: 23, milliseconds: 98, microseconds: 997}) #=> "5:07:23.098997"
279
+ Digital formatting folds milliseconds, microseconds, and nanoseconds into seconds.
280
+ Supplying a smaller unit preserves its trailing zeros:
182
281
 
183
- # Zero-extension of sub-second parts in digital style
184
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
185
- formatter.format({hours: 5, minutes: 7, seconds: 23, milliseconds: 400}) #=> "5:07:23.400"
282
+ ```ruby
186
283
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
187
- formatter.format({hours: 5, minutes: 7, seconds: 23, milliseconds: 400, microseconds: 700}) #=> "5:07:23.400700"
284
+ formatter.format({ hours: 5, minutes: 7, seconds: 23, milliseconds: 98, microseconds: 997 })
285
+ # => "5:07:23.098997"
286
+ formatter.format({ hours: 5, minutes: 7, seconds: 23, milliseconds: 400 })
287
+ # => "5:07:23.400"
288
+ formatter.format({ hours: 5, minutes: 7, seconds: 23, milliseconds: 400, microseconds: 700 })
289
+ # => "5:07:23.400700"
290
+ ```
291
+
292
+ Fractional parts are truncated except on the smallest supplied unit, which must
293
+ be seconds or smaller to retain a fraction:
188
294
 
189
- # All fractional parts except the last are truncated
295
+ ```ruby
190
296
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
191
- formatter.format({days: 2, hours: 7.3, minutes: 40.9, seconds:0.43}) #=> "2 days, 7 hours, 40 minutes, 0.43 seconds"
297
+ formatter.format({ days: 2, hours: 7.3, minutes: 40.9, seconds: 0.43 })
298
+ # => "2 days, 7 hours, 40 minutes, 0.43 seconds"
299
+ ```
192
300
 
193
- # With RU locale
194
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :long)
195
- formatter.format({hours: 1, minutes: 2, seconds: 3}) #=> "1 час 2 минуты 3 секунды"
301
+ Unit names and separators follow the requested locale:
302
+
303
+ ```ruby
196
304
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :long)
197
- formatter.format({hours: 10, minutes: 20, seconds: 30}) #=> "10 часов 20 минут 30 секунд"
198
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :narrow)
199
- formatter.format({hours: 1, minutes: 2, seconds: 3}) #=> "1 ч 2 мин 3 с"
305
+ formatter.format({ hours: 1, minutes: 2, seconds: 3 }) # => "1 час 2 минуты 3 секунды"
306
+ formatter.format({ hours: 10, minutes: 20, seconds: 30 }) # => "10 часов 20 минут 30 секунд"
307
+
200
308
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :narrow)
201
- formatter.format({hours: 10, minutes: 20, seconds: 30}) #=> "10 ч 20 мин 30 с"
309
+ formatter.format({ hours: 1, minutes: 2, seconds: 3 }) # => "1 ч 2 мин 3 с"
202
310
  ```
203
311
 
204
- ## Transliteration
312
+ ### Transliteration
205
313
 
206
- Example:
314
+ Convert between scripts or reuse a transliterator for multiple inputs:
207
315
 
208
316
  ```ruby
209
317
  ICU::Transliteration.transliterate('Traditional-Simplified', '沈從文') # => "沈从文"
318
+
319
+ transliterator = ICU::Transliteration::Transliterator.new('Latin-ASCII')
320
+ transliterator.transliterate('café') # => "cafe"
210
321
  ```
211
322
 
212
- ## List Formatting
323
+ Apply a sequence of custom transliteration rules:
213
324
 
214
- Join items with locale-aware separators and conjunctions:
325
+ ```ruby
326
+ ICU::Transliteration.translit('NFD; [:Nonspacing Mark:] Remove; NFC', 'âêîôû') # => "aeiou"
327
+ ```
328
+
329
+ ### Unicode Normalization
330
+
331
+ Compose canonically equivalent text into NFC, or check whether it is already normalized:
332
+
333
+ ```ruby
334
+ normalizer = ICU::Normalizer.new(nil, 'nfc', :compose)
335
+ normalizer.normalize("A\u030A") # => "Å"
336
+ normalizer.normalized?('Å') # => true
337
+ normalizer.normalized?("A\u030A") # => false
338
+ ```
339
+
340
+ Use `:decompose` with `'nfc'` for NFD. Use `'nfkc'` with `:compose` or `:decompose`
341
+ for compatibility normalization (NFKC or NFKD):
215
342
 
216
343
  ```ruby
217
- ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'en', style: :and)
218
- # => "Alice, Bob, and Jane"
344
+ normalizer = ICU::Normalizer.new(nil, 'nfkc', :compose)
345
+ normalizer.normalize('ffi') # => "ffi"
346
+ ```
347
+
348
+ A convenience method also supports one-off normalization:
219
349
 
220
- ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'fr', style: :and)
221
- # => "Alice, Bob et Jane"
350
+ ```ruby
351
+ ICU::Normalization.normalize('Å', :nfd) # => "A\u030A"
352
+ ICU::Normalization.normalize("A\u030A", :nfc) # => "Å"
222
353
  ```
223
354
 
224
- Supported styles are:
355
+ ### List Formatting
225
356
 
226
- - `:and`: the default
227
- - `:or`
228
- - `:unit`: ICU's units list type
357
+ List formatting requires ICU 67 or newer. Join strings with locale-aware
358
+ separators and conjunctions:
229
359
 
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.
360
+ ```ruby
361
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'en') # => "Alice, Bob, and Jane"
362
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'fr', style: :and) # => "Alice, Bob et Jane"
363
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'en', style: :or) # => "Alice, Bob, or Jane"
364
+ ICU::ListFormatting.format(['Alice', 'Bob', 'Jane'], locale: 'en', style: :unit) # => "Alice, Bob, Jane"
365
+ ```
231
366
 
232
- ## Locale
367
+ The default style is `:and`. Styles must be symbols, and all items must be strings.
368
+ The formatter uses ICU's wide list width.
233
369
 
234
- Examples:
370
+ ### Locale
371
+
372
+ Look up localized names and choose display-name contexts:
235
373
 
236
374
  ```ruby
237
375
  locale = ICU::Locale.new('en-US')
238
- locale.display_country('en-US') #=> "United States"
239
- locale.display_language('es') #=> "inglés"
240
- locale.display_name('es') #=> "inglés (Estados Unidos)"
241
- locale.display_name_with_context('en-US', [:length_short]) #=> "English (US)"
242
- locale.display_name_with_context('en-US', [:length_full]) #=> "English (United States)"
376
+ locale.display_country('en-US') # => "United States"
377
+ locale.display_language('es') # => "inglés"
378
+ locale.display_name('es') # => "inglés (Estados Unidos)"
379
+ locale.display_name_with_context('en-US', [:length_short]) # => "English (US)"
380
+ locale.display_name_with_context('en-US', [:length_full]) # => "English (United States)"
381
+
382
+ ICU::Locale.new('fa-AF').display_name_with_context('en', [:dialect_names]) # => "Dari"
383
+ ```
384
+
385
+ Convert BCP 47 language tags to ICU locale IDs or add likely subtags:
243
386
 
244
- dari = ICU::Locale.new('fa-AF')
245
- dari.display_name_with_context('en', [:dialect_names]) #=> "Dari"
387
+ ```ruby
388
+ locale = ICU::Locale.for_language_tag('en-US')
389
+ locale.id # => "en_US"
390
+ locale.to_language_tag # => "en-US"
391
+ ICU::Locale.new('en').with_likely_subtags.to_s # => "en_Latn_US"
246
392
  ```
247
393
 
394
+ ## Benchmarks
395
+
396
+ Benchmarks live in [`benchmark/`](benchmark/README.md) with a separate Gemfile.
397
+
248
398
  [icu]: https://github.com/unicode-org/icu
399
+ [cldr]: https://cldr.unicode.org/
400
+ [rubygems]: https://rubygems.org/gems/ffi-icu
401
+ [debian-icu]: https://packages.debian.org/stable/libicu-dev
402
+ [ubuntu-icu]: https://packages.ubuntu.com/search?keywords=libicu-dev
403
+ [fedora-icu]: https://packages.fedoraproject.org/pkgs/icu/libicu/
404
+ [date-fields]: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table
data/Rakefile CHANGED
@@ -4,18 +4,14 @@ require 'rake'
4
4
  require 'bundler'
5
5
  Bundler::GemHelper.install_tasks
6
6
 
7
- require 'rspec/core/rake_task'
8
- RSpec::Core::RakeTask.new(:spec) do |spec|
9
- spec.pattern = 'spec/**/*_spec.rb'
10
- spec.rspec_opts = '--backtrace'
7
+ require 'rake/testtask'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << 'test' << 'lib'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ t.warning = false
11
12
  end
12
13
 
13
- RSpec::Core::RakeTask.new(:rcov) do |spec|
14
- spec.pattern = 'spec/**/*_spec.rb'
15
- spec.rcov = true
16
- end
17
-
18
- task default: :spec
14
+ task default: :test
19
15
 
20
16
  begin
21
17
  require('yard')
@@ -262,32 +262,8 @@ 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_utf8(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)
data/lib/ffi-icu/lib.rb CHANGED
@@ -497,22 +497,27 @@ module ICU
497
497
  attach_function :ubrk_following, "ubrk_following#{suffix}", [:pointer, :int32_t], :int32_t
498
498
  attach_function :ubrk_isBoundary, "ubrk_isBoundary#{suffix}", [:pointer, :int32_t], :int32_t
499
499
 
500
+ # UNUM_FORMAT_STYLE_COUNT is deprecated: "The numeric value may change over time, see ICU ticket #12420."
500
501
  enum :number_format_style, [
501
- :pattern_decimal,
502
- :decimal,
503
- :currency,
504
- :percent,
505
- :scientific,
506
- :spellout,
507
- :ordinal,
508
- :duration,
509
- :numbering_system,
510
- :pattern_rule_based,
511
- :currency_iso,
512
- :currency_plural,
513
- :format_style_count,
514
- :default,
515
- :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
516
521
  ]
517
522
  enum :number_format_attribute, [
518
523
  :parse_int_only, :grouping_used, :decimal_always_show, :max_integer_digits, :min_integer_digits, :integer_digits,
@@ -34,8 +34,8 @@ module ICU
34
34
  create(pattern, locale:).format(arguments)
35
35
  end
36
36
 
37
- # rubocop:disable-next Naming/PredicateMethod
38
- def self.validate(pattern, locale:)
37
+ # Raises on invalid patterns; use valid? for a predicate.
38
+ def self.validate(pattern, locale:) # rubocop:disable Naming/PredicateMethod
39
39
  create(pattern, locale:)
40
40
  true
41
41
  end
@@ -15,6 +15,10 @@ module ICU
15
15
  end
16
16
  end
17
17
 
18
+ def self.create_with_skeleton(locale, skeleton)
19
+ SkeletonFormatter.new(locale, skeleton)
20
+ end
21
+
18
22
  def self.clear_default_options
19
23
  @default_options.clear
20
24
  end
@@ -35,6 +39,10 @@ module ICU
35
39
  create(locale, :currency, options).format(number, currency)
36
40
  end
37
41
 
42
+ def self.format_with_skeleton(locale, number, skeleton)
43
+ create_with_skeleton(locale, skeleton).format(number)
44
+ end
45
+
38
46
  def self.spell(locale, number, options = {})
39
47
  create(locale, :spellout, options).format(number)
40
48
  end
@@ -48,6 +56,11 @@ module ICU
48
56
  private
49
57
 
50
58
  def make_formatter(type, locale)
59
+ if type == :pattern_rule_based
60
+ raise(ArgumentError, 'The pattern_rule_based style needs custom rules, which ffi-icu does not support; ' \
61
+ 'ICU::NumberFormatting.create_with_skeleton covers most other formats')
62
+ end
63
+
51
64
  ptr = Lib.check_error do |error|
52
65
  Lib.unum_open(type, FFI::MemoryPointer.new(4), 0, locale, FFI::MemoryPointer.new(4), error)
53
66
  end
@@ -149,5 +162,33 @@ module ICU
149
162
  end
150
163
  end
151
164
  end
165
+
166
+ class SkeletonFormatter
167
+ def initialize(locale, skeleton)
168
+ unless Lib.respond_to?(:unumf_openForSkeletonAndLocale)
169
+ raise('ICU::NumberFormatting::SkeletonFormatter requires ICU >= 62')
170
+ end
171
+
172
+ skeleton_uchar = UCharPointer.from_utf8(skeleton)
173
+ @f = FFI::AutoPointer.new(
174
+ Lib.check_error do |error|
175
+ Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars, locale, error)
176
+ end,
177
+ Lib.method(:unumf_close)
178
+ )
179
+ end
180
+
181
+ def format(number)
182
+ decimal = number.to_s
183
+ result = FFI::AutoPointer.new(
184
+ Lib.check_error { |error| Lib.unumf_openResult(error) },
185
+ Lib.method(:unumf_closeResult)
186
+ )
187
+ Lib.check_error { |error| Lib.unumf_formatDecimal(@f, decimal, decimal.bytesize, result, error) }
188
+ Lib::Util.read_uchar_buffer(0) do |buffer, error|
189
+ Lib.unumf_resultToString(result, buffer, buffer.length_in_uchars, error)
190
+ end
191
+ end
192
+ end
152
193
  end
153
194
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ICU
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erick Guan