Day 3: Gear Ratios


Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ or pastebin (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒This post will be unlocked when there is a decent amount of submissions on the leaderboard to avoid cheating for top spots

🔓 Edit: Post has been unlocked after 11 minutes

    • @Gobbel2000@feddit.de
      link
      fedilink
      21 year ago

      I like it, it’s simple and to the point. I’ve learned that one of the most helpful things to do when solving these puzzles is to not make it more complicated than it needs to be, and you certainly succeeded better at that today than I did.

      • cacheson
        link
        fedilink
        11 year ago

        My solution for day 1 part 1 was simple and to the point. The other ones are getting increasingly less so. You’re right that sometimes it’s best not to get too fancy, but I think soon I may have to break out such advanced programming techniques as “functions” and maybe “objects”, instead of writing increasingly convoluted piles of nested loops. xD

  • janAkali
    link
    English
    7
    edit-2
    1 year ago

    That was not fun to solve. Lots of ugly code. This is a less ugly second version. Language: Nim.

    day_03.nim

  • @Gobbel2000@feddit.de
    link
    fedilink
    51 year ago

    Rust

    I’ve been using Regexes for every day so far, this time it helped in finding numbers along with their start and end position in a line. For the second part I mostly went with the approach of part 1 which was to look at all numbers and then figure out if it has a part symbol around it. Only in part 2 I saved all numbers next to a gear * in a hash table that maps each gear position to a list of adjacent numbers. Then in the end I can just look at all gears with exactly 2 numbers attached.

    Also it has to be said, multiplying two numbers is the exact opposite of getting their ratio!

  • @Nighed@sffa.community
    link
    fedilink
    English
    3
    edit-2
    1 year ago

    Language: C#

    I aimed at keeping it as simple and short as reasonably possible this time, no overbuilding here!

    I even used a goto to let me break out of multiple loops at once 🤮 (I had to look up how they worked!) I would totally fail me in a code review!

    One solution for both
    internal class Day3 : IRunnable
        {
            public void Run()
            {
                var input = File.ReadAllLines("Days/Three/Day3Input.txt");
                int sum = 0;
                string numStr = "";
                var starMap = new Dictionary<(int,int),List>();
                for (int i = 0; i < input.Length; i++)           
                    for (int j = 0; j < input[i].Length; j++)
                    {
                        if (char.IsDigit(input[i][j]))                    
                            numStr += input[i][j];                    
                        if (numStr.Length > 0 && (j == input[i].Length - 1 || !char.IsDigit(input[i][j + 1])))
                        {
                            for (int k = Math.Max(0, i - 1); k < Math.Min(i + 2, input.Length); k++)                        
                                for (int l = Math.Max(0, j - numStr.Length); l < Math.Min(j + 2, input[i].Length); l++)                            
                                    if (!char.IsDigit(input[k][l]) && input[k][l] != '.')
                                    {
                                        sum += int.Parse(numStr);
                                        if (input[k][l] == '*')
                                        {
                                            if (starMap.ContainsKey((k, l)))                                        
                                                starMap[(k, l)].Add(int.Parse(numStr));                                        
                                            else
                                                starMap.Add((k,l),new List { int.Parse(numStr) });
                                        }
                                        goto endSymbSearch;
                                    }                           
                        endSymbSearch:
                            numStr = "";
                        }
                    }            
                Console.WriteLine("Result1:"+sum.ToString());
                Console.WriteLine("Result2:" + starMap.Where(sm => sm.Value.Count == 2).Sum(sm => sm.Value[0] * sm.Value[1]));
            }
        }
    
    
  • @StreetKid@reddthat.com
    link
    fedilink
    English
    31 year ago

    My Python solution for part 1 and part 2. I really practice my regex skills.

    spoiler
    #!/usr/bin/python3
    
    import re
    
    value_re = '(\d+)'
    symbol_re = '[^\d.]'
    gear_re = '(\*)'
    
    def main():
        input = list()
        with open("input.txt", 'r') as in_file:
            for line in in_file:
                input.append(line.strip('\n'))
        length = len(input)
        width = len(input[0])
        value_sum = 0
        for idx, line in enumerate(input):
            for match in re.finditer(value_re, line):
                for line_mask in input[max(idx - 1, 0):min(idx + 2, length)]:
                    valid_chars = line_mask[max(match.span()[0] - 1, 0):min(match.span()[1] + 1, width)]
                    if re.search(symbol_re, valid_chars):
                        value_sum += int(match[0])
                        break
        print(f"Value sum = {value_sum}")
    
        gear_ratio = 0
        for idx, line in enumerate(input):
            for match in re.finditer(gear_re, line):
                valid_lines = input[max(idx - 1, 0):min(idx + 2, length)]
                min_range = max(match.span()[0] - 1, 0)
                max_range = min(match.span()[1], width)
                num_of_adjacent = 0
                temp_gear_ratio = 1
                for valid_line in valid_lines:
                    for match in re.finditer(value_re, valid_line):
                        if match.span()[0] in range(min_range,max_range + 1) or match.span()[1] - 1 in range(min_range,max_range + 1):
                            num_of_adjacent += 1
                            temp_gear_ratio *= int(match[0])
                if num_of_adjacent == 2:
                    gear_ratio += temp_gear_ratio
        print(f"Gear ratio = {gear_ratio}")
    
    if __name__ == '__main__':
        main()
    
  • bugsmith
    link
    fedilink
    3
    edit-2
    1 year ago

    Edit: Updated now with part 2.

    Managed to have a crack at this a bit earlier today, I’ve only done Part 01 so far. I’ll update with Part 02 later.

    I tackled this with the personal challenge of not loading the entire puzzle input into memory, which would have made this a bit easier.

    Solution in Rust 🦀

    View formatted on GitLab

    use std::{
        env, fs,
        io::{self, BufRead, BufReader, Read},
    };
    
    fn main() -> io::Result<()> {
        let args: Vec = env::args().collect();
        let filename = &args[1];
        let file1 = fs::File::open(filename)?;
        let file2 = fs::File::open(filename)?;
        let reader1 = BufReader::new(file1);
        let reader2 = BufReader::new(file2);
    
        println!("Part one: {}", process_part_one(reader1));
        println!("Part two: {}", process_part_two(reader2));
        Ok(())
    }
    
    fn process_part_one(reader: BufReader) -> u32 {
        let mut lines = reader.lines().peekable();
        let mut prev_line: Option = None;
        let mut sum = 0;
        while let Some(line) = lines.next() {
            let current_line = line.expect("line exists");
            let next_line = match lines.peek() {
                Some(Ok(line)) => Some(line),
                Some(Err(_)) => None,
                None => None,
            };
            match (prev_line, next_line) {
                (None, Some(next)) => {
                    let lines = vec![¤t_line, next];
                    sum += parse_lines(lines, true);
                }
                (Some(prev), Some(next)) => {
                    let lines = vec![&prev, ¤t_line, next];
                    sum += parse_lines(lines, false);
                }
                (Some(prev), None) => {
                    let lines = vec![&prev, ¤t_line];
                    sum += parse_lines(lines, false);
                }
                (None, None) => {}
            }
    
            prev_line = Some(current_line);
        }
        sum
    }
    
    fn process_part_two(reader: BufReader) -> u32 {
        let mut lines = reader.lines().peekable();
        let mut prev_line: Option = None;
        let mut sum = 0;
        while let Some(line) = lines.next() {
            let current_line = line.expect("line exists");
            let next_line = match lines.peek() {
                Some(Ok(line)) => Some(line),
                Some(Err(_)) => None,
                None => None,
            };
            match (prev_line, next_line) {
                (None, Some(next)) => {
                    let lines = vec![¤t_line, next];
                    sum += parse_lines_for_gears(lines, true);
                }
                (Some(prev), Some(next)) => {
                    let lines = vec![&prev, ¤t_line, next];
                    sum += parse_lines_for_gears(lines, false);
                }
                (Some(prev), None) => {
                    let lines = vec![&prev, ¤t_line];
                    sum += parse_lines_for_gears(lines, false);
                }
                (None, None) => {}
            }
    
            prev_line = Some(current_line);
        }
    
        sum
    }
    
    fn parse_lines(lines: Vec<&String>, first_line: bool) -> u32 {
        let mut sum = 0;
        let mut num = 0;
        let mut valid = false;
        let mut char_vec: Vec> = Vec::new();
        for line in lines {
            char_vec.push(line.chars().collect());
        }
        let chars = match first_line {
            true => &char_vec[0],
            false => &char_vec[1],
        };
        for i in 0..chars.len() {
            if chars[i].is_digit(10) {
                // Add the digit to the number
                num = num * 10 + chars[i].to_digit(10).expect("is digit");
    
                // Check the surrounding character for non-period symbols
                for &x in &[-1, 0, 1] {
                    for chars in &char_vec {
                        if (i as isize + x).is_positive() && ((i as isize + x) as usize) < chars.len() {
                            let index = (i as isize + x) as usize;
                            if !chars[index].is_digit(10) && chars[index] != '.' {
                                valid = true;
                            }
                        }
                    }
                }
            } else {
                if valid {
                    sum += num;
                }
                valid = false;
                num = 0;
            }
        }
        if valid {
            sum += num;
        }
        sum
    }
    
    fn parse_lines_for_gears(lines: Vec<&String>, first_line: bool) -> u32 {
        let mut sum = 0;
        let mut char_vec: Vec> = Vec::new();
        for line in &lines {
            char_vec.push(line.chars().collect());
        }
        let chars = match first_line {
            true => &char_vec[0],
            false => &char_vec[1],
        };
        for i in 0..chars.len() {
            if chars[i] == '*' {
                let surrounding_nums = get_surrounding_numbers(&lines, i);
                let product = match surrounding_nums.len() {
                    0 | 1 => 0,
                    _ => surrounding_nums.iter().product(),
                };
                sum += product;
            }
        }
        sum
    }
    
    fn get_surrounding_numbers(lines: &Vec<&String>, gear_pos: usize) -> Vec {
        let mut nums: Vec = Vec::new();
        let mut num: u32 = 0;
        let mut valid = false;
        for line in lines {
            for (i, char) in line.chars().enumerate() {
                if char.is_digit(10) {
                    num = num * 10 + char.to_digit(10).expect("is digit");
                    if [gear_pos - 1, gear_pos, gear_pos + 1].contains(&i) {
                        valid = true;
                    }
                } else if num > 0 && valid {
                    nums.push(num);
                    num = 0;
                    valid = false;
                } else {
                    num = 0;
                    valid = false;
                }
            }
            if num > 0 && valid {
                nums.push(num);
            }
            num = 0;
            valid = false;
        }
        nums
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        const INPUT: &str = "467..114..
    ...*......
    ..35..633.
    ......#...
    617*......
    .....+.58.
    ..592.....
    ......755.
    ...$.*....
    .664.598..";
    
        #[test]
        fn test_process_part_one() {
            let input_bytes = INPUT.as_bytes();
            assert_eq!(4361, process_part_one(BufReader::new(input_bytes)));
        }
    
        #[test]
        fn test_process_part_two() {
            let input_bytes = INPUT.as_bytes();
            assert_eq!(467835, process_part_two(BufReader::new(input_bytes)));
        }
    }
    
  • @pnutzh4x0r@lemmy.ndlug.org
    link
    fedilink
    English
    3
    edit-2
    1 year ago

    Language: Python

    Classic AoC grid problem… Tedious as usual, but very doable. Took my time and I’m pretty happy with the result. :]

    Part 1

    For the first part, I decided to break the problem into: 1. Reading the schematic, 2. Finding the numbers, 3. Finding the parts. This was useful for Part 2 as I could re-use my read_schematic and find_numbers functions.

    Two things I typically do for grid problems:

    1. Pad the grid so you can avoid annoying boundary checks.
    2. I have a DIRECTIONS list I loop through so I can check easily check the neighbors.
    Schematic  = list[str]
    Number     = tuple[int, int, int]
    
    DIRECTIONS = (
        (-1, -1),
        (-1,  0),
        (-1,  1),
        ( 0, -1),
        ( 0,  1),
        ( 1, -1),
        ( 1,  0),
        ( 1,  1),
    )
    
    def read_schematic(stream=sys.stdin) -> Schematic:
        schematic = [line.strip() for line in stream]
        columns   = len(schematic[0]) + 2
        return [
            '.'*columns,
            *['.' + line + '.' for line in schematic],
            '.'*columns,
        ]
    
    def is_symbol(s: str) -> bool:
        return not (s.isdigit() or s == '.')
    
    def find_numbers(schematic: Schematic) -> Iterator[Number]:
        rows    = len(schematic)
        columns = len(schematic[0])
    
        for r in range(1, rows):
            for number in re.finditer(r'[0-9]+', schematic[r]):
                yield (r, *number.span())
    
    def find_parts(schematic: Schematic, numbers: Iterator[Number]) -> Iterator[int]:
        for r, c_head, c_tail in numbers:
            part = int(schematic[r][c_head:c_tail])
            for c in range(c_head, c_tail):
                neighbors = (schematic[r + dr][c + dc] for dr, dc in DIRECTIONS)
                if any(is_symbol(neighbor) for neighbor in neighbors):
                    yield part
                    break
    
    def main(stream=sys.stdin) -> None:
        schematic = read_schematic(stream)
        numbers   = find_numbers(schematic)
        parts     = find_parts(schematic, numbers)
        print(sum(parts))
    
    Part 2

    For the second part, I just found the stars, and then I found the gears by checking if the stars are next to two numbers (which I had found previously).

    def find_stars(schematic: Schematic) -> Iterator[Star]:
        rows    = len(schematic)
        columns = len(schematic[0])
    
        for r in range(1, rows):
            for c in range(1, columns):
                token = schematic[r][c]
                if token == '*':
                    yield (r, c)
    
    def find_gears(schematic: Schematic, stars: Iterator[Star], numbers: list[Number]) -> Iterator[int]:
        for star_r, star_c in stars:
            gears = [                                                                                                                      
                int(schematic[number_r][number_c_head:number_c_tail])
                for number_r, number_c_head, number_c_tail in numbers
                if any(star_r + dr == number_r and number_c_head <= (star_c + dc) < number_c_tail for dr, dc in DIRECTIONS)
            ]
            if len(gears) == 2:
                yield gears[0] * gears[1]
    
    def main(stream=sys.stdin) -> None:
        schematic = read_schematic(stream)
        numbers   = find_numbers(schematic)
        stars     = find_stars(schematic)
        gears     = find_gears(schematic, stars, list(numbers))
        print(sum(gears))
    

    GitHub Repo

  • @hades@lemm.ee
    link
    fedilink
    31 year ago

    Python

    Questions and comments welcome!

    import collections
    import re
    
    from .solver import Solver
    
    class Day03(Solver):
      def __init__(self):
        super().__init__(3)
        self.lines = []
    
      def presolve(self, input: str):
        self.lines = input.rstrip().split('\n')
    
      def solve_first_star(self):
        adjacent_to_symbols = set()
        for i, line in enumerate(self.lines):
          for j, sym in enumerate(line):
            if sym in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'):
              continue
            for di in (-1, 0, 1):
              for dj in (-1, 0, 1):
                adjacent_to_symbols.add((i + di, j + dj))
        numbers = []
        for i, line in enumerate(self. lines):
          for number_match in re.finditer(r'\d+', line):
            is_adjacent_to_symbol = False
            for j in range(number_match.start(), number_match.end()):
              if (i, j) in adjacent_to_symbols:
                is_adjacent_to_symbol = True
            if is_adjacent_to_symbol:
              numbers.append(int(number_match.group()))
        return sum(numbers)
    
      def solve_second_star(self):
        gear_numbers = collections.defaultdict(list)
        adjacent_to_gears = {}
        for i, line in enumerate(self.lines):
          for j, sym in enumerate(line):
            if sym == '*':
              for di in (-1, 0, 1):
                for dj in (-1, 0, 1):
                  adjacent_to_gears[(i + di, j + dj)] = (i, j)
        for i, line in enumerate(self. lines):
          for number_match in re.finditer(r'\d+', line):
            adjacent_to_gear = None
            for j in range(number_match.start(), number_match.end()):
              if (i, j) in adjacent_to_gears:
                adjacent_to_gear = adjacent_to_gears[(i, j)]
            if adjacent_to_gear:
              gear_numbers[adjacent_to_gear].append(int(number_match.group()))
        ratios = []
        for gear_numbers in gear_numbers.values():
          match gear_numbers:
            case [a, b]:
              ratios.append(a * b)
        return sum(ratios)
    
    
  • @Jummit
    link
    2
    edit-2
    1 year ago

    Input parsing AGAIN?

    Lua
    -- SPDX-FileCopyrightText: 2023 Jummit
    --
    -- SPDX-License-Identifier: GPL-3.0-or-later
    
    local lines = {}
    for line in io.open("3.input"):lines() do
    	table.insert(lines, "."..line..".")
    end
    local width = #lines[1]
    local height = #lines
    local function at(x, y, w)
    	if y < 1 or y > height then return nil end
    	return lines[y]:sub(x, x + w - 1)
    end
    local sum = 0
    local gears = {}
    for y, line in ipairs(lines) do
    	local start = 1
    	local outLine = line
    	while true do
    		local newStart, numEnd = line:find("%d+", start)
    		if not newStart then break end
    		local symbol = false
    		local num = tonumber(line:sub(newStart, numEnd))
    		for y = y - 1, y + 1 do
    			local surrounding = at(newStart - 1, y, numEnd - newStart + 3)
    			if surrounding then
    				if surrounding and surrounding:match("[^.%d]") then
    					symbol = true
    				end
    				for i = 1, #surrounding do
    					local gear = surrounding:sub(i, i) == "*"
    					if gear then
    						if not gears[y] then
    							gears[y] = {}
    						end
    						local x = i + newStart - 2
    						if not gears[y][x] then
    							gears[y][i + newStart - 2] = {}
    						end
    						table.insert(gears[y][x], num)
    					end
    				end
    			end
    		end
    		if symbol then
    			sum = sum + num
    		end
    		start = numEnd + 1
    	end
    end
    print(sum)
    local ratio = 0
    for _, line in pairs(gears) do
    	for _, gears in pairs(line) do
    		if #gears == 2 then
    			ratio = ratio + gears[1] * gears[2]
    		end
    	end
    end
    print(ratio)
    
    Hare (Part one only)
    // SPDX-FileCopyrightText: 2023 Jummit
    //
    // SPDX-License-Identifier: GPL-3.0-or-later
    
    use strings;
    use regex;
    use fmt;
    use os;
    use bufio;
    use io;
    use strconv;
    use types;
    
    fn star_in(lines: []str, x: uint, y: uint, w: uint) bool = {
    	let start = y;
    	if (start > 0) start -= 1;
    	let end = y + 1;
    	if (end >= len(lines)) end -= 1;
    	const re = regex::compile(`[^.0-9]`)!;
    	for (let h = start; h <= end; h += 1) {
    		fmt::println(strings::sub(lines[h], x, x + w))!;
    		if (regex::test(&re, strings::sub(lines[h], x, x + w))) {
    			fmt::println("")!;
    			return true;
    		};
    	};
    	fmt::println("")!;
    	return false;
    };
    
    export fn main() void = {
    	const file = os::open("3.input")!;
    	defer io::close(file)!;
    	const buf = bufio::newscanner(file, types::SIZE_MAX);
    	let lines: []str = [];
    	defer strings::freeall(lines);
    	for (true) {
    		match (bufio::scan_line(&buf)!) {
    		case io::EOF =>
    			break;
    		case let line: const str =>
    			append(lines, strings::dup(line));
    		};
    	};
    	const height = len(lines);
    	const width = len(lines[0]);
    	let sum: uint = 0;
    	let gears: [](uint, uint) = [];
    	const num_re = regex::compile(`[0-9]+`)!;
    	for (let y = 0u; y < len(lines); y += 1) {
    		let nums = regex::findall(&num_re, lines[y]);
    		defer regex::result_freeall(nums);
    		for (let i = 0z; i < len(nums); i += 1) {
    			for (let j = 0z; j < len(nums[i]); j += 1) {
    				const find = nums[i][j];
    				const num = strconv::stou(find.content)!;
    				let start = find.start: uint;
    				let w = len(find.content): uint + 2;
    				if (start > 0) {
    					start -= 1;
    				} else {
    					w -= 1;
    				};
    				if (star_in(lines, start, y, w)) {
    					sum += num;
    				};
    			};
    		};
    	};
    	fmt::printfln("{}", sum)!;
    };
    
  • @sjmulder@lemmy.sdf.org
    link
    fedilink
    2
    edit-2
    1 year ago

    Language: C

    Part 2 stumped me for a little bit, it wasn’t an obvious extension of part 1. Part 1 was about numbers (with one or more …) while part 2 worked from the symbols (with exactly two …). Going the other way would require more bookkeeping to avoid double counting.

    And for the implementation: if you loop over the grid and check surrounding cells for digits you’d have to account for a bunch of cases, e.g. NW/N or N/NE being part of the same number or NW and NE being part of separate numbers. And you’d have to parse the numbers again. But building a graph or reference list of some sort is both unergonomic with C and not necessarily any simpler.

    I ended up just writing out the cases, and honestly it didn’t turn out too bad.

    GitHub link

    Abridged code
    int main(int argc, char **argv)
    {
    	static char G[GSZ][GSZ];
    	static int N[GSZ][GSZ];
    	int p1=0,p2=0, h=0, x,y, dx,dy, n=0,sym=0,r;
    	
    	for (h=0; fgets(&G[h+1][1], GSZ-1, stdin); h++)
    		assert(h < GSZ);
    
    	/*
    	 * Pass 1: parse numbers and solve part 1. For every digit in
    	 * G, the full number it is part of is stored in N.
    	 */
    	for (y=1; y<=h; y++)
    	for (x=1; G[y][x]; x++)
    		if (isdigit(G[y][x])) {
    			n = n*10 + G[y][x]-'0';
    
    			for (dy=-1; dy<2; dy++)
    			for (dx=-1; dx<2; dx++)
    				sym = sym || (x && y &&
    				    G[y+dy][x+dx] != '.' &&
    				    ispunct(G[y+dy][x+dx]));
    		} else {
    			for (dx=-1; isdigit(G[y][x+dx]); dx--)
    				N[y][x+dx] = n;
    			if (sym)
    				p1 += n;
    			n = sym = 0;
    		}
    
    	/*
    	 * Pass 2: solve part 2 by finding all '*', then counting and
    	 * multiplying adjecent numbers.
    	 *
    	 * Horizontal adjecency is trivial but vertical/diagonal has
    	 * two situations: if there's a digit directly North of the '+',
    	 * it must be a single number: NW and NE would connect to it.
    	 * If N isn't a digit, digits in NW and NE belong to separate
    	 * numbers.
    	 */
    	for (y=1; y<=h; y++)
    	for (x=1; G[y][x]; x++) {
    		if (G[y][x] != '*')
    			continue;
    
    		n = 0; r = 1;
    
    		if (N[y][x-1]) { n++; r *= N[y][x-1]; }
    		if (N[y][x+1]) { n++; r *= N[y][x+1]; }
    
    		if (N[y-1][x]) { n++; r *= N[y-1][x]; } else {
    			if (N[y-1][x-1]) { n++; r *= N[y-1][x-1]; }
    			if (N[y-1][x+1]) { n++; r *= N[y-1][x+1]; }
    		}
    
    		if (N[y+1][x]) { n++; r *= N[y+1][x]; } else {
    			if (N[y+1][x-1]) { n++; r *= N[y+1][x-1]; }
    			if (N[y+1][x+1]) { n++; r *= N[y+1][x+1]; }
    		}
    
    		if (n == 2)
    			p2 += r;
    	}
    
    	printf("%d %d\n", p1, p2);
    	return 0;
    }
    
  • @mykl@lemmy.world
    link
    fedilink
    2
    edit-2
    1 year ago

    Dart Solution

    Holy moley, if this year is intended to be impervious to AI solution, it’s also working pretty well as a filter to this paltry Human Intelligence.

    Find interesting symbols, look around for digits, and expand these into numbers. A dirty hacky solution leaning on a re-used Grid class. Not recommended.

    late ListGrid grid;
    
    Index look(Index ix, Index dir) {
      var here = ix;
      var next = here + dir;
      while (grid.isInBounds(next) && '1234567890'.contains(grid.at(next))) {
        here = next;
        next = here + dir;
      }
      return here;
    }
    
    /// Return start and end indices of a number at a point.
    (Index, Index) expandNumber(Index ix) =>
        (look(ix, Grid.dirs['L']!), look(ix, Grid.dirs['R']!));
    
    int parseNumber((Index, Index) e) => int.parse([
          for (var i = e.$1; i != e.$2 + Grid.dirs['R']!; i += Grid.dirs['R']!)
            grid.at(i)
        ].join());
    
    /// Return de-duplicated positions of all numbers near the given symbols.
    nearSymbols(Set syms) => [
          for (var ix in grid.indexes.where((i) => syms.contains(grid.at(i))))
            {
              for (var n in grid
                  .near8(ix)
                  .where((n) => ('1234567890'.contains(grid.at(n)))))
                expandNumber(n)
            }.toList()
        ];
    
    part1(List lines) {
      grid = ListGrid([for (var e in lines) e.split('')]);
      var syms = lines
          .join('')
          .split('')
          .toSet()
          .difference('0123456789.'.split('').toSet());
      // Find distinct number locations near symbols and sum them.
      return {
        for (var ns in nearSymbols(syms))
          for (var n in ns) n
      }.map(parseNumber).sum;
    }
    
    part2(List lines) {
      grid = ListGrid([for (var e in lines) e.split('')]);
      // Look for _pairs_ of numbers near '*' and multiply them.
      var products = [
        for (var ns in nearSymbols({'*'}).where((e) => e.length == 2))
          ns.map(parseNumber).reduce((s, t) => s * t)
      ];
      return products.sum;
    }