module JosieHealth module Utils module Recurrence DAYS_OF_WEEK = { "monday" => 1, "tuesday" => 2, "wednesday" => 3, "thursday" => 4, "friday" => 5, "saturday" => 6, "sunday" => 7, } def self.parse_interval(pattern : String) : Int32? total_seconds = 0.0 pattern.downcase.scan(/(\d+(?:\.\d+)?)([wdhms])/) do |match| amount = match[1].to_f unit = match[2] multiplier = case unit when "w" then 604800.0 when "d" then 86400.0 when "h" then 3600.0 when "m" then 60.0 when "s" then 1.0 else 0.0 end total_seconds += amount * multiplier end return nil if total_seconds == 0 total_seconds.to_i end def self.parse_day_of_week(pattern : String) : Int32? DAYS_OF_WEEK[pattern.downcase]? end def self.valid?(pattern : String) : Bool return false if pattern.empty? !parse_interval(pattern).nil? || !parse_day_of_week(pattern).nil? end def self.next_trigger(pattern : String, from_time : Time = Time.utc) : Int64 if interval = parse_interval(pattern) (from_time + interval.seconds).to_unix elsif target_day = parse_day_of_week(pattern) next_day_of_week(target_day, from_time).to_unix else from_time.to_unix end end def self.first_trigger(pattern : String, from_time : Time = Time.utc) : Int64 next_trigger(pattern, from_time) end def self.next_day_of_week(target_day : Int32, from_time : Time) : Time current_day = from_time.day_of_week.value current_day = 7 if current_day == 0 days_until = target_day - current_day days_until += 7 if days_until <= 0 from_time + days_until.days end def self.format(pattern : String) : String if interval = parse_interval(pattern) format_compact(interval) elsif parse_day_of_week(pattern) pattern.downcase.capitalize else pattern end end def self.format_compact(total_seconds : Int32) : String weeks = total_seconds // 604800 remaining = total_seconds % 604800 days = remaining // 86400 remaining = remaining % 86400 hours = remaining // 3600 remaining = remaining % 3600 minutes = remaining // 60 parts = [] of String parts << "#{weeks}w" if weeks > 0 parts << "#{days}d" if days > 0 parts << "#{hours}h" if hours > 0 parts << "#{minutes}m" if minutes > 0 parts.empty? ? "#{total_seconds}s" : parts.join end def self.format_human(total_seconds : Int32) : String weeks = total_seconds // 604800 days = (total_seconds % 604800) // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 parts = [] of String parts << "#{weeks} week#{weeks == 1 ? "" : "s"}" if weeks > 0 parts << "#{days} day#{days == 1 ? "" : "s"}" if days > 0 parts << "#{hours} hour#{hours == 1 ? "" : "s"}" if hours > 0 parts << "#{minutes} minute#{minutes == 1 ? "" : "s"}" if minutes > 0 parts << "#{seconds} second#{seconds == 1 ? "" : "s"}" if seconds > 0 && parts.empty? parts.join(", ") end end end end