Skip to content

Instantly share code, notes, and snippets.

@timsu
Created February 4, 2022 22:42
Show Gist options
  • Save timsu/8a610a89af37dc80fb779963cb18eb44 to your computer and use it in GitHub Desktop.
Save timsu/8a610a89af37dc80fb779963cb18eb44 to your computer and use it in GitHub Desktop.
Astrid recurrence calculator
# 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