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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 357d3d218075dcfd56e963cd261c5fbe947d8daeb5d5826aebce8f1bedfe7ffe
4
- data.tar.gz: afb2c28c1730cf05652433a1a721447385de47fa0fd105cb9782fe7496c55d65
3
+ metadata.gz: 77b5088964964f78d79893dc7260d5e01dde9e701c62877eb19556ecc3ee460b
4
+ data.tar.gz: b008d71ecdd7360bbe8bf24544f888a71fede1934a5fdaf1bb46b121b21be95e
5
5
  SHA512:
6
- metadata.gz: a257ab16df440695f42f6db486950e62fa1c08655324ad59d347eeb0e02d46c37581f04845cf035c528c7223a8e3d092a9bac877f7da5da10daee18e701d9aa3
7
- data.tar.gz: be241d1659c60288c84f7844efaec71d368c1c650b4c1be410cbb0ff99cf6f08f468af30fd02a5ac050d853b18e243250dcc199c81815a5d2f7b1bf9bd783559
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-get install -y libicu-dev
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
- - arch: arm64
28
+ - rvm: truffleruby
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ffi-icu [![Build Status](https://travis-ci.org/erickguan/ffi-icu.svg?branch=master)](https://travis-ci.org/erickguan/ffi-icu)
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.+;&gt;\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
@@ -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 = ICU::Lib.ucol_getRules(@c, length)
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
- begin
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
- skeleton_str << 'a' unless skeleton_str.include? 'a'
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
@@ -1,3 +1,3 @@
1
1
  module ICU
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/ffi-icu.rb CHANGED
@@ -33,3 +33,4 @@ require "ffi-icu/normalizer"
33
33
  require "ffi-icu/break_iterator"
34
34
  require "ffi-icu/number_formatting"
35
35
  require "ffi-icu/time_formatting"
36
+ require "ffi-icu/duration_formatting"
@@ -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.2
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.0.9
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: []