The Crystal Programming Language Forum

Advent of code 2019

Hi,

Just a reminder that Advent of code has started for 2019.
If you don’t know it, it is a coding advent calendar which has a small programming task every day until christmas.

They usually start simple, but get really hard towards the end. (at least for me :) )

Its still fun to do, and the tasks are usually well and funny written.

https://adventofcode.com/

3 Likes

It is, it’s great! And it’s a very good way to learn a language or keep learning it.

I personally do it every year because I like these relaxed challenges, and also to see if Crystal can handle all these problems. Many Enumerable methods were added to the std after doing some of these problems, and I think some improvements or bug fixes too.

We can also post our Crystal solutions here to compare and learn from each other.

Here’s my Day 1 solution:

puts File
  .read("#{__DIR__}/input.txt")
  .each_line
  .map(&.to_i)
  .map { |mass| fuel(mass) }
  .sum

# This is for part 2: for part 1 it's just `mass // 3 - 2`
def fuel(mass)
  fuel = mass // 3 - 2
  fuel += fuel(fuel) if fuel > 0
  fuel
end
3 Likes

Did not know about the // operator! I used Float64#trunc, but that one is cleaner.

1 Like

Awesome, I will also be doing these to learn more about Crystal! (Probably just some of the first ones, because they can get really difficult towards the end.)

Sharing the solutions is a very good idea, I’m curious how much better others can make these :slight_smile:

This is what I came up for the second half of day one:

def calc_fuel(mass)
  (mass // 3) - 2
end

def fuel_for(mass)
  fuel_needed = calc_fuel(mass)
  if fuel_needed < 1
    return mass
  else
    return mass + fuel_for(fuel_needed)
  end
end

input = File.read("input1")

masses = [] of Int64
input.each_line do |line|
  masses << line.to_i64
end

modules_fuel = masses.reduce(0) do |memo, m|
  memo + calc_fuel(m)
end
puts "fuel for modules : " + modules_fuel.to_s

total_fuel = masses.reduce(0) do |memo, m|
  memo + fuel_for(calc_fuel(m))
end
puts "fuel for modules with fuel for fuel: " + total_fuel.to_s

You should avoid propagating File.open.each_line and map.sum. People probably look at your code as a reference for good practice ;)

2 Likes

It’s a good observation, but in a script like this leaking a file descriptor is no problem at all. That’s why it’s good to be able to have the possibility to do this in these scenarios. Also, I still think we should GC file descriptors once we run out of them.

But yeah, I guess I’ll change it to read

You can still use File.open &. instead of File.open. :wink:

1 Like

Totally agree. But it depends on the context and that’s not obvious without explanation.

Somehow my result for part 2 of day 1 is not correct. I ran @asterite’s algorithm from above and it returns the same value as mine.

Can someone confirm it returns the correct value for their input?

INPUT = File.read_lines(File.join(__DIR__, "../input/day1.txt")).map(&.to_i)

def fuel(mass)
  fuel = mass // 3 - 2
  if fuel > 0
    fuel += fuel(fuel)
  end
  fuel
end

total_mass = INPUT.sum do |mass|
  fuel(mass)
end

p total_mass

Since the first part’s answer is correct, I assume it’s not an issue with the input data.

For my input the result is wrong.
I get

modules with fuel : 4946546

and yours outputs 4946421

My solution was

require "./aoc2019"

module Day1
  include Day
  extend self

  def to_list(input)
    input.chomp.split(/[,\n]/).map(&.chomp).map(&.to_i)
  end

  def part1(input)
    to_list(input).sum { |x| x // 3 - 2 }
  end

  def part2(input)
    to_list(input).sum do |x|
      sum = 0
      loop do
        x = x // 3 - 2
        break if x <= 0
        sum += x
      end
      sum
    end
  end
end

You solution returns fuel = -1 for mass = 3, and should return 0.

1 Like

Great catch! I totally missed that. Thanks.
It seems, @asterite your’s, too =)

1 Like

Day 3 took me way too long.
I at first didn’t find the “&” function for Arrays. I was looking for something like exclusive union. Should just have looked at the simple things :slight_smile:

I would be interested in other solutions, because my plot function does not seem very elegant.

input = File.read("input3").split().map(&.split(","))

record Point, x : Int32 = 0, y : Int32 = 0

def distance(a : Point, b : Point)
  return ( a.x - b.x ).abs + ( a.y - b.y ).abs
end

class Plot
  property points = [] of Point
  property data = [] of String

  def initialize(@data)
    @points << Point.new
    plot
  end

  def steps_to_first(point)
    return @points.index(point)
  end

  def plot
    current = Point.new
    @data.each do |move|
      matches = move.match(/([UDRL])(\d*)/)
      if matches
        whole, dir, steps = matches
        steps = steps.to_i
        case dir
        when "U"
          (current.y + 1).step(to: current.y + steps) do |y|
            @points << Point.new( current.x, y )
          end
          current = Point.new(current.x, current.y + steps)
        when "D"
          (current.y - 1).step(to: current.y - steps, by: -1) do |y|
            @points << Point.new( current.x, y )
          end
          current = Point.new(current.x, current.y - steps)
        when "R"
          (current.x + 1).step(to: current.x + steps) do |x|
            @points << Point.new( x, current.y )
          end
          current = Point.new(current.x + steps, current.y)
        when "L"
          (current.x - 1).step(to: current.x - steps, by: -1) do |x|
            @points << Point.new( x, current.y )
          end
          current = Point.new(current.x - steps, current.y)
        end
      end
    end
  end

end

# exampe inputs 1 and 2
input1a = "R75,D30,R83,U83,L12,D49,R71,U7,L72".split(",")
input1b = "U62,R66,U55,R34,D71,R55,D58,R83".split(",")
input2a = "R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51".split(",")
input2b = "U98,R91,D20,R16,D67,R40,U7,R15,U6,R7".split(",")


a = Plot.new(input[0])
b = Plot.new(input[1])

result = a.points & b.points
result.delete Point.new #delete coordinate origin

distances = result.map do |point|
  distance(Point.new, point).abs
end

puts "shortest distance: #{distances.sort.first}"

record StepsToPoint, steps : Int32 = 0, point : Point = Point.new

fastest = result.map do |point|
  steps = a.points.index(point) || 0
  steps += b.points.index(point) || 0
  StepsToPoint.new( steps, point || Point.new )
end

fastest.sort_by!(&.steps)

puts "Fastest combined steps: #{ fastest.first.steps }"

Here’s mine for part 2.

I struggled a bit about finding a sweet spot between repetition and abstracting things away. For example, I had a macro at some point to define the methods in the Point struct as one-liners. Also tried ways to move the steps.times loops to one single place.

However, when comparing the listings, I was not convinced and the one below seemed the most easy to understand to me. Vertical alignment makes it easy to parse also.

struct Point
  def initialize(@x : Int32, @y : Int32)
  end

  def left
    @x -= 1; self
  end

  def right
    @x += 1; self
  end

  def up
    @y += 1; self
  end

  def down
    @y -= 1; self
  end
end

class Wire
  getter :points

  def initialize(path)
    @points = [Point.new(0, 0)]
    follow(path)
  end

  def follow(path)
    path.split(",").each do |move|
      steps = move[1..].to_i
      case move[0]
      when 'L' then steps.times { @points << points.last.left }
      when 'R' then steps.times { @points << points.last.right }
      when 'U' then steps.times { @points << points.last.up }
      when 'D' then steps.times { @points << points.last.down }
      end
    end
  end
end

def min_delay(path1, path2)
  wire1 = Wire.new(path1)
  wire2 = Wire.new(path2)

  (wire1.points[1..] & wire2.points[1..]).map do |point|
    wire1.points.index(point).not_nil! + wire2.points.index(point).not_nil!
  end.min
end

p 30 == min_delay("R8,U5,L5,D3", "U7,R6,D4,L4")
p 610 == min_delay("R75,D30,R83,U83,L12,D49,R71,U7,L72", "U62,R66,U55,R34,D71,R55,D58,R83")
p 410 == min_delay("R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51", "U98,R91,D20,R16,D67,R40,U7,R15,U6,R7")

path1 = "R1000,D940,L143,D182,L877,D709,L253,U248,L301,U434,R841,U715,R701,U92,R284,U115,R223,U702,R969,U184,L992,U47,L183,U474,L437,D769,L71,U96,R14,U503,R144,U432,R948,U96,L118,D696,R684,U539,L47,D851,L943,U606,L109,D884,R157,U946,R75,U702,L414,U347,R98,D517,L963,D177,R467,D142,L845,U427,R357,D528,L836,D222,L328,U504,R237,U99,L192,D147,L544,D466,R765,U845,L267,D217,L138,U182,R226,U466,R785,U989,R55,D822,L101,U292,R78,U962,R918,U218,L619,D324,L467,U885,L658,U890,L764,D747,R369,D930,L264,D916,L696,U698,R143,U537,L922,U131,R141,D97,L76,D883,R75,D657,R859,U503,R399,U33,L510,D318,L455,U128,R146,D645,L147,D651,L388,D338,L998,U321,L982,U150,R123,U834,R913,D200,L455,D479,L38,U860,L471,U945,L946,D365,L377,U816,R988,D597,R181,D253,R744,U472,L345,U495,L187,D443,R924,D536,R847,U430,L145,D827,L152,D831,L886,D597,R699,D751,R638,D580,L488,D566,L717,D220,L965,D587,L638,D880,L475,D165,L899,U388,R326,D568,R940,U550,R788,D76,L189,D641,R629,D383,L272,D840,L441,D709,L424,U158,L831,D576,R96,D401,R425,U525,L378,D907,L645,U609,L336,D232,L259,D280,L523,U938,R190,D9,L284,U941,L254,D657,R572,U443,L850,U508,L742,D661,L977,U910,L190,U626,R140,U762,L673,U741,R317,D518,R111,U28,R598,D403,R465,D684,R79,U725,L556,U302,L367,U306,R632,D550,R89,D292,R561,D84,L923,D109,L865,D880,L387,D24,R99,U934,L41,U29,L225,D12,L818,U696,R652,U327,L69,D773,L618,U803,L433,D467,R840,D281,R161,D400,R266,D67,L205,D94,R551,U332,R938,D759,L437,D515,L480,U774,L373,U478,R963,D863,L735,U138,L580,U72,L770,U968,L594"
path2 = "L990,D248,L833,U137,L556,U943,R599,U481,R963,U812,L825,U421,R998,D847,R377,D19,R588,D657,R197,D354,L548,U849,R30,D209,L745,U594,L168,U5,L357,D135,R94,D686,R965,U838,R192,U428,L861,U354,R653,U543,L633,D508,R655,U575,R709,D53,L801,D709,L92,U289,L466,D875,R75,D448,R576,D972,L77,U4,L267,D727,L3,D687,R743,D830,L803,D537,L180,U644,L204,U407,R866,U886,R560,D848,R507,U470,R38,D652,R806,D283,L836,D629,R347,D679,R609,D224,L131,D616,L687,U181,R539,D829,L598,D55,L806,U208,R886,U794,L268,D365,L145,U690,R50,D698,L140,D512,L551,U845,R351,U724,R405,D245,L324,U181,L824,U351,R223,D360,L687,D640,L653,U158,R786,D962,R931,D151,R939,D34,R610,U684,L694,D283,R402,D253,R388,D195,R732,U809,R246,D571,L820,U742,L507,U967,L886,D693,L273,U558,L914,D122,R146,U788,R83,U149,R241,U616,R326,U40,L192,D845,L577,U803,L668,D443,R705,D793,R443,D883,L715,U757,R767,D360,L289,D756,R696,D236,L525,U872,L332,U203,L152,D234,R559,U191,R340,U926,L746,D128,R867,D562,L100,U445,L489,D814,R921,D286,L378,D956,L36,D998,R158,D611,L493,U542,R932,U957,R55,D608,R790,D388,R414,U670,R845,D394,L572,D612,R842,U792,R959,U7,L285,U769,L410,D940,L319,D182,R42,D774,R758,D457,R10,U82,L861,D901,L310,D217,R644,U305,R92,U339,R252,U460,R609,D486,R553,D798,R809,U552,L183,D238,R138,D147,L343,D597,L670,U237,L878,U872,R789,U268,L97,D313,R22,U343,R907,D646,L36,D516,L808,U622,L927,D982,L810,D149,R390,U101,L565,U488,L588,U426,L386,U305,R503,U227,R969,U201,L698,D850,R800,D961,R387,U632,R543,D541,R750,D174,R543,D237,R487,D932,R220"

p min_delay(path1, path2)

@fxn Your solution is much nicer!

I think I like this. I’ll try to keep posting mine and hope to see better solutions from others.

Like day4, I like my part 1, but for part 2 I didn’t find a solution that produced a correct result
for cases like 566677.
I didn’t have much time and too tired now to pick it up again.
If anyone has a good solution for part 2, I’d be interested.

range_start,range_end = "356261-846303".split("-").map(&.to_i)

def walk_num(num, &block)
  str = num.to_s
  result = [] of Bool
  0.step(to: str.size - 2) do |i|
    result << yield(str[i],str[i+1])
  end
  return result
end

def check_twin_digits(num)
  result = walk_num(num) do |x,y|
    x == y
  end
   result.includes? true
end

def check_increasing_digits(num)
  result = walk_num(num) do |x,y|
    x <= y
  end
  return !( result.includes? false )
end


result = [] of Int32
range_start.step(to: range_end) do |num|
  if check_increasing_digits(num)
    if check_twin_digits(num)
      result << num
    end
  end
end

puts "part 1 solution: " + result.size.to_s
1 Like

yeah, double digits at last position are special case. But my solutions are mostly imperative, so I didn’t care much:

  def check2(number)
    digits = number.to_s.chars.map(&.ord)
    match = 0
    was_double = false
    digits.each_cons(2, reuse: true) do |(first, second)|
      return false if second < first
      if first == second
        match += 1
      else
        was_double = true if match == 1
        match = 0
      end
    end
    was_double || match == 1
  end

  def part2(input)
    v1, v2 = input.chomp.split('-').map(&.to_i)
    (v1..v2).count { |x| check2(x) }
  end

Similar solution here. Trailing double needed an inelegant for my taste last check:

def valid_password?(password : String)
  non_trailing_double = false
  streak = 0

  password.chars.each_cons(2) do |(c, d)|
    if c == d
      streak += 1
    elsif c > d
      return false
    else
      non_trailing_double = true if streak == 1
      streak = 0
    end
  end

  non_trailing_double || streak == 1
end

p ("353096".."843212").count { |password| valid_password?(password) }

I started writing such code to check for “groups of 2 but no more than 2” until I realized I could use chunk_while. This is my solution for Day 4 part 2:

different_passwords =
  (138241..674034).count do |number|
    digits = number.to_s.chars.map(&.to_i)

    # Two adjacent digits are the same,
    # are not part of a larger group of matching digits
    digits.chunk_while { |x, y| x == y }.any? &.size.==(2) &&
      # Going from left to right, the digits never decrease
      digits.each_cons(2).all? { |(x, y)| x <= y }
  end
puts different_passwords
2 Likes