ffi-icu 0.4.1 → 0.4.2

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