anoff

7142618?v=4

anoff
Andreas Offenhaeuser
: anoff

About me

Nothing here yet. Update your profile at /profiles/anoff.adoc

Day 00: ruby

this documentation is autogenerated. Add a README.adoc to your solution to take over the control of this :-)

ruby

solution.rb
puts "Hello World!"

Day 00: python

Hello world for Advent of Code 2019

Python

This year I want to refurbish my python skills..and maybe try some Ruby.

Run the solution with python solution.py

print("hello world")

Day 01: go

Part 1

The biggest struggle was to configure VScode to allow me adding the "math" package to my solution.

I also did not read the manual correctly and forgot to subtract minus 2 at first.

Part 2

I solved the fuel needs more fuel dilemma by creating a requiredFuelRecursive function that takes its intermediate result and calls itself again. To prevent an endless loop it checks the intermediate result to be positive before recursion.

Solution

package main

import (
	"fmt"
	"math"
)

func main() {
	input := readInput("./input.txt")
	part1(StringSlice2IntSlice(input))
	part2(StringSlice2IntSlice(input))
}

func part1(massInventory []int) {
	var fuelSum int = 0
	for _, mass := range massInventory {
		fuelSum += requiredFuel(mass)
	}
	fmt.Println("Solution for part1:", fuelSum)
}

func part2(massInventory []int) {
	var fuelSum int = 0
	for _, mass := range massInventory {
		fuelSum += requiredFuelRecursive(mass)
	}
	fmt.Println("Solution for part2:", fuelSum)
}

func requiredFuel(mass int) int {
	var massFloat = float64(mass)
	var fuelFloat = math.Floor(massFloat / 3)
	return int(fuelFloat) - 2
}

func requiredFuelRecursive(mass int) int {
	var massFloat = float64(mass)
	var fuelFloat = math.Floor(massFloat/3) - 2
	var fuelInt = int(math.Max(fuelFloat, 0))
	if fuelInt > 0 {
		var additionalFuel = requiredFuelRecursive(fuelInt)
		fuelInt += additionalFuel
	}
	return fuelInt
}
Test cases
package main

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestRequiredFuel(t *testing.T) {
	m := make(map[int]int)
	m[12] = 2
	m[14] = 2
	m[1969] = 654
	m[100756] = 33583
	for mass, fuel := range m {
		res := requiredFuel(mass)
		assert.Equal(t, fuel, res, "Wrong result for mass=%d", mass)
	}
}

func TestRequiredFuelRecursive(t *testing.T) {
	m := make(map[int]int)
	m[12] = 2
	m[14] = 2
	m[1969] = 966
	m[100756] = 50346
	for mass, fuel := range m {
		res := requiredFuelRecursive(mass)
		assert.Equal(t, fuel, res, "Wrong result for mass=%d", mass)
	}
}

Day 01: ruby

Part 1

First program in ruby. Had to figure out how to layout the main file to allow multiple methods and running tests. Solution is pretty straight forward. Way shorter than go 😲

Part 2

Decided to do a loop instead of a recursion to find the total amount of fuel.

Solution

#!/usr/bin/ruby
def required_fuel(mass)
  (mass / 3) - 2
end

def part1(massInventory)
  fuelSum = massInventory
    .map { |mass| required_fuel(mass) }
    .reduce(0) { |sum, num| sum + num }
end

def part2(massInventory)
  fuelSum = 0
  massInventory.each{ |mass|
    fuel = required_fuel(mass)
    while fuel > 0 do
      fuelSum += fuel
      fuel = required_fuel(fuel)
    end
  }
  fuelSum
end

if caller.length == 0
  input = File.read("./input.txt")

  massInventory = input.split("\n").map(&:to_i)

  puts "Solution for part1: %d" % part1(massInventory)
  puts "Solution for part2: %d" % part2(massInventory)
end
Test cases
require "test/unit"
require_relative './solution'

class TestSolution < Test::Unit::TestCase
  def test_required_fuel
    d = {
      12 => 2,
      14 => 2,
      1969 => 654,
      100756 => 33583
    }
    d.each{ |mass, fuel|
      assert_equal fuel, required_fuel(mass), "Incorrect fuel for mass=%d" % mass
    }
  end
end

Day 02: ruby

Part 1

At first I was reminded of last years puzzle. But thinking about it for a while it seemed to be the best approach to just manipulate the program in memory. After figuring out how ruby passes data - by argument - the implementation was rather straight forward.

However I forgot to RTFM again and did not configure my program prior to start and was wondering why test cases passed and the program failed.

Part 2

The major challenge for part 2 was to figure out how I can clone a list in ruby to make sure that each program execution really starts with a clean memory.

Solution

def op(program, ptr)
  opcode = program[ptr]
  # puts "Running opcode:%d on pos:%d" % [opcode, ptr]
  case opcode
  when 99
    return 1
  when 1..2
    if (program.length - 1) <= ptr + 3
      return -2
    end
    in1 = program[ptr+1]
    in2 = program[ptr+2]
    out = program[ptr+3]
    if opcode == 1
      program[out] = program[in1] + program[in2]
    elsif opcode == 2
      program[out] = program[in1] * program[in2]
    end
    return 0
  else
    return -1
  end
end

def run_to_termination(program)
  ptr = 0
  while op(program, ptr) == 0
    ptr += 4
  end
  return program[0]
end

def try_parameter(program, arg1, arg2)
  prog_copy = Array.new(program)
  prog_copy[1] = arg1
  prog_copy[2] = arg2
  res = run_to_termination(prog_copy)
  # puts "Ran %d, %d = %d" % [arg1, arg2, res]
  res
end

def part1(program)
  program[1] = 12
  program[2] = 2
  run_to_termination(program)
end

def part2(program)
  noun = 0
  verb = 0
  while try_parameter(program, noun, verb) != 19690720
    noun += 1
    if noun > 99
      noun = 0
      verb += 1
    end
    if noun == 99 && verb == 99
      puts "No solution found"
      exit(1)
    end
  end
  return 100 * noun + verb
end

if caller.length == 0
  input = File.read("./input.txt")

  program = input.split(",").map(&:to_i)

  puts "Solution for part1: %d" % part1(Array.new(program))
  puts "Solution for part2: %d" % part2(Array.new(program))
end
Test cases
require "test/unit"
require_relative './solution'

class TestSolution < Test::Unit::TestCase
  def test_required_fuel
    d = {
      3500 => [1,9,10,3,2,3,11,0,99,30,40,50],
      30 => [1,1,1,4,99,5,6,0,99]
    }
    d.each{ |result, program|
      assert_equal result, run_to_termination(program), "Incorrect result for input" % program
    }
  end
end

Day 03: ruby

Puzzle: Crossed Wires

Part 1

This was another challenge where having participated previously makes the solution process a lot easier. Having thought about similar problems I quickly decided to create a write:

  1. a parser for the wire paths

  2. create a hashmap with key=gridpoint and value=wires passing

  3. after parsing all wires, check the entire hashmaps for keys where multiple wires passed

  4. identify crossing with smallest manhattan distance

Playing around with classes in Ruby I learned a lot of interesting things:

  • you can overload + and == operators which allows you to define the behavior of simple additions and comparisons

  • class properties do not have accessory by default

  • when put (printing) a class instance it is automatically turned to a string if the to_s method is implemented

#!/usr/bin/ruby
require_relative './geom'

# take a wire and return a hash with Point->wire ID for each point the wire crossed
#   if a hash was passed it will be modified in place, otherwise a blank one will be created
def parseWire(wire, wireID, map = Hash.new)
  pos = Point.new(0, 0)
  steps = 0
  for instruction in wire
    delta = Point.new(0, 0)
    dir = instruction[0]
    length = instruction[1..-1].to_i
    case dir
    when 'R'
      delta.x = 1
    when 'D'
      delta.y = -1
    when 'L'
      delta.x = -1
    when 'U'
      delta.y = 1
    else
      raise "Unexpected direction instruction %s" % dir
    end

    while length > 0
      map[pos.to_s] = [wireID + "-" + steps.to_s].concat(map[pos.to_s] || Array.new())
      pos = pos + delta
      length -= 1
      steps += 1
    end
  end
  return map
end

def part1(wires)
  map = Hash.new
  wireID = 1
  for wire in wires
    parseWire(wire, wireID.to_s, map)
    wireID += 1
  end
  closestPoint = nil
  shortestDistance = 1/0.0 # infinity
  origin = Point.new(0, 0)
  map.each do |key, value|
    if value.find {|item| item.include?("1-")} && value.find {|item| item.include?("2-")}
      p1 = Point.from_s(key)
      distance = p1.manhattan(origin)
      if distance < shortestDistance && p1 != origin
        closestPoint = p1
        shortestDistance = distance
      end
    end
  end
  shortestDistance
end


def part2(wires)
  map = Hash.new
  wireID = 1
  for wire in wires
    parseWire(wire, wireID.to_s, map)
    wireID += 1
  end
  closestPoint = nil
  shortestDistance = 1/0.0 # infinity
  origin = Point.new(0, 0)
  map.each do |key, value|
    if value.find {|item| item.include?("1-")} && value.find {|item| item.include?("2-")}
      p1 = Point.from_s(key)
      steps1 = value.select {|item| item.include?("1-")}.map{|item| item[2..-1].to_i}.reduce(1/0.0){|prev, cur| prev = cur if cur < prev}
      steps2 = value.select {|item| item.include?("2-")}.map{|item| item[2..-1].to_i}.reduce(1/0.0){|prev, cur| prev = cur if cur < prev}
      distance = steps1 + steps2
      if distance < shortestDistance && p1 != origin
        closestPoint = p1
        shortestDistance = distance
      end
    end
  end
  shortestDistance
end

if caller.length == 0
  input = File.read("./input.txt")

  wires = input.split("\n").map{ |wire| wire.split(",") }
  puts "Solution for part1: %d" % part1(wires)
  puts "Solution for part2: %d" % part2(wires)
end

Part 2

To calculate the length of the wires until they cross I simply added the wire length to the Hash map from part 1. That way at each position on the grid I can identify if a wire is there and how many steps the wire took to get there. Using the same method to identify crossings I simply changed the selection logic to take into account the wire length instead of the manhattan distance.

Solution

#!/usr/bin/ruby
require_relative './geom'

# take a wire and return a hash with Point->wire ID for each point the wire crossed
#   if a hash was passed it will be modified in place, otherwise a blank one will be created
def parseWire(wire, wireID, map = Hash.new)
  pos = Point.new(0, 0)
  steps = 0
  for instruction in wire
    delta = Point.new(0, 0)
    dir = instruction[0]
    length = instruction[1..-1].to_i
    case dir
    when 'R'
      delta.x = 1
    when 'D'
      delta.y = -1
    when 'L'
      delta.x = -1
    when 'U'
      delta.y = 1
    else
      raise "Unexpected direction instruction %s" % dir
    end

    while length > 0
      map[pos.to_s] = [wireID + "-" + steps.to_s].concat(map[pos.to_s] || Array.new())
      pos = pos + delta
      length -= 1
      steps += 1
    end
  end
  return map
end

def part1(wires)
  map = Hash.new
  wireID = 1
  for wire in wires
    parseWire(wire, wireID.to_s, map)
    wireID += 1
  end
  closestPoint = nil
  shortestDistance = 1/0.0 # infinity
  origin = Point.new(0, 0)
  map.each do |key, value|
    if value.find {|item| item.include?("1-")} && value.find {|item| item.include?("2-")}
      p1 = Point.from_s(key)
      distance = p1.manhattan(origin)
      if distance < shortestDistance && p1 != origin
        closestPoint = p1
        shortestDistance = distance
      end
    end
  end
  shortestDistance
end


def part2(wires)
  map = Hash.new
  wireID = 1
  for wire in wires
    parseWire(wire, wireID.to_s, map)
    wireID += 1
  end
  closestPoint = nil
  shortestDistance = 1/0.0 # infinity
  origin = Point.new(0, 0)
  map.each do |key, value|
    if value.find {|item| item.include?("1-")} && value.find {|item| item.include?("2-")}
      p1 = Point.from_s(key)
      steps1 = value.select {|item| item.include?("1-")}.map{|item| item[2..-1].to_i}.reduce(1/0.0){|prev, cur| prev = cur if cur < prev}
      steps2 = value.select {|item| item.include?("2-")}.map{|item| item[2..-1].to_i}.reduce(1/0.0){|prev, cur| prev = cur if cur < prev}
      distance = steps1 + steps2
      if distance < shortestDistance && p1 != origin
        closestPoint = p1
        shortestDistance = distance
      end
    end
  end
  shortestDistance
end

if caller.length == 0
  input = File.read("./input.txt")

  wires = input.split("\n").map{ |wire| wire.split(",") }
  puts "Solution for part1: %d" % part1(wires)
  puts "Solution for part2: %d" % part2(wires)
end
Test cases
require "test/unit"
require_relative './solution'

class TestSolution < Test::Unit::TestCase
  def test_manhattan
    assert_equal 3, Point.new(0,0).manhattan(Point.new(3, 0))
    assert_equal 5, Point.new(0,-2).manhattan(Point.new(3, 0))
    assert_equal 7+4, Point.new(10,4).manhattan(Point.new(3, 0))
  end
  def test_point_add
    assert Point.new(3,3) == Point.new(1, 1) + Point.new(2, 2)
  end
  def test_point_from_s
    assert Point.from_s("(x:3,y:4)") == Point.new(3, 4)
    assert Point.from_s("(x:153,y:246)") == Point.new(153, 246)
  end
  def test_part1
    d = {
      6 => [['R8','U5','L5','D3'], ['U7','R6','D4','L4']],
      159 => [['R75','D30','R83','U83','L12','D49','R71','U7','L72'],
      ['U62','R66','U55','R34','D71','R55','D58','R83']],
      135 => [['R98','U47','R26','D63','R33','U87','L62','D20','R33','U53','R51'],
      ['U98','R91','D20','R16','D67','R40','U7','R15','U6','R7']]
    }
    d.each{ |score, input|
      assert_equal score, part1(input)
    }
  end
  def test_part2
    d = {
      30 => [['R8','U5','L5','D3'], ['U7','R6','D4','L4']],
      610 => [['R75','D30','R83','U83','L12','D49','R71','U7','L72'],
      ['U62','R66','U55','R34','D71','R55','D58','R83']],
      410 => [['R98','U47','R26','D63','R33','U87','L62','D20','R33','U53','R51'],
      ['U98','R91','D20','R16','D67','R40','U7','R15','U6','R7']]
    }
    d.each{ |score, input|
      assert_equal score, part2(input)
    }
  end
end

Day 04: ruby

Part 2

AHHHHHHHH! Forgot to add the .length and always tried to fix my code but the problem was in outputting the length of solutions correctly 😨

Solution

To run the solution use

#!/usr/bin/ruby
def is_decreasing(number)
  diff = diff_vector(number)
  return diff.find{|e| e < 0} != nil
end

def is_valid(number)
  diff = diff_vector(number)
  has_dupe = diff.find{|e| e == 0} != nil

  has_dupe && !is_decreasing(number)
end

def has_real_dupes(number)
  map = Hash.new()
  digits = number.to_s.split("")
  for digit in digits
    if map[digit] == nil
      map[digit] = 1
    else
      map[digit] += 1
    end
  end

  for digit in digits
    if map[digit] == 2
      return true
    end
  end
  return false
end

def is_valid_p2(number)
  diff = diff_vector(number)
  is_decreasing = diff.find{|e| e < 0} != nil

  has_real_dupes(number) && !is_decreasing
end
def diff_vector(number)
  arr = number.to_s.split("").map(&:to_i)
  arr[1..-1].map.with_index{|e,ix| e - arr[ix]} # arr[ix] = previous element because index in map starts with an offset
end
def part1(start, stop)
  (start..stop).select{|n| is_valid(n)}.length
end

def part2(start, stop)
  (start..stop).select{|n| !is_decreasing(n) && has_real_dupes(n)}.length
end

if caller.length == 0
  puts "Solution for part1: %d" % part1(245318, 765747)
  puts "Solution for part2: %d" % part2(245318, 765747)
end
Test cases
require "test/unit"
require_relative './solution'

class TestSolution < Test::Unit::TestCase
  def test_is_decreasing
    d = {
       321 => true,
       123456 => false,
       222222 => false,
       412341 => true,
       65555 => true,
       12345 => false
    }
    d.each{ |input, result|
      assert_equal result, is_decreasing(input), "for input %d" % input
    }
  end
  def test_is_valid
    d = {
       111111 => true,
       223450 => false,
       123789 => false
    }
    d.each{ |input, result|
      assert_equal result, is_valid(input)
    }
  end
  def test_is_valid_p2
    d = {
       111111 => false,
       223450 => false,
       112233 => true,
       22 => true,
       444 => false,
       123789 => false,
       111122 => true,
       1111222447789 => true,
       1111 => false,
       123444 => false
    }
    d.each{ |input, result|
      assert_equal result, is_valid_p2(input), "wrong result for input %d" % input
    }
  end
  def test_has_real_dupes
    d = {
      111111 => false,
      223450 => true,
      112233 => true,
      22 => true,
      444 => false,
      123789 => false,
      111122 => true,
      1111222447789 => true,
      1111 => false,
      123444 => false
   }
   d.each{ |input, result|
     assert_equal result, has_real_dupes(input), "wrong result for input %d" % input
   }
 end
end

Day 05: ruby

Part 1

Luckily I could re-use a lot of day2 code. The main changes were adding new opcodes and initially splitting the opcode and modes from the command instruction.

Another minor tweak was to modify my op(program, ptr) method to return a variable pointer offset. During day02 I used the return value of op() and treated it like a program return code in bash - meaning 0 = ok and everything else leads to an error. With day05 every return code >= 0 is a valid return code, where the returned number equals the steps the pointer needs to take. Negative return codes result in errors caused by unknown opcodes or parsing errors - happened a few times during building this solution 🙄

Part 2

This was not really a challenge and only meant implementing a few more instructions into my op() method.

Solution

To run the solution use ruby solution.rb

def op(program, ptr)
  cmd = program[ptr]
  opcode = cmd % 100
  modes = Array.new()
  modes[2] = cmd / 10000
  modes[1] = cmd % 10000 / 1000
  modes[0] = cmd % 1000 / 100

  puts "Running opcode:%d on pos:%d with modes:%s from cmd:%d" % [opcode, ptr, modes.map(&:to_s).join(","), cmd]
  case opcode
  when 99
    return 0
  when 1..8
    if opcode == 1
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      out = program[ptr+3]
      program[out] = in1 + in2
      return 4
    elsif opcode == 2
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      out = program[ptr+3]
      program[out] = in1 * in2
      return 4
    elsif opcode == 3
      out = program[ptr+1]
      puts "Please provide numeric input:"
      in1 = gets.to_i
      puts "Storing %d at position %d" % [in1, out]
      program[out] = in1
      return 2
    elsif opcode == 4
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      puts "Running output for ptr:%d mode:%d at:%d" % [ptr, modes[0], program[ptr+1]]
      puts "Output: %d" % in1
      return 2
    elsif opcode == 5
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      if in1 > 0
        return in2 - ptr
      else
        return 3
      end
    elsif opcode == 6
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      if in1 == 0
        return in2 - ptr
      else
        return 3
      end
    elsif opcode == 7
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      out = program[ptr+3]
      if in1 < in2
        program[out] = 1
      else
        program[out] = 0
      end
      return 4
    elsif opcode == 8
      in1 = modes[0] == 1 ? program[ptr+1] : program[program[ptr+1]] # switch between position mode (0) and immediate/value mode (1)
      in2 = modes[1] == 1 ? program[ptr+2] : program[program[ptr+2]]
      out = program[ptr+3]
      if in1 == in2
        program[out] = 1
      else
        program[out] = 0
      end
      return 4
    end
  else
    puts "Unknown opcode: %d" % opcode
    return -1 # unknown opcode (not 1..4)
  end
end

def run_to_termination(program)
  puts program.length
  ptr = 0
  step = op(program, ptr)
  while step > 0
    ptr += step
    step = op(program, ptr)
  end
  if step < 0
    throw "Unexpected error occured: %d" % step
  end
end

def part1(program)
  run_to_termination(program)
end


if caller.length == 0
  input = File.read("./input.txt")

  program = input.split(",").map(&:to_i)

  puts "Running part1.."
  part1(Array.new(program))
  puts "..Done"
  # puts "Solution for part2: %d" % part2(Array.new(program))
end
Test cases
Unresolved directive in ../../../../../../day05/ruby/anoff/README.adoc - include::solution_test.rb[]

Day 06: ruby

Part 1

At first I thought this would require implementing a full graph solution again. However after studying the first part of the challenge I thought a simple hash map could also do the trick so I tried. Going from method to method in my head I built them using TDD on a unit test level to make sure the piece do their job correctly. Finally I implemented the overall testcase of check the overall functionality (excluding parsing the input file).

  def test_total_orbit_count
    d = {
       ["COM)B",
        "B)C",
        "C)D",
        "D)E",
        "E)F",
        "B)G",
        "G)H",
        "D)I",
        "E)J",
        "J)K",
        "K)L"] => 42
    }
    d.each{ |input, result|
      map = build_map(input)
      assert_equal result, total_orbit_count(map), "for input %s" % input
    }
  end

Part 2

For part two I thought about how to implement this with my one-way graph implementation that only allows me to go from satellite to its center of orbit. My approach for this explicit problem was to just start with me and walk through my orbital path back to the center of mass. For each orbit I pass I check if this is also on santas way to the COM. If there is an overlap, that is the intermediate destination I need to go to before following SANta.

The actual result is calcualted by using array operations to identify the intersection of mine and santas path. Using the .each_with_index array operation in Ruby it is easy to get the index (count) of array elements that are in between me and the common orbit.

def part2(map_data)
  orbit_map = build_map(map_data)
  target_center = orbit_map["SAN"] # the object we want to orbit as well
  santas_orbits = get_orbit_centers(orbit_map, "SAN")
  my_orbits = get_orbit_centers(orbit_map, "YOU")
  first_common_center = my_orbits.each_with_index.find{|(center, ix)| santas_orbits.include?(center)}
  my_hops_to_common_center = first_common_center[1] # the index of the array
  hops_from_common_to_santa = santas_orbits.each_with_index.find{|(center, ix)| center == first_common_center[0]}[1]
  my_hops_to_common_center + hops_from_common_to_santa
end

Solution

To run the solution use ruby solution.py

#!/usr/bin/ruby
# the orbit map points from satellite to its orbit center
def build_map(map_data)
  map = Hash.new()
  for entry in map_data
    parts = entry.split(")")
    center = parts[0]
    satellite = parts[1]
    map[satellite] = center
  end
  map
end
# calculate how many orbits a single satellite has in relation to the center of mass
def orbit_count(orbit_map, satellite)
  count = 0
  while satellite = orbit_map[satellite]
    count += 1
  end
  count
end
# total sum of orbits of all satellites in the system
def total_orbit_count(orbit_map)
  count = 0
  orbit_map.each{|satellite, _|
    central_objects = orbit_count(orbit_map, satellite)
    # puts "Satellite %s orbits %d objects" % [satellite, central_objects]
    count += central_objects
  }
  count
end
def part1(map_data)
  map = build_map(map_data)
  total_orbit_count(map)
end

def get_orbit_centers(orbit_map, satellite)
  centers = []
  while satellite = orbit_map[satellite]
    centers.push(satellite)
  end
  centers
end
# tag::p2[]
def part2(map_data)
  orbit_map = build_map(map_data)
  target_center = orbit_map["SAN"] # the object we want to orbit as well
  santas_orbits = get_orbit_centers(orbit_map, "SAN")
  my_orbits = get_orbit_centers(orbit_map, "YOU")
  first_common_center = my_orbits.each_with_index.find{|(center, ix)| santas_orbits.include?(center)}
  my_hops_to_common_center = first_common_center[1] # the index of the array
  hops_from_common_to_santa = santas_orbits.each_with_index.find{|(center, ix)| center == first_common_center[0]}[1]
  my_hops_to_common_center + hops_from_common_to_santa
end
# end::p2[]
if caller.length == 0
  input = File.read("./input.txt")

  data = input.split("\n")
  puts "Solution for part1: %d" % part1(data)
  puts "Solution for part2: %d" % part2(data)
end
Test cases
require "test/unit"
require_relative './solution'

class TestSolution < Test::Unit::TestCase
  def test_build_map
    d = {
      ["COM)B",
        "B)C",
        "C)D"] => { "D"=>"C", "C"=>"B", "B"=>"COM"},
        ["COM)B",
          "B)C",
          "C)D",
          "B)E"] => { "E"=>"B", "D"=>"C", "C"=>"B", "B"=>"COM"}
    }
    d.each{ |input, result|
      assert_equal result, build_map(input), "for input %s" % input
    }
  end
  def test_orbit_count
    map_data = ["COM)B",
      "B)C",
      "C)D",
      "D)E",
      "E)F",
      "B)G",
      "G)H",
      "D)I",
      "E)J",
      "J)K",
      "K)L"]
    d = {
      "B" => 1,
      "D" => 3,
      "L" => 7,
      "COM" => 0
    }
    map = build_map(map_data)
    d.each{ |input, result|
      assert_equal result, orbit_count(map, input), "for input %s" % input
    }
  end
  # tag::full_tdd[]
  def test_total_orbit_count
    d = {
       ["COM)B",
        "B)C",
        "C)D",
        "D)E",
        "E)F",
        "B)G",
        "G)H",
        "D)I",
        "E)J",
        "J)K",
        "K)L"] => 42
    }
    d.each{ |input, result|
      map = build_map(input)
      assert_equal result, total_orbit_count(map), "for input %s" % input
    }
  end
  # end::full_tdd[]
end