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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f219be994f4a69799cf8b848c0909786476face806e223c89f8f4b455ef9b85
4
- data.tar.gz: 562ceb9c82764d4251566107cadaabe7d0f63c69f012a0771c6ebf2d75677252
3
+ metadata.gz: 0f0256350b6cfd290356bcab5c7e501dcc55959f2a995dac452bb43756050788
4
+ data.tar.gz: 4fe1bd3bf83133f012f6d71d07ec997ceb9abae835e8b7be4bed6d8a7ea27bb1
5
5
  SHA512:
6
- metadata.gz: ca6afe1397a8879f89303ed2b9866caaa906d0b4271b4fb75f8eb8a51e8e9a482f8b398072f13a85e5d790e9b261a7ab3178dda2c2a35a5dceb575fd798b5679
7
- data.tar.gz: 9f722bd3b1248aeac053d74230ce9c98a9cde2d1e925a86284179880d969ec7d1fe59dbabf579306e9f63101bb013198ba6c637e4aa7e561a48b7995451e4411
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,203 +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
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
12
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:
40
+
41
+ ```ruby
42
+ require('ffi-icu')
43
+ ```
17
44
 
18
- ICU.
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
29
59
 
30
- ## Character Encoding Detection
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.
31
62
 
32
- Examples:
63
+ ### Character Encoding Detection
64
+
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
82
+
83
+ Sort strings using a locale's collation rules:
48
84
 
49
- ## Locale Sensitive Collation
85
+ ```ruby
86
+ ICU::Collation.collate('nb', ['å', 'æ', 'ø']) # => ["æ", "ø", "å"]
87
+ ```
50
88
 
51
- Examples:
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
+ ```
87
148
 
88
- # reusable formatting objects
89
- numf = ICU::NumberFormatting.create('fr-CA')
90
- numf.format(1000) #=> "1 000"
149
+ ### Currency Metadata
91
150
 
92
- curf = ICU::NumberFormatting.create('en-US', :currency)
93
- curf.format(1234.56, 'USD') #=> "$1,234.56"
151
+ Look up ISO 4217 codes, localized names, symbols, and decimal digits:
152
+
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
- ## Time Formatting/Parsing
171
+ `narrow_symbol(locale)` requires ICU 61 or newer. `numeric_code` requires ICU 49 or newer.
172
+
173
+ ### Message Formatting
97
174
 
98
- Examples:
175
+ Use numbered arguments for ICU MessageFormat patterns, including plurals and selects:
99
176
 
100
177
  ```ruby
101
- # class method interface
102
- f = ICU::TimeFormatting.format(Time.mktime(2015, 11, 12, 15, 21, 16), {:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :short, :time => :short})
103
- f #=> "12.11.15 15:21"
178
+ formatter = ICU::MessageFormatting.create(
179
+ '{0, plural, one {# item} other {# items}}',
180
+ locale: 'en-US'
181
+ )
182
+ formatter.format([1]) # => "1 item"
183
+ formatter.format([2]) # => "2 items"
184
+
185
+ ICU::MessageFormatting.format('Hello, {0}!', locale: 'en-US', arguments: ['Ada'])
186
+ # => "Hello, Ada!"
187
+ ```
188
+
189
+ Validate a pattern before formatting:
104
190
 
105
- # reusable formatting objects
106
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :long, :time => :none)
107
- formatter.format(Time.now) #=> "25. února 2015"
191
+ ```ruby
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
108
195
  ```
109
196
 
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.
200
+
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.
204
+
205
+ ### Time Formatting and Parsing
206
+
207
+ Set the time zone explicitly for predictable output:
208
+
110
209
  ```ruby
111
- # reusable formatting objects
112
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :long, :time => :none)
113
- formatter.parse("25. února 2015") #=> Wed Feb 25 00:00:00 +0100 2015
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"
114
216
  ```
115
217
 
116
- 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.
218
+ Reuse a formatter for formatting and parsing:
117
219
 
118
220
  ```ruby
119
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :date => :pattern, :time => :pattern, :skeleton => 'MMMMY')
120
- formatter.format(Time.now) #=> "únor 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"
226
+ ```
227
+
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:
121
230
 
122
- formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :date => :pattern, :time => :pattern, :skeleton => 'Y')
123
- formatter.format(Time.now) #=> "2015"
231
+ ```ruby
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"
124
236
  ```
125
237
 
126
- ## Duration Formatting
238
+ Choose an hour cycle with `h11`, `h12`, `h23`, or `h24`, or use `:locale` for the
239
+ locale's default:
127
240
 
128
241
  ```ruby
129
- # What the various styles look like
130
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
131
- 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
+ ```
132
248
 
133
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :short)
134
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8 hrs, 40 mins, 35 secs"
249
+ ### Duration Formatting
135
250
 
136
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :narrow)
137
- formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8h 40min. 35s."
138
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
139
- 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`:
140
253
 
141
- # How digital & non-digital formats deal with units > hours
142
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :narrow)
143
- formatter.format({days: 2, hours: 8, minutes: 40, seconds: 35}) #=> "2d 8h 40min. 35s."
144
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
145
- 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
+ ```
146
265
 
147
- # Missing or zero parts are omitted
148
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
149
- 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:
150
268
 
269
+ ```ruby
151
270
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
152
- 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"
153
274
 
154
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
155
- 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
+ ```
156
278
 
157
- # Sub-second parts are folded into seconds for digital display
158
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
159
- 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:
160
281
 
161
- # Zero-extension of sub-second parts in digital style
162
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
163
- formatter.format({hours: 5, minutes: 7, seconds: 23, milliseconds: 400}) #=> "5:07:23.400"
282
+ ```ruby
164
283
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :digital)
165
- 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:
166
294
 
167
- # All fractional parts except the last are truncated
295
+ ```ruby
168
296
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'en-AU', style: :long)
169
- 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
+ ```
170
300
 
171
- # With RU locale
172
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :long)
173
- formatter.format({hours: 1, minutes: 2, seconds: 3}) #=> "1 час 2 минуты 3 секунды"
301
+ Unit names and separators follow the requested locale:
302
+
303
+ ```ruby
174
304
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :long)
175
- formatter.format({hours: 10, minutes: 20, seconds: 30}) #=> "10 часов 20 минут 30 секунд"
176
- formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :narrow)
177
- 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
+
178
308
  formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: :narrow)
179
- formatter.format({hours: 10, minutes: 20, seconds: 30}) #=> "10 ч 20 мин 30 с"
309
+ formatter.format({ hours: 1, minutes: 2, seconds: 3 }) # => "1 ч 2 мин 3 с"
180
310
  ```
181
311
 
182
- ## Transliteration
312
+ ### Transliteration
183
313
 
184
- Example:
314
+ Convert between scripts or reuse a transliterator for multiple inputs:
185
315
 
186
316
  ```ruby
187
317
  ICU::Transliteration.transliterate('Traditional-Simplified', '沈從文') # => "沈从文"
318
+
319
+ transliterator = ICU::Transliteration::Transliterator.new('Latin-ASCII')
320
+ transliterator.transliterate('café') # => "cafe"
321
+ ```
322
+
323
+ Apply a sequence of custom transliteration rules:
324
+
325
+ ```ruby
326
+ ICU::Transliteration.translit('NFD; [:Nonspacing Mark:] Remove; NFC', 'âêîôû') # => "aeiou"
188
327
  ```
189
328
 
190
- ## Locale
329
+ ### Unicode Normalization
191
330
 
192
- Examples:
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):
342
+
343
+ ```ruby
344
+ normalizer = ICU::Normalizer.new(nil, 'nfkc', :compose)
345
+ normalizer.normalize('ffi') # => "ffi"
346
+ ```
347
+
348
+ A convenience method also supports one-off normalization:
349
+
350
+ ```ruby
351
+ ICU::Normalization.normalize('Å', :nfd) # => "A\u030A"
352
+ ICU::Normalization.normalize("A\u030A", :nfc) # => "Å"
353
+ ```
354
+
355
+ ### List Formatting
356
+
357
+ List formatting requires ICU 67 or newer. Join strings with locale-aware
358
+ separators and conjunctions:
359
+
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
+ ```
366
+
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.
369
+
370
+ ### Locale
371
+
372
+ Look up localized names and choose display-name contexts:
193
373
 
194
374
  ```ruby
195
375
  locale = ICU::Locale.new('en-US')
196
- locale.display_country('en-US') #=> "United States"
197
- locale.display_language('es') #=> "inglés"
198
- locale.display_name('es') #=> "inglés (Estados Unidos)"
199
- locale.display_name_with_context('en-US', [:length_short]) #=> "English (US)"
200
- locale.display_name_with_context('en-US', [:length_long]) #=> "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"
201
383
  ```
202
384
 
385
+ Convert BCP 47 language tags to ICU locale IDs or add likely subtags:
386
+
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"
392
+ ```
393
+
394
+ ## Benchmarks
395
+
396
+ Benchmarks live in [`benchmark/`](benchmark/README.md) with a separate Gemfile.
397
+
203
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')
@@ -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