Created
February 4, 2022 22:42
-
-
Save timsu/8a610a89af37dc80fb779963cb18eb44 to your computer and use it in GitHub Desktop.
Astrid recurrence calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# task plugin helper | |
module RepeatsPlugin | |
FREQUENCY = { | |
"MINUTELY" => "minute", | |
"HOURLY" => "hour", | |
"DAILY" => "day", | |
"WEEKLY" => "week", | |
"MONTHLY" => "month", | |
"YEARLY" => "year" | |
} | |
BYDAY = { | |
"SU" => "Sun", | |
"MO" => "Mon", | |
"TU" => "Tue", | |
"WE" => "Wed", | |
"TH" => "Thu", | |
"FR" => "Fri", | |
"SA" => "Sat" | |
} | |
EDIT_SETTING = { | |
"NONE" => "None", | |
"DAILY" => "Daily", | |
"WEEKDAYS" => "Every Weekday (Mon-Fri)", | |
"WEEKLY" => "Weekly", | |
"MONTHLY" => "Monthly", | |
"YEARLY" => "Yearly", | |
"HOURLY" => "Hourly" | |
} | |
def update_value(task, params) | |
if params[:repeat] | |
rrule = { | |
:freq => params[:repeat][:freq], | |
:interval => params[:repeat][:interval].to_i, | |
:byday => params[:repeat][:byday], | |
:from => params[:repeat][:from] | |
} | |
case rrule[:freq] | |
when "NONE" | |
rrule = {} | |
when "WEEKDAYS" | |
rrule[:freq] = "WEEKLY" | |
rrule[:interval] = 1 | |
rrule[:byday] = ["MO", "WE", "FR"] | |
when "WEEKLY" | |
else | |
rrule[:byday] = nil | |
end | |
@error = "Invalid repeat interval" if !rrule.empty? && rrule[:interval] <= 0 | |
@error = "Invalid repeat frequency" if !rrule.empty? && !FREQUENCY[rrule[:freq]] | |
task.repeat = from_rrule(rrule) | |
end | |
end | |
def before_save() | |
if self.completed_at_changed? and self.repeating? | |
@broadcast_repeat_completed = true | |
rrule = RepeatsPlugin.to_rrule(self.repeat) | |
next_date = next_due_date(rrule, self) | |
if next_date | |
unless self.due_changed? | |
if rrule[:freq] == "HOURLY" or rrule[:freq] == "MINUTELY" | |
self.has_due_time = true | |
elsif self.due | |
prev_due = self.due | |
next_date = Time.zone.local(next_date.year, next_date.month, next_date.day, | |
prev_due.hour, prev_due.min, prev_due.sec) if prev_due.respond_to? :hour | |
end | |
self.due = next_date | |
end | |
self.last_reminder = nil | |
self.completed_at = nil | |
@repeat_occurred = true | |
if self.persisted? | |
self.task_repeat_records.create(:user => @completed_by, | |
:at => Time.current) | |
end | |
end | |
end | |
end | |
### helpers ### | |
def next_due_date(rrule, task) | |
from_completion = rrule[:from] == "COMPLETION" | |
if task.due.nil? or from_completion | |
original = date = Time.current | |
elsif task.due.class == Date | |
original = date = task.due.to_time | |
else | |
original = date = task.due | |
end | |
interval = rrule[:interval] | |
if rrule[:freq] == "MONTHLY" and (date + 1.day).month != date.month | |
new_date = date + (interval + 1).month | |
return Date.new(new_date.year, new_date.month) - 1.day | |
end | |
shift_by_day = lambda do | |
return if rrule[:byday].empty? | |
wdays = rrule[:byday].map do |day| | |
BYDAY.find_index { |k, v| k == day } | |
end.uniq.compact | |
return if wdays.empty? | |
date += 1.day if wdays.length > 1 | |
(0..6).each do | |
break if wdays.include? date.wday | |
date += 1.day | |
end | |
end | |
shift_by_freq = lambda do | |
freq = { | |
"MINUTELY" => :minute, | |
"HOURLY" => :hour, | |
"DAILY" => :day, | |
"WEEKLY" => :week, | |
"MONTHLY" => :month, | |
"YEARLY" => :year | |
}[rrule[:freq]] or raise "Unknown frequency: #{task.repeat}" | |
date = date + interval.send(freq) | |
end | |
shift_by_day.call | |
interval -= 1 if original != date | |
shift_by_freq.call unless original.wday < date.wday | |
date | |
end | |
def from_rrule(rrule) | |
return nil if rrule.empty? | |
repeat = "RRULE:" | |
if rrule["freq"] == "WEEKDAYS" | |
rrule["freq"] = "WEEKLY" | |
rrule["byday"] = "MO,TU,WE,TH,FR" | |
end | |
rrule.sort { |a, b| a[0] <=> b[0] }.each do |item| | |
key, value = item | |
data = key.to_s.upcase + "=" | |
case value | |
when Fixnum | |
data << value.to_s | |
when String | |
data << value.upcase | |
when Array | |
next if value.empty? | |
data << value.uniq.join(",").upcase | |
end | |
repeat << data << ";" | |
end | |
repeat[0..-2] | |
end | |
def self.to_rrule(repeat) | |
return {} if repeat.blank? | |
rrule = { | |
:freq => repeat.match(/FREQ=([^;]*)/).try(:[], 1), | |
:interval => repeat.match(/INTERVAL=([^;]*)/).try(:[], 1).try(:to_i) || 1, | |
:byday => repeat.match(/BYDAY=([^;]*)/).try(:[], 1).try(:split, ","), | |
:count => repeat.match(/COUNT=([^;]*)/).try(:[], 1), | |
:until => repeat.match(/UNTIL=([^;]*)/).try(:[], 1), | |
:from => repeat.match(/FROM=([^;]*)/).try(:[], 1), | |
}.delete_if { |k, v| v.nil? } | |
raise "Invalid rrule: #{repeat}" unless rrule[:freq] and rrule[:interval] | |
rrule | |
end | |
end | |
TaskPlugins::PLUGINS << RepeatsPlugin unless TaskPlugins::PLUGINS.include? RepeatsPlugin | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment