Jump to content

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

From Wikipedia, the free encyclopedia
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)()