ffi-icu 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -3
- data/README.md +11 -1
- data/build_icu.sh +53 -0
- data/lib/ffi-icu/collation.rb +67 -1
- data/lib/ffi-icu/duration_formatting.rb +282 -0
- data/lib/ffi-icu/lib.rb +51 -4
- data/lib/ffi-icu/time_formatting.rb +4 -1
- data/lib/ffi-icu/version.rb +1 -1
- data/lib/ffi-icu.rb +1 -0
- data/spec/collation_spec.rb +21 -0
- data/spec/duration_formatting_spec.rb +143 -0
- data/spec/time_spec.rb +6 -0
- metadata +6 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77b5088964964f78d79893dc7260d5e01dde9e701c62877eb19556ecc3ee460b
|
4
|
+
data.tar.gz: b008d71ecdd7360bbe8bf24544f888a71fede1934a5fdaf1bb46b121b21be95e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9074f912c536402fadf8e489989bc475f4330fac69cd32b720c5916fa0f5a4b447a1ef4994692f0116c36def1cc84632d2e6f4529071412d4a44b01d2f10e5a1
|
7
|
+
data.tar.gz: '028fab98aaf0c3ddef7f6a4af81f9c228c84bd1d2a17be241ab4834fb0c4922ca51dd1c7e49860589a54178a0c02430b2a60cb3e1f3cd460e605336a58200982'
|
data/.travis.yml
CHANGED
@@ -7,14 +7,22 @@ arch:
|
|
7
7
|
- arm64
|
8
8
|
|
9
9
|
rvm:
|
10
|
-
- 2.6
|
11
10
|
- 2.7
|
12
11
|
- 3.0
|
12
|
+
- 3.1
|
13
13
|
- ruby-head
|
14
|
+
- truffleruby
|
14
15
|
|
15
16
|
before_script:
|
16
|
-
- sudo apt
|
17
|
+
- sudo apt install -y icu-devtools g++
|
18
|
+
- sudo chmod +x build_icu.sh
|
19
|
+
- sudo $PWD/build_icu.sh versions
|
20
|
+
- sudo $PWD/build_icu.sh install 71.1
|
21
|
+
- export LD_LIBRARY_PATH=/usr/local/lib
|
22
|
+
- icuinfo
|
23
|
+
- yes | gem update --system --force
|
24
|
+
- gem install bundler
|
17
25
|
|
18
26
|
jobs:
|
19
27
|
allow_failures:
|
20
|
-
-
|
28
|
+
- rvm: truffleruby
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
ffi-icu [![Build Status](https://travis-ci.
|
1
|
+
ffi-icu [![Build Status](https://app.travis-ci.com/erickguan/ffi-icu.svg?branch=master)](https://app.travis-ci.com/erickguan/ffi-icu)
|
2
2
|
=======
|
3
3
|
|
4
4
|
Simple FFI wrappers for ICU. Checkout the renovated [ICU gem](https://github.com/fantasticfears/icu4r) instead which supports various of encoding and distributed with packaged source. FFI-ICU needs some love with ICU gem's transcoding method.
|
@@ -129,6 +129,16 @@ For skeleton formatting, visit the [Unicode date field symbol table](https://uni
|
|
129
129
|
formatter.format(Time.now) #=> "2015"
|
130
130
|
```
|
131
131
|
|
132
|
+
Transliteration
|
133
|
+
---------------
|
134
|
+
|
135
|
+
Example:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
ICU::Transliteration.transliterate('Traditional-Simplified', '沈從文') # => "沈从文"
|
139
|
+
|
140
|
+
```
|
141
|
+
|
132
142
|
Tested on:
|
133
143
|
==========
|
134
144
|
|
data/build_icu.sh
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
if [[ -x $(which icuinfo) ]]; then
|
4
|
+
echo System ICU version: $(icuinfo | grep -o '"version">[^<]\+' | grep -o '[^"><]\+$')
|
5
|
+
else
|
6
|
+
echo 'System ICU not installed'
|
7
|
+
fi
|
8
|
+
|
9
|
+
if [[ "$1" == '' ]]; then
|
10
|
+
echo ''
|
11
|
+
echo 'Usage:'
|
12
|
+
echo ''
|
13
|
+
echo '1) bash icu-install.sh versions'
|
14
|
+
echo ''
|
15
|
+
echo '2) bash icu-install.sh install <version>'
|
16
|
+
fi
|
17
|
+
|
18
|
+
if [[ "$1" == 'versions' ]]; then
|
19
|
+
echo ''
|
20
|
+
echo 'Available ICU versions'
|
21
|
+
wget -O - https://icu.unicode.org/download 2>/dev/null | grep -P -o '(?<=http://site.icu-project.org/download/)\d+#TOC-ICU4C-Download.+;>\K[\d.]+'
|
22
|
+
fi
|
23
|
+
|
24
|
+
if [[ "$2" != "" && "$1" == 'install' ]]; then
|
25
|
+
which g++ || sudo apt install -y g++
|
26
|
+
|
27
|
+
ICU_VERSION=$2
|
28
|
+
ICU_SRC_FILE="icu4c-$(echo $ICU_VERSION | sed -e 's/\./_/')-src.tgz"
|
29
|
+
echo "Trying to install ICU version: $ICU_VERSION"
|
30
|
+
if [[ ! -e "$ICU_SRC_FILE" ]]; then
|
31
|
+
wget "https://github.com/unicode-org/icu/releases/download/release-$(echo $ICU_VERSION | sed -e 's/\./-/')/$ICU_SRC_FILE"
|
32
|
+
fi
|
33
|
+
if [[ ! -e "$ICU_SRC_FILE" ]]; then
|
34
|
+
exit 1;
|
35
|
+
fi
|
36
|
+
|
37
|
+
ICU_SRC_FOLDER="icu-release-$(echo $ICU_VERSION | sed -e 's/\./-/')"
|
38
|
+
tar zxvf "$ICU_SRC_FILE"
|
39
|
+
which g++ || sudo apt install -y g++
|
40
|
+
|
41
|
+
if [[ ! -e "/opt/icu$ICU_VERSION" ]]; then
|
42
|
+
pushd icu/source
|
43
|
+
sudo mkdir "/opt/icu$ICU_VERSION"
|
44
|
+
./configure --prefix="/opt/icu$ICU_VERSION" && make -j2 && sudo make install
|
45
|
+
ls -alh /opt/icu$ICU_VERSION/lib/
|
46
|
+
sudo cp -r /opt/icu$ICU_VERSION/lib/* /usr/local/lib
|
47
|
+
popd
|
48
|
+
else
|
49
|
+
echo "ICU already installed at (/opt/icu$ICU_VERSION)"
|
50
|
+
fi
|
51
|
+
|
52
|
+
rm -f "$ICU_SRC_FILE"
|
53
|
+
fi
|
data/lib/ffi-icu/collation.rb
CHANGED
@@ -1,6 +1,38 @@
|
|
1
1
|
module ICU
|
2
2
|
module Collation
|
3
3
|
|
4
|
+
ATTRIBUTES = {
|
5
|
+
french_collation: 0,
|
6
|
+
alternate_handling: 1,
|
7
|
+
case_first: 2,
|
8
|
+
case_level: 3,
|
9
|
+
normalization_mode: 4,
|
10
|
+
strength: 5,
|
11
|
+
hiragana_quaternary_mode: 6,
|
12
|
+
numeric_collation: 7,
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
ATTRIBUTE_VALUES = {
|
16
|
+
nil => -1,
|
17
|
+
primary: 0,
|
18
|
+
secondary: 1,
|
19
|
+
default_strength: 2,
|
20
|
+
tertiary: 2,
|
21
|
+
quaternary: 3,
|
22
|
+
identical: 15,
|
23
|
+
|
24
|
+
false => 16,
|
25
|
+
true => 17,
|
26
|
+
|
27
|
+
shifted: 20,
|
28
|
+
non_ignorable: 21,
|
29
|
+
|
30
|
+
lower_first: 24,
|
31
|
+
upper_first: 25,
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
ATTRIBUTE_VALUES_INVERSE = Hash[ATTRIBUTE_VALUES.map {|k,v| [v, k]}].freeze
|
35
|
+
|
4
36
|
def self.collate(locale, arr)
|
5
37
|
Collator.new(locale).collate(arr)
|
6
38
|
end
|
@@ -80,10 +112,44 @@ module ICU
|
|
80
112
|
def rules
|
81
113
|
@rules ||= begin
|
82
114
|
length = FFI::MemoryPointer.new(:int)
|
83
|
-
ptr =
|
115
|
+
ptr = Lib.ucol_getRules(@c, length)
|
84
116
|
ptr.read_array_of_uint16(length.read_int).pack("U*")
|
85
117
|
end
|
86
118
|
end
|
119
|
+
|
120
|
+
def collation_key(string)
|
121
|
+
ptr = UCharPointer.from_string(string)
|
122
|
+
size = Lib.ucol_getSortKey(@c, ptr, string.jlength, nil, 0)
|
123
|
+
buffer = FFI::MemoryPointer.new(:char, size)
|
124
|
+
Lib.ucol_getSortKey(@c, ptr, string.jlength, buffer, size)
|
125
|
+
buffer.read_bytes(size - 1)
|
126
|
+
end
|
127
|
+
|
128
|
+
def [](attribute)
|
129
|
+
ATTRIBUTE_VALUES_INVERSE[Lib.check_error do |error|
|
130
|
+
Lib.ucol_getAttribute(@c, ATTRIBUTES[attribute], error)
|
131
|
+
end]
|
132
|
+
end
|
133
|
+
|
134
|
+
def []=(attribute, value)
|
135
|
+
Lib.check_error do |error|
|
136
|
+
Lib.ucol_setAttribute(@c, ATTRIBUTES[attribute], ATTRIBUTE_VALUES[value], error)
|
137
|
+
end
|
138
|
+
value
|
139
|
+
end
|
140
|
+
|
141
|
+
# create friendly named methods for setting attributes
|
142
|
+
ATTRIBUTES.each_key do |attribute|
|
143
|
+
class_eval <<-CODE
|
144
|
+
def #{attribute}
|
145
|
+
self[:#{attribute}]
|
146
|
+
end
|
147
|
+
|
148
|
+
def #{attribute}=(value)
|
149
|
+
self[:#{attribute}] = value
|
150
|
+
end
|
151
|
+
CODE
|
152
|
+
end
|
87
153
|
end # Collator
|
88
154
|
|
89
155
|
end # Collate
|
@@ -0,0 +1,282 @@
|
|
1
|
+
module ICU
|
2
|
+
module DurationFormatting
|
3
|
+
VALID_FIELDS = %i[
|
4
|
+
years
|
5
|
+
months
|
6
|
+
weeks
|
7
|
+
days
|
8
|
+
hours
|
9
|
+
minutes
|
10
|
+
seconds
|
11
|
+
milliseconds
|
12
|
+
microseconds
|
13
|
+
nanoseconds
|
14
|
+
]
|
15
|
+
HMS_FIELDS = %i[
|
16
|
+
hours
|
17
|
+
minutes
|
18
|
+
seconds
|
19
|
+
milliseconds
|
20
|
+
microseconds
|
21
|
+
nanoseconds
|
22
|
+
]
|
23
|
+
ROUNDABLE_FIELDS = %i[
|
24
|
+
seconds
|
25
|
+
milliseconds
|
26
|
+
microseconds
|
27
|
+
nanoseconds
|
28
|
+
]
|
29
|
+
VALID_STYLES = %i[long short narrow digital]
|
30
|
+
STYLES_TO_LIST_JOIN_FORMAT = {
|
31
|
+
long: :wide,
|
32
|
+
short: :short,
|
33
|
+
narrow: :narrow,
|
34
|
+
digital: :narrow,
|
35
|
+
}
|
36
|
+
UNIT_FORMAT_STRINGS = {
|
37
|
+
years: 'measure-unit/duration-year',
|
38
|
+
months: 'measure-unit/duration-month',
|
39
|
+
weeks: 'measure-unit/duration-week',
|
40
|
+
days: 'measure-unit/duration-day',
|
41
|
+
hours: 'measure-unit/duration-hour',
|
42
|
+
minutes: 'measure-unit/duration-minute',
|
43
|
+
seconds: 'measure-unit/duration-second',
|
44
|
+
milliseconds: 'measure-unit/duration-millisecond',
|
45
|
+
microseconds: 'measure-unit/duration-microsecond',
|
46
|
+
nanoseconds: 'measure-unit/duration-nanosecond',
|
47
|
+
}
|
48
|
+
STYLES_TO_NUMBER_FORMAT_WIDTH = {
|
49
|
+
long: 'unit-width-full-name',
|
50
|
+
short: 'unit-width-short',
|
51
|
+
narrow: 'unit-width-narrow',
|
52
|
+
# digital for hours:minutes:seconds has some special casing.
|
53
|
+
digital: 'unit-width-narrow',
|
54
|
+
}
|
55
|
+
|
56
|
+
def self.format(fields, locale:, style: :long)
|
57
|
+
DurationFormatter.new(locale: locale, style: style).format(fields)
|
58
|
+
end
|
59
|
+
|
60
|
+
class DurationFormatter
|
61
|
+
def initialize(locale:, style: :long)
|
62
|
+
if !Lib.respond_to?(:unumf_openForSkeletonAndLocale) || !Lib.respond_to?(:ulistfmt_openForType)
|
63
|
+
raise "ICU::DurationFormatting requires ICU >= 67"
|
64
|
+
end
|
65
|
+
|
66
|
+
raise ArgumentError, "Unknown style #{style}" unless VALID_STYLES.include?(style)
|
67
|
+
|
68
|
+
@locale = locale
|
69
|
+
@style = style
|
70
|
+
# These are created lazily based on what parts are actually included
|
71
|
+
@number_formatters = {}
|
72
|
+
|
73
|
+
list_join_format = STYLES_TO_LIST_JOIN_FORMAT.fetch(style)
|
74
|
+
@list_formatter = FFI::AutoPointer.new(
|
75
|
+
Lib.check_error { |error|
|
76
|
+
Lib.ulistfmt_openForType(@locale, :units, list_join_format, error)
|
77
|
+
},
|
78
|
+
Lib.method(:ulistfmt_close)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def format(fields)
|
83
|
+
fields.each_key do |field|
|
84
|
+
raise "Unknown field #{field}" unless VALID_FIELDS.include?(field)
|
85
|
+
end
|
86
|
+
fields = fields.dup # we might modify this argument.
|
87
|
+
|
88
|
+
# Intl.js spec says that rounding options affect only the smallest unit, and only
|
89
|
+
# if that unit is sub-second. All other fields therefore need to be truncated.
|
90
|
+
smallest_unit = VALID_FIELDS[fields.keys.map { |k| VALID_FIELDS.index(k) }.max]
|
91
|
+
fields.each_key do |k|
|
92
|
+
raise ArgumentError, "Negative durations are not yet supported" if fields[k] < 0
|
93
|
+
fields[k] = fields[k].to_i unless k == smallest_unit && ROUNDABLE_FIELDS.include?(smallest_unit)
|
94
|
+
end
|
95
|
+
|
96
|
+
formatted_hms = nil
|
97
|
+
if @style == :digital
|
98
|
+
# icu::MeasureFormat contains special casing for hours/minutes/seconds formatted
|
99
|
+
# at numeric width, to render it as h:mm:s, essentially. This involves using
|
100
|
+
# a pattern called durationUnits defined in the ICU data for the locale.
|
101
|
+
# If we have data for this combination of hours/mins/seconds in this locale,
|
102
|
+
# use that and emulate ICU's special casing.
|
103
|
+
formatted_hms = format_hms(fields)
|
104
|
+
if formatted_hms
|
105
|
+
# We've taken care of all these fields now.
|
106
|
+
HMS_FIELDS.each do |f|
|
107
|
+
fields.delete f
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
formatted_fields = VALID_FIELDS.map do |f|
|
113
|
+
next unless fields.key?(f)
|
114
|
+
next unless fields[f] != 0
|
115
|
+
|
116
|
+
format_number(fields[f], [
|
117
|
+
UNIT_FORMAT_STRINGS[f], STYLES_TO_NUMBER_FORMAT_WIDTH[@style],
|
118
|
+
(".#########" if f == smallest_unit),
|
119
|
+
].compact.join(' '))
|
120
|
+
end
|
121
|
+
formatted_fields << formatted_hms
|
122
|
+
formatted_fields.compact!
|
123
|
+
|
124
|
+
format_list(formatted_fields)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def hms_duration_units_pattern(fields)
|
130
|
+
return nil unless HMS_FIELDS.any? { |k| fields.key?(k) }
|
131
|
+
@unit_res_bundle ||= FFI::AutoPointer.new(
|
132
|
+
Lib.check_error { |error| Lib.ures_open(Lib.resource_bundle_name(:unit), @locale, error) },
|
133
|
+
Lib.method(:ures_close)
|
134
|
+
)
|
135
|
+
|
136
|
+
resource_key = "durationUnits/"
|
137
|
+
resource_key << "h" if fields.key?(:hours)
|
138
|
+
resource_key << "m" if fields.key?(:minutes)
|
139
|
+
resource_key << "s" if [:seconds, :milliseconds, :microseconds, :nanoseconds].any? { |f| fields.key?(f) }
|
140
|
+
|
141
|
+
begin
|
142
|
+
pattern_resource = FFI::AutoPointer.new(
|
143
|
+
Lib.check_error { |error| Lib.ures_getBykeyWithFallback(@unit_res_bundle, resource_key, nil, error) },
|
144
|
+
Lib.method(:ures_close)
|
145
|
+
)
|
146
|
+
rescue MissingResourceError
|
147
|
+
# This combination of h,m,s not present for this locale.
|
148
|
+
return nil
|
149
|
+
end
|
150
|
+
# Read the resource as a UChar (whose memory we _do not own_ - it's static data) and
|
151
|
+
# convert it to a Ruby string.
|
152
|
+
pattern_uchar_len = FFI::MemoryPointer.new(:int32_t)
|
153
|
+
pattern_uchar = Lib.check_error { |error|
|
154
|
+
Lib.ures_getString(pattern_resource, pattern_uchar_len, error)
|
155
|
+
}
|
156
|
+
pattern_str = pattern_uchar.read_array_of_uint16(pattern_uchar_len.read_int32).pack("U*")
|
157
|
+
|
158
|
+
# For some reason I can't comprehend, loadNumericDateFormatterPattern in ICU wants to turn
|
159
|
+
# h's into H's here. I guess we have to do it too because the pattern data could in theory
|
160
|
+
# now contain either.
|
161
|
+
pattern_str.gsub('h', 'H')
|
162
|
+
end
|
163
|
+
|
164
|
+
def format_hms(fields)
|
165
|
+
pattern = hms_duration_units_pattern(fields)
|
166
|
+
return nil if pattern.nil?
|
167
|
+
|
168
|
+
# According to the Intl.js spec, when formatting in digital, everything < seconds
|
169
|
+
# should be coalesced into decimal seconds
|
170
|
+
seconds_incl_fractional = fields.fetch(:seconds, 0)
|
171
|
+
second_precision = 0
|
172
|
+
if fields.key?(:milliseconds)
|
173
|
+
seconds_incl_fractional += fields[:milliseconds] / 1e3
|
174
|
+
second_precision = 3
|
175
|
+
end
|
176
|
+
if fields.key?(:microseconds)
|
177
|
+
seconds_incl_fractional += fields[:microseconds] / 1e6
|
178
|
+
second_precision = 6
|
179
|
+
end
|
180
|
+
if fields.key?(:nanoseconds)
|
181
|
+
seconds_incl_fractional += fields[:nanoseconds] / 1e9
|
182
|
+
second_precision = 9
|
183
|
+
end
|
184
|
+
|
185
|
+
# Follow the rules in ICU measfmt.cpp formatNumeric to fill in the patterns here with
|
186
|
+
# the appropriate values.
|
187
|
+
enum = pattern.each_char
|
188
|
+
protect = false
|
189
|
+
result = ""
|
190
|
+
loop do
|
191
|
+
char = enum.next
|
192
|
+
next_char = enum.peek rescue nil
|
193
|
+
|
194
|
+
if protect
|
195
|
+
# In literal mode
|
196
|
+
if char == "'"
|
197
|
+
protect = false
|
198
|
+
next
|
199
|
+
end
|
200
|
+
result << char
|
201
|
+
next
|
202
|
+
end
|
203
|
+
|
204
|
+
value = case char
|
205
|
+
when 'H' then fields[:hours]
|
206
|
+
when 'm' then fields[:minutes]
|
207
|
+
when 's' then seconds_incl_fractional
|
208
|
+
end
|
209
|
+
|
210
|
+
case char
|
211
|
+
when 'H', 'm', 's'
|
212
|
+
skeleton = "."
|
213
|
+
if char == 's' && second_precision > 0
|
214
|
+
skeleton << ("0" * second_precision)
|
215
|
+
else
|
216
|
+
skeleton << ("#" * 9)
|
217
|
+
end
|
218
|
+
if char == next_char
|
219
|
+
# It's doubled - means format it at zero fill
|
220
|
+
skeleton << " integer-width/00"
|
221
|
+
enum.next
|
222
|
+
end
|
223
|
+
result << format_number(value, skeleton)
|
224
|
+
when "'"
|
225
|
+
if next_char == char
|
226
|
+
# double-apostrophe, means literal '
|
227
|
+
result << "'"
|
228
|
+
enum.next
|
229
|
+
else
|
230
|
+
protect = true
|
231
|
+
end
|
232
|
+
else
|
233
|
+
result << char
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
result
|
238
|
+
end
|
239
|
+
|
240
|
+
def number_formatter(skeleton)
|
241
|
+
@number_formatters[skeleton] ||= begin
|
242
|
+
skeleton_uchar = UCharPointer.from_string(skeleton)
|
243
|
+
FFI::AutoPointer.new(
|
244
|
+
Lib.check_error { |error|
|
245
|
+
Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars, @locale, error)
|
246
|
+
},
|
247
|
+
Lib.method(:unumf_close)
|
248
|
+
)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def format_number(value, skeleton)
|
253
|
+
formatter = number_formatter(skeleton)
|
254
|
+
result = FFI::AutoPointer.new(
|
255
|
+
Lib.check_error { |error| Lib.unumf_openResult(error) },
|
256
|
+
Lib.method(:unumf_closeResult)
|
257
|
+
)
|
258
|
+
value_str = value.to_s
|
259
|
+
Lib.check_error do |error|
|
260
|
+
Lib.unumf_formatDecimal(formatter, value_str, value_str.size, result, error)
|
261
|
+
end
|
262
|
+
Lib::Util.read_uchar_buffer(0) do |buf, error|
|
263
|
+
Lib.unumf_resultToString(result, buf, buf.length_in_uchars, error)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def format_list(values)
|
268
|
+
value_uchars = values.map(&UCharPointer.method(:from_string))
|
269
|
+
value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size)
|
270
|
+
value_uchars_array.put_array_of_pointer(0, value_uchars)
|
271
|
+
value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size)
|
272
|
+
value_lengths_array.put_array_of_int32(0, value_uchars.map(&:length_in_uchars))
|
273
|
+
Lib::Util.read_uchar_buffer(0) do |buf, error|
|
274
|
+
Lib.ulistfmt_format(
|
275
|
+
@list_formatter, value_uchars_array, value_lengths_array,
|
276
|
+
value_uchars.size, buf, buf.length_in_uchars, error
|
277
|
+
)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
data/lib/ffi-icu/lib.rb
CHANGED
@@ -5,6 +5,9 @@ module ICU
|
|
5
5
|
class BufferOverflowError < StandardError
|
6
6
|
end
|
7
7
|
|
8
|
+
class MissingResourceError < StandardError
|
9
|
+
end
|
10
|
+
|
8
11
|
module Lib
|
9
12
|
extend FFI::Library
|
10
13
|
|
@@ -129,6 +132,8 @@ module ICU
|
|
129
132
|
name = Lib.u_errorName error_code
|
130
133
|
if name == "U_BUFFER_OVERFLOW_ERROR"
|
131
134
|
raise BufferOverflowError
|
135
|
+
elsif name == "U_MISSING_RESOURCE_ERROR"
|
136
|
+
raise MissingResourceError
|
132
137
|
else
|
133
138
|
raise Error, name
|
134
139
|
end
|
@@ -192,6 +197,11 @@ module ICU
|
|
192
197
|
@version ||= VersionInfo.new.tap { |version| u_getVersion(version) }
|
193
198
|
end
|
194
199
|
|
200
|
+
def self.attach_optional_function(*args)
|
201
|
+
attach_function *args
|
202
|
+
rescue FFI::NotFoundError
|
203
|
+
end
|
204
|
+
|
195
205
|
version = load_icu
|
196
206
|
suffix = figure_suffix(version)
|
197
207
|
|
@@ -297,6 +307,10 @@ module ICU
|
|
297
307
|
attach_function :ucol_greaterOrEqual, "ucol_greaterOrEqual#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool
|
298
308
|
attach_function :ucol_equal, "ucol_equal#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool
|
299
309
|
attach_function :ucol_getRules, "ucol_getRules#{suffix}", [:pointer, :pointer], :pointer
|
310
|
+
attach_function :ucol_getSortKey, "ucol_getSortKey#{suffix}", [:pointer, :pointer, :int, :pointer, :int], :int
|
311
|
+
attach_function :ucol_getAttribute, "ucol_getAttribute#{suffix}", [:pointer, :int, :pointer], :int
|
312
|
+
attach_function :ucol_setAttribute, "ucol_setAttribute#{suffix}", [:pointer, :int, :int, :pointer], :void
|
313
|
+
|
300
314
|
|
301
315
|
# Transliteration
|
302
316
|
#
|
@@ -419,12 +433,45 @@ module ICU
|
|
419
433
|
attach_function :unum_format_int32, "unum_format#{suffix}", [:pointer, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
420
434
|
attach_function :unum_format_int64, "unum_formatInt64#{suffix}", [:pointer, :int64_t, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
421
435
|
attach_function :unum_format_double, "unum_formatDouble#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
422
|
-
|
423
|
-
attach_function :unum_format_decimal, "unum_formatDecimal#{suffix}", [:pointer, :string, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
424
|
-
rescue FFI::NotFoundError
|
425
|
-
end
|
436
|
+
attach_optional_function :unum_format_decimal, "unum_formatDecimal#{suffix}", [:pointer, :string, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
426
437
|
attach_function :unum_format_currency, "unum_formatDoubleCurrency#{suffix}", [:pointer, :double, :pointer, :pointer, :int32_t, :pointer, :pointer], :int32_t
|
427
438
|
attach_function :unum_set_attribute, "unum_setAttribute#{suffix}", [:pointer, :number_format_attribute, :int32_t], :void
|
439
|
+
|
440
|
+
# UResourceBundle
|
441
|
+
attach_function :ures_open, "ures_open#{suffix}", [:string, :string, :pointer], :pointer
|
442
|
+
attach_function :ures_close, "ures_close#{suffix}", [:pointer], :void
|
443
|
+
# This function is marked "internal" but it's fully exported by the library ABI, so we can use it anyway.
|
444
|
+
attach_function :ures_getBykeyWithFallback, "ures_getByKeyWithFallback#{suffix}", [:pointer, :string, :pointer, :pointer], :pointer
|
445
|
+
attach_function :ures_getString, "ures_getString#{suffix}", [:pointer, :pointer, :pointer], :pointer
|
446
|
+
|
447
|
+
def self.resource_bundle_name(type)
|
448
|
+
stem = "icudt" + version.read_array_of_char(4)[0].to_s + "l" + "-"
|
449
|
+
stem + type.to_s
|
450
|
+
end
|
451
|
+
|
452
|
+
# UNumberFormatter
|
453
|
+
attach_optional_function :unumf_openForSkeletonAndLocale, "unumf_openForSkeletonAndLocale#{suffix}", [:pointer, :int32_t, :string, :pointer], :pointer
|
454
|
+
attach_optional_function :unumf_close, "unumf_close#{suffix}", [:pointer], :void
|
455
|
+
attach_optional_function :unumf_openResult, "unumf_openResult#{suffix}", [:pointer], :pointer
|
456
|
+
attach_optional_function :unumf_closeResult, "unumf_closeResult#{suffix}", [:pointer], :void
|
457
|
+
attach_optional_function :unumf_formatDecimal, "unumf_formatDecimal#{suffix}", [:pointer, :string, :int32_t, :pointer, :pointer], :void
|
458
|
+
attach_optional_function :unumf_resultToString, "unumf_resultToString#{suffix}", [:pointer, :pointer, :int32_t, :pointer], :int32_t
|
459
|
+
|
460
|
+
# UListFormatter
|
461
|
+
enum :ulistfmt_type, [
|
462
|
+
:and, 0,
|
463
|
+
:or, 1,
|
464
|
+
:units, 2,
|
465
|
+
]
|
466
|
+
enum :ulistfmt_width, [
|
467
|
+
:wide, 0,
|
468
|
+
:short, 1,
|
469
|
+
:narrow, 2,
|
470
|
+
]
|
471
|
+
attach_optional_function :ulistfmt_openForType, "ulistfmt_openForType#{suffix}", [:string, :ulistfmt_type, :ulistfmt_width, :pointer], :pointer
|
472
|
+
attach_optional_function :ulistfmt_close, "ulistfmt_close#{suffix}", [:pointer], :void
|
473
|
+
attach_optional_function :ulistfmt_format, "ulistfmt_format#{suffix}", [:pointer, :pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t
|
474
|
+
|
428
475
|
# date
|
429
476
|
enum :date_format_style, [
|
430
477
|
:pattern, -2,
|
@@ -254,7 +254,10 @@ module ICU
|
|
254
254
|
|
255
255
|
# Either ensure the skeleton has, or does not have, am/pm, as appropriate
|
256
256
|
if ['h11', 'h12'].include?(@hour_cycle)
|
257
|
-
|
257
|
+
# Only actually append 'am/pm' if there is an hour in the format string
|
258
|
+
if skeleton_str =~ /[hHkKjJ]/ && !skeleton_str.include?('a')
|
259
|
+
skeleton_str << 'a'
|
260
|
+
end
|
258
261
|
else
|
259
262
|
skeleton_str.gsub!('a', '')
|
260
263
|
end
|
data/lib/ffi-icu/version.rb
CHANGED
data/lib/ffi-icu.rb
CHANGED
data/spec/collation_spec.rb
CHANGED
@@ -58,6 +58,27 @@ module ICU
|
|
58
58
|
expect(collator.rules).to include('ö<<<Ö')
|
59
59
|
end
|
60
60
|
|
61
|
+
it "returns usable collation keys" do
|
62
|
+
collator.collation_key("abc").should be < collator.collation_key("xyz")
|
63
|
+
end
|
64
|
+
|
65
|
+
context "attributes" do
|
66
|
+
it "can set and get normalization_mode" do
|
67
|
+
collator.normalization_mode = true
|
68
|
+
collator.normalization_mode.should be true
|
69
|
+
|
70
|
+
collator[:normalization_mode].should be true
|
71
|
+
collator[:normalization_mode] = false
|
72
|
+
collator.normalization_mode.should be false
|
73
|
+
|
74
|
+
collator.case_first.should be false
|
75
|
+
collator.case_first = :lower_first
|
76
|
+
collator.case_first.should == :lower_first
|
77
|
+
|
78
|
+
collator.strength = :tertiary
|
79
|
+
collator.strength.should == :tertiary
|
80
|
+
end
|
81
|
+
end
|
61
82
|
end
|
62
83
|
end # Collate
|
63
84
|
end # ICU
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module ICU
|
2
|
+
module DurationFormatting
|
3
|
+
describe 'DurationFormatting::format' do
|
4
|
+
before(:each) do
|
5
|
+
skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'produces hours, minutes, and seconds in order' do
|
9
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'C', style: :long)
|
10
|
+
expect(result).to match(/1.*hour.*2.*minute.*3.*second/i)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'rounds down fractional seconds < 0.5' do
|
14
|
+
result = DurationFormatting.format({seconds: 5.4}, locale: 'C', style: :long)
|
15
|
+
expect(result).to match(/5.*second/i)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'rounds up fractional seconds > 0.5' do
|
19
|
+
result = DurationFormatting.format({seconds: 5.6}, locale: 'C', style: :long)
|
20
|
+
expect(result).to match(/6.*second/i)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'trims off leading zero values' do
|
24
|
+
result = DurationFormatting.format({hours: 0, minutes: 1, seconds: 30}, locale: 'C', style: :long)
|
25
|
+
expect(result).to match(/1.*minute.*30.*second/i)
|
26
|
+
expect(result).to_not match(/hour/i)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'trims off leading missing values' do
|
30
|
+
result = DurationFormatting.format({minutes: 1, seconds: 30}, locale: 'C', style: :long)
|
31
|
+
expect(result).to match(/1.*minute.*30.*second/i)
|
32
|
+
expect(result).to_not match(/hour/i)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'trims off non-leading zero values' do
|
36
|
+
result = DurationFormatting.format({hours: 1, minutes: 0, seconds: 10}, locale: 'C', style: :long)
|
37
|
+
expect(result).to match(/1.*hour.*10.*second/i)
|
38
|
+
expect(result).to_not match(/minute/i)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'trims off non-leading missing values' do
|
42
|
+
result = DurationFormatting.format({hours: 1, seconds: 10}, locale: 'C', style: :long)
|
43
|
+
expect(result).to match(/1.*hour.*10.*second/i)
|
44
|
+
expect(result).to_not match(/minute/i)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'uses comma-based number formatting as appropriate for locale' do
|
48
|
+
result = DurationFormatting.format({seconds: 90123}, locale: 'en-AU', style: :long)
|
49
|
+
expect(result).to match(/90,123.*second/i)
|
50
|
+
expect(result).to_not match(/hour/i)
|
51
|
+
expect(result).to_not match(/minute/i)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'localizes unit names' do
|
55
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'el', style: :long)
|
56
|
+
expect(result).to match(/1.*ώρα.*2.*λεπτά.*3.*δευτερόλεπτα/i)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'can format long' do
|
60
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :long)
|
61
|
+
expect(result).to match(/hour.*minute.*second/i)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'can format short' do
|
65
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :short)
|
66
|
+
expect(result).to match(/hr.*min.*sec/i)
|
67
|
+
expect(result).to_not match(/hour/i)
|
68
|
+
expect(result).to_not match(/minute/i)
|
69
|
+
expect(result).to_not match(/second/i)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'can format narrow' do
|
73
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :narrow)
|
74
|
+
expect(result).to match(/h.*min.*s/i)
|
75
|
+
expect(result).to_not match(/hr/i)
|
76
|
+
expect(result).to_not match(/sec/i)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'can format digital' do
|
80
|
+
result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :digital)
|
81
|
+
expect(result).to eql('1:02:03')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can format the full sequence of time units in order' do
|
85
|
+
duration = {
|
86
|
+
years: 1,
|
87
|
+
months: 2,
|
88
|
+
weeks: 3,
|
89
|
+
days: 4,
|
90
|
+
hours: 5,
|
91
|
+
minutes: 6,
|
92
|
+
seconds: 7,
|
93
|
+
milliseconds: 8,
|
94
|
+
microseconds: 9,
|
95
|
+
nanoseconds: 10,
|
96
|
+
}
|
97
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :short)
|
98
|
+
expect(result).to match(/1.yr.*2.*mths.*3.*wks.*4.*days.*5.*hrs.*6.*mins.*7.*secs.*8.*ms.*9.*μs.*10.*ns/)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'joins ms, us, ns values to seconds in digital format' do
|
102
|
+
duration = {minutes: 10, seconds: 5, milliseconds: 325, microseconds: 53, nanoseconds: 236}
|
103
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital)
|
104
|
+
expect(result).to eql('10:05.325053236')
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'includes trailing zeros as appropriate for the last unit in digital format' do
|
108
|
+
duration = {minutes: 10, seconds: 5, milliseconds: 325, microseconds: 400}
|
109
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital)
|
110
|
+
expect(result).to eql('10:05.325400')
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'joins h:mm:ss and other units in digital format' do
|
114
|
+
duration = {days: 8, hours: 23, minutes: 10, seconds: 9}
|
115
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital)
|
116
|
+
expect(result).to match(/8.*d.*23:10:09/ )
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'ignores all decimal parts except the last, if it is seconds' do
|
120
|
+
duration = {hours: 7.3, minutes: 9.7, seconds: 8.93}
|
121
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :short)
|
122
|
+
expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min.*8\.93[^0-9]*secs/)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'ignores all decimal parts except the last, if it is milliseconds' do
|
126
|
+
duration = {hours: 7.3, minutes: 9.7, seconds: 8.93, milliseconds: 632.2}
|
127
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :short)
|
128
|
+
expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min.*8[^0-9]*secs.*632\.2[^0-9]*ms/)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'ignores all decimal parts including the last, if it is > seconds' do
|
132
|
+
duration = {hours: 7.3, minutes: 9.7}
|
133
|
+
result = DurationFormatting.format(duration, locale: 'en-AU', style: :short)
|
134
|
+
expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min/)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'raises on durations with any negative component' do
|
138
|
+
duration = {hours: 7.3, minutes: -9.7}
|
139
|
+
expect { DurationFormatting.format(duration, locale: 'en-AU') }.to raise_error(ArgumentError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/spec/time_spec.rb
CHANGED
@@ -128,6 +128,12 @@ module ICU
|
|
128
128
|
expect(str).to_not match(/(am|pm)/i)
|
129
129
|
end
|
130
130
|
|
131
|
+
it 'does not include am/pm if time is not requested' do
|
132
|
+
t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
|
133
|
+
str = TimeFormatting.format(t, time: :none, date: :short, locale: locale_name, zone: 'UTC', hour_cycle: 'h12')
|
134
|
+
expect(str).to_not match(/(am|pm|下午|上午)/i)
|
135
|
+
end
|
136
|
+
|
131
137
|
context '@hours keyword' do
|
132
138
|
before(:each) do
|
133
139
|
skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67
|
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.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jari Bakken
|
@@ -77,12 +77,14 @@ files:
|
|
77
77
|
- Rakefile
|
78
78
|
- benchmark/detect.rb
|
79
79
|
- benchmark/shared.rb
|
80
|
+
- build_icu.sh
|
80
81
|
- ffi-icu.gemspec
|
81
82
|
- lib/ffi-icu.rb
|
82
83
|
- lib/ffi-icu/break_iterator.rb
|
83
84
|
- lib/ffi-icu/chardet.rb
|
84
85
|
- lib/ffi-icu/collation.rb
|
85
86
|
- lib/ffi-icu/core_ext/string.rb
|
87
|
+
- lib/ffi-icu/duration_formatting.rb
|
86
88
|
- lib/ffi-icu/lib.rb
|
87
89
|
- lib/ffi-icu/lib/util.rb
|
88
90
|
- lib/ffi-icu/locale.rb
|
@@ -96,6 +98,7 @@ files:
|
|
96
98
|
- spec/break_iterator_spec.rb
|
97
99
|
- spec/chardet_spec.rb
|
98
100
|
- spec/collation_spec.rb
|
101
|
+
- spec/duration_formatting_spec.rb
|
99
102
|
- spec/lib/version_info_spec.rb
|
100
103
|
- spec/lib_spec.rb
|
101
104
|
- spec/locale_spec.rb
|
@@ -127,21 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
130
|
- !ruby/object:Gem::Version
|
128
131
|
version: '0'
|
129
132
|
requirements: []
|
130
|
-
rubygems_version: 3.
|
133
|
+
rubygems_version: 3.3.7
|
131
134
|
signing_key:
|
132
135
|
specification_version: 4
|
133
136
|
summary: Simple Ruby FFI wrappers for things I need from ICU.
|
134
|
-
test_files:
|
135
|
-
- spec/break_iterator_spec.rb
|
136
|
-
- spec/chardet_spec.rb
|
137
|
-
- spec/collation_spec.rb
|
138
|
-
- spec/lib/version_info_spec.rb
|
139
|
-
- spec/lib_spec.rb
|
140
|
-
- spec/locale_spec.rb
|
141
|
-
- spec/normalization_spec.rb
|
142
|
-
- spec/normalizer_spec.rb
|
143
|
-
- spec/number_formatting_spec.rb
|
144
|
-
- spec/spec_helper.rb
|
145
|
-
- spec/time_spec.rb
|
146
|
-
- spec/transliteration_spec.rb
|
147
|
-
- spec/uchar_spec.rb
|
137
|
+
test_files: []
|