DEV Community

Cover image for AoC Day 1: Chronal Calibration
Ryan Palo
Ryan Palo

Posted on • Updated on

AoC Day 1: Chronal Calibration

Overview

After my post last night, @aspittel suggested that we start a daily discussion post chain for these challenges. I think that's a neat idea, and significantly more efficient than everyone saying "Hey check out my git repo!"

I'm excited about this year's Advent mission, time traveling to save Christmas! This will be a place for discussing today's problem, Chronal Calibration. We're tasked with calibrating our time travel device by adding up sequential integers.

On a side note, if you get some extra time, it's probably a good idea to set up some infrastructure for reading input text from a file, because almost all of last year's challenges gave a few simpler examples (good for writing some basic tests) and then a huge-ish input given as a multiline plaintext file.

If you haven't solved today's puzzle yet, stop reading now, because I'm going to post my solution here. If you have finished a solution, post it in the comments! Also feel free to give (loving, instructional, nice) feedback to others' solutions to help them read better or run faster. I've seen a few people that are using this opportunity to learn a new language, so if you see something that works but isn't idiomatic to your favorite language, go ahead and offer suggestions! This includes me, since I'm just learning Rust, so I'm sure it's going to be a mixture of poorly translated Python-isms and gobbledygook.

My Solution

// day1.rs

/// Given a bunch of integers (changes in frequency), one per line,
/// calculate the final sum
pub fn final_frequency(text: &str) -> i32 {
    text.lines()
        .map(|value: &str| -> i32 {value.parse().expect("Not a number")})
        .sum()
}

/// Given a bunch of integers (changes in frequency), one per line,
/// calculate the running sum until you hit a cumulative sum that you've
/// seen before.  Return that first repeated cumulative sum.
pub fn first_duplicate_frequency(text: &str) -> i32 {
    let mut history = vec![0];
    let mut total = 0;
    for line in text.lines().cycle() {
        let value: i32 = line.parse().expect("Not a number.");
        total += value;
        if history.contains(&total) {
            return total;
        }
        history.push(total);
    }
    return 0;
}

#[cfg(test)]
mod tests {
    use super::final_frequency;
    use super::first_duplicate_frequency;

    #[test]
    fn final_all_positives() {
        assert_eq!(3, final_frequency("+1\n+1\n+1"));
    }

    // More tests...
Enter fullscreen mode Exit fullscreen mode
// main.rs

// This file will change day to day as I just use it to exercise each day's module.

use std::fs::File;
use std::io::prelude::*;

mod day1;

fn main() {

    let mut input = File::open("data/day1.txt").expect("File not found.");

    let mut contents = String::new();
    input.read_to_string(&mut contents).expect("Couldn't read file.");
    println!("{}", day1::first_duplicate_frequency(&contents));
}
Enter fullscreen mode Exit fullscreen mode

DEV Leaderboards

Lastly, I created a "private" leaderboard for DEV.to, in case anyone's interested. Apparently, they only hold 200 people per leaderboard, though, so first-come-first serve. Obviously, there are waaaay more than 200 people in the DEV family, so if you get there and the room is full, feel free to create your own on the Advent of Code site and post your room code in the comments. If I see somebody post a room code, I'll do my best to edit my post and copy it below, but read through the comments just in case I miss it.

Happy-- I mean, Merry Coding! I'm excited to see what people think.

@rpalo: 224198-25048a19

Top comments (73)

Collapse
 
aspittel profile image
Ali Spittel • Edited

So excited about this 🙌🏻!

Python

Part 1

data = open('input.txt', 'r')
total = 0
for line in data:
    total += int(line)
print(total)

Part 2

data = [int(i) for i in open('input.txt', 'r')]

def get_first_duplicate_total(data):
    total = 0
    prev_totals = set([0])
    while True:
        for i in data:
            total += i
            if total in prev_totals:
                return total
            prev_totals.add(total)
    return total

print(get_first_duplicate_total(data))

I also learned about itertools.cycle through reading the Reddit solutions, that would make it so that I don't need the while True:

Collapse
 
rpalo profile image
Ryan Palo

Ah! So good. The clean-ness of Python never stops making me happy!

Using a set was a good idea. I didn't think of that, but it would be a lot faster for checking whether or not an item was present.

I'm going to offer unsolicited suggestions, but feel free to totally ignore them if you already knew about them (probable) or don't like them, since your solution already looks really nice.

  1. For part one, generator expressions could be your friend:
data = open('input.txt', 'r')
total = sum(int(line) for line in data)
print(total)
  1. For the second part, check out itertools.cycle.
Collapse
 
aspittel profile image
Ali Spittel

Awesome! Yes, thank you! Found out about itertools.cycle this morning -- feels super niche but still really cool.

Part one could also be a one-liner!

print(sum(int(line for line in open('input.txt', 'r'))))
Collapse
 
r0f1 profile image
Florian Rohrer • Edited

I'd also suggest to use a context manager (the with keyword) for clean opening and closing of files.

Part 1:

with open("input.txt") as f:
    freq = sum(int(i.strip()) for i in f)
freq

Part 2:

from itertools import cycle

with open("input.txt") as f:
    freqs = [int(i.strip()) for i in f]

seen = set()
current = 0
for f in cycle(freqs):
    if current in seen:
        print(current)
        break
    else:
        seen.add(current)
        current += f

Also: "Hey checkout my Github Repo!"

Collapse
 
aspittel profile image
Ali Spittel

True! Always forget to do that since I really only do file handling for code challenges at this point.

Collapse
 
thejessleigh profile image
jess unrein • Edited

Golang solution

Part 1

package main

import (
    "bufio"
    "os"
    "strconv"
)

func freq(f *os.File)(sum int) {
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        i, err := strconv.Atoi(scanner.Text())
        if err != nil {
            panic(err)
        }
        sum += i
    }
    return
}

func main() {
    f, err := os.Open("input1.txt")
    if err != nil {
        panic(err)
    }
    freq(f)
}

Part 2

package main

import (
    "io/ioutil"
    "strconv"
    "container/ring"
    "strings"
)

func dup(x string)(sum int) {
    list := strings.Split(x, "\n")
    r := ring.New(len(list))
    seen := map[int]bool{0: true}
    for i := 0; i < r.Len(); i++ {
        num, err := strconv.Atoi(list[i])
        if err != nil {
            panic(err)
        }
        r.Value = num
        r = r.Next()
    }
    for true {
        sum += r.Value.(int)
        if (seen[sum] == true) {
            return sum
        }
        seen[sum] = true
        r = r.Next()
    }
    return
}

func main() {
    f, err := ioutil.ReadFile("input1.txt")
    if err != nil {
        panic(err)
    }
    dup(string(f))
}

I also wrote solutions in Python that look very similar to Ali's, so I decided to run a little benchmark test using today's input from the Advent of Code website. Here are the results

`python3 1_1.py` 100 times
real    0m4.261s
user    0m2.845s
sys 0m0.936s

`go run 1_1.go` 100 times
real    0m34.169s
user    0m26.057s
sys 0m13.171s

go build 1_1.go

`./1_1` 100 times
real    0m0.641s
user    0m0.216s
sys 0m0.253s

`python3 1_2.py` 100 times
real    0m8.347s
user    0m6.126s
sys 0m1.477s

`go run 1_2.go` 100 times
real    0m38.925s
user    0m31.556s
sys 0m14.668s

go build 1_2.go

`./1_2` 100 times
real    0m3.891s
user    0m3.067s
sys 0m0.657s
Collapse
 
rpalo profile image
Ryan Palo

Woah, nice! Am I reading this right, that the Python is significantly faster than Go? Or is this including compilation each time?

Collapse
 
thejessleigh profile image
jess unrein • Edited

Python is significantly faster than running go run myfile.go. However, that's not Go's intended use case, really. That's more for debugging along the way, since go run does include the compilation each time.

I think it's interesting to compare the difference between go run and executing the compiled go executable, so the third set of each (ex: ./1_1) is go running the resulting compiled executable 100 times.

Thread Thread
 
rpalo profile image
Ryan Palo

Ooooh ok that makes sense. Cool!

Collapse
 
thejessleigh profile image
jess unrein

Also, I am not a regular Go programmer. I've started doing challenges in Go just for my own amusement, but I would definitely not assume that whatever solution I come up with is optimal 😅

Thread Thread
 
tpires profile image
Tiago Pires

Hi jess!

Just a small advert: you should defer f.Close() after check if there's an error.

Also: Check out my GitHub Repo.

Thread Thread
 
thejessleigh profile image
jess unrein

Oh, if this were production code I totally would, but I'm really only building these solutions to solve the problem, and not looking to make them infinitely extensible.

Collapse
 
rhymes profile image
rhymes • Edited

Is it normal that part 2 is killing my CPU :D ? It's been running for minutes in Elixir but nada.

Anyhow, I've tried day 1 with all three languages which is not a great idea!

I just googled the parts of the various languages I needed

Clojure

part 1

(def numbers-as-strings (clojure.string/split (slurp "input.txt") #"\n"))
(def numbers (map read-string numbers-as-strings))
(defn sum [coll] (reduce + coll))
(println (sum numbers))

I kind of hated it, I still don't have syntax highlighting nor formatting for some reason in VSCode and the REPL is quite slow to start (I didn't try with ClojureScript)

part 2

(def repeated-numbers (cycle numbers))

(loop
    [known-totals (set nil), total 0]
    (if (contains? known-totals total)
        (println total)
        (recur (conj known-totals total) (+ total (first (take 1 repeated-numbers))))))

I'm not sure it's correct, I had to kill it because it was hogging the CPU

Elixir

part 1

numbers_as_strings = String.split(String.trim(File.read!("input.txt")), "\n")
numbers = Enum.map(numbers_as_strings, fn x -> String.to_integer(x) end)
IO.puts(Enum.sum(numbers))

Well, this was the easiest one

part 2

defmodule Part2 do
    def find_total(repeated_numbers, totals, sum) do
        unless MapSet.member?(totals, sum) do
            MapSet.put(totals, sum)
            sum = sum + hd(Enum.take(repeated_numbers, 1))
            find_total(repeated_numbers, totals, sum)
        end

        IO.puts sum
    end
end

Part2.find_total(Stream.cycle(numbers), MapSet.new, 0)

Again, I'm not sure it's correct, it just kills my computer

Rust

part 1

use std::fs;

fn main() {
    let data = fs::read_to_string("input.txt").expect("Unable to read file");
    let numbers_as_strings = data.split("\n").collect::<Vec<&str>>();
    let numbers = numbers_as_strings.iter().filter_map(|n| n.parse::<i32>().ok()).collect::<Vec<i32>>();
    let sum: i32 = numbers.iter().sum();
    println!("{}", sum);
}

There's a bit of fiddling with types and syntax but the compiler it's quite helpful when you type stuff that doesn't make sense. It can get in the way of me doing the excercises :D

part 2

Well, this ran for half a second a produced the correct answer

use std::fs;
use std::collections::HashSet;

fn main() {
    // part 1
    let data = fs::read_to_string("input.txt").expect("Unable to read file");
    let numbers_as_strings = data.split("\n").collect::<Vec<&str>>();
    let numbers = numbers_as_strings.iter().filter_map(|n| n.parse::<i32>().ok()).collect::<Vec<i32>>();
    let sum: i32 = numbers.iter().sum();
    println!("{}", sum);

    // part 2
    let mut totals: HashSet<i32> = HashSet::new();
    let repeated_numbers = numbers.iter().cycle();
    let mut repeated_sum: i32 = 0;
    for num in repeated_numbers {
        if totals.contains(&repeated_sum) {
            println!("{}", repeated_sum);
            break;
        }
        totals.insert(repeated_sum);
        repeated_sum += num
    }
}

What am I doing wrong with Clojure and Elixir :D ?

I've put Day 1 on Github: github.com/rhymes/aoc2018/tree/mas...

Collapse
 
ryanwilldev profile image
Ryan Will

At a glance, in the Elixir part two solution it looks like the list of numbers is just being passed through as is. So, it is never moving on to the rest of the numbers and instead the sum is just adding the first number in the list over and over.

Because repeated_numbers is never reassigned the recursive call find_total(repeated_numbers, totals, sum) is passing the unchanged list of all the numbers through to the next iteration.

Collapse
 
rhymes profile image
rhymes

At a glance, in the Elixir part two solution it looks like the list of numbers is just being passed through as is. So, it is never moving on to the rest of the numbers and instead the sum is just adding the first number in the list over and over.

That's probably it, I thought that:

hd(Enum.take(repeated_numbers, 1))

would take 1 number (and hence move the cursor).

Thread Thread
 
ryanwilldev profile image
Ryan Will

It does, but because Elixir is immutable it returns a new list with the first element from repeated_numbers instead of mutating (changing in place) repeating_numbers

Also, this line hd(Enum.take(repeated_numbers, 1)) could be rewritten as hd(repeated_numbers) to get the same functionality. The more "Elixir way to write that would be to use pattern matching. Something like [head | tail] = repeated_numbers Here is some more info on pattern matching, if you're interested. I think it is super cool!

Thread Thread
 
rhymes profile image
rhymes

Thank you! :-)

I have to get used again to immutability and transformation instead of mutability and assignment :D

Collapse
 
trueneu profile image
Pavel Gurkov

On the Clojure pt 2, you're effectively running an endless loop. You always take the first element from the cycled number collection, so you'll never hit the exit condition.
To avoid this while using the approach you chose, add a third numbers binding to the loop, and pass (rest numbers) when recuring, while computing total using (first numbers).

Collapse
 
rhymes profile image
rhymes

Thanks! I then solved it thanks to this post

exactly like you said. You can see my version here github.com/rhymes/aoc2018/blob/mas...

Collapse
 
alephnaught2tog profile image
Max Cerrina

I totally thought my elixir one was dead at first too, so I kept killing it. Only about a 15 second run time though I'm just impatient. lemme look at yours!

Collapse
 
rhymes profile image
rhymes

Please do, I found a solution at the end, using reduce_while, instead of recursion:

repeated_numbers = Stream.cycle(numbers)
repeated_sum = Enum.reduce_while(repeated_numbers, {0, MapSet.new([0])}, fn i, {current, totals} ->
  sum = current + i

  if MapSet.member?(totals, sum) do
    {:halt, sum}
  else
    {:cont, {sum, MapSet.put(totals, sum)}}
  end
end)
IO.puts(repeated_sum)

I feel like I should get through the tutorial at least :D

Collapse
 
harri_etty profile image
Harriet

I wanted to solve these ones in bash, which I've been focussing on learning lately. First challenge was great, found a simple oneliner:

echo $(( 0$(tr -d '\n' < ./day1/input.txt) ))

But part 2 was a nightmare. Super slow. Ended up using JS in the end, but would still love to know anyone's thoughts on how this could be optimised. Wasn't watching the clock but think it took >20 mins to run!

It's collecting previously computed frequencies in an array, and checking for the frequency in the array each time a new frequency is computed. Is it the maths that's likely to be so slow, or the looping, or both?

declare -i total=0
seen=()
found=

array_contains () {
  for i in "${seen[@]}"; do
    if [[ "$i" == "$1" ]]; then
      return 0
    fi
  done
  return 1
}

while [[ ! $found ]]; do
  for line in $(cat $1); do
    total=$(( ${total}${line} ))

    if array_contains "$total"; then
      echo "FOUND " $total
      found=1
      break
    else
      echo 'not found'
    fi

    seen+=($total)
  done
done

# ./main.sh input.txt
Collapse
 
rpalo profile image
Ryan Palo

Woah, this is awesome! Yeah the second parts always seem to need some fancier algorithmic trick to speed them up.

You might look into using an associative array, as those provide a much faster lookup time and you don’t have to loop through every value each time? I don’t know if that will be enough though.

Collapse
 
quoll profile image
Paula Gearon

Unfortunately, associative arrays only appeared in Bash4. That's fine for Linux, but doesn't appear on Macs unless you manually install it (boo!).

Thread Thread
 
rpalo profile image
Ryan Palo

Good caveat to note, thanks!

P.S. Getting Bash 4 on MacOS is a very good idea 😬

Collapse
 
alephnaught2tog profile image
Max Cerrina • Edited

First version, with Elixir, was ~15 seconds, to do what ended up being 130+ iterations on part two.

After some careful matching down to a more or less more-than-binary tree, I got it to about 10 seconds.

... then I went nuts and decided to see if I could do it with JUST sending messages aaaaaaand boy does Elixir do well with that, because I got it to 1.5 seconds total.

I just doublechecked and it is still hitting that loop the same number of times. 137991.

Collapse
 
jpyamamoto profile image
JP

Did it with Elixir too. I found the Stream module to be quite useful, especially for the first part. For the second part, I'm not sure my approach is best, but at least it is working.
If you want to check my repo: github.com/JPYamamoto/advent_of_co...

Collapse
 
alephnaught2tog profile image
Max Cerrina

Yeah, that's what I started with and definitely what I would like actually use for anything. It's soooooooo much more readable like how you have it! The trick with named Processes I did totally works here but I suspect would become insanely complex or fail as soon as you needed data that relied on the other data too, instead of just "has this been seen before" :D

Thread Thread
 
jpyamamoto profile image
JP

Also, I suspect there's a way to do the second part with Streams and avoid having so much information in memory as I did with my approach. Maybe something that takes advantage of Stream.cycle and Stream.scan. But not sure how to implement it. I'll let you know if I figure it out. 😀

Thread Thread
 
alephnaught2tog profile image
Max Cerrina

Yeah, I kept my memory footprint relatively low I think even in version one because I read everything into the IO stream and then just kept throwing that list around, which wasn't greaaaaaaaaaaaat but.

Collapse
 
rpalo profile image
Ryan Palo

Wow, nice!

Collapse
 
alephnaught2tog profile image
Max Cerrina

Thanks! I rechecked it like five times because I was so surprised.

Collapse
 
alephnaught2tog profile image
Max Cerrina
Collapse
 
ryanwilldev profile image
Ryan Will

Thanks for making this post and sharing your solutions!

Here are my solutions in Elixir and JavaScript.

To start a couple functions to parse the input.

def parse(input) do
  input
  |> String.split("\n")
  |> Enum.map(&(String.trim(&1)
    |> String.to_integer())
  )
end
function parse() {
  return input()
    .split('\n')
    .map(Number);
}

Part one's solutions.

def part_1 do
  input() 
  |> parse() 
  |> Enum.sum()
end
function partOne() {
  return parse()
    .reduce((total, current) => total + current, 0);
}

Part two's solutions.

This solution takes advantage of Elixir's recursion, pattern matching, and multiple function clauses.

def part_2 do
 input() 
 |> parse() 
 |> part_2(0, %{})
end

def part_2([current | rest] = _frequencies, total, record) do
  total = current + total
  record = Map.update(record, total, 1, &(&1 + 1))

  case record[total] do
    n when n > 1 ->
      total

    _ ->
      part_2(rest, total, record)
  end
end

def part_2([], total, record) do
  input()
  |> parse()
  |> part_2(total, record)
end

I made two solutions to part two in JavaScript the first uses a more imperative approach, and the second was meant to mimic the Elixir solution using recursion. The issue with that is JavaScript not being tail call optimized. In order to get around that I used a trampoline function and a while loop. Though, that approach is insanely slow.

function partTwoImperative() {
  let record = {};
  let frequency = 0;
  let index = 0;
  let frequencyList = parse();
  let totalLoops = 0;

  const found = (record, frequency) => 
    (record[frequency] || 0) > 1;
  const getFrequencyRecordValue = (record, frequency) =>
    (record[frequency] || 0) + 1;

  while (!found(record, frequency)) {
    if (index === frequencyList.length) index = 0;

    frequency += frequencyList[index];
    record[frequency] = getFrequencyRecordValue(record, frequency);
    index += 1;
    totalLoops += 1;
  }

  return [frequency, totalLoops];
}

function partTwoTrampoline() {
  const found = (record, frequency) => (record[frequency] || 0) > 1;

  const getFrequencyRecordValue = (record, frequency) =>
    (record[frequency] || 0) + 1;

  const partTwoRecursive = ([current, ...rest], total, record) => {
    const newTotal = total + current;

    const newRecord = {
      ...record,
      [newTotal]: getFrequencyRecordValue(record, newTotal)
    };

    return !found(newRecord, newTotal)
      ? () =>
          partTwoRecursive(
            rest.length ? rest : [3, 3, 4, -2, -4],
            newTotal,
            newRecord
          )
      : newTotal;
  };

  let ret = partTwoRecursive([3, 3, 4, -2, -4], 0, {});

  while (typeof ret === 'function') {
    ret = ret();
  }

  return ret;
}
Collapse
 
am_myrick profile image
Andre Myrick

Wow, this is really cool! I completed day one too, but my code is nowhere near that clean. Could you recommend any resources that helped you get into Elixir?

Collapse
 
ryanwilldev profile image
Ryan Will

Thanks! This sounds like a cop-out answer but the Elixir docs are a great resource. elixir-lang.org/

I'd also be happy to answer any questions you have.

Collapse
 
yordiverkroost profile image
Yordi Verkroost

Awesome, this is a nice place to discuss the solutions! This is mine, in Elixir:

Common code (used by both part 1 and part 2):

defmodule AoC.DayOne.Common do
  def read_input(path) do
    File.stream!(path)
    |> Stream.map(&String.trim/1)
    |> Stream.filter(fn x -> x != "" end)
    |> Stream.map(&String.trim_trailing/1)
    |> Stream.map(&String.to_integer/1)
    |> Enum.to_list()
  end
end

Part 1


defmodule AoC.DayOne.PartOne do
  alias AoC.DayOne.Common

  def main() do
    Common.read_input("lib/day1/input.txt")
    |> calculate_sum()
    |> IO.puts()
  end

  def calculate_sum(numbers) do
    Enum.sum(numbers)
  end
end

Part 2

defmodule AoC.DayOne.PartTwo do
  alias AoC.DayOne.Common

  def main() do
    Common.read_input("lib/day1/input.txt")
    |> calculate_result()
    |> IO.puts()
  end

  def calculate_result(numbers, frequencies \\ [], base \\ 0, index \\ 0)

  def calculate_result(numbers, frequencies, base, index) when index == length(numbers) do
    calculate_result(numbers, frequencies, base, 0)
  end

  def calculate_result(numbers, frequencies, base, index) do
    base = base + Enum.at(numbers, index)
    if (Enum.member?(frequencies, base)) do
      base
    else
      frequencies = [base | frequencies]
      calculate_result(numbers, frequencies, base, index + 1)
    end
  end
end

If you want, you could check out my full repo on GitHub.

Collapse
 
dnamsons profile image
Dāvis Namsons

That's a good idea !

Ruby

Part 1

input = File.open('./freqs.in').read

frequency = 0

input.each_line do |line|
  frequency += line.to_i
end

puts frequency

Part 2

input = File.open('./freqs.in').read

frequency_changes = input.scan(/[-+]\d+/).collect! &:to_i

frequency = 0
frequencies = []
i = 0

until frequencies.include?(frequency)
  frequencies << frequency
  frequency += frequency_changes[i]
  i == frequency_changes.length - 1 ? i = 0 : i += 1
end

puts frequency
Collapse
 
arjunrajkumar profile image
Arjun Rajkumar • Edited

Hi Davis.. Just starting on the #adventofcode series.

Just curious how you are reading the inputs. E.g. first problem says the input is on adventofcode.com/2018/day/1/input

Will you be using Nokigiri or something similar to read the URL and parse the page to get the inputs?
Sorry if this sounds dumb.. But am wondering how you going to download the file?

Thanks

Collapse
 
mattmorgis profile image
Matt Morgis • Edited

Node.js

First, I created an async generator that read the input file stream chunk by chunk and yield each number line by line.

async function* streamToFrequencies(stream) {
  let previous = "";
  for await (const chunk of stream) {
    previous += chunk;
    let eolIndex;
    while ((eolIndex = previous.indexOf("\n")) >= 0) {
      // exclude the EOL
      const number = previous.slice(0, eolIndex);
      yield parseInt(number);
      previous = previous.slice(eolIndex + 1);
    }
  }
  if (previous.length > 0) {
    yield parseInt(previous);
  }
}

Then part 1 was pretty straight forward:

const addFrequencies = async frequencies => {
  let sum = 0;
  for await (const frequency of frequencies) {
    sum += frequency;
  }
  return sum;
};

const sum = stream => {
  return addFrequencies(streamToFrequencies(stream));
};

This approach made part 2 ugly because I had to find a way to re-open the file stream and loop back through the inputs. Using a set over an array really helped with performance.

const calibrate = async stream => {
  let currentFrequency = 0;
  const frequenciesFound = new Set([0]);

  while (true) {
    // clone stream and put in cold storage
    // in case we need to re-read inputs.
    let frozenStream = clone(stream);

    for await (const frequency of streamToFrequencies(stream)) {
      currentFrequency += frequency;
      if (frequenciesFound.has(currentFrequency)) {
        return currentFrequency;
      }

      frequenciesFound.add(currentFrequency);
    }
    stream = frozenStream;
  }
};

Putting it all together:

const frequencyStream = () => {
  return fs.createReadStream(__dirname + "/input.txt", {
    encoding: "utf-8",
    highWaterMark: 256
  });
};

const main = async () => {
  const frequencySum = await sum(frequencyStream());
  console.log({frequencySum})
  const frequencyCalibration = await calibrate(frequencyStream());
  console.log({frequencyCalibration});
};

main();

Full code: github.com/MattMorgis/Advent-Of-Co...

Collapse
 
themindfuldev profile image
Tiago Romero • Edited

I also did mine in JS but I decided to use the readline interface to read each line individually and spend less memory by not loading the entire file in the memory at once.

I haven't trying using for await (... of ...) with the readline interface. Maybe I'll try that next. If anyone would like to try it please post it here.

Here are my solutions:

My solution in JavaScript / Node 11, using the readline interface:

readLines.js

const fs = require('fs');
const readline = require('readline');

const readLines = (file, onLine) => {
    const reader = readline.createInterface({
        input: fs.createReadStream(file),
        crlfDelay: Infinity
    });

    reader.on('line', onLine);

    return new Promise(resolve => reader.on('close', resolve));
};

const readFile = async file => {
    const lines = [];
    await readLines(file, line => lines.push(line));  
    return lines;
}

module.exports = {
    readLines,
    readFile
};

01a.js

const { readFile } = require('./readLines');

(async () => {
    const lines = await readFile('01-input.txt');

    const frequency = lines.reduce((frequency, line) => frequency + Number(line), 0);

    console.log(`The final frequency is ${frequency}`);
})();

01b.js

const { readFile } = require('./readLines');

(async () => {
    const lines = await readFile('01-input.txt');

    const frequencySet = new Set();

    let frequency = 0;
    let didAFrequencyReachTwice = false;

    while (!didAFrequencyReachTwice) {
        for (let line of lines) {
            frequency += Number(line);
            if (frequencySet.has(frequency)) {
                didAFrequencyReachTwice = true;
                break;
            }
            else {
                frequencySet.add(frequency);
            }
        }
    }

    console.log(`The first frequency reached twice is ${frequency}`);
})();
Collapse
 
mattmorgis profile image
Matt Morgis

Readline doesn't work with async iterators and for await yet, but it just landed in 11.x staging.

Once it is released, my streamToFrequencies generator won't be needed.

Also, createReadStream only reads the file in 256 byte chunks at a time (or whatever you set the highwatermark to be, it does not read the entire file into memory. readFile would, however.

To read more about async iterators and generators and the for await syntax, check out 2ality.com/2018/04/async-iter-node...

Thread Thread
 
themindfuldev profile image
Tiago Romero

Thanks a bunch @mattmorgis !

Collapse
 
milmazz profile image
Milton Mazzarri

Thanks for sharing, here are my solutions in Elixir and Racket.

I'm not sure if the Racket code is idiomatic, so, any advice is more than welcome.

Elixir

# https://adventofcode.com/2018/day/1
#
# To run each exercise, you can do the following:
#
# elixir -r exercise.exs -e "IO.inspect(Frequency.first_exercise())"
# elixir -r exercise.exs -e "IO.inspect(Frequency.second_exercise())"
#
defmodule Frequency do
  def first_exercise, do: frequency(process_file())

  def second_exercise, do: first_frequency_reached_twice(process_file())

  @spec frequency([integer]) :: integer
  def frequency(freq_changes), do: Enum.sum(freq_changes)

  @spec first_frequency_reached_twice([integer], {integer, MapSet.t()} | integer) :: integer
  def first_frequency_reached_twice(frequency_changes, acc \\ {0, MapSet.new([0])})

  def first_frequency_reached_twice(_, acc) when is_integer(acc), do: acc

  def first_frequency_reached_twice(frequency_changes, acc) do
    result =
      Enum.reduce_while(frequency_changes, acc, fn digit, {current, past} ->
        next = current + digit

        if MapSet.member?(past, next) do
          {:halt, next}
        else
          {:cont, {next, MapSet.put(past, next)}}
        end
      end)

    first_frequency_reached_twice(frequency_changes, result)
  end

  defp process_file do
    "input"
    |> File.stream!()
    |> Stream.map(fn x -> x |> String.trim() |> String.to_integer() end)
    |> Enum.to_list()
  end
end

ExUnit.start()

defmodule FrequencyTest do
  use ExUnit.Case

  import Frequency

  test "should calculate frequency" do
    test_cases = [
      {[1, -2, 3, 1], 3},
      {[1, 1, 1], 3},
      {[1, 1, -2], 0},
      {[-1, -2, -3], -6}
    ]

    Enum.each(test_cases, fn {changes, expected} ->
      assert frequency(changes) == expected
    end)
  end

  test "should stop when a frequency is reached twice" do
    test_cases = [
      {[1, -2, 3, 1, 1, -2], 2},
      {[1, -1], 0},
      {[3, 3, 4, -2, -4], 10},
      {[-6, 3, 8, 5, -6], 5},
      {[7, 7, -2, -7, -4], 14}
    ]

    Enum.each(test_cases, fn {changes, expected} ->
      assert first_frequency_reached_twice(changes) == expected
    end)
  end
end

Racket

#lang racket/base

#|
Reference: https://adventofcode.com/2018/day/1

If you want to test the results:

λ racket
> (require (file "exercise.rkt"))
> (frequency (file->list "input"))
520
> (first-frequency-reached-twice (file->list "input"))
394
|#

(require racket/set)
(require racket/match)

(provide frequency first-frequency-reached-twice)

(define (frequency changes)
  (foldl + 0 changes))

(define (first-frequency-reached-twice changes)
  (find-frequency (cons 0 (set 0)) changes))

(define (find-frequency acc changes)
  (match acc
    [result
     #:when (integer? result)
     result]
    [(cons current previous-frequencies)
     (find-frequency (reduce-while changes previous-frequencies current) changes)]))

(define (reduce-while changes previous-frequencies current)
  (match changes
    [changes
     #:when (eq? '() changes)
     (cons current previous-frequencies)]
    [(cons head tail)
     (define next (+ current head))
     (if (set-member? previous-frequencies next)
         next
         (reduce-while tail (set-add previous-frequencies next) next))]))

Unit tests for Racket:

#lang racket/base

(require rackunit
         "exercise.rkt")

(define exercise-tests
  (test-suite
   "Tests for exercise day 1"

   (test-case
    "Should calculate frequency"

    (check-equal? (frequency '(1 -2 3 1)) 3)
    (check-equal? (frequency '(1 1 1)) 3)
    (check-equal? (frequency '(1 1 -2)) 0)
    (check-equal? (frequency '(-1 -2 -3)) -6))

   (test-case
    "Should find first frequency reached twice"

    (check-equal? (first-frequency-reached-twice '(1 -1)) 0)
    (check-equal? (first-frequency-reached-twice '(1 -2 3 1 1 -2)) 2)
    (check-equal? (first-frequency-reached-twice '(3 3 4 -2 -4)) 10)
    (check-equal? (first-frequency-reached-twice '(-6 3 8 5 -6)) 5)
    (check-equal? (first-frequency-reached-twice '(7 7 -2 -7 -4)) 14))))

(require rackunit/text-ui)
(run-tests exercise-tests)