From 521db2b2541ec5fb87144c8e9d8b4dadf3321787 Mon Sep 17 00:00:00 2001 From: Philip Corliss Date: Thu, 30 Nov 2023 23:50:34 -0600 Subject: [PATCH] 2023 Init --- 2023/.ruby-gemset | 1 + 2023/.ruby-version | 1 + 2023/blank/Guardfile | 17 ++ 2023/blank/README.md | 0 2023/blank/blank.rb | 18 ++ 2023/blank/input.txt | 0 2023/blank/run.rb | 7 + 2023/blank/spec/blank_spec.rb | 21 ++ 2023/daily.sh | 21 ++ 2023/lib/grid.rb | 316 +++++++++++++++++++++++++++++ 2023/lib/ring.rb | 116 +++++++++++ 2023/spec/grid_spec.rb | 371 ++++++++++++++++++++++++++++++++++ 2023/spec/ring_spec.rb | 146 +++++++++++++ 13 files changed, 1035 insertions(+) create mode 100644 2023/.ruby-gemset create mode 100644 2023/.ruby-version create mode 100644 2023/blank/Guardfile create mode 100644 2023/blank/README.md create mode 100644 2023/blank/blank.rb create mode 100644 2023/blank/input.txt create mode 100755 2023/blank/run.rb create mode 100644 2023/blank/spec/blank_spec.rb create mode 100755 2023/daily.sh create mode 100644 2023/lib/grid.rb create mode 100644 2023/lib/ring.rb create mode 100644 2023/spec/grid_spec.rb create mode 100644 2023/spec/ring_spec.rb diff --git a/2023/.ruby-gemset b/2023/.ruby-gemset new file mode 100644 index 0000000..487ed64 --- /dev/null +++ b/2023/.ruby-gemset @@ -0,0 +1 @@ +advent_of_code diff --git a/2023/.ruby-version b/2023/.ruby-version new file mode 100644 index 0000000..be94e6f --- /dev/null +++ b/2023/.ruby-version @@ -0,0 +1 @@ +3.2.2 diff --git a/2023/blank/Guardfile b/2023/blank/Guardfile new file mode 100644 index 0000000..77abec9 --- /dev/null +++ b/2023/blank/Guardfile @@ -0,0 +1,17 @@ +#guard :shell do +# watch(%r{^*\.rb}) { `bundle exec rspec --force-color -f doc spec/ ` } +#end + +guard 'rspec', cmd: 'bundle exec rspec --force-color -f doc spec/', :all_on_start => true do + watch(%r{^([^/]+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^spec/.*_spec\.rb$}) +end + +notification :tmux, + display_message: true, + timeout: 5, # in seconds + default_message_format: '%s >> %s', + success: "green", + failed: "red", + pending: "yellow", + default: "green" diff --git a/2023/blank/README.md b/2023/blank/README.md new file mode 100644 index 0000000..e69de29 diff --git a/2023/blank/blank.rb b/2023/blank/blank.rb new file mode 100644 index 0000000..c21f967 --- /dev/null +++ b/2023/blank/blank.rb @@ -0,0 +1,18 @@ +require 'set' +require '../lib/grid.rb' +require '../lib/ring.rb' + +module Advent + + class Blank + attr_accessor :debug + + def initialize(input) + @debug = false + end + + def debug! + @debug = true + end + end +end diff --git a/2023/blank/input.txt b/2023/blank/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/2023/blank/run.rb b/2023/blank/run.rb new file mode 100755 index 0000000..3f98931 --- /dev/null +++ b/2023/blank/run.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require_relative 'blank' + +input = File.read('./input.txt') + +ad = Advent::Blank.new(input) diff --git a/2023/blank/spec/blank_spec.rb b/2023/blank/spec/blank_spec.rb new file mode 100644 index 0000000..d85d2da --- /dev/null +++ b/2023/blank/spec/blank_spec.rb @@ -0,0 +1,21 @@ +require './blank.rb' +require 'rspec' +require 'pry' + +describe Advent do + + let(:input) { + <<~EOS + EOS + } + + describe Advent::Blank do + let(:ad) { Advent::Blank.new(input) } + + describe "#new" do + end + + context "validation" do + end + end +end diff --git a/2023/daily.sh b/2023/daily.sh new file mode 100755 index 0000000..169e3fe --- /dev/null +++ b/2023/daily.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail +set -o verbose + +NAME=$1 +DATE=$(date "+%d") +DAY=${2:-$DATE} +NAME="$(tr '[:lower:]' '[:upper:]' <<< ${NAME:0:1})${NAME:1}" +FNAME=$(echo ${NAME} | tr '[:upper:]' '[:lower:]') + +cp -pr blank ${DAY} +pushd ${DAY} +find . | grep rb | xargs gsed -i "s/blank/${FNAME}/g" +find . | grep rb | xargs gsed -i "s/Blank/${NAME}/g" +mv blank.rb ${FNAME}.rb +mv spec/blank_spec.rb spec/${FNAME}_spec.rb +touch input.txt +popd diff --git a/2023/lib/grid.rb b/2023/lib/grid.rb new file mode 100644 index 0000000..07ac57e --- /dev/null +++ b/2023/lib/grid.rb @@ -0,0 +1,316 @@ +class Grid + attr_accessor :width, :height + attr_accessor :cells, :pos + + X = 0 + Y = 1 + + def initialize(init = nil, width = nil, height = nil) + @debug = false + @cells = {} + @pos = [0,0] + if init && width + @width = width + init.each_with_index do |val, idx| + x = idx % width + y = idx / width + @cells[[x, y]] = val + end + @height = height + end + if init.is_a?(String) && width.nil? + @width = init.lines.first.chomp.length + y = 0 + init.lines do |line| + x = 0 + line.chomp.chars.each do |val| + @cells[[x,y]] = val + x += 1 + end + y += 1 + end + @height = y + end + end + + def debug! + @debug = true + end + + def cell_direction + directions = {} + @cells.each do |cell_a, val_a| + @cells.each do |cell_b, val_b| + next if cell_a == cell_b + x = cell_a[X] - cell_b[X] + y = cell_a[Y] - cell_b[Y] + atan = Math.atan2(x, y) + + directions[cell_a] ||= {} + directions[cell_a][atan] ||= [] + directions[cell_a][atan] << cell_b + end + end + directions + end + + def render(pad = 0, box_start = nil, box_end = nil) + min_y, max_y = @cells.keys.map(&:last).minmax + min_x, max_x = @cells.keys.map(&:first).minmax + + min_x, min_y = box_start if box_start + max_x, max_y = box_end if box_end + + max_val_width = @cells.values.map(&:to_s).map(&:length).max + max_val_width += pad + + puts "Grid: #{@cells}" if @debug + puts "Cells: #{@cells.keys}" if @debug + puts "[#{min_x},#{min_y}] -> [#{max_x},#{max_y}]" if @debug + acc = "" + (min_y..max_y).each do |y| + (min_x..max_x).each do |x| + cell = @cells[[x,y]] || ' ' + str = cell.to_s + padding = ' '*(max_val_width - str.length) + acc << padding + acc << str + end + acc << "\n" + end + acc.chomp! + acc + end + + def draw!(direction, distance, val = true, operator = nil) + x, y = direction + distance.times do |i| + new_pos = [@pos[X] + x, @pos[Y] + y] + if @cells[new_pos].nil? + @cells[new_pos] = val + else + if operator + @cells[new_pos] = @cells[new_pos].__send__(operator, val) + else + @cells[new_pos] = val + end + end + @pos = new_pos + end + end + + def trace!(direction, distance, val) + x, y = direction + counter = val + distance.times do |i| + counter = val + i + 1 + new_pos = [@pos[X] + x, @pos[Y] + y] + @cells[new_pos] ||= 0 + @cells[new_pos] += counter + @pos = new_pos + end + counter + end + + def box!(start, finish, val = nil, operator = nil) + x_start, y_start = start + x_finish, y_finish = finish + + (x_start..x_finish).each do |x| + (y_start..y_finish).each do |y| + new_pos = [x,y] + if operator + @cells[new_pos] ||= 0 + @cells[new_pos] = @cells[new_pos].__send__(operator, val) + elsif val.nil? + @cells[new_pos] = yield(@cells[new_pos]) + else + @cells[new_pos] = val + end + end + end + end + + CARDINAL_DIRECTIONS = [ + [-1, 0], # West + [ 1, 0], # East + [ 0,-1], # North + [ 0, 1], # South + ] + + DIAGONAL_DIRECTIONS = [ + [-1, 0], # West + [ 1, 0], # East + [ 0,-1], # North + [ 0, 1], # South + [-1,-1], # NorthWest + [ 1,-1], # NorthEast + [-1, 1], # SouthWest + [ 1, 1], # SouthEast + ] + + def neighbor_coords(cell, diagonals = false) + directions = CARDINAL_DIRECTIONS + directions = DIAGONAL_DIRECTIONS if diagonals + directions.map do |dir| + [cell[X] + dir[X], cell[Y] + dir[Y]] + end + end + + def neighbors(cell, diagonals = false) + transformed = neighbor_coords(cell, diagonals) + @cells.slice(*transformed) + end + + def get(pos_x, pos_y = nil) + pos_x, pos_y = pos_x if pos_x.is_a? Array + @cells[[pos_x, pos_y]] + end + + def [](pos_x, pos_y = nil) + get(pos_x, pos_y) + end + + def []=(pos_x, pos_y = nil, val=nil) + if pos_x.is_a? Array + val = pos_y + pos_x, pos_y = pos_x + end + @cells[[pos_x, pos_y]] = val + end + + def find(val) + cell = @cells.find do |cell, v| + v == val + end + cell.first unless cell.nil? + end + + def find_all(val) + cells = @cells.find_all do |cell, v| + v == val + end + cells.map(&:first) + end + + def rotate + grid = Grid.new + grid.height = @height + grid.width = @width + i = 0 + @width.times do |x| + (@height - 1).downto(0) do |y| + grid[(i % @width),(i / @width)] = @cells[[x,y]] + i += 1 + end + end + grid + end + + def flip(vertically: false) + grid = Grid.new + grid.height = @height + grid.width = @width + i = 0 + if vertically + (@height - 1).downto(0) do |y| + @width.times do |x| + grid[(i % @width),(i / @width)] = @cells[[x,y]] + i += 1 + end + end + else + @height.times do |y| + (@width - 1).downto(0) do |x| + grid[(i % @width),(i / @width)] = @cells[[x,y]] + i += 1 + end + end + end + grid + end + + def split(dim) + grids = [] + (@height / dim).times do |section_y| + (@width / dim).times do |section_x| + g = Grid.new + g.width = dim + g.height = dim + grids << g + + min_x = section_x * dim + max_x = min_x + dim - 1 + min_y = section_y * dim + max_y = min_y + dim - 1 + + i = 0 + (min_y..max_y).each do |y| + (min_x..max_x).each do |x| + g[(i % dim),(i / dim)] = @cells[[x,y]] + i += 1 + end + end + end + end + grids + end + + def self.join(grids) + dim = Math.sqrt(grids.length).to_i + g = Grid.new + sub_grid_w = grids.first.width + g.width = dim * sub_grid_w + g.height = g.width + grids.each_with_index do |grid, idx| + x_offset = (idx % dim) * sub_grid_w + y_offset = (idx / dim) * sub_grid_w + grid.cells.each do |cell, val| + x, y = cell + g[x + x_offset, y + y_offset] = val + end + end + g + end + + # Sourced From: https://github.com/bsoyka/advent-of-code-ocr/blob/main/advent_of_code_ocr/characters.py + ALPHABET_6 = { + ".##.\n#..#\n#..#\n####\n#..#\n#..#" => "A", + "###.\n#..#\n###.\n#..#\n#..#\n###." => "B", + ".##.\n#..#\n#...\n#...\n#..#\n.##." => "C", + "####\n#...\n###.\n#...\n#...\n####" => "E", + "####\n#...\n###.\n#...\n#...\n#..." => "F", + ".##.\n#..#\n#...\n#.##\n#..#\n.###" => "G", + "#..#\n#..#\n####\n#..#\n#..#\n#..#" => "H", + ".###\n..#.\n..#.\n..#.\n..#.\n.###" => "I", + "..##\n...#\n...#\n...#\n#..#\n.##." => "J", + "#..#\n#.#.\n##..\n#.#.\n#.#.\n#..#" => "K", + "#...\n#...\n#...\n#...\n#...\n####" => "L", + ".##.\n#..#\n#..#\n#..#\n#..#\n.##." => "O", + "###.\n#..#\n#..#\n###.\n#...\n#..." => "P", + "###.\n#..#\n#..#\n###.\n#.#.\n#..#" => "R", + ".###\n#...\n#...\n.##.\n...#\n###." => "S", + "#..#\n#..#\n#..#\n#..#\n#..#\n.##." => "U", + "#...\n#...\n.#.#\n..#.\n..#.\n..#." => "Y", + "####\n...#\n..#.\n.#..\n#...\n####" => "Z", + } + + def ocr + i = 0 + return_str = "" + while i < @width do + char = ALPHABET_6[render(0,[i,0],[i+3,5])] + if char + return_str << char + i += 3 + end + i += 1 + end + + return_str + end + + def highlight + render.gsub('#', '█').gsub('.',' ') + end +end diff --git a/2023/lib/ring.rb b/2023/lib/ring.rb new file mode 100644 index 0000000..a69d8f5 --- /dev/null +++ b/2023/lib/ring.rb @@ -0,0 +1,116 @@ +module Advent + class CircularLinkedList + attr_reader :length + + def initialize(values) + prev_node = nil + @nodes = values.map do |val| + new_node = Node.new(val, nil, prev_node) + prev_node.next = new_node if prev_node + prev_node = new_node + end + prev_node.next = @nodes.first + @nodes.first.prev = prev_node + @length = @nodes.length + @first = @nodes.first + end + + # There's a bug here where the first node can be destroyed + def first + @first + end + + def destroy(node) + prev_node = node.prev + next_node = node.next + prev_node.next = next_node + next_node.prev = prev_node + @length -= 1 + @first = node.next if node == @first + end + + def add(node, val) + n = nil + if val.is_a? Node + n = val + n.next = node.next + n.prev = node + else + n = Node.new(val, node.next, node) + end + @nodes << n + @length += 1 + node.next.prev = n + node.next = n + end + + def render(node = @nodes.first, x = @length) + to_a(node, x).join('->') + end + + def to_a(node = @nodes.first, x = @length) + nodes(node, x).map(&:val) + end + + def nodes(node = @nodes.first, x = @length) + n = node + x.times.map do |i| + p = n + n = n.next + p + end + end + + def shift(node, amount) + return if amount == 0 + destroy(node) + n = node.prev # 0 + + if amount >= 0 + amount.times { n = n.next } # 2, 3 + else + amount.abs.times { n = n.prev } # 4, 3, 2 + end + + add(n, node) + end + + def reverse(node, length) + return if length <= 1 + # This node is before the reversing and needs it's next updated # to the last node + pre_node = node.prev + # This node is after the last node reversed and needs it's prev updated to the first node + pos_node = nil + first_node = node + n = node + last_node = nil + + length.times do |i| + tmp = n.next + n.next = n.prev + n.prev = tmp + + last_node = n + n = n.prev + pos_node = n + end + + if length < @length + pos_node.prev = first_node + first_node.next = pos_node + + last_node.prev = pre_node + pre_node.next = last_node + end + end + + class Node + attr_accessor :val, :next, :prev + def initialize(val, n = nil, p = nil) + @val = val + @next = n + @prev = p + end + end + end +end diff --git a/2023/spec/grid_spec.rb b/2023/spec/grid_spec.rb new file mode 100644 index 0000000..29fc53c --- /dev/null +++ b/2023/spec/grid_spec.rb @@ -0,0 +1,371 @@ +require 'rspec' +require 'pry' +require_relative '../lib/grid.rb' + +describe Grid do + let (:grid) { Grid.new } + + describe "#new" do + it "instantiates an empty infinite grid" do + expect(grid).to be_a(Grid) + expect(grid.cells).to be_empty + end + + it "instnatiates a filled grid given an array and a width" do + grid = Grid.new(4.times.to_a, 2) + expect(grid.width).to eq(2) + expect(grid.cells).to eq({ + [0,0] => 0, + [1,0] => 1, + [0,1] => 2, + [1,1] => 3, + }) + end + + it "instantiates a filled grid using a string with newlines" do + grid = Grid.new( +<<~EOS +1##2 +# # +# # +3##4 + EOS +) + expect(grid.width).to eq(4) + expect(grid.height).to eq(4) + expect(grid.cells).to include({ + [0,0] => '1', + [3,0] => '2', + [0,3] => '3', + [3,3] => '4', + }) + end + + it "instantiates an empty grid using width and height" do + grid = Grid.new([], 10, 11) + expect(grid.width).to eq(10) + expect(grid.height).to eq(11) + end + end + + describe "#cell_direction" do + it "returns a hash of all the unique angles to all other cells" do + grid.cells = { + [1,0] => true, + [0,2] => true, + [1,1] => true, + [1,2] => true, + [2,2] => true, + } + expect(grid.cell_direction[[1,0]].count).to eq(3) + expect(grid.cell_direction[[0,2]].count).to eq(3) + end + end + + describe "#draw!" do + it "takes a direction, distance and value and marks cells along the route" do + grid.draw!([0,1], 4, 1) + expect(grid.pos).to eq([0,4]) + expect(grid.cells.count).to eq(4) + expect(grid.cells.values.uniq).to eq([1]) + expect(grid.cells.keys).to contain_exactly([0,1],[0,2],[0,3],[0,4]) + end + + it "takes an operator for manipulating values" do + grid.draw!([0,1], 4, 1, :+) + grid.draw!([0,-1], 4, 2, :+) + # Doesn't set origin point so different values will appear + expect(grid.cells.values).to include(3, 1, 2) + end + end + + describe "#trace!" do + it "takes a direction, distance and value and marks cells along the route with increasing values" do + grid.trace!([0,1], 3, 3) + expect(grid.pos).to eq([0,3]) + expect(grid.cells.count).to eq(3) + expect(grid.cells.values).to eq([4,5,6]) + end + end + + describe "#box!" do + it "takes a beginning end and val" do + grid.box!([1,1],[2,2],5) + expect(grid.cells).to eq({ + [1,1] => 5, + [1,2] => 5, + [2,1] => 5, + [2,2] => 5, + }) + end + + it "takes an operator" do + grid.box!([1,1],[2,2],-5,:-) + expect(grid.cells).to eq({ + [1,1] => 5, + [1,2] => 5, + [2,1] => 5, + [2,2] => 5, + }) + end + + it "takes a block" do + grid.box!([1,1],[2,2]) do |val| + val ||= 0 + val += 5 + end + expect(grid.cells).to eq({ + [1,1] => 5, + [1,2] => 5, + [2,1] => 5, + [2,2] => 5, + }) + end + end + + describe "#render" do + let(:cells) {{ + [0,0] => 0, + [1,0] => 1, + [0,1] => 2, + [1,1] => 3, + }} + it "returns a string suitable for printing" do + grid.cells = cells + grid.width = 2 + expect(grid.render).to eq("01\n23") + end + + it "finds the max width and adds spacing" do + grid.cells = cells + grid[1,1] = 999 + grid.width = 2 + expect(grid.render).to eq(" 0 1\n 2999") + end + + it "renders padding" do + grid.cells = cells + grid.width = 2 + expect(grid.render(1)).to eq(" 0 1\n 2 3") + end + + it "takes an optional bounding box" do + grid.cells = { + [0,0] => 0, + [1,0] => 1, + [2,0] => 2, + [0,1] => 3, + [1,1] => 4, + [2,1] => 5, + [0,2] => 6, + [1,2] => 7, + [2,2] => 8, + } + grid.width = 3 + expect(grid.render(0, [0,0], [1,1])).to eq("01\n34") + end + end + + describe "#neighbors" do + it "returns a subhash containing cardinal direction neighbors" do + grid = Grid.new(9.times.to_a, 3) + expect(grid.neighbors([1,1])).to eq({ + [1,0] => 1, + [0,1] => 3, + [2,1] => 5, + [1,2] => 7, + }) + end + + it "takes an optional argument to handle diagonals as well as cardinal direction neigbors" do + grid = Grid.new(9.times.to_a, 3) + expect(grid.neighbors([1,1], true)).to eq({ + [0,0] => 0, + [1,0] => 1, + [2,0] => 2, + [0,1] => 3, + [2,1] => 5, + [0,2] => 6, + [1,2] => 7, + [2,2] => 8, + }) + end + end + + describe "#neighbor_coords" do + it "returns just the coordinates of the cardinal direction neighbors" do + grid = Grid.new + expect(grid.neighbor_coords([1,1])).to contain_exactly( + [0,1], [2,1], [1,0], [1,2], + ) + end + + it "takes an optional argument to consider diagonals" do + grid = Grid.new + expect(grid.neighbor_coords([1,1], true)).to contain_exactly( + [0,0], [1,0], [2,0], [0,1], + [2,1], [0,2], [1,2], [2,2], + ) + end + end + + describe "#get" do + before do + grid.cells[[1,1]] = true + end + + it "it gets a value from cells using an array" do + expect(grid.get([1,1])).to be_truthy + end + + it "gets a value from cells using params" do + expect(grid.get(1,1)).to be_truthy + end + + it "gets a value from cells using array getter ([])" do + expect(grid[1,1]).to be_truthy + expect(grid[[1,1]]).to be_truthy + end + end + + describe "#set" do + it "sets a value from cells using array setter ([])" do + grid[1,1] = 7 + grid[[2,2]] = 13 + + expect(grid[1,1]).to eq(7) + expect(grid[2,2]).to eq(13) + end + end + + describe "#find" do + it "returns nil if it can't find anything" do + expect(grid.find('@')).to be_nil + end + + it "returns a single element if it finds a value match" do + grid[1,1] = '@' + expect(grid.find('@')).to eq([1,1]) + end + end + + describe "#find_all" do + it "returns an empty array if it can't find anything" do + expect(grid.find_all('@')).to be_empty + end + + it "returns an array of matching values" do + grid[1,1] = '@' + grid[2,2] = '@' + expect(grid.find_all('@')).to contain_exactly([1,1], [2,2]) + end + end + + describe "#rotate" do + let(:grid) { Grid.new(9.times.to_a, 3, 3) } + + it "rotates the grid 90 degrees" do + new_grid = grid.rotate + expect(new_grid.render).to eq("630\n741\n852") + end + + it "rotates 360 degrees" do + new_grid = grid.rotate.rotate.rotate.rotate + expect(new_grid.cells).to eq(grid.cells) + end + end + + describe "#flip" do + let(:grid) { Grid.new(9.times.to_a, 3, 3) } + + it "flips the grid horizontally" do + new_grid = grid.flip + expect(new_grid.render).to eq("210\n543\n876") + end + + it "reproduces the original grid after two flips" do + new_grid = grid.flip.flip + expect(new_grid.cells).to eq(grid.cells) + end + + it "flips the grid vertically" do + new_grid = grid.flip(vertically: true) + expect(new_grid.render).to eq("678\n345\n012") + end + + it "reproduces the original grid after two vertical flips" do + new_grid = grid.flip(vertically: true).flip(vertically: true) + expect(new_grid.cells).to eq(grid.cells) + end + end + + describe "#split" do + it "returns 4 grids" do + grid = Grid.new(16.times.to_a, 4, 4) + expect(grid.split(2).count).to eq(4) + expect(grid.split(2).map(&:render)).to eq([ + "01\n45", + "23\n67", + " 8 9\n1213", + "1011\n1415", + ]) + end + + it "returns 9 grids" do + grid = Grid.new(81.times.to_a, 9, 9) + expect(grid.split(3).count).to eq(9) + expect(grid.split(3).first.render).to eq(" 0 1 2\n 91011\n181920") + end + end + + describe "#self.join" do + it "joins multiple grids" do + grid = Grid.new(81.times.to_a, 9, 9) + joined_grid = Grid.join(grid.split(3)) + expect(joined_grid.cells).to eq(grid.cells) + expect(joined_grid.width).to eq(9) + expect(joined_grid.height).to eq(9) + end + + it "handles non-perfect square dimensions" do + grid = Grid.new(36.times.to_a, 6, 6) + split_grid = grid.split(3) + expect(split_grid.count).to eq(4) + joined_grid = Grid.join(split_grid) + expect(joined_grid.cells).to eq(grid.cells) + expect(joined_grid.width).to eq(6) + expect(joined_grid.height).to eq(6) + end + end + + describe "#ocr" do + let(:grid) { Grid.new( + <<~EOS +####.####.###..###..###..####.####.####. +#.......#.#..#.#..#.#..#.#.......#.#.... +###....#..###..#..#.###..###....#..###.. +#.....#...#..#.###..#..#.#.....#...#.... +#....#....#..#.#....#..#.#....#....#.... +#....####.###..#....###..#....####.#.... + EOS + )} + + it "returns the string equivalent" do + expect(grid.ocr).to eq('FZBPBFZF') + end + end + + describe "#highlight" do + let(:grid) { Grid.new( + <<~EOS +### +#.. +#.# + EOS + )} + + it "renders but replaces '#' with '█' and '.' with whitespace" do + expect(grid.highlight).to eq("███\n█ \n█ █") + end + end +end diff --git a/2023/spec/ring_spec.rb b/2023/spec/ring_spec.rb new file mode 100644 index 0000000..5de7c2b --- /dev/null +++ b/2023/spec/ring_spec.rb @@ -0,0 +1,146 @@ +require 'rspec' +require 'pry' +require_relative '../lib/ring.rb' + +describe Advent do + describe Advent::CircularLinkedList do + let(:list) { Advent::CircularLinkedList.new(5.times) } + let(:node) { list.first } + + describe "#new" do + it "inits a linked list with the passed values" do + expect(node.val).to eq(0) + end + + it "links to the next element in the list" do + expect(node.next.val).to eq(1) + end + + it "the last element links back to first element" do + expect(node.next.next.next.next.next.next.val).to eq(1) + end + + it "the first element links back to the last element" do + expect(node.prev.val).to eq(4) + end + + it "maintains a length" do + expect(list.length).to eq(5) + end + end + + describe "#first" do + it "returns the first element" do + expect(list.first.val).to eq(0) + end + + it "returns the next element if the first element is destroyed" do + list.destroy(list.first) + expect(list.first.val).to eq(1) + end + end + + describe "#destroy" do + it "resets the next element" do + prev_node = node + to_destroy = node.next + next_node = node.next.next + list.destroy(to_destroy) + expect(prev_node.next).to eq(next_node) + end + + it "resets the prev element" do + prev_node = node + to_destroy = node.next + next_node = node.next.next + list.destroy(to_destroy) + expect(next_node.prev).to eq(prev_node) + end + + it "decrements the length" do + list.destroy(node) + expect(list.length).to eq(4) + end + end + + describe "#to_a" do + it "returns the list as an array" do + expect(list.to_a).to eq(5.times.to_a) + end + end + + describe "#add" do + it "adds a new node based on the passed start and value" do + list.add(node.next, 99) + expect(list.to_a).to eq([0, 1, 99, 2, 3, 4]) + expect(list.length).to eq(6) + expect(node.next.next.val).to eq(99) + expect(node.next.next.next.val).to eq(2) + expect(node.next.next.next.prev.val).to eq(99) + end + + it "allows adding a node value back in" do + one = list.first.next + node.val = 99 + list.destroy(node) + list.add(node.next, node) + expect(list.to_a(one)).to eq([1, 99, 2, 3, 4]) + end + end + + describe "#nodes" do + it "returns the list as an array of nodes" do + expect(list.nodes.map(&:val)).to eq(5.times.to_a) + end + end + + describe "#shift" do + it "shifts forward" do + list.shift(node.next, 2) + expect(list.to_a).to eq([0, 2, 3, 1, 4]) + end + + it "shifts backwards" do + list.shift(node.next, -3) + expect(list.to_a).to eq([0, 2, 1, 3, 4]) + end + end + + describe "#reverse" do + it "reverses a section of the linked list" do + list.reverse(node, 3) # Node 0 and 3 total elements (0, 1, 2) + # [0, 1, 2, 3, 4] becomes [2, 1, 0, 3, 4] + expect(list.to_a).to eq([0, 3, 4, 2, 1]) + expect(node.next.val).to eq(3) + expect(node.prev.val).to eq(1) + expect(node.prev.prev.prev.val).to eq(4) + expect(node.next.next.next.val).to eq(2) + end + + it "handles reversing the entire list" do + list.reverse(node, 5) + expect(list.to_a).to eq([0, 4, 3, 2, 1]) + expect(node.next.val).to eq(4) + expect(node.prev.val).to eq(1) + expect(node.next.prev).to eq(node) + expect(node.prev.next).to eq(node) + end + + [ + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 2, 3, 4, 1], + [0, 3, 4, 2, 1], + [0, 4, 3, 2, 1], + [0, 4, 3, 2, 1], + ].each_with_index do |expected, count| + it "reverses with a count of #{count}" do + list.reverse(node, count) + expect(list.to_a).to eq(expected) + expect(node.next.prev).to eq(node) + expect(node.prev.next).to eq(node) + end + end + end + end +end