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