chronic 0.4.1 → 0.4.3

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg
2
+ *.rbc
3
+ rdoc
4
+ .yardoc
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ -m markdown
2
+ --main README.md
3
+ --title "Chronic - Natural language date/time parsing"
data/HISTORY.md CHANGED
@@ -1,4 +1,11 @@
1
- # 0.4.1
1
+ # 0.4.2 / 2011-06-07
2
+
3
+ * Fix MonthRepeater for fetching past month offsets when current day is
4
+ later than the last day of a past month (ie on 29th of March when parsing
5
+ `last month` Chronic would return March instead of February. Now Chronic
6
+ returns the last day of the past month)
7
+
8
+ # 0.4.1 / 2011-06-05
2
9
 
3
10
  * Fix MiniDate ranges for parsing seasons (Thomas Walpole)
4
11
 
@@ -27,7 +34,7 @@
27
34
  * Fix undefined variable in RepeaterHour (Ryan Garver)
28
35
  * Added support for parsing 'Fourty' as another mis-spelling (Lee Reilly)
29
36
  * Added ordinal format support: ie 'February 14th, 2004' (Jeff Felchner)
30
- * Fix dates when working with daylight saving times Mike Mangino
37
+ * Fix dates when working with daylight saving times (Mike Mangino)
31
38
 
32
39
  # 0.3.0 / 2010-10-22
33
40
 
data/Rakefile CHANGED
@@ -1,44 +1,10 @@
1
1
  require 'date'
2
2
 
3
- #############################################################################
4
- #
5
- # Helper functions
6
- #
7
- #############################################################################
8
-
9
- def name
10
- @name ||= Dir['*.gemspec'].first.split('.').first
11
- end
12
-
13
3
  def version
14
- line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
15
- line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
16
- end
17
-
18
- def rubyforge_project
19
- name
20
- end
21
-
22
- def gemspec_file
23
- "#{name}.gemspec"
4
+ contents = File.read File.expand_path('../lib/chronic.rb', __FILE__)
5
+ contents[/VERSION = "([^"]+)"/, 1]
24
6
  end
25
7
 
26
- def gem_file
27
- "#{name}-#{version}.gem"
28
- end
29
-
30
- def replace_header(head, header_name)
31
- head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
32
- end
33
-
34
- #############################################################################
35
- #
36
- # Standard tasks
37
- #
38
- #############################################################################
39
-
40
- task :default => :test
41
-
42
8
  require 'rake/testtask'
43
9
  Rake::TestTask.new(:test) do |test|
44
10
  test.libs << 'lib' << 'test'
@@ -54,33 +20,12 @@ task :coverage do
54
20
  sh "open coverage/index.html"
55
21
  end
56
22
 
57
- require 'rake/rdoctask'
58
- Rake::RDocTask.new do |rdoc|
59
- rdoc.rdoc_dir = 'rdoc'
60
- rdoc.title = "#{name} #{version}"
61
- rdoc.rdoc_files.include('README*')
62
- rdoc.rdoc_files.include('lib/**/*.rb')
63
- end
64
-
65
23
  desc "Open an irb session preloaded with this library"
66
24
  task :console do
67
25
  sh "irb -Ilib -rchronic"
68
26
  end
69
27
 
70
- #############################################################################
71
- #
72
- # Custom tasks (add your own tasks here)
73
- #
74
- #############################################################################
75
-
76
-
77
-
78
- #############################################################################
79
- #
80
- # Packaging tasks
81
- #
82
- #############################################################################
83
-
28
+ desc "Release Chronic version #{version}"
84
29
  task :release => :build do
85
30
  unless `git branch` =~ /^\* master$/
86
31
  puts "You must be on the master branch to release!"
@@ -90,51 +35,14 @@ task :release => :build do
90
35
  sh "git tag v#{version}"
91
36
  sh "git push origin master"
92
37
  sh "git push origin v#{version}"
93
- sh "gem push pkg/#{name}-#{version}.gem"
38
+ sh "gem push pkg/chronic-#{version}.gem"
94
39
  end
95
40
 
96
- task :build => :gemspec do
41
+ desc "Build a gem from the gemspec"
42
+ task :build do
97
43
  sh "mkdir -p pkg"
98
- sh "gem build #{gemspec_file}"
99
- sh "mv #{gem_file} pkg"
100
- end
101
-
102
- task :gemspec => :validate do
103
- # read spec file and split out manifest section
104
- spec = File.read(gemspec_file)
105
- head, manifest, tail = spec.split(" # = MANIFEST =\n")
106
-
107
- # replace name version and date
108
- replace_header(head, :name)
109
- replace_header(head, :version)
110
-
111
- #comment this out if your rubyforge_project has a different name
112
- replace_header(head, :rubyforge_project)
113
-
114
- # determine file list from git ls-files
115
- files = `git ls-files`.
116
- split("\n").
117
- sort.
118
- reject { |file| file =~ /^\./ }.
119
- reject { |file| file =~ /^(rdoc|pkg)/ }.
120
- map { |file| " #{file}" }.
121
- join("\n")
122
-
123
- # piece file back together and write
124
- manifest = " s.files = %w[\n#{files}\n ]\n"
125
- spec = [head, manifest, tail].join(" # = MANIFEST =\n")
126
- File.open(gemspec_file, 'w') { |io| io.write(spec) }
127
- puts "Updated #{gemspec_file}"
44
+ sh "gem build chronic.gemspec"
45
+ sh "mv chronic-#{version}.gem pkg"
128
46
  end
129
47
 
130
- task :validate do
131
- libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
132
- unless libfiles.empty?
133
- puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
134
- exit!
135
- end
136
- unless Dir['VERSION*'].empty?
137
- puts "A `VERSION` file at root level violates Gem best practices."
138
- exit!
139
- end
140
- end
48
+ task :default => :test
data/chronic.gemspec CHANGED
@@ -1,83 +1,17 @@
1
+ $:.unshift File.expand_path('../lib', __FILE__)
2
+ require 'chronic'
3
+
1
4
  Gem::Specification.new do |s|
2
- s.name = 'chronic'
3
- s.version = '0.4.1'
5
+ s.name = 'chronic'
6
+ s.version = Chronic::VERSION
4
7
  s.rubyforge_project = 'chronic'
5
-
6
- s.summary = "Natural language date/time parsing."
7
- s.description = "Chronic is a natural language date/time parser written in pure Ruby."
8
-
9
- s.authors = ["Tom Preston-Werner", "Lee Jarvis"]
8
+ s.summary = 'Natural language date/time parsing.'
9
+ s.description = 'Chronic is a natural language date/time parser written in pure Ruby.'
10
+ s.authors = ['Tom Preston-Werner', 'Lee Jarvis']
10
11
  s.email = ['tom@mojombo.com', 'lee@jarvis.co']
11
12
  s.homepage = 'http://github.com/mojombo/chronic'
12
-
13
- s.rdoc_options = ["--charset=UTF-8"]
13
+ s.rdoc_options = ['--charset=UTF-8']
14
14
  s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
15
-
16
- # = MANIFEST =
17
- s.files = %w[
18
- HISTORY.md
19
- LICENSE
20
- Manifest.txt
21
- README.md
22
- Rakefile
23
- benchmark/benchmark.rb
24
- chronic.gemspec
25
- lib/chronic.rb
26
- lib/chronic/chronic.rb
27
- lib/chronic/grabber.rb
28
- lib/chronic/handlers.rb
29
- lib/chronic/mini_date.rb
30
- lib/chronic/numerizer.rb
31
- lib/chronic/ordinal.rb
32
- lib/chronic/pointer.rb
33
- lib/chronic/repeater.rb
34
- lib/chronic/repeaters/repeater_day.rb
35
- lib/chronic/repeaters/repeater_day_name.rb
36
- lib/chronic/repeaters/repeater_day_portion.rb
37
- lib/chronic/repeaters/repeater_fortnight.rb
38
- lib/chronic/repeaters/repeater_hour.rb
39
- lib/chronic/repeaters/repeater_minute.rb
40
- lib/chronic/repeaters/repeater_month.rb
41
- lib/chronic/repeaters/repeater_month_name.rb
42
- lib/chronic/repeaters/repeater_season.rb
43
- lib/chronic/repeaters/repeater_season_name.rb
44
- lib/chronic/repeaters/repeater_second.rb
45
- lib/chronic/repeaters/repeater_time.rb
46
- lib/chronic/repeaters/repeater_week.rb
47
- lib/chronic/repeaters/repeater_weekday.rb
48
- lib/chronic/repeaters/repeater_weekend.rb
49
- lib/chronic/repeaters/repeater_year.rb
50
- lib/chronic/scalar.rb
51
- lib/chronic/separator.rb
52
- lib/chronic/span.rb
53
- lib/chronic/tag.rb
54
- lib/chronic/time_zone.rb
55
- lib/chronic/token.rb
56
- test/helper.rb
57
- test/test_Chronic.rb
58
- test/test_DaylightSavings.rb
59
- test/test_Handler.rb
60
- test/test_MiniDate.rb
61
- test/test_Numerizer.rb
62
- test/test_RepeaterDayName.rb
63
- test/test_RepeaterFortnight.rb
64
- test/test_RepeaterHour.rb
65
- test/test_RepeaterMinute.rb
66
- test/test_RepeaterMonth.rb
67
- test/test_RepeaterMonthName.rb
68
- test/test_RepeaterTime.rb
69
- test/test_RepeaterWeek.rb
70
- test/test_RepeaterWeekday.rb
71
- test/test_RepeaterWeekend.rb
72
- test/test_RepeaterYear.rb
73
- test/test_Span.rb
74
- test/test_Time.rb
75
- test/test_Token.rb
76
- test/test_parsing.rb
77
- ]
78
- # = MANIFEST =
79
-
80
- ## Test files will be grabbed from the file list. Make sure the path glob
81
- ## matches what you actually use.
82
- s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
83
- end
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- test`.split("\n")
17
+ end
@@ -11,76 +11,76 @@ module Chronic
11
11
 
12
12
  class << self
13
13
 
14
- # Parses a string containing a natural language date or time. If the parser
15
- # can find a date or time, either a Time or Chronic::Span will be returned
16
- # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
17
- # +nil+ will be returned.
14
+ # Parses a string containing a natural language date or time
18
15
  #
19
- # Options are:
16
+ # If the parser can find a date or time, either a Time or Chronic::Span
17
+ # will be returned (depending on the value of `:guess`). If no
18
+ # date or time can be found, `nil` will be returned
20
19
  #
21
- # [<tt>:context</tt>]
22
- # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
20
+ # @option opts [Symbol] :context (:future)
21
+ # * If your string represents a birthday, you can set `:context` to
22
+ # `:past` and if an ambiguous string is given, it will assume it is
23
+ # in the past. Specify `:future` or omit to set a future context.
23
24
  #
24
- # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
25
- # and if an ambiguous string is given, it will assume it is in the
26
- # past. Specify <tt>:future</tt> or omit to set a future context.
25
+ # @option opts [Object] :now (Time.now)
26
+ # * By setting `:now` to a Time, all computations will be based off of
27
+ # that time instead of `Time.now`. If set to nil, Chronic will use
28
+ # `Time.now`
27
29
  #
28
- # [<tt>:now</tt>]
29
- # Time (defaults to Time.now)
30
+ # @option opts [Boolean] :guess (true)
31
+ # * By default, the parser will guess a single point in time for the
32
+ # given date or time. If you'd rather have the entire time span
33
+ # returned, set `:guess` to `false` and a {Chronic::Span} will
34
+ # be returned
30
35
  #
31
- # By setting <tt>:now</tt> to a Time, all computations will be based off
32
- # of that time instead of Time.now. If set to nil, Chronic will use Time.now.
33
- #
34
- # [<tt>:guess</tt>]
35
- # +true+ or +false+ (defaults to +true+)
36
- #
37
- # By default, the parser will guess a single point in time for the
38
- # given date or time. If you'd rather have the entire time span returned,
39
- # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
40
- #
41
- # [<tt>:ambiguous_time_range</tt>]
42
- # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
43
- #
44
- # If an Integer is given, ambiguous times (like 5:00) will be
36
+ # @option opts [Integer] :ambiguous_time_range (6)
37
+ # * If an Integer is given, ambiguous times (like 5:00) will be
45
38
  # assumed to be within the range of that time in the AM to that time
46
- # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
47
- # look for the time between 7am and 7pm. In the case of 5:00, it would
48
- # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
49
- # will be made, and the first matching instance of that time will
50
- # be used.
39
+ # in the PM. For example, if you set it to `7`, then the parser
40
+ # will look for the time between 7am and 7pm. In the case of 5:00, it
41
+ # would assume that means 5:00pm. If `:none` is given, no
42
+ # assumption will be made, and the first matching instance of that
43
+ # time will be used
51
44
  #
52
- # [<tt>:endian_precedence</tt>]
53
- # Array (defaults to <tt>[:middle, :little]</tt>)
54
- #
55
- # By default, Chronic will parse "03/04/2011" as the fourth day
45
+ # @option opts [Array] :endian_precedence ([:middle, :little])
46
+ # * By default, Chronic will parse "03/04/2011" as the fourth day
56
47
  # of the third month. Alternatively you can tell Chronic to parse
57
48
  # this as the third day of the fourth month by altering the
58
- # <tt>:endian_precedence</tt> to <tt>[:little, :middle]</tt>.
59
- def parse(text, specified_options = {})
49
+ # `:endian_precedence` to `[:little, :middle]`
50
+ #
51
+ # @option opts [Integer] :ambiguous_year_future_bias (50)
52
+ # * When parsing two digit years (ie 79) unlike Rubys Time class,
53
+ # Chronic will attempt to assume the full year using this figure.
54
+ # Chronic will look x amount of years into the future and past. If
55
+ # the two digit year is `now + x years` it's assumed to be the
56
+ # future, `now - x years` is assumed to be the past
57
+ #
58
+ # @return [Time, Chronic::Span, nil]
59
+ def parse(text, opts={})
60
60
  @text = text
61
- options = DEFAULT_OPTIONS.merge specified_options
61
+ options = DEFAULT_OPTIONS.merge opts
62
62
 
63
63
  # ensure the specified options are valid
64
- (specified_options.keys-DEFAULT_OPTIONS.keys).each {|key| raise(InvalidArgumentException, "#{key} is not a valid option key.")}
64
+ (opts.keys - DEFAULT_OPTIONS.keys).each do |key|
65
+ raise InvalidArgumentException, "#{key} is not a valid option key."
66
+ end
65
67
 
66
- [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
68
+ unless [:past, :future, :none].include?(options[:context])
69
+ raise InvalidArgumentException, "Invalid context, :past/:future only"
70
+ end
67
71
 
72
+ options[:text] = text
68
73
  options[:now] ||= Chronic.time_class.now
69
- @now = options[:now]
70
-
71
- # put the text into a normal format to ease scanning
72
- text = pre_normalize(text)
74
+ Chronic.now = options[:now]
73
75
 
74
76
  # tokenize words
75
- @tokens = tokenize(text, options)
77
+ tokens = tokenize(text, options)
76
78
 
77
79
  if Chronic.debug
78
- puts "+---------------------------------------------------"
79
- puts "| " + @tokens.to_s
80
- puts "+---------------------------------------------------"
80
+ puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
81
81
  end
82
82
 
83
- span = tokens_to_span(@tokens, options)
83
+ span = tokens_to_span(tokens, options)
84
84
 
85
85
  if options[:guess]
86
86
  guess span
@@ -89,52 +89,66 @@ module Chronic
89
89
  end
90
90
  end
91
91
 
92
- # Clean up the specified input text by stripping unwanted characters,
93
- # converting idioms to their canonical form, converting number words
94
- # to numbers (three => 3), and converting ordinal words to numeric
92
+ # Clean up the specified text ready for parsing
93
+ #
94
+ # Clean up the string by stripping unwanted characters, converting
95
+ # idioms to their canonical form, converting number words to numbers
96
+ # (three => 3), and converting ordinal words to numeric
95
97
  # ordinals (third => 3rd)
96
- def pre_normalize(text) #:nodoc:
97
- normalized_text = text.to_s.downcase
98
- normalized_text.gsub!(/['"\.,]/, '')
99
- normalized_text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
100
- normalized_text = numericize_numbers(normalized_text)
101
- normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
102
- normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
103
- normalized_text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
104
- normalized_text.gsub!(/\btoday\b/, 'this day')
105
- normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
106
- normalized_text.gsub!(/\byesterday\b/, 'last day')
107
- normalized_text.gsub!(/\bnoon\b/, '12:00')
108
- normalized_text.gsub!(/\bmidnight\b/, '24:00')
109
- normalized_text.gsub!(/\bbefore now\b/, 'past')
110
- normalized_text.gsub!(/\bnow\b/, 'this second')
111
- normalized_text.gsub!(/\b(ago|before)\b/, 'past')
112
- normalized_text.gsub!(/\bthis past\b/, 'last')
113
- normalized_text.gsub!(/\bthis last\b/, 'last')
114
- normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
115
- normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
116
- normalized_text.gsub!(/\btonight\b/, 'this night')
117
- normalized_text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
118
- normalized_text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
119
- normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
120
- normalized_text
98
+ #
99
+ # @example
100
+ # Chronic.pre_normalize('first day in May')
101
+ # #=> "1st day in may"
102
+ #
103
+ # Chronic.pre_normalize('tomorrow after noon')
104
+ # #=> "next day future 12:00"
105
+ #
106
+ # Chronic.pre_normalize('one hundred and thirty six days from now')
107
+ # #=> "136 days future this second"
108
+ #
109
+ # @param [String] text The string to normalize
110
+ # @return [String] A new string ready for Chronic to parse
111
+ def pre_normalize(text)
112
+ text = text.to_s.downcase
113
+ text.gsub!(/['"\.,]/, '')
114
+ text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
115
+ text = numericize_numbers(text)
116
+ text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
117
+ text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
118
+ text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
119
+ text.gsub!(/\btoday\b/, 'this day')
120
+ text.gsub!(/\btomm?orr?ow\b/, 'next day')
121
+ text.gsub!(/\byesterday\b/, 'last day')
122
+ text.gsub!(/\bnoon\b/, '12:00')
123
+ text.gsub!(/\bmidnight\b/, '24:00')
124
+ text.gsub!(/\bbefore now\b/, 'past')
125
+ text.gsub!(/\bnow\b/, 'this second')
126
+ text.gsub!(/\b(ago|before)\b/, 'past')
127
+ text.gsub!(/\bthis past\b/, 'last')
128
+ text.gsub!(/\bthis last\b/, 'last')
129
+ text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
130
+ text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
131
+ text.gsub!(/\btonight\b/, 'this night')
132
+ text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
133
+ text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
134
+ text.gsub!(/\b(hence|after|from)\b/, 'future')
135
+ text
121
136
  end
122
137
 
123
- # Convert number words to numbers (three => 3)
124
- def numericize_numbers(text) #:nodoc:
138
+ # Convert number words to numbers (three => 3, fourth => 4th)
139
+ #
140
+ # @see Numerizer.numerize
141
+ # @param [String] text The string to convert
142
+ # @return [String] A new string with words converted to numbers
143
+ def numericize_numbers(text)
125
144
  Numerizer.numerize(text)
126
145
  end
127
146
 
128
- def tokenize(text, options) #:nodoc:
129
- tokens = text.split(' ').map { |word| Token.new(word) }
130
- [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
131
- tokens = tok.scan(tokens, options)
132
- end
133
- tokens.delete_if { |token| !token.tagged? }
134
- end
135
-
136
147
  # Guess a specific time within the given span
137
- def guess(span) #:nodoc:
148
+ #
149
+ # @param [Span] span
150
+ # @return [Time]
151
+ def guess(span)
138
152
  return nil if span.nil?
139
153
  if span.width > 1
140
154
  span.begin + (span.width / 2)
@@ -142,16 +156,133 @@ module Chronic
142
156
  span.begin
143
157
  end
144
158
  end
159
+
160
+ # List of {Handler} definitions. See {parse} for a list of options this
161
+ # method accepts
162
+ #
163
+ # @see parse
164
+ # @return [Hash] A Hash of Handler definitions
165
+ def definitions(options={})
166
+ options[:endian_precedence] ||= [:middle, :little]
167
+
168
+ @definitions ||= {
169
+ :time => [
170
+ Handler.new([:repeater_time, :repeater_day_portion?], nil)
171
+ ],
172
+
173
+ :date => [
174
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :separator_slash_or_dash?, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
175
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
176
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
177
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
178
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
179
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
180
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
181
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
182
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
183
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
184
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
185
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
186
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
187
+ ],
188
+
189
+ # tonight at 7pm
190
+ :anchor => [
191
+ Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
192
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
193
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
194
+ ],
195
+
196
+ # 3 weeks from now, in 2 months
197
+ :arrow => [
198
+ Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
199
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
200
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
201
+ ],
202
+
203
+ # 3rd week in march
204
+ :narrow => [
205
+ Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
206
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
207
+ ]
208
+ }
209
+
210
+ endians = [
211
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
212
+ Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
213
+ ]
214
+
215
+ case endian = Array(options[:endian_precedence]).first
216
+ when :little
217
+ @definitions[:endian] = endians.reverse
218
+ when :middle
219
+ @definitions[:endian] = endians
220
+ else
221
+ raise InvalidArgumentException, "Unknown endian option '#{endian}'"
222
+ end
223
+
224
+ @definitions
225
+ end
226
+
227
+ private
228
+
229
+ def tokenize(text, options)
230
+ text = pre_normalize(text)
231
+ tokens = text.split(' ').map { |word| Token.new(word) }
232
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
233
+ tokens = tok.scan(tokens, options)
234
+ end
235
+ tokens.select { |token| token.tagged? }
236
+ end
237
+
238
+ def tokens_to_span(tokens, options)
239
+ definitions = definitions(options)
240
+
241
+ (definitions[:date] + definitions[:endian]).each do |handler|
242
+ if handler.match(tokens, definitions)
243
+ puts "-date" if Chronic.debug
244
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
245
+ return Handlers.send(handler.handler_method, good_tokens, options)
246
+ end
247
+ end
248
+
249
+ definitions[:anchor].each do |handler|
250
+ if handler.match(tokens, definitions)
251
+ puts "-anchor" if Chronic.debug
252
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
253
+ return Handlers.send(handler.handler_method, good_tokens, options)
254
+ end
255
+ end
256
+
257
+ definitions[:arrow].each do |handler|
258
+ if handler.match(tokens, definitions)
259
+ puts "-arrow" if Chronic.debug
260
+ tags = [SeparatorAt, SeparatorSlashOrDash, SeparatorComma]
261
+ good_tokens = tokens.reject { |o| tags.any? { |t| o.get_tag(t) } }
262
+ return Handlers.send(handler.handler_method, good_tokens, options)
263
+ end
264
+ end
265
+
266
+ definitions[:narrow].each do |handler|
267
+ if handler.match(tokens, definitions)
268
+ puts "-narrow" if Chronic.debug
269
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
270
+ return Handlers.send(handler.handler_method, tokens, options)
271
+ end
272
+ end
273
+
274
+ puts "-none" if Chronic.debug
275
+ return nil
276
+ end
277
+
145
278
  end
146
279
 
147
280
  # Internal exception
148
- class ChronicPain < Exception #:nodoc:
149
-
281
+ class ChronicPain < Exception
150
282
  end
151
283
 
152
284
  # This exception is raised if an invalid argument is provided to
153
285
  # any of Chronic's methods
154
286
  class InvalidArgumentException < Exception
155
-
156
287
  end
157
288
  end
@@ -1,11 +1,20 @@
1
1
  module Chronic
2
- class Grabber < Tag #:nodoc:
2
+ class Grabber < Tag
3
+
4
+ # Scan an Array of {Token}s and apply any necessary Grabber tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
3
10
  def self.scan(tokens, options)
4
- tokens.each_index do |i|
5
- if t = scan_for_all(tokens[i]) then tokens[i].tag(t); next end
11
+ tokens.each do |token|
12
+ if t = scan_for_all(token) then token.tag(t); next end
6
13
  end
7
14
  end
8
15
 
16
+ # @param [Token] token
17
+ # @return [Grabber, nil]
9
18
  def self.scan_for_all(token)
10
19
  scan_for token, self,
11
20
  {