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

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('day5', function(myrequire) -- DAY 5 --

local lpegrex = myrequire('llpeg.lpegrex')

-- PARSING -- local patt = lpegrex.compile([[ Start <== {:seeds: Seeds :} nl nl {:maps: {| Map (nl nl Map)* |} :} nl* !.

Seeds <-- `seeds:` NumberList MapTitle <- {:source: %w+ :} `-` `to` `-`{:dest: %w+ :} SKIP `map:` Map <-- {| MapTitle nl {:ranges: {| Range (nl Range)* |} :} |} Range <-- {| SKIP {:destStart: Number :} SKIP {:sourceStart: Number :} SKIP {:rangeLength: Number :} SKIP &(nl/!.) |} NumberList <-- {| (Number SKIP)* |} Number <-- %d+ -> tonumber 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)) -- Post process maps local maps = {} for _,m in ipairs(ast.maps) do     maps[m.source] = m      -- add 'sourceEnd' for convenience for _,r in ipairs(m.ranges) do        r.sourceEnd = r.sourceStart + r.rangeLength - 1 end -- sort ranges table.sort(m.ranges, function(a,b)                   return a.sourceStart < b.sourceStart      end) end return ast.seeds, maps end

-- PART 1 --

function map_one(ranges, val) for _,r in ipairs(ranges) do     if r.sourceStart <= val and val < (r.sourceStart + r.rangeLength) then return (val - r.sourceStart) + r.destStart end end return val -- not mapped => unchanged end

function get_location(seed, maps) local have = "seed" local want = "location" local val = seed while have ~= want do     m = maps[have] val = map_one(m.ranges, val) have = m.dest end return val end

function get_first_location_part1(source) seeds, maps = parse(source) local locations = {} for _,seed in ipairs(seeds) do     table.insert(locations, get_location(seed, maps)) end table.sort(locations) return locations[1] end

-- PART 2 --

function addTo(table1, table2) for _,v in ipairs(table2) do     table.insert(table1, v)   end end

local RangeMT = {} function RangeMT:__tostring return self.start .. "-" .. self["end"] end

function new_range(start, len) return setmetatable(     { start = start, len = len, ["end"] = start + len - 1 },      RangeMT   ) end

function map_ranges_for_range(mapping, inputRange) local result = {} for _,mapRange in ipairs(mapping) do     if inputRange["end"] < mapRange.sourceStart then -- mapping is sorted, so nothing's going to match after this point break end if mapRange.sourceStart < (inputRange.start + inputRange.len) and inputRange.start < (mapRange.sourceStart + mapRange.rangeLength) then -- there is some overlap! break into three pieces: -- unmapped_low mapped unmapped_high

if inputRange.start < mapRange.sourceStart then -- unmapped low. since the mapping is sorted, we know no other -- mapping range overlaps this, so we can add it to the result -- as-is table.insert(result, new_range(inputRange.start, mapRange.sourceStart - inputRange.start)) end

-- mapped middle local overlapStart = math.max(inputRange.start, mapRange.sourceStart) local overlapEnd = math.min(inputRange["end"], mapRange.sourceEnd) table.insert(result, new_range(mapRange.destStart + overlapStart - mapRange.sourceStart, 1 + overlapEnd - overlapStart))

if mapRange.sourceEnd < inputRange["end"] then -- unmapped high: we need to keep going for this one inputRange = new_range(mapRange.sourceEnd, inputRange["end"] - mapRange.sourceEnd) else -- nothing left to remap! return result end end end -- not mapped => unchanged table.insert(result, inputRange) return result end

function get_first_location_for_range(seedStart, rangeLen, maps) -- print("First location for", seedStart,"-",seedStart + rangeLen - 1) local have = "seed" local want = "location" local ranges = { new_range(seedStart, rangeLen) } while have ~= want do     -- print("Have", have, "Want", want) local m = maps[have] local result = {} for _,r in ipairs(ranges) do        -- print("Mapping range", r)         addTo(result, map_ranges_for_range(m.ranges, r)) end ranges = result have = m.dest --     for _,r in ipairs(ranges) do         print("  Got", r)      end      -- end -- now we just need to find the lowest value in a range local starts = {} for _,r in ipairs(ranges) do     table.insert(starts, r.start) end table.sort(starts) return starts[1] end

function get_first_location_part2(source) local seeds, maps = parse(source) -- print(inspect(maps)) local locations = {} for i=1,#seeds,2 do     table.insert(locations, get_first_location_for_range(seeds[i], seeds[i+1], maps)) end table.sort(locations) return locations[1] end

--[[ CLI start ] ]-- --local source = io.input("day5.input"):read("a") --print(get_first_location_part1(source))

local source = io.input("day5.input"):read("a") print(get_first_location_part2(source)) --[ [ CLI end ]]--

return { part1 = function(frame) local s = mw.title.new(frame.args[1]):getContent return get_first_location_part1(s) end, part2 = function(frame) local s = mw.title.new(frame.args[1]):getContent return get_first_location_part2(s) end, }

end)

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