AoC Day 4: Repose Record

Part of "Advent of Code" series

Day 4 is here! Our first puzzle launched on a weeknight. Hopefully, we're able to keep up this pace!

Today's challenge makes me feel very much like a secret agent as we observe elfy guards falling asleep on duty: figuring out who is sleeping, when, and how much.

I haven't even started this challenge yet (hoping to get to it this evening or maybe at lunch), but I wanted to get the post up for everybody else to share their solutions.

Good luck!

Did you find this post useful? Show some love!
DISCUSSION (36)

Python!

May refactor later, but this problem was a little less fun for me than other ones so far, to be honest.

part 1

import re
from collections import Counter

with open('input.txt', 'r') as f:
    data = []
    for line in f:
       date = line[line.find("[")+1:line.find("]")]
       action = line.split("] ")[1].strip()
       data.append((date, action,))
    data = sorted(data, key=lambda i: i[0])

guards = {}
for time, action in data:
    time = int(time[-2:])
    if "Guard" in action:
        _id = re.findall(r'\d+', action)[0]
        if not _id in guards:
            guards[_id] = {'length': 0, 'minutes': []}
    elif action == "falls asleep":
        start = time
    else:
        guards[_id]['length'] += time - start
        guards[_id]['minutes'] += list(range(start, time))

sleep_longest = max(guards.items(),key=lambda guard: guard[1]['length'])
minutes = sleep_longest[1]['minutes']
print(Counter(minutes).most_common(1)[0][0] * int(sleep_longest[0]))

part 2

import re
from collections import Counter

with open('input.txt', 'r') as f:
    data = []
    for line in f:
       date = line[line.find("[")+1:line.find("]")]
       action = line.split("] ")[1].strip()
       data.append((date, action,))
    data = sorted(data, key=lambda i: i[0])

guards = {}
for time, action in data:
    time = int(time[-2:])
    if "Guard" in action:
        _id = re.findall(r'\d+', action)[0]
        if not _id in guards:
            guards[_id] = []
    elif action == "falls asleep":
        start = time
    else:
        guards[_id] += list(range(start, time))

max_count = 0
solution = 0
for guard, minutes in guards.items():
    if minutes:
        most_frequent = Counter(minutes).most_common(1)[0]
        if most_frequent[1] > max_count:
            max_count = most_frequent[1]
            solution = int(guard) * most_frequent[0]
print(solution)

Agree that this one was less fun, and I kind of hate my Python solution, which is much muddier than yours. I don't know that I have the energy to try to do this in Go.

Yeah, I like the algorithm-y ones, this one was more string parsing I think!

It was a complicated description though, I had a lot of problems parsing what I was supposed to be finding.

TOTALLY -- I only figured it out once I looked at the input. The graph actually totally threw me off.

from collections import defaultdict


def main():
    guard_sleep_dict = calculate_guard_sleep_dict()
    part_one(guard_sleep_dict)
    part_two(guard_sleep_dict)


def calculate_guard_sleep_dict():
    with open('input.txt', 'r') as data:

        sorted_data = sorted(data)  # This will work too... No extra key required

        guard_sleep_dict = defaultdict(lambda: [0 for x in range(60)])
        current_guard = -1
        start_sleeping = -1
        for line in sorted_data:
            if line[25] == "#":
                current_guard = line.split()[3]
            elif line[25] == "a":
                start_sleeping = int(line[15:17])
            else:  # "wakes up"
                end_sleeping = int(line[15:17])
                for x in range(start_sleeping, end_sleeping):
                    guard_sleep_dict[current_guard][x] += 1

    return guard_sleep_dict


def part_one(guard_sleep_dict):
    guard = sorted(guard_sleep_dict.keys(), key=lambda g: -sum(guard_sleep_dict[g]))[0]

    gh = guard_sleep_dict[guard]
    minute = gh.index(max(gh))
    print int(guard[1:]) * minute


def part_two(guard_sleep_dict):
    guard = sorted(guard_sleep_dict.keys(), key=lambda g: -max(guard_sleep_dict[g]))[0]
    gh = guard_sleep_dict[guard]
    minute = gh.index(max(gh))
    print int(guard[1:]) * minute


if __name__ == '__main__':
    main()

First missunderstood the task, so I wasted my whole time before university for the wrong solution... Fortunately I had some time after the first seminar to fix my solution.

Then found out that I can just call sorted and sort the data and I don't need any extra key for this. This made solving the puzzle much easier.

Did that yesterday! Elegantly solved exactly the wrong problem, had to go back and re-read the whole page to figure out what I was doing wrong. Devastating.

Yeah it is just super frustrating... Especially if you notice then that you have to move on to do other things and will loose expensive points on your private leaderboards 🙈
Tomorrow I will read the task at least twice before I start typing.

Damn life, getting in the way of AoC. The very nerve.

I used the same approach, but I really like the tools you have for searching the structure. The Clojure ones work, but were much more manual, making the code a little more clunky.

PHP

Had to brush up on the difference between usort, uasort, and uksort for this one, ahaha.

Part 1:

<?php
$list = file_get_contents($argv[1]);
$records = explode("\n", trim($list));
$currentguard = null;
$lastminute = 0;
$awake = true;
$guardtotals = array();

usort($records, function($a, $b) {
  preg_match('/\[([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $a, $atimes);
  preg_match('/\[([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $b, $btimes);
  if (intval($atimes[1].$atimes[2].$atimes[3].$atimes[4].$atimes[5]) == intval($btimes[1].$btimes[2].$btimes[3].$btimes[4].$btimes[5])) {
    return 0;
  }
  return (intval($atimes[1].$atimes[2].$atimes[3].$atimes[4].$atimes[5]) < intval($btimes[1].$btimes[2].$btimes[3].$btimes[4].$btimes[5])) ? -1 : 1;
});

foreach ($records as $record) {
  preg_match('/\[[0-9]{4}\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $record, $timestamps);
  preg_match('/Guard #([0-9]+)/', $record, $guard);
  if (count($guard) > 0) {
    if (!$awake && $currentguard) {
      $timeasleep = 60 - $lastminute;
      $guardtotals[$currentguard]['asleep'] += $timeasleep;
      $sleep = array_slice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep);
      $sleep = array_map(function($x) {
        return $x+1;
      }, $sleep);
      array_splice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep, $sleep);
      $lastminute = 0;
    }
    $currentguard = intval($guard[1]);
    $awake = true;
    if (!array_key_exists($currentguard, $guardtotals)) {
      $guardtotals[$currentguard] = array(
        'asleep' => 0,
        'minutes' => array_fill(0, 60, 0)
      );
    }
  } elseif (strpos($record, 'falls asleep')) {
    $awake = false;
    $lastminute = intval($timestamps[4]);
  } elseif (strpos($record, 'wakes up')) {
    if ($currentguard) {
      $timeasleep = intval($timestamps[4]) - $lastminute;
      $guardtotals[$currentguard]['asleep'] += $timeasleep;
      $sleep = array_slice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep);
      $sleep = array_map(function($x) {
        return $x+1;
      }, $sleep);
      array_splice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep, $sleep);
      $lastminute = intval($timestamps[4]);
      $awake = true;
    }
  }
}
uasort($guardtotals, function($a, $b) {
  if ($a['asleep'] == $b['asleep']) {
    return 0;
  }
  return ($a['asleep'] < $b['asleep']) ? 1 : -1;
});
$chosenguard = array_keys($guardtotals)[0];
$chosenminute = array_search(max($guardtotals[$chosenguard]['minutes']), $guardtotals[$chosenguard]['minutes']);
echo $chosenguard*$chosenminute;
die(1);

Part 2:

<?php
$list = file_get_contents($argv[1]);
$records = explode("\n", trim($list));
$currentguard = null;
$lastminute = 0;
$awake = true;
$guardtotals = array();

usort($records, function($a, $b) {
  preg_match('/\[([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $a, $atimes);
  preg_match('/\[([0-9]{4})\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $b, $btimes);
  if (intval($atimes[1].$atimes[2].$atimes[3].$atimes[4].$atimes[5]) == intval($btimes[1].$btimes[2].$btimes[3].$btimes[4].$btimes[5])) {
    return 0;
  }
  return (intval($atimes[1].$atimes[2].$atimes[3].$atimes[4].$atimes[5]) < intval($btimes[1].$btimes[2].$btimes[3].$btimes[4].$btimes[5])) ? -1 : 1;
});

foreach ($records as $record) {
  preg_match('/\[[0-9]{4}\-([0-9]{2})\-([0-9]{2}) ([0-9]{2}):([0-9]{2})\]/', $record, $timestamps);
  preg_match('/Guard #([0-9]+)/', $record, $guard);
  if (count($guard) > 0) {
    if (!$awake && $currentguard) {
      $timeasleep = 60 - $lastminute;
      $guardtotals[$currentguard]['asleep'] += $timeasleep;
      $sleep = array_slice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep);
      $sleep = array_map(function($x) {
        return $x+1;
      }, $sleep);
      array_splice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep, $sleep);
      $lastminute = 0;
    }
    $currentguard = intval($guard[1]);
    $awake = true;
    if (!array_key_exists($currentguard, $guardtotals)) {
      $guardtotals[$currentguard] = array(
        'asleep' => 0,
        'minutes' => array_fill(0, 60, 0)
      );
    }
  } elseif (strpos($record, 'falls asleep')) {
    $awake = false;
    $lastminute = intval($timestamps[4]);
  } elseif (strpos($record, 'wakes up')) {
    if ($currentguard) {
      $timeasleep = intval($timestamps[4]) - $lastminute;
      $guardtotals[$currentguard]['asleep'] += $timeasleep;
      $sleep = array_slice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep);
      $sleep = array_map(function($x) {
        return $x+1;
      }, $sleep);
      array_splice($guardtotals[$currentguard]['minutes'], $lastminute, $timeasleep, $sleep);
      $lastminute = intval($timestamps[4]);
      $awake = true;
    }
  }
}
$maximums = array_map(function($z) {
  return max($z['minutes']);
}, $guardtotals);
uasort($maximums, function($a, $b) {
  if ($a == $b) {
    return 0;
  }
  return ($a < $b) ? 1 : -1;
});
$chosenguard = array_keys($maximums)[0];
$chosenminute = array_search($maximums[$chosenguard], $guardtotals[$chosenguard]['minutes']);
echo $chosenguard*$chosenminute;
die(1);

UKSort works the same, but is just more polite and has a slight accent, right? :]

The first part made me think a little about the data structure, so I decided to create a map of the guard ID to the guard's times, where the times is a map of the minute to the number of times that the guard was asleep in that minute.

Fortunately, the date/time format is year/month/day/hour/minute, which can be sorted alphanumerically, and once that's done all sleep and wake events occur in pairs together.

(def build-table
  [input-file]
  (loop [[line & xlines] (sort (lines input-file)) guard 0 table {}]
    (if-not line
      table
      (if (= "Guard" (subs line 19 24))
        (recur xlines (-> (subs line 26) (split #" ") first as-long) table)
        (let [sleep (as-long (subs line 15 17))
              wake (as-long (subs (first xlines) 15 17))
              table' (reduce (fn [t minute]
                               (update-in t [guard minute] #(if % (inc %) 1)))
                             table (range sleep wake))]
          (recur (rest xlines) guard table'))))))

Given the fixed-width, it seemed easier to substring out the bits I was looking for.

Now that I had this table, I just needed to search through it for the bits I was looking for. After trying to pull out a key/value entry from a map for the largest value, I decided to create a helper function that I could send to reduce which let me select the "largest" value, using an arbitrary function on the value in order to compare:

(defn max-finder
  [vfn]
  (fn [[_ mv :as mkv] [_ v :as kv]] (if (> (vfn v) (vfn mv)) kv mkv)))

Later on, someone pointed out that Clojure already has the function max-key which finds the key for the largest value, but if you want to look for a non-numeric value then you need to update the entries of the map. That meant that I ended up with a lot more code, and I don't think it was as elegant.

Anyway, now that I was reading the table and had my helper function, here is the first part:

(defn star
  [input-file]
  (let [table (build-table input-file)
        [guard times] (reduce (max-finder count) [0 []] table)
        [max-time _] (reduce (max-finder identity) [0 0] times)]
    (* guard max-time)))

Part 2 was similar. This time I created a temporary var to hold the function that (max-finder identity) returns, so that the map function doesn't keep calling it unnecessarily:

(defn star2
  [input-file]
  (let [table (build-table input-file)
        ident-finder (max-finder identity)
        guards-maxes (map (fn [[g tm]] [g (reduce ident-finder [0 0] tm)]) table)
        [guard [minute _]] (reduce (max-finder second) [0 [0 0]] guards-maxes)]
    (* guard minute)))

I like how having the correct data structure meant that the final calculations could be encapsulated in just a couple of lines.

YES I had to read the question like 18 times.

Indeed, it was the longest by far so far. The challenge was posted at 9pm, PST time. I started about 9:30pm. I went to bed around midnight. Still worth it :)

Timezones really matter if you're trying to get a good score (which is one reason why I'm not trying). I really feel for the Europeans who want to compete.

Yeah, it's possible for me to change the leaderboard to order based on number of stars. It still breaks ties based on who got the most recent star sooner, but it would be a little bit less demoralizing. Does that seem like a good idea?

Also, while it may seem that it's mighty convenient that I decide to make the leaderboard more forgiving on the first day that I miss a 9PM start, that honestly wasn't why I'm thinking about this. It just seems like more fun if there's not a difference of 400 points between 1st place and last place. :)

Non-functional-paradigm kotlin solution

See Neil Gall's wonderful post for a more functional-paradigm approach. Kotlin not being pure-functional makes me lean into the procedural style more often than I'd like.

Anyway...

Part 1

I didn't read the problem properly. Some key things I missed:

  • The input data is random.
  • We only care about minutes.
  • There's only ever one guard on duty (not a stack of sleeping and awake guards).

Fortunately, once I knew what I was supposed to be recording, it was just a simple matter of adding sleeping events to guards.

typealias Duration = Pair<LocalDateTime, LocalDateTime>

private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")

fun String.parseEntry() =
    Regex("""\[(.*)] (.*)""").matchEntire(this)!!.groupValues.let {
        LocalDateTime.parse(it[1], dateFormat) to it[2]
    }

fun String.parseGuardId() =
    Regex("""Guard #(\d+) begins shift""")
        .matchEntire(this)!!
        .groupValues[1]
        .toInt()


fun Duration.minutes() = let { (start, end) ->
    ((end.toEpochSecond(UTC) - start.toEpochSecond(UTC)) / 60).toInt()
}

fun Duration.minuteRange() = let { (start, end) ->
    start.minute until end.minute
}

private fun computeGuardSchedules(initial: List<Pair<LocalDateTime, String>>): List<Guard> {
    val guards = mutableMapOf<Int, Guard>()
    initial.sortedBy { it.first }.fold(-1) { onDuty, (ts, type) ->
        val guard = when (type) {
            "wakes up" -> guards[onDuty]?.awaken(ts)
            "falls asleep" -> guards[onDuty]?.asleepen(ts)
            else -> type.parseGuardId().let { gid ->
                if (gid !in guards) {
                    guards[gid] = Guard(gid)
                }
                guards[gid]!!.ondutyen(ts)
            }
        }!!
        guard.id
    }
    return guards.values.toList()
}


class Guard(val id: Int) {
    private var lastStatusChange: LocalDateTime? = null
    val sleepLog = mutableListOf<Duration>()

    fun awaken(ts: LocalDateTime): Guard {
        sleepLog.add(lastStatusChange!! to ts)
        lastStatusChange = ts
        return this
    }


    fun asleepen(ts: LocalDateTime): Guard {
        lastStatusChange = ts
        return this
    }

    fun ondutyen(ts: LocalDateTime): Guard {
        lastStatusChange = ts
        return this
    }
}



fun answer1(input: List<Guard>): Int {
    val sleepiest = input.maxBy {
        it.sleepLog.sumBy(Duration::minutes)
    }!!

    val minuteMostOftenAsleep = sleepiest.sleepLog
        .flatMap(Duration::minuteRange)
        .frequencies()
        .maxBy { (_, v) -> v }!!.key

    return sleepiest.id * minuteMostOftenAsleep
}

Part 2

So, the thing that threw me here is that some guards don't fall asleep, leading .maxBy to return null. Otherwise, this was a fairly simple modification of the first part, just aggregating at different points. At this point, I'm wondering if Eric Wastl is experimenting with reversing the difficulties this year. This is two in a row where the heavy lifting gets done in part 1. We'll see when tomorrow's drops!

    fun answer2(input: List<Guard>) =
        input.map {
            it.id to (
                    it.sleepLog
                        .flatMap(Duration::minuteRange)
                        .frequencies()
                        .toList()
                        .sortedBy { (k, v) -> -v }
                        .firstOrNull()
                    )
        }.sortedBy { (k, v) ->
            -(v?.second ?: -10000)
        }.first().let {
            it.first * it.second!!.first
        }

and since I got bored waiting for some scans to complete, here's what my schedule looks like!

I should probably just pick the days that don't have guards!

                 000000000011111111112222222222333333333344444444445555555555
                 012345678901234567890123456789012345678901234567890123456789
1518-03-01  none ............................................................
1518-03-02  #587 ....zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.....zzzz...............
1518-03-03 #2113 ......................................................zz....
1518-03-04 #2699 .............zzzzzzzzzzzzzzzzzzzzzzzz...zzzzz...............
1518-03-05  #593 ..............zzzzzzzzzzzzzzzzzz............................
1518-03-06 #1543 ..............zz...........................zzzzzzz..........
1518-03-07 #2039 ....zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...zzzzzzzzzzzzz........
1518-03-08  #587 .................................................zz.........
1518-03-09 #1601 ...........zzzzzzz..............zzzzzzzzzzzzzzzzzzzzzz......
1518-03-10 #1553 ...................zzzzzzzzzzzzzzzzzzzzzzzzzzz..............
1518-03-11 #1433 .................zzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzzzz.....
1518-03-12 #2113 ..........zzzzzzzzzzzz........zzzzzzzzzzzzzzzzzzzzzz...z....
1518-03-13 #3121 ......zz.............................zzzzzzzz...............
1518-03-14 #1549 .......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......
1518-03-15  #587 .............................zzzz........................z..
1518-03-16 #1553 .........................................zzzzzzz.......zzzz.
1518-03-17  #587 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
1518-03-18 #1601 zzzzzzzzzzzzzzzzzzzzzzzzzzzzz...............................
1518-03-19 #1553 ...................................zzzzzz...zz....zzzzzzzz..
1518-03-20  #181 ......................zzzzzzzzzzzzzzzzzzzzz.................
1518-03-21 #1553 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzzz.
1518-03-22 #1601 ...............................zzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-03-23  #229 ............................................zzzz............
1518-03-24 #1553 ................zzzzzzzzzzzzzz.............zzz.......zz.....
1518-03-25 #1549 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzz...zz.....
1518-03-26 #1553 ................................................zzzzzzz.....
1518-03-27  #199 ................................................zzzzzz......
1518-03-28 #1549 ....................................zzzzzz........zzzzzzz...
1518-03-29 #1553 .....................zzzzzzzzzzzzzzzzzzzzzzzzzzz............
1518-03-30 #2699 ..............................................zzzzzz........
1518-03-31  #593 ................................zzzzz.......................
1518-04-01  none ............................................................
1518-04-02 #1597 ..........zzzzzzzzzzzz........zzzzzzzz......................
1518-04-03 #2699 .............................................zzz............
1518-04-04 #1489 ........................zzzzzzzz............................
1518-04-05 #1433 ..zzzzzzzzzzzzzzzzzzzz......................................
1518-04-06 #1549 ..................................zzzzzz...zzzz.............
1518-04-07 #1433 .............................zzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-04-08 #1601 .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz................
1518-04-09  #199 .....................................................zz.....
1518-04-10  #587 ....................zzzzzzzzzzzzzzz.........................
1518-04-11  #587 ..................................zzzzzzzzzzz...............
1518-04-12 #1433 ...........................zzzzzzzzzzzzzzzzzzzzzzzzz........
1518-04-13 #1549 .........................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.....
1518-04-14 #3121 .........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz................
1518-04-15  #587 ......................zzzzzzzzzzzzzzzzzzzzzzzzzzzz..........
1518-04-16 #1039 ...................................zzzzzzzzzzzzzzzzz........
1518-04-17 #1039 ..........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz................
1518-04-18 #2927 ...................................zzzzzzzzzzzz...zzz....zz.
1518-04-19 #2113 .............zz.....zzzzzzzzzz.................zzzz.........
1518-04-20 #1601 ...zzzzzzzzzzzzzzzzzzzzz.................zzzzzzzzzzzzzzzzzz.
1518-04-21 #1433 ............zzzzzzzzzzz.....................................
1518-04-22 #1433 .....................................................zz.....
1518-04-23  #587 ...............zzzzzzzzzzzzzzzzzzzzzzzzzzzzz................
1518-04-24 #1597 ..............................zzzzz......zzzzzz.........z...
1518-04-25 #1549 ........................................zzzzzzzzzzzz....zz..
1518-04-26 #1489 ..............zzzzzzz..............zzzzzzzzzzzzz............
1518-04-27 #2699 ...............................................zzzzzzz......
1518-04-28 #2699 ....zzz.....................................zzzzzzzzzzzzz...
1518-04-29 #2113 .........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-04-30 #1597 ...zzzzzzzzz....zzzzzzzzzzzzzzzzzzzzz.......................
1518-05-01 #1433 ...................................................zzz......
1518-05-02 #3121 ........................zzzzz......zzz......................
1518-05-03 #1039 ...........................zzzzzzzzzzz......................
1518-05-04 #3121 ....................................................zzzzz...
1518-05-05  #199 zzzzzzzzzzzzzzzzzzzzzzzzz.......................zzzzzzzzzz..
1518-05-06  #229 ...............................................zzzzz........
1518-05-07 #3121 .............zzzzzzzzzzzzzz...........................zzzzz.
1518-05-08 #1489 ...............................zzzzzzzzzzzzzzzzzzzz.........
1518-05-09  #229 ........................................zzzzzzzzzzzzzzz.....
1518-05-10 #1549 ...........................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
1518-05-11 #1549 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....
1518-05-12 #1601 ...........................zzz......zzzzzzzzzzzzzzzzzzzzz...
1518-05-13 #2039 .............................zzzzzzzzzzzzz..................
1518-05-14  #181 ...........................zzz......................zzzzzz..
1518-05-15  #587 ...........................zzz...............zzz............
1518-05-16 #1543 ............................................zzzzzzzzzzzz....
1518-05-17 #1549 ..........................zzzzzzzzzzzzzzzzzzzzzzz...zzzzz...
1518-05-18 #1597 .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......................
1518-05-19 #1601 ...............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....zzzzzzzzz.
1518-05-20 #1543 ......zzzzzzzzzzzzzzz...............zzzzzz..............zzz.
1518-05-21 #2039 ..............zzzzzzzzzzzzzzzzzz............................
1518-05-22 #1553 .......................................zzzzzzzzzzzzzz...z...
1518-05-23  #587 .............zzzzzzzzzzzzzzzzz..............................
1518-05-24  #199 ..................................................z.........
1518-05-25 #1549 .....................................................zz.....
1518-05-26 #1597 ........zzzz..............zzzzzzzzzzzzzzzzzzzzzz............
1518-05-27 #2113 .....zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz......................
1518-05-28  #181 .......zz..................zzzzzzzzzzzzzzzzzzzz....z........
1518-05-29 #1597 .................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
1518-05-30 #2927 ................zzzzzzzzzzzzzzz.......zzzzzzz...............
1518-05-31 #1489 ........zzzzzzzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzzz..........
1518-06-01  #587 ...............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-06-02 #1549 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........
1518-06-03 #1601 .................................zzzzzzzzzzzzz..............
1518-06-04 #1489 .........................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-06-05 #2699 .......................................zzzzzzzzzzzzzzz......
1518-06-06  #229 ...........................................zzzzz......z.....
1518-06-07  #181 ............zzzzzzzzz.....zzzzzzz...........................
1518-06-08  none ............................................................
1518-06-09  #587 .............................zzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-06-10 #1543 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...zzzzz......
1518-06-11 #1597 .........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.............
1518-06-12 #1553 ..........zzzzzzzzzzzzzzzzzzzz.............zzzzzzz..........
1518-06-13 #1433 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzz.....
1518-06-14 #1489 ..........................zz......zzzzzzzzzzzzzzzzzzzzzzzz..
1518-06-15 #1433 ...............zzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzzzzzzzz...
1518-06-16 #1597 .............zzzzzzzzzzzzzzzzzzzzzzzzzzzz...................
1518-06-17 #1597 .............................zzzzz...............z..........
1518-06-18  #239 .....zzzzzzzzzzzzzzzzzzzzz....................zzzz.......zz.
1518-06-19  none ............................................................
1518-06-20  #199 .............zzzzzzzzzzzzzzzzzzzzzz...............zzzzzzzz..
1518-06-21 #2699 ................................................zzz.....z...
1518-06-22 #1039 ..................zzzzzzzzzzzzzzzzzz........................
1518-06-23 #1549 ......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........
1518-06-24  #199 ...................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........
1518-06-25 #2699 .............zzzzzzzzzzzzzzzzzzzzz....zzzzzzzzzzzzzzzz......
1518-06-26 #1597 ....................zzzzzzzzzzzzz...........................
1518-06-27 #1601 ...........................zzzzzzzzzzzzzzzzzz...............
1518-06-28  #587 .......zzzzzzzzzzzzzzz.............zzzzzzzzzzzzzzz..........
1518-06-29 #1549 .........................zzzzzzzzzzzzzzzzzzzzzz.............
1518-06-30 #2927 zzzzzzzzzzzzzzzz........................zzzzzzzz............
1518-07-01 #2927 ...............................zzzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-07-02  #239 ......zzzzzzzzzzz...............zzzzzzzzzzzzzzzzz...........
1518-07-03 #1489 .....................zzzzzzzzzzzzzz.........zzzzzzzzzzz.....
1518-07-04 #1597 ..........................zzzzz.............................
1518-07-05 #1489 ................zzzzzzz....zzzzzzzzzzzzz....zzz.............
1518-07-06 #3121 ....zzzzzzzzzz...zzzzzzzzzzzzzzzzzzzzzz.....................
1518-07-07 #1433 .......zzzzzzzzzzzzzzzzzzzzz.......zzzzzzzzz....zzzzzzzzzz..
1518-07-08  #593 ......zzzzzzzzzzzzzzzzzzzzzzzzzzz....zzzzz...........zzz....
1518-07-09  #239 ...............................................zzz..........
1518-07-10 #1597 ...........................zzzzz..............zzzzzzz.......
1518-07-11  #199 ......z..........................................zzzzz...zz.
1518-07-12 #1433 ...zzzzzzzzzzzzzzzzzzzzzzz.............zzzzzzzzzzzzz...zzzz.
1518-07-13 #3121 ................zzzzzzz.............zzzzzzzzzzzzzzzzzzz.....
1518-07-14  #229 ............................zzz....................zz....z..
1518-07-15 #1489 ......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..................
1518-07-16 #1489 ..........zzzzz........zzz....zzzzzzzzzzz...................
1518-07-17 #1543 ..................................zzzzzzzzzzzzzzzzzzz.......
1518-07-18 #2113 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......zzzz...
1518-07-19  #587 .....................zzzzzzzzzzzzzzzz.......................
1518-07-20 #3121 ................................zzzzzzzzzzzzzzzzzzzzz.......
1518-07-21  #587 ......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......
1518-07-22 #2699 ...............................................zzzzzzzz.....
1518-07-23 #1543 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzz.............zz..
1518-07-24 #1433 .........................................zzzzzzzzzzzzz......
1518-07-25  none ............................................................
1518-07-26 #1553 ...........................zzzzzzzzzzzzzzzzzzzzzzz..........
1518-07-27  #199 ................................zzzzz.......................
1518-07-28  #587 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-07-29  #593 ....zzzzzzzz.....zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....
1518-07-30 #1601 ......zzzzzzzzzzzzzzzzzzzzzzzzz.............................
1518-07-31 #1549 ..............zzzzzzzzz.....................................
1518-08-01 #1597 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-08-02  #199 ................................zzzzzzzz.........zzzzzzzzz..
1518-08-03  #181 ...............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..........
1518-08-04 #3121 .........................zzzzzzzzzzzzzzzzzzzzz..............
1518-08-05  #181 ................................zzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-08-06 #1039 .......................zzzzzzzzzzzz.........................
1518-08-07 #2113 ...............zzzzzzzzzzzzzzzz................zzzzzzzzz....
1518-08-08 #2699 ...............................................zzzzzzzzz....
1518-08-09 #2113 ..zzzzzzzzzzzzzzzzzzzzzzzzzzz...............zzzzzz.....z....
1518-08-10  none ............................................................
1518-08-11  none ............................................................
1518-08-12 #1601 ....zzzzzzzzzzzzzzzzzzz......zzzzzzzzzzzzzzzzzzzzzzzz.......
1518-08-13  #239 ..............................................zzzzz.....zz..
1518-08-14 #2039 .......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...........
1518-08-15 #2699 ...................................zzzzzzzzzzzzzzzzzzzzzzz..
1518-08-16  #199 ....................................zzzzzzz.................
1518-08-17  #239 ..................................zzzzzzzzzzzzz.............
1518-08-18  #317 ..zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....z..........zzz.
1518-08-19 #3121 ......................zzzz....zzzz..........................
1518-08-20 #2039 ................zzzzzzzzzzzzzzzzzzz..............zzzzz......
1518-08-21  #587 ......................zzzzzzzzzzzz....zzzzzzzzzzzzzzzz......
1518-08-22 #1553 zzzzzzzzzzzzzz.................zzzzzz.......................
1518-08-23 #2039 ...............zzzzzzzzzzzzzzzz............zzzzzz...........
1518-08-24 #1597 ....zzzzzzzzzzzzzzz...zzzzzzzzzzzzzz........................
1518-08-25 #1039 .........................zzzzzzzzzzzzzzzzzzzz....zzzzzzz....
1518-08-26 #1597 .......zzzzzzzzzzzzzzzzzzzzzzzzzzzzz........................
1518-08-27 #1489 ...........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........zzzzzzz....
1518-08-28  #587 ...............................zzzzzzzzzzzzz................
1518-08-29 #3121 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzz....z...
1518-08-30 #1601 .....................zzzzzzzzzzzzzzzzzzzzzzzzzzz............
1518-08-31  #317 ..................zzzzzzzzzzzzzzzzzzz............zzz........
1518-09-01 #2699 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-09-02 #1597 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz......
1518-09-03 #2113 zzzzzzzzzzzzzzzzzzzzz.....................zzzzzzzzzzzz......
1518-09-04 #1553 ..................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-09-05 #1433 ........................................zzzzzzzzzzzzzzzzzz..
1518-09-06 #1039 .............................zzzzzzzzzz.....................
1518-09-07 #1489 ......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-09-08  #229 ............................zzzzzzzzzzzzzzzzzzzzzzz.........
1518-09-09 #1601 ..........................zzzzzzzzzzzzzzzzz.............z...
1518-09-10 #1549 .......................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....
1518-09-11 #3121 ........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........
1518-09-12 #2699 .............zzzzzz.............zzzzzzzzzzzzzzzzzzzz........
1518-09-13 #1553 ...zzzzzzzzzzzzz.............zzzzzzzzzzzzzzzzzzzzz....zzzz..
1518-09-14  #587 ..zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz........zzzzzzzzz....
1518-09-15  none ............................................................
1518-09-16 #3121 ....zzz.................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.....
1518-09-17 #1489 .......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
1518-09-18 #1597 .....zzzzzzzzzzzzzzzzzzz....................................
1518-09-19 #1597 ..........................zzzzzzzzzzzzzzzzzzzzzz............
1518-09-20 #1601 ......................zzzzzzzzzzzz........zzzzzzzzzzzzzzzzz.
1518-09-21 #1433 ....................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..
1518-09-22 #1553 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..........
1518-09-23 #2039 ..............zzzzzzzzzzzzzzzzzzz...............zz..........
1518-09-24 #2927 ....................zzzzzzzzzzzzzzzzzzzzzzzzz...............
1518-09-25 #1597 ............................zzzzz...zzzzzzzzz...............
1518-09-26 #1543 ...zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...........................
1518-09-27 #1553 .................zzzzzzzzzz.......zzzzzzzzzzzzzzzzzzz.......
1518-09-28  #593 ...zzzzzzzzzzzzzzzzzzzzzz...................................
1518-09-29 #1039 ...................................zzzzzzzzzzzzzzzzzzzzzzzz.
1518-09-30 #1549 .........................zzzzzzzzzzzzzzzzzzzzzzzzz..........
1518-10-01  #587 ...................................zzzzzzzz....zzzzz........
1518-10-02 #2039 ......................z.......zz............................
1518-10-03 #1549 .........................................zzz.....zzzzzzzzz..
1518-10-04  #587 ...........................zzzzzzzzzzzzzzzzzzzzzzzzzzzz.....
1518-10-05  none ............................................................
1518-10-06 #1601 ...........................................z....zzzz........
1518-10-07 #3121 ......................zzzzzzzzzz............zzzzzzzz........
1518-10-08  #593 ....zzzzzzzzzzzzzzzzzzzzz.................zzz............zz.
1518-10-09  #181 ...............zzzzzzz......................................
1518-10-10 #3121 ...............................zzzzzzzzzzzzzzzzzzzzzzz......
1518-10-11  #181 ...................zzzzzzzzzzzzzzzzzzzzz....................
1518-10-12 #1601 ......................................................zzzzz.
1518-10-13 #2699 .....zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..............
1518-10-14 #2927 ........................................zzzzzzzzzzzzz.......
1518-10-15  none ............................................................
1518-10-16 #1549 ..............zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
1518-10-17  #181 .......................zzzzzzzzzzzzzzzzzzzzzzzz.............
1518-10-18  #317 ..............zzzzzzzzzzzzzzzzzzz......zzzzzzzzz........z...
1518-10-19 #1549 ......zzzzzzzzzzzzzzzzzz....................................
1518-10-20 #1489 ..........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......
1518-10-21 #1489 .............zzzzzzzzzzzzzzzzzzzzzzzz...........z......zzz..
1518-10-22 #2699 .........................................zzzzzzzzzzzzzzzz...
1518-10-23 #1601 .............................zzzzzzzzzz.....................
1518-10-24 #1549 ...zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.....zzzzzzzzzzz.
1518-10-25 #1553 ...zzzzzzzzzzzzzzzzz.............................zzz........
1518-10-26  #181 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.............
1518-10-27 #1543 ....zzzzzzzzzzzzzzz............zz........zzzzzzzzzzzzzz.....
1518-10-28  #229 ....zzzzzzzzzzzzzzzzzzzzzzz....................zzzzzzzzzzzz.
1518-10-29 #2699 .....................zzzzzzzzzzzzzzzzzzzzzzzzz.......zzzzzz.
1518-10-30  #239 ............................................zzzzzzzzzzzzzz..
1518-10-31 #1597 ...................zzzzz.....zzzzzzzzz......................
1518-11-01 #2699 ...................................zzzzz.......zz.......zzz.
1518-11-02 #2113 .....zzzzzzz................................................
1518-11-03  #587 ............................zzzzzzzzzzzzzzzzzzzzz...........
1518-11-04 #1039 ......................................zzzzzzzzzzzzzzzzzzzz..
1518-11-05 #1549 ......................................................z.....
1518-11-06 #2113 ..........zzzzzzzzzzzzzzzzzzzzzzzz.................zzzzzzz..
1518-11-07  #593 .....................................z......................
1518-11-08 #2113 ...................zzz.......zzzzzzzzzzzzzzzzzzzzzz.........
1518-11-09  #317 ..........................zzzzzzzzzzzzzzzzzzzzzzz...........
1518-11-10 #2039 .........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.............
1518-11-11 #1489 ...........zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz....
1518-11-12 #2699 ..................................zzzzzzzzzzzzzzzzzzz.......
1518-11-13 #1597 .................................zzzzzzzzzzzzzzzzzzzzzzzzzz.
1518-11-14  #239 ......zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.........
1518-11-15 #3121 ..........................zzzz...................zzzzzzzz...
1518-11-16 #1549 ........................zzzzzzzzzzz.........................
1518-11-17 #1601 ..zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.............
1518-11-18  #317 ....................zzzzzzzzzzzzzz................zzzzz.....
1518-11-19  #239 .zzzzzzzz.........................................zzz.......
1518-11-20 #1433 .................zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.......
1518-11-21 #2039 ............zzzzzzzzzzzzzz.......zzzzzzzzz......zzzzzzzzz...
1518-11-22  #317 ....zz.....................zzzzzzzzzzz......................

I like your use of -en suffixes to denote verbs. Makes a lot of sense, actually, easy to glean intent.

Haha, I was tired and started with awaken and I thought asleepen was really hilarious.

Of course once I realized that there was a distinction between going on duty and waking up from a sleep, I was honor-bound to add ondutyen

F#!
I found today the easiest thus far, but it ended up being the most verbose solution. Despite that I'm relatively pleased with it - it runs fast and I find it readable.

namespace Day4
open System.Text.RegularExpressions

module util =
  let getContents fileName =
    System.IO.File.ReadLines(fileName) |> Seq.toList

  type LogEntry =
    | NewGuard of System.DateTime * int
    | Asleep of System.DateTime
    | Awake of System.DateTime
    | Nonsense

  let recordRegex = @"^\[([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-3]{2}):([0-9]{2})] (.*)$"

  let (|FirstRegexGroup|_|) pattern input =
   let m = Regex.Match(input,pattern) 
   if (m.Success) then Some m.Groups.[1].Value else None  

  let sortLog log = List.sortBy (fun el -> 
    match el with
    | LogEntry.Asleep dt -> dt
    | LogEntry.Awake dt -> dt
    | LogEntry.NewGuard (dt, _) -> dt
    | LogEntry.Nonsense -> new System.DateTime()) log

  let scrapeLog inputs =
     List.map (fun logEntry ->
      let m = Regex.Match(logEntry, recordRegex)
      if m.Success then
        let dt =
          new System.DateTime(
            (m.Groups.[1].Value |> System.Convert.ToInt32),
            (m.Groups.[2].Value |> System.Convert.ToInt32),
            (m.Groups.[3].Value |> System.Convert.ToInt32),
            (m.Groups.[4].Value |> System.Convert.ToInt32),
            (m.Groups.[5].Value |> System.Convert.ToInt32),
            0)

        match m.Groups.[6].Value with
        | "falls asleep" -> LogEntry.Asleep (dt)
        | "wakes up" -> LogEntry.Awake (dt)
        | FirstRegexGroup "Guard #([0-9]+) begins shift" badge -> LogEntry.NewGuard (dt, System.Convert.ToInt32 badge)
        | _ -> LogEntry.Nonsense
      else LogEntry.Nonsense) inputs
      |> sortLog


  let first (a, _, _) = a
  let second (_, b, _) = b
  let third (_, _, c) = c

  type MinuteCounts = Map<int, int>

  let getHighest minuteCounts =
    Map.fold (fun s k v -> if v > snd s then (k, v) else s) (0, 0) minuteCounts

  type SingleEntry =
    { TotalAsleep : int
      MinuteCounts : MinuteCounts }
  type SleepLog = Map<int, SingleEntry>

  let tickMinute (mcs: MinuteCounts) m =
    match (mcs.TryFind m) with
    | Some x -> Map.add m (x + 1) mcs
    | None -> Map.add m 1 mcs

  let addRangeToLog sleep wake sleepLogEntry =
    let timeAsleep = wake - sleep
    let eachMinute = [ for i in sleep..wake - 1 do yield i ]
    { TotalAsleep = sleepLogEntry.TotalAsleep + timeAsleep;
      MinuteCounts = List.fold tickMinute sleepLogEntry.MinuteCounts eachMinute }

  let processLog log =
    List.fold
      (fun s el ->
        match el with
        | LogEntry.NewGuard (_, badge) -> (Some badge, second s, third s)
        | LogEntry.Awake dt ->
          match (first s) with
          | Some badge ->
            match (second s) with
            | Some sleepTime ->
              let map: SleepLog = third s 
              let newMap =
                match map.TryFind badge with
                | Some x -> Map.add badge (addRangeToLog sleepTime dt.Minute x) map
                | None -> Map.add badge (addRangeToLog sleepTime dt.Minute {TotalAsleep = 0; MinuteCounts = (new Map<int, int> (Seq.empty))}) map
              (first s, None, newMap)
            | None -> s // This branch indicates an error in the input - there was no "time asleep" stored for a "wakes up" entry
          | None -> s // This branch indicates an error in the input - there was no active badge for a "wakes up" entry
        | LogEntry.Asleep dt -> (first s, Some dt.Minute, third s)
        | LogEntry.Nonsense -> s)
      (None, None, new Map<int, SingleEntry> (Seq.empty)) // Active Badge, Minute Fell Asleep, SleepLog
      log

  let getSleepLog fileName =
    getContents fileName |> scrapeLog |> processLog |> third

module part1 =
  open util

  let getLongestAsleep sleepLog =
    sleepLog |> Map.fold (fun s k v -> if v.TotalAsleep > first s then (v.TotalAsleep, k, getHighest v.MinuteCounts) else s) (0, 0, (0, 0))

  let execute fileName =
    let res = getSleepLog fileName |> getLongestAsleep
    (second res) * (fst (third res))

module part2 =
  open util
  let getMostFrequentAsleep sleepLog =
    sleepLog |> Map.fold (fun s k (v: SingleEntry) ->
      let h = getHighest v.MinuteCounts
      if snd h >= snd (snd s) then
        (k, h)
      else s
      ) (0, (0, 0))

  let execute fileName =
    let res = getSleepLog fileName |> getMostFrequentAsleep
    (fst res) * (fst (snd res))

My Python solution, which I'm less than jazzed about. And I definitely have no interest in redoing it in Go today. I do not have the drive or energy. /shrug

def sort_log(log):
    log = [line.split("]") for line in log]
    log = sorted(log, key=lambda x: datetime.datetime.strptime(x[0], "[%Y-%m-%d %H:%M"))
    return [line[0] + "]" + line[1] for line in log]

def tally_shift(current_guard, guards):
    try:
        previous_shift = current_guard.pop(0)
        if previous_shift not in guards.keys():
            guards[previous_shift] = dict((k, 0) for k in range(60))
    except IndexError:
        pass
    while len(current_guard) > 0:
        guard = guards[previous_shift]
        range_start = int(current_guard.pop(0))
        range_end = int(current_guard.pop(0))
        for m in range(range_start, range_end):
            guard[m] += 1
    return guards

def process_shifts(entries):
    guard_pattern = re.compile(r'^.*#(?P<guard_id>\d*)')
    minute_pattern = re.compile(r'^.*:(?P<minute>\d{2})\]')
    guards = {}
    current_guard = []
    entries = sort_log(entries)
    for entry in entries:
        if "begins shift" in entry:
            guards = tally_shift(current_guard, guards)
            current_guard = []
            current_guard.append(guard_pattern.search(entry).group('guard_id'))
        elif "falls asleep" in entry:
            current_guard.append(minute_pattern.search(entry).group('minute'))
        else:
            current_guard.append(minute_pattern.search(entry).group('minute'))
    return tally_shift(current_guard, guards)

def part_one(schedule):
    current_leader = None
    most_minutes_slept = 0
    minute_most_likely_asleep = 0
    for k, v in process_shifts(schedule).items():
        total_mins_asleep = sum(v.values())
        minute_most_slept_through = max(v.values())
        if total_mins_asleep >= most_minutes_slept:
            current_leader = k
            most_minutes_slept = total_mins_asleep
            minute_most_likely_asleep = [min for min, freq in v.items() if freq == minute_most_slept_through]

    return int(current_leader) * minute_most_likely_asleep.pop()

def part_two(schedule):
    current_leader = None
    max_global_frequency = 0
    for k, v in process_shifts(schedule).items():
        max_freq = max(v.values())
        if max_freq >= max_global_frequency:
            max_global_frequency = max_freq
            current_leader = k
            most_freq = [min for min, freq in v.items() if freq == max_freq][0]

    return int(current_leader) * most_freq

I have been trying to learn F#, coming from C#, and I'm quite "happy" with my previous solutions. This one challenged me more, having to think 'functional'! I am not happy with how it looks, but here it is anyway!

My other attempts are on github. Critique is highly welcome!

I made a datastructure that mapped guards to days to minutes with type: Map<string,Map<string,bool array> which made it 'easy' (or so I though) to extract the data.

let rec handle guard (datastructure:Map<string,Map<string,bool []>>) (arr:string list) =
    match arr with
    | head :: tail ->
        match head with
        | Regex @"\[(\d+)-(\d\d)-(\d\d) (\d+):(\d+)\] (.+)" [year; month; day; hour; min; action] ->
            let date = new DateTime(year|>int,month|>int,day|>int,hour|>int,min|>int,0)
            let origday = date.ToString "MM-dd"
            let newdate =
                if date.Hour = 0 then
                    date
                else
                    new DateTime (date.AddDays(1.0).Year,date.AddDays(1.0).Month,date.AddDays(1.0).Day)
            let newday = newdate.ToString "MM-dd"
            match action with
            | Regex @"Guard #(\d+)" [newguard] ->
                if datastructure.ContainsKey newguard then
                    if not (datastructure.[newguard].ContainsKey newday) then
                        let newDs =
                            let minutes : bool array = Array.zeroCreate 60
                            let newDay =
                                if hour = "00" then
                                    minutes.[min|>int] <- true
                                else
                                    minutes.[0] <- true
                                datastructure.[newguard]
                                |> Map.add newday minutes
                            datastructure |> Map.add newguard newDay
                        handle newguard newDs tail
                    else
                        handle newguard datastructure tail
                else
                    // create new level in datastructure
                    // new array of waking minutes, default to sleep
                    let minutes : bool array = Array.zeroCreate 60
                    // guard starts out as awake
                    let newMap =
                        Map.empty
                        |> Map.add newday minutes
                    let newDs =
                        datastructure
                        |> Map.add newguard newMap
                    handle newguard newDs tail
            | Regex @"wakes up" [] ->
                // set time to awake
                datastructure.[guard].[newday].[min|>int] <- true
                handle guard datastructure tail
            | Regex @"falls asleep" []->
                // set all times before this to awake
                let maxMin = min|> int
                let minMin =
                    try
                        Array.findIndexBack (fun a -> a) datastructure.[guard].[newday].[0..maxMin]
                    with
                        | :? KeyNotFoundException as ex -> 0

                for i in [minMin..maxMin-1] do
                    datastructure.[guard].[newday].[i] <- true
                handle guard datastructure tail
            | _ -> handle guard datastructure tail
        | _ -> handle guard datastructure tail
    | [] ->
        // guard ends his day awake
        Map.iter (fun g dm ->
            Map.iter (fun d ms ->
                let minMin =
                    try
                        Array.findIndexBack (fun a -> a) ms
                    with
                        | :? KeyNotFoundException as ex -> 0
                for i in [minMin..59] do
                    ms.[i] <- true
            ) dm ) datastructure
        // guard starts his day awake
        Map.iter (fun g dm ->
            Map.iter (fun d ms ->
                let minMin =
                    try
                        Array.findIndex (fun a -> a) ms
                    with
                        | :? KeyNotFoundException as ex -> 0
                for i in [0..minMin] do
                    ms.[i] <- true
            ) dm ) datastructure
        datastructure

Then Part 1

let testinput ="[1518-11-01 00:00] Guard #10 begins shift
[1518-11-01 00:05] falls asleep
[1518-11-01 00:25] wakes up
[1518-11-01 00:30] falls asleep
[1518-11-01 00:55] wakes up
[1518-11-01 23:58] Guard #99 begins shift
[1518-11-02 00:40] falls asleep
[1518-11-02 00:50] wakes up
[1518-11-03 00:05] Guard #10 begins shift
[1518-11-03 00:24] falls asleep
[1518-11-03 00:29] wakes up
[1518-11-04 00:02] Guard #99 begins shift
[1518-11-04 00:36] falls asleep
[1518-11-04 00:46] wakes up
[1518-11-05 00:03] Guard #99 begins shift
[1518-11-05 00:45] falls asleep
[1518-11-05 00:55] wakes up"

    let stopwatch = System.Diagnostics.Stopwatch.StartNew()

    let split =
        split testinput
        |> Array.sort

    // (guard,day,minute) -> awake
    let datastruct =
        split
        |> Array.toList
        |> handle "" Map.empty

    stopwatch.Stop()
    printfn "creating datastructure took %i milliseconds" stopwatch.ElapsedMilliseconds
    stopwatch.Restart()

    // find guard that is asleep most
    let sleepyGuard =
        datastruct
        |> Map.map (fun guard day ->
            Map.map (fun day mins ->
                Array.sumBy (fun awake -> if awake then 0 else 1) mins
            ) day
        )
        |> Map.map (fun guard day ->
            day
            |> Map.fold (fun acc day mins -> acc+mins ) 0
        )
        |> Map.toList
        |> List.maxBy (fun (g,min) -> min)
        |> fst
    stopwatch.Stop()
    printfn "sleepiest guard is %s" sleepyGuard
    printfn "\t calculation too %i milliseconds" stopwatch.ElapsedMilliseconds
    stopwatch.Restart()
    // find that guards most likely minute of being asleep

    let sleepyMinute =
        datastruct.[sleepyGuard]
        |> Map.map (fun day mins -> Array.indexed mins)
        |> Map.toList
        |> List.map (fun i -> snd i)
        |> List.collect (fun arr -> Array.toList arr)
        |> List.groupBy (fun (index, awake) -> index)
        |> List.map (fun arr -> snd arr)
        |> List.map (fun a -> List.map (fun b -> snd b) a)
        |> List.map (fun a -> a |> List.filter (fun awake -> not awake) |> List.length )
        |> List.indexed
        |> List.maxBy (fun (index,m) -> m)
        |> fst

    stopwatch.Stop()
    printfn "sleepiest minute of guard %s is %i" sleepyGuard sleepyMinute
    printfn "result of day 4 part 1 is: %i" ((sleepyGuard|>int) * sleepyMinute)
    printfn "\t calculation took %i milliseconds" stopwatch.ElapsedMilliseconds

and Part 2

let popular =
        datastruct
        |> Map.map (fun guard day ->
            Map.toList day
            |> List.map snd
            |> List.map (fun i -> Array.toList i |> List.indexed)
            |> List.concat
            //|> List.filter (fun (_,awake) -> not awake)
            |> List.groupBy (fun (i,_) -> i)
            |> List.map (fun (i,l) -> (i,(List.fold (fun acc (_,awake) -> if awake then acc else acc+1) 0 l)))
            |> List.maxBy (fun (_,l) -> l)
        )
        |> Map.toList
        |> List.maxBy (fun (_,(_,t)) -> t)

    stopwatch.Stop()
    printfn "Guard %s spent minute %i asleep more than anyone: %i times" (fst popular) (fst (snd popular)) (snd (snd popular))
    printfn "result of day 4 part 2 is: %i" (((fst popular)|>int) * (fst (snd popular)))
    printfn "\t calculation took %i milliseconds" stopwatch.ElapsedMilliseconds

Didn't really have much time to do the challenges today, but yeah, whenever you did the first three days it's hard to skip one.

So here are my solutions in Elixir. Some function returns are a bit messy, but it does what it needs to do. The basic idea is that all lines are first parsed into a map with guards, where each guard is the key for another map containing the minutes and the number of times the guard was asleep during that minute.

Having that basic map makes the calculations for both strategies relatively easy.

Common:

defmodule AoC.DayFour.Common do
  @line_regex ~r/\[(.*)\] (.*)/
  @date_format "{YYYY}-{M}-{D} {h24}:{m}"

  def read_input(path) do
    path
    |> File.stream!()
    |> Stream.map(&String.trim_trailing/1)
    |> Enum.to_list()
    |> Enum.sort()
    |> Enum.map(fn x ->
      [datetime, log] = Regex.run(@line_regex, x, capture: :all_but_first)
      {:ok, datetime} = Timex.parse(datetime, @date_format)
      %{datetime: datetime, log: log}
    end)
  end

  def calculate_guard_minutes(input, map \\ %{}, current_guard \\ "", last_minute \\ 0)

  def calculate_guard_minutes([line | rest], map, current_guard, last_minute) do
    case line.log do
      "falls asleep" ->
        calculate_guard_minutes(rest, map, current_guard, line.datetime.minute)

      "wakes up" ->
        map = Map.put_new(map, current_guard, %{})
        guard = Map.get(map, current_guard)

        guard =
          Enum.reduce(last_minute..(line.datetime.minute - 1), guard, fn x, acc ->
            Map.update(acc, x, 1, &(&1 + 1))
          end)

        map = Map.put(map, current_guard, guard)
        calculate_guard_minutes(rest, map, current_guard, line.datetime.minute)

      _ ->
        [new_guard] =
          Regex.run(~r/^Guard #(\d+) begins shift$/, line.log, capture: :all_but_first)

        calculate_guard_minutes(rest, map, new_guard, 0)
    end
  end

  def calculate_guard_minutes([], map, _current_guard, _last_minute) do
    map
  end
end

Part one:

defmodule AoC.DayFour.PartOne do
  alias AoC.DayFour.Common

  def main() do
    "lib/day4/input.txt"
    |> Common.read_input()
    |> Common.calculate_guard_minutes()
    |> get_sleepiest_guard()
    |> get_sleepiest_minute()
    |> calculate_result()
  end

  defp calculate_result({guard, minute}) do
    guard * minute
  end

  defp get_sleepiest_minute(map) do
    {_, {guard, minutes}} = map

    {sleepiest_minute, _} =
      # Format: {guard, minutes_slept}
      Enum.reduce(minutes, {0, 0}, fn {current_minute, count}, {highest_minute, highest_count} ->
        if count > highest_count,
          do: {current_minute, count},
          else: {highest_minute, highest_count}
      end)

    {String.to_integer(guard), sleepiest_minute}
  end

  defp get_sleepiest_guard(map) do
    # Format: {minutes_slept, {guard, minutes}}
    Enum.reduce(map, {0, {0, %{}}}, fn {guard, minutes}, sleepiest_guard ->
      minutes_slept = Enum.reduce(minutes, 0, fn {_, count}, acc -> acc + count end)
      {current_sleep_minutes, _} = sleepiest_guard

      if minutes_slept > current_sleep_minutes,
        do: {minutes_slept, {guard, minutes}},
        else: sleepiest_guard
    end)
  end
end

Part two:

defmodule AoC.DayFour.PartTwo do
  alias AoC.DayFour.Common

  def main() do
    "lib/day4/input.txt"
    |> Common.read_input()
    |> Common.calculate_guard_minutes()
    |> get_sleepiest_guard()
    |> calculate_result()
  end

  defp calculate_result({guard, minute}) do
    guard * minute
  end

  defp get_sleepiest_guard(map) do
    # Format: {guard, minute, count}
    {guard, minute, _} =
      Enum.reduce(map, {0, 0, 0}, fn {guard, minutes}, sleepiest_guard ->
        # Format: {minute, count}
        {minute, count} =
          Enum.reduce(minutes, {0, 0}, fn {minute, count}, {current_minute, highest_count} ->
            if count > highest_count, do: {minute, count}, else: {current_minute, highest_count}
          end)

        {_, _, current_count} = sleepiest_guard

        if count > current_count,
          do: {String.to_integer(guard), minute, count},
          else: sleepiest_guard
      end)

    {guard, minute}
  end
end

JavaScript solution

reader.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
};

04-common.js

class SleepingSchedule {
    constructor(guard) {
        this.guard = guard;
        this.guard.schedules.push(this);

        this.minutesSlept = [];
    }

    setDate(year, month, day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    addNap(start, end) {
        const length = end - start;
        this.minutesSlept.push(...Array.from({ length }, (e, i) => start + i));
        this.guard.maxMinutesSlept += length;
    }
}

class Guard {
    constructor(id) {
        this.id = id;
        this.maxMinutesSlept = 0;
        this.schedules = [];
    }
}

const buildGuards = lines => {
    const beginsShiftRegex = /^\[(?<year>\d+)-(?<month>\d+)-(?<day>\d+)\s(?<hour>\d+):(?<minute>\d+)\]\sGuard\s#(?<id>\d+) begins shift$/;
    const fallsAsleepRegex = /^\[(?<year>\d+)-(?<month>\d+)-(?<day>\d+)\s(?<hour>\d+):(?<minute>\d+)\]\sfalls\sasleep$/;
    const wakesUpRegex = /^\[(?<year>\d+)-(?<month>\d+)-(?<day>\d+)\s(?<hour>\d+):(?<minute>\d+)\]\swakes\sup$/;

    const sleepingSchedules = new Map();
    const guards = new Map();

    let currentSchedule;
    let hasNapped;
    let napStartMinute;
    let currentGuard;
    for (let line of lines) {
        let match = line.match(beginsShiftRegex);
        if (match) {
            if (napStartMinute) {
                currentSchedule.addNap(napStartMinute, 60);
            }

            const { id } = match.groups;

            if (guards.has(id)) {
                currentGuard = guards.get(id);
            }
            else {
                currentGuard = new Guard(id);
                guards.set(id, currentGuard);
            }
            currentSchedule = new SleepingSchedule(currentGuard);

            hasNapped = false;
            napStartMinute = undefined;
        }
        else {
            match = line.match(fallsAsleepRegex);
            if (match) {
                if (!hasNapped) {
                    const { year, month, day } = match.groups;
                    currentSchedule.setDate(year, month, day);
                    hasNapped = true;
                }
                const { minute } = match.groups;
                napStartMinute = +minute;
            }
            else {
                match = line.match(wakesUpRegex);
                if (match) {
                    const { minute } = match.groups;
                    currentSchedule.addNap(napStartMinute, +minute);
                    napStartMinute = undefined;
                }
            }
        }
    }
    return guards;
};

module.exports = { 
    SleepingSchedule,
    Guard,
    buildGuards
};

04a.js

const { readFile } = require('./reader');
const {
    SleepingSchedule,
    Guard,
    buildGuards
} = require('./04-common');

const findZonkedGuard = guards => {
    return [...guards.values()].reduce((zonkedGuard, guard) => {
        if (guard.maxMinutesSlept > zonkedGuard.maxMinutesSlept) {
            zonkedGuard = guard;
        }
        return zonkedGuard;
    });
};

const findMostAsleepMinute = guard => {
    const minutesCount = Array.from({ length: 60 }, m => 0);
    for (let i = 0; i < 60; i++) {
        for (let schedule of guard.schedules) {
            minutesCount[i] += +(schedule.minutesSlept.indexOf(i) > -1);
        }
    }

    const mostAsleepMinuteCount = Math.max(...minutesCount);
    return minutesCount.indexOf(mostAsleepMinuteCount);
};

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

    const guards = buildGuards(lines);
    const zonkedGuard = findZonkedGuard(guards);
    const mostAsleepMinute = findMostAsleepMinute(zonkedGuard);

    const solution = +zonkedGuard.id * mostAsleepMinute;
    console.log(`The ID of the guard multiplied by the minute is ${solution}`);
})();

04b.js

const { readFile } = require('./reader');
const {
    SleepingSchedule,
    Guard,
    buildGuards
} = require('./04-common');


const findMostAsleepMinute = guard => {
    const minutesCount = Array.from({ length: 60 }, m => 0);
    for (let i = 0; i < 60; i++) {
        for (let schedule of guard.schedules) {
            minutesCount[i] += +(schedule.minutesSlept.indexOf(i) > -1);
        }
    }

    const mostAsleepMinuteCount = Math.max(...minutesCount);
    return minutesCount.indexOf(mostAsleepMinuteCount);
};

const findMostFrequentlySleptMinute = guards => {
    const minutesMostSlept = [];

    // Finding the guard which slept the most minutes for every minute
    for (let i = 0; i < 60; i++) {
        const guardsWhichSleptThisMinute = new Map();
        for (let guard of guards.values()) {
            let minutesThisGuardSlept = 0;
            for (let schedule of guard.schedules) {
                minutesThisGuardSlept += +(schedule.minutesSlept.indexOf(i) > -1);
            }
            guardsWhichSleptThisMinute.set(guard.id, minutesThisGuardSlept);
        }

        let zonkedGuardId;
        let maxMinutesSlept = 0;
        for (let [guardId, minutes] of guardsWhichSleptThisMinute.entries()) {
            if (minutes > maxMinutesSlept) {
                maxMinutesSlept = minutes;
                zonkedGuardId = guardId;
            }
        }

        minutesMostSlept[i] = { zonkedGuardId, maxMinutesSlept };
    }

    // Finding the guard which slept the most minutes total
    let zonkedGuardIdTotal;
    let maxMinutesSleptTotal = 0;
    let minuteMostSleptTotal = -1;
    for (let i = 0; i < 60; i++) {
        const { zonkedGuardId, maxMinutesSlept } = minutesMostSlept[i];
        if (maxMinutesSlept > maxMinutesSleptTotal) {
            maxMinutesSleptTotal = maxMinutesSlept;
            zonkedGuardIdTotal = zonkedGuardId;
            minuteMostSleptTotal = i;
        }
    }

    return { zonkedGuardIdTotal, minuteMostSleptTotal }
}

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

    const guards = buildGuards(lines);
    const { zonkedGuardIdTotal, minuteMostSleptTotal } = findMostFrequentlySleptMinute(guards);

    const solution = +zonkedGuardIdTotal * minuteMostSleptTotal;
    console.log(`The ID of the guard multiplied by the minute is ${solution}`);
})();

Had a particularly long day at work, so got to the problem late and only just now finished. I'm with all of you that this one was less fun...I haven't refactored since I don't have much brain left for today, but here's my solution for now!

#!/usr/bin/env python

import operator
import re
from collections import defaultdict

RECORD_PATTERN = re.compile('\[.* \d\d:(\d\d)\] .*')
GUARD_ID_PATTERN = re.compile('.* Guard #(\d+) begins shift')


if __name__ == '__main__':
    with open('input.txt') as guard_records_file:
        guard_records = guard_records_file.read().splitlines()
        guard_records.sort()

    minute_data = defaultdict(lambda: defaultdict(int))
    guard_data = defaultdict(lambda: defaultdict(int))

    for guard_record in guard_records:
        minute = int(RECORD_PATTERN.match(guard_record).group(1))

        if 'begins shift' in guard_record:
            just_started_shift = True
            current_guard = int(GUARD_ID_PATTERN.match(guard_record).group(1))
        elif 'wakes up' in guard_record:
            minutes_asleep = minute - fell_asleep_minute
            for sleep_minute in range(fell_asleep_minute, minute):
                guard_data[current_guard][sleep_minute] += 1
                minute_data[sleep_minute][current_guard] += 1
        elif 'falls asleep' in guard_record:
            fell_asleep_minute = minute

    # Part 1
    guard_who_slept_most = max(guard_data, key=lambda key: sum(guard_data[key].values()))
    sleepiest_minute_for_guard = max(minute_data, key=lambda key: minute_data[key][guard_who_slept_most])
    print(guard_who_slept_most * sleepiest_minute_for_guard)

    # Part 2
    guard_with_sleepiest_minute = max(guard_data, key=lambda key: max(guard_data[key].values()))
    sleepiest_minute_for_guard = max(guard_data[guard_with_sleepiest_minute], key=lambda key: guard_data[guard_with_sleepiest_minute][key])
    print(guard_with_sleepiest_minute * sleepiest_minute_for_guard)

Had an afterwork event that included drinks which didn't mix well with the problem description ;-) Also it's past midnight here by now, so it's not exactly the most elegant code:

require 'date'

TIMESTAMP_REGEX = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.freeze
ID_REGEX = /#(?<id>\d+)/.freeze
MINUTE_REGEX = /(?<minutes>\d\d)\]/.freeze

records = DATA.readlines.sort_by { |line| DateTime.parse(line[TIMESTAMP_REGEX]) }
grouped_records = records.chunk_while { |_l1, l2| l2 !~ /Guard/ }

sleep_ranges = Hash.new { |h, k| h[k] = [] }
grouped_records.each do |records|
  guard_id = records.shift.match(ID_REGEX)[:id].to_i
  sleep_ranges[guard_id] += records.each_slice(2).map { |period|
    Range.new(
      *period.map { |entry| entry.match(MINUTE_REGEX)[:minutes].to_i },
      true
    )
  }
end

sleep_minutes = sleep_ranges.map { |id, ranges|
  [
    id,
    (0..59).each_with_object(Hash.new(0)) { |minute, counts|
      counts[minute] = ranges.count { |r| r.include?(minute) }
    }
  ]
}.to_h

id, = sleep_ranges.max_by { |_, rs| rs.sum(&:size) }
puts id * sleep_minutes[id].max_by(&:last).first

id, minutes = sleep_minutes.max_by { |id, minutes| minutes.max_by(&:last).last }
puts id * minutes.max_by(&:last).first

__END__
[1518-06-25 23:58] Guard #1069 begins shift
[1518-09-16 00:24] falls asleep
[1518-04-06 00:56] wakes up
...

Done! Completed my solution with just minutes to spare before the next day goes live! I thought I would like this challenge. I did not. It was fine, for the most part. But I spent at least fifteen minutes wondering why my solution wasn't working before I realized the #!@%#@!$! LOGS WERE NOT IN CHRONOLOGICAL ORDER.

Let me tell you: I was not calm in that moment.

After I used VS Code's built-in sorting functionality to pre-sort the input text, it was mostly smooth sailing. However, rust gurus, before I sorted my input, every time I ran cargo run, the output would be a different answer, even with no changes to the code. Anybody know why that might be? I would expect a static input file and static code to make the same result every time.

Anyways.

Part 1

use std::collections::HashMap;

/// Day 4: Repose Record
/// 
/// Track the sleeping times of various security guards

// Part 1: Find the guard who slept most and their most-slept minute

/// The security team (made up of a bunch of Guards) that we're monitoring
struct SecurityTeam {
    guards: HashMap<usize, Guard>,
}

impl SecurityTeam {
    pub fn new() -> Self {
        Self { guards: HashMap::new() }
    }

    /// Loads in a log-file-like schedule from text
    /// 
    /// Entries have the form '[YYYY-MM-DD HH:MM] message'
    /// Possible messages are 'Guard #<id> begins shift'
    ///                       'falls asleep'
    ///                       'wakes up'
    /// Assumes that the logs are in chronological order (WHICH IS SANE)
    pub fn load_schedule(&mut self, text: &str) {
        let mut current_guard = 0;
        let mut current_state = "";
        let mut sleep_start = 0;

        for line in text.lines() {
            // Yeah I'm hard-coding the minutes location, DON'T JUDGE ME
            let minute: usize = line[15..17].parse().expect("Minutes weren't a number");
            let action = &line[19..];
            if action.contains("Guard") {
                // New guard starting shift.
                let id = action.trim_matches(|c: char| !c.is_numeric()).parse().expect("No guard ID");
                self.guards.entry(id).or_insert(Guard::new(id));
                current_guard = id;
                current_state = "awake";
            } else if action.contains("falls asleep") {
                // Ignores double-falls asleep
                if current_state == "awake" {
                    current_state = "asleep";
                    sleep_start = minute;
                }
            } else if action.contains("wakes up") {
                // Ignores double-wakes
                if current_state == "asleep" {
                    self.guards.get_mut(&current_guard).unwrap().track_sleep(sleep_start, minute);
                    current_state = "awake";
                }
            }
        }
    }

    /// Returns the guard with the overall most minutes asleep
    pub fn sleepiest_guard(&self) -> &Guard {
        self.guards.values()
            .max_by_key(|guard| guard.total_minutes_asleep())
            .expect("No guards on team")
    }
}

/// A security guard.  He keeps track of his own sleep times (what a great person)!
struct Guard {
    id: usize,
    sleep_minutes: HashMap<usize, usize>,
}

impl Guard {
    pub fn new(id: usize) -> Self {
        Self { id, sleep_minutes: HashMap::new() }
    }

    pub fn id(&self) -> usize {
        self.id
    }

    /// Returns the minute in which this guard most commonly slept
    /// 
    /// Accounts for the possibility that this guard doesn't suck at their
    /// job and stays awake the whole time.
    pub fn sleepiest_minute(&self) -> usize {
        *self.sleep_minutes.iter()
            .max_by_key(|(_id, minutes)| **minutes)
            .unwrap_or((&0, &0))
            .0
    }

    /// Sums up this guards total sleeping time
    pub fn total_minutes_asleep(&self) -> usize {
        self.sleep_minutes.values().sum()
    }

    /// Logs in a length of time where the guard was asleep
    pub fn track_sleep(&mut self, asleep: usize, awake: usize) {
        for minute in asleep..awake {
            *self.sleep_minutes.entry(minute).or_insert(0) += 1
        }
    }
}

/// Part 1 asks for the ID of the guard who slept the most multiplied by
/// the minute they slept most
pub fn part1(text: &str) -> usize {
    let mut guards = SecurityTeam::new();
    guards.load_schedule(text);
    let sleepy = guards.sleepiest_guard();
    sleepy.id() * sleepy.sleepiest_minute()
}

Part 2

Part 2 went pretty quick now that I've got all this infrastructure in place.

// Part 2

impl SecurityTeam {
    /// Returns the guard that fell asleep the most on the same minute
    pub fn most_consistent_sleeper(&self) -> &Guard {
        self.guards.values()
            .max_by_key(|guard| guard.sleep_on(guard.sleepiest_minute()))
            .expect("No guards on team")
    }
}

impl Guard {
    /// Returns the amount of times this guard slept on a given minute
    pub fn sleep_on(&self, minute: usize) -> usize {
        *self.sleep_minutes.get(&minute).unwrap_or(&0)
    }
}

/// Part two asks for the ID of the guard who slept the most on a single
/// minute multiplied by that minute
pub fn part2(text: &str) -> usize {
    let mut guards = SecurityTeam::new();
    guards.load_schedule(text);
    let consistent_guard = guards.most_consistent_sleeper();
    consistent_guard.id() * consistent_guard.sleepiest_minute()
}

Someone needs to talk to these elves about the awful ways they're recording and storing data.

Data integrity is important people. Especially at this kind of scale!

Classic DEV Post from Oct 8

Start-up v Corporate, which do you prefer?

Becoming a Corporate is bad, staying a Start-up forever is good. Or is it?

Ryan Palo
Ryan is a mechanical engineer in the East SF Bay Area with a focus on dynamic languages like Ruby & Python. Goal: learn a ton and become a physics, math, and programming teacher. Message me on DEV.TO

Do you write code almost every day?

Join dev.to ❤️