module Tickle

Constants

VERSION

This library's current version.

Public Class Methods

combine_multiple_numbers() click to toggle source

Turns compound numbers, like 'twenty first' => 21

# File lib/tickle/tickle.rb, line 258
def combine_multiple_numbers
  if [:number, :ordinal].all? {|type| token_types.include? type}
    number = token_of_type(:number)
    ordinal = token_of_type(:ordinal)
    combined_original = "#{number.original} #{ordinal.original}"
    combined_word = (number.start.to_s[0] + ordinal.word)
    combined_value = (number.start.to_s[0] + ordinal.start.to_s)
    new_number_token = Token.new(combined_original, combined_word, :ordinal, combined_value, 365)
    @tokens.reject! {|token| (token.type == :number || token.type == :ordinal)}
    @tokens << new_number_token
  end
end
days_in_month(month=nil) click to toggle source

Return the number of days in a specified month. If no month is specified, current month is used.

# File lib/tickle/tickle.rb, line 291
def days_in_month(month=nil)
  month ||= Date.today.month
  days_in_mon = Date.civil(Date.today.year, month, -1).day
end
get_next_month(number) click to toggle source

Returns the next available month based on the current day of the month. For example, if get_next_month(15) is called and the start date is the 10th, then it will return the 15th of this month. However, if get_next_month(15) is called and the start date is the 18th, it will return the 15th of next month.

# File lib/tickle/tickle.rb, line 279
def get_next_month(number)
  month = number.to_i < @start.day ? (@start.month == 12 ? 1 : @start.month + 1) : @start.month
end
next_appropriate_year(month, day) click to toggle source
# File lib/tickle/tickle.rb, line 283
def next_appropriate_year(month, day)
  start = @start || Date.today
  year = (Date.new(start.year.to_i, month.to_i, day.to_i) == start.to_date) ? start.year + 1 : start.year
  return year
end
parse(text, specified_options = {}) click to toggle source

Configuration options

  • start - start date for future occurrences. Must be in valid date format.

  • until - last date to run occurrences until. Must be in valid date format.

Use by calling Tickle.parse and passing natural language with or without options.

def get_next_occurrence
    results = Tickle.parse('every Wednesday starting June 1st until Dec 15th')
    return results[:next] if results
end
# File lib/tickle/tickle.rb, line 38
def parse(text, specified_options = {})
  # get options and set defaults if necessary.  Ability to set now is mostly for debugging
  default_options = {:start => Time.now, :next_only => false, :until => nil, :now => Time.now}
  options = default_options.merge specified_options

  # ensure an expression was provided
  raise(InvalidArgumentException, 'date expression is required') unless text

  # ensure the specified options are valid
  specified_options.keys.each do |key|
    raise(InvalidArgumentException, "#{key} is not a valid option key.") unless default_options.keys.include?(key)
  end
  raise(InvalidArgumentException, ':start specified is not a valid datetime.') unless  (is_date(specified_options[:start]) || Chronic.parse(specified_options[:start])) if specified_options[:start]

  # check to see if a valid datetime was passed
  return text if text.is_a?(Date) ||  text.is_a?(Time)

  # check to see if this event starts some other time and reset now
  event = scan_expression(text, options)

  Tickle.dwrite("start: #{@start}, until: #{@until}, now: #{options[:now].to_date}")

  # => ** this is mostly for testing. Bump by 1 day if today (or in the past for testing)
  raise(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur in the past for a future event") if @start && @start.to_date < Date.today
  raise(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur after the end date") if @until && @start.to_date > @until.to_date

  # no need to guess at expression if the start_date is in the future
  best_guess = nil
  if @start.to_date > options[:now].to_date
    best_guess = @start
  else
    # put the text into a normal format to ease scanning using Chronic
    event = pre_filter(event)

    # split into tokens
    @tokens = base_tokenize(event)

    # process each original word for implied word
    post_tokenize

    @tokens.each {|x| Tickle.dwrite("raw: #{x.inspect}")}

    # scan the tokens with each token scanner
    @tokens = Repeater.scan(@tokens)

    # remove all tokens without a type
    @tokens.reject! {|token| token.type.nil? }

    # combine number and ordinals into single number
    combine_multiple_numbers

    @tokens.each {|x| Tickle.dwrite("processed: #{x.inspect}")}

    # if we can't guess it maybe chronic can
    best_guess = (guess || chronic_parse(event))
  end

  raise(InvalidDateExpression, "the next occurrence takes place after the end date specified") if @until && best_guess.to_date > @until.to_date

  if !best_guess
    return nil
  elsif options[:next_only] != true
    return {:next => best_guess.to_time, :expression => event.strip, :starting => @start, :until => @until}
  else
    return best_guess
  end
end
post_tokenize() click to toggle source

normalizes each token

# File lib/tickle/tickle.rb, line 205
def post_tokenize
  @tokens.each do |token|
    token.word = normalize(token.original)
  end
end
pre_filter(text) click to toggle source

Normalize natural string removing prefix language

# File lib/tickle/tickle.rb, line 187
def pre_filter(text)
  return nil unless text

  text.gsub!(/every(\s)?/, '')
  text.gsub!(/each(\s)?/, '')
  text.gsub!(/repeat(s|ing)?(\s)?/, '')
  text.gsub!(/on the(\s)?/, '')
  text.gsub!(/([^\w\d\s])+/, '')
  normalize_us_holidays(text.downcase.strip)
end
process_for_ending(text) click to toggle source

process the remaining expression to see if an until, end, ending is specified

# File lib/tickle/tickle.rb, line 177
def process_for_ending(text)
  regex = /^(.*)(\s(?:\bend|until)(?:s|ing)?)(.*)/i
  if text =~ regex
    return text.match(regex)[1], text.match(regex)[3]
  else
    return text, nil
  end
end
scan_expression(text, options) click to toggle source

scans the expression for a variety of natural formats, such as 'every thursday starting tomorrow until May 15th

# File lib/tickle/tickle.rb, line 107
def scan_expression(text, options)
  starting = ending = nil

  start_every_regex = /^
    (start(?:s|ing)?)                 # 0
    \s
    (.*)
    (\s(?:every|each|\bon\b|repeat)   # 1
    (?:s|ing)?)                       # 2
    (.*)                              # 3
  /ix
  every_start_regex = /^(every|each|\bon\b|repeat(?:the)?)\s(.*)(\s(?:start)(?:s|ing)?)(.*)/i
  start_ending_regex = /^
    (start(?:s|ing)?)   # 0
    \s+
    (.*?)(?:\s+and)?      # 1
    (\s
      (?:\bend|until)
      (?:s|ing)?
    )                   # 2
    (.*)                # 3
  /ix
  if text =~ start_every_regex
    starting = text.match(start_every_regex)[2].strip
    text = text.match(start_every_regex)[4].strip
    event, ending = process_for_ending(text)
  elsif text =~ every_start_regex
    event = text.match(every_start_regex)[2].strip
    text = text.match(every_start_regex)[4].strip
    starting, ending = process_for_ending(text)
  elsif text =~ start_ending_regex
    md = text.match start_ending_regex
    starting = md.captures[1]
    ending = md.captures.last.strip
    event = 'day'
  else
    event, ending = process_for_ending(text)
  end

  # they gave a phrase so if we can't interpret then we need to raise an error
  if starting
    Tickle.dwrite("starting: #{starting}")
    @start ||= nil # initialize the variable to quell warnings
    @start = chronic_parse(pre_filter(starting))
    if @start
      @start.to_time
    else
      raise(InvalidDateExpression,"the starting date expression \"#{starting}\" could not be interpretted")
    end
  else
    @start = options[:start].to_time rescue nil
  end

  if ending
    @until = chronic_parse(pre_filter(ending))
    if @until
      @until.to_time
    else
      raise(InvalidDateExpression,"the ending date expression \"#{ending}\" could not be interpretted")
    end
  else
    @until = options[:until].to_time rescue nil
  end

  @next = nil

  return event
end
token_types() click to toggle source

Returns an array of types for all tokens

# File lib/tickle/tickle.rb, line 272
def token_types
  @tokens.map(&:type)
end

Private Class Methods

chronic_parse(exp) click to toggle source

slightly modified chronic parser to ensure that the date found is in the future first we check to see if an explicit date was passed and, if so, dont do anything. if, however, a date expression was passed we evaluate and shift forward if needed

# File lib/tickle/tickle.rb, line 301
def chronic_parse(exp)
  result = Chronic.parse(exp.ordinal_as_number)
  result = Time.local(result.year + 1, result.month, result.day, result.hour, result.min, result.sec) if result && result.to_time < Time.now
  Tickle.dwrite("Chronic.parse('#{exp}') # => #{result}")
  result
end