chronic 0.6.3 → 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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -3
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +76 -0
  5. data/README.md +47 -42
  6. data/Rakefile +30 -12
  7. data/chronic.gemspec +10 -4
  8. data/lib/chronic/date.rb +82 -0
  9. data/lib/chronic/grabber.rb +9 -7
  10. data/lib/chronic/handler.rb +47 -40
  11. data/lib/chronic/handlers.rb +240 -28
  12. data/lib/chronic/numerizer.rb +11 -2
  13. data/lib/chronic/ordinal.rb +28 -23
  14. data/lib/chronic/parser.rb +268 -0
  15. data/lib/chronic/pointer.rb +9 -7
  16. data/lib/chronic/repeater.rb +58 -48
  17. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  18. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  19. data/lib/chronic/repeaters/repeater_day_portion.rb +33 -18
  20. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  21. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  22. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  23. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  24. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  25. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  26. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  27. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  28. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  29. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  30. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  31. data/lib/chronic/repeaters/repeater_year.rb +6 -5
  32. data/lib/chronic/scalar.rb +40 -68
  33. data/lib/chronic/season.rb +1 -12
  34. data/lib/chronic/separator.rb +142 -23
  35. data/lib/chronic/sign.rb +49 -0
  36. data/lib/chronic/span.rb +2 -2
  37. data/lib/chronic/tag.rb +10 -15
  38. data/lib/chronic/time.rb +40 -0
  39. data/lib/chronic/time_zone.rb +9 -7
  40. data/lib/chronic/token.rb +16 -10
  41. data/lib/chronic.rb +109 -70
  42. data/test/helper.rb +8 -2
  43. data/test/{test_Chronic.rb → test_chronic.rb} +69 -34
  44. data/test/{test_DaylightSavings.rb → test_daylight_savings.rb} +1 -1
  45. data/test/{test_Handler.rb → test_handler.rb} +38 -14
  46. data/test/{test_MiniDate.rb → test_mini_date.rb} +9 -9
  47. data/test/{test_Numerizer.rb → test_numerizer.rb} +16 -2
  48. data/test/test_parsing.rb +392 -22
  49. data/test/{test_RepeaterDayName.rb → test_repeater_day_name.rb} +1 -1
  50. data/test/test_repeater_day_portion.rb +254 -0
  51. data/test/{test_RepeaterFortnight.rb → test_repeater_fortnight.rb} +1 -1
  52. data/test/{test_RepeaterHour.rb → test_repeater_hour.rb} +1 -1
  53. data/test/{test_RepeaterMinute.rb → test_repeater_minute.rb} +1 -1
  54. data/test/{test_RepeaterMonth.rb → test_repeater_month.rb} +1 -1
  55. data/test/{test_RepeaterMonthName.rb → test_repeater_month_name.rb} +1 -1
  56. data/test/{test_RepeaterSeason.rb → test_repeater_season.rb} +1 -1
  57. data/test/{test_RepeaterTime.rb → test_repeater_time.rb} +19 -1
  58. data/test/{test_RepeaterWeek.rb → test_repeater_week.rb} +1 -1
  59. data/test/{test_RepeaterWeekday.rb → test_repeater_weekday.rb} +1 -1
  60. data/test/{test_RepeaterWeekend.rb → test_repeater_weekend.rb} +1 -1
  61. data/test/{test_RepeaterYear.rb → test_repeater_year.rb} +1 -1
  62. data/test/{test_Span.rb → test_span.rb} +2 -2
  63. data/test/{test_Token.rb → test_token.rb} +1 -1
  64. metadata +129 -87
  65. data/.gemtest +0 -0
  66. data/.yardopts +0 -3
  67. data/lib/chronic/chronic.rb +0 -323
@@ -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,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
  {
@@ -1,73 +1,78 @@
1
1
  module Chronic
2
2
  class Repeater < Tag
3
3
 
4
- # Scan an Array of {Token}s and apply any necessary Repeater tags to
5
- # each token
4
+ # Scan an Array of Token objects and apply any necessary Repeater
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
- if t = scan_for_season_names(token) then token.tag(t); next end
13
- if t = scan_for_month_names(token) then token.tag(t); next end
14
- if t = scan_for_day_names(token) then token.tag(t); next end
15
- if t = scan_for_day_portions(token) then token.tag(t); next end
16
- if t = scan_for_times(token) then token.tag(t); next end
17
- if t = scan_for_units(token) then token.tag(t); next end
13
+ if t = scan_for_season_names(token, options) then token.tag(t); next end
14
+ if t = scan_for_month_names(token, options) then token.tag(t); next end
15
+ if t = scan_for_day_names(token, options) then token.tag(t); next end
16
+ if t = scan_for_day_portions(token, options) then token.tag(t); next end
17
+ if t = scan_for_times(token, options) then token.tag(t); next end
18
+ if t = scan_for_units(token, options) then token.tag(t); next end
18
19
  end
19
20
  end
20
21
 
21
- # @param [Token] token
22
- # @return [RepeaterSeasonName, nil]
23
- def self.scan_for_season_names(token)
22
+ # token - The Token object we want to scan.
23
+ #
24
+ # Returns a new Repeater object.
25
+ def self.scan_for_season_names(token, options = {})
24
26
  scan_for token, RepeaterSeasonName,
25
27
  {
26
28
  /^springs?$/ => :spring,
27
29
  /^summers?$/ => :summer,
28
30
  /^(autumn)|(fall)s?$/ => :autumn,
29
31
  /^winters?$/ => :winter
30
- }
32
+ }, options
31
33
  end
32
34
 
33
- # @param [Token] token
34
- # @return [RepeaterMonthName, nil]
35
- def self.scan_for_month_names(token)
35
+ # token - The Token object we want to scan.
36
+ #
37
+ # Returns a new Repeater object.
38
+ def self.scan_for_month_names(token, options = {})
36
39
  scan_for token, RepeaterMonthName,
37
40
  {
38
- /^jan\.?(uary)?$/ => :january,
39
- /^feb\.?(ruary)?$/ => :february,
40
- /^mar\.?(ch)?$/ => :march,
41
- /^apr\.?(il)?$/ => :april,
41
+ /^jan[:\.]?(uary)?$/ => :january,
42
+ /^feb[:\.]?(ruary)?$/ => :february,
43
+ /^mar[:\.]?(ch)?$/ => :march,
44
+ /^apr[:\.]?(il)?$/ => :april,
42
45
  /^may$/ => :may,
43
- /^jun\.?e?$/ => :june,
44
- /^jul\.?y?$/ => :july,
45
- /^aug\.?(ust)?$/ => :august,
46
- /^sep\.?(t\.?|tember)?$/ => :september,
47
- /^oct\.?(ober)?$/ => :october,
48
- /^nov\.?(ember)?$/ => :november,
49
- /^dec\.?(ember)?$/ => :december
50
- }
46
+ /^jun[:\.]?e?$/ => :june,
47
+ /^jul[:\.]?y?$/ => :july,
48
+ /^aug[:\.]?(ust)?$/ => :august,
49
+ /^sep[:\.]?(t[:\.]?|tember)?$/ => :september,
50
+ /^oct[:\.]?(ober)?$/ => :october,
51
+ /^nov[:\.]?(ember)?$/ => :november,
52
+ /^dec[:\.]?(ember)?$/ => :december
53
+ }, options
51
54
  end
52
55
 
53
- # @param [Token] token
54
- # @return [RepeaterDayName, nil]
55
- def self.scan_for_day_names(token)
56
+ # token - The Token object we want to scan.
57
+ #
58
+ # Returns a new Repeater object.
59
+ def self.scan_for_day_names(token, options = {})
56
60
  scan_for token, RepeaterDayName,
57
61
  {
58
62
  /^m[ou]n(day)?$/ => :monday,
59
63
  /^t(ue|eu|oo|u|)s?(day)?$/ => :tuesday,
60
64
  /^we(d|dnes|nds|nns)(day)?$/ => :wednesday,
61
- /^th(u(?:rs)?|ers)(day)?$/ => :thursday,
65
+ /^th(u|ur|urs|ers)(day)?$/ => :thursday,
62
66
  /^fr[iy](day)?$/ => :friday,
63
67
  /^sat(t?[ue]rday)?$/ => :saturday,
64
68
  /^su[nm](day)?$/ => :sunday
65
- }
69
+ }, options
66
70
  end
67
71
 
68
- # @param [Token] token
69
- # @return [RepeaterDayPortion, nil]
70
- def self.scan_for_day_portions(token)
72
+ # token - The Token object we want to scan.
73
+ #
74
+ # Returns a new Repeater object.
75
+ def self.scan_for_day_portions(token, options = {})
71
76
  scan_for token, RepeaterDayPortion,
72
77
  {
73
78
  /^ams?$/ => :am,
@@ -76,18 +81,20 @@ module Chronic
76
81
  /^afternoons?$/ => :afternoon,
77
82
  /^evenings?$/ => :evening,
78
83
  /^(night|nite)s?$/ => :night
79
- }
84
+ }, options
80
85
  end
81
86
 
82
- # @param [Token] token
83
- # @return [RepeaterTime, nil]
84
- def self.scan_for_times(token)
85
- scan_for token, RepeaterTime, /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/
87
+ # token - The Token object we want to scan.
88
+ #
89
+ # Returns a new Repeater object.
90
+ def self.scan_for_times(token, options = {})
91
+ scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/, options
86
92
  end
87
93
 
88
- # @param [Token] token
89
- # @return [Repeater] A new instance of a subclass of Repeater
90
- def self.scan_for_units(token)
94
+ # token - The Token object we want to scan.
95
+ #
96
+ # Returns a new Repeater object.
97
+ def self.scan_for_units(token, options = {})
91
98
  {
92
99
  /^years?$/ => :year,
93
100
  /^seasons?$/ => :season,
@@ -97,14 +104,17 @@ module Chronic
97
104
  /^weekends?$/ => :weekend,
98
105
  /^(week|business)days?$/ => :weekday,
99
106
  /^days?$/ => :day,
107
+ /^hrs?$/ => :hour,
100
108
  /^hours?$/ => :hour,
109
+ /^mins?$/ => :minute,
101
110
  /^minutes?$/ => :minute,
111
+ /^secs?$/ => :second,
102
112
  /^seconds?$/ => :second
103
113
  }.each do |item, symbol|
104
114
  if item =~ token.word
105
115
  klass_name = 'Repeater' + symbol.to_s.capitalize
106
116
  klass = Chronic.const_get(klass_name)
107
- return klass.new(symbol)
117
+ return klass.new(symbol, options)
108
118
  end
109
119
  end
110
120
  return nil
@@ -132,4 +142,4 @@ module Chronic
132
142
  'repeater'
133
143
  end
134
144
  end
135
- end
145
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterDay < Repeater #:nodoc:
3
3
  DAY_SECONDS = 86_400 # (24 * 60 * 60)
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_day_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_day_start
13
+ unless @current_day_start
13
14
  @current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day)
14
15
  end
15
16
 
@@ -50,4 +51,4 @@ module Chronic
50
51
  super << '-day'
51
52
  end
52
53
  end
53
- end
54
+ end
@@ -2,8 +2,9 @@ module Chronic
2
2
  class RepeaterDayName < Repeater #:nodoc:
3
3
  DAY_SECONDS = 86400 # (24 * 60 * 60)
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_date = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
@@ -11,8 +12,8 @@ module Chronic
11
12
 
12
13
  direction = pointer == :future ? 1 : -1
13
14
 
14
- if !@current_date
15
- @current_date = Date.new(@now.year, @now.month, @now.day)
15
+ unless @current_date
16
+ @current_date = ::Date.new(@now.year, @now.month, @now.day)
16
17
  @current_date += direction
17
18
 
18
19
  day_num = symbol_to_number(@type)
@@ -49,4 +50,4 @@ module Chronic
49
50
  lookup[sym] || raise("Invalid symbol specified")
50
51
  end
51
52
  end
52
- end
53
+ end
@@ -3,14 +3,15 @@ module Chronic
3
3
  PORTIONS = {
4
4
  :am => 0..(12 * 60 * 60 - 1),
5
5
  :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1),
6
- :morning => (6 * 60 * 60)..(12 * 60 * 60), # 6am-12am,
7
- :afternoon =>(13 * 60 * 60)..(17 * 60 * 60), # 1pm-5pm,
8
- :evening => (17 * 60 * 60)..(20 * 60 * 60), # 5pm-8pm,
9
- :night =>(20 * 60 * 60)..(24 * 60 * 60), # 8pm-12pm
6
+ :morning => (6 * 60 * 60)..(12 * 60 * 60), # 6am-12am,
7
+ :afternoon => (13 * 60 * 60)..(17 * 60 * 60), # 1pm-5pm,
8
+ :evening => (17 * 60 * 60)..(20 * 60 * 60), # 5pm-8pm,
9
+ :night => (20 * 60 * 60)..(24 * 60 * 60), # 8pm-12pm
10
10
  }
11
11
 
12
- def initialize(type)
12
+ def initialize(type, options = {})
13
13
  super
14
+ @current_span = nil
14
15
 
15
16
  if type.kind_of? Integer
16
17
  @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
@@ -25,41 +26,45 @@ module Chronic
25
26
  def next(pointer)
26
27
  super
27
28
 
28
- full_day = 60 * 60 * 24
29
-
30
- if !@current_span
29
+ unless @current_span
31
30
  now_seconds = @now - Chronic.construct(@now.year, @now.month, @now.day)
32
31
  if now_seconds < @range.begin
33
32
  case pointer
34
33
  when :future
35
34
  range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
36
35
  when :past
37
- range_start = Chronic.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
36
+ range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
38
37
  end
39
38
  elsif now_seconds > @range.end
40
39
  case pointer
41
40
  when :future
42
- range_start = Chronic.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
41
+ range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
43
42
  when :past
44
43
  range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
45
44
  end
46
45
  else
47
46
  case pointer
48
47
  when :future
49
- range_start = Chronic.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
48
+ range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
50
49
  when :past
51
- range_start = Chronic.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
50
+ range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
52
51
  end
53
52
  end
54
-
55
- @current_span = Span.new(range_start, range_start + (@range.end - @range.begin))
53
+ offset = (@range.end - @range.begin)
54
+ range_end = construct_date_from_reference_and_offset(range_start, offset)
55
+ @current_span = Span.new(range_start, range_end)
56
56
  else
57
+ days_to_shift_window =
57
58
  case pointer
58
59
  when :future
59
- @current_span += full_day
60
+ 1
60
61
  when :past
61
- @current_span -= full_day
62
+ -1
62
63
  end
64
+
65
+ new_begin = Chronic.construct(@current_span.begin.year, @current_span.begin.month, @current_span.begin.day + days_to_shift_window, @current_span.begin.hour, @current_span.begin.min, @current_span.begin.sec)
66
+ new_end = Chronic.construct(@current_span.end.year, @current_span.end.month, @current_span.end.day + days_to_shift_window, @current_span.end.hour, @current_span.end.min, @current_span.end.sec)
67
+ @current_span = Span.new(new_begin, new_end)
63
68
  end
64
69
  end
65
70
 
@@ -67,7 +72,8 @@ module Chronic
67
72
  super
68
73
 
69
74
  range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
70
- @current_span = Span.new(range_start, range_start + (@range.end - @range.begin))
75
+ range_end = construct_date_from_reference_and_offset(range_start)
76
+ @current_span = Span.new(range_start, range_end)
71
77
  end
72
78
 
73
79
  def offset(span, amount, pointer)
@@ -90,5 +96,14 @@ module Chronic
90
96
  def to_s
91
97
  super << '-dayportion-' << @type.to_s
92
98
  end
99
+
100
+ private
101
+ def construct_date_from_reference_and_offset(reference, offset = nil)
102
+ elapsed_seconds_for_range = offset || (@range.end - @range.begin)
103
+ second_hand = ((elapsed_seconds_for_range - (12 * 60))) % 60
104
+ minute_hand = (elapsed_seconds_for_range - second_hand) / (60) % 60
105
+ hour_hand = (elapsed_seconds_for_range - minute_hand - second_hand) / (60 * 60) + reference.hour % 24
106
+ Chronic.construct(reference.year, reference.month, reference.day, hour_hand, minute_hand, second_hand)
107
+ end
93
108
  end
94
- end
109
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterFortnight < Repeater #:nodoc:
3
3
  FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_fortnight_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_fortnight_start
13
+ unless @current_fortnight_start
13
14
  case pointer
14
15
  when :future
15
16
  sunday_repeater = RepeaterDayName.new(:sunday)
@@ -68,4 +69,4 @@ module Chronic
68
69
  super << '-fortnight'
69
70
  end
70
71
  end
71
- end
72
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterHour < Repeater #:nodoc:
3
3
  HOUR_SECONDS = 3600 # 60 * 60
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_hour_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_hour_start
13
+ unless @current_hour_start
13
14
  case pointer
14
15
  when :future
15
16
  @current_hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour + 1)
@@ -55,4 +56,4 @@ module Chronic
55
56
  super << '-hour'
56
57
  end
57
58
  end
58
- end
59
+ end