ffi-icu 0.4.0 → 0.4.3

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: 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