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 +4 -4
- data/lib/ffi-icu/lib/util.rb +14 -2
- data/lib/ffi-icu/lib.rb +12 -2
- data/lib/ffi-icu/time_formatting.rb +125 -14
- data/lib/ffi-icu/uchar.rb +4 -0
- data/lib/ffi-icu/version.rb +1 -1
- data/spec/time_spec.rb +86 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 357d3d218075dcfd56e963cd261c5fbe947d8daeb5d5826aebce8f1bedfe7ffe
|
4
|
+
data.tar.gz: afb2c28c1730cf05652433a1a721447385de47fa0fd105cb9782fe7496c55d65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a257ab16df440695f42f6db486950e62fa1c08655324ad59d347eeb0e02d46c37581f04845cf035c528c7223a8e3d092a9bac877f7da5da10daee18e701d9aa3
|
7
|
+
data.tar.gz: be241d1659c60288c84f7844efaec71d368c1c650b4c1be410cbb0ff99cf6f08f468af30fd02a5ac050d853b18e243250dcc199c81815a5d2f7b1bf9bd783559
|
data/lib/ffi-icu/lib/util.rb
CHANGED
@@ -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
|
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
|
-
|
41
|
-
|
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
|
100
|
-
date_style
|
101
|
-
locale = options[:locale] || 'C'
|
102
|
-
tz_style
|
103
|
-
time_zone
|
104
|
-
skeleton
|
105
|
-
|
106
|
-
|
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
|
-
|
186
|
-
pattern_len = pattern_str.size
|
200
|
+
set_date_format_impl(localized, pattern_str)
|
187
201
|
|
188
|
-
|
189
|
-
|
190
|
-
|
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(:
|
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
data/lib/ffi-icu/version.rb
CHANGED
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
|