Permalink
Browse files

seasons and weekday/weekend support (Edwin Chen)

  • Loading branch information...
1 parent b9eb114 commit 3d7d840f8e6dac043c0733d29f727a4d7835e9b5 @mojombo mojombo committed Feb 18, 2008
View
@@ -23,6 +23,7 @@
require 'chronic/repeaters/repeater_fortnight'
require 'chronic/repeaters/repeater_week'
require 'chronic/repeaters/repeater_weekend'
+require 'chronic/repeaters/repeater_weekday'
require 'chronic/repeaters/repeater_day'
require 'chronic/repeaters/repeater_day_name'
require 'chronic/repeaters/repeater_day_portion'
View
@@ -2,6 +2,7 @@ class Chronic::Repeater < Chronic::Tag #:nodoc:
def self.scan(tokens, options)
# for each token
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_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
@@ -11,6 +12,18 @@ def self.scan(tokens, options)
tokens
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)
scanner = {/^jan\.?(uary)?$/ => :january,
/^feb\.?(ruary)?$/ => :february,
@@ -74,6 +87,7 @@ def self.scan_for_units(token)
/^fortnights?$/ => :fortnight,
/^weeks?$/ => :week,
/^weekends?$/ => :weekend,
+ /^(week|business)days?$/ => :weekday,
/^days?$/ => :day,
/^hours?$/ => :hour,
/^minutes?$/ => :minute,
@@ -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:
+ YEAR_SEASONS = 4
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)
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
def this(pointer = :future)
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
def width
@@ -20,4 +103,43 @@ def width
def to_s
super << '-season'
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
@@ -1,24 +1,45 @@
+require 'chronic/repeaters/repeater_season.rb'
+
class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc:
- @summer = ['jul 21', 'sep 22']
- @autumn = ['sep 23', 'dec 21']
- @winter = ['dec 22', 'mar 19']
- @spring = ['mar 20', 'jul 20']
+ SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
+ DAY_SECONDS = 86_400 # (24 * 60 * 60)
def next(pointer)
- super
- raise 'Not implemented'
+ direction = pointer == :future ? 1 : -1
+ find_next_season_span(direction, @type)
end
def this(pointer = :future)
- super
- raise 'Not implemented'
+ # super
+
+ 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
- def width
- (91 * 24 * 60 * 60)
+ def offset(span, amount, pointer)
+ Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
end
- def to_s
- super << '-season-' << @type.to_s
+ def offset_by(time, amount, pointer)
+ direction = pointer == :future ? 1 : -1
+ time + amount * direction * Chronic::RepeaterYear::YEAR_SECONDS
end
+
end
@@ -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,4 +1,5 @@
class Chronic::RepeaterYear < Chronic::Repeater #:nodoc:
+ YEAR_SECONDS = 31536000 # 365 * 24 * 60 * 60
def next(pointer)
super
@@ -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
Oops, something went wrong.

0 comments on commit 3d7d840

Please sign in to comment.