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 +4 -4
- data/Gemfile +2 -3
- data/README.md +285 -129
- data/Rakefile +6 -10
- data/lib/ffi-icu/duration_formatting.rb +1 -25
- data/lib/ffi-icu/lib.rb +20 -15
- data/lib/ffi-icu/message_formatting.rb +2 -2
- data/lib/ffi-icu/number_formatting.rb +41 -0
- data/lib/ffi-icu/version.rb +1 -1
- metadata +1 -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,248 +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
|
|
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
|
-
|
|
39
|
+
The gem is published on [RubyGems][rubygems]. Load it before running the examples:
|
|
17
40
|
|
|
18
|
-
|
|
41
|
+
```ruby
|
|
42
|
+
require('ffi-icu')
|
|
43
|
+
```
|
|
44
|
+
|
|
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
|
|
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
|
-
|
|
63
|
+
### Character Encoding Detection
|
|
31
64
|
|
|
32
|
-
|
|
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
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
Sort strings using a locale's collation rules:
|
|
50
84
|
|
|
51
|
-
|
|
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.
|
|
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
|
+
```
|
|
148
|
+
|
|
149
|
+
### Currency Metadata
|
|
87
150
|
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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.
|
|
97
172
|
|
|
98
|
-
Message
|
|
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
|
-
|
|
185
|
+
ICU::MessageFormatting.format('Hello, {0}!', locale: 'en-US', arguments: ['Ada'])
|
|
186
|
+
# => "Hello, Ada!"
|
|
107
187
|
```
|
|
108
188
|
|
|
109
|
-
|
|
189
|
+
Validate a pattern before formatting:
|
|
110
190
|
|
|
111
191
|
```ruby
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
### Time Formatting and Parsing
|
|
121
206
|
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
formatter
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
formatter.format({hours: 8, minutes: 40, seconds: 35}) #=> "8 hrs, 40 mins, 35 secs"
|
|
249
|
+
### Duration Formatting
|
|
157
250
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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({
|
|
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: :
|
|
177
|
-
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
|
+
```
|
|
178
278
|
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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})
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
198
|
-
formatter
|
|
199
|
-
|
|
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:
|
|
309
|
+
formatter.format({ hours: 1, minutes: 2, seconds: 3 }) # => "1 ч 2 мин 3 с"
|
|
202
310
|
```
|
|
203
311
|
|
|
204
|
-
|
|
312
|
+
### Transliteration
|
|
205
313
|
|
|
206
|
-
|
|
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
|
-
|
|
323
|
+
Apply a sequence of custom transliteration rules:
|
|
213
324
|
|
|
214
|
-
|
|
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::
|
|
218
|
-
# => "
|
|
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
|
-
|
|
221
|
-
# => "
|
|
350
|
+
```ruby
|
|
351
|
+
ICU::Normalization.normalize('Å', :nfd) # => "A\u030A"
|
|
352
|
+
ICU::Normalization.normalize("A\u030A", :nfc) # => "Å"
|
|
222
353
|
```
|
|
223
354
|
|
|
224
|
-
|
|
355
|
+
### List Formatting
|
|
225
356
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
|
239
|
-
locale.display_language('es')
|
|
240
|
-
locale.display_name('es')
|
|
241
|
-
locale.display_name_with_context('en-US', [:length_short])
|
|
242
|
-
locale.display_name_with_context('en-US', [:length_full])
|
|
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
|
-
|
|
245
|
-
|
|
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 '
|
|
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')
|
|
@@ -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
|
-
|
|
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
|
-
:
|
|
514
|
-
:
|
|
515
|
-
:
|
|
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
|
-
#
|
|
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
|
data/lib/ffi-icu/version.rb
CHANGED