module Chronic

Parse natural language dates and times into Time or {Chronic::Span} objects

@example

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

Constants

DEFAULT_OPTIONS
VERSION

Attributes

debug[RW]

@return [Boolean] true when debug mode is enabled

now[RW]

The current Time Chronic is using to base from

@example

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

@return [Time, nil]

time_class[RW]

@example

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

@return [Time] The time class Chronic uses internally

Public Class Methods

construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) click to toggle source

Construct a time Object

@return [Time]

# File lib/chronic/chronic.rb, line 235
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={}) click to toggle source

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

@see parse @return [Hash] A Hash of Handler definitions

# 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, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
      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) click to toggle source

Guess a specific time within the given span

@param [Span] span @return [Time]

# 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) click to toggle source

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

@see Chronic::Numerizer.numerize @param [String] text The string to convert @return [String] A new string with words converted to numbers

# 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={}) click to toggle source

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

@param [String] text The text to parse

@option opts [Symbol] :context (: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.

@option opts [Object] :now (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`

@option opts [Boolean] :guess (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

@option opts [Integer] :ambiguous_time_range (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

@option opts [Array] :endian_precedence ([: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]`

@option opts [Integer] :ambiguous_year_future_bias (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 %xnow + x years` it's assumed to be the
  future, `now - x years` is assumed to be the past

@return [Time, Chronic::Span, nil]

# 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) click to toggle source

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)

@example

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"

@param [String] text The string to normalize @return [String] A new string ready for Chronic to parse

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