chronic 0.6.2 → 0.7.0

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 (47) hide show
  1. data/.gitignore +1 -0
  2. data/HISTORY.md +36 -0
  3. data/README.md +7 -3
  4. data/Rakefile +3 -5
  5. data/chronic.gemspec +3 -0
  6. data/lib/chronic/chronic.rb +78 -63
  7. data/lib/chronic/grabber.rb +9 -7
  8. data/lib/chronic/handler.rb +10 -12
  9. data/lib/chronic/handlers.rb +130 -3
  10. data/lib/chronic/ordinal.rb +12 -9
  11. data/lib/chronic/pointer.rb +9 -7
  12. data/lib/chronic/repeater.rb +29 -19
  13. data/lib/chronic/repeaters/repeater_day_portion.rb +29 -15
  14. data/lib/chronic/repeaters/repeater_time.rb +19 -19
  15. data/lib/chronic/repeaters/repeater_year.rb +1 -1
  16. data/lib/chronic/scalar.rb +30 -23
  17. data/lib/chronic/season.rb +1 -12
  18. data/lib/chronic/separator.rb +21 -15
  19. data/lib/chronic/tag.rb +5 -11
  20. data/lib/chronic/time_zone.rb +9 -7
  21. data/lib/chronic/token.rb +12 -10
  22. data/lib/chronic.rb +50 -44
  23. data/test/helper.rb +8 -2
  24. data/test/{test_Chronic.rb → test_chronic.rb} +6 -6
  25. data/test/{test_DaylightSavings.rb → test_daylight_savings.rb} +1 -1
  26. data/test/{test_Handler.rb → test_handler.rb} +1 -1
  27. data/test/{test_MiniDate.rb → test_mini_date.rb} +9 -9
  28. data/test/{test_Numerizer.rb → test_numerizer.rb} +1 -1
  29. data/test/test_parsing.rb +173 -14
  30. data/test/{test_RepeaterDayName.rb → test_repeater_day_name.rb} +1 -1
  31. data/test/test_repeater_day_portion.rb +254 -0
  32. data/test/{test_RepeaterFortnight.rb → test_repeater_fortnight.rb} +1 -1
  33. data/test/{test_RepeaterHour.rb → test_repeater_hour.rb} +1 -1
  34. data/test/{test_RepeaterMinute.rb → test_repeater_minute.rb} +1 -1
  35. data/test/{test_RepeaterMonth.rb → test_repeater_month.rb} +1 -1
  36. data/test/{test_RepeaterMonthName.rb → test_repeater_month_name.rb} +1 -1
  37. data/test/{test_RepeaterSeason.rb → test_repeater_season.rb} +1 -1
  38. data/test/{test_RepeaterTime.rb → test_repeater_time.rb} +1 -1
  39. data/test/{test_RepeaterWeek.rb → test_repeater_week.rb} +1 -1
  40. data/test/{test_RepeaterWeekday.rb → test_repeater_weekday.rb} +1 -1
  41. data/test/{test_RepeaterWeekend.rb → test_repeater_weekend.rb} +1 -1
  42. data/test/{test_RepeaterYear.rb → test_repeater_year.rb} +1 -1
  43. data/test/{test_Span.rb → test_span.rb} +1 -1
  44. data/test/{test_Token.rb → test_token.rb} +1 -1
  45. metadata +96 -83
  46. data/.gemtest +0 -0
  47. data/.yardopts +0 -3
data/.gitignore CHANGED
@@ -3,3 +3,4 @@ pkg
3
3
  rdoc
4
4
  .yardoc
5
5
  doc
6
+ tags
data/HISTORY.md CHANGED
@@ -1,3 +1,39 @@
1
+ # HEAD
2
+
3
+ * Support parsing EXIF date format (#112)
4
+ * Start using minitest for testing
5
+ * Ensure periods are interpreted as colons (#81).
6
+ * Support month/day and day/month parsing (#59).
7
+ * Support day(scalar)-month(name)-year(scalar) (#99).
8
+ * Handle text starting with 'a' or 'an' (#101, @steveburkett).
9
+ * Ensure post medium timestamps are correctly formatted (#89)
10
+
11
+ # 0.6.7 / 2012-01-31
12
+
13
+ * Handle day, month names with scalar day and year (Joe Fiorini)
14
+ * Ensure 31st parses correctly with day names (Joe Fiorini)
15
+
16
+ # 0.6.6 / 2011-11-23
17
+
18
+ * `Chronic.parse('thur')` no longer returns `nil` (@harold)
19
+
20
+ # 0.6.5 / 2011-11-04
21
+
22
+ * Fix bug when parsing ordinal repeaters (#73)
23
+ * Added handler support for day_name month_name (@imme5150)
24
+ * Fix bug when parsing strings prefixed with PM
25
+
26
+ # 0.6.4 / 2011-08-08
27
+
28
+ * Fixed bug where 'noon' was parsed as 00:00 rather than 12:00
29
+ with :ambiguous_time_range => :none (Vladimir Chernis)
30
+ * Add support for handling '2009 May 22nd'
31
+ * Add the ability to handle scalar-day/repeater-month-name as well as ordinals
32
+
33
+ # 0.6.3 / 2011-08-01
34
+
35
+ * Ensure 'thu' is parsed as Thursday for 1.8.7 generic timestamp
36
+
1
37
  # 0.6.2 / 2011-07-28
2
38
 
3
39
  * Ensure specific endian handlers are prioritised over normal date handlers
data/README.md CHANGED
@@ -46,6 +46,10 @@ You can parse strings containing a natural language date using the
46
46
 
47
47
  Chronic.parse('may 27th', :guess => false)
48
48
  #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
49
+
50
+ Chronic.parse('6/4/2012', :endian_precedence => :little)
51
+ #=> Fri Apr 06 00:00:00 PDT 2012
52
+
49
53
 
50
54
  See `Chronic.parse` for detailed usage instructions.
51
55
 
@@ -86,6 +90,7 @@ Simple
86
90
  Complex
87
91
 
88
92
  * 3 years ago
93
+ * a year ago
89
94
  * 5 months before now
90
95
  * 7 hours ago
91
96
  * 7 days from now
@@ -162,14 +167,13 @@ If you'd like to hack on Chronic, start by forking the repo on GitHub:
162
167
 
163
168
  https://github.com/mojombo/chronic
164
169
 
165
- To get all of the dependencies, install the gem first. The best way to get
166
- your changes merged back into core is as follows:
170
+ The best way to get your changes merged back into core is as follows:
167
171
 
168
172
  1. Clone down your fork
169
173
  1. Create a thoughtfully named topic branch to contain your change
170
174
  1. Hack away
171
175
  1. Add tests and make sure everything still passes by running `rake`
172
- 1. Ensure your tests pass in multiple timezones
176
+ 1. Ensure your tests pass in multiple timezones. ie `TZ=utc rake` `TZ=BST rake`
173
177
  1. If you are adding new functionality, document it in the README
174
178
  1. Do not change the version number, we will do that on our end
175
179
  1. If necessary, rebase your commits into logical chunks, without errors
data/Rakefile CHANGED
@@ -5,11 +5,9 @@ def version
5
5
  contents[/VERSION = "([^"]+)"/, 1]
6
6
  end
7
7
 
8
- require 'rake/testtask'
9
- Rake::TestTask.new(:test) do |test|
10
- test.libs << 'lib' << 'test'
11
- test.pattern = 'test/**/test_*.rb'
12
- test.verbose = true
8
+ task :test do
9
+ $:.unshift './test'
10
+ Dir.glob('test/test_*.rb').each { |t| require File.basename(t) }
13
11
  end
14
12
 
15
13
  desc "Generate RCov test coverage and open in your browser"
data/chronic.gemspec CHANGED
@@ -14,4 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
15
15
  s.files = `git ls-files`.split("\n")
16
16
  s.test_files = `git ls-files -- test`.split("\n")
17
+
18
+ s.add_development_dependency 'rake'
19
+ s.add_development_dependency 'minitest'
17
20
  end
@@ -1,5 +1,6 @@
1
1
  module Chronic
2
2
 
3
+ # Returns a Hash of default configuration options.
3
4
  DEFAULT_OPTIONS = {
4
5
  :context => :future,
5
6
  :now => nil,
@@ -11,53 +12,43 @@ module Chronic
11
12
 
12
13
  class << self
13
14
 
14
- # Parses a string containing a natural language date or time
15
+ # Parses a string containing a natural language date or time.
15
16
  #
16
17
  # If the parser can find a date or time, either a Time or Chronic::Span
17
18
  # will be returned (depending on the value of `:guess`). If no
18
- # date or time can be found, `nil` will be returned
19
+ # date or time can be found, `nil` will be returned.
19
20
  #
20
- # @param [String] text The text to parse
21
+ # text - The String text to parse.
22
+ # opts - An optional Hash of configuration options:
23
+ # :context - If your string represents a birthday, you can set
24
+ # this value to :past and if an ambiguous string is
25
+ # given, it will assume it is in the past.
26
+ # :now - Time, all computations will be based off of time
27
+ # instead of Time.now.
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.
32
+ # :ambiguous_time_range - If an Integer is given, ambiguous times
33
+ # (like 5:00) will be assumed to be within the range of
34
+ # that time in the AM to that time in the PM. For
35
+ # example, if you set it to `7`, then the parser will
36
+ # look for the time between 7am and 7pm. In the case of
37
+ # 5:00, it would assume that means 5:00pm. If `:none`
38
+ # is given, no assumption will be made, and the first
39
+ # matching instance of that time will be used.
40
+ # :endian_precedence - By default, Chronic will parse "03/04/2011"
41
+ # as the fourth day of the third month. Alternatively you
42
+ # can tell Chronic to parse this as the third day of the
43
+ # fourth month by setting this to [:little, :middle].
44
+ # :ambiguous_year_future_bias - When parsing two digit years
45
+ # (ie 79) unlike Rubys Time class, Chronic will attempt
46
+ # to assume the full year using this figure. Chronic will
47
+ # look x amount of years into the future and past. If the
48
+ # two digit year is `now + x years` it's assumed to be the
49
+ # future, `now - x years` is assumed to be the past.
21
50
  #
22
- # @option opts [Symbol] :context (:future)
23
- # * If your string represents a birthday, you can set `:context` to
24
- # `:past` and if an ambiguous string is given, it will assume it is
25
- # in the past. Specify `:future` or omit to set a future context.
26
- #
27
- # @option opts [Object] :now (Time.now)
28
- # * By setting `:now` to a Time, all computations will be based off of
29
- # that time instead of `Time.now`. If set to nil, Chronic will use
30
- # `Time.now`
31
- #
32
- # @option opts [Boolean] :guess (true)
33
- # * By default, the parser will guess a single point in time for the
34
- # given date or time. If you'd rather have the entire time span
35
- # returned, set `:guess` to `false` and a {Chronic::Span} will
36
- # be returned
37
- #
38
- # @option opts [Integer] :ambiguous_time_range (6)
39
- # * If an Integer is given, ambiguous times (like 5:00) will be
40
- # assumed to be within the range of that time in the AM to that time
41
- # in the PM. For example, if you set it to `7`, then the parser
42
- # will look for the time between 7am and 7pm. In the case of 5:00, it
43
- # would assume that means 5:00pm. If `:none` is given, no
44
- # assumption will be made, and the first matching instance of that
45
- # time will be used
46
- #
47
- # @option opts [Array] :endian_precedence ([:middle, :little])
48
- # * By default, Chronic will parse "03/04/2011" as the fourth day
49
- # of the third month. Alternatively you can tell Chronic to parse
50
- # this as the third day of the fourth month by altering the
51
- # `:endian_precedence` to `[:little, :middle]`
52
- #
53
- # @option opts [Integer] :ambiguous_year_future_bias (50)
54
- # * When parsing two digit years (ie 79) unlike Rubys Time class,
55
- # Chronic will attempt to assume the full year using this figure.
56
- # Chronic will look x amount of years into the future and past. If
57
- # the two digit year is `now + x years` it's assumed to be the
58
- # future, `now - x years` is assumed to be the past
59
- #
60
- # @return [Time, Chronic::Span, nil]
51
+ # Returns a new Time object, or Chronic::Span if :guess option is false.
61
52
  def parse(text, opts={})
62
53
  options = DEFAULT_OPTIONS.merge opts
63
54
 
@@ -87,14 +78,17 @@ module Chronic
87
78
  end
88
79
  end
89
80
 
90
- # Clean up the specified text ready for parsing
81
+ # Clean up the specified text ready for parsing.
91
82
  #
92
83
  # Clean up the string by stripping unwanted characters, converting
93
84
  # idioms to their canonical form, converting number words to numbers
94
85
  # (three => 3), and converting ordinal words to numeric
95
86
  # ordinals (third => 3rd)
96
87
  #
97
- # @example
88
+ # text - The String text to normalize.
89
+ #
90
+ # Examples:
91
+ #
98
92
  # Chronic.pre_normalize('first day in May')
99
93
  # #=> "1st day in may"
100
94
  #
@@ -104,21 +98,22 @@ module Chronic
104
98
  # Chronic.pre_normalize('one hundred and thirty six days from now')
105
99
  # #=> "136 days future this second"
106
100
  #
107
- # @param [String] text The string to normalize
108
- # @return [String] A new string ready for Chronic to parse
101
+ # Returns a new String ready for Chronic to parse.
109
102
  def pre_normalize(text)
110
103
  text = text.to_s.downcase
111
- text.gsub!(/['"\.]/, '')
104
+ text.gsub!(/\./, ':')
105
+ text.gsub!(/['"]/, '')
112
106
  text.gsub!(/,/, ' ')
107
+ text.gsub!(/^second /, '2nd ')
113
108
  text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
114
109
  text = Numerizer.numerize(text)
115
110
  text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
116
111
  text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
117
- text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
112
+ text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
118
113
  text.gsub!(/\btoday\b/, 'this day')
119
114
  text.gsub!(/\btomm?orr?ow\b/, 'next day')
120
115
  text.gsub!(/\byesterday\b/, 'last day')
121
- text.gsub!(/\bnoon\b/, '12:00')
116
+ text.gsub!(/\bnoon\b/, '12:00pm')
122
117
  text.gsub!(/\bmidnight\b/, '24:00')
123
118
  text.gsub!(/\bnow\b/, 'this second')
124
119
  text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
@@ -129,23 +124,26 @@ module Chronic
129
124
  text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
130
125
  text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
131
126
  text.gsub!(/\b(hence|after|from)\b/, 'future')
127
+ text.gsub!(/^\s?an? /i, '1 ')
128
+ text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
132
129
  text
133
130
  end
134
131
 
135
- # Convert number words to numbers (three => 3, fourth => 4th)
132
+ # Convert number words to numbers (three => 3, fourth => 4th).
133
+ #
134
+ # text - The String to convert.
136
135
  #
137
- # @see Numerizer.numerize
138
- # @param [String] text The string to convert
139
- # @return [String] A new string with words converted to numbers
136
+ # Returns a new String with words converted to numbers.
140
137
  def numericize_numbers(text)
141
138
  warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
142
139
  Numerizer.numerize(text)
143
140
  end
144
141
 
145
- # Guess a specific time within the given span
142
+ # Guess a specific time within the given span.
146
143
  #
147
- # @param [Span] span
148
- # @return [Time]
144
+ # span - The Chronic::Span object to calcuate a guess from.
145
+ #
146
+ # Returns a new Time object.
149
147
  def guess(span)
150
148
  if span.width > 1
151
149
  span.begin + (span.width / 2)
@@ -154,11 +152,13 @@ module Chronic
154
152
  end
155
153
  end
156
154
 
157
- # List of {Handler} definitions. See {parse} for a list of options this
158
- # method accepts
155
+ # List of Handler definitions. See #parse for a list of options this
156
+ # method accepts.
157
+ #
158
+ # options - An optional Hash of configuration options:
159
+ # :endian_precedence -
159
160
  #
160
- # @see parse
161
- # @return [Hash] A Hash of Handler definitions
161
+ # Returns A Hash of Handler definitions.
162
162
  def definitions(options={})
163
163
  options[:endian_precedence] ||= [:middle, :little]
164
164
 
@@ -169,6 +169,9 @@ module Chronic
169
169
 
170
170
  :date => [
171
171
  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),
172
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
173
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
174
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
172
175
  Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_sy_sm_sd_t_tz),
173
176
  Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
174
177
  Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
@@ -179,11 +182,15 @@ module Chronic
179
182
  Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
180
183
  Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
181
184
  Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
185
+ Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
182
186
  Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
183
187
  Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
184
188
  Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
189
+ Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
185
190
  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)
191
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day], :handle_sm_sd),
192
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy),
193
+ Handler.new([:scalar_day, :separator_slash_or_dash, :repeater_month_name, :separator_slash_or_dash, :scalar_year], :handle_sm_rmn_sy)
187
194
  ],
188
195
 
189
196
  # tonight at 7pm
@@ -224,9 +231,17 @@ module Chronic
224
231
  @definitions
225
232
  end
226
233
 
227
- # Construct a time Object
234
+ # Construct a new time object determining possible month overflows
235
+ # and leap years.
236
+ #
237
+ # year - Integer year.
238
+ # month - Integer month.
239
+ # day - Integer day.
240
+ # hour - Integer hour.
241
+ # minute - Integer minute.
242
+ # second - Integer second.
228
243
  #
229
- # @return [Time]
244
+ # Returns a new Time object constructed from these params.
230
245
  def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
231
246
  if second >= 60
232
247
  minute += second / 60
@@ -1,20 +1,22 @@
1
1
  module Chronic
2
2
  class Grabber < Tag
3
3
 
4
- # Scan an Array of {Token}s and apply any necessary Grabber tags to
5
- # each token
4
+ # Scan an Array of Tokens and apply any necessary Grabber tags to
5
+ # each token.
6
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
7
+ # tokens - An Array of Token objects to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of Token objects.
10
11
  def self.scan(tokens, options)
11
12
  tokens.each do |token|
12
13
  if t = scan_for_all(token) then token.tag(t); next end
13
14
  end
14
15
  end
15
16
 
16
- # @param [Token] token
17
- # @return [Grabber, nil]
17
+ # token - The Token object to scan.
18
+ #
19
+ # Returns a new Grabber object.
18
20
  def self.scan_for_all(token)
19
21
  scan_for token, self,
20
22
  {
@@ -1,25 +1,22 @@
1
1
  module Chronic
2
2
  class Handler
3
3
 
4
- # @return [Array] A list of patterns
5
4
  attr_reader :pattern
6
5
 
7
- # @return [Symbol] The method which handles this list of patterns.
8
- # This method should exist inside the {Handlers} module
9
6
  attr_reader :handler_method
10
7
 
11
- # @param [Array] pattern A list of patterns to match tokens against
12
- # @param [Symbol] handler_method The method to be invoked when patterns
13
- # are matched. This method should exist inside the {Handlers} module
8
+ # pattern - An Array of patterns to match tokens against.
9
+ # handler_method - A Symbole representing the method to be invoked
10
+ # when patterns are matched.
14
11
  def initialize(pattern, handler_method)
15
12
  @pattern = pattern
16
13
  @handler_method = handler_method
17
14
  end
18
15
 
19
- # @param [Array] tokens
20
- # @param [Hash] definitions
21
- # @return [Boolean]
22
- # @see Chronic.tokens_to_span
16
+ # tokens - An Array of tokens to process.
17
+ # definitions - A Hash of definitions to check against.
18
+ #
19
+ # Returns true if a match is found.
23
20
  def match(tokens, definitions)
24
21
  token_index = 0
25
22
 
@@ -70,8 +67,9 @@ module Chronic
70
67
  Handlers.send(@handler_method, tokens, options)
71
68
  end
72
69
 
73
- # @param [Handler] The handler to compare
74
- # @return [Boolean] True if these handlers match
70
+ # other - The other Handler object to compare.
71
+ #
72
+ # Returns true if these Handlers match.
75
73
  def ==(other)
76
74
  @pattern == other.pattern
77
75
  end
@@ -59,6 +59,32 @@ module Chronic
59
59
  handle_m_d(month, day, tokens[2..tokens.size], options)
60
60
  end
61
61
 
62
+ def handle_sy_rmn_od(tokens, options)
63
+ year = tokens[0].get_tag(ScalarYear).type
64
+ month = tokens[1].get_tag(RepeaterMonthName).index
65
+ day = tokens[2].get_tag(OrdinalDay).type
66
+ time_tokens = tokens.last(tokens.size - 3)
67
+
68
+ return if month_overflow?(year, month, day)
69
+
70
+ begin
71
+ day_start = Chronic.time_class.local(year, month, day)
72
+ day_or_time(day_start, time_tokens, options)
73
+ rescue ArgumentError
74
+ nil
75
+ end
76
+ end
77
+
78
+ # Handle scalar-day/repeater-month-name
79
+ def handle_sd_rmn(tokens, options)
80
+ month = tokens[1].get_tag(RepeaterMonthName)
81
+ day = tokens[0].get_tag(ScalarDay).type
82
+
83
+ return if month_overflow?(Chronic.now.year, month.index, day)
84
+
85
+ handle_m_d(month, day, tokens[2..tokens.size], options)
86
+ end
87
+
62
88
  # Handle repeater-month-name/ordinal-day with separator-on
63
89
  def handle_rmn_od_on(tokens, options)
64
90
  if tokens.size > 3
@@ -198,6 +224,27 @@ module Chronic
198
224
  handle_sm_sd_sy(new_tokens + time_tokens, options)
199
225
  end
200
226
 
227
+ # Handle scalar-day/scalar-month AND scalar-month/scalar-day
228
+ def handle_sm_sd(tokens, options)
229
+ month = tokens[0].get_tag(ScalarMonth).type
230
+ day = tokens[1].get_tag(ScalarDay).type
231
+ year = Chronic.now.year
232
+
233
+ if Array(options[:endian_precedence]).first == :little
234
+ day, month = month, day
235
+ end
236
+
237
+ return if month_overflow?(year, month, day)
238
+
239
+ begin
240
+ start_time = Chronic.time_class.local(year, month, day)
241
+ end_time = Chronic.time_class.local(year, month, day + 1)
242
+ Span.new(start_time, end_time)
243
+ rescue ArgumentError
244
+ nil
245
+ end
246
+ end
247
+
201
248
  # Handle scalar-month/scalar-year
202
249
  def handle_sm_sy(tokens, options)
203
250
  month = tokens[0].get_tag(ScalarMonth).type
@@ -219,6 +266,66 @@ module Chronic
219
266
  end
220
267
  end
221
268
 
269
+ # Handle RepeaterDayName RepeaterMonthName OrdinalDay
270
+ def handle_rdn_rmn_od(tokens, options)
271
+ month = tokens[1].get_tag(RepeaterMonthName)
272
+ day = tokens[2].get_tag(OrdinalDay).type
273
+ year = Chronic.now.year
274
+
275
+ return if month_overflow?(year, month.index, day)
276
+
277
+ begin
278
+ start_time = Chronic.time_class.local(year, month.index, day)
279
+ end_time = time_with_rollover(year, month.index, day + 1)
280
+ Span.new(start_time, end_time)
281
+ rescue ArgumentError
282
+ nil
283
+ end
284
+ end
285
+
286
+ # Handle RepeaterDayName RepeaterMonthName ScalarDay
287
+ def handle_rdn_rmn_sd(tokens, options)
288
+ month = tokens[1].get_tag(RepeaterMonthName)
289
+ day = tokens[2].get_tag(ScalarDay).type
290
+ year = Chronic.now.year
291
+
292
+ return if month_overflow?(year, month.index, day)
293
+
294
+ begin
295
+ start_time = Chronic.time_class.local(year, month.index, day)
296
+ end_time = time_with_rollover(year, month.index, day + 1)
297
+ Span.new(start_time, end_time)
298
+ rescue ArgumentError
299
+ nil
300
+ end
301
+ end
302
+
303
+ # Handle RepeaterDayName RepeaterMonthName ScalarDay ScalarYear
304
+ def handle_rdn_rmn_sd_sy(tokens, options)
305
+ month = tokens[1].get_tag(RepeaterMonthName)
306
+ day = tokens[2].get_tag(ScalarDay).type
307
+ year = tokens[3].get_tag(ScalarYear).type
308
+
309
+ return if month_overflow?(year, month.index, day)
310
+
311
+ begin
312
+ start_time = Chronic.time_class.local(year, month.index, day)
313
+ end_time = time_with_rollover(year, month.index, day + 1)
314
+ Span.new(start_time, end_time)
315
+ rescue ArgumentError
316
+ nil
317
+ end
318
+ end
319
+
320
+ def handle_sm_rmn_sy(tokens, options)
321
+ day = tokens[0].get_tag(ScalarDay).type
322
+ month = tokens[1].get_tag(RepeaterMonthName).index
323
+ year = tokens[2].get_tag(ScalarYear).type
324
+ time = Chronic.time_class.local(year, month, day)
325
+ end_time = Chronic.time_class.local(year, month, day + 1)
326
+ Span.new(time, end_time)
327
+ end
328
+
222
329
  # anchors
223
330
 
224
331
  # Handle repeaters
@@ -241,7 +348,7 @@ module Chronic
241
348
  repeater = tokens[1].get_tag(Repeater)
242
349
  pointer = tokens[2].get_tag(Pointer).type
243
350
 
244
- repeater.offset(span, distance, pointer)
351
+ repeater.offset(span, distance, pointer) if repeater.respond_to?(:offset)
245
352
  end
246
353
 
247
354
  # Handle scalar/repeater/pointer
@@ -276,7 +383,7 @@ module Chronic
276
383
  ordinal.times do
277
384
  span = repeater.next(:future)
278
385
 
279
- if span.begin > outer_span.end
386
+ if span.begin >= outer_span.end
280
387
  span = nil
281
388
  break
282
389
  end
@@ -339,7 +446,11 @@ module Chronic
339
446
  raise ChronicPain, "Invalid grabber"
340
447
  end
341
448
 
342
- puts "--#{outer_span}" if Chronic.debug
449
+ if Chronic.debug
450
+ puts "Handler-class: #{head.class}"
451
+ puts "--#{outer_span}"
452
+ end
453
+
343
454
  find_within(repeaters, outer_span, pointer)
344
455
  end
345
456
 
@@ -353,6 +464,8 @@ module Chronic
353
464
  else
354
465
  day > RepeaterMonth::MONTH_DAYS[month - 1]
355
466
  end
467
+ rescue ArgumentError
468
+ false
356
469
  end
357
470
 
358
471
  # Recursively finds repeaters within other repeaters.
@@ -371,6 +484,20 @@ module Chronic
371
484
  end
372
485
  end
373
486
 
487
+ def time_with_rollover(year, month, day)
488
+ date_parts =
489
+ if month_overflow?(year, month, day)
490
+ if month == 12
491
+ [year + 1, 1, 1]
492
+ else
493
+ [year, month + 1, 1]
494
+ end
495
+ else
496
+ [year, month, day]
497
+ end
498
+ Chronic.time_class.local(*date_parts)
499
+ end
500
+
374
501
  def dealias_and_disambiguate_times(tokens, options)
375
502
  # handle aliases of am/pm
376
503
  # 5:00 in the morning -> 5:00 am
@@ -1,12 +1,13 @@
1
1
  module Chronic
2
2
  class Ordinal < Tag
3
3
 
4
- # Scan an Array of {Token}s and apply any necessary Ordinal tags to
5
- # each token
4
+ # Scan an Array of Token objects and apply any necessary Ordinal
5
+ # tags to each token.
6
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
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.
10
11
  def self.scan(tokens, options)
11
12
  tokens.each do |token|
12
13
  if t = scan_for_ordinals(token) then token.tag(t) end
@@ -14,14 +15,16 @@ module Chronic
14
15
  end
15
16
  end
16
17
 
17
- # @param [Token] token
18
- # @return [Ordinal, nil]
18
+ # token - The Token object we want to scan.
19
+ #
20
+ # Returns a new Ordinal object.
19
21
  def self.scan_for_ordinals(token)
20
22
  Ordinal.new($1.to_i) if token.word =~ /^(\d*)(st|nd|rd|th)$/
21
23
  end
22
24
 
23
- # @param [Token] token
24
- # @return [OrdinalDay, nil]
25
+ # token - The Token object we want to scan.
26
+ #
27
+ # Returns a new Ordinal object.
25
28
  def self.scan_for_days(token)
26
29
  if token.word =~ /^(\d*)(st|nd|rd|th)$/
27
30
  unless $1.to_i > 31 || $1.to_i < 1
@@ -1,20 +1,22 @@
1
1
  module Chronic
2
2
  class Pointer < Tag
3
3
 
4
- # Scan an Array of {Token}s and apply any necessary Pointer tags to
5
- # each token
4
+ # Scan an Array of Token objects and apply any necessary Pointer
5
+ # tags to each token.
6
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
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.
10
11
  def self.scan(tokens, options)
11
12
  tokens.each do |token|
12
13
  if t = scan_for_all(token) then token.tag(t) end
13
14
  end
14
15
  end
15
16
 
16
- # @param [Token] token
17
- # @return [Pointer, nil]
17
+ # token - The Token object we want to scan.
18
+ #
19
+ # Returns a new Pointer object.
18
20
  def self.scan_for_all(token)
19
21
  scan_for token, self,
20
22
  {