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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f0256350b6cfd290356bcab5c7e501dcc55959f2a995dac452bb43756050788
|
|
4
|
+
data.tar.gz: 4fe1bd3bf83133f012f6d71d07ec997ceb9abae835e8b7be4bed6d8a7ea27bb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
8
|
+
## Installation
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
56
|
+
The library detects ICU's versioned function names automatically.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
29
59
|
|
|
30
|
-
|
|
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
|
-
|
|
63
|
+
### Character Encoding Detection
|
|
64
|
+
|
|
65
|
+
Detect an encoding and inspect the confidence score:
|
|
33
66
|
|
|
34
67
|
```ruby
|
|
35
|
-
match = ICU::CharDet.detect(
|
|
68
|
+
match = ICU::CharDet.detect('æåø')
|
|
36
69
|
match.name # => "UTF-8"
|
|
37
70
|
match.confidence # => 80
|
|
38
71
|
```
|
|
39
72
|
|
|
40
|
-
|
|
73
|
+
Reuse a detector for multiple inputs:
|
|
41
74
|
|
|
42
75
|
```ruby
|
|
43
76
|
detector = ICU::CharDet::Detector.new
|
|
44
|
-
detector.detect(
|
|
77
|
+
detector.detect('æåø').name # => "UTF-8"
|
|
78
|
+
detector.detect_all('foo bar').is_a?(Array) # => true
|
|
45
79
|
```
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
### Locale-Aware Collation
|
|
82
|
+
|
|
83
|
+
Sort strings using a locale's collation rules:
|
|
48
84
|
|
|
49
|
-
|
|
85
|
+
```ruby
|
|
86
|
+
ICU::Collation.collate('nb', ['å', 'æ', 'ø']) # => ["æ", "ø", "å"]
|
|
87
|
+
```
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
Reuse a collator to compare and sort strings:
|
|
52
90
|
|
|
53
91
|
```ruby
|
|
54
|
-
ICU::Collation.
|
|
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
|
-
|
|
98
|
+
### Text Boundary Analysis
|
|
99
|
+
|
|
100
|
+
Find word boundaries or extract the substrings between them:
|
|
58
101
|
|
|
59
102
|
```ruby
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
Format numbers, percentages, and currency amounts:
|
|
69
115
|
|
|
70
116
|
```ruby
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
82
|
-
ICU::NumberFormatting.
|
|
83
|
-
|
|
84
|
-
ICU::NumberFormatting.
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
numf = ICU::NumberFormatting.create('fr-CA')
|
|
90
|
-
numf.format(1000) #=> "1 000"
|
|
149
|
+
### Currency Metadata
|
|
91
150
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
171
|
+
`narrow_symbol(locale)` requires ICU 61 or newer. `numeric_code` requires ICU 49 or newer.
|
|
172
|
+
|
|
173
|
+
### Message Formatting
|
|
97
174
|
|
|
98
|
-
|
|
175
|
+
Use numbered arguments for ICU MessageFormat patterns, including plurals and selects:
|
|
99
176
|
|
|
100
177
|
```ruby
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
218
|
+
Reuse a formatter for formatting and parsing:
|
|
117
219
|
|
|
118
220
|
```ruby
|
|
119
|
-
formatter = ICU::TimeFormatting.create(
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
formatter.
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8 hrs, 40 mins, 35 secs"
|
|
249
|
+
### Duration Formatting
|
|
135
250
|
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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({
|
|
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: :
|
|
155
|
-
formatter.format({minutes: 40, seconds:
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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})
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
176
|
-
formatter
|
|
177
|
-
|
|
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:
|
|
309
|
+
formatter.format({ hours: 1, minutes: 2, seconds: 3 }) # => "1 ч 2 мин 3 с"
|
|
180
310
|
```
|
|
181
311
|
|
|
182
|
-
|
|
312
|
+
### Transliteration
|
|
183
313
|
|
|
184
|
-
|
|
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
|
-
|
|
329
|
+
### Unicode Normalization
|
|
191
330
|
|
|
192
|
-
|
|
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')
|
|
197
|
-
locale.display_language('es')
|
|
198
|
-
locale.display_name('es')
|
|
199
|
-
locale.display_name_with_context('en-US', [:length_short])
|
|
200
|
-
locale.display_name_with_context('en-US', [:
|
|
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 '
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
text_pointer = UCharPointer.from_utf8(str)
|
|
24
24
|
|
|
25
25
|
Lib.check_error do |err|
|
|
26
|
-
Lib.ubrk_setText(@iterator,
|
|
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
|
-
|
|
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.
|
|
86
|
+
Lib.ubrk_previous(@iterator)
|
|
68
87
|
end
|
|
69
88
|
|
|
70
89
|
def first
|