ffi-icu 0.5.3 → 0.6.0
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/Gemfile +10 -0
- data/LICENSE +1 -1
- data/README.md +21 -51
- data/Rakefile +4 -5
- data/ffi-icu.gemspec +34 -25
- data/lib/ffi-icu/break_iterator.rb +19 -18
- data/lib/ffi-icu/chardet.rb +12 -13
- data/lib/ffi-icu/collation.rb +62 -59
- data/lib/ffi-icu/duration_formatting.rb +293 -267
- data/lib/ffi-icu/lib/util.rb +10 -10
- data/lib/ffi-icu/lib.rb +273 -202
- data/lib/ffi-icu/locale.rb +12 -8
- data/lib/ffi-icu/normalization.rb +7 -7
- data/lib/ffi-icu/normalizer.rb +14 -8
- data/lib/ffi-icu/number_formatting.rb +41 -27
- data/lib/ffi-icu/time_formatting.rb +116 -93
- data/lib/ffi-icu/transliteration.rb +19 -19
- data/lib/ffi-icu/uchar.rb +14 -17
- data/lib/ffi-icu/version.rb +3 -1
- data/lib/ffi-icu.rb +16 -17
- metadata +35 -71
- data/.document +0 -5
- data/.gitignore +0 -23
- data/.rspec +0 -2
- data/.travis.yml +0 -28
- data/benchmark/detect.rb +0 -14
- data/benchmark/shared.rb +0 -17
- data/build_icu.sh +0 -53
- data/lib/ffi-icu/core_ext/string.rb +0 -9
- data/spec/break_iterator_spec.rb +0 -77
- data/spec/chardet_spec.rb +0 -42
- data/spec/collation_spec.rb +0 -84
- data/spec/duration_formatting_spec.rb +0 -143
- data/spec/lib/version_info_spec.rb +0 -20
- data/spec/lib_spec.rb +0 -63
- data/spec/locale_spec.rb +0 -280
- data/spec/normalization_spec.rb +0 -22
- data/spec/normalizer_spec.rb +0 -57
- data/spec/number_formatting_spec.rb +0 -79
- data/spec/spec_helper.rb +0 -13
- data/spec/time_spec.rb +0 -198
- data/spec/transliteration_spec.rb +0 -36
- data/spec/uchar_spec.rb +0 -34
- data/test.c +0 -56
|
@@ -1,282 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
1
5
|
module ICU
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
6
|
+
module DurationFormatting
|
|
7
|
+
VALID_FIELDS = [
|
|
8
|
+
:years,
|
|
9
|
+
:months,
|
|
10
|
+
:weeks,
|
|
11
|
+
:days,
|
|
12
|
+
:hours,
|
|
13
|
+
:minutes,
|
|
14
|
+
:seconds,
|
|
15
|
+
:milliseconds,
|
|
16
|
+
:microseconds,
|
|
17
|
+
:nanoseconds
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
HMS_FIELDS = [
|
|
21
|
+
:hours,
|
|
22
|
+
:minutes,
|
|
23
|
+
:seconds,
|
|
24
|
+
:milliseconds,
|
|
25
|
+
:microseconds,
|
|
26
|
+
:nanoseconds
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
ROUNDABLE_FIELDS = [
|
|
30
|
+
:seconds,
|
|
31
|
+
:milliseconds,
|
|
32
|
+
:microseconds,
|
|
33
|
+
:nanoseconds
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
VALID_STYLES = [:long, :short, :narrow, :digital].freeze
|
|
37
|
+
|
|
38
|
+
STYLES_TO_LIST_JOIN_FORMAT = {
|
|
39
|
+
:long => :wide,
|
|
40
|
+
:short => :short,
|
|
41
|
+
:narrow => :narrow,
|
|
42
|
+
:digital => :narrow
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
UNIT_FORMAT_STRINGS = {
|
|
46
|
+
:years => 'measure-unit/duration-year',
|
|
47
|
+
:months => 'measure-unit/duration-month',
|
|
48
|
+
:weeks => 'measure-unit/duration-week',
|
|
49
|
+
:days => 'measure-unit/duration-day',
|
|
50
|
+
:hours => 'measure-unit/duration-hour',
|
|
51
|
+
:minutes => 'measure-unit/duration-minute',
|
|
52
|
+
:seconds => 'measure-unit/duration-second',
|
|
53
|
+
:milliseconds => 'measure-unit/duration-millisecond',
|
|
54
|
+
:microseconds => 'measure-unit/duration-microsecond',
|
|
55
|
+
:nanoseconds => 'measure-unit/duration-nanosecond'
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
STYLES_TO_NUMBER_FORMAT_WIDTH = {
|
|
59
|
+
:long => 'unit-width-full-name',
|
|
60
|
+
:short => 'unit-width-short',
|
|
61
|
+
:narrow => 'unit-width-narrow',
|
|
62
|
+
# digital for hours:minutes:seconds has some special casing.
|
|
63
|
+
:digital => 'unit-width-narrow'
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
def self.format(fields, locale:, style: :long)
|
|
67
|
+
DurationFormatter.new(locale:, style:).format(fields)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class DurationFormatter
|
|
71
|
+
def initialize(locale:, style: :long)
|
|
72
|
+
if !Lib.respond_to?(:unumf_openForSkeletonAndLocale) || !Lib.respond_to?(:ulistfmt_openForType)
|
|
73
|
+
raise('ICU::DurationFormatting requires ICU >= 67')
|
|
58
74
|
end
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
def initialize(locale:, style: :long)
|
|
62
|
-
if !Lib.respond_to?(:unumf_openForSkeletonAndLocale) || !Lib.respond_to?(:ulistfmt_openForType)
|
|
63
|
-
raise "ICU::DurationFormatting requires ICU >= 67"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
raise ArgumentError, "Unknown style #{style}" unless VALID_STYLES.include?(style)
|
|
67
|
-
|
|
68
|
-
@locale = locale
|
|
69
|
-
@style = style
|
|
70
|
-
# These are created lazily based on what parts are actually included
|
|
71
|
-
@number_formatters = {}
|
|
72
|
-
|
|
73
|
-
list_join_format = STYLES_TO_LIST_JOIN_FORMAT.fetch(style)
|
|
74
|
-
@list_formatter = FFI::AutoPointer.new(
|
|
75
|
-
Lib.check_error { |error|
|
|
76
|
-
Lib.ulistfmt_openForType(@locale, :units, list_join_format, error)
|
|
77
|
-
},
|
|
78
|
-
Lib.method(:ulistfmt_close)
|
|
79
|
-
)
|
|
80
|
-
end
|
|
76
|
+
raise(ArgumentError, "Unknown style #{style}") unless VALID_STYLES.include?(style)
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fields = fields.dup # we might modify this argument.
|
|
87
|
-
|
|
88
|
-
# Intl.js spec says that rounding options affect only the smallest unit, and only
|
|
89
|
-
# if that unit is sub-second. All other fields therefore need to be truncated.
|
|
90
|
-
smallest_unit = VALID_FIELDS[fields.keys.map { |k| VALID_FIELDS.index(k) }.max]
|
|
91
|
-
fields.each_key do |k|
|
|
92
|
-
raise ArgumentError, "Negative durations are not yet supported" if fields[k] < 0
|
|
93
|
-
fields[k] = fields[k].to_i unless k == smallest_unit && ROUNDABLE_FIELDS.include?(smallest_unit)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
formatted_hms = nil
|
|
97
|
-
if @style == :digital
|
|
98
|
-
# icu::MeasureFormat contains special casing for hours/minutes/seconds formatted
|
|
99
|
-
# at numeric width, to render it as h:mm:s, essentially. This involves using
|
|
100
|
-
# a pattern called durationUnits defined in the ICU data for the locale.
|
|
101
|
-
# If we have data for this combination of hours/mins/seconds in this locale,
|
|
102
|
-
# use that and emulate ICU's special casing.
|
|
103
|
-
formatted_hms = format_hms(fields)
|
|
104
|
-
if formatted_hms
|
|
105
|
-
# We've taken care of all these fields now.
|
|
106
|
-
HMS_FIELDS.each do |f|
|
|
107
|
-
fields.delete f
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
formatted_fields = VALID_FIELDS.map do |f|
|
|
113
|
-
next unless fields.key?(f)
|
|
114
|
-
next unless fields[f] != 0
|
|
115
|
-
|
|
116
|
-
format_number(fields[f], [
|
|
117
|
-
UNIT_FORMAT_STRINGS[f], STYLES_TO_NUMBER_FORMAT_WIDTH[@style],
|
|
118
|
-
(".#########" if f == smallest_unit),
|
|
119
|
-
].compact.join(' '))
|
|
120
|
-
end
|
|
121
|
-
formatted_fields << formatted_hms
|
|
122
|
-
formatted_fields.compact!
|
|
123
|
-
|
|
124
|
-
format_list(formatted_fields)
|
|
125
|
-
end
|
|
78
|
+
@locale = locale
|
|
79
|
+
@style = style
|
|
80
|
+
# These are created lazily based on what parts are actually included
|
|
81
|
+
@number_formatters = {}
|
|
126
82
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
resource_key = "durationUnits/"
|
|
137
|
-
resource_key << "h" if fields.key?(:hours)
|
|
138
|
-
resource_key << "m" if fields.key?(:minutes)
|
|
139
|
-
resource_key << "s" if [:seconds, :milliseconds, :microseconds, :nanoseconds].any? { |f| fields.key?(f) }
|
|
140
|
-
|
|
141
|
-
begin
|
|
142
|
-
pattern_resource = FFI::AutoPointer.new(
|
|
143
|
-
Lib.check_error { |error| Lib.ures_getBykeyWithFallback(@unit_res_bundle, resource_key, nil, error) },
|
|
144
|
-
Lib.method(:ures_close)
|
|
145
|
-
)
|
|
146
|
-
rescue MissingResourceError
|
|
147
|
-
# This combination of h,m,s not present for this locale.
|
|
148
|
-
return nil
|
|
149
|
-
end
|
|
150
|
-
# Read the resource as a UChar (whose memory we _do not own_ - it's static data) and
|
|
151
|
-
# convert it to a Ruby string.
|
|
152
|
-
pattern_uchar_len = FFI::MemoryPointer.new(:int32_t)
|
|
153
|
-
pattern_uchar = Lib.check_error { |error|
|
|
154
|
-
Lib.ures_getString(pattern_resource, pattern_uchar_len, error)
|
|
155
|
-
}
|
|
156
|
-
pattern_str = pattern_uchar.read_array_of_uint16(pattern_uchar_len.read_int32).pack("U*")
|
|
157
|
-
|
|
158
|
-
# For some reason I can't comprehend, loadNumericDateFormatterPattern in ICU wants to turn
|
|
159
|
-
# h's into H's here. I guess we have to do it too because the pattern data could in theory
|
|
160
|
-
# now contain either.
|
|
161
|
-
pattern_str.gsub('h', 'H')
|
|
162
|
-
end
|
|
83
|
+
list_join_format = STYLES_TO_LIST_JOIN_FORMAT.fetch(style)
|
|
84
|
+
@list_formatter = FFI::AutoPointer.new(
|
|
85
|
+
Lib.check_error do |error|
|
|
86
|
+
Lib.ulistfmt_openForType(@locale, :units, list_join_format, error)
|
|
87
|
+
end,
|
|
88
|
+
Lib.method(:ulistfmt_close)
|
|
89
|
+
)
|
|
90
|
+
end
|
|
163
91
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
176
|
-
if fields.key?(:microseconds)
|
|
177
|
-
seconds_incl_fractional += fields[:microseconds] / 1e6
|
|
178
|
-
second_precision = 6
|
|
179
|
-
end
|
|
180
|
-
if fields.key?(:nanoseconds)
|
|
181
|
-
seconds_incl_fractional += fields[:nanoseconds] / 1e9
|
|
182
|
-
second_precision = 9
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Follow the rules in ICU measfmt.cpp formatNumeric to fill in the patterns here with
|
|
186
|
-
# the appropriate values.
|
|
187
|
-
enum = pattern.each_char
|
|
188
|
-
protect = false
|
|
189
|
-
result = ""
|
|
190
|
-
loop do
|
|
191
|
-
char = enum.next
|
|
192
|
-
next_char = enum.peek rescue nil
|
|
193
|
-
|
|
194
|
-
if protect
|
|
195
|
-
# In literal mode
|
|
196
|
-
if char == "'"
|
|
197
|
-
protect = false
|
|
198
|
-
next
|
|
199
|
-
end
|
|
200
|
-
result << char
|
|
201
|
-
next
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
value = case char
|
|
205
|
-
when 'H' then fields[:hours]
|
|
206
|
-
when 'm' then fields[:minutes]
|
|
207
|
-
when 's' then seconds_incl_fractional
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
case char
|
|
211
|
-
when 'H', 'm', 's'
|
|
212
|
-
skeleton = "."
|
|
213
|
-
if char == 's' && second_precision > 0
|
|
214
|
-
skeleton << ("0" * second_precision)
|
|
215
|
-
else
|
|
216
|
-
skeleton << ("#" * 9)
|
|
217
|
-
end
|
|
218
|
-
if char == next_char
|
|
219
|
-
# It's doubled - means format it at zero fill
|
|
220
|
-
skeleton << " integer-width/00"
|
|
221
|
-
enum.next
|
|
222
|
-
end
|
|
223
|
-
result << format_number(value, skeleton)
|
|
224
|
-
when "'"
|
|
225
|
-
if next_char == char
|
|
226
|
-
# double-apostrophe, means literal '
|
|
227
|
-
result << "'"
|
|
228
|
-
enum.next
|
|
229
|
-
else
|
|
230
|
-
protect = true
|
|
231
|
-
end
|
|
232
|
-
else
|
|
233
|
-
result << char
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
result
|
|
238
|
-
end
|
|
92
|
+
def format(fields)
|
|
93
|
+
fields.each_key do |field|
|
|
94
|
+
raise("Unknown field #{field}") unless VALID_FIELDS.include?(field)
|
|
95
|
+
end
|
|
96
|
+
fields = fields.dup # we might modify this argument.
|
|
97
|
+
|
|
98
|
+
# Intl.js spec says that rounding options affect only the smallest unit, and only
|
|
99
|
+
# if that unit is sub-second. All other fields therefore need to be truncated.
|
|
100
|
+
smallest_unit = VALID_FIELDS[fields.keys.map { |k| VALID_FIELDS.index(k) }.max]
|
|
101
|
+
fields.each_key do |k|
|
|
102
|
+
raise(ArgumentError, 'Negative durations are not yet supported') if fields[k].negative?
|
|
239
103
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
104
|
+
fields[k] = fields[k].to_i unless k == smallest_unit && ROUNDABLE_FIELDS.include?(smallest_unit)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
formatted_hms = nil
|
|
108
|
+
if @style == :digital
|
|
109
|
+
# icu::MeasureFormat contains special casing for hours/minutes/seconds formatted
|
|
110
|
+
# at numeric width, to render it as h:mm:s, essentially. This involves using
|
|
111
|
+
# a pattern called durationUnits defined in the ICU data for the locale.
|
|
112
|
+
# If we have data for this combination of hours/mins/seconds in this locale,
|
|
113
|
+
# use that and emulate ICU's special casing.
|
|
114
|
+
formatted_hms = format_hms(fields)
|
|
115
|
+
if formatted_hms
|
|
116
|
+
# We've taken care of all these fields now.
|
|
117
|
+
HMS_FIELDS.each do |f|
|
|
118
|
+
fields.delete(f)
|
|
250
119
|
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
formatted_fields = VALID_FIELDS.map do |f|
|
|
124
|
+
next unless fields.key?(f)
|
|
125
|
+
next unless fields[f] != 0
|
|
126
|
+
|
|
127
|
+
format_number(fields[f], [
|
|
128
|
+
UNIT_FORMAT_STRINGS[f], STYLES_TO_NUMBER_FORMAT_WIDTH[@style],
|
|
129
|
+
('.#########' if f == smallest_unit)
|
|
130
|
+
].compact.join(' '))
|
|
131
|
+
end
|
|
132
|
+
formatted_fields << formatted_hms
|
|
133
|
+
formatted_fields.compact!
|
|
134
|
+
|
|
135
|
+
format_list(formatted_fields)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def hms_duration_units_pattern(fields)
|
|
141
|
+
return nil unless HMS_FIELDS.any? { |k| fields.key?(k) }
|
|
251
142
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
143
|
+
@unit_res_bundle ||= FFI::AutoPointer.new(
|
|
144
|
+
Lib.check_error { |error| Lib.ures_open(Lib.resource_bundle_name(:unit), @locale, error) },
|
|
145
|
+
Lib.method(:ures_close)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
resource_key_builder = StringIO.new
|
|
149
|
+
resource_key_builder.write('durationUnits/')
|
|
150
|
+
resource_key_builder.putc('h') if fields.key?(:hours)
|
|
151
|
+
resource_key_builder.putc('m') if fields.key?(:minutes)
|
|
152
|
+
if [:seconds, :milliseconds, :microseconds, :nanoseconds].any? { |f| fields.key?(f) }
|
|
153
|
+
resource_key_builder.putc('s')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
resource_key = resource_key_builder.string
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
pattern_resource = FFI::AutoPointer.new(
|
|
160
|
+
Lib.check_error do |error|
|
|
161
|
+
Lib.ures_getBykeyWithFallback(@unit_res_bundle, resource_key, nil, error)
|
|
162
|
+
end,
|
|
163
|
+
Lib.method(:ures_close)
|
|
164
|
+
)
|
|
165
|
+
rescue MissingResourceError
|
|
166
|
+
# This combination of h,m,s not present for this locale.
|
|
167
|
+
return nil
|
|
168
|
+
end
|
|
169
|
+
# Read the resource as a UChar (whose memory we _do not own_ - it's static data) and
|
|
170
|
+
# convert it to a Ruby string.
|
|
171
|
+
pattern_uchar_len = FFI::MemoryPointer.new(:int32_t)
|
|
172
|
+
pattern_uchar = Lib.check_error do |error|
|
|
173
|
+
Lib.ures_getString(pattern_resource, pattern_uchar_len, error)
|
|
174
|
+
end
|
|
175
|
+
pattern_str = pattern_uchar.read_array_of_uint16(pattern_uchar_len.read_int32).pack('U*')
|
|
176
|
+
|
|
177
|
+
# For some reason I can't comprehend, loadNumericDateFormatterPattern in ICU wants to turn
|
|
178
|
+
# h's into H's here. I guess we have to do it too because the pattern data could in theory
|
|
179
|
+
# now contain either.
|
|
180
|
+
pattern_str.gsub('h', 'H')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def format_hms(fields)
|
|
184
|
+
pattern = hms_duration_units_pattern(fields)
|
|
185
|
+
return nil if pattern.nil?
|
|
186
|
+
|
|
187
|
+
# According to the Intl.js spec, when formatting in digital, everything < seconds
|
|
188
|
+
# should be coalesced into decimal seconds
|
|
189
|
+
seconds_incl_fractional = fields.fetch(:seconds, 0)
|
|
190
|
+
second_precision = 0
|
|
191
|
+
if fields.key?(:milliseconds)
|
|
192
|
+
seconds_incl_fractional += fields[:milliseconds] / 1e3
|
|
193
|
+
second_precision = 3
|
|
194
|
+
end
|
|
195
|
+
if fields.key?(:microseconds)
|
|
196
|
+
seconds_incl_fractional += fields[:microseconds] / 1e6
|
|
197
|
+
second_precision = 6
|
|
198
|
+
end
|
|
199
|
+
if fields.key?(:nanoseconds)
|
|
200
|
+
seconds_incl_fractional += fields[:nanoseconds] / 1e9
|
|
201
|
+
second_precision = 9
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Follow the rules in ICU measfmt.cpp formatNumeric to fill in the patterns here with
|
|
205
|
+
# the appropriate values.
|
|
206
|
+
enum = pattern.each_char
|
|
207
|
+
protect = false
|
|
208
|
+
result = StringIO.new
|
|
209
|
+
loop do
|
|
210
|
+
char = enum.next
|
|
211
|
+
next_char = begin
|
|
212
|
+
enum.peek
|
|
213
|
+
rescue StandardError
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
if protect
|
|
218
|
+
# In literal mode
|
|
219
|
+
if char == "'"
|
|
220
|
+
protect = false
|
|
221
|
+
next
|
|
265
222
|
end
|
|
223
|
+
result.write(char)
|
|
224
|
+
next
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
value = case char
|
|
228
|
+
when 'H' then fields[:hours]
|
|
229
|
+
when 'm' then fields[:minutes]
|
|
230
|
+
when 's' then seconds_incl_fractional
|
|
231
|
+
end
|
|
266
232
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
233
|
+
case char
|
|
234
|
+
when 'H', 'm', 's'
|
|
235
|
+
skeleton_builder = StringIO.new
|
|
236
|
+
skeleton_builder.write('.')
|
|
237
|
+
if char == 's' && second_precision.positive?
|
|
238
|
+
skeleton_builder.write('0' * second_precision)
|
|
239
|
+
else
|
|
240
|
+
skeleton_builder.write('#' * 9)
|
|
241
|
+
end
|
|
242
|
+
if char == next_char
|
|
243
|
+
# It's doubled - means format it at zero fill
|
|
244
|
+
skeleton_builder.write(' integer-width/00')
|
|
245
|
+
enum.next
|
|
279
246
|
end
|
|
247
|
+
skeleton = skeleton_builder.string
|
|
248
|
+
result.write(format_number(value, skeleton))
|
|
249
|
+
when "'"
|
|
250
|
+
if next_char == char
|
|
251
|
+
# double-apostrophe, means literal '
|
|
252
|
+
result.write("'")
|
|
253
|
+
enum.next
|
|
254
|
+
else
|
|
255
|
+
protect = true
|
|
256
|
+
end
|
|
257
|
+
else
|
|
258
|
+
result.write(char)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
result.string
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def number_formatter(skeleton)
|
|
266
|
+
@number_formatters[skeleton] ||= begin
|
|
267
|
+
skeleton_uchar = UCharPointer.from_string(skeleton)
|
|
268
|
+
FFI::AutoPointer.new(
|
|
269
|
+
Lib.check_error do |error|
|
|
270
|
+
Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars,
|
|
271
|
+
@locale, error)
|
|
272
|
+
end,
|
|
273
|
+
Lib.method(:unumf_close)
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def format_number(value, skeleton)
|
|
279
|
+
formatter = number_formatter(skeleton)
|
|
280
|
+
result = FFI::AutoPointer.new(
|
|
281
|
+
Lib.check_error { |error| Lib.unumf_openResult(error) },
|
|
282
|
+
Lib.method(:unumf_closeResult)
|
|
283
|
+
)
|
|
284
|
+
value_str = value.to_s
|
|
285
|
+
Lib.check_error do |error|
|
|
286
|
+
Lib.unumf_formatDecimal(formatter, value_str, value_str.size, result, error)
|
|
287
|
+
end
|
|
288
|
+
Lib::Util.read_uchar_buffer(0) do |buf, error|
|
|
289
|
+
Lib.unumf_resultToString(result, buf, buf.length_in_uchars, error)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def format_list(values)
|
|
294
|
+
value_uchars = values.map(&UCharPointer.method(:from_string))
|
|
295
|
+
value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size)
|
|
296
|
+
value_uchars_array.put_array_of_pointer(0, value_uchars)
|
|
297
|
+
value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size)
|
|
298
|
+
value_lengths_array.put_array_of_int32(0, value_uchars.map(&:length_in_uchars))
|
|
299
|
+
Lib::Util.read_uchar_buffer(0) do |buf, error|
|
|
300
|
+
Lib.ulistfmt_format(
|
|
301
|
+
@list_formatter, value_uchars_array, value_lengths_array,
|
|
302
|
+
value_uchars.size, buf, buf.length_in_uchars, error
|
|
303
|
+
)
|
|
280
304
|
end
|
|
305
|
+
end
|
|
281
306
|
end
|
|
307
|
+
end
|
|
282
308
|
end
|
data/lib/ffi-icu/lib/util.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ICU
|
|
2
4
|
module Lib
|
|
3
5
|
module Util
|
|
@@ -18,38 +20,36 @@ module ICU
|
|
|
18
20
|
|
|
19
21
|
begin
|
|
20
22
|
result = FFI::MemoryPointer.new(:char, length)
|
|
21
|
-
Lib.check_error { |status| length = yield
|
|
23
|
+
Lib.check_error { |status| length = yield(result, status) }
|
|
22
24
|
rescue BufferOverflowError
|
|
23
25
|
attempts += 1
|
|
24
26
|
retry if attempts < 2
|
|
25
|
-
raise
|
|
27
|
+
raise(BufferOverflowError, "needed: #{length}")
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
result.read_string(length)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def self.read_uchar_buffer(length, &
|
|
32
|
-
buf, len = read_uchar_buffer_as_ptr_impl(length, &
|
|
33
|
+
def self.read_uchar_buffer(length, &)
|
|
34
|
+
buf, len = read_uchar_buffer_as_ptr_impl(length, &)
|
|
33
35
|
buf.string(len)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def self.read_uchar_buffer_as_ptr(length, &
|
|
37
|
-
buf,
|
|
38
|
+
def self.read_uchar_buffer_as_ptr(length, &)
|
|
39
|
+
buf, = read_uchar_buffer_as_ptr_impl(length, &)
|
|
38
40
|
buf
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
43
|
def self.read_uchar_buffer_as_ptr_impl(length)
|
|
44
44
|
attempts = 0
|
|
45
45
|
|
|
46
46
|
begin
|
|
47
47
|
result = UCharPointer.new(length)
|
|
48
|
-
Lib.check_error { |status| length = yield
|
|
48
|
+
Lib.check_error { |status| length = yield(result, status) }
|
|
49
49
|
rescue BufferOverflowError
|
|
50
50
|
attempts += 1
|
|
51
51
|
retry if attempts < 2
|
|
52
|
-
raise
|
|
52
|
+
raise(BufferOverflowError, "needed: #{length}")
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
[result, length]
|