ffi-icu 0.4.1 → 0.4.2

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: 448bbb49f51d0f81ab1aba8425c9c814fa9bb2ca06317022f1bc703769d87838
4
- data.tar.gz: 27331bf11c467dc52c4ddb4bfbee954d7b0682dbeb90e802482631640e0db0fb
3
+ metadata.gz: 357d3d218075dcfd56e963cd261c5fbe947d8daeb5d5826aebce8f1bedfe7ffe
4
+ data.tar.gz: afb2c28c1730cf05652433a1a721447385de47fa0fd105cb9782fe7496c55d65
5
5
  SHA512:
6
- metadata.gz: d6d001728de2428460f163248ed9394680e9ca5509aa0644b9290de294993637ff982ec0fe7e87f40f3f1fba4a6b9dfe542147ed9a699588aae9aa80b5d01c59
7
- data.tar.gz: 3c9fa41df884315d1d4a9e726b4d55c20d1d962c6e9abce260a9ab68a9ae12cb4ba0c7a8fb0f04628b9e9497c41594a5394a994f2b154aad4da1d177f4997abd
6
+ metadata.gz: a257ab16df440695f42f6db486950e62fa1c08655324ad59d347eeb0e02d46c37581f04845cf035c528c7223a8e3d092a9bac877f7da5da10daee18e701d9aa3
7
+ data.tar.gz: be241d1659c60288c84f7844efaec71d368c1c650b4c1be410cbb0ff99cf6f08f468af30fd02a5ac050d853b18e243250dcc199c81815a5d2f7b1bf9bd783559
@@ -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
@@ -37,8 +37,12 @@ module ICU
37
37
  [find_lib("libicui18n.#{FFI::Platform::LIBSUFFIX}.??"),
38
38
  find_lib("libicutu.#{FFI::Platform::LIBSUFFIX}.??")]
39
39
  when :osx
40
- # See https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11_0_1-release-notes (62986286)
41
- 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)
42
46
  ["libicucore.#{FFI::Platform::LIBSUFFIX}"]
43
47
  else
44
48
  [find_lib("libicucore.#{FFI::Platform::LIBSUFFIX}")]
@@ -430,6 +434,10 @@ module ICU
430
434
  :medium, 2,
431
435
  :short, 3,
432
436
  ]
437
+ enum :uloc_data_locale_type, [
438
+ :actual_locale, 0,
439
+ :valid_locale, 1,
440
+ ]
433
441
  attach_function :udat_open, "udat_open#{suffix}", [:date_format_style, :date_format_style, :string, :pointer, :int32_t, :pointer, :int32_t, :pointer ], :pointer
434
442
  attach_function :udat_close, "unum_close#{suffix}", [:pointer], :void
435
443
  attach_function :udat_format, "udat_format#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t
@@ -438,7 +446,9 @@ module ICU
438
446
  attach_function :udat_applyPattern, "udat_applyPattern#{suffix}", [:pointer, :bool , :pointer, :int32_t ], :void
439
447
  # skeleton pattern
440
448
  attach_function :udatpg_open, "udatpg_open#{suffix}", [:string, :pointer], :pointer
449
+ attach_function :udatpg_close, "udatpg_close#{suffix}", [:pointer], :void
441
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
442
452
  # tz
443
453
  attach_function :ucal_setDefaultTimeZone, "ucal_setDefaultTimeZone#{suffix}", [:pointer, :pointer], :int32_t
444
454
  attach_function :ucal_getDefaultTimeZone, "ucal_getDefaultTimeZone#{suffix}", [:pointer, :int32_t, :pointer], :int32_t
@@ -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,6 +124,8 @@ 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)
@@ -182,12 +197,11 @@ 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
207
  def skeleton_format(skeleton_pattern_str, locale)
@@ -198,7 +212,7 @@ module ICU
198
212
  pattern_ptr = UCharPointer.new(needed_length)
199
213
 
200
214
  udatpg_ptr = Lib.check_error { |error| Lib.udatpg_open(locale, error) }
201
- generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udat_close))
215
+ generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udatpg_close))
202
216
 
203
217
  retried = false
204
218
 
@@ -215,6 +229,103 @@ module ICU
215
229
  retry
216
230
  end
217
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
+ skeleton_str << 'a' unless skeleton_str.include? 'a'
258
+ else
259
+ skeleton_str.gsub!('a', '')
260
+ end
261
+
262
+ # Convert the skeleton back to a pattern
263
+ new_pattern_str = skeleton_to_pattern_uchar(UCharPointer.from_string(skeleton_str)).string
264
+
265
+ # We also need to manipulate the _pattern_, a little bit, because (according to Firefox source):
266
+ #
267
+ # Input skeletons don't differentiate between "K" and "h" resp. "k" and "H".
268
+ #
269
+ # https://searchfox.org/mozilla-central/rev/625c3d0c8ae46502aed83f33bd530cb93e926e9f/intl/components/src/DateTimeFormat.cpp#183
270
+ # So, if we put a skeleton with a k in it into getBestPattern, it comes out with a H (and a
271
+ # skeleton with a K in it comes out with a h). Need to fix this in the generated pattern.
272
+ resolved_hour_cycle = @hour_cycle == :locale ? Locale.new(@locale).keyword('hours') : @hour_cycle
273
+
274
+ if HOUR_CYCLE_SYMS.keys.include?(resolved_hour_cycle)
275
+ new_pattern_str.gsub!(/[hHkK]/, HOUR_CYCLE_SYMS[resolved_hour_cycle])
276
+ end
277
+
278
+ # Finally, set the new pattern onto the date time formatter
279
+ set_date_format_impl(false, new_pattern_str)
280
+ end
281
+
282
+ # Load up the date formatter locale and make a generator
283
+ # Note that we _MUST_ actually use @locale as passed to us, rather than calling
284
+ # udat_getLocaleByType to look it up from @f, because the latter will throw away
285
+ # any @hours specifier in the locale, and we need it.
286
+ def datetime_pattern_generator
287
+ @datetime_pattern_generator ||= FFI::AutoPointer.new(
288
+ Lib.check_error { |error| Lib.udatpg_open(@locale, error) },
289
+ Lib.method(:udatpg_close)
290
+ )
291
+ end
292
+
293
+ def current_pattern_as_uchar
294
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
295
+ Lib.udat_toPattern(@f, false, buf, buf.length_in_uchars, error)
296
+ end
297
+ end
298
+
299
+ def pattern_to_skeleton_uchar(pattern_uchar)
300
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
301
+ Lib.udatpg_getSkeleton(
302
+ datetime_pattern_generator,
303
+ pattern_uchar, pattern_uchar.length_in_uchars,
304
+ buf, buf.length_in_uchars,
305
+ error
306
+ )
307
+ end
308
+ end
309
+
310
+ def skeleton_to_pattern_uchar(skeleton_uchar)
311
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
312
+ Lib.udatpg_getBestPattern(
313
+ datetime_pattern_generator,
314
+ skeleton_uchar, skeleton_uchar.length_in_uchars,
315
+ buf, buf.length_in_uchars,
316
+ error
317
+ )
318
+ end
319
+ end
320
+
321
+ def set_date_format_impl(localized, pattern_str)
322
+ pattern = UCharPointer.from_string(pattern_str)
323
+ pattern_len = pattern_str.size
324
+
325
+ Lib.check_error do |error|
326
+ needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
327
+ end
328
+ end
218
329
  end # DateTimeFormatter
219
330
  end # Formatting
220
331
  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.1"
2
+ VERSION = "0.4.2"
3
3
  end
data/spec/time_spec.rb CHANGED
@@ -95,6 +95,92 @@ 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
+ context '@hours keyword' do
132
+ before(:each) do
133
+ skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67
134
+ end
135
+
136
+ it 'works with @hours=h11 keyword' do
137
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
138
+ locale = Locale.new(locale_name).with_keyword('hours', 'h11').to_s
139
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
140
+ expect(str).to match(/0:05/i)
141
+ expect(str).to match(/(pm|下午)/i)
142
+ end
143
+ it 'works with @hours=h12 keyword' do
144
+ t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00")
145
+ locale = Locale.new(locale_name).with_keyword('hours', 'h12').to_s
146
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
147
+ expect(str).to match(/12:05/i)
148
+ expect(str).to match(/(pm|下午)/i)
149
+ end
150
+
151
+ it 'works with @hours=h23 keyword' do
152
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
153
+ locale = Locale.new(locale_name).with_keyword('hours', 'h23').to_s
154
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
155
+ expect(str).to match(/0:05/i)
156
+ expect(str).to_not match(/(am|pm)/i)
157
+ end
158
+
159
+ it 'works with @hours=h24 keyword' do
160
+ t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00")
161
+ locale = Locale.new(locale_name).with_keyword('hours', 'h24').to_s
162
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale)
163
+ expect(str).to match(/24:05/i)
164
+ expect(str).to_not match(/(am|pm)/i)
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ it 'works with defaults on a h12 locale' do
171
+ t = Time.new(2021, 04, 01, 13, 05, 0, "+00:00")
172
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: 'en_AU', zone: 'UTC', hour_cycle: :locale)
173
+ expect(str).to match(/1:05/i)
174
+ expect(str).to match(/pm/i)
175
+ end
176
+
177
+ it 'works with defaults on a h23 locale' do
178
+ t = Time.new(2021, 04, 01, 0, 05, 0, "+00:00")
179
+ str = TimeFormatting.format(t, time: :short, date: :none, locale: 'fr_FR', zone: 'UTC', hour_cycle: :locale)
180
+ expect(str).to match(/0:05/i)
181
+ expect(str).to_not match(/(am|pm)/i)
182
+ end
183
+ end
98
184
  end
99
185
  end
100
186
  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.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jari Bakken