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}) return schedule, categories end

local currentDayState function h.parseEntry(entryInput) local entry = {} local events = {} local categories = "" local preconditions if #entryInput == 4 then local preconditionsInput = table.remove(entryInput, 2) preconditions, events, categories = h.parsePreconditions(preconditionsInput) 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.preconditions = preconditions entry.scheduleEntry = { time = time, location = location, actions = actions, }	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.parsePreconditions(conditions) local events = {} local preconditions local categories = "" for condition in string.gmatch(conditions, "%[([^%]]+)%]") do		local operator = string.match(condition, "^([^:]+):") local eventName = string.gsub(condition, "^([^:]+):", "") local event = Data.events[eventName] local eventState, categories = h.eventState(operator) if eventState == nil then h.warn("Invalid event condition . Recognized operators are   and  .", condition) categories = categories.."" elseif event == nil then h.warn("Unrecognized event name . See Module:MM Schedule/Data for supported events.", eventName) categories = categories.."" else preconditions = preconditions or {} preconditions[eventName] = eventState if not seenEvents[eventName] then local forkEventEntry = { epochMinutes = h.epochMinutes(event.sortTime), preconditions = event.preconditions, forkEvent = { name = eventName, description = event.description, notes = event.notes, }				}				table.insert(events, forkEventEntry) seenEvents[eventName] = event end end end return preconditions, 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 = utilsTable.takeWhile(schedule, function(entry)		return not entry.forkEvent	end) local forkEvents = {} for i = #entries + 1, #schedule do		if not schedule[i].forkEvent then break else table.insert(forkEvents, schedule[i].forkEvent) end end local html = mw.html.create("div") if not isBranch then html:addClass("mm-schedule") else html:addClass("mm-schedule__branch") end local wikitable = html:tag("table") :addClass("wikitable") :tag("tr") :tag("th") :wikitext("Time") :done :tag("th") :wikitext("Location") :done :tag("th") :wikitext("Action(s)") :done 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") :wikitext(scheduleEntry.time) :css({						["white-space"] = "nowrap",						["text-align"] = "center",					}) :done :tag("td") :wikitext(location) :css({						["white-space"] = "nowrap",						["text-align"] = "center",					}) :done :tag("td") :wikitext(scheduleEntry.actions) :done end end if #forkEvents > 0 then local forkEntries = utilsTable.slice(schedule, #entries + #forkEvents + 1) local scheduleBranches = h.scheduleBranches(forkEntries, forkEvents) local tabData = {} for i, branch in ipairs(scheduleBranches) do			local tabLabel = h.branchLabel(branch.conditions) local tabDescription = h.branchDescription(branch.conditions) local tabContent = h.printSchedule(branch.schedule, true) table.insert(tabData, {				label = tabLabel,				content = tabDescription..tabContent,			}) end local tabs = utilsLayout.tabs(tabData) html:wikitext(tabs) end return tostring(html) end function h.branchLabel(conditions) local listItems = {} for i, condition in ipairs(conditions) do		local eventName, eventState = unpack(condition) local file = ({			[true] = "File:Green check.svg",			[false] = "File:X mark.png",			["late"] = "File:MM3D Schedule Icon.png",		})[eventState] table.insert(listItems, "15px|link= "..eventName) end if #listItems == 1 then return listItems[1] else return utilsMarkup.list(listItems) end end function h.branchDescription(conditions) local conditionDescriptions = {} for i, condition in ipairs(conditions) do		local eventName, eventState = unpack(condition) local conditionDescription = Data.events[eventName].description[eventState] local stateDescription = ({			[true] = "completed",			[false] = "not completed",			["late"] = "completed late",		})[eventState] conditionDescription = string.format("%s. (\"%s\" Event %s) ", conditionDescription, eventName, stateDescription) table.insert(conditionDescriptions, conditionDescription) end local branchDescription if #conditions == 1 then branchDescription = "This schedule occurs when Link "..conditionDescriptions[1] else branchDescription = "This schedule occurs when Link: "..utilsMarkup.bulletList(conditionDescriptions) end

return branchDescription end

-- Generally, each "fork" in a character's schedule is determined by one event being completed or not. -- For Kafei's schedule however, the "Thief, begone!" and "Deliver the love letter" events happen at around the same time, so we want to show these two events in the same tab group. function h.scheduleBranches(schedule, events) local conditionCombinations = h.conditionCombinations(schedule, events) local branches = {} for i, conditions in ipairs(conditionCombinations) do		 local branchSchedule = h.filterByPreconditions(schedule, conditions) table.insert(branches, {		 	conditions = conditions,		 	schedule = branchSchedule,		 }) end return branches end

function h.conditionCombinations(schedule, events) local conditions = {} for i, event in ipairs(events) do		local hasLateCondition = utilsTable.find(schedule, function(entry) 			return h.getEventState(entry, event.name) == "late"		end) local eventConditions = {} table.insert(eventConditions, {event.name, true}) table.insert(eventConditions, {event.name, false}) if hasLateCondition then table.insert(eventConditions, {event.name, "late"}) end table.insert(conditions, eventConditions) end local conditionCombinations = h.cartesianProduct(conditions) return conditionCombinations end function h.cartesianProduct(sets) local setA = table.remove(sets, 1) local setB = table.remove(sets, 1) local product = {} for _, a in ipairs(setA) do		if setB then for _, b in ipairs(setB) do				table.insert(product, {a, b}) end else table.insert(product, {a}) end end if #sets > 0 then table.insert(sets, product) return h.cartesianProduct(sets) else return product end end

function h.filterByPreconditions(schedule, preconditions) return utilsTable.filter(schedule, function(entry)		for i, condition in ipairs(preconditions) do			local eventName, conditionState = unpack(condition)			local eventState = h.getEventState(entry, eventName)			if eventState ~= nil and eventState ~= conditionState then				return false			end			return true		end	end) end

function h.getEventState(entry, eventName) return entry and entry.preconditions and entry.preconditions[eventName] end

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

return p