1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
require "strscan"

Part = Data.define(:number) do
  def eql?(other)
    self.object_id == other.object_id
  end
end

def parse(input)
  ss = StringScanner.new(input)
  y = x = 0
  schematic = {}
  until ss.eos?
    case
      when ss.scan(/\./)
        x += 1
      when num = ss.scan(/\d+/)
        coords = (0...num.length).map {|dx| [y, x+dx] }
        part = Part.new(num.to_i)
        schematic.merge!(coords.to_h { [_1, part] })
        x += num.length
      when ss.scan(/\n/)
        y += 1
        x = 0
      when sym = ss.scan(/./)
        schematic[[y,x]] = sym
        x += 1
      else
        fail
    end
  end
  schematic
end

schematic = parse(ARGF.read)
syms, nums = schematic.partition { String === _2 }.map(&:to_h)
neighboring_parts = ->((y,x)) {
  dy = (-1..1).to_a
  dx = (-1..1).to_a
  dy.product(dx)
    .map {|dy,dx| [y+dy,x+dx] }
    .filter_map { nums.fetch(_1, nil) }
}

# part one
p syms.keys.flat_map { neighboring_parts.(_1) }.uniq.sum(&:number)

# part two
p syms
  .select { _2 == ?* }
  .filter_map {|coord, _|
    parts = neighboring_parts.(coord).uniq
    parts.length == 2 ? parts : nil
  }
  .sum { _1.map(&:number).inject(:*) }