ffi-icu 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2066f9d1a113f07fb761d2c97f742e6cad8dde11b26c0e4d1da2ed8a85567297
4
- data.tar.gz: e6df31c42fc3e518d8d19a98dec0b5aeed37ae90da3fd7570f32ef58397d531d
3
+ metadata.gz: e05cacd8501af573e102abe908556c1ea0f2eb1990b560271890c9d0827feb09
4
+ data.tar.gz: 4c5433c4183b376548f20ff533311d1acc61035d395b63a35f8e89a387ca7d98
5
5
  SHA512:
6
- metadata.gz: bf39178c2893939c57a8e7810d407da635ceff9b4cc5ddbe60360df53a0bf7f35b17d8f4036cf001d85edf4fa83efd8f4ed1b91d6cdb21e54d52810f536bf7ce
7
- data.tar.gz: ee8c887487e3c3a0c0511e7017753b4801da3fc6a73e4d476211452026fd0ae3d8ae0d4c35be9eb5ed8827df8b757bc985dc5f5eafc77a12c872ca37347e9924
6
+ metadata.gz: a40fa82617a2c16aa759b263e5153e4135b5844823666ac5d0b5d205b300819f3ac1cf10d5e8e9f9182c2357080e6ed6add40388287b308ef6eae645f052a38f
7
+ data.tar.gz: 4d34feb23961b844cc9701d09a52a806f375bc58b10a04622d2230d57a1a97cd015f004ed72239a039041e901d304deb4051f81400191981116b7b055ef8b15b
@@ -28,7 +28,19 @@ module ICU
28
28
  result.read_string(length)
29
29
  end
30
30
 
31
- def self.read_uchar_buffer(length)
31
+ def self.read_uchar_buffer(length, &blk)
32
+ buf, len = read_uchar_buffer_as_ptr_impl(length, &blk)
33
+ buf.string(len)
34
+ end
35
+
36
+ def self.read_uchar_buffer_as_ptr(length, &blk)
37
+ buf, _ = read_uchar_buffer_as_ptr_impl(length, &blk)
38
+ buf
39
+ end
40
+
41
+ private
42
+
43
+ def self.read_uchar_buffer_as_ptr_impl(length)
32
44
  attempts = 0
33
45
 
34
46
  begin
@@ -40,7 +52,7 @@ module ICU
40
52
  raise BufferOverflowError, "needed: #{length}"
41
53
  end
42
54
 
43
- result.string(length)
55
+ [result, length]
44
56
  end
45
57
  end
46
58
  end
data/lib/ffi-icu/lib.rb CHANGED
@@ -19,9 +19,7 @@ module ICU
19
19
  '/usr/local/{lib64,lib}',
20
20
  '/opt/local/{lib64,lib}',
21
21
  '/usr/{lib64,lib}',
22
- '/usr/lib/x86_64-linux-gnu', # for Debian Multiarch http://wiki.debian.org/Multiarch
23
- '/usr/lib/i386-linux-gnu', # for Debian Multiarch
24
- ]
22
+ ] + Dir['/usr/lib/*-linux-gnu'] # for Debian Multiarch http://wiki.debian.org/Multiarch
25
23
  end
26
24
  end
27
25
  end
@@ -39,8 +37,12 @@ module ICU
39
37
  [find_lib("libicui18n.#{FFI::Platform::LIBSUFFIX}.??"),
40
38
  find_lib("libicutu.#{FFI::Platform::LIBSUFFIX}.??")]
41
39
  when :osx
42
- # See https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11_0_1-release-notes (62986286)
43
- if Gem::Version.new(`sw_vers -productVersion`) >= Gem::Version.new('11')
40
+ if ENV.key?('FFI_ICU_LIB')
41
+ # Ensure we look in the user-supplied override dir for a user-compiled libicu
42
+ [find_lib("libicui18n.??.#{FFI::Platform::LIBSUFFIX}"),
43
+ find_lib("libicutu.??.#{FFI::Platform::LIBSUFFIX}")]
44
+ elsif Gem::Version.new(`sw_vers -productVersion`) >= Gem::Version.new('11')
45
+ # See https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11_0_1-release-notes (62986286)
44
46
  ["libicucore.#{FFI::Platform::LIBSUFFIX}"]
45
47
  else
46
48
  [find_lib("libicucore.#{FFI::Platform::LIBSUFFIX}")]
@@ -432,6 +434,10 @@ module ICU
432
434
  :medium, 2,
433
435
  :short, 3,
434
436
  ]
437
+ enum :uloc_data_locale_type, [
438
+ :actual_locale, 0,
439
+ :valid_locale, 1,
440
+ ]
435
441
  attach_function :udat_open, "udat_open#{suffix}", [:date_format_style, :date_format_style, :string, :pointer, :int32_t, :pointer, :int32_t, :pointer ], :pointer
436
442
  attach_function :udat_close, "unum_close#{suffix}", [:pointer], :void
437
443
  attach_function :udat_format, "udat_format#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t
@@ -440,7 +446,9 @@ module ICU
440
446
  attach_function :udat_applyPattern, "udat_applyPattern#{suffix}", [:pointer, :bool , :pointer, :int32_t ], :void
441
447
  # skeleton pattern
442
448
  attach_function :udatpg_open, "udatpg_open#{suffix}", [:string, :pointer], :pointer
449
+ attach_function :udatpg_close, "udatpg_close#{suffix}", [:pointer], :void
443
450
  attach_function :udatpg_getBestPattern, "udatpg_getBestPattern#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t
451
+ attach_function :udatpg_getSkeleton, "udatpg_getSkeleton#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t
444
452
  # tz
445
453
  attach_function :ucal_setDefaultTimeZone, "ucal_setDefaultTimeZone#{suffix}", [:pointer, :pointer], :int32_t
446
454
  attach_function :ucal_getDefaultTimeZone, "ucal_getDefaultTimeZone#{suffix}", [:pointer, :int32_t, :pointer], :int32_t
@@ -68,8 +68,19 @@ module ICU
68
68
  case number
69
69
  when Float
70
70
  needed_length = Lib.unum_format_double(@f, number, out_ptr, needed_length, nil, error)
71
- when Fixnum
72
- needed_length = Lib.unum_format_int32(@f, number, out_ptr, needed_length, nil, error)
71
+ when Integer
72
+ begin
73
+ # Try doing it fast, for integers that can be marshaled into an int64_t
74
+ needed_length = Lib.unum_format_int64(@f, number, out_ptr, needed_length, nil, error)
75
+ rescue RangeError
76
+ # Fall back to stringifying in Ruby and passing that to ICU
77
+ unless defined? Lib.unum_format_decimal
78
+ raise RangeError,"Number #{number} is too big to fit in int64_t and your "\
79
+ "ICU version is too old to have unum_format_decimal"
80
+ end
81
+ string_version = number.to_s
82
+ needed_length = Lib.unum_format_decimal(@f, string_version, string_version.bytesize, out_ptr, needed_length, nil, error)
83
+ end
73
84
  when BigDecimal
74
85
  string_version = number.to_s('F')
75
86
  if Lib.respond_to? :unum_format_decimal
@@ -77,8 +88,6 @@ module ICU
77
88
  else
78
89
  needed_length = Lib.unum_format_double(@f, number.to_f, out_ptr, needed_length, nil, error)
79
90
  end
80
- when Bignum
81
- needed_length = Lib.unum_format_int64(@f, number, out_ptr, needed_length, nil, error)
82
91
  end
83
92
  end
84
93
  out_ptr.string needed_length
@@ -38,6 +38,14 @@ module ICU
38
38
  # (for example, "Unknown City"), such as Los Angeles
39
39
  # see: http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
40
40
  }
41
+
42
+ HOUR_CYCLE_SYMS = {
43
+ 'h11' => 'K',
44
+ 'h12' => 'h',
45
+ 'h23' => 'H',
46
+ 'h24' => 'k',
47
+ :locale => 'j',
48
+ }
41
49
  @default_options = {}
42
50
 
43
51
  def self.create(options = {})
@@ -96,14 +104,19 @@ module ICU
96
104
 
97
105
  class DateTimeFormatter < BaseFormatter
98
106
  def initialize(options={})
99
- time_style = options[:time] || :short
100
- date_style = options[:date] || :short
101
- locale = options[:locale] || 'C'
102
- tz_style = options[:tz_style]
103
- time_zone = options[:zone]
104
- skeleton = options[:skeleton]
105
-
106
- @f = make_formatter(time_style, date_style, locale, time_zone, skeleton)
107
+ time_style = options[:time] || :short
108
+ date_style = options[:date] || :short
109
+ @locale = options[:locale] || 'C'
110
+ tz_style = options[:tz_style]
111
+ time_zone = options[:zone]
112
+ skeleton = options[:skeleton]
113
+ @hour_cycle = options[:hour_cycle]
114
+
115
+ if @hour_cycle && !HOUR_CYCLE_SYMS.keys.include?(@hour_cycle)
116
+ raise ICU::Error.new("Unknown hour cycle #{@hour_cycle}")
117
+ end
118
+
119
+ @f = make_formatter(time_style, date_style, @locale, time_zone, skeleton)
107
120
  if tz_style
108
121
  f0 = date_format(true)
109
122
  f1 = update_tz_format(f0, tz_style)
@@ -111,11 +124,13 @@ module ICU
111
124
  set_date_format(true, f1)
112
125
  end
113
126
  end
127
+
128
+ replace_hour_symbol!
114
129
  end
115
130
 
116
131
  def parse(str)
117
132
  str_u = UCharPointer.from_string(str)
118
- str_l = str_u.size
133
+ str_l = str.size
119
134
  Lib.check_error do |error|
120
135
  ret = Lib.udat_parse(@f, str_u, str_l, nil, error)
121
136
  Time.at(ret / 1000.0)
@@ -182,28 +197,28 @@ module ICU
182
197
  end
183
198
 
184
199
  def set_date_format(localized, pattern_str)
185
- pattern = UCharPointer.from_string(pattern_str)
186
- pattern_len = pattern_str.size
200
+ set_date_format_impl(localized, pattern_str)
187
201
 
188
- Lib.check_error do |error|
189
- needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
190
- end
202
+ # After setting the date format string, we need to ensure that any hour
203
+ # symbols were properly localised according to @hour_cycle.
204
+ replace_hour_symbol!
191
205
  end
192
206
 
193
- def skeleton_format(pattern, locale)
194
- pattern = UCharPointer.from_string(pattern)
207
+ def skeleton_format(skeleton_pattern_str, locale)
208
+ skeleton_pattern_ptr = UCharPointer.from_string(skeleton_pattern_str)
209
+ skeleton_pattern_len = skeleton_pattern_str.size
195
210
 
196
211
  needed_length = 0
197
212
  pattern_ptr = UCharPointer.new(needed_length)
198
213
 
199
214
  udatpg_ptr = Lib.check_error { |error| Lib.udatpg_open(locale, error) }
200
- generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udat_close))
215
+ generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udatpg_close))
201
216
 
202
217
  retried = false
203
218
 
204
219
  begin
205
220
  Lib.check_error do |error|
206
- needed_length = Lib.udatpg_getBestPattern(generator, pattern, pattern.size, pattern_ptr, needed_length, error)
221
+ needed_length = Lib.udatpg_getBestPattern(generator, skeleton_pattern_ptr, skeleton_pattern_len, pattern_ptr, needed_length, error)
207
222
  end
208
223
 
209
224
  return needed_length, pattern_ptr
@@ -214,6 +229,106 @@ module ICU
214
229
  retry
215
230
  end
216
231
  end
232
+
233
+ private
234
+
235
+ # Converts the current pattern to a pattern that takes the desired hour cycle
236
+ # into account. This is needed because most of the standard patterns in ICU
237
+ # contain either h (12 hour) or H (23 hour) in them, instead of j (locale-
238
+ # specified hour cycle). This means if you use a locale with an @hours=h12
239
+ # keyword in it, for example, it would normally be totally ignored by ICU.
240
+ #
241
+ # This is the same fixup done by Firefox:
242
+ # https://github.com/tc39/ecma402/issues/665#issuecomment-1084833809
243
+ # https://searchfox.org/mozilla-central/rev/625c3d0c8ae46502aed83f33bd530cb93e926e9f/intl/components/src/DateTimeFormat.cpp#282-323
244
+ def replace_hour_symbol!
245
+ # Short circuit this case - nil means "use whatever is in the pattern already", so
246
+ # no need to actually run any of this implementation.
247
+ return unless @hour_cycle
248
+
249
+ # Get the current pattern and convert to a skeleton
250
+ skeleton_str = pattern_to_skeleton_uchar(current_pattern_as_uchar).string
251
+
252
+ # Manipulate the skeleton to make it work with the correct hour cycle.
253
+ skeleton_str.gsub!(/[hHkKjJ]/, HOUR_CYCLE_SYMS[@hour_cycle])
254
+
255
+ # Either ensure the skeleton has, or does not have, am/pm, as appropriate
256
+ if ['h11', 'h12'].include?(@hour_cycle)
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
261
+ else
262
+ skeleton_str.gsub!('a', '')
263
+ end
264
+
265
+ # Convert the skeleton back to a pattern
266
+ new_pattern_str = skeleton_to_pattern_uchar(UCharPointer.from_string(skeleton_str)).string
267
+
268
+ # We also need to manipulate the _pattern_, a little bit, because (according to Firefox source):
269
+ #
270
+ # Input skeletons don't differentiate between "K" and "h" resp. "k" and "H".
271
+ #
272
+ # https://searchfox.org/mozilla-central/rev/625c3d0c8ae46502aed83f33bd530cb93e926e9f/intl/components/src/DateTimeFormat.cpp#183
273
+ # So, if we put a skeleton with a k in it into getBestPattern, it comes out with a H (and a
274
+ # skeleton with a K in it comes out with a h). Need to fix this in the generated pattern.
275
+ resolved_hour_cycle = @hour_cycle == :locale ? Locale.new(@locale).keyword('hours') : @hour_cycle
276
+
277
+ if HOUR_CYCLE_SYMS.keys.include?(resolved_hour_cycle)
278
+ new_pattern_str.gsub!(/[hHkK]/, HOUR_CYCLE_SYMS[resolved_hour_cycle])
279
+ end
280
+
281
+ # Finally, set the new pattern onto the date time formatter
282
+ set_date_format_impl(false, new_pattern_str)
283
+ end
284
+
285
+ # Load up the date formatter locale and make a generator
286
+ # Note that we _MUST_ actually use @locale as passed to us, rather than calling
287
+ # udat_getLocaleByType to look it up from @f, because the latter will throw away
288
+ # any @hours specifier in the locale, and we need it.
289
+ def datetime_pattern_generator
290
+ @datetime_pattern_generator ||= FFI::AutoPointer.new(
291
+ Lib.check_error { |error| Lib.udatpg_open(@locale, error) },
292
+ Lib.method(:udatpg_close)
293
+ )
294
+ end
295
+
296
+ def current_pattern_as_uchar
297
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
298
+ Lib.udat_toPattern(@f, false, buf, buf.length_in_uchars, error)
299
+ end
300
+ end
301
+
302
+ def pattern_to_skeleton_uchar(pattern_uchar)
303
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
304
+ Lib.udatpg_getSkeleton(
305
+ datetime_pattern_generator,
306
+ pattern_uchar, pattern_uchar.length_in_uchars,
307
+ buf, buf.length_in_uchars,
308
+ error
309
+ )
310
+ end
311
+ end
312
+
313
+ def skeleton_to_pattern_uchar(skeleton_uchar)
314
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
315
+ Lib.udatpg_getBestPattern(
316
+ datetime_pattern_generator,
317
+ skeleton_uchar, skeleton_uchar.length_in_uchars,
318
+ buf, buf.length_in_uchars,
319
+ error
320
+ )
321
+ end
322
+ end
323
+
324
+ def set_date_format_impl(localized, pattern_str)
325
+ pattern = UCharPointer.from_string(pattern_str)
326
+ pattern_len = pattern_str.size
327
+
328
+ Lib.check_error do |error|
329
+ needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
330
+ end
331
+ end
217
332
  end # DateTimeFormatter
218
333
  end # Formatting
219
334
  end # ICU
data/lib/ffi-icu/uchar.rb CHANGED
@@ -43,6 +43,10 @@ module ICU
43
43
  wstring.pack("U*")
44
44
  end
45
45
 
46
+ def length_in_uchars
47
+ size / type_size
48
+ end
49
+
46
50
 
47
51
  end # UCharPointer
48
52
  end # ICU
@@ -1,3 +1,3 @@
1
1
  module ICU
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.3"
3
3
  end
@@ -69,6 +69,11 @@ module ICU
69
69
  expect { NumberFormatting.create('en-US', :currency, style: :iso) }.to raise_error(StandardError)
70
70
  end
71
71
  end
72
+
73
+ it 'should format a bignum' do
74
+ str = NumberFormatting.format_number("en", 1_000_000_000_000_000_000_000_000_000_000_000_000_000)
75
+ expect(str).to eq('1,000,000,000,000,000,000,000,000,000,000,000,000,000')
76
+ end
72
77
  end
73
78
  end # NumberFormatting
74
79
  end # ICU
data/spec/time_spec.rb CHANGED
@@ -95,6 +95,98 @@ module ICU
95
95
  expect(f4.date_format(true)).to eq("MMM y")
96
96
  end
97
97
  end
98
+
99
+ context 'hour cycle' do
100
+ # en_AU normally is 12 hours, fr_FR is normally 23 hours
101
+ ['en_AU', 'fr_FR', 'zh_CN'].each do |locale_name|
102
+ context "with locale #{locale_name}" do
103
+ it 'works with hour_cycle: h11' do
104
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
105
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h11')
106
+ expect(str).to match(/0:05/i)
107
+ expect(str).to match(/(pm|下午)/i)
108
+ end
109
+
110
+ it 'works with hour_cycle: h12' do
111
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
112
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h12')
113
+ expect(str).to match(/12:05/i)
114
+ expect(str).to match(/(pm|下午)/i)
115
+ end
116
+
117
+ it 'works with hour_cycle: h23' do
118
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
119
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h23')
120
+ expect(str).to match(/0:05/i)
121
+ expect(str).to_not match(/(am|pm)/i)
122
+ end
123
+
124
+ it 'works with hour_cycle: h24' do
125
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
126
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h24')
127
+ expect(str).to match(/24:05/i)
128
+ expect(str).to_not match(/(am|pm)/i)
129
+ end
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
+
137
+ context '@hours keyword' do
138
+ before(:each) do
139
+ skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67
140
+ end
141
+
142
+ it 'works with @hours=h11 keyword' do
143
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
144
+ locale = Locale.new(locale_name).with_keyword('hours', 'h11').to_s
145
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
146
+ expect(str).to match(/0:05/i)
147
+ expect(str).to match(/(pm|下午)/i)
148
+ end
149
+ it 'works with @hours=h12 keyword' do
150
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
151
+ locale = Locale.new(locale_name).with_keyword('hours', 'h12').to_s
152
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
153
+ expect(str).to match(/12:05/i)
154
+ expect(str).to match(/(pm|下午)/i)
155
+ end
156
+
157
+ it 'works with @hours=h23 keyword' do
158
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
159
+ locale = Locale.new(locale_name).with_keyword('hours', 'h23').to_s
160
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
161
+ expect(str).to match(/0:05/i)
162
+ expect(str).to_not match(/(am|pm)/i)
163
+ end
164
+
165
+ it 'works with @hours=h24 keyword' do
166
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
167
+ locale = Locale.new(locale_name).with_keyword('hours', 'h24').to_s
168
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
169
+ expect(str).to match(/24:05/i)
170
+ expect(str).to_not match(/(am|pm)/i)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ it 'works with defaults on a h12 locale' do
177
+ t = Time.new(2021, 04, 01, 13, 05, 0, "+00:00")
178
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: 'en_AU', zone: 'UTC', hour_cycle: :locale)
179
+ expect(str).to match(/1:05/i)
180
+ expect(str).to match(/pm/i)
181
+ end
182
+
183
+ it 'works with defaults on a h23 locale' do
184
+ t = Time.new(2021, 04, 01, 0, 05, 0, "+00:00")
185
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: 'fr_FR', zone: 'UTC', hour_cycle: :locale)
186
+ expect(str).to match(/0:05/i)
187
+ expect(str).to_not match(/(am|pm)/i)
188
+ end
189
+ end
98
190
  end
99
191
  end
100
192
  end
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.0
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jari Bakken