12 votes

Day 2: Cube Conundrum

Today's problem description: https://adventofcode.com/2023/day/2

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

25 comments

  1. pnutzh4x0r
    Link
    This was mostly straightforward... basically just parsing input. Here are my condensed solutions in Python Part 1 Game = dict[str, int] RED_MAX = 12 GREEN_MAX = 13 BLUE_MAX = 14 def...

    This was mostly straightforward... basically just parsing input. Here are my condensed solutions in Python

    Part 1
    Game = dict[str, int]
    
    RED_MAX   = 12
    GREEN_MAX = 13
    BLUE_MAX  = 14
    
    def read_game(stream=sys.stdin) -> Game:
        try:
            game_string, cubes_string = stream.readline().split(':')
        except ValueError:
            return {}
    
        game: Game = defaultdict(int)
        game['id'] = int(game_string.split()[-1])
    
        for cubes in cubes_string.split(';'):
            for cube in cubes.split(','):
                count, color = cube.split()
                game[color] = max(game[color], int(count))
    
        return game
    
    def read_games(stream=sys.stdin) -> Iterator[Game]:
        while game := read_game(stream):
            yield game
    
    def is_valid_game(game: Game) -> bool:
        return all([
            game['red']   <= RED_MAX,
            game['green'] <= GREEN_MAX,
            game['blue']  <= BLUE_MAX,
        ])
    
    def main(stream=sys.stdin) -> None:
        valid_games = filter(is_valid_game, read_games(stream))
        sum_of_ids  = sum(game['id'] for game in valid_games)
        print(sum_of_ids)
    
    Part 2

    For the second part, the main parsing remainded the same. I just had to change what I did with the games I read.

    def power(game: Game) -> int:
        return game['red'] * game['green'] * game['blue']
    
    def main(stream=sys.stdin) -> None:
        sum_of_sets = sum(power(game) for game in read_games(stream))
        print(sum_of_sets)
    

    GitHub Repo

    4 votes
  2. [2]
    akk
    Link
    More Java. I didn't need to make a class or anything, but I figured why not. I'm still pretty noob at Java (< 12 months), so I'm not sure what counts as "idiomatic java" or not. I'm happy with it,...

    More Java. I didn't need to make a class or anything, but I figured why not. I'm still pretty noob at Java (< 12 months), so I'm not sure what counts as "idiomatic java" or not. I'm happy with it, though.

    Solution
    package com.michaeleisemann.aoc23.services;
    
    import com.michaeleisemann.aoc23.interfaces.DayInterface;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    
    // https://adventofcode.com/2023/day/2
    @Service
    public class Day2Service implements DayInterface {
    
        private final static int RED_AMOUNT  = 12;
        private final static int GREEN_AMOUNT  = 13;
        private final static int BLUE_AMOUNT  = 14;
    
        private static class Game {
    
            private final List<Round> rounds = new ArrayList<>();
    
            private final int id;
    
            private static class Round {
    
                private int red;
                private int blue;
                private int green;
    
                public Round(int red, int blue, int green) {
                    this.red = red;
                    this.blue = blue;
                    this.green = green;
                }
    
                public Round(String round) {
                    String[] cubes = round.split(",");
                    for (String cube : cubes) {
                        cube = cube.trim();
                        String[] colors = cube.split(" ");
                        switch (colors[1]) {
                            case "red":
                                red = Integer.parseInt(colors[0]);
                                break;
                            case "blue":
                                blue = Integer.parseInt(colors[0]);
                                break;
                            case "green":
                                green = Integer.parseInt(colors[0]);
                                break;
                        }
                    }
                }
    
                public int getRed() {
                    return red;
                }
    
                public int getBlue() {
                    return blue;
                }
    
                public int getGreen() {
                    return green;
                }
    
                @Override
                public String toString() {
                    return "Round: " + red + " red, " + blue + " blue, " + green + " green";
                }
            }
    
            public Game(int id, String rounds) {
                // rounds are semi-colon delimited
                // cubes are comma delimited
                this.id = id;
                String[] roundStrings = rounds.split(";");
                for (String round : roundStrings) {
                    this.rounds.add(new Round(round.trim()));
                }
            }
    
            public int getId() {
                return id;
            }
    
            public int getMaxRed() {
                int max = 0;
                for (Round round : rounds) {
                    if (round.getRed() > max) {
                        max = round.getRed();
                    }
                }
                return max;
            }
    
            public int getMaxBlue() {
                int max = 0;
                for (Round round : rounds) {
                    if (round.getBlue() > max) {
                        max = round.getBlue();
                    }
                }
                return max;
            }
    
            public int getMaxGreen() {
                int max = 0;
                for (Round round : rounds) {
                    if (round.getGreen() > max) {
                        max = round.getGreen();
                    }
                }
                return max;
            }
    
            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                sb.append("Game: ").append(id).append("\n");
                for (Round round : rounds) {
                    sb.append(round).append("\n");
                }
                return sb.toString();
            }
        }
    
        public String part1(String puzzleInput) {
            String[] lines = puzzleInput.split("\n");
            List<Game> games = new ArrayList<>();
            for (String line : lines) {
                games.add(buildGame(line));
            }
            int gameSum = 0;
            for (Game game : games) {
                int reds = game.getMaxRed();
                int blues = game.getMaxBlue();
                int greens = game.getMaxGreen();
                if (reds <= RED_AMOUNT && blues <= BLUE_AMOUNT && greens <= GREEN_AMOUNT) {
                    logger.debug("Game {} is valid", game.getId());
                    gameSum += game.getId();
                }
            }
            return String.valueOf(gameSum);
        }
    
        public String part2(String puzzleInput) {
            String[] lines = puzzleInput.split("\n");
            List<Game> games = new ArrayList<>();
            for (String line : lines) {
                games.add(buildGame(line));
            }
            int powerSum = 0;
            for (Game game : games) {
                int reds = game.getMaxRed();
                int blues = game.getMaxBlue();
                int greens = game.getMaxGreen();
                int power = 1;
                power *= reds;
                power *= blues;
                power *= greens;
                powerSum += power;
            }
            return String.valueOf(powerSum);
        }
    
        private Game buildGame(String line) {
            String[] parts = line.split(":");
            // remove ervything that is not a number from the first part
            String id = parts[0].replaceAll("\\D+", "");
            String rounds = parts[1].trim();
            return new Game(Integer.parseInt(id), rounds);
        }
    }
    
    4 votes
    1. fxgn
      Link Parent
      Making classes for everything even when you don't need to is as idiomatic as Java gets

      I didn't need to make a class or anything, but I figured why not

      Making classes for everything even when you don't need to is as idiomatic as Java gets

      5 votes
  3. lily
    Link
    I was happy to see I was fast enough this time to have the site tell me I got a leaderboard spot (though it's only top 1000, haha). This was a neat problem, though I found part 2 to be rather easy...

    I was happy to see I was fast enough this time to have the site tell me I got a leaderboard spot (though it's only top 1000, haha). This was a neat problem, though I found part 2 to be rather easy the way I had my code set up.

    Solution
    # Advent of Code 2023
    # Day 2: Cube Conundrum
    
    id_sum = 0
    power_sum = 0
    
    with open("inputs/day_2.txt") as file:
        for i, line in enumerate(file):
            handfuls = [
                [
                    {
                        "amount": int((split := color.split(" "))[0]),
                        "color": split[1],
                    }
                    for color in handful.split(", ")
                ]
                for handful in line.rstrip("\n").split(": ")[1].split("; ")
            ]
    
            possible = True
    
            red_max = 0
            green_max = 0
            blue_max = 0
    
            for handful in handfuls:
                red = 0
                green = 0
                blue = 0
    
                for color in handful:
                    if color["color"] == "red":
                        red += color["amount"]
                    elif color["color"] == "green":
                        green += color["amount"]
                    elif color["color"] == "blue":
                        blue += color["amount"]
    
                if red > 12 or green > 13 or blue > 14:
                    possible = False
    
                if red > red_max:
                    red_max = red
                if green > green_max:
                    green_max = green
                if blue > blue_max:
                    blue_max = blue
    
            if possible:
                id_sum += i + 1
    
            power_sum += red_max * green_max * blue_max
    
    print(f"Day 1: {id_sum}")
    print(f"Day 2: {power_sum}")
    
    2 votes
  4. [2]
    csos95
    (edited )
    Link
    Today's problem was very straightforward without any gotchas that I ran into. I'm going for solve speed rather than code golfing for now so there's nothing fancy in my solution. Rust use...

    Today's problem was very straightforward without any gotchas that I ran into.

    I'm going for solve speed rather than code golfing for now so there's nothing fancy in my solution.

    Rust
    use aoc_runner_derive::aoc;
    use regex::Regex;
    
    #[aoc(day2, part1)]
    fn part1(input: &str) -> usize {
        let game_re = Regex::new("Game ([0-9]+):").unwrap();
        let set_re = Regex::new("(?::|;)(.*?)(?:;|$)").unwrap();
        let red_re = Regex::new("([0-9]+) red").unwrap();
        let green_re = Regex::new("([0-9]+) green").unwrap();
        let blue_re = Regex::new("([0-9]+) blue").unwrap();
        input
            .lines()
            .filter_map(|line| {
                let game: usize = game_re.captures(line).unwrap()[1].parse().unwrap();
                let mut i = 0;
                while i < line.len() {
                    if let Some(s) = set_re.find_at(line, i) {
                        if red_re
                            .captures(s.as_str())
                            .map(|c| c[1].parse().unwrap())
                            .unwrap_or(0)
                            > 12
                        {
                            break;
                        }
                        if green_re
                            .captures(s.as_str())
                            .map(|c| c[1].parse().unwrap())
                            .unwrap_or(0)
                            > 13
                        {
                            break;
                        }
                        if blue_re
                            .captures(s.as_str())
                            .map(|c| c[1].parse().unwrap())
                            .unwrap_or(0)
                            > 14
                        {
                            break;
                        }
                        i = s.start() + 1;
                    } else {
                        return Some(game);
                    }
                }
    
                None
            })
            .sum()
    }
    
    #[aoc(day2, part2)]
    fn part2(input: &str) -> usize {
        let set_re = Regex::new("(?::|;)(.*?)(?:;|$)").unwrap();
        let red_re = Regex::new("([0-9]+) red").unwrap();
        let green_re = Regex::new("([0-9]+) green").unwrap();
        let blue_re = Regex::new("([0-9]+) blue").unwrap();
        input
            .lines()
            .map(|line| {
                let mut max_red = 0;
                let mut max_green = 0;
                let mut max_blue = 0;
                let mut i = 0;
                while i < line.len() {
                    if let Some(s) = set_re.find_at(line, i) {
                        max_red = max_red.max(
                            red_re
                                .captures(s.as_str())
                                .map(|c| c[1].parse().unwrap())
                                .unwrap_or(0),
                        );
                        max_green = max_green.max(
                            green_re
                                .captures(s.as_str())
                                .map(|c| c[1].parse().unwrap())
                                .unwrap_or(0),
                        );
                        max_blue = max_blue.max(
                            blue_re
                                .captures(s.as_str())
                                .map(|c| c[1].parse().unwrap())
                                .unwrap_or(0),
                        );
                        i = s.start() + 1;
                    } else {
                        break;
                    }
                }
    
                max_red * max_green * max_blue
            })
            .sum()
    }
    

    Edit: I'll almost certainly need to use something more powerful than just regex at some point so I decided to make another version using logos to refresh my memory of it.
    I'll probably also make a nom version of today's or tomorrow's puzzle.

    Rust - Logos
    use aoc_runner_derive::aoc;
    use logos::{Lexer, Logos};
    
    fn get_count(lex: &mut Lexer<Color>) -> Option<usize> {
        Some(lex.slice().split_once(" ").unwrap().0.parse().unwrap())
    }
    
    #[derive(Logos)]
    enum Color {
        #[regex("[0-9]+ red", get_count)]
        Red(usize),
        #[regex("[0-9]+ green", get_count)]
        Green(usize),
        #[regex("[0-9]+ blue", get_count)]
        Blue(usize),
    
        #[error]
        #[regex(r"(Game [0-9]+|: |, |; )", logos::skip)]
        Error,
    }
    
    #[aoc(day2, part1, logos)]
    fn part1_logos(input: &str) -> usize {
        input
            .lines()
            .enumerate()
            .filter_map(|(game, line)| {
                let mut possible = true;
                for color in Color::lexer(line) {
                    match color {
                        Color::Red(n) if n > 12 => possible = false,
                        Color::Green(n) if n > 13 => possible = false,
                        Color::Blue(n) if n > 14 => possible = false,
                        _ => {},
                    }
                }
    
                if possible {
                    Some(game + 1)
                } else {
                    None
                }
            })
            .sum()
    }
    
    #[aoc(day2, part2, logos)]
    fn part2_logos(input: &str) -> usize {
        input
            .lines()
            .map(|line| {
                let mut max_red = 0;
                let mut max_green = 0;
                let mut max_blue = 0;
                for color in Color::lexer(line) {
                    match color {
                        Color::Red(n) => max_red = max_red.max(n),
                        Color::Green(n) => max_green = max_green.max(n),
                        Color::Blue(n) => max_blue = max_blue.max(n),
                        _ => {},
                    }
                }
    
                max_red * max_green * max_blue
            })
            .sum()
    }
    
    Run Times
    AOC 2023
    Day 2 - Part 1 : 3035
    	generator: 2.863µs,
    	runner: 815.616µs
    
    Day 2 - Part 1 - logos : 3035
    	generator: 2.375µs,
    	runner: 45.886µs
    
    Day 2 - Part 2 : 66027
    	generator: 2.374µs,
    	runner: 683.266µs
    
    Day 2 - Part 2 - logos : 66027
    	generator: 2.375µs,
    	runner: 46.166µs
    
    2 votes
    1. Toric
      Link Parent
      today was the day i decided to learn nom, it was really confusing at first, but I think im warming up to it.

      today was the day i decided to learn nom, it was really confusing at first, but I think im warming up to it.

      1 vote
  5. [2]
    spit-evil-olive-tips
    Link
    a familiar pattern with early days of AoC, where most of the complexity is just in parsing the input, and once you have it parsed the actual problem is fairly straightforward. I find myself...

    a familiar pattern with early days of AoC, where most of the complexity is just in parsing the input, and once you have it parsed the actual problem is fairly straightforward.

    I find myself yearning for Python again, particularly when it comes to the string-parsing.

    part 1
    package day02
    
    import (
    	"fmt"
    	"strconv"
    	"strings"
    
    	"github.com/dlclark/regexp2"
    	"github.com/spf13/cobra"
    
    	"spit-evil-olive.tips/aoc2023/common"
    )
    
    type Round struct {
    	Red   int
    	Green int
    	Blue  int
    }
    
    type Game struct {
    	ID     int
    	Rounds []Round
    }
    
    var gameIdRegex = regexp2.MustCompile(`Game (?<id>\d+):`, regexp2.None)
    
    var roundRegex = regexp2.MustCompile(`(?<count>\d+) (?<color>\w+)`, regexp2.None)
    
    func parseRound(description string) (Round, error) {
    	var round Round
    
    	index := 0
    
    	for {
    		match, err := roundRegex.FindStringMatchStartingAt(description, index)
    		if err != nil {
    			return round, err
    		}
    
    		if match == nil {
    			break
    		}
    
    		count, err := strconv.Atoi(match.GroupByName("count").String())
    		if err != nil {
    			return round, err
    		}
    
    		switch match.GroupByName("color").String() {
    		case "red":
    			round.Red = count
    		case "green":
    			round.Green = count
    		case "blue":
    			round.Blue = count
    		}
    
    		index = match.Index + match.Length
    	}
    
    	return round, nil
    }
    
    func parseGame(line string) (Game, error) {
    	var game Game
    
    	match, err := gameIdRegex.FindStringMatch(line)
    	if err != nil {
    		return game, err
    	}
    
    	id, err := strconv.Atoi(match.GroupByName("id").String())
    	if err != nil {
    		return game, err
    	}
    
    	game.ID = id
    
    	roundDescriptions := strings.Split(line, ";")
    
    	for _, roundDesc := range roundDescriptions {
    		round, err := parseRound(roundDesc)
    		if err != nil {
    			return game, err
    		}
    
    		game.Rounds = append(game.Rounds, round)
    	}
    
    	return game, nil
    }
    
    func parseInput(lines []string) ([]Game, error) {
    	var games []Game
    
    	for _, line := range lines {
    		game, err := parseGame(line)
    		if err != nil {
    			return games, err
    		}
    
    		games = append(games, game)
    	}
    
    	return games, nil
    }
    
    const (
    	maxRed   = 12
    	maxGreen = 13
    	maxBlue  = 14
    )
    
    func isPossible(game Game) bool {
    	for _, round := range game.Rounds {
    		if round.Red > maxRed ||
    			round.Green > maxGreen ||
    			round.Blue > maxBlue {
    			return false
    		}
    	}
    
    	return true
    }
    
    var CommandA = &cobra.Command{
    	Use:  "02a",
    	Args: cobra.ExactArgs(1),
    	RunE: func(cmd *cobra.Command, args []string) error {
    		lines, err := common.ReadFileLines(args[0])
    		if err != nil {
    			return err
    		}
    
    		games, err := parseInput(lines)
    		if err != nil {
    			return err
    		}
    
    		sum := 0
    
    		for _, game := range games {
    			if isPossible(game) {
    				sum += game.ID
    			}
    		}
    
    		fmt.Printf("%v\n", sum)
    
    		return nil
    	},
    }
    
    part 2
    func getMinimumCubes(game Game) Round {
    	var minimums Round
    
    	for _, round := range game.Rounds {
    		if round.Red > minimums.Red {
    			minimums.Red = round.Red
    		}
    
    		if round.Green > minimums.Green {
    			minimums.Green = round.Green
    		}
    
    		if round.Blue > minimums.Blue {
    			minimums.Blue = round.Blue
    		}
    	}
    
    	return minimums
    }
    
    var CommandB = &cobra.Command{
    	Use:  "02b",
    	Args: cobra.ExactArgs(1),
    	RunE: func(cmd *cobra.Command, args []string) error {
    		lines, err := common.ReadFileLines(args[0])
    		if err != nil {
    			return err
    		}
    
    		games, err := parseInput(lines)
    		if err != nil {
    			return err
    		}
    
    		sum := 0
    
    		for _, game := range games {
    			minimums := getMinimumCubes(game)
    			power := minimums.Red * minimums.Green * minimums.Blue
    			sum += power
    		}
    
    		fmt.Printf("%v\n", sum)
    
    		return nil
    	},
    }
    

    I also wrote a justfile that takes care of setting up the per-day boilerplate for me:

    justfile
    day := `date --utc +%d`
    name := "day" + day
    
    newday:
    	#!/usr/bin/env fish
    
    	mkdir -p {{name}}
    	touch {{name}}/{{day}}.txt
    	touch {{name}}/{{day}}e.txt
    	cp template {{name}}/{{name}}.go
    	sed -i 's/NN/{{day}}/g' {{name}}/{{name}}.go
    
    	awk -i inplace '{
    		if (/import/) {
    			in_import=1
    		} else if (in_import==1 && /\)/) {
    			in_import=0
    			print "\t\"spit-evil-olive.tips/aoc2023/{{name}}\""
    		} else if (/func init/) {
    			in_init=1
    		} else if (in_init==1 && /}/) {
    			in_init=0
    			print "\trootCmd.AddCommand({{name}}.CommandA)"
    			print "\trootCmd.AddCommand({{name}}.CommandB)"
    		}
    		print
    	}' main.go
    

    just newday sets up the directory structure that I like, creates the input files (by convention I use 02.txt for my input and 02e.txt for the example input), copies a template file with the skeleton of my two Cobra commands, and then in one of the more complicated bits of Awk I've ever had to write, patches my main.go to reference those commands:

    diff
    > diff -u orig.go main.go
    --- orig.go     2023-12-01 20:39:28.359367947 -0800
    +++ main.go     2023-12-01 20:39:29.824376038 -0800
    @@ -7,6 +7,7 @@
            "github.com/spf13/cobra"
    
            "spit-evil-olive.tips/aoc2023/day01"
    +       "spit-evil-olive.tips/aoc2023/day02"
     )
    
     var rootCmd = &cobra.Command{}
    @@ -14,6 +15,8 @@
     func init() {
            rootCmd.AddCommand(day01.CommandA)
            rootCmd.AddCommand(day01.CommandB)
    +       rootCmd.AddCommand(day02.CommandA)
    +       rootCmd.AddCommand(day02.CommandB)
     }
    
     func main() {
    
    2 votes
    1. xavdid
      Link Parent
      Nice! I've been really digging just as a program. Is powerful, but stays out of my way.

      Nice! I've been really digging just as a program. Is powerful, but stays out of my way.

      1 vote
  6. fxgn
    Link
    Julia This task was pretty simple, so I was able to solve it directly from the Julia REPL. I'm actually somewhat surprised seeing how long other people's solutions are. Part 1 julia> input =...

    Julia

    This task was pretty simple, so I was able to solve it directly from the Julia REPL.

    I'm actually somewhat surprised seeing how long other people's solutions are.

    Part 1
    julia> input = readlines("Downloads/input");
    
    julia> function check(data; red, green, blue)
               limits = Dict("red" => red, "green" => green, "blue" => blue)
               groups = split.(split(data, "; "), ", ")
               for group in groups
                   for set in group
                       number, color = split(set, " ")
                       if limits[color] < parse(Int, number)
                           return false
                       end
                   end
               end
               return true
           end;
    
    julia> result = 0;
    
    julia> for game in input
               name, data = split(game, ": ")
               if check(data; red=12, green=13, blue=14)
                   id = parse(Int, split(name, " ")[2])
                   result += id
               end
           end
    
    julia> result
    2447
    
    Part 2
    julia> function power(data)
               r = Dict("red" => 0, "green" => 0, "blue" => 0)
               groups = split.(split(data, "; "), ", ")
               for group in groups
                   for set in group
                       number, color = split(set, " ")
                       number = parse(Int, number)
                       if r[color] < number
                           r[color] = number
                       end
                   end
               end
               return values(r) |> prod
           end
    
    julia> result = 0;
    
    julia> for game in input
               name, data = split(game, ": ")
               result += power(data)
           end
    
    julia> result
    56322
    
    2 votes
  7. whs
    Link
    I wrote the Kubernetes Jsonnet library at work (it unfortunately came out 3 months before Grafana Tanka did). So I suppose this would be a good exercise in Jsonnet, and perhaps functional...

    I wrote the Kubernetes Jsonnet library at work (it unfortunately came out 3 months before Grafana Tanka did). So I suppose this would be a good exercise in Jsonnet, and perhaps functional programming.

    Turns out Jsonnet is pretty slow. Solving both problems in the same invocation takes almost 4 seconds on my i9. Parsing of the input is also a bit annoying as std.split only take a single separator, so the leading space must be stripped later.

    // [r, g, b]
    local getColor(str, color) =
    	local trimmed = std.stripChars(str, " ");
    	if std.endsWith(trimmed, " " +color)
    		then std.parseInt(std.split(trimmed, " ")[0])
    		else 0;
    local parseTurns(turnStr) = 
    	local turns = std.split(turnStr, ",");
    	local sumColor(color) = function(acc, str) acc + getColor(str, color);
    	[
    		std.foldl(sumColor("red"), turns, 0),
    		std.foldl(sumColor("green"), turns, 0),
    		std.foldl(sumColor("blue"), turns, 0),
    	];
    // parseGame(gameStr: str) -> {gameId: number, turns: [[r, g, b], ...]]
    local parseGame(gameStr) =
    	local parts = std.split(gameStr, ":");
    	local gameId = std.parseInt(std.substr(parts[0], std.length("Game "), 999));
    	local turns = std.split(parts[1], ";");
    	{
    		id: gameId,
    		turns: std.map(parseTurns, turns),
    	};
    // isValidTurn(turnColor: [r, g, b]) -> bool
    local isValidTurn(turnColor) =
    	if turnColor[0] > 12 then false
    	else if turnColor[1] > 13 then false
    	else if turnColor[2] > 14 then false
    	else true;
    local isValidGame(turns) = std.foldl(
    	function(acc, turn)
    		if !acc then false
    		else isValidTurn(turn),
    	turns, true);
    local sum(arr) = std.foldl(function(a, b) a+b, arr, 0);
    
    local solve(problemStr) =
    	local lines = std.split(problemStr, "\n");
    	local parsedLines = std.map(parseGame, lines);
    	local possibleGames = std.filter(function(game) isValidGame(game.turns), parsedLines);
    	sum(std.map(function(game) game.id, possibleGames));
    
    // part 2
    local maxColor(turns) = std.foldl(
    	function(acc, turn) [
    		std.max(acc[0], turn[0]),
    		std.max(acc[1], turn[1]),
    		std.max(acc[2], turn[2]),
    	],
    	turns, [0, 0, 0]
    );
    local solve2(problemStr) =
    	local lines = std.split(problemStr, "\n");
    	local parsedLines = std.map(parseGame, lines);
    	local gameMaxColors = std.map(function(game) maxColor(game.turns), parsedLines);
    	sum(std.map(function(colors) colors[0] * colors[1] * colors[2], gameMaxColors));
    
    local assertEq(a, b) = assert a == b : std.format("%s != %s", [a, b]); "pass";
    
    {
    	input:: std.stripChars(importstr "./input.txt", " \n"),
    	part1: solve($.input),
    	part2: solve2($.input),
    	test:: {
    		exampleProblem:: "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
    Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
    Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
    Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
    Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
    		exampleGame:: "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
    
    		parseGame: assertEq(parseGame(self.exampleGame), {id: 1, turns: [[4, 0, 3], [1, 2, 6], [0, 2, 0]]}),
    		isValidTurn: assertEq(isValidTurn([4, 0, 3]), true),
    		isValidTurnR: assertEq(isValidTurn([13, 0, 0]), false),
    		isValidTurnG: assertEq(isValidTurn([0, 14, 0]), false),
    		isValidTurnB: assertEq(isValidTurn([0, 0, 15]), false),
    		isValidGame: assertEq(isValidGame([[4, 0, 3], [1, 2, 6], [0, 2, 0]]), true),
    		isValidGameR: assertEq(isValidGame([[4, 0, 3], [13, 0, 0]]), false),
    
    		part1Example: assertEq(solve(self.exampleProblem), 8),
    
    		maxColor: assertEq(maxColor([[4, 0, 3], [1, 2, 6], [0, 2, 0]]), [4, 2, 6]),
    
    		part2Example: assertEq(solve2(self.exampleProblem), 2286),
    	}
    }
    

    Note that the test doesn't get run if it's not in the output (the test:: must be changed to test:)

    Maybe I'll try Nix tomorrow...

    2 votes
  8. wycy
    Link
    Rust Day 2 use std::env; use std::io::{self, prelude::*, BufReader}; use std::fs::File; extern crate regex; use regex::Regex; const MAX_RED: usize = 12; const MAX_GREEN: usize = 13; const...

    Rust

    Day 2
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    
    extern crate regex;
    use regex::Regex;
    
    const MAX_RED: usize = 12;
    const MAX_GREEN: usize = 13;
    const MAX_BLUE: usize = 14;
    
    struct Game {
        id: usize,
        hands: Vec<Hand>,
    }
    impl From<&String> for Game {
        fn from(s: &String) -> Self {
            let re = Regex::new(r"Game (\d+): (.+)$").unwrap();
            let matches = re.captures(&s).unwrap();
            let hands = matches[2]
                .split(";")
                .map(Hand::from)
                .collect::<Vec<_>>();
            Game {
                id: matches[1].parse().unwrap(),
                hands: hands,
            }
        }
    }
    impl Game {
        pub fn is_possible(&self) -> bool {
            self.hands.len() == self.hands.iter().filter(|h| h.is_valid()).count()
        }
        pub fn power(&self) -> usize {
            self.hands.iter().map(|x| x.red  ).max().unwrap() *
            self.hands.iter().map(|x| x.green).max().unwrap() *
            self.hands.iter().map(|x| x.blue ).max().unwrap()
        }
    }
    
    struct Hand {
        red: usize,
        green: usize,
        blue: usize,
    }
    impl Hand {
        pub fn is_valid(&self) -> bool {
            self.red <= MAX_RED && self.green <= MAX_GREEN && self.blue <= MAX_BLUE
        }
    }
    impl From<&str> for Hand {
        fn from(s: &str) -> Self {
            Hand {
                red:   cubes_for_color("red",  &s),
                green: cubes_for_color("green",&s),
                blue:  cubes_for_color("blue", &s),
            }
        }
    }
    
    fn cubes_for_color(color: &str, hand: &str) -> usize {
        let re = format!(r"(\d+) {color}");
        let re_color = Regex::new(&re).unwrap();
        if let Some(matches) = re_color.captures(hand) {
            return matches[1].parse().unwrap();
        }
        0
    }
    
    fn solve(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
            Err(err) => panic!("Unknown error reading input: {}", err),
            Ok(result) => result,
        };
    
        let games: Vec<_> = input.iter().map(Game::from).collect();
        let part1: usize = games.iter().filter(|x| x.is_possible()).map(|g| g.id).sum();
    
        println!("Part 1: {part1}"); // 2369
    
        let part2: usize = games.iter().map(|g| g.power()).sum();
        println!("Part 2: {part2}"); // 66363
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    2 votes
  9. tomf
    Link
    Google Sheets I don't like the way I did the first part part 1 =ARRAYFORMULA( IF(F2=FALSE,, SUM( IFERROR( FILTER( --REGEXEXTRACT(A2:A,"Game (\d+): "), MAP( A2:A, LAMBDA( i, LET( s,SPLIT( TOCOL(...

    Google Sheets

    I don't like the way I did the first part

    part 1
    =ARRAYFORMULA(
      IF(F2=FALSE,,
       SUM(
        IFERROR(
         FILTER(
          --REGEXEXTRACT(A2:A,"Game (\d+): "), 
          MAP(
           A2:A,
           LAMBDA(
            i,
            LET(
             s,SPLIT(
              TOCOL(
               SPLIT(REGEXEXTRACT(i,": (.*)"),
                ";,"),
               3),
              " "),
               SUM(
                COUNTIFS(
                 INDEX(s,0,2),{"red","green","blue"},
                 INDEX(s,0,1),{">12",">13",">14"})))))=0)))))
    
    part 2
    =ARRAYFORMULA(
      SUM(
       BYROW(
        MAP(
         {"r","g","b"},
         LAMBDA(
          x,
          BYROW(
           IFERROR(--REGEXEXTRACT(SPLIT(A2:A,";"),"(\d+) "&x)),
           LAMBDA(
            x,
            MAX(x))))),
        LAMBDA(
         x,
         PRODUCT(x)))))
    
    2 votes
  10. [3]
    xavdid
    Link
    Step-by-step Python explanation: https://advent-of-code.xavd.id/writeups/2023/day/2/ Pretty straightforward today - Python's re.findall and a regex got me all the pairs of count + color and a...

    Step-by-step Python explanation: https://advent-of-code.xavd.id/writeups/2023/day/2/

    Pretty straightforward today - Python's re.findall and a regex got me all the pairs of count + color and a defaultdict made it easy to calculate the min number required. Oddly, this felt more like a day 1 than yesterday did 😅

    2 votes
    1. [2]
      tjf
      Link Parent
      Nice writeup! One neat alternative to using reduce and mul on an iterable is to use math.prod.

      Nice writeup! One neat alternative to using reduce and mul on an iterable is to use math.prod.

      1 vote
      1. xavdid
        Link Parent
        Ah, that's awesome! I always use sum but never knew there was a multiplication version!

        Ah, that's awesome! I always use sum but never knew there was a multiplication version!

        1 vote
  11. jzimbel
    Link
    Elixir Pretty straightforward. Map.merge/2 and Map.merge/3 were helpful. Parts 1 and 2 defmodule AdventOfCode.Solution.Year2023.Day02 do def part1(input) do input |> String.split("\n", trim: true)...

    Elixir

    Pretty straightforward. Map.merge/2 and Map.merge/3 were helpful.

    Parts 1 and 2
    defmodule AdventOfCode.Solution.Year2023.Day02 do
      def part1(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_game/1)
        |> Enum.filter(&valid_for_part1?/1)
        |> Enum.map(fn {game_id, _plays} -> game_id end)
        |> Enum.sum()
      end
    
      def part2(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_game/1)
        |> Enum.map(&min_cube_power/1)
        |> Enum.sum()
      end
    
      @bag %{red: 12, green: 13, blue: 14}
      @default_play %{red: 0, green: 0, blue: 0}
    
      defp valid_for_part1?({_game_id, plays}) do
        Enum.all?(plays, &(&1.red <= @bag.red and &1.green <= @bag.green and &1.blue <= @bag.blue))
      end
    
      defp min_cube_power({_game_id, plays}) do
        plays
        |> Enum.reduce(@default_play, fn play, acc ->
          Map.merge(acc, play, fn _k, acc_val, play_val -> max(acc_val, play_val) end)
        end)
        |> Map.values()
        |> Enum.product()
      end
    
      @type parsed_game :: {pos_integer, list(play)}
      @type play :: %{red: non_neg_integer, green: non_neg_integer, blue: non_neg_integer}
    
      @spec parse_game(String.t()) :: parsed_game
      defp parse_game(line) do
        ["Game " <> game_id, plays] = String.split(line, ": ")
    
        game_id = String.to_integer(game_id)
    
        plays =
          plays
          |> String.split("; ")
          |> Enum.map(&parse_play/1)
    
        {game_id, plays}
      end
    
      defp parse_play(play) do
        counts =
          ~r/(\d+) (\w+)/
          |> Regex.scan(play, capture: :all_but_first)
          |> Map.new(fn [count, color] ->
            {String.to_existing_atom(color), String.to_integer(count)}
          end)
    
        Map.merge(@default_play, counts)
      end
    end
    
    1 vote
  12. Crestwave
    Link
    Not much to say, pretty straightforward overall. AWK solutions: Part 1 #!/usr/bin/awk -f BEGIN { FS = "; " max["red"] = 12 max["green"] = 13 max["blue"] = 14 } { sub(/Game [0-9]*: /, "") for (i =...

    Not much to say, pretty straightforward overall. AWK solutions:

    Part 1
    #!/usr/bin/awk -f
    BEGIN {
    	FS = "; "
    	max["red"] = 12
    	max["green"] = 13
    	max["blue"] = 14
    }
    
    {
    	sub(/Game [0-9]*: /, "")
    	for (i = 1; i <= NF; ++i) {
    		split($i, set, ", ")
    		for (j in set) {
    			split(set[j], cube, " ")
    			if (max[cube[2]] < cube[1])
    				next
    		}
    	}
    
    	total += NR
    }
    
    END { print total}
    
    Part 2
    #!/usr/bin/awk -f
    BEGIN { FS = "; " }
    
    {
    	split("", max)
    	sub(/Game [0-9]*: /, "")
    
    	for (i = 1; i <= NF; ++i) {
    		split($i, set, ", ")
    		for (j in set) {
    			split(set[j], cube, " ")
    			if (cube[1] > max[cube[2]])
    				max[cube[2]] = cube[1]
    		}
    	}
    
    	total += max["red"] * max["green"] * max["blue"]
    }
    
    END { print total }
    
    1 vote
  13. asciipip
    Link
    My solution in Common Lisp. FSet made this fairly straightforward. I represented each game round as a bag (aka a multiset). Allowed games for part 1 were games where every round was a subset of a...

    My solution in Common Lisp.

    FSet made this fairly straightforward. I represented each game round as a bag (aka a multiset). Allowed games for part 1 were games where every round was a subset of a conjectured bag. Minimum cube quantities were just the union of all of the rounds for a game.

    The one thing that tripped me up for a bit was that when I calculated the “power” of a set of cubes. I didn't see a way to map across just a bag's multiplicities, so I converted the bag to a set, used fset:image to map across that set, calling fset:multiplicity for each cube color. Well, that puts the results into a set, too, so it was collapsing duplicate multiplicities (e.g. “1 red, 2 blue, 2 green” turned into a set of just 1 and 2). I dealt with that by turning the set back into a bag and everything came out right.

    1 vote
  14. thorondir
    Link
    Guile Scheme again, and again my solution feels... Complicated. My utils.scm got an addition: utils.scm It makes sure everything in the `lst` maps to `true` when `proc` is applied to it (define...

    Guile Scheme again, and again my solution feels... Complicated.

    My utils.scm got an addition:

    utils.scm It makes sure everything in the `lst` maps to `true` when `proc` is applied to it
    (define (and-map proc lst)
      (define (and-inner p ls cur-val)
        (cond
          [(null? ls) cur-val]
          [else (and (p (car ls)) (and-inner p (cdr ls) cur-val))]))
      (and-inner proc lst #t))
    
    Part 1
    (use-modules (ice-9 regex)
                 (ice-9 format)
                 (srfi srfi-1))
    (load "utils.scm")
    
    (define (is-game-possible round-dicts cubes)
      (define (is-round-possible round-dict)
        (and (<= (hash-ref round-dict "red" 0) ; defaults to zero, because not all colors get drawn each time
                 (hash-ref cubes      "red"))
             (<= (hash-ref round-dict "green" 0)
                 (hash-ref cubes      "green"))
             (<= (hash-ref round-dict "blue" 0)
                 (hash-ref cubes      "blue"))))
      (and-map is-round-possible round-dicts))
    
    (define (round-parser current-round)
      (define round-dict (make-hash-table 3)) ; one each for red, blue, and green
      (define matches (list-matches "([0-9]+) (red|green|blue)" current-round))
      (map (lambda (m)
             (let [(key (match:substring m 2)) ; the second group in the regex
                   (value (string->number (match:substring m 1)))] ; the first group in the regex
               (hash-set! round-dict key (+ value (hash-ref round-dict key 0)))))
           matches)
      round-dict)
    
    (define (game-parser line)
      (map round-parser (string-split line #\;)))
    
    (define (prepare-cubes)
      (let [(t (make-hash-table 3))]
        (hash-set! t "red" 12)
        (hash-set! t "green" 13)
        (hash-set! t "blue" 14)
        t))
    
    (define (count-games games)
      (define (counter gs index sum)
        (cond
          [(null? gs) sum]
          [(eq? #t (car gs)) (counter (cdr gs) (1+ index) (+ sum index))]
          [else (counter (cdr gs) (1+ index) sum)]))
      (counter games 1 0))
    
    (define (proc-task-02a filename)
      (define cubes (prepare-cubes))
      (define games (map game-parser (reverse (slurp filename))))
      (define possible-games (map (lambda (round-dicts)
                                    (is-game-possible round-dicts cubes))
                                  games))
      (count-games possible-games))
    
    (if (= (proc-task-02a "tiny_input_02") 8)
      (format #t "Task 02, part 1: ~a\n" (proc-task-02a "input_02"))
      (error "tiny-task-02a didn't pass sanity checks"))
    

    For part 2 I ran through every game again to calculate the number of cubes.

    Part 2
    ;; update table-1 with values from table-2, for key
    (define (hash-updater table-1 table-2 key)
      (let [(val-1 (hash-ref table-1 key 0))
            (val-2 (hash-ref table-2 key 0)) ]
        (when (< val-1 val-2)
          (hash-set! table-1 key val-2))))
    
    (define (calculate-powers game)
      (define cube-minimus (make-hash-table 3))
      (define (calculate-round-power rnd)
        (map (lambda (key)
               (hash-updater cube-minimus rnd key))
             '("red" "green" "blue")))
      (map calculate-round-power game)
      (hash-fold (lambda (k v prev)
                   (* prev v))
                 1 cube-minimus))
    
    (define (proc-task-02b filename)
      (define games (map game-parser (reverse (slurp filename))))
      (define powers (map calculate-powers games))
      (apply + powers))
    
    (if (= (proc-task-02b "tiny_input_02") 2286)
      (format #t "Task 02, part 2: ~a\n" (proc-task-02b "input_02"))
      (error "tiny-task-02b didn't pass sanity checks"))
    
    1 vote
  15. Crespyl
    Link
    Didn't start this one until the afternoon. Day 2 - Ruby #!/usr/bin/env ruby require 'benchmark' require 'minitest' require 'pry-byebug' TEST_STR = "\ Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue;...

    Didn't start this one until the afternoon.

    Day 2 - Ruby
    #!/usr/bin/env ruby
    
    require 'benchmark'
    require 'minitest'
    require 'pry-byebug'
    
    TEST_STR = "\
    Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
    Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
    Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
    Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
    Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"
    
    class Test < MiniTest::Test
      def test_p1
        assert_equal(8, compute_p1(TEST_STR))
      end
    
      def test_p2
        assert_equal(2286, compute_p2(TEST_STR))
      end
    end
    
    def parse_game(line)
      m = line.match(/Game (\d+): (.+;?)/)
      id = m[1].to_i
      draws = m[2]
                .split(";")
                .map { |cubes| cubes.split(",")
                            .map { |s| s.strip.split(" ") }}
                .map { |cubes|
                  cubes.reduce({}) { |h,a| h[a[1].to_sym] = a[0].to_i; h }}
      {id: id, draws: draws}
    end
    
    def is_possible?(game, limits)
      game[:draws].each do |draw|
        return false unless draw.all? do |color, count|
          count <= limits[color]
        end
      end
      return true
    end
    
    def minimum_cubes_power(game)
      game[:draws].reduce(Hash.new(0)) { |maximums,draw|
        draw.each { |color, count|
          if maximums[color] < count
            maximums[color] = count
          end
        }
        maximums
      }.reduce(1) { |product,kv| kv.last * product }
    end
    
    def compute_p1(input)
      limits = {red: 12, green: 13, blue: 14}
      input
        .lines
        .map { |l| parse_game(l) }
        .filter { |g| is_possible?(g, limits) }
        .map { |g| g[:id] }
        .sum
    end
    
    def compute_p2(input)
      input.lines
        .map { |l| parse_game(l) }
        .map { |g| minimum_cubes_power(g) }
        .sum
    end
    
    if MiniTest.run
      puts 'Test case OK, running...'
    
      @input = File.read(ARGV[0] || 'input')
      do_p2 = defined?(compute_p2)
    
      Benchmark.bm do |bm|
        bm.report('Part 1:') { @p1 = compute_p1(@input) }
        bm.report('Part 2:') { @p2 = compute_p2(@input) } if do_p2
      end
    
      puts "\nResults:"
      puts 'Part 1: %i' % @p1
      puts 'Part 2: %i' % @p2 if do_p2
    
    else
      puts 'Test case ERR'
    end
    
    1 vote
  16. whispersilk
    Link
    Rust again this year, helped by working with it a lot in the past year. Like last year, I'm going to stick to the standard library for as long as I can. Hopefully I won't fall completely off of...

    Rust again this year, helped by working with it a lot in the past year. Like last year, I'm going to stick to the standard library for as long as I can. Hopefully I won't fall completely off of doing this like I did last year!

    Parts 1 and 2 at once because, as other have mentioned, the complexity today was entirely in the parsing.
    use std::convert::TryFrom;
    
    use crate::{load_input, Puzzle, Result};
    
    fn first() -> Result<String> {
        let id_sum = load_input(2)?
            .lines()
            .map(Game::try_from)
            .filter(|game| game.as_ref().is_ok_and(|v| v.valid_for(12, 13, 14)))
            .try_fold(0, |acc, x| x.map(|game| acc + game.id))?
            .to_string();
        Ok(id_sum)
    }
    
    fn second() -> Result<String> {
        let power_sum = load_input(2)?
            .lines()
            .map(Game::try_from)
            .try_fold(0, |acc, x| x.map(|game| acc + game.power()))?
            .to_string();
        Ok(power_sum)
    }
    
    #[derive(Debug)]
    struct Game {
        pub id: u32,
        pub red: u32,
        pub green: u32,
        pub blue: u32,
    }
    
    impl Game {
        fn valid_for(&self, r: u32, g: u32, b: u32) -> bool {
            self.red <= r && self.green <= g && self.blue <= b
        }
    
        fn power(&self) -> u32 {
            self.red * self.green * self.blue
        }
    }
    
    impl TryFrom<&str> for Game {
        type Error = Box<dyn std::error::Error>;
        fn try_from(value: &str) -> Result<Self> {
            use std::cmp::max;
    
            let (id, rounds) = value.split_once(':').ok_or(format!("No ':' in {value}"))?;
            let id = id
                .strip_prefix("Game ")
                .ok_or(format!("{id} doesn't start with 'Game '"))?
                .parse::<u32>()?;
    
            let (red, green, blue) = rounds
                .split(';')
                .map(|round| {
                    let (mut r, mut g, mut b) = (0, 0, 0);
                    for pull in round.split(',') {
                        let (count, color) = pull
                            .trim()
                            .split_once(' ')
                            .ok_or(format!("No ' ' in pull {pull}"))?;
                        let count = count.parse::<u32>()?;
                        match color {
                            "red" => r = count,
                            "green" => g = count,
                            "blue" => b = count,
                            x => Err(format!("Color {x} is invalid"))?,
                        };
                    }
                    Ok((r, g, b))
                })
                .try_fold((0, 0, 0), |a, x: Result<_>| {
                    x.map(|x| (max(a.0, x.0), max(a.1, x.1), max(a.2, x.2)))
                })?;
            Ok(Self {
                id,
                red,
                green,
                blue,
            })
        }
    }
    
    1 vote
  17. tjf
    Link
    Here are my Python solutions: Part 1 #!/usr/bin/env pypy3 from collections import defaultdict total = 0 constraints = {'red': 12, 'green': 13, 'blue': 14} for i, line in enumerate(open(0),...

    Here are my Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    from collections import defaultdict
    
    total = 0
    constraints = {'red': 12, 'green': 13, 'blue': 14}
    for i, line in enumerate(open(0), start=1):
        game = defaultdict(int)
        for handful in line.strip().split(':')[1].split(';'):
            for n, color in map(str.split, handful.split(',')):
                game[color] = max(game[color], int(n))
        if all(constraints[color] >= game[color] for color in constraints):
            total += i
    
    print(total)
    
    Only a few line changes needed to solve part 2:
    Part 2
    #!/usr/bin/env pypy3
    
    from collections import defaultdict
    from math import prod
    
    total = 0
    for i, line in enumerate(open(0), start=1):
        game = defaultdict(int)
        for handful in line.strip().split(':')[1].split(';'):
            for n, color in map(str.split, handful.split(',')):
                game[color] = max(game[color], int(n))
        total += prod(game.values())
    
    print(total)
    

    Btw, today's post isn't tagged advent_of_code.2023 so I missed it on the tag's RSS feed. Could we please tag these?

    1 vote
  18. infinitesimal
    Link
    Yesterday I hadn't written a line of Kotlin before. Today I learned more of its conveniences. Kotlin class Day2 { // Parse each line into a game and filter games where all bags are less than the...

    Yesterday I hadn't written a line of Kotlin before. Today I learned more of its conveniences.

    Kotlin
    class Day2 {
        // Parse each line into a game and filter games where all bags are less than the limit. Then sum the game ids.
        fun part1(lines: List<String>) = let {
            val limit = mapOf("red" to 12, "green" to 13, "blue" to 14)
            fun isPossible(game: Game) = game.bags.all { bag -> bag.entries.all { (k, v) -> v <= limit[k]!! } }
            lines.map { parseGame(it) }.filter { isPossible(it) }.sumOf { it.id }
        }
    
        // Parse each line into a game and take the union of all bags in a game. Take the product of each bag's counts and
        // then sum the products.
        fun part2(lines: List<String>) = let {
            fun Map<String, Int>.union(that: Map<String, Int>) =
                (this.keys + that.keys).associateWith { (this[it] ?: 0).coerceAtLeast(that[it] ?: 0) }
            lines.sumOf {
                val game = parseGame(it)
                val bag = game.bags.reduce { prev, next -> prev.union(next) }
                val power = bag.values.reduce { prev, next -> prev * next }
                power
            }
        }
    
        data class Game(val id: Int, val bags: List<Map<String, Int>>)
    
        private fun parseGame(line: String) = let {
            fun parseBag(s: String) =
                Regex("""(\d+) (\w+)""").findAll(s).map { it.groupValues[2] to it.groupValues[1].toInt() }.toMap()
    
            fun parseBags(s: String) = s.split(";").map { parseBag(it) }
            Regex("""Game (\d+): (.*)""").find(line)!!.groupValues.let { Game(it[1].toInt(), parseBags(it[2])) }
        }
    
        val input = javaClass.getResourceAsStream("${javaClass.name}.txt")!!.reader().readLines()
    }
    
    fun main() {
        val example1 = """
            Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
            Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
            Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
            Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
            Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
        """.trimIndent().lines()
    
        val example1Expected1 = 8
        val example2Expected2 = 2286
        val inputExpected1 = 2331
        val inputExpected2 = 71585
    
        val day = Day2()
        check(day.part1(example1) == example1Expected1)
        check(day.part2(example1) == example2Expected2)
        day.part1(day.input).also { println(it) }.also { check(it == inputExpected1) }
        day.part2(day.input).also { println(it) }.also { check(it == inputExpected2) }
    }
    
    1 vote
  19. first-must-burn
    Link
    A not-very-exciting Golang solution. I tend to think in object-oriented terms which is slow but (IMO) clear. I am trying to guess at what primitives / helpers will be useful, like a helper that...

    A not-very-exciting Golang solution. I tend to think in object-oriented terms which is slow but (IMO) clear.
    I am trying to guess at what primitives / helpers will be useful, like a helper that parses "Game 1" and "4 red" as we're likely to hit inputs with similar structure later. My experience is that the AoC folks are pretty good at mixing things up, so I don't spend a lot of time implementing more than what is immediately needed.

    Github

    Both parts
    package main
    
    import (
    	"aoc2023/internal/helpers"
    	"fmt"
    	"os"
    	"strings"
    )
    
    func main() {
    
    	d := &Day2{}
    
    	err := helpers.Dispatch(os.Args, d)
    	if err != nil {
    		fmt.Println("Error:", err)
    		os.Exit(1)
    	}
    
    }
    
    type ColorSet struct {
    	Red   int
    	Blue  int
    	Green int
    }
    
    func (t ColorSet) IsPossible(red int, green int, blue int) bool {
    	return t.Red <= red && t.Blue <= blue && t.Green <= green
    }
    
    func (t *ColorSet) Parse(input string) {
    	colorTokens := strings.Split(input, ",")
    
    	//assign colors for each token
    	for _, token := range colorTokens {
    		colorNum, colorLabel := helpers.ParseLabeledValue(strings.TrimSpace(token), true)
    		if colorLabel == "red" {
    			t.Red = colorNum
    		} else if colorLabel == "blue" {
    			t.Blue = colorNum
    		} else if colorLabel == "green" {
    			t.Green = colorNum
    		} else {
    			panic(fmt.Sprintf("Unrecognized color %s", colorLabel))
    		}
    	}
    }
    
    type Game struct {
    	ID    int
    	Tries []ColorSet
    }
    
    func (g Game) IsPossible(red int, green int, blue int) bool {
    	for _, try := range g.Tries {
    		if !try.IsPossible(red, green, blue) {
    			return false
    		}
    	}
    	return true
    }
    
    func (g Game) GetMinColorSet() ColorSet {
    	minSet := ColorSet{
    		Red:   0,
    		Green: 0,
    		Blue:  0,
    	}
    
    	//the minset is the smallest set of cubes that contains all the observed values, so we're actually
    	//looking for the max of each value
    	for _, try := range g.Tries {
    		minSet.Red = max(try.Red, minSet.Red)
    		minSet.Green = max(try.Green, minSet.Green)
    		minSet.Blue = max(try.Blue, minSet.Blue)
    	}
    
    	return minSet
    }
    
    func (g *Game) Parse(input string) {
    	gameTokens := strings.Split(input, ":")
    	helpers.Assert(len(gameTokens) == 2, "token length != 2")
    
    	//get the ID
    	gameID, gameStr := helpers.ParseLabeledValue(strings.TrimSpace(gameTokens[0]), false)
    	helpers.Assert(gameStr == "Game", "Expected Game label")
    	g.ID = gameID
    
    	//process the remainder as tries
    	tryTokens := strings.Split(gameTokens[1], ";")
    	for _, tryToken := range tryTokens {
    		try := ColorSet{}
    		try.Parse(tryToken)
    		g.Tries = append(g.Tries, try)
    	}
    }
    
    type Day2 struct {
    }
    
    func (d *Day2) Part1(filename string) error {
    
    	lines := helpers.ReadFileToLines(filename)
    
    	possibleRed := 12
    	possibleGreen := 13
    	possibleBlue := 14
    	possibleSum := 0
    
    	for _, line := range lines {
    		game := Game{}
    		game.Parse(line)
    		fmt.Printf("%#v\n", game)
    
    		if game.IsPossible(possibleRed, possibleGreen, possibleBlue) {
    			possibleSum += game.ID
    			fmt.Printf("  IS possible\n")
    		} else {
    			fmt.Printf("  NOT possible\n")
    		}
    	}
    
    	fmt.Println("Possible sum is", possibleSum)
    
    	return nil
    }
    
    func (d *Day2) Part2(filename string) error {
    
    	lines := helpers.ReadFileToLines(filename)
    
    	powerSum := 0
    
    	for _, line := range lines {
    		game := Game{}
    		game.Parse(line)
    		fmt.Printf("game %#v\n", game)
    
    		minSet := game.GetMinColorSet()
    		fmt.Printf("  minset %#v\n", minSet)
    
    		power := minSet.Red * minSet.Blue * minSet.Green
    		fmt.Printf("  power %d\n", power)
    
    		powerSum += power
    	}
    
    	fmt.Println("Power sum is", powerSum)
    
    	return nil
    }
    

    Plus the parsing helper function

    package helpers
    
    import (
    	"fmt"
    	"strconv"
    	"strings"
    )
    
    // ParselabeledValue expects a string like "7 sheep" and returns 7 as an int and the string "sheep"
    // if numberFirst is true, it processes the string like "7 sheep"
    // if numberFirst is false, it processes the string liek "Game 12"
    // if the string does not conform to the pattern, then it panics
    func ParseLabeledValue(s string, numberFirst bool) (int, string) {
    	tokens := strings.Split(s, " ")
    	Assert(len(tokens) == 2, "token len != 2")
    
    	numIndex := 0
    	labelIndex := 1
    	if !numberFirst {
    		numIndex = 1
    		labelIndex = 0
    	}
    
    	numVal, err := strconv.Atoi(tokens[numIndex])
    	if err != nil {
    		panic(fmt.Sprintf("number token '%s' is not an int: %s", tokens[numIndex], err))
    	}
    
    	return numVal, tokens[labelIndex]
    }
    
    1 vote