chronic 0.2.2 → 0.10.2

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +243 -0
  5. data/LICENSE +21 -0
  6. data/README.md +182 -0
  7. data/Rakefile +62 -15
  8. data/chronic.gemspec +23 -0
  9. data/lib/chronic/date.rb +82 -0
  10. data/lib/chronic/grabber.rb +24 -17
  11. data/lib/chronic/handler.rb +97 -0
  12. data/lib/chronic/handlers.rb +490 -312
  13. data/lib/chronic/mini_date.rb +38 -0
  14. data/lib/chronic/numerizer.rb +130 -0
  15. data/lib/chronic/ordinal.rb +33 -24
  16. data/lib/chronic/parser.rb +268 -0
  17. data/lib/chronic/pointer.rb +22 -17
  18. data/lib/chronic/repeater.rb +137 -107
  19. data/lib/chronic/repeaters/repeater_day.rb +51 -44
  20. data/lib/chronic/repeaters/repeater_day_name.rb +50 -43
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +97 -81
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +61 -54
  23. data/lib/chronic/repeaters/repeater_hour.rb +51 -44
  24. data/lib/chronic/repeaters/repeater_minute.rb +51 -44
  25. data/lib/chronic/repeaters/repeater_month.rb +79 -60
  26. data/lib/chronic/repeaters/repeater_month_name.rb +85 -83
  27. data/lib/chronic/repeaters/repeater_season.rb +110 -22
  28. data/lib/chronic/repeaters/repeater_season_name.rb +41 -22
  29. data/lib/chronic/repeaters/repeater_second.rb +41 -34
  30. data/lib/chronic/repeaters/repeater_time.rb +128 -104
  31. data/lib/chronic/repeaters/repeater_week.rb +65 -58
  32. data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
  33. data/lib/chronic/repeaters/repeater_weekend.rb +59 -52
  34. data/lib/chronic/repeaters/repeater_year.rb +72 -52
  35. data/lib/chronic/scalar.rb +53 -46
  36. data/lib/chronic/season.rb +26 -0
  37. data/lib/chronic/separator.rb +174 -43
  38. data/lib/chronic/sign.rb +49 -0
  39. data/lib/chronic/span.rb +31 -0
  40. data/lib/chronic/tag.rb +37 -0
  41. data/lib/chronic/time.rb +40 -0
  42. data/lib/chronic/time_zone.rb +21 -11
  43. data/lib/chronic/token.rb +51 -0
  44. data/lib/chronic.rb +94 -69
  45. data/test/helper.rb +12 -0
  46. data/test/test_chronic.rb +183 -0
  47. data/test/test_daylight_savings.rb +118 -0
  48. data/test/{test_Handler.rb → test_handler.rb} +73 -55
  49. data/test/test_mini_date.rb +32 -0
  50. data/test/test_numerizer.rb +86 -0
  51. data/test/test_parsing.rb +891 -248
  52. data/test/{test_RepeaterDayName.rb → test_repeater_day_name.rb} +16 -17
  53. data/test/test_repeater_day_portion.rb +254 -0
  54. data/test/{test_RepeaterFortnight.rb → test_repeater_fortnight.rb} +17 -18
  55. data/test/{test_RepeaterHour.rb → test_repeater_hour.rb} +23 -20
  56. data/test/test_repeater_minute.rb +34 -0
  57. data/test/{test_RepeaterMonth.rb → test_repeater_month.rb} +21 -18
  58. data/test/{test_RepeaterMonthName.rb → test_repeater_month_name.rb} +18 -19
  59. data/test/test_repeater_season.rb +40 -0
  60. data/test/{test_RepeaterTime.rb → test_repeater_time.rb} +39 -23
  61. data/test/{test_RepeaterWeek.rb → test_repeater_week.rb} +17 -18
  62. data/test/test_repeater_weekday.rb +55 -0
  63. data/test/{test_RepeaterWeekend.rb → test_repeater_weekend.rb} +22 -23
  64. data/test/{test_RepeaterYear.rb → test_repeater_year.rb} +25 -19
  65. data/test/{test_Span.rb → test_span.rb} +7 -8
  66. data/test/{test_Token.rb → test_token.rb} +6 -7
  67. metadata +163 -85
  68. data/History.txt +0 -49
  69. data/Manifest.txt +0 -47
  70. data/README.txt +0 -149
  71. data/lib/chronic/chronic.rb +0 -239
  72. data/lib/numerizer/numerizer.rb +0 -103
  73. data/test/suite.rb +0 -9
  74. data/test/test_Chronic.rb +0 -50
  75. data/test/test_Numerizer.rb +0 -48
  76. data/test/test_Time.rb +0 -50
@@ -0,0 +1,38 @@
1
+ module Chronic
2
+ class MiniDate
3
+ attr_accessor :month, :day
4
+
5
+ def self.from_time(time)
6
+ new(time.month, time.day)
7
+ end
8
+
9
+ def initialize(month, day)
10
+ unless (1..12).include?(month)
11
+ raise ArgumentError, "1..12 are valid months"
12
+ end
13
+
14
+ @month = month
15
+ @day = day
16
+ end
17
+
18
+ def is_between?(md_start, md_end)
19
+ return false if (@month == md_start.month && @month == md_end.month) &&
20
+ (@day < md_start.day || @day > md_end.day)
21
+ return true if (@month == md_start.month && @day >= md_start.day) ||
22
+ (@month == md_end.month && @day <= md_end.day)
23
+
24
+ i = (md_start.month % 12) + 1
25
+
26
+ until i == md_end.month
27
+ return true if @month == i
28
+ i = (i % 12) + 1
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+ def equals?(other)
35
+ @month == other.month and @day == other.day
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,130 @@
1
+ require 'strscan'
2
+
3
+ module Chronic
4
+ class Numerizer
5
+
6
+ DIRECT_NUMS = [
7
+ ['eleven', '11'],
8
+ ['twelve', '12'],
9
+ ['thirteen', '13'],
10
+ ['fourteen', '14'],
11
+ ['fifteen', '15'],
12
+ ['sixteen', '16'],
13
+ ['seventeen', '17'],
14
+ ['eighteen', '18'],
15
+ ['nineteen', '19'],
16
+ ['ninteen', '19'], # Common mis-spelling
17
+ ['zero', '0'],
18
+ ['one', '1'],
19
+ ['two', '2'],
20
+ ['three', '3'],
21
+ ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
22
+ ['five', '5'],
23
+ ['six(\W|$)', '6\1'],
24
+ ['seven(\W|$)', '7\1'],
25
+ ['eight(\W|$)', '8\1'],
26
+ ['nine(\W|$)', '9\1'],
27
+ ['ten', '10'],
28
+ ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
29
+ ]
30
+
31
+ ORDINALS = [
32
+ ['first', '1'],
33
+ ['third', '3'],
34
+ ['fourth', '4'],
35
+ ['fifth', '5'],
36
+ ['sixth', '6'],
37
+ ['seventh', '7'],
38
+ ['eighth', '8'],
39
+ ['ninth', '9'],
40
+ ['tenth', '10'],
41
+ ['twelfth', '12'],
42
+ ['twentieth', '20'],
43
+ ['thirtieth', '30'],
44
+ ['fourtieth', '40'],
45
+ ['fiftieth', '50'],
46
+ ['sixtieth', '60'],
47
+ ['seventieth', '70'],
48
+ ['eightieth', '80'],
49
+ ['ninetieth', '90']
50
+ ]
51
+
52
+ TEN_PREFIXES = [
53
+ ['twenty', 20],
54
+ ['thirty', 30],
55
+ ['forty', 40],
56
+ ['fourty', 40], # Common mis-spelling
57
+ ['fifty', 50],
58
+ ['sixty', 60],
59
+ ['seventy', 70],
60
+ ['eighty', 80],
61
+ ['ninety', 90]
62
+ ]
63
+
64
+ BIG_PREFIXES = [
65
+ ['hundred', 100],
66
+ ['thousand', 1000],
67
+ ['million', 1_000_000],
68
+ ['billion', 1_000_000_000],
69
+ ['trillion', 1_000_000_000_000],
70
+ ]
71
+
72
+ def self.numerize(string)
73
+ string = string.dup
74
+
75
+ # preprocess
76
+ string.gsub!(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
77
+ string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
78
+
79
+ # easy/direct replacements
80
+
81
+ DIRECT_NUMS.each do |dn|
82
+ string.gsub!(/#{dn[0]}/i, '<num>' + dn[1])
83
+ end
84
+
85
+ ORDINALS.each do |on|
86
+ string.gsub!(/#{on[0]}/i, '<num>' + on[1] + on[0][-2, 2])
87
+ end
88
+
89
+ # ten, twenty, etc.
90
+
91
+ TEN_PREFIXES.each do |tp|
92
+ string.gsub!(/(?:#{tp[0]}) *<num>(\d(?=[^\d]|$))*/i) { '<num>' + (tp[1] + $1.to_i).to_s }
93
+ end
94
+
95
+ TEN_PREFIXES.each do |tp|
96
+ string.gsub!(/#{tp[0]}/i) { '<num>' + tp[1].to_s }
97
+ end
98
+
99
+ # hundreds, thousands, millions, etc.
100
+
101
+ BIG_PREFIXES.each do |bp|
102
+ string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { $1.empty? ? bp[1] : '<num>' + (bp[1] * $1.to_i).to_s}
103
+ andition(string)
104
+ end
105
+
106
+ # fractional addition
107
+ # I'm not combining this with the previous block as using float addition complicates the strings
108
+ # (with extraneous .0's and such )
109
+ string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
110
+
111
+ string.gsub(/<num>/, '')
112
+ end
113
+
114
+ class << self
115
+ private
116
+
117
+ def andition(string)
118
+ sc = StringScanner.new(string)
119
+
120
+ while sc.scan_until(/<num>(\d+)( | and )<num>(\d+)(?=[^\w]|$)/i)
121
+ if sc[2] =~ /and/ || sc[1].size > sc[3].size
122
+ string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '<num>' + (sc[1].to_i + sc[3].to_i).to_s
123
+ sc.reset
124
+ end
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -1,40 +1,49 @@
1
1
  module Chronic
2
+ class Ordinal < Tag
2
3
 
3
- class Ordinal < Tag #:nodoc:
4
- def self.scan(tokens)
5
- # for each token
4
+ # Scan an Array of Token objects and apply any necessary Ordinal
5
+ # tags to each token.
6
+ #
7
+ # tokens - An Array of tokens to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of tokens.
11
+ def self.scan(tokens, options)
6
12
  tokens.each_index do |i|
7
- if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end
8
- if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end
9
- end
10
- tokens
11
- end
12
-
13
- def self.scan_for_ordinals(token)
14
- if token.word =~ /^(\d*)(st|nd|rd|th)$/
15
- return Ordinal.new($1.to_i)
16
- end
17
- return nil
18
- end
19
-
20
- def self.scan_for_days(token)
21
- if token.word =~ /^(\d*)(st|nd|rd|th)$/
22
- unless $1.to_i > 31
23
- return OrdinalDay.new(token.word.to_i)
13
+ if tokens[i].word =~ /^(\d+)(st|nd|rd|th|\.)$/
14
+ ordinal = $1.to_i
15
+ tokens[i].tag(Ordinal.new(ordinal))
16
+ tokens[i].tag(OrdinalDay.new(ordinal)) if Chronic::Date::could_be_day?(ordinal)
17
+ tokens[i].tag(OrdinalMonth.new(ordinal)) if Chronic::Date::could_be_month?(ordinal)
18
+ if Chronic::Date::could_be_year?(ordinal)
19
+ year = Chronic::Date::make_year(ordinal, options[:ambiguous_year_future_bias])
20
+ tokens[i].tag(OrdinalYear.new(year.to_i))
21
+ end
24
22
  end
25
23
  end
26
- return nil
27
24
  end
28
-
25
+
29
26
  def to_s
30
27
  'ordinal'
31
28
  end
32
29
  end
33
-
30
+
34
31
  class OrdinalDay < Ordinal #:nodoc:
35
32
  def to_s
36
33
  super << '-day-' << @type.to_s
37
34
  end
38
35
  end
39
36
 
40
- end
37
+ class OrdinalMonth < Ordinal #:nodoc:
38
+ def to_s
39
+ super << '-month-' << @type.to_s
40
+ end
41
+ end
42
+
43
+ class OrdinalYear < Ordinal #:nodoc:
44
+ def to_s
45
+ super << '-year-' << @type.to_s
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,268 @@
1
+ require 'chronic/handlers'
2
+
3
+ module Chronic
4
+ class Parser
5
+ include Handlers
6
+
7
+ # Hash of default configuration options.
8
+ DEFAULT_OPTIONS = {
9
+ :context => :future,
10
+ :now => nil,
11
+ :hours24 => nil,
12
+ :guess => true,
13
+ :ambiguous_time_range => 6,
14
+ :endian_precedence => [:middle, :little],
15
+ :ambiguous_year_future_bias => 50
16
+ }
17
+
18
+ attr_accessor :now
19
+ attr_reader :options
20
+
21
+ # options - An optional Hash of configuration options:
22
+ # :context - If your string represents a birthday, you can set
23
+ # this value to :past and if an ambiguous string is
24
+ # given, it will assume it is in the past.
25
+ # :now - Time, all computations will be based off of time
26
+ # instead of Time.now.
27
+ # :hours24 - Time will be parsed as it would be 24 hour clock.
28
+ # :guess - By default the parser will guess a single point in time
29
+ # for the given date or time. If you'd rather have the
30
+ # entire time span returned, set this to false
31
+ # and a Chronic::Span will be returned. Setting :guess to :end
32
+ # will return last time from Span, to :middle for middle (same as just true)
33
+ # and :begin for first time from span.
34
+ # :ambiguous_time_range - If an Integer is given, ambiguous times
35
+ # (like 5:00) will be assumed to be within the range of
36
+ # that time in the AM to that time in the PM. For
37
+ # example, if you set it to `7`, then the parser will
38
+ # look for the time between 7am and 7pm. In the case of
39
+ # 5:00, it would assume that means 5:00pm. If `:none`
40
+ # is given, no assumption will be made, and the first
41
+ # matching instance of that time will be used.
42
+ # :endian_precedence - By default, Chronic will parse "03/04/2011"
43
+ # as the fourth day of the third month. Alternatively you
44
+ # can tell Chronic to parse this as the third day of the
45
+ # fourth month by setting this to [:little, :middle].
46
+ # :ambiguous_year_future_bias - When parsing two digit years
47
+ # (ie 79) unlike Rubys Time class, Chronic will attempt
48
+ # to assume the full year using this figure. Chronic will
49
+ # look x amount of years into the future and past. If the
50
+ # two digit year is `now + x years` it's assumed to be the
51
+ # future, `now - x years` is assumed to be the past.
52
+ def initialize(options = {})
53
+ @options = DEFAULT_OPTIONS.merge(options)
54
+ @now = options.delete(:now) || Chronic.time_class.now
55
+ end
56
+
57
+ # Parse "text" with the given options
58
+ # Returns either a Time or Chronic::Span, depending on the value of options[:guess]
59
+ def parse(text)
60
+ tokens = tokenize(text, options)
61
+ span = tokens_to_span(tokens, options.merge(:text => text))
62
+
63
+ puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
64
+
65
+ guess(span, options[:guess]) if span
66
+ end
67
+
68
+ # Clean up the specified text ready for parsing.
69
+ #
70
+ # Clean up the string by stripping unwanted characters, converting
71
+ # idioms to their canonical form, converting number words to numbers
72
+ # (three => 3), and converting ordinal words to numeric
73
+ # ordinals (third => 3rd)
74
+ #
75
+ # text - The String text to normalize.
76
+ #
77
+ # Examples:
78
+ #
79
+ # Chronic.pre_normalize('first day in May')
80
+ # #=> "1st day in may"
81
+ #
82
+ # Chronic.pre_normalize('tomorrow after noon')
83
+ # #=> "next day future 12:00"
84
+ #
85
+ # Chronic.pre_normalize('one hundred and thirty six days from now')
86
+ # #=> "136 days future this second"
87
+ #
88
+ # Returns a new String ready for Chronic to parse.
89
+ def pre_normalize(text)
90
+ text = text.to_s.downcase
91
+ text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
92
+ text.gsub!(/\b([ap])\.m\.?/, '\1m')
93
+ text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
94
+ text.gsub!(/\./, ':')
95
+ text.gsub!(/([ap]):m:?/, '\1m')
96
+ text.gsub!(/['"]/, '')
97
+ text.gsub!(/,/, ' ')
98
+ text.gsub!(/^second /, '2nd ')
99
+ text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
100
+ text = Numerizer.numerize(text)
101
+ text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
102
+ text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
103
+ text.gsub!(/\btoday\b/, 'this day')
104
+ text.gsub!(/\btomm?orr?ow\b/, 'next day')
105
+ text.gsub!(/\byesterday\b/, 'last day')
106
+ text.gsub!(/\bnoon\b/, '12:00pm')
107
+ text.gsub!(/\bmidnight\b/, '24:00')
108
+ text.gsub!(/\bnow\b/, 'this second')
109
+ text.gsub!('quarter', '15')
110
+ text.gsub!('half', '30')
111
+ text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
112
+ text.gsub!(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
113
+ text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
114
+ text.gsub!(/\bthis (?:last|past)\b/, 'last')
115
+ text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
116
+ text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
117
+ text.gsub!(/\btonight\b/, 'this night')
118
+ text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
119
+ text.gsub!(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
120
+ text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
121
+ text.gsub!(/\b(hence|after|from)\b/, 'future')
122
+ text.gsub!(/^\s?an? /i, '1 ')
123
+ text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
124
+ text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
125
+ text
126
+ end
127
+
128
+ # Guess a specific time within the given span.
129
+ #
130
+ # span - The Chronic::Span object to calcuate a guess from.
131
+ #
132
+ # Returns a new Time object.
133
+ def guess(span, mode = :middle)
134
+ return span if not mode
135
+ return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
136
+ return span.end if mode == :end
137
+ span.begin
138
+ end
139
+
140
+ # List of Handler definitions. See Chronic.parse for a list of options this
141
+ # method accepts.
142
+ #
143
+ # options - An optional Hash of configuration options.
144
+ #
145
+ # Returns a Hash of Handler definitions.
146
+ def definitions(options = {})
147
+ options[:endian_precedence] ||= [:middle, :little]
148
+
149
+ @@definitions ||= {
150
+ :time => [
151
+ Handler.new([:repeater_time, :repeater_day_portion?], nil)
152
+ ],
153
+
154
+ :date => [
155
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic),
156
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
157
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
158
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
159
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy),
160
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd),
161
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od),
162
+ Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od),
163
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic),
164
+ Handler.new([:ordinal_day], :handle_generic),
165
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
166
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
167
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
168
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
169
+ Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
170
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
171
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
172
+ Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
173
+ Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
174
+ Handler.new([:ordinal_day, :grabber?, :repeater_month, :separator_at?, 'time?'], :handle_od_rm),
175
+ Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
176
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
177
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
178
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
179
+ Handler.new([:scalar_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
180
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
181
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm),
182
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy),
183
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
184
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic),
185
+ ],
186
+
187
+ :anchor => [
188
+ Handler.new([:separator_on?, :grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
189
+ Handler.new([:grabber?, :repeater, :repeater, :separator?, :repeater?, :repeater?], :handle_r),
190
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
191
+ ],
192
+
193
+ :arrow => [
194
+ Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
195
+ Handler.new([:scalar, :repeater, :separator_and?, :scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_a_s_r_p_a),
196
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
197
+ Handler.new([:scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_p_a)
198
+ ],
199
+
200
+ :narrow => [
201
+ Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
202
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
203
+ ]
204
+ }
205
+
206
+ endians = [
207
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
208
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd),
209
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm),
210
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
211
+ ]
212
+
213
+ case endian = Array(options[:endian_precedence]).first
214
+ when :little
215
+ @@definitions.merge(:endian => endians.reverse)
216
+ when :middle
217
+ @@definitions.merge(:endian => endians)
218
+ else
219
+ raise ArgumentError, "Unknown endian option '#{endian}'"
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def tokenize(text, options)
226
+ text = pre_normalize(text)
227
+ tokens = text.split(' ').map { |word| Token.new(word) }
228
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok|
229
+ tok.scan(tokens, options)
230
+ end
231
+ tokens.select { |token| token.tagged? }
232
+ end
233
+
234
+ def tokens_to_span(tokens, options)
235
+ definitions = definitions(options)
236
+
237
+ (definitions[:endian] + definitions[:date]).each do |handler|
238
+ if handler.match(tokens, definitions)
239
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
240
+ return handler.invoke(:date, good_tokens, self, options)
241
+ end
242
+ end
243
+
244
+ definitions[:anchor].each do |handler|
245
+ if handler.match(tokens, definitions)
246
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
247
+ return handler.invoke(:anchor, good_tokens, self, options)
248
+ end
249
+ end
250
+
251
+ definitions[:arrow].each do |handler|
252
+ if handler.match(tokens, definitions)
253
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
254
+ return handler.invoke(:arrow, good_tokens, self, options)
255
+ end
256
+ end
257
+
258
+ definitions[:narrow].each do |handler|
259
+ if handler.match(tokens, definitions)
260
+ return handler.invoke(:narrow, tokens, self, options)
261
+ end
262
+ end
263
+
264
+ puts "-none" if Chronic.debug
265
+ return nil
266
+ end
267
+ end
268
+ end
@@ -1,27 +1,32 @@
1
1
  module Chronic
2
+ class Pointer < Tag
2
3
 
3
- class Pointer < Tag #:nodoc:
4
- def self.scan(tokens)
5
- # for each token
6
- tokens.each_index do |i|
7
- if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end
4
+ # Scan an Array of Token objects and apply any necessary Pointer
5
+ # tags to each token.
6
+ #
7
+ # tokens - An Array of tokens to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of tokens.
11
+ def self.scan(tokens, options)
12
+ tokens.each do |token|
13
+ if t = scan_for_all(token) then token.tag(t) end
8
14
  end
9
- tokens
10
15
  end
11
-
16
+
17
+ # token - The Token object we want to scan.
18
+ #
19
+ # Returns a new Pointer object.
12
20
  def self.scan_for_all(token)
13
- scanner = {/\bpast\b/ => :past,
14
- /\bfuture\b/ => :future,
15
- /\bin\b/ => :future}
16
- scanner.keys.each do |scanner_item|
17
- return self.new(scanner[scanner_item]) if scanner_item =~ token.word
18
- end
19
- return nil
21
+ scan_for token, self,
22
+ {
23
+ /\bpast\b/ => :past,
24
+ /\b(?:future|in)\b/ => :future,
25
+ }
20
26
  end
21
-
27
+
22
28
  def to_s
23
29
  'pointer-' << @type.to_s
24
30
  end
25
31
  end
26
-
27
- end
32
+ end