Module:User:Cscott/Advent Of Code 2023/Day 19

return (function local builders = {} local function register(name, f) builders[name] = f end register('llpeg.lpegrex', function return require Module:User:Cscott/lpegrex end)

register('advent.compat', function return require Module:User:Cscott/compat end)

register('bignum', function(myrequire) local compat = myrequire('advent.compat')

-- poor man's bignum library local RADIX = 1000 -- power of 10 to make printout easier

local BigNum = {} BigNum.__index = BigNum function BigNum:new(n) return setmetatable( {n or 0}, self):normalize end function BigNum:__tostring local result = {} local first = true for i=#self,1,-1 do   local n = self[i] if n ~= 0 or not first then local s = tostring(n) if first then first = false else while #s < 3 do s = '0' .. s end end table.insert(result, s)   end end if #result == 0 then return "0" end return table.concat(result) end function BigNum:normalize local i = 1 while self[i] ~= nil do   if self[i] >= 1000 then local carry = math.floor(self[i] / 1000) self[i] = self[i] % 1000 self[i+1] = (self[i+1] or 0) + carry end i = i + 1 end return self end function BigNum:copy local r = BigNum:new for i=1,#self do   r[i] = self[i] end return r end function BigNum.__add(a, b) if type(a) == 'number' then a,b = b,a end local r = a:copy if type(b) == 'number' then r[1] = (r[1] or 0) + b else for i=1,#b do     r[i] = (r[i] or 0) + b[i] end end return r:normalize end function BigNum.__mul(a, b) if type(a) == 'number' then a,b = b,a end local r = BigNum:new if type(b) == 'number' then for i=1,#a do     r[i] = a[i] * b    end return r:normalize end for i=1,#a do   for j=1,#b do      local prod = a[i] * b[j] r[i+j-1] = (r[i+j-1] or 0) + prod end r:normalize end return r end

return BigNum

end)

register('util', function(myrequire) local function read_wiki_input(func)   return function (frame, ...)      if type(frame)=='string' then        frame = { args = { frame, ... } }      end      local title = mw.title.new(frame.args[1])      local source = title:getContent      if source == nil then        error("Can't find title " .. tostring(title))     end      source = source:gsub("^%s*]*>", "", 1)      source = source:gsub("]*>%s*$", "", 1)      return func(source)    end end

return { read_wiki_input = read_wiki_input, }

end)

register('day19', function(myrequire) -- DAY 19 --

local lpegrex = myrequire('llpeg.lpegrex') local compat = myrequire('advent.compat') local BigNum = myrequire('bignum') local read_wiki_input = myrequire('util').read_wiki_input

-- PARSING --

local patt = lpegrex.compile([[ Puzzle <== nl* {:workflows: Workflows :} nl+ {:parts: Parts :} nl* Workflows <-| Workflow (nl Workflow)* Workflow <-| Name `{` (Rule `,`)* FinalRule `}` Name <-- {:name: %w+ :} SKIP Rule <== Prop Op Val `:` Dest FinalRule:Rule <== Dest Prop <-- {:prop: %w+ :} Op <-- {:op: `<` -> 'lt' / `>` -> 'gt' :} Val <-- {:val: Number :} Dest <-- {:dest: %w+ :} Number <- (%d+ -> tonumber) SKIP

Parts <-| Part (nl Part)* Part <== `{` Rating ( `,` Rating )* `}` Rating <-| Prop `=` Val

nl <-- %nl SKIP <- [ ]* NAME_SUFFIX <- [_%w]+ ]])

function parse(source) --print(inspect(source)) local ast, errlabel, pos = patt:match(source) if not ast then local lineno, colno, line = lpegrex.calcline(source, pos) local colhelp = string.rep(' ', colno-1)..'^' error('syntax error: '..lineno..':'..colno..': '..errlabel..           '\n'..line..'\n'..colhelp) end --print('Parsed with success!') --print(inspect(ast)) -- turn the lists into maps local workflowMap = {} for _,v in ipairs(ast.workflows) do    workflowMap[v.name] = v   end ast.workflows = workflowMap

local partList = {} for i,part in ipairs(ast.parts) do    local partProps = {} for _,v in ipairs(part) do      partProps[v.prop] = v.val end partProps.id = i    partList[i] = partProps end ast.parts = partList return ast end

-- PART 1 --

local function processPart(workflows, name, part) if name == 'R' then return false end -- rejected if name == 'A' then return true end -- accepted! local w = workflows[name] for _,rule in ipairs(w) do   -- final rule: automatically dispatch if rule.op == nil then return processPart(workflows, rule.dest, part) end -- otherwise, process 'lt' rule if rule.op == 'lt' and part[rule.prop] < rule.val then return processPart(workflows, rule.dest, part) end if rule.op == 'gt' and part[rule.prop] > rule.val then return processPart(workflows, rule.dest, part) end end error("should not reach here") end

local function part1(source) local puzzle = parse(source) local sum = 0 for _,part in ipairs(puzzle.parts) do   local accept = processPart(puzzle.workflows, 'in', part) if accept then sum = sum + part.x + part.m + part.a + part.s   end end return sum end

-- PART 2 --

local Range = {} Range.__index = Range function Range:new(p) return setmetatable(p or { min=1, max=4000 }, self) end -- returns lt, ge split function Range:split(val) if val <= self.min then return nil, self -- entire self is above the split elseif val > self.max then return self, nil -- entire self is below the split else local lt = Range:new{ min=self.min, max=val-1 } local ge = Range:new{ min=val, max=self.max } return lt, ge end end function Range:__len -- number of values in the range return 1 + self.max - self.min end function Range:__tostring return string.format("[%d,%d]", self.min, self.max) end

local XMAS = {} XMAS.__index = XMAS function XMAS:new(p) return setmetatable(p or {     x=Range:new, m=Range:new, a=Range:new, s=Range:new,  }, self) end function XMAS:__len -- number of values in the XMAS return compat.len(self.x) * compat.len(self.m) * compat.len(self.a) * compat.len(self.s) end function XMAS:biglen return BigNum:new(1) * compat.len(self.x) * compat.len(self.m) * compat.len(self.a) * compat.len(self.s) end function XMAS:__tostring return string.format("{x=%s,m=%s,a=%s,s=%s}", self.x, self.m, self.a, self.s) end function XMAS:replace(prop, newRange) local xmas = XMAS:new{ x=self.x, m=self.m, a=self.a, s=self.s } xmas[prop] = newRange return xmas end -- returns lt, ge split function XMAS:split(prop, val) local r = self[prop] local lt, ge = self[prop]:split(val) if lt ~= nil then lt = self:replace(prop, lt) end if ge ~= nil then ge = self:replace(prop, ge) end return lt, ge end

local function processXMAS(workflows, name, xmas, accepted, rejected) if name == 'R' then -- rejected if rejected ~= nil then table.insert(rejected, xmas) end return end if name == 'A' then -- accepted! table.insert(accepted, xmas) return end local w = workflows[name] for _,rule in ipairs(w) do   -- final rule: automatically dispatch if rule.op == nil then return processXMAS(workflows, rule.dest, xmas, accepted, rejected) -- otherwise, process 'lt' rule elseif rule.op == 'lt' then local lt,ge = xmas:split(rule.prop, rule.val) -- recurse to handle the part which matches this rule processXMAS(workflows, rule.dest, lt, accepted, rejected) -- continue to handle the part which doesn't match xmas = ge   elseif rule.op == 'gt' then local le,gt = xmas:split(rule.prop, rule.val + 1) -- recurse to handle the part which matches this rule processXMAS(workflows, rule.dest, gt, accepted, rejected) -- continue to handle the part which doesn't match xmas = le   end end error("should not reach here") end

local function part2(source) local puzzle = parse(source) local accepted = {} processXMAS(puzzle.workflows, 'in', XMAS:new, accepted)

local sum = BigNum:new(0) for _,v in ipairs(accepted) do   --print(tostring(v), compat.len(v)) sum = sum + v:biglen end return sum end

-- CLI ] ]-- local source = io.input("day19.input"):read("*a") print('Sum:', part1(source)) print('Combinations:', part2(source)) --[ [ END CLI --

return { part1 = read_wiki_input(part1), part2 = read_wiki_input(part2), }

end)

local modules = {} modules['bit32'] = require('bit32') modules['string'] = require('string') modules['strict'] = {} modules['table'] = require('table') local function myrequire(name) if modules[name] == nil then modules[name] = true modules[name] = (builders[name])(myrequire) end return modules[name] end return myrequire('day19') end)