In an attempt to push yet more behaviour from my Rails controllers into model classes, I was extracting some code into a yet-to-be-published plugin that allows date and time columns to be set using more human-readable values. For examples:
Naturally (no pun intended), Chronic also supports explicit dates such as "1/2/08". As part of my testing however, I discovered that the date parsing is decidedly US-centric presuming, of course, that dates are specified as "month-day-year". So for anyone living in say, Australia, it can be pretty frustrating to have Chronic.parse("1/2/08") return "Wed Jan 02 12:00:00 +1100 2008" rather than the expected "Fri Feb 01 12:00:00 +1100 2008".
The good news is, there is a solution. The bad news is, the solution is far from elegant. But first some context.
Chronic is actually written very nicely and the code is fairly easy to follow. In essence it works as follows: the input string is tokenized; each token is inspected to see if it's a keyword such as a month name, day name, or a number, etc.; and finally tries to matches the sequence of tokens against a pattern such as "a day number followed by a month name and then a year." The problem arises because the pattern for matching "month-day-year" comes before the one for "day-month-year" meaning that unless the first number is greater than 12, Chronic will always consider it to be a month.
The less than elegant solution is almost trivial and involves switching the order in which the patterns are matched. Doing so returns the desired result:
Which is all very well and good but now we have the reverse problem. What would be better is if we had a more general solution, one that allows us to specify the desired precedence when parsing:
Among other things, this allows us to have users in Australia enter dates with one format and users in the U.S. another.
For anyone interested, I've pasted a diff for each approach below.
--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -13,8 +13,8 @@ module Chronic
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
--- a/chronic-0.2.3/lib/chronic/chronic.rb
+++ b/chronic-0.2.3/lib/chronic/chronic.rb
@@ -43,7 +43,8 @@ module Chronic
default_options = {:context => :future,
:now => Time.now,
:guess => true,
- :ambiguous_time_range => 6}
+ :ambiguous_time_range => 6,
+ :explicit_date_format => :us}
options = default_options.merge specified_options
# ensure the specified options are valid
@@ -51,6 +52,7 @@ module Chronic
default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
end
[:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
+ [:us, :non_us].include?(options[:explicit_date_format]) || raise(InvalidArgumentException, "Invalid value ':#{options[:explicit_date_format]}' for :explicit_date_format specified. Valid values are :us and :non_us.")
# store now for later =)
@now = options[:now]
--- a/chronic-0.2.3/lib/chronic/handlers.rb
+++ b/chronic-0.2.3/lib/chronic/handlers.rb
@@ -3,41 +3,50 @@ module Chronic
class << self
def definitions #:nodoc:
- @definitions ||=
- {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
+ if @definitions.nil?
+ us_date = [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
+ Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)]
+
+ non_us_date = us_date.dup
+ non_us_date[7] = us_date[8]
+ non_us_date[8] = us_date[7]
+
+ @definitions =
+ {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
- :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
- Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
- Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
- Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
- Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
- Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
- Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
- Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
- Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
+ :date => {:us => us_date, :non_us => non_us_date},
- # tonight at 7pm
- :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
- Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
- Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
+ # tonight at 7pm
+ :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
- # 3 weeks from now, in 2 months
- :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
- Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
- Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
+ # 3 weeks from now, in 2 months
+ :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
- # 3rd week in march
- :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
- Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
- }
+ # 3rd week in march
+ :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
+ }
+ end
+ @definitions
end
def tokens_to_span(tokens, options) #:nodoc:
# maybe it's a specific date
- self.definitions[:date].each do |handler|
+ self.definitions[:date][options[:explicit_date_format]].each do |handler|
if handler.match(tokens, self.definitions)
puts "-date" if Chronic.debug
good_tokens = tokens.select { |o| !o.get_tag Separator }