Module: Chronic

Defined in:
lib/chronic.rb,
lib/chronic/tag.rb,
lib/chronic/span.rb,
lib/chronic/token.rb,
lib/chronic/scalar.rb,
lib/chronic/season.rb,
lib/chronic/chronic.rb,
lib/chronic/grabber.rb,
lib/chronic/handler.rb,
lib/chronic/ordinal.rb,
lib/chronic/pointer.rb,
lib/chronic/handlers.rb,
lib/chronic/repeater.rb,
lib/chronic/mini_date.rb,
lib/chronic/numerizer.rb,
lib/chronic/separator.rb,
lib/chronic/time_zone.rb,
lib/chronic/repeaters/repeater_day.rb,
lib/chronic/repeaters/repeater_hour.rb,
lib/chronic/repeaters/repeater_time.rb,
lib/chronic/repeaters/repeater_week.rb,
lib/chronic/repeaters/repeater_year.rb,
lib/chronic/repeaters/repeater_month.rb,
lib/chronic/repeaters/repeater_minute.rb,
lib/chronic/repeaters/repeater_season.rb,
lib/chronic/repeaters/repeater_second.rb,
lib/chronic/repeaters/repeater_weekday.rb,
lib/chronic/repeaters/repeater_weekend.rb,
lib/chronic/repeaters/repeater_day_name.rb,
lib/chronic/repeaters/repeater_fortnight.rb,
lib/chronic/repeaters/repeater_month_name.rb,
lib/chronic/repeaters/repeater_day_portion.rb,
lib/chronic/repeaters/repeater_season_name.rb

Overview

Parse natural language dates and times into Time or Span objects

Examples:

require 'chronic'

Time.now   #=> Sun Aug 27 23:18:25 PDT 2006

Chronic.parse('tomorrow')
  #=> Mon Aug 28 12:00:00 PDT 2006

Chronic.parse('monday', :context => :past)
  #=> Mon Aug 21 12:00:00 PDT 2006

Chronic.parse('this tuesday 5:00')
  #=> Tue Aug 29 17:00:00 PDT 2006

Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
  #=> Tue Aug 29 05:00:00 PDT 2006

Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
  #=> Sat May 27 12:00:00 PDT 2000

Chronic.parse('may 27th', :guess => false)
  #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007

Author:

  • Tom Preston-Werner, Lee Jarvis

Defined Under Namespace

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

Constant Summary collapse

VERSION =
"0.6.5"
DEFAULT_OPTIONS =
{
  :context => :future,
  :now => nil,
  :guess => true,
  :ambiguous_time_range => 6,
  :endian_precedence    => [:middle, :little],
  :ambiguous_year_future_bias => 50
}

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.debugBoolean

Returns true when debug mode is enabled.

Returns:

  • (Boolean)

    true when debug mode is enabled



36
37
38
# File 'lib/chronic.rb', line 36

def debug
  @debug
end

.nowTime?

The current Time Chronic is using to base from

Examples:

Time.now #=> 2011-06-06 14:13:43 +0100
Chronic.parse('yesterday') #=> 2011-06-05 12:00:00 +0100

now = Time.local(2025, 12, 24)
Chronic.parse('tomorrow', :now => now) #=> 2025-12-25 12:00:00 +0000

Returns:



60
61
62
# File 'lib/chronic.rb', line 60

def now
  @now
end

.time_classTime

Returns The time class Chronic uses internally.

Examples:

require 'chronic'
require 'active_support/time'

Time.zone = 'UTC'
Chronic.time_class = Time.zone
Chronic.parse('June 15 2006 at 5:54 AM')
  # => Thu, 15 Jun 2006 05:45:00 UTC +00:00

Returns:

  • (Time)

    The time class Chronic uses internally



48
49
50
# File 'lib/chronic.rb', line 48

def time_class
  @time_class
end

Class Method Details

.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) ⇒ Time

Construct a time Object

Returns:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/chronic/chronic.rb', line 234

def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
  if second >= 60
    minute += second / 60
    second = second % 60
  end

  if minute >= 60
    hour += minute / 60
    minute = minute % 60
  end

  if hour >= 24
    day += hour / 24
    hour = hour % 24
  end

  # determine if there is a day overflow. this is complicated by our crappy calendar
  # system (non-constant number of days per month)
  day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
  if day > 28
    # no month ever has fewer than 28 days, so only do this if necessary
    leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
    if day > days_this_month
      month += day / days_this_month
      day = day % days_this_month
    end
  end

  if month > 12
    if month % 12 == 0
      year += (month - 12) / 12
      month = 12
    else
      year += month / 12
      month = month % 12
    end
  end

  Chronic.time_class.local(year, month, day, hour, minute, second)
end

.definitions(options = {}) ⇒ Hash

List of Handler definitions. See parse for a list of options this method accepts

Returns:

  • (Hash)

    A Hash of Handler definitions

See Also:



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/chronic/chronic.rb', line 162

def definitions(options={})
  options[:endian_precedence] ||= [:middle, :little]

  @definitions ||= {
    :time => [
      Handler.new([:repeater_time, :repeater_day_portion?], nil)
    ],

    :date => [
      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),
      Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
      Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
      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),
      Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
      Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
      Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
      Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
      Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
      Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
      Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
      Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
      Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
      Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
      Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
      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_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
      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)
    ],

    # tonight at 7pm
    :anchor => [
      Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
      Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
      Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
    ],

    # 3 weeks from now, in 2 months
    :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)
    ],

    # 3rd week in march
    :narrow => [
      Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
      Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
    ]
  }

  endians = [
    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)
  ]

  case endian = Array(options[:endian_precedence]).first
  when :little
    @definitions[:endian] = endians.reverse
  when :middle
    @definitions[:endian] = endians
  else
    raise ArgumentError, "Unknown endian option '#{endian}'"
  end

  @definitions
end

.guess(span) ⇒ Time

Guess a specific time within the given span

Parameters:

Returns:



149
150
151
152
153
154
155
# File 'lib/chronic/chronic.rb', line 149

def guess(span)
  if span.width > 1
    span.begin + (span.width / 2)
  else
    span.begin
  end
end

.numericize_numbers(text) ⇒ String

Convert number words to numbers (three => 3, fourth => 4th)

Parameters:

  • text (String)

    The string to convert

Returns:

  • (String)

    A new string with words converted to numbers

See Also:



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

def numericize_numbers(text)
  warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
  Numerizer.numerize(text)
end

.parse(text, opts = {}) ⇒ Time, ...

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

Parameters:

  • text (String)

    The text to parse

  • opts (Hash) (defaults to: {})

    a customizable set of options

Options Hash (opts):

  • :context (Symbol) — default: :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 (Object) — default: Time.now
    • By setting :now to a Time, all computations will be based off of that time instead of Time.now. If set to nil, Chronic will use Time.now
  • :guess (Boolean) — default: 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 Span will be returned
  • :ambiguous_time_range (Integer) — default: 6
    • 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
  • :endian_precedence (Array) — default: [:middle, :little]
    • By default, Chronic will parse “03/04/2011” as the fourth day of the third month. Alternatively you can tell Chronic to parse this as the third day of the fourth month by altering the :endian_precedence to [:little, :middle]
  • :ambiguous_year_future_bias (Integer) — default: 50
    • When parsing two digit years (ie 79) unlike Rubys Time class, Chronic will attempt to assume the full year using this figure. Chronic will look x amount of years into the future and past. If the two digit year is now + x years it’s assumed to be the future, now - x years is assumed to be the past

Returns:



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
# File 'lib/chronic/chronic.rb', line 61

def parse(text, opts={})
  options = DEFAULT_OPTIONS.merge opts

  # ensure the specified options are valid
  (opts.keys - DEFAULT_OPTIONS.keys).each do |key|
    raise ArgumentError, "#{key} is not a valid option key."
  end

  unless [:past, :future, :none].include?(options[:context])
    raise ArgumentError, "Invalid context, :past/:future only"
  end

  options[:text] = text
  Chronic.now = options[:now] || Chronic.time_class.now

  # tokenize words
  tokens = tokenize(text, options)

  if Chronic.debug
    puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
  end

  span = tokens_to_span(tokens, options)

  if span
    options[:guess] ? guess(span) : span
  end
end

.pre_normalize(text) ⇒ String

Clean up the specified text ready for parsing

Clean up the string 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)

Examples:

Chronic.pre_normalize('first day in May')
  #=> "1st day in may"

Chronic.pre_normalize('tomorrow after noon')
  #=> "next day future 12:00"

Chronic.pre_normalize('one hundred and thirty six days from now')
  #=> "136 days future this second"

Parameters:

  • text (String)

    The string to normalize

Returns:

  • (String)

    A new string ready for Chronic to parse



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/chronic/chronic.rb', line 109

def pre_normalize(text)
  text = text.to_s.downcase
  text.gsub!(/['"\.]/, '')
  text.gsub!(/,/, ' ')
  text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
  text = Numerizer.numerize(text)
  text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
  text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
  text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, '\1')
  text.gsub!(/\btoday\b/, 'this day')
  text.gsub!(/\btomm?orr?ow\b/, 'next day')
  text.gsub!(/\byesterday\b/, 'last day')
  text.gsub!(/\bnoon\b/, '12:00pm')
  text.gsub!(/\bmidnight\b/, '24:00')
  text.gsub!(/\bnow\b/, 'this second')
  text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
  text.gsub!(/\bthis (?:last|past)\b/, 'last')
  text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
  text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
  text.gsub!(/\btonight\b/, 'this night')
  text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
  text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
  text.gsub!(/\b(hence|after|from)\b/, 'future')
  text
end