schedule
start_datetime
,结束时间 end_datetime
repeat
标记),重复的日程的 start_datetime
和 end_datetime
必须在同一天finish_date
(只记录到哪天,不关心具体到几点)finish_date
为 nil
表示它一直重复下去,不终止repeat
: 0 不重复none_day
,1 每天every_day
,2 每个工作日every_wday
,3 每周every_week
,4 每月every_month
我用 conflict? 作为方法名,英语能力差,有好的方法名希望能提出来
分两步解决
none_day
not (s1.start_datetime > s2.end_datetime or s1.end_datetime < s2.start_datetime)
every_???
start_datetime
的日期相差 20 年,那也只有 20 x 12 = 120 个的判断,在考虑到项目的操作上,穷举是可取的require 'active_support/all'
# repeat:
# 0 -> 不重复
# 1 -> 每天
# 2 -> 每个工作日
# 3 -> 每周
# 4 -> 每月
# [OPTIMIZE]
module ScheduleCheck
SCHEDULEREPEAT = [:none_day, :every_day, :every_wday, :every_week, :every_month]
SCHEDULEREPEATMAP = { none_day: 0, every_day: 1, every_wday: 2, every_week: 3, every_month: 4 }
class << self
def find_conflicted ar_schedule, ar_maybe_conf_schedules
schedule = Schedule.new(ar_schedule)
ar_maybe_conf_schedules
.map { |ar_maybe_conf_schedule| Schedule.new(ar_maybe_conf_schedule) }
.select { |maybe_conf_schedule| conflict? schedule, maybe_conf_schedule }
.map(&:model)
end
def conflict? s1, s2
s1, s2 = sort(s1, s2)
s1.conflict?(s2)
end
def sort s1, s2
s1.repeat_value <= s2.repeat_value ? [s1, s2] : [s2, s1]
end
def max date1, date2
date1 >= date2 ? date1 : date2
end
def min date1, date2
return date1 if date2.blank?
return date2 if date1.blank?
date1 <= date2 ? date1 : date2
end
def time_compare_lt_and_eq date1, date2
date1 <= date2.change(year: date1.year, month: date1.month, day: date1.day)
end
def time_compare_gt_and_eq date1, date2
date1 >= date2.change(year: date1.year, month: date1.month, day: date1.day)
end
end
class Repeat
attr_reader :type, :start_datetime, :end_datetime, :finish_date
def initialize params
@type = params[:repeat_type]
@start_datetime = params[:start_datetime]
@end_datetime = params[:end_datetime]
@finish_date = params[:finish_date]
validate
tidy
end
def tidy; end
def next_repeat
return if finish_date <= start_datetime.to_date
Schedule.new(*to_repeat, SCHEDULEREPEATMAP[type], finish_date)
end
def repeat_value
SCHEDULEREPEATMAP[type]
end
def finish_datetime
return end_datetime if finish_date <= end_datetime.to_date
end_datetime.change(year: finish_date.year, month: finish_date.month, day: finish_date.day)
end
def to_repeat
raise "you need to rewrite this method!"
end
def weekend?
return false if none_day?
start_datetime.day.in?(5..6)
end
%w( none_day every_day every_wday every_week every_month ).each do |repeat_type|
define_method "#{repeat_type}?" do
type.to_s == repeat_type
end
end
# 冲突
def conflict? other
return false if impossibility_conflict other
oh_my_god? other
end
def infinite_time?
finish_date == 999.years.since(Date.new(2015))
end
protected
def validate
raise EndDateError if end_datetime < start_datetime
raise EndDateError if !none_day? && end_datetime.to_date != start_datetime.to_date
end
def impossibility_conflict other
return false if none_day?
time_start_after_other_end(other) ||
time_end_before_other_start(other) ||
finish_before_other_start(other) ||
start_after_other_finish(other)
end
def time_start_after_other_end other
ScheduleCheck.time_compare_gt_and_eq(start_datetime, other.end_datetime)
end
def time_end_before_other_start other
ScheduleCheck.time_compare_lt_and_eq(end_datetime, other.start_datetime)
end
def finish_before_other_start other
finish_date < other.start_datetime.to_date
end
def start_after_other_finish other
other.finish_date < start_datetime.to_date
end
def end_before_other_start other
end_datetime < other.start_datetime
end
def start_after_other_end other
start_datetime > other.end_datetime
end
end
class RepeatNoneDay < Repeat
def to_repeat
raise RepeatError
end
def oh_my_god? other
if other.none_day?
compare_with_other_none_day(other)
elsif end_before_other_start(other) || start_after_other_finish(other)
false
else
compare_with_repeat_day(other)
end
end
protected
def compare_with_other_none_day other
!(start_after_other_end(other) || end_before_other_start(other))
end
def compare_with_repeat_day other
s = Schedule.new(
ScheduleCheck.max(@start_datetime, other.start_datetime).change(hour: other.start_datetime.hour, min: other.start_datetime.min),
ScheduleCheck.max(@start_datetime, other.start_datetime).change(hour: other.end_datetime.hour, min: other.end_datetime.min),
other.repeat_value,
ScheduleCheck.min(@end_datetime.to_date, other.finish_date)
)
rescure(s, s.finish_date)
end
def rescure other, f_date
return false if other.blank? || other.end_datetime.to_date > f_date
return true unless other.start_datetime > end_datetime || other.end_datetime < start_datetime
return false if other.every_day?
rescure(other.next_repeat, f_date)
end
end
class RepeatEveryDay < Repeat
def to_repeat
[1.day.since(start_datetime), 1.day.since(end_datetime)]
end
def oh_my_god? other
return true
end
end
class RepeatEveryWday < Repeat
def tidy
return if infinite_time?
if finish_date.days_to_week_start.in?(5..6)
@finish_date = (finish_date.days_to_week_start - 4).days.ago(finish_date)
end
if start_datetime.days_to_week_start.in?(5..6)
@start_datetime = (7 - start_datetime.days_to_week_start).days.since(start_datetime)
@end_datetime = (7 - end_datetime.days_to_week_start).days.since(end_datetime)
end
end
def to_repeat
if start_datetime.days_to_week_start == 4
[3.day.since(start_datetime), 3.day.since(end_datetime)]
else
[ 1.day.since(start_datetime), 1.day.since(end_datetime)]
end
end
def one_period_days
rs = [self.start_datetime]
(1..5).inject(next_repeat) do |next_r, _|
break if next_r.blank?
rs << next_r.start_datetime
next_r.next_repeat
end
rs
end
def oh_my_god? other
return true if other.every_wday?
return false if other.weekend?
if other.every_week?
one_period_days
.map(&:days_to_week_start)
.include?(other.start_datetime.days_to_week_start)
elsif other.every_month?
return true if infinite_time? && other.infinite_time?
max_start_datetime = ScheduleCheck.max(start_datetime, other.start_datetime)
!(other
.days_until(finish_date)
.delete_if { |s| s < max_start_datetime }
.map(&:days_to_week_start) & (0..4).to_a).blank?
end
end
end
class RepeatEveryWeek < Repeat
def tidy
return if infinite_time?
if finish_date.days_to_week_start != start_datetime.days_to_week_start
days = finish_date.days_to_week_start - start_datetime.days_to_week_start
new_finish_date = days > 0 ? days.days.ago(finish_date) : (7 + days).days.ago(finish_date)
@finish_date = new_finish_date > start_datetime.to_date ? new_finish_date : start_datetime.to_date
end
end
def to_repeat
[1.week.since(start_datetime), 1.week.since(end_datetime)]
end
def oh_my_god? other
if other.every_week?
start_datetime.days_to_week_start == other.start_datetime.days_to_week_start
elsif other.every_month?
return true if infinite_time? && other.infinite_time?
other
.days_until(finish_date)
.delete_if { |d| d < ScheduleCheck.max(start_datetime, other.start_datetime) }
.inject(false) { |conf, date| conf || include?(date) }
end
end
protected
def include? date
days_dis = (date.at_beginning_of_day.to_i / 86400) - (start_datetime.at_beginning_of_day.to_i / 86400)
return days_dis % 7 == 0
end
end
class RepeatEveryMonth < Repeat
def tidy
return if infinite_time?
if start_datetime.day != finish_date.day
if start_datetime.day > finish_date.day
# 下个月的最大天数是否包含重复的那天(eg:重复的是31号,4月是没有31号的)
# 两个月内,一定会有重复的那天
if 1.month.ago(finish_date).at_end_of_month.day < start_datetime.day
new_finish_date = 2.months.ago(finish_date).change(day: start_datetime.day)
else
new_finish_date = 1.months.ago(finish_date).change(day: start_datetime.day)
end
@finish_date = new_finish_date > start_datetime.to_date ? new_finish_date : start_datetime.to_date
else
@finish_date = finish_date.change(day: start_datetime.day)
end
end
end
def to_repeat
if 1.month.since(start_datetime).day == start_datetime.day
[1.month.since(start_datetime), 1.month.since(end_datetime)]
else
[2.month.since(start_datetime), 2.month.since(end_datetime)]
end
end
def oh_my_god? other
if other.every_month?
start_datetime.day == other.start_datetime.day
end
end
def days_until e_date, count = nil
e_date = ScheduleCheck.min(e_date, finish_date)
s = self
days = []
while(e_date >= s.start_datetime.to_date)
days << s.start_datetime
s = s.next_repeat
break if s.blank?
end
days
end
end
class Schedule
attr_reader :repeat, :model
def initialize *args
options = args.extract_options!
params = if args.first.is_a?(ActiveRecord::Base) && args.first.class.name == "Schedule"
# activerecord
@model = args.first
{
start_datetime: @model.plan_start_time,
end_datetime: @model.plan_finish_time,
repeat_type: @model.period,
finish_date: @model.feal_deadline
}
elsif options.present?
# hash
raise "need start_datetime and end_datetime" if (options.keys.map(&:to_sym) & [:start_datetime, :end_datetime]).blank?
options
.tap { |o| o[:finish_date] ||= infinite_long_date }
.reverse_merge({ repeat_type: :none_day})
else
# array
raise "wrong number of arguments #{args.length} to 4" if args.length != 4
{
start_datetime: args[0],
end_datetime: args[1],
repeat_type: args[2],
finish_date: args[3] || infinite_long_date
}
end
@repeat_type = params[:repeat_type] = params[:repeat_type].is_a?(Fixnum) ? SCHEDULEREPEAT[params[:repeat_type]] : params[:repeat_type]
@repeat = ("ScheduleCheck::Repeat#{@repeat_type.to_s.camelize}").constantize.new params
end
def method_missing(name, *args, &block)
if repeat.respond_to?(name)
repeat.public_send(name, *args, &block)
else
super
end
end
protected
def infinite_long_date
999.years.since Date.new(2015)
end
end
class EndDateError < StandardError; end
class RepeatError < StandardError; end
class RepeatTypeError < StandardError; end
end