Module: Chronic

Defined in:
lib/chronic.rb,
lib/chronic/scalar.rb,
lib/chronic/chronic.rb,
lib/chronic/ordinal.rb,
lib/chronic/pointer.rb,
lib/chronic/handlers.rb,
lib/chronic/separator.rb

Defined Under Namespace

Classes: ChronicPain, Grabber, Handler, InvalidArgumentException, Ordinal, OrdinalDay, Pointer, Repeater, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterFortnight, RepeaterHour, RepeaterMinute, RepeaterMonth, RepeaterMonthName, RepeaterSeason, RepeaterSeasonName, RepeaterSecond, RepeaterTime, RepeaterWeek, RepeaterWeekend, RepeaterYear, Scalar, ScalarDay, ScalarMonth, ScalarYear, Separator, SeparatorAt, SeparatorComma, SeparatorIn, SeparatorSlashOrDash, Span, Tag, Token

Constant Summary collapse

VERSION =
"0.1.6"

Class Method Summary collapse

Class Method Details

.base_tokenize(text) ⇒ Object

Split the text on spaces and convert each word into a Token



139
140
141
# File 'lib/chronic/chronic.rb', line 139

def base_tokenize(text) #:nodoc:
  text.split(' ').map { |word| Token.new(word) }
end

.day_or_time(day_start, time_tokens, options) ⇒ Object




75
76
77
78
79
80
81
82
83
84
85
# File 'lib/chronic/handlers.rb', line 75

def day_or_time(day_start, time_tokens, options)
  outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
  
  if !time_tokens.empty?
    @now = outer_span.begin
    time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
    return time
  else
    return outer_span
  end
end

.dealias_and_disambiguate_times(tokens, options) ⇒ Object

:nodoc:



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/chronic/handlers.rb', line 332

def dealias_and_disambiguate_times(tokens, options) #:nodoc:
  # handle aliases of am/pm
  # 5:00 in the morning => 5:00 am
  # 7:00 in the evening => 7:00 pm
  #ttokens = []
  tokens.each_with_index do |t0, i|
    t1 = tokens[i + 1]
    if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
      if [:morning].include?(t1tag.type)
        t1.untag(RepeaterDayPortion)
        t1.tag(RepeaterDayPortion.new(:am))
      elsif [:afternoon, :evening, :night].include?(t1tag.type)
        t1.untag(RepeaterDayPortion)
        t1.tag(RepeaterDayPortion.new(:pm))
      end
    end
  end
  #tokens = ttokens
        
  # handle ambiguous times if :ambiguous_time_range is specified
  if options[:ambiguous_time_range] != :none
    ttokens = []
    tokens.each_with_index do |t0, i|
      ttokens << t0
      t1 = tokens[i + 1]
      if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
        distoken = Token.new('disambiguator')
        distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
        ttokens << distoken
      end
    end
    tokens = ttokens
  end
  
  tokens
end

.debugObject



39
# File 'lib/chronic.rb', line 39

def self.debug; false; end

.definitionsObject

:nodoc:



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/chronic/handlers.rb', line 5

def definitions #:nodoc:
 @definitions ||= 
  {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
    
   :date => [Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
             Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
             Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
             Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
             Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
             Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
             Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
             Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
             Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
             Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
             
   :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
               Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
               
   :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
              Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
              Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
              
   :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
               Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
  }
end

.find_within(tags, span, pointer) ⇒ Object

Recursively finds repeaters within other repeaters. Returns a Span representing the innermost time span or nil if no repeater union could be found



317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/chronic/handlers.rb', line 317

def find_within(tags, span, pointer) #:nodoc:
  puts "--#{span}" if Chronic.debug
  return span if tags.empty?
  
  head, *rest = tags
  head.start = pointer == :future ? span.begin : span.end
  h = head.this(:none)
        
  if span.include?(h.begin) || span.include?(h.end)
    return find_within(rest, h, pointer)
  else
    return nil
  end
end

.get_anchor(tokens, options) ⇒ Object

support methods



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/chronic/handlers.rb', line 271

def get_anchor(tokens, options) #:nodoc:
  grabber = Grabber.new(:this)
  pointer = :future
  
  repeaters = self.get_repeaters(tokens)
  repeaters.size.times { tokens.pop }
                
  if tokens.first && tokens.first.get_tag(Grabber)
    grabber = tokens.first.get_tag(Grabber)
    tokens.pop
  end
  
  head = repeaters.shift
  head.start = @now
              
  case grabber.type
    when :last
      outer_span = head.next(:past)
    when :this
      if repeaters.size > 0
        outer_span = head.this(:none)
      else
        outer_span = head.this(options[:context])
      end
    when :next
      outer_span = head.next(:future)
    else raise(ChronicPain, "Invalid grabber")
  end
  
  puts "--#{outer_span}" if Chronic.debug
  anchor = find_within(repeaters, outer_span, pointer)
end

.get_repeaters(tokens) ⇒ Object

:nodoc:



304
305
306
307
308
309
310
311
312
# File 'lib/chronic/handlers.rb', line 304

def get_repeaters(tokens) #:nodoc:
  repeaters = []
tokens.each do |token|
  if t = token.get_tag(Repeater)
      repeaters << t
    end
  end
  repeaters.sort.reverse
end

.guess(span) ⇒ Object

Guess a specific time within the given span



144
145
146
147
148
149
150
151
# File 'lib/chronic/chronic.rb', line 144

def guess(span) #:nodoc:
  return nil if span.nil?
  if span.width > 1
    span.begin + (span.width / 2)
  else
    span.begin
  end
end

.handle_m_d(month, day, time_tokens, options) ⇒ Object




89
90
91
92
93
94
95
96
# File 'lib/chronic/handlers.rb', line 89

def handle_m_d(month, day, time_tokens, options) #:nodoc:
  month.start = @now
  span = month.this(options[:context])
  
  day_start = Time.local(span.begin.year, span.begin.month, day)
  
  day_or_time(day_start, time_tokens, options)
end

.handle_o_r_g_r(tokens, options) ⇒ Object

:nodoc:



264
265
266
267
# File 'lib/chronic/handlers.rb', line 264

def handle_o_r_g_r(tokens, options) #:nodoc:
  outer_span = get_anchor(tokens[2..3], options)
  handle_orr(tokens[0..1], outer_span, options)
end

.handle_o_r_s_r(tokens, options) ⇒ Object

:nodoc:



259
260
261
262
# File 'lib/chronic/handlers.rb', line 259

def handle_o_r_s_r(tokens, options) #:nodoc:
  outer_span = get_anchor([tokens[3]], options)
  handle_orr(tokens[0..1], outer_span, options)
end

.handle_orr(tokens, outer_span, options) ⇒ Object

narrows



244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/chronic/handlers.rb', line 244

def handle_orr(tokens, outer_span, options) #:nodoc:
  repeater = tokens[1].get_tag(Repeater)
  repeater.start = outer_span.begin - 1
  ordinal = tokens[0].get_tag(Ordinal).type
  span = nil
  ordinal.times do
    span = repeater.next(:future)
    if span.begin > outer_span.end
      span = nil
      break
    end
  end
  span
end

.handle_p_s_r(tokens, options) ⇒ Object

:nodoc:



232
233
234
235
# File 'lib/chronic/handlers.rb', line 232

def handle_p_s_r(tokens, options) #:nodoc:
  new_tokens = [tokens[1], tokens[2], tokens[0]]
  self.handle_s_r_p(new_tokens, options)
end

.handle_r(tokens, options) ⇒ Object

anchors



194
195
196
197
# File 'lib/chronic/handlers.rb', line 194

def handle_r(tokens, options) #:nodoc:
  dd_tokens = dealias_and_disambiguate_times(tokens, options)
  self.get_anchor(dd_tokens, options)
end

.handle_r_g_r(tokens, options) ⇒ Object

:nodoc:



199
200
201
202
# File 'lib/chronic/handlers.rb', line 199

def handle_r_g_r(tokens, options) #:nodoc:
  new_tokens = [tokens[1], tokens[0], tokens[2]]
  self.handle_r(new_tokens, options)
end

.handle_rmn_od(tokens, options) ⇒ Object

:nodoc:



102
103
104
# File 'lib/chronic/handlers.rb', line 102

def handle_rmn_od(tokens, options) #:nodoc:
  handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
end

.handle_rmn_sd(tokens, options) ⇒ Object

:nodoc:



98
99
100
# File 'lib/chronic/handlers.rb', line 98

def handle_rmn_sd(tokens, options) #:nodoc:
  handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
end

.handle_rmn_sd_sy(tokens, options) ⇒ Object

:nodoc:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/chronic/handlers.rb', line 125

def handle_rmn_sd_sy(tokens, options) #:nodoc:
  month = tokens[0].get_tag(RepeaterMonthName).index
  day = tokens[1].get_tag(ScalarDay).type
  year = tokens[2].get_tag(ScalarYear).type
  
  time_tokens = tokens.last(tokens.size - 3)
  
  begin
    day_start = Time.local(year, month, day)
    day_or_time(day_start, time_tokens, options)
  rescue ArgumentError
    nil
  end
end

.handle_rmn_sy(tokens, options) ⇒ Object

:nodoc:



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/chronic/handlers.rb', line 106

def handle_rmn_sy(tokens, options) #:nodoc:
  month = tokens[0].get_tag(RepeaterMonthName).index
  year = tokens[1].get_tag(ScalarYear).type
  
  if month == 12
    next_month_year = year + 1
    next_month_month = 1
  else
    next_month_year = year
    next_month_month = month + 1
  end
  
  begin
    Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
  rescue ArgumentError
    nil
  end
end

.handle_s_r_p(tokens, options) ⇒ Object

:nodoc:



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/chronic/handlers.rb', line 214

def handle_s_r_p(tokens, options) #:nodoc:
  repeater = tokens[1].get_tag(Repeater)
        
  span = 
  case true
  when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
    self.parse("this hour", :guess => false, :now => @now)
  when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
    self.parse("this minute", :guess => false, :now => @now)
  when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
    self.parse("this second", :guess => false, :now => @now)
  else
    raise(ChronicPain, "Invalid repeater: #{repeater.class}")
  end
  
  self.handle_srp(tokens, span, options)
end

.handle_s_r_p_a(tokens, options) ⇒ Object

:nodoc:



237
238
239
240
# File 'lib/chronic/handlers.rb', line 237

def handle_s_r_p_a(tokens, options) #:nodoc:
  anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
  self.handle_srp(tokens, anchor_span, options)
end

.handle_sd_rmn_sy(tokens, options) ⇒ Object

:nodoc:



140
141
142
143
144
# File 'lib/chronic/handlers.rb', line 140

def handle_sd_rmn_sy(tokens, options) #:nodoc:
  new_tokens = [tokens[1], tokens[0], tokens[2]]
  time_tokens = tokens.last(tokens.size - 3)
  self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
end

.handle_sd_sm_sy(tokens, options) ⇒ Object

:nodoc:



161
162
163
164
165
# File 'lib/chronic/handlers.rb', line 161

def handle_sd_sm_sy(tokens, options) #:nodoc:
  new_tokens = [tokens[1], tokens[0], tokens[2]]
  time_tokens = tokens.last(tokens.size - 3)
  self.handle_sm_sd_sy(new_tokens + time_tokens, options)
end

.handle_sm_sd_sy(tokens, options) ⇒ Object

:nodoc:



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/chronic/handlers.rb', line 146

def handle_sm_sd_sy(tokens, options) #:nodoc:
  month = tokens[0].get_tag(ScalarMonth).type
  day = tokens[1].get_tag(ScalarDay).type
  year = tokens[2].get_tag(ScalarYear).type
  
  time_tokens = tokens.last(tokens.size - 3)
  
  begin
    day_start = Time.local(year, month, day) #:nodoc:
    day_or_time(day_start, time_tokens, options)
  rescue ArgumentError
    nil
  end
end

.handle_sm_sy(tokens, options) ⇒ Object

:nodoc:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/chronic/handlers.rb', line 173

def handle_sm_sy(tokens, options) #:nodoc:
  month = tokens[0].get_tag(ScalarMonth).type
  year = tokens[1].get_tag(ScalarYear).type
  
  if month == 12
    next_month_year = year + 1
    next_month_month = 1
  else
    next_month_year = year
    next_month_month = month + 1
  end
  
  begin
    Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
  rescue ArgumentError
    nil
  end
end

.handle_srp(tokens, span, options) ⇒ Object

arrows



206
207
208
209
210
211
212
# File 'lib/chronic/handlers.rb', line 206

def handle_srp(tokens, span, options) #:nodoc:
  distance = tokens[0].get_tag(Scalar).type
  repeater = tokens[1].get_tag(Repeater)
  pointer = tokens[2].get_tag(Pointer).type
  
  repeater.offset(span, distance, pointer)
end

.handle_sy_sm_sd(tokens, options) ⇒ Object

:nodoc:



167
168
169
170
171
# File 'lib/chronic/handlers.rb', line 167

def handle_sy_sm_sd(tokens, options) #:nodoc:
  new_tokens = [tokens[1], tokens[2], tokens[0]]
  time_tokens = tokens.last(tokens.size - 3)
  self.handle_sm_sd_sy(new_tokens + time_tokens, options)
end

.numericize_numbers(text) ⇒ Object

Convert number words to numbers (three => 3)



128
129
130
# File 'lib/chronic/chronic.rb', line 128

def numericize_numbers(text) #:nodoc:
  text
end

.numericize_ordinals(text) ⇒ Object

Convert ordinal words to numeric ordinals (third => 3rd)



133
134
135
# File 'lib/chronic/chronic.rb', line 133

def numericize_ordinals(text) #:nodoc:
  text
end

.parse(text, specified_options = {}) ⇒ Object

Parses a string containing a natural language date or time. If the parser can find a date or time, either a Time or Chronic::Span will be returned (depending on the value of :guess). If no date or time can be found, nil will be returned.

Options are:

:context

:past or :future (defaults to :future)

If your string represents a birthday, you can set :context to :past and if an ambiguous string is given, it will assume it is in the past. Specify :future or omit to set a future context.

:now

Time (defaults to Time.now)

By setting :now to a Time, all computations will be based off of that time instead of Time.now

:guess

true or false (defaults to true)

By default, the parser will guess a single point in time for the given date or time. If you’d rather have the entire time span returned, set :guess to false and a Chronic::Span will be returned.

:ambiguous_time_range

Integer or :none (defaults to 6 (6am-6pm))

If an Integer is given, ambiguous times (like 5:00) will be assumed to be within the range of that time in the AM to that time in the PM. For example, if you set it to 7, then the parser will look for the time between 7am and 7pm. In the case of 5:00, it would assume that means 5:00pm. If :none is given, no assumption will be made, and the first matching instance of that time will be used.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/chronic/chronic.rb', line 41

def parse(text, specified_options = {})
  # get options and set defaults if necessary
  default_options = {:context => :future,
                     :now => Time.now,
                     :guess => true,
                     :ambiguous_time_range => 6}
  options = default_options.merge specified_options
        
  # ensure the specified options are valid
  specified_options.keys.each do |key|
    default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
  end
  [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
  
  # store now for later =)
  @now = options[:now]
  
  # put the text into a normal format to ease scanning
  text = self.pre_normalize(text)
      
  # get base tokens for each word
  @tokens = self.base_tokenize(text)

  # scan the tokens with each token scanner
  [Repeater].each do |tokenizer|
    @tokens = tokenizer.scan(@tokens, options)
  end
  
  [Grabber, Pointer, Scalar, Ordinal, Separator].each do |tokenizer|
    @tokens = tokenizer.scan(@tokens)
  end
  
  # strip any non-tagged tokens
  @tokens = @tokens.select { |token| token.tagged? }
  
  if Chronic.debug
    puts "+---------------------------------------------------"
    puts "| " + @tokens.to_s
    puts "+---------------------------------------------------"
  end
  
  # do the heavy lifting
  begin
    span = self.tokens_to_span(@tokens, options)
  rescue
    raise
    return nil
  end
  
  # guess a time within a span if required
  if options[:guess]
    return self.guess(span)
  else
    return span
  end
end

.pre_normalize(text) ⇒ Object

Clean up the specified input text by stripping unwanted characters, converting idioms to their canonical form, converting number words to numbers (three => 3), and converting ordinal words to numeric ordinals (third => 3rd)



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/chronic/chronic.rb', line 102

def pre_normalize(text) #:nodoc:
  normalized_text = text.to_s.downcase
  normalized_text.gsub!(/['"\.]/, '')
  normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
  normalized_text.gsub!(/\btoday\b/, 'this day')
  normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
  normalized_text.gsub!(/\byesterday\b/, 'last day')
  normalized_text.gsub!(/\bnoon\b/, '12:00')
  normalized_text.gsub!(/\bmidnight\b/, '24:00')
  normalized_text.gsub!(/\bbefore now\b/, 'past')
  normalized_text.gsub!(/\bnow\b/, 'this second')
  normalized_text.gsub!(/\b(ago|before)\b/, 'past')
  normalized_text.gsub!(/\bthis past\b/, 'last')
  normalized_text.gsub!(/\bthis last\b/, 'last')
  normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
  normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
  normalized_text.gsub!(/\btonight\b/, 'this night')
  normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
  normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
  normalized_text.gsub!(/\ba\b/, '1')
  normalized_text.gsub!(/\s+/, ' ')
  normalized_text = numericize_numbers(normalized_text)
  normalized_text = numericize_ordinals(normalized_text)
end

.tokens_to_span(tokens, options) ⇒ Object

:nodoc:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/chronic/handlers.rb', line 32

def tokens_to_span(tokens, options) #:nodoc:                   
  # maybe it's a specific date
  
  self.definitions[:date].each do |handler|
    if handler.match(tokens, self.definitions)
      good_tokens = tokens.select { |o| !o.get_tag Separator }
      return self.send(handler.handler_method, good_tokens, options)
    end
  end
        
  # I guess it's not a specific date, maybe it's just an anchor
        
  self.definitions[:anchor].each do |handler|
    if handler.match(tokens, self.definitions)
      good_tokens = tokens.select { |o| !o.get_tag Separator }
      return self.send(handler.handler_method, good_tokens, options)
    end
  end
        
  # not an anchor, perhaps it's an arrow
  
  self.definitions[:arrow].each do |handler|
    if handler.match(tokens, self.definitions)
      good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
      return self.send(handler.handler_method, good_tokens, options)
    end
  end
  
  # not an arrow, let's hope it's an narrow
  
  self.definitions[:narrow].each do |handler|
    if handler.match(tokens, self.definitions)
      #good_tokens = tokens.select { |o| !o.get_tag Separator }
      return self.send(handler.handler_method, tokens, options)
    end
  end
  
  # I guess you're out of luck!
  return nil
end