Skip to content

Commit

Permalink
working gem with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
noctivityinc committed Apr 22, 2010
1 parent 789855c commit 02ccfaf
Show file tree
Hide file tree
Showing 13 changed files with 634 additions and 31 deletions.
136 changes: 121 additions & 15 deletions README.rdoc
Expand Up @@ -20,34 +20,140 @@ $ gem install tickle

Everything's at Github - http://github.com/noctivityinc/tickle

-- DEPENDENCIES

chronic gem (gem install chronic)
thoughtbot's shoulda (gem install shoulda)

== USAGE

You can parse strings containing a natural language interval using the Tickle.parse method.

Tickle.parse returns an array of first occurrence, next occurrence, interval between occurrences.

You can also pass a start date with the word "starting" (e.g. Tickle.parse('every 3 days starting next friday'))

Tickle HEAVILY uses chronic for parsing both the event and the start date.

-- EXAMPLES

require 'rubygems'
require 'tickle'

Time.now #=> 2010-04-21 14:32:02 -0400

Tickle.parse('every 2 days')
#=> 2010-04-23, 2

Tickle.parse('every Sunday') #=> note, this upcoming Sunday is 4/25/2010
#=> 2010-04-25, 7

Tickle.parse('every 3 weeks')
#=> 2010-05-12, 21

Tickle.parse('2 days')
#=> 2010-04-23, 2
Time.now
2010-04-22 16:38:12 -0400

Tickle.parse('each day')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]

Tickle.parse('every day')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]

Tickle.parse('every week')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 7]

Tickle.parse('every Month')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 30]

Tickle.parse('every year')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 365]

Tickle.parse('daily')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1]

Tickle.parse('weekly')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 7]

Tickle.parse('monthly')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 30]

Tickle.parse('yearly')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 365]

Tickle.parse('every 3 days')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 3]

Tickle.parse('every 3 weeks')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 21]

Tickle.parse('every 3 months')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 90]

Tickle.parse('every 3 years')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 1095]

Tickle.parse('every other day')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 2]

Tickle.parse('every other week')
#=> [2010-04-22 16:38:12 -0400, 2010-04-23 16:38:12 -0400, 14]

Tickle.parse('every other month')
#=> [2010-04-22 16:38:12 -0400, 2010-06-22 16:38:12 -0400, 60]

Tickle.parse('every other year')
#=> [2010-04-22 16:38:12 -0400, 2012-04-22 16:38:12 -0400, 730]

Tickle.parse('every other day starting May 1st')
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 2]

Tickle.parse('every other week starting this Sunday')
#=> [2010-04-25 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 14]

Tickle.parse('every Monday')
#=> [2010-04-26 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('every Wednesday')
#=> [2010-04-28 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('every Friday')
#=> [2010-04-23 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('every May')
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]

Tickle.parse('every june')
#=> [2010-06-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]

Tickle.parse('beginning of the week')
#=> [2010-04-25 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('middle of the week')
#=> [2010-04-28 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('end of the week')
#=> [2010-04-24 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 7]

Tickle.parse('beginning of the month')
#=> [2010-05-01 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]

Tickle.parse('middle of the month')
#=> [2010-05-15 12:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]

Tickle.parse('end of the month')
#=> [2010-04-30 00:00:00 -0400, 2012-04-22 16:38:12 -0400, 30]

Tickle.parse('beginning of the year')
#=> [2011-01-01 12:00:00 -0500, 2012-04-22 16:38:12 -0400, 365]

Tickle.parse('middle of the year')
#=> [2010-06-15 00:00:00 -0400, 2012-04-22 16:38:12 -0400, 365]

Tickle.parse('end of the year')
#=> [2010-12-31 00:00:00 -0500, 2012-04-22 16:38:12 -0400, 365]


You can either pass a string prefixed with the word "every" or simply the time frame.
You can either pass a string prefixed with the word "every, each or 'on the'" or simply the time frame.

-- LIMITATIONS

Currently, Tickle only works for day intervals but feel free to fork and add time-based interval support. Also, numbers must be entered in numeric and not string form (i.e. 3 not three).
Currently, Tickle only works for day intervals but feel free to fork and add time-based interval support or send me a note if you really want me to add it.

== CREDIT

HUGE shout-out to both the creator of Chronic, Tom Preston-Werner (http://chronic.rubyforge.org/) as well as Brian Brownling who maintains a github version at http://github.com/mojombo/chronic.

Without their work and code structure I'd be lost.


== Note on Patches/Pull Requests
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -11,7 +11,7 @@ begin
gem.homepage = "http://github.com/noctivityinc/tickle"
gem.authors = ["Joshua Lippiner"]
gem.add_dependency('chronic', '>= 0.2.3')
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
gem.add_development_dependency "shoulda", ">= 2.10.3"

# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Expand Down
12 changes: 12 additions & 0 deletions SCENARIOS.rdoc
@@ -0,0 +1,12 @@
every 2 weeks => word 'week' found plus a number so the interval = number * 7 and start_date = today
every second tuesday => => day of week found WITH number (ordinal converted to 2). interval = 30 with start_date = Chronic.parse(Ordinal Number Day Of Week 'in' Start Date Month)
every sunday => day of week found without number. interval = 7, start_date = next day of week occurrence
every other day => word 'day' found with word 'other.' interval = 2, start_date = today
every fourth thursday => day of week found WITH number (ordinal converted to 4). interval = 30 with start_date = next occurrence of 'event' as parsed by chronic
on the 15th of each month => 'each month' becomes interval = 30, number found + start month through chronic equals start date
on the 15th of November => month found with number. interval = 365, start_date = Chronic.parse(month + number)
on the second monday in April => month, day and number found. interval = 365, start_date = Chronic.parse(ordinal number form of number, day of week, month)
every November 15th => month found with number. interval = 365, start_date = Chronic.parse(month + number)
every day => word 'day' found without a number. interval = 1. start_day = today
every week => word 'week' found without a number. interval = 7. start_day = today
every month => word 'month' found without a number. interval = 30. start_day = today
24 changes: 22 additions & 2 deletions lib/tickle.rb
Expand Up @@ -8,13 +8,33 @@

$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed

require 'date'
require 'chronic'
require 'numerizer/numerizer'

require 'tickle/tickle'
require 'tickle/handler'
require 'tickle/repeater'

module Tickle
VERSION = "0.0.1"

def self.debug; false; end
def self.debug; true; end

def self.dwrite(msg)
puts msg if Tickle.debug
end
end

class Date
def days_in_month
d,m,y = mday,month,year
d += 1 while Date.valid_civil?(y,m,d)
d - 1
end
end

class Array
def same?(y)
self.sort == y.sort
end
end
98 changes: 98 additions & 0 deletions lib/tickle/handler.rb
@@ -0,0 +1,98 @@
module Tickle
class << self

def guess()
interval = guess_unit_types
interval ||= guess_weekday
interval ||= guess_weekday
interval ||= guess_month_names
interval ||= guess_number_and_unit
interval ||= guess_special

# defines the next occurrence of this tickle if not set in a guess routine
@next ||= @start + (interval * 60 * 60 * 24) if interval
return [@start.to_time, @next.to_time, interval] if interval
end

def guess_unit_types
interval = 1 if token_types.same?([:day])
interval = 7 if token_types.same?([:week])
interval = 30 if token_types.same?([:month])
interval = 365 if token_types.same?([:year])
interval
end

def guess_weekday
if token_types.same?([:weekday]) then
@start = Chronic.parse(token_of_type(:weekday).start.to_s)
interval = 7
end
interval
end

def guess_month_names
if token_types.same?([:month_name]) then
@start = Chronic.parse("#{token_of_type(:month_name).start.to_s} 1")
interval = 30
end
interval
end

def guess_number_and_unit
interval = token_of_type(:number).interval if token_types.same?([:number, :day])
interval = (token_of_type(:number).interval * 7) if token_types.same?([:number, :week])
interval = (token_of_type(:number).interval * 30) if token_types.same?([:number, :month])
interval = (token_of_type(:number).interval * 365) if token_types.same?([:number, :year])
interval
end

def guess_special
interval = guess_special_other
interval ||= guess_special_beginning
interval ||= guess_special_middle
interval ||= guess_special_end
end

private

def guess_special_other
interval = 2 if token_types.same?([:special, :day]) && token_of_type(:special).start == :other
interval = 14 if token_types.same?([:special, :week]) && token_of_type(:special).start == :other
if token_types.same?([:special, :month]) && token_of_type(:special).start == :other then interval = 60; @next = Chronic.parse('2 months from now'); end
if token_types.same?([:special, :year]) && token_of_type(:special).start == :other then interval = 730; @next = Chronic.parse('2 years from now'); end
interval
end

def guess_special_beginning
if token_types.same?([:special, :week]) && token_of_type(:special).start == :beginning then interval = 7; @start = Chronic.parse('Sunday'); end
if token_types.same?([:special, :month]) && token_of_type(:special).start == :beginning then interval = 30; @start = Chronic.parse('1st day next month'); end
if token_types.same?([:special, :year]) && token_of_type(:special).start == :beginning then interval = 365; @start = Chronic.parse('1st day next year'); end
interval
end

def guess_special_end
if token_types.same?([:special, :week]) && token_of_type(:special).start == :end then interval = 7; @start = Chronic.parse('Saturday'); end
if token_types.same?([:special, :month]) && token_of_type(:special).start == :end then interval = 30; @start = Date.new(Date.today.year, Date.today.month, Date.today.days_in_month); end
if token_types.same?([:special, :year]) && token_of_type(:special).start == :end then interval = 365; @start = Date.new(Date.today.year, 12, 31); end
interval
end

def guess_special_middle
if token_types.same?([:special, :week]) && token_of_type(:special).start == :middle then interval = 7; @start = Chronic.parse('Wednesday'); end
if token_types.same?([:special, :month]) && token_of_type(:special).start == :middle then
interval = 30;
@start = (Date.today.day >= 15 ? Chronic.parse('15th day of next month') : Date.new(Date.today.year, Date.today.month, 15))
end
if token_types.same?([:special, :year]) && token_of_type(:special).start == :middle then
interval = 365;
@start = (Date.today.day >= 15 && Date.today.month >= 6 ? Date.new(Date.today.year+1, 6, 15) : Date.new(Date.today.year, 6, 15))
end
interval
end

def token_of_type(type)
@tokens.detect {|token| token.type == type}
end

end
end
85 changes: 85 additions & 0 deletions lib/tickle/repeater.rb
@@ -0,0 +1,85 @@
class Tickle::Repeater < Chronic::Tag #:nodoc:
#
def self.scan(tokens)
# for each token
tokens.each do |token|
token = self.scan_for_numbers(token)
token = self.scan_for_month_names(token)
token = self.scan_for_day_names(token)
token = self.scan_for_special_text(token)
token = self.scan_for_units(token)
end
tokens
end

def self.scan_for_numbers(token)
num = Float(token.word) rescue nil
token.update(:number, nil, num.to_i) if num
token
end

def self.scan_for_month_names(token)
scanner = {/^jan\.?(uary)?$/ => :january,
/^feb\.?(ruary)?$/ => :february,
/^mar\.?(ch)?$/ => :march,
/^apr\.?(il)?$/ => :april,
/^may$/ => :may,
/^jun\.?e?$/ => :june,
/^jul\.?y?$/ => :july,
/^aug\.?(ust)?$/ => :august,
/^sep\.?(t\.?|tember)?$/ => :september,
/^oct\.?(ober)?$/ => :october,
/^nov\.?(ember)?$/ => :november,
/^dec\.?(ember)?$/ => :december}
scanner.keys.each do |scanner_item|
token.update(:month_name, scanner[scanner_item], 30) if scanner_item =~ token.word
end
token
end

def self.scan_for_day_names(token)
scanner = {/^m[ou]n(day)?$/ => :monday,
/^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
/^tue$/ => :tuesday,
/^we(dnes|nds|nns)day$/ => :wednesday,
/^wed$/ => :wednesday,
/^th(urs|ers)day$/ => :thursday,
/^thu$/ => :thursday,
/^fr[iy](day)?$/ => :friday,
/^sat(t?[ue]rday)?$/ => :saturday,
/^su[nm](day)?$/ => :sunday}
scanner.keys.each do |scanner_item|
token.update(:weekday, scanner[scanner_item], 7) if scanner_item =~ token.word
end
token
end

def self.scan_for_special_text(token)
scanner = {/^other$/ => :other,
/^begin(ing|ning)?$/ => :beginning,
/^start$/ => :beginning,
/^end$/ => :end,
/^mid(d)?le$/ => :middle}
scanner.keys.each do |scanner_item|
token.update(:special, scanner[scanner_item], 7) if scanner_item =~ token.word
end
token
end

def self.scan_for_units(token)
scanner = {/^year(ly)?s?$/ => {:type => :year, :interval => 365, :start => :today},
/^month(ly)?s?$/ => {:type => :month, :interval => 30, :start => :today},
/^fortnights?$/ => {:type => :fortnight, :interval => 365, :start => :today},
/^week(ly)?s?$/ => {:type => :week, :interval => 7, :start => :today},
/^weekends?$/ => {:type => :weekend, :interval => 7, :start => :saturday},
/^days?$/ => {:type => :day, :interval => 1, :start => :today},
/^daily?$/ => {:type => :day, :interval => 1, :start => :today}}
scanner.keys.each do |scanner_item|
if scanner_item =~ token.word
token.update(scanner[scanner_item][:type], scanner[scanner_item][:start], scanner[scanner_item][:interval]) if scanner_item =~ token.word
end
end
token
end

end

0 comments on commit 02ccfaf

Please sign in to comment.