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