module Tickle
Constants
- VERSION
This library's current version.
Public Class Methods
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
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
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
# 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
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
normalizes each token
# File lib/tickle/tickle.rb, line 205 def post_tokenize @tokens.each do |token| token.word = normalize(token.original) end end
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 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
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
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
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