Module:MM Schedule

local p = {} local h = {} local Data = require("Module:MM Schedule/Data")

local DataTable = require("Module:Data Table") local Term = require("Module:Term") local utilsArg = require("Module:UtilsArg") local utilsLayout = require("Module:UtilsLayout") local utilsMarkup = require("Module:UtilsMarkup") local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable")

local CATEGORY_INVALID_ARGS = require("Module:Constants/category/invalidArgs")

function h.warn(msg, ...) local utilsError = require("Module:UtilsError") msg = string.format(msg, ...) utilsError.warn(msg) end

-- Template:MM Schedule/Header function p.Header(frame) local header = " " return footer end

-- Template:MM Schedule 2 (WIP) function p.Main2(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates["MM Schedule"]) local categories = err and err.categoryText or "" local rows = DataTable.parseRows(args.schedule) local scheduleEntries = utilsTable.map(rows, "cells") local scheduleTree, categories2 = h.parseSchedule(scheduleEntries) local scheduleOutput = h.printSchedule(scheduleTree) categories = categories..categories2

return scheduleOutput, categories end

function h.parseSchedule(entries) local categories = "" local schedule = {} local events = {} for i, entryInput in ipairs(entries) do		local entry, entryEvents, errCategories = h.parseEntry(entryInput) categories = categories..errCategories if errCategories == "" then events = utilsTable.concat(events, entryEvents) table.insert(schedule, entry) end end events = utilsTable.uniqueBy(events, function(event)		return event.forkEvent.name	end)

schedule = utilsTable.concat(events, schedule) schedule = utilsTable.sortBy(schedule, {"epochMinutes", function(entry)		if entry.dayStateChange then			return 0		elseif entry.scheduleEntry then			return 1		elseif entry.forkEvent then			return 2		end	end}) schedule = h.buildScheduleTree(schedule) return schedule, categories end

function h.buildScheduleTree(schedule, branchConditions) local entries = {} local i = 1 for i, entry in ipairs(schedule) do		branchConditions = branchConditions or {} if entry.forkEvent then local forkEvent = entry.forkEvent local eventName = entry.forkEvent.name local forkEntries = utilsTable.slice(schedule, i + 1) entry.forkEvent.entries = h.groupEntriesByEventState(forkEntries, branchConditions, eventName) for eventState, branchEntries in pairs(entry.forkEvent.entries) do				branchConditions[eventName] = eventState entry.forkEvent.entries[eventState] = h.buildScheduleTree(branchEntries, branchConditions) end table.insert(entries, entry) break else table.insert(entries, entry) end end return entries end function h.groupEntriesByEventState(schedule, branchConditions, eventName) local hasLateState = utilsTable.find(schedule, function(entry) 		local eventState = h.getEventState(entry, eventName)		return eventState == "late"	end) local entriesByEventState = {} entriesByEventState[true] = h.filterByEventState(schedule, branchConditions, eventName, true) entriesByEventState[false] = h.filterByEventState(schedule, branchConditions, eventName, false) if hasLateState then entriesByEventState["late"] = h.filterByEventState(schedule, branchConditions, eventName, "late") end return entriesByEventState end function h.filterByEventState(schedule, branchConditions, eventName, desiredState) return utilsTable.filter(schedule, function(entry)		local eventState = h.getEventState(entry, eventName)		if eventState == nil and not entry.forkEvent then			return true		elseif eventState == nil then			-- local preconditions = Data.events[entry.forkEvent.name].preconditions or {}			-- for k, v in pairs(preconditions) do			-- 	if branchConditions[k] ~= v then			-- 		return false			-- 	end			-- end			-- return true			return false		else			return eventState == desiredState		end	end) end function h.getEventState(entry, eventName) return entry.scheduleEntry and entry.scheduleEntry.eventConditions and entry.scheduleEntry.eventConditions[eventName] end

local currentDayState function h.parseEntry(entryInput) local entry = {} local events = {} local categories = "" local eventConditions if #entryInput == 4 then local eventConditionsInput = table.remove(entryInput, 2) eventConditions, events, categories = h.parseEvents(eventConditionsInput) end if #entryInput == 3 then local time = entryInput[1] local epochMinutes = h.epochMinutes(currentDayState.. " "..time) if not epochMinutes then h.warn("Invalid time entry: %s. See Template:MM Schedule for correct time format.", time) categories = "" end local location = entryInput[2] local actions = entryInput[3] entry.epochMinutes = epochMinutes entry.scheduleEntry = { time = time, location = location, actions = actions, eventConditions = eventConditions, }	elseif #entryInput == 1 then local dayState = entryInput[1] local dayStateCode = Data.dayStates[dayState] if dayStateCode then currentDayState = dayStateCode else local dayStates = utilsTable.keys(Data.dayStates) local dayStateList = utilsMarkup.bulletList(dayStates) h.warn("Invalid day state . Accepted day states are: %s", dayState, dayStateList) categories = "" end entry.dayStateChange = dayState entry.epochMinutes = h.dayStateToMinutes(dayStateCode) else h.warn("Invalid entry: %s", utilsTable.print(entryInput)) entry = nil categories = "" end return entry, events, categories end

local seenEvents = {} function h.parseEvents(eventConditionsInput) local events = {} local eventConditions local categories = "" for eventConditionInput in string.gmatch(eventConditionsInput, "%[([^%]]+)%]") do		local operator = string.match(eventConditionInput, "^([^:]+):") local eventName = string.gsub(eventConditionInput, "^([^:]+):", "") local event = Data.events[eventName] local eventState, categories = h.eventState(operator) if eventState == nil then h.warn("Invalid event condition . Recognized operators are   and  .", eventCondition) categories = categories.."" elseif event == nil then h.warn("Unrecognized event name . See Module:MM Schedule/Data for supported events.", eventName) categories = categories.."" else eventConditions = eventConditions or {} eventConditions[eventName] = eventState if not seenEvents[eventName] then event = utilsTable.merge({ name = eventName }, event) table.insert(events, {					forkEvent = event,					epochMinutes = h.epochMinutes(event.sortTime),				}) seenEvents[eventName] = event end end end return eventConditions, events, categories end function h.eventState(operator) if operator == nil then return true elseif operator == "Not" then return false elseif operator == "Late" then return "late" else return nil end end

-- @param timeStr a string such as "D2 12:00 PM" -- @return an integer representation of the time in minutes since the Dawn of the First Day, or nil if timeStr is invalid function h.epochMinutes(timeStr) local categories = "" local isValid = string.match(timeStr, "^[DN][1-3] 1?[0-9]:[0-5][0-9] [AP]M") if not isValid then return nil end local timeParts = utilsString.split(timeStr, " ") local dayState = timeParts[1] local dayPeriod = timeParts[3] local hours, minutes = unpack(utilsString.split(timeParts[2], ":")) hours = tonumber(hours) minutes = tonumber(minutes)

local totalMinutes = h.dayStateToMinutes(dayState) if hours < 6 then totalMinutes = totalMinutes + 6*60 + hours*60 elseif hours > 6 then totalMinutes = totalMinutes + (hours-6)*60 end totalMinutes = totalMinutes + minutes return totalMinutes end

function h.dayStateToMinutes(dayState) dayState = utilsString.split(dayState, "") local dayNum = tonumber(dayState[2]) local minutes = (dayNum-1)*24*60 -- offset by 24 hours for each day if dayState[1] == "N" then minutes = minutes + 12*60 -- offset by an additional 12 hours for nights end return minutes end

function h.printSchedule(schedule, isBranch) local entries, fork = utilsTable.partition(schedule, function(entry)		return not entry.forkEvent	end) fork = fork[1] local html = mw.html.create("div") if isBranch then html:addClass("mm-schedule__branch") else html:addClass("mm-schedule") :css({				["border"] = "1px solid #426787",				["border-radius"] = "0.5rem",				["max-width"] = "60rem",			}) end local wikitable = html:tag("table") :addClass("wikitable") :css({			["width"] = "100%",			["border"] = "none",		}) if not isBranch then wikitable :tag("tr") :tag("th") :wikitext("Time") :done :tag("th") :wikitext("Location") :done :tag("th") :wikitext("Action(s)") :done end for i, entry in ipairs(entries) do		local trow = wikitable:tag("tr") if entry.dayStateChange then trow:tag("th") :attr("colspan", 3) :wikitext(entry.dayStateChange) :done elseif entry.scheduleEntry then local scheduleEntry = entry.scheduleEntry local locationInput = scheduleEntry.location local location if utilsMarkup.containsLink(locationInput) then location = locationInput else local locationSubject, locationNotes = utilsMarkup.separateMarkup(locationInput) local locationLink = Term.link(locationSubject, "MM3D") location = locationLink..locationNotes end trow:tag("td") :css("width", "10%") :wikitext(scheduleEntry.time) :done :tag("td") :css("width", "20%") :wikitext(location) :done :tag("td") :wikitext(scheduleEntry.actions) :done end end if fork then local eventName = fork.forkEvent.name local eventDescription = fork.forkEvent.description local forkEntries = fork.forkEvent.entries local tabData = { {				label = " "..eventName, content = string.format("This schedule occurs when Link %s (\"%s\" Event completed).", eventDescription[true], eventName) .. h.printSchedule(forkEntries[true], true) },			{				label = " "..eventName, content = string.format("This schedule occurs when Link %s (\"%s\" Event not completed).", eventDescription[false], eventName) .. h.printSchedule(forkEntries[false], true) },		}		if forkEntries["late"] then table.insert(tabData, {				{					label = "(late) "..eventName, -- TODO: Icon					content = string.format("This schedule occurs when Link %s (\"%s\" Event completed late).", eventDescription["late"], eventName)					.. h.printSchedule(forkEntries["late"], true)				},			}) end local tabs = utilsLayout.tabs(tabData) html:wikitext(tabs) end return tostring(html) end

p.Templates = { ["MM Schedule"] = { params = { ["..."] = {				name = "schedule", type = "content", trim = true, },		},	} }

return p