lib/chronic/parser.rb in gitlab-chronic-0.10.4 vs lib/chronic/parser.rb in gitlab-chronic-0.10.5

- old
+ new

@@ -1,6 +1,5 @@ -require 'chronic/dictionary' require 'chronic/handlers' module Chronic class Parser include Handlers @@ -8,11 +7,10 @@ # Hash of default configuration options. DEFAULT_OPTIONS = { :context => :future, :now => nil, :hours24 => nil, - :week_start => :sunday, :guess => true, :ambiguous_time_range => 6, :endian_precedence => [:middle, :little], :ambiguous_year_future_bias => 50 } @@ -25,13 +23,10 @@ # this value to :past and if an ambiguous string is # given, it will assume it is in the past. # :now - Time, all computations will be based off of time # instead of Time.now. # :hours24 - Time will be parsed as it would be 24 hour clock. - # :week_start - By default, the parser assesses weeks start on - # sunday but you can change this value to :monday if - # needed. # :guess - By default the parser will guess a single point in time # for the given date or time. If you'd rather have the # entire time span returned, set this to false # and a Chronic::Span will be returned. Setting :guess to :end # will return last time from Span, to :middle for middle (same as just true) @@ -53,13 +48,12 @@ # to assume the full year using this figure. Chronic will # look x amount of years into the future and past. If the # two digit year is `now + x years` it's assumed to be the # future, `now - x years` is assumed to be the past. def initialize(options = {}) - validate_options!(options) @options = DEFAULT_OPTIONS.merge(options) - @now = options[:now] || Chronic.time_class.now + @now = options.delete(:now) || Chronic.time_class.now end # Parse "text" with the given options # Returns either a Time or Chronic::Span, depending on the value of options[:guess] def parse(text) @@ -92,39 +86,26 @@ # #=> "136 days future this second" # # Returns a new String ready for Chronic to parse. def pre_normalize(text) text = text.to_s.downcase - text.gsub!(/\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b/, '\3 / \2 / \1') + text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1') text.gsub!(/\b([ap])\.m\.?/, '\1m') - text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d+)\-(\d{2}:?\d{2})\b/, '\1tzminus\2') + text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2') text.gsub!(/\./, ':') text.gsub!(/([ap]):m:?/, '\1m') - text.gsub!(/'(\d{2})\b/) do - number = $1.to_i - - if Chronic::Date::could_be_year?(number) - Chronic::Date::make_year(number, options[:ambiguous_year_future_bias]) - else - number - end - end text.gsub!(/['"]/, '') text.gsub!(/,/, ' ') text.gsub!(/^second /, '2nd ') - text.gsub!(/\bsecond (of|day|month|hour|minute|second|quarter)\b/, '2nd \1') - text.gsub!(/\bthird quarter\b/, '3rd q') - text.gsub!(/\bfourth quarter\b/, '4th q') - text.gsub!(/quarters?(\s+|$)(?!to|till|past|after|before)/, 'q\1') + text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1') text = Numerizer.numerize(text) - text.gsub!(/\b(\d)(?:st|nd|rd|th)\s+q\b/, 'q\1') text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' } text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1') text.gsub!(/\btoday\b/, 'this day') text.gsub!(/\btomm?orr?ow\b/, 'next day') text.gsub!(/\byesterday\b/, 'last day') - text.gsub!(/\bnoon|midday\b/, '12:00pm') + text.gsub!(/\bnoon\b/, '12:00pm') text.gsub!(/\bmidnight\b/, '24:00') text.gsub!(/\bnow\b/, 'this second') text.gsub!('quarter', '15') text.gsub!('half', '30') text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past') @@ -161,26 +142,91 @@ # # options - An optional Hash of configuration options. # # Returns a Hash of Handler definitions. def definitions(options = {}) - SpanDictionary.new(options).definitions - end + options[:endian_precedence] ||= [:middle, :little] - private + @@definitions ||= { + :time => [ + Handler.new([:repeater_time, :repeater_day_portion?], nil) + ], + :date => [ + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od), + Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic), + Handler.new([:ordinal_day], :handle_generic), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy), + Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy), + Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn), + Handler.new([:ordinal_day, :grabber?, :repeater_month, :separator_at?, 'time?'], :handle_od_rm), + Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od), + Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on), + 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_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm), + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic), + ], - def validate_options!(options) - given = options.keys.map(&:to_s).sort - allowed = DEFAULT_OPTIONS.keys.map(&:to_s).sort - non_permitted = given - allowed - raise ArgumentError, "Unsupported option(s): #{non_permitted.join(', ')}" if non_permitted.any? + :anchor => [ + Handler.new([:separator_on?, :grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r) + ], + + :arrow => [ + Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:scalar, :repeater, :separator_and?, :scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_a_s_r_p_a), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_p_a) + ], + + :narrow => [ + Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r) + ] + } + + endians = [ + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy) + ] + + case endian = Array(options[:endian_precedence]).first + when :little + @@definitions.merge(:endian => endians.reverse) + when :middle + @@definitions.merge(:endian => endians) + else + raise ArgumentError, "Unknown endian option '#{endian}'" + end end + private + def tokenize(text, options) text = pre_normalize(text) - tokens = Tokenizer::tokenize(text) + tokens = text.split(' ').map { |word| Token.new(word) } [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok| tok.scan(tokens, options) end tokens.select { |token| token.tagged? } end @@ -213,10 +259,10 @@ if handler.match(tokens, definitions) return handler.invoke(:narrow, tokens, self, options) end end - puts '-none' if Chronic.debug + puts "-none" if Chronic.debug return nil end end end