Advent of Code 2018: Day 4

Wow, taking the effort to make a reasonable API for these one off scripts really takes it out of you, I kind of give up half way today. I think today will be the last where I actually consider the readability and reusability of my answers.

From tomorrow I'll be moving to Kotlin to make data objects easier to make and I'll stop worrying about ensuring my objects are immutable etc.

In today's task we're given a list of log entries of the form:

[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

Part 1

For part 1 we need to first find the guard who spends the most time asleep, and then find the minute that they're most likely to be asleep.

Part 2

For part 2 we need to find which guard is most frequently asleep at the same minute, and find which minute that is.

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toMap;

abstract class LogEntry {
    protected final LocalDateTime dateTime;
    static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    LocalDateTime dateTime() {
        return dateTime;
    }

    protected LogEntry(LocalDateTime dateTime) {
        this.dateTime = dateTime;
    }

    static LogEntry fromString(String logString) {
        Matcher matcher;

        matcher = BeginLogEntry.pattern.matcher(logString);
        if (matcher.matches()) {
            return new BeginLogEntry(matcher);
        }

        matcher = FallAsleepLogEntry.pattern.matcher(logString);
        if (matcher.matches()) {
            return new FallAsleepLogEntry(matcher);
        }

        matcher = WakeUpLogEntry.pattern.matcher(logString);
        if (matcher.matches()) {
            return new WakeUpLogEntry(matcher);
        }

        throw new IllegalArgumentException(String.format("LogString is not valid: %s", logString));
    }
}


class BeginLogEntry extends LogEntry {
    protected static final Pattern pattern = Pattern.compile("\\[(.+)\\] Guard #(\\d+) begins shift");

    private final int guardId;

    BeginLogEntry(Matcher matcher) {
        super(LocalDateTime.parse(matcher.group(1), dateTimeFormatter));
        this.guardId = Integer.parseInt(matcher.group(2));
    }

    int guardId() {
        return guardId;
    }

    @Override
    public String toString() {
        return String.format("[%s] Guard #%d begins shift", dateTime.format(dateTimeFormatter), guardId);
    }
}

class FallAsleepLogEntry extends LogEntry {
    protected static final Pattern pattern = Pattern.compile("\\[(.+)\\] falls asleep");

    FallAsleepLogEntry(Matcher matcher) {
        super(LocalDateTime.parse(matcher.group(1), dateTimeFormatter));
    }

    @Override
    public String toString() {
        return String.format("[%s] falls asleep", dateTime.format(dateTimeFormatter));
    }
}

class WakeUpLogEntry extends LogEntry {
    protected static final Pattern pattern = Pattern.compile("\\[(.+)\\] wakes up");

    WakeUpLogEntry(Matcher matcher) {
        super(LocalDateTime.parse(matcher.group(1), dateTimeFormatter));
    }

    @Override
    public String toString() {
        return String.format("[%s] wakes up", dateTime.format(dateTimeFormatter));
    }
}

class Shift {
    private int guardId;
    private LocalDateTime start;
    private Collection<Period> naps;

    private Shift(int guardId, LocalDateTime start, Collection<Period> naps) {
        this.guardId = guardId;
        this.start = start;
        this.naps = new ArrayList<>(naps);
    }

    int guardId() {
        return guardId;
    }

    LocalDateTime start() {
        return start;
    }

    Collection<Period> naps() {
        return Collections.unmodifiableCollection(naps);
    }

    int calculateTotalMinutesSleeping() {
        return naps.stream()
                .mapToInt(p -> (int) p.start().until(p.end(), ChronoUnit.MINUTES))
                .sum();
    }

    @Override
    public String toString() {
        return String.format("[%s] #%d %d naps, %d total minutes sleeping",
                start, guardId, naps.size(), calculateTotalMinutesSleeping());
    }

    static Builder builder() {
        return new Builder();
    }

    static class Builder {
        private int guardId;
        private LocalDateTime start;
        private Collection<Period> naps = new ArrayList<>();

        Builder guardId(int guardId) {
            this.guardId = guardId;
            return this;
        }

        Builder start(LocalDateTime start) {
            this.start = start;
            return this;
        }

        Builder addNap(Period period) {
            naps.add(period);
            return this;
        }

        Shift build() {
            return new Shift(guardId, start, naps);
        }
    }
}

class Period {
    private final LocalDateTime start;
    private final LocalDateTime end;

    Period(LocalDateTime start, LocalDateTime end) {
        this.start = start;
        this.end = end;
    }

    static Builder builder() {
        return new Builder();
    }

    LocalDateTime start() {
        return start;
    }

    LocalDateTime end() {
        return end;
    }

    static class Builder {
        private LocalDateTime start;
        private LocalDateTime end;

        Builder start(LocalDateTime start) {
            this.start = start;
            return this;
        }

        Builder end(LocalDateTime end) {
            this.end = end;
            return this;
        }

        Period build() {
            return new Period(start, end);
        }
    }
}

public class GuardTracker {
    private static class MostOftenSlept {
        private final int minute;
        private final int count;

        MostOftenSlept(int minute, int count) {
            this.minute = minute;
            this.count = count;
        }

        int minute() {
            return minute;
        }

        int count() {
            return count;
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Error: First argument must be log string");
            System.exit(1);
        }

        String logString = args[0];

        List<LogEntry> log = parseLogString(logString);
        List<Shift> shifts = logToShifts(log);
        Map<Integer, List<Shift>> guardShifts = groupShiftsByGuardId(shifts);

        // Part 1
        int mostSleepGuardId = findMostSleepingGuardId(guardShifts);
        int minuteMostOftenSlept = calculateMinuteMostOftenSlept(guardShifts.get(mostSleepGuardId)).minute();

        System.out.println(String.format("Part 1: %d", mostSleepGuardId * minuteMostOftenSlept));

        // Part 2
        Map<Integer, MostOftenSlept> guardMostOftenSlept = calculateGuardMostOftenSlept(guardShifts);
        Map.Entry<Integer, MostOftenSlept> part2 = guardMostOftenSlept.entrySet().stream()
                .max(Comparator.comparingInt(a -> a.getValue().count()))
                .get();

        System.out.println(String.format("Part 2: %d", part2.getKey() * part2.getValue().minute()));
    }

    private static List<LogEntry> parseLogString(String logString) {
        return Arrays.stream(logString.split("\n")).map(LogEntry::fromString).collect(Collectors.toList());
    }

    private static Map<Integer, List<Shift>> groupShiftsByGuardId(List<Shift> shifts) {
        return shifts.stream()
                .collect(Collectors.groupingBy(Shift::guardId));
    }

    private static int findMostSleepingGuardId(Map<Integer, List<Shift>> guardShifts) {
        int mostSleepGuardId = 0;
        int mostSleepAmount = 0;

        for (int guardId : guardShifts.keySet()) {
            int sleepAmount = calculateTotalSleepAmount(guardShifts.get(guardId));
            if (sleepAmount > mostSleepAmount) {
                mostSleepAmount = sleepAmount;
                mostSleepGuardId = guardId;
            }
        }

        return mostSleepGuardId;
    }

    private static Map<Integer, MostOftenSlept> calculateGuardMostOftenSlept(Map<Integer, List<Shift>> guardShifts) {
        return guardShifts.entrySet().stream()
                .collect(toMap(Map.Entry::getKey, e -> calculateMinuteMostOftenSlept(e.getValue())));
    }

    private static MostOftenSlept calculateMinuteMostOftenSlept(Collection<Shift> shifts) {
        Map<Integer, Integer> sleepCountByMinute = new HashMap<>();
        for (Shift shift: shifts) {
            for (Period nap : shift.naps()) {
                for (int minute = nap.start().getMinute(); minute < nap.end().getMinute(); minute++) {
                    sleepCountByMinute.put(minute, sleepCountByMinute.getOrDefault(minute, 0) + 1);
                }
            }
        }

        if (sleepCountByMinute.isEmpty()) {
            return new MostOftenSlept(0, 0);
        }

        Map.Entry<Integer, Integer> count = sleepCountByMinute.entrySet().stream()
                .max(Comparator.comparing(Map.Entry::getValue))
                .get();

        return new MostOftenSlept(count.getKey(), count.getValue());
    }

    private static int calculateTotalSleepAmount(Collection<Shift> shifts) {
        return shifts.stream().mapToInt(Shift::calculateTotalMinutesSleeping).sum();
    }

    private static List<Shift> logToShifts(List<LogEntry> log) {
        List<LogEntry> logCopy = new ArrayList<>(log);
        logCopy.sort(Comparator.comparing(LogEntry::dateTime));
        List<Shift> shifts = new ArrayList<>();

        int index = 0;

        Shift.Builder shiftBuilder = null;
        Period.Builder napBuilder = null;

        while (index < logCopy.size()) {
            LogEntry logEntry = logCopy.get(index);
            if (logEntry instanceof BeginLogEntry) {
                if (shiftBuilder != null) {
                    shifts.add(shiftBuilder.build());
                }

                BeginLogEntry beginLogEntry = (BeginLogEntry) logEntry;
                shiftBuilder = Shift.builder();
                shiftBuilder.guardId(beginLogEntry.guardId());
                shiftBuilder.start(beginLogEntry.dateTime());
            } else if (logEntry instanceof FallAsleepLogEntry) {
                napBuilder = Period.builder();
                napBuilder.start(logEntry.dateTime());
            } else if (logEntry instanceof WakeUpLogEntry) {
                napBuilder.end(logEntry.dateTime());
                shiftBuilder.addNap(napBuilder.build());
            }

            index++;
        }

        if (shiftBuilder != null) {
            shifts.add(shiftBuilder.build());
        }

        return shifts;
    }
}

Advent Of Code runs until December 25. You should get involved!

Show Comments

Get the latest posts delivered right to your inbox.