lib/ffi-icu/time_formatting.rb in ffi-icu-0.4.1 vs lib/ffi-icu/time_formatting.rb in ffi-icu-0.4.2
- old
+ new
@@ -36,10 +36,18 @@
:city_location => 'VVV', # The exemplar city (location) for the time zone. Where that is unavailable,
# the localized exemplar city name for the special zone Etc/Unknown is used as the fallback
# (for example, "Unknown City"), such as Los Angeles
# see: http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
}
+
+ HOUR_CYCLE_SYMS = {
+ 'h11' => 'K',
+ 'h12' => 'h',
+ 'h23' => 'H',
+ 'h24' => 'k',
+ :locale => 'j',
+ }
@default_options = {}
def self.create(options = {})
DateTimeFormatter.new(@default_options.merge(options))
end
@@ -94,25 +102,32 @@
end
end
class DateTimeFormatter < BaseFormatter
def initialize(options={})
- time_style = options[:time] || :short
- date_style = options[:date] || :short
- locale = options[:locale] || 'C'
- tz_style = options[:tz_style]
- time_zone = options[:zone]
- skeleton = options[:skeleton]
+ time_style = options[:time] || :short
+ date_style = options[:date] || :short
+ @locale = options[:locale] || 'C'
+ tz_style = options[:tz_style]
+ time_zone = options[:zone]
+ skeleton = options[:skeleton]
+ @hour_cycle = options[:hour_cycle]
- @f = make_formatter(time_style, date_style, locale, time_zone, skeleton)
+ if @hour_cycle && !HOUR_CYCLE_SYMS.keys.include?(@hour_cycle)
+ raise ICU::Error.new("Unknown hour cycle #{@hour_cycle}")
+ end
+
+ @f = make_formatter(time_style, date_style, @locale, time_zone, skeleton)
if tz_style
f0 = date_format(true)
f1 = update_tz_format(f0, tz_style)
if f1 != f0
set_date_format(true, f1)
end
end
+
+ replace_hour_symbol!
end
def parse(str)
str_u = UCharPointer.from_string(str)
str_l = str.size
@@ -180,27 +195,26 @@
retry
end
end
def set_date_format(localized, pattern_str)
- pattern = UCharPointer.from_string(pattern_str)
- pattern_len = pattern_str.size
+ set_date_format_impl(localized, pattern_str)
- Lib.check_error do |error|
- needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
- end
+ # After setting the date format string, we need to ensure that any hour
+ # symbols were properly localised according to @hour_cycle.
+ replace_hour_symbol!
end
def skeleton_format(skeleton_pattern_str, locale)
skeleton_pattern_ptr = UCharPointer.from_string(skeleton_pattern_str)
skeleton_pattern_len = skeleton_pattern_str.size
needed_length = 0
pattern_ptr = UCharPointer.new(needed_length)
udatpg_ptr = Lib.check_error { |error| Lib.udatpg_open(locale, error) }
- generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udat_close))
+ generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udatpg_close))
retried = false
begin
Lib.check_error do |error|
@@ -211,9 +225,106 @@
rescue BufferOverflowError
raise BufferOverflowError, "needed: #{needed_length}" if retried
pattern_ptr = pattern_ptr.resized_to needed_length
retried = true
retry
+ end
+ end
+
+ private
+
+ # Converts the current pattern to a pattern that takes the desired hour cycle
+ # into account. This is needed because most of the standard patterns in ICU
+ # contain either h (12 hour) or H (23 hour) in them, instead of j (locale-
+ # specified hour cycle). This means if you use a locale with an @hours=h12
+ # keyword in it, for example, it would normally be totally ignored by ICU.
+ #
+ # This is the same fixup done by Firefox:
+ # https://github.com/tc39/ecma402/issues/665#issuecomment-1084833809
+ # https://searchfox.org/mozilla-central/rev/625c3d0c8ae46502aed83f33bd530cb93e926e9f/intl/components/src/DateTimeFormat.cpp#282-323
+ def replace_hour_symbol!
+ # Short circuit this case - nil means "use whatever is in the pattern already", so
+ # no need to actually run any of this implementation.
+ return unless @hour_cycle
+
+ # Get the current pattern and convert to a skeleton
+ skeleton_str = pattern_to_skeleton_uchar(current_pattern_as_uchar).string
+
+ # Manipulate the skeleton to make it work with the correct hour cycle.
+ skeleton_str.gsub!(/[hHkKjJ]/, HOUR_CYCLE_SYMS[@hour_cycle])
+
+ # Either ensure the skeleton has, or does not have, am/pm, as appropriate
+ if ['h11', 'h12'].include?(@hour_cycle)
+ skeleton_str << 'a' unless skeleton_str.include? 'a'
+ else
+ skeleton_str.gsub!('a', '')
+ end
+
+ # Convert the skeleton back to a pattern
+ new_pattern_str = skeleton_to_pattern_uchar(UCharPointer.from_string(skeleton_str)).string
+
+ # We also need to manipulate the _pattern_, a little bit, because (according to Firefox source):
+ #
+ # Input skeletons don't differentiate between "K" and "h" resp. "k" and "H".
+ #
+ # https://searchfox.org/mozilla-central/rev/625c3d0c8ae46502aed83f33bd530cb93e926e9f/intl/components/src/DateTimeFormat.cpp#183
+ # So, if we put a skeleton with a k in it into getBestPattern, it comes out with a H (and a
+ # skeleton with a K in it comes out with a h). Need to fix this in the generated pattern.
+ resolved_hour_cycle = @hour_cycle == :locale ? Locale.new(@locale).keyword('hours') : @hour_cycle
+
+ if HOUR_CYCLE_SYMS.keys.include?(resolved_hour_cycle)
+ new_pattern_str.gsub!(/[hHkK]/, HOUR_CYCLE_SYMS[resolved_hour_cycle])
+ end
+
+ # Finally, set the new pattern onto the date time formatter
+ set_date_format_impl(false, new_pattern_str)
+ end
+
+ # Load up the date formatter locale and make a generator
+ # Note that we _MUST_ actually use @locale as passed to us, rather than calling
+ # udat_getLocaleByType to look it up from @f, because the latter will throw away
+ # any @hours specifier in the locale, and we need it.
+ def datetime_pattern_generator
+ @datetime_pattern_generator ||= FFI::AutoPointer.new(
+ Lib.check_error { |error| Lib.udatpg_open(@locale, error) },
+ Lib.method(:udatpg_close)
+ )
+ end
+
+ def current_pattern_as_uchar
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
+ Lib.udat_toPattern(@f, false, buf, buf.length_in_uchars, error)
+ end
+ end
+
+ def pattern_to_skeleton_uchar(pattern_uchar)
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
+ Lib.udatpg_getSkeleton(
+ datetime_pattern_generator,
+ pattern_uchar, pattern_uchar.length_in_uchars,
+ buf, buf.length_in_uchars,
+ error
+ )
+ end
+ end
+
+ def skeleton_to_pattern_uchar(skeleton_uchar)
+ Lib::Util.read_uchar_buffer_as_ptr(0) do |buf, error|
+ Lib.udatpg_getBestPattern(
+ datetime_pattern_generator,
+ skeleton_uchar, skeleton_uchar.length_in_uchars,
+ buf, buf.length_in_uchars,
+ error
+ )
+ end
+ end
+
+ def set_date_format_impl(localized, pattern_str)
+ pattern = UCharPointer.from_string(pattern_str)
+ pattern_len = pattern_str.size
+
+ Lib.check_error do |error|
+ needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len)
end
end
end # DateTimeFormatter
end # Formatting
end # ICU