-- Schedule: An object that contains a schedule of one or more times of week and/or times in an n-day rotation.
Schedule = class(nil);
function Schedule:Constructor(scheduleStr)
self.dailyTimes = {};
self.weekTimes = {};
self.rotationTimes = {};
if (scheduleStr) then
self:AddStr(scheduleStr);
end
end
-- A schedule string is a semicolon-separated list of day:time pairs, where
-- 'day' can be
-- 1...7 => Sunday...Saturday
-- * => every day
-- x/y => the xth day in a y-day rotation
-- 'time' can be a fractional number
-- Examples:
-- "2:3;*:6;7:21.5" => Monday at 3:00 AM and any day at 6:00AM and Saturday at 9:30 PM
-- "2/8:3" => The 2nd day in an 8 day rotation (starting from a particular day in 2007) at 3:00 AM server time
-- Adds entries based on the provided schedule string. The times and days are server times/days.
function Schedule:AddStr(str)
for dayStr, hourStr in str:gmatch("([0-9*/]+):([0-9.]+)") do
hour = tonumber(hourStr);
if (dayStr == "*") then
self:AddTimeOfDay(hour);
elseif (dayStr:find("/")) then
local day, days = dayStr:match("^(.+)/(.+)$");
self:AddTimeOfRotation(tonumber(days), tonumber(day), hour)
else
local dayOfWeek = tonumber(dayStr);
self:AddTimeOfWeek(dayOfWeek, hour);
end
end
end
-- Adds an entry to the schedule on every day of the week at the specified time.
-- 'hour' is a decimal number indicating the time of day (e.g. 3.5 => 03:30 am)
function Schedule:AddTimeOfDay(hour)
local seconds = hour * Time.lengthOfHour;
if (self.dailyTimes[seconds]) then
return;
end
-- Add entry to self.dailyTimes
self.dailyTimes[seconds] = true;
-- Eliminate redundancy in self.weekTimes
for weekTime, _ in pairs(self.weekTimes) do
if (weekTime % Time.lengthOfDay == seconds) then
self.weekTimes[weekTime] = nil;
end
end
-- Eliminate redundancies in self.rotationTimes[]
for numDays, rotationTimes in pairs(self.rotationTimes) do
for rotationTime, _ in pairs(rotationTimes) do
if (rotationTime % Time.lengthOfDay == seconds) then
rotationTimes[rotationTime] = nil;
end
end
end
end
-- Adds an entry to the schedule on the specified day(s) of the week at the specified time.
-- 'dayOfWeek' is an integer 1...7 indicating Sunday...Saturday
-- 'hour' is a decimal number indicating the time of day (e.g. 3.5 => 03:30 am)
function Schedule:AddTimeOfWeek(dayOfWeek, hour)
local seconds = hour * Time.lengthOfHour;
if (self.dailyTimes[seconds]) then
return;
end
-- Add entry to self.weekTimes
local weekTime = (dayOfWeek - 1) * Time.lengthOfDay + seconds;
self.weekTimes[weekTime] = true;
-- Check if this hour is now daily
for dayOfWeek = 1, 7 do
local weekTime = (dayOfWeek - 1) * Time.lengthOfDay + seconds;
if (not self.weekTimes[weekTime]) then
-- There is at least one day in the week on which this hour is not present.
return;
end
end
-- This hour is now daily. Eliminate redundancies.
self:AddTimeOfDay(hour);
end
-- Adds an entry to the schedule on the specified day of an n-day rotation.
-- 'days' is n, the number of days in the rotation
-- 'day' is the day number 1...n in the rotation
-- 'hour' is a decimal number indicating the time of day (e.g. 3.5 => 03:30 am)
function Schedule:AddTimeOfRotation(days, day, hour)
local seconds = hour * Time.lengthOfHour;
if (self.dailyTimes[seconds]) then
return;
end
-- Add entry to self.rotationTimes
local rotationTime = (day - 1) * Time.lengthOfDay + seconds;
self.rotationTimes[days] = self.rotationTimes[days] or {};
self.rotationTimes[days][rotationTime] = true;
-- Check if this hour is now daily
for day = 1, days do
local rotationTime = (day - 1) * Time.lengthOfDay + seconds;
if (not self.rotationTimes[days][rotationTime]) then
-- There is at least one day in the rotation on which this hour is not present.
return;
end
end
-- This hour is now daily. Eliminate redundancies.
self:AddTimeOfDay(hour);
end
-- Generates schedule string (or nil). The times and days are server times/days.
function Schedule:GetStr()
local substrings = {};
for hour in self:daily_entries() do
table.insert(substrings, "*:" .. tostring(hour));
end
for dayOfWeek, hour in self:weekly_entries() do
table.insert(substrings, tostring(dayOfWeek) .. ":" .. tostring(hour));
end
for numDays, day, hour in self:rotation_entries() do
table.insert(substrings, tostring(day) .. "/" .. tostring(numDays) .. ":" .. tostring(hour));
end
if (next(substrings)) then
return table.concat(substrings, ";");
end
end
-- Returns game-time of the next entry in the schedule.
-- The result will be a Time >= 'prevTime' (optional, default is the current Time).
-- If 'excludePrev' is true, this function will return a Time strictly greater than prevTime.
-- If 'excludePrev' is false, this function will return a Time >= prevTime.
function Schedule:GetNextTime(prevTime, excludePrev)
prevTime = prevTime or Time();
if (excludePrev) then
prevTime = prevTime:JustAfter();
end
local nextTime = Time.huge;
-- Find next time among the time-of-day entries (if any)
if (next(self.dailyTimes)) then
local startOfDay = prevTime:GetPrevServerMidnight();
local prevOffset = prevTime:GetSecondsSince(startOfDay);
local soonestOffset = math.huge;
for offset, _ in pairs(self.dailyTimes) do
if (offset < prevOffset) then
offset = offset + Time.lengthOfDay;
end
soonestOffset = math.min(soonestOffset, offset);
end
local nextDailyTime = startOfDay:AddSeconds(soonestOffset);
nextTime = Time.min(nextTime, nextDailyTime);
end
-- Find next time among the time-of-week entries (if any)
if (next(self.weekTimes)) then
local startOfWeek = prevTime:GetStartOfWeek();
local prevOffset = prevTime:GetSecondsSince(startOfWeek);
local soonestOffset = math.huge;
for offset, _ in pairs(self.weekTimes) do
if (offset < prevOffset) then
offset = offset + Time.lengthOfWeek;
end
soonestOffset = math.min(soonestOffset, offset);
end
local nextWeekTime = startOfWeek:AddSeconds(soonestOffset);
nextTime = Time.min(nextTime, nextWeekTime);
end
-- Find next time among the time-of-rotation entries (if any)
if (next(self.rotationTimes)) then
for numDays, times in pairs(self.rotationTimes) do
local startOfRotation = prevTime:GetStartOfRotation(numDays, 0.0);
local prevOffset = prevTime:GetSecondsSince(startOfRotation);
local lengthOfRotation = numDays * Time.lengthOfDay;
local soonestOffset = math.huge;
for offset, _ in pairs(times) do
if (offset < prevOffset) then
offset = offset + lengthOfRotation;
end
soonestOffset = math.min(soonestOffset, offset);
end
local nextRotationTime = startOfRotation:AddSeconds(soonestOffset);
nextTime = Time.min(nextTime, nextRotationTime);
end
end
return nextTime;
end
-- Returns 'true' if the specified Time occurs within 24 hours after a rotation time in the schedule.
function Schedule:AvailableInRotation(currentTime)
for numDays, day, hour in self:rotation_entries() do
local currentDay = currentTime:GetDayInRotation(numDays, hour);
if (day == currentDay) then
return true;
end
end
end
-- For iterating over the time-of-day entries ("*:y") in a schedule. The entries are sorted.
-- Example (where 's' is a Schedule):
-- for hour in s:daily_entries() do
-- ...
-- end
function Schedule:daily_entries()
local entries = {};
for offset, _ in pairs(self.dailyTimes) do
local hour = offset / Time.lengthOfHour;
entries[hour] = true;
end
return sorted_keys(entries);
end
-- For iterating over the unpacked values of 'tableVar', where 'tableVar' is a table
-- with numeric indices.
local function unpacked_values(tableVar)
local n = 1;
return function()
local value = tableVar[n];
n = n + 1;
if (value) then
return unpack(value);
end
end
end
-- For iterating over the time-of-week entries ("x:y") in a schedule. The entries are sorted.
-- Example (where 's' is a Schedule):
-- for dayOfWeek, hour in s:weekly_entries() do
-- ...
-- end
-- 'dayOfWeek' is a number 1..7 indicating Sunday..Saturday.
function Schedule:weekly_entries()
local entries = {};
for offset in sorted_keys(self.weekTimes) do
local dayOfWeek = 1 + math.floor(offset / Time.lengthOfDay);
local seconds = offset % Time.lengthOfDay;
local hour = seconds / Time.lengthOfHour;
table.insert(entries, { dayOfWeek, hour });
end
return unpacked_values(entries);
end
-- For iterating over the rotation entries ("x/y:z") in a schedule. The entries are sorted.
-- Example (where 's' is a Schedule):
-- for numDays, day, hour in s:rotation_entries() do
-- ...
-- end
function Schedule:rotation_entries()
local entries = {};
for numDays in sorted_keys(self.rotationTimes) do
for offset in sorted_keys(self.rotationTimes[numDays]) do
local day = 1 + math.floor(offset / Time.lengthOfDay);
local seconds = offset % Time.lengthOfDay;
local hour = seconds / Time.lengthOfHour;
table.insert(entries, { numDays, day, hour });
end
end
return unpacked_values(entries);
end