Skip to content

Commit

Permalink
seasons and weekday/weekend support (Edwin Chen)
Browse files Browse the repository at this point in the history
  • Loading branch information
mojombo committed Feb 18, 2008
1 parent b9eb114 commit 3d7d840
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 14 deletions.
1 change: 1 addition & 0 deletions lib/chronic.rb
Expand Up @@ -23,6 +23,7 @@
require 'chronic/repeaters/repeater_fortnight' require 'chronic/repeaters/repeater_fortnight'
require 'chronic/repeaters/repeater_week' require 'chronic/repeaters/repeater_week'
require 'chronic/repeaters/repeater_weekend' require 'chronic/repeaters/repeater_weekend'
require 'chronic/repeaters/repeater_weekday'
require 'chronic/repeaters/repeater_day' require 'chronic/repeaters/repeater_day'
require 'chronic/repeaters/repeater_day_name' require 'chronic/repeaters/repeater_day_name'
require 'chronic/repeaters/repeater_day_portion' require 'chronic/repeaters/repeater_day_portion'
Expand Down
14 changes: 14 additions & 0 deletions lib/chronic/repeater.rb
Expand Up @@ -2,6 +2,7 @@ class Chronic::Repeater < Chronic::Tag #:nodoc:
def self.scan(tokens, options) def self.scan(tokens, options)
# for each token # for each token
tokens.each_index do |i| tokens.each_index do |i|
if t = self.scan_for_season_names(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end
Expand All @@ -11,6 +12,18 @@ def self.scan(tokens, options)
tokens tokens
end end


def self.scan_for_season_names(token)
scanner = {/^springs?$/ => :spring,
/^summers?$/ => :summer,
/^(autumn)|(fall)s?$/ => :autumn,
/^winters?$/ => :winter}
scanner.keys.each do |scanner_item|
return Chronic::RepeaterSeasonName.new(scanner[scanner_item]) if scanner_item =~ token.word
end

return nil
end

def self.scan_for_month_names(token) def self.scan_for_month_names(token)
scanner = {/^jan\.?(uary)?$/ => :january, scanner = {/^jan\.?(uary)?$/ => :january,
/^feb\.?(ruary)?$/ => :february, /^feb\.?(ruary)?$/ => :february,
Expand Down Expand Up @@ -74,6 +87,7 @@ def self.scan_for_units(token)
/^fortnights?$/ => :fortnight, /^fortnights?$/ => :fortnight,
/^weeks?$/ => :week, /^weeks?$/ => :week,
/^weekends?$/ => :weekend, /^weekends?$/ => :weekend,
/^(week|business)days?$/ => :weekday,
/^days?$/ => :day, /^days?$/ => :day,
/^hours?$/ => :hour, /^hours?$/ => :hour,
/^minutes?$/ => :minute, /^minutes?$/ => :minute,
Expand Down
126 changes: 124 additions & 2 deletions lib/chronic/repeaters/repeater_season.rb
@@ -1,16 +1,99 @@
class Time
def to_minidate
MiniDate.new(self.month, self.day)
end
end

class Season
attr_reader :start, :end

def initialize(myStart, myEnd)
@start = myStart
@end = myEnd
end

def self.find_next_season(season, pointer)
lookup = {:spring => 0, :summer => 1, :autumn => 2, :winter => 3}
next_season_num = (lookup[season]+1*pointer) % 4
lookup.invert[next_season_num]
end

def self.season_after(season); find_next_season(season, +1); end
def self.season_before(season); find_next_season(season, -1); end
end

class MiniDate
attr_accessor :month, :day

def initialize(month, day)
@month = month
@day = day
end

def is_between?(md_start, md_end)
return true if (@month == md_start.month and @day >= md_start.day) ||
(@month == md_end.month and @day <= md_end.day)

i = md_start.month + 1
until i == md_end.month
return true if @month == i
i = (i+1) % 12
end

return false
end

def equals?(other)
@month == other.month and day == other.day
end
end

class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc: class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc:
YEAR_SEASONS = 4
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
SEASONS = { :spring => Season.new( MiniDate.new(3,20),MiniDate.new(6,20) ),
:summer => Season.new( MiniDate.new(6,21),MiniDate.new(9,22) ),
:autumn => Season.new( MiniDate.new(9,23),MiniDate.new(12,21) ),
:winter => Season.new( MiniDate.new(12,22),MiniDate.new(3,19) ) }


def next(pointer) def next(pointer)
super super


raise 'Not implemented' direction = pointer == :future ? 1 : -1
next_season = Season.find_next_season(find_current_season(@now.to_minidate), direction)

find_next_season_span(direction, next_season)
end end


def this(pointer = :future) def this(pointer = :future)
super super


raise 'Not implemented' direction = pointer == :future ? 1 : -1

today = Time.construct(@now.year, @now.month, @now.day)
this_ssn = find_current_season(@now.to_minidate)
case pointer
when :past
this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
this_ssn_end = today
when :future
this_ssn_start = today + Chronic::RepeaterDay::DAY_SECONDS
this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
when :none
this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
end

Chronic::Span.new(this_ssn_start, this_ssn_end)
end

def offset(span, amount, pointer)
Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
end

def offset_by(time, amount, pointer)
direction = pointer == :future ? 1 : -1
time + amount * direction * SEASON_SECONDS
end end


def width def width
Expand All @@ -20,4 +103,43 @@ def width
def to_s def to_s
super << '-season' super << '-season'
end end

private

def find_next_season_span(direction, next_season)
if !@next_season_start or !@next_season_end
@next_season_start = Time.construct(@now.year, @now.month, @now.day)
@next_season_end = Time.construct(@now.year, @now.month, @now.day)
end

@next_season_start += direction * num_seconds_til_start(next_season, direction)
@next_season_end += direction * num_seconds_til_end(next_season, direction)

Chronic::Span.new(@next_season_start, @next_season_end)
end

def find_current_season(md)
[:spring, :summer, :autumn, :winter].each do |season|
return season if md.is_between?(SEASONS[season].start, SEASONS[season].end)
end
end

def num_seconds_til(goal, direction)
start = Time.construct(@now.year, @now.month, @now.day)
seconds = 0

until (start + direction * seconds).to_minidate.equals?(goal)
seconds += Chronic::RepeaterDay::DAY_SECONDS
end

seconds
end

def num_seconds_til_start(season_symbol, direction)
num_seconds_til(SEASONS[season_symbol].start, direction)
end

def num_seconds_til_end(season_symbol, direction)
num_seconds_til(SEASONS[season_symbol].end, direction)
end
end end
45 changes: 33 additions & 12 deletions lib/chronic/repeaters/repeater_season_name.rb
@@ -1,24 +1,45 @@
require 'chronic/repeaters/repeater_season.rb'

class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc: class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc:
@summer = ['jul 21', 'sep 22'] SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
@autumn = ['sep 23', 'dec 21'] DAY_SECONDS = 86_400 # (24 * 60 * 60)
@winter = ['dec 22', 'mar 19']
@spring = ['mar 20', 'jul 20']


def next(pointer) def next(pointer)
super direction = pointer == :future ? 1 : -1
raise 'Not implemented' find_next_season_span(direction, @type)
end end


def this(pointer = :future) def this(pointer = :future)
super # super
raise 'Not implemented'
direction = pointer == :future ? 1 : -1

today = Time.construct(@now.year, @now.month, @now.day)
goal_ssn_start = today + direction * num_seconds_til_start(@type, direction)
goal_ssn_end = today + direction * num_seconds_til_end(@type, direction)
curr_ssn = find_current_season(@now.to_minidate)
case pointer
when :past
this_ssn_start = goal_ssn_start
this_ssn_end = (curr_ssn == @type) ? today : goal_ssn_end
when :future
this_ssn_start = (curr_ssn == @type) ? today + Chronic::RepeaterDay::DAY_SECONDS : goal_ssn_start
this_ssn_end = goal_ssn_end
when :none
this_ssn_start = goal_ssn_start
this_ssn_end = goal_ssn_end
end

Chronic::Span.new(this_ssn_start, this_ssn_end)
end end


def width def offset(span, amount, pointer)
(91 * 24 * 60 * 60) Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
end end


def to_s def offset_by(time, amount, pointer)
super << '-season-' << @type.to_s direction = pointer == :future ? 1 : -1
time + amount * direction * Chronic::RepeaterYear::YEAR_SECONDS
end end

end end
72 changes: 72 additions & 0 deletions lib/chronic/repeaters/repeater_weekday.rb
@@ -0,0 +1,72 @@
class Chronic::RepeaterWeekday < Chronic::Repeater #:nodoc:
WEEK_WEEKDAYS = 5
DAY_SECONDS = 86400 # (24 * 60 * 60)

def next(pointer)
super

direction = pointer == :future ? 1 : -1

if !@current_weekday_start
@current_weekday_start = Time.construct(@now.year, @now.month, @now.day)
@current_weekday_start += direction * DAY_SECONDS

until is_weekday?(@current_weekday_start.wday)
@current_weekday_start += direction * DAY_SECONDS
end
else
loop do
@current_weekday_start += direction * DAY_SECONDS
break if is_weekday?(@current_weekday_start.wday)
end
end

Chronic::Span.new(@current_weekday_start, @current_weekday_start + DAY_SECONDS)
end

def this(pointer = :future)
super

case pointer
when :past
self.next(:past)
when :future, :none
self.next(:future)
end
end

def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1

num_weekdays_passed = 0; offset = 0
until num_weekdays_passed == amount
offset += direction * DAY_SECONDS
num_weekdays_passed += 1 if is_weekday?((span.begin+offset).wday)
end

span + offset
end

def width
DAY_SECONDS
end

def to_s
super << '-weekday'
end

private

def is_weekend?(day)
day == symbol_to_number(:saturday) || day == symbol_to_number(:sunday)
end

def is_weekday?(day)
!is_weekend?(day)
end

def symbol_to_number(sym)
lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
lookup[sym] || raise("Invalid symbol specified")
end
end
1 change: 1 addition & 0 deletions lib/chronic/repeaters/repeater_year.rb
@@ -1,4 +1,5 @@
class Chronic::RepeaterYear < Chronic::Repeater #:nodoc: class Chronic::RepeaterYear < Chronic::Repeater #:nodoc:
YEAR_SECONDS = 31536000 # 365 * 24 * 60 * 60


def next(pointer) def next(pointer)
super super
Expand Down
56 changes: 56 additions & 0 deletions test/test_RepeaterWeekday.rb
@@ -0,0 +1,56 @@
require 'lib/chronic'
require 'test/unit'

class TestRepeaterWeekday < Test::Unit::TestCase

def setup
@now = Time.local(2007, 6, 11, 14, 0, 0, 0) # Mon
end

def test_next_future
weekdays = Chronic::RepeaterWeekday.new(:weekday)
weekdays.start = @now

next1_weekday = weekdays.next(:future) # Tues
assert_equal Time.local(2007, 6, 12), next1_weekday.begin
assert_equal Time.local(2007, 6, 13), next1_weekday.end

next2_weekday = weekdays.next(:future) # Wed
assert_equal Time.local(2007, 6, 13), next2_weekday.begin
assert_equal Time.local(2007, 6, 14), next2_weekday.end

next3_weekday = weekdays.next(:future) # Thurs
assert_equal Time.local(2007, 6, 14), next3_weekday.begin
assert_equal Time.local(2007, 6, 15), next3_weekday.end

next4_weekday = weekdays.next(:future) # Fri
assert_equal Time.local(2007, 6, 15), next4_weekday.begin
assert_equal Time.local(2007, 6, 16), next4_weekday.end

next5_weekday = weekdays.next(:future) # Mon
assert_equal Time.local(2007, 6, 18), next5_weekday.begin
assert_equal Time.local(2007, 6, 19), next5_weekday.end
end

def test_next_past
weekdays = Chronic::RepeaterWeekday.new(:weekday)
weekdays.start = @now

last1_weekday = weekdays.next(:past) # Fri
assert_equal Time.local(2007, 6, 8), last1_weekday.begin
assert_equal Time.local(2007, 6, 9), last1_weekday.end

last2_weekday = weekdays.next(:past) # Thurs
assert_equal Time.local(2007, 6, 7), last2_weekday.begin
assert_equal Time.local(2007, 6, 8), last2_weekday.end
end

def test_offset
span = Chronic::Span.new(@now, @now + 1)

offset_span = Chronic::RepeaterWeekday.new(:weekday).offset(span, 5, :future)

assert_equal Time.local(2007, 6, 18, 14), offset_span.begin
assert_equal Time.local(2007, 6, 18, 14, 0, 1), offset_span.end
end
end

0 comments on commit 3d7d840

Please sign in to comment.