whispersilk's recent activity

  1. Comment on US Election Distractions Thread in ~talk

    whispersilk
    Link Parent
    I've always remembered them by making up initialisms (or adopting them from somewhere? I don't remember; I've been doing this since I was a kid). i.e. is "in effect" and e.g. is "example given."...

    I've always remembered them by making up initialisms (or adopting them from somewhere? I don't remember; I've been doing this since I was a kid). i.e. is "in effect" and e.g. is "example given." They aren't perfect, but I've never confused the two.

    6 votes
  2. Comment on US study on puberty blockers goes unpublished because of politics, doctor says (gifted link) in ~lgbt

    whispersilk
    (edited )
    Link
    For additional conversation about this piece, Erin in the Morning responded to it. For context, Erin does not have a glowing opinion of the New York times' reporting on trans issues in general.
    • Exemplary

    For additional conversation about this piece, Erin in the Morning responded to it. For context, Erin does not have a glowing opinion of the New York times' reporting on trans issues in general.

    23 votes
  3. Comment on Cards Against Humanity pays you to give a shit in ~society

    whispersilk
    Link Parent
    Less even than that, they're paying people to make a voting plan and a social media post. Whether or not you actually vote afterwards is up to you and none of their concern.

    They’re not paying people to vote for a candidate, they’re paying people to vote at all.

    Less even than that, they're paying people to make a voting plan and a social media post. Whether or not you actually vote afterwards is up to you and none of their concern.

    12 votes
  4. Comment on US judge rules Google must give rival third-party app stores access to the full catalog of Google Play apps — and distribute third-party stores in ~tech

    whispersilk
    Link Parent
    From the article (emphasis mine):

    From the article (emphasis mine):

    Google will have to distribute rival third-party app stores within Google Play, and it must give rival third-party app stores access to the full catalog of Google Play apps, unless developers opt out individually.

    19 votes
  5. Comment on Look at this photo of Ursula von der Leyen’s new team – and tell me the EU doesn’t have a diversity problem in ~society

    whispersilk
    Link Parent
    I think they mean what percentage of the EU's PoC population has emigrated [from non-EU countries to EU countries] within the last decade. That is, how much of the EU's PoC population is "recent"...

    I think they mean what percentage of the EU's PoC population has emigrated [from non-EU countries to EU countries] within the last decade. That is, how much of the EU's PoC population is "recent" immigrants as opposed to people who have been EU residents for more than a decade.

    But that's just how I read the question and it's possible I'm wrong.

    9 votes
  6. Comment on Sports celebrate physical variation—until it challenges social norms in ~life.women

    whispersilk
    Link Parent
    I think there are two ways to read this. In the first reading, it says that someone who looks like a woman to you may not be because they may be trans(-masc), gender non-conforming, or literally...

    There was no widespread knowledge that just because someone looked like a man or a woman to you, your perceptions may or may not be accurate. That's becoming less the case as time goes on.

    I think there are two ways to read this. In the first reading, it says that someone who looks like a woman to you may not be because they may be trans(-masc), gender non-conforming, or literally anything other than a cis woman. In the second reading, it says that someone who looks like a woman to you may not be because they may be trans(-fem and thus secretly a man).

    Given the context of the conversation — all of the athletes that people wring their hands over are trans women multiple years into their transition and openly femme-presenting if they are trans at all (many are, as the article says, cis but outside "feminine norms") — I can see how sparksbet might read it the second way even though you wrote it intending for it to be read the first way.

    3 votes
  7. Comment on What LitRPG Series do you recommend? in ~books

    whispersilk
    Link Parent
    Oh, I'm so glad you're enjoying Super Supportive! I've been keeping up with it as well and it's really only gotten better. I recently started reading Sunspot on ScribbleHub; it's both very new and...

    Oh, I'm so glad you're enjoying Super Supportive! I've been keeping up with it as well and it's really only gotten better. I recently started reading Sunspot on ScribbleHub; it's both very new and very promising, and it gets bonus points from me for having a trans protagonist.

    1 vote
  8. Comment on cohost.org to shut down by the end of 2024 in ~tech

    whispersilk
    Link Parent
    No, I can confirm it's a monthly expense report. One indication of this is the fact that some numbers wouldn't make sense if it was year-to-date. Income, for example, couldn't possibly go *down *...

    No, I can confirm it's a monthly expense report. One indication of this is the fact that some numbers wouldn't make sense if it was year-to-date. Income, for example, couldn't possibly go *down * from July to August.

    6 votes
  9. Comment on What the death of Cohost tells me about my future on the internet in ~life

    whispersilk
    Link
    I'm very sad to see Cohost go, but I also really appreciate it for what it was and (for a little while longer) still is. I'm also thinking of setting up my own website: like you, I can't imagine...

    I'm very sad to see Cohost go, but I also really appreciate it for what it was and (for a little while longer) still is. I'm also thinking of setting up my own website: like you, I can't imagine shifting to Reddit- or Twitter-alikes to fill the same gap.

    5 votes
  10. Comment on What is a software you wish existed? in ~comp

    whispersilk
    Link Parent
    If you're on iOS, Sometime handles this use case quite well.

    If you're on iOS, Sometime handles this use case quite well.

    1 vote
  11. Comment on Google’s greenhouse gas emissions jump 48% in five years in ~enviro

    whispersilk
    Link Parent
    AI. Training the LLMs that power Gemini and friends requires a staggering amount of computation and thus energy, and I think running the things once they're trained is only cheap by comparison.

    AI. Training the LLMs that power Gemini and friends requires a staggering amount of computation and thus energy, and I think running the things once they're trained is only cheap by comparison.

    33 votes
  12. Comment on Webcomics recommendations in ~comics

    whispersilk
    Link
    There were a ton of good suggestions in this thread from last year, and personally I stand by all the ones I recommended there.

    There were a ton of good suggestions in this thread from last year, and personally I stand by all the ones I recommended there.

    5 votes
  13. Comment on Generative AI for Krita in ~tech

    whispersilk
    Link Parent
    Ah, I thought they'd removed that but it was just formatted differently than I remembered. Yes, that works too and is simpler!

    Ah, I thought they'd removed that but it was just formatted differently than I remembered. Yes, that works too and is simpler!

    2 votes
  14. Comment on Generative AI for Krita in ~tech

    whispersilk
    Link Parent
    You can also add the &t=x to the URL manually if you want to, where x is the timestamp in seconds. In this case 2:18 would make it &t=138. Also very valid not to do that, though. YouTube is good...

    You can also add the &t=x to the URL manually if you want to, where x is the timestamp in seconds. In this case 2:18 would make it &t=138. Also very valid not to do that, though. YouTube is good about making it easy to scroll.

    5 votes
  15. Comment on Day 6: Wait For It in ~comp.advent_of_code

    whispersilk
    Link
    Well, after day 5 this was refreshingly easy. Brute-force ran in ~4 ms even for part 2, but I wound up implementing a binary search that skips to linear searching when <10 elements are left...

    Well, after day 5 this was refreshingly easy. Brute-force ran in ~4 ms even for part 2, but I wound up implementing a binary search that skips to linear searching when <10 elements are left anyway. With that, runtime for parts 1 and 2 combined comes out around 12 μs, making today the fastest day by roughly an order of magnitude.

    Parser and data structure
    #[derive(Debug)]
    struct Race {
        time: usize,
        record: usize,
    }
    
    impl Race {
        fn num_solutions(&self) -> usize {
            let min = binary_search(0, self.time / 2 - 1, |val| {
                match (self.is_solution(val), self.is_solution(val + 1)) {
                    (true, true) => Ordering::Greater,
                    (false, false) => Ordering::Less,
                    (false, true) => Ordering::Equal,
                    (true, false) => unreachable!(),
                }
            });
            let max = binary_search(self.time / 2, self.time, |val| {
                match (self.is_solution(val), self.is_solution(val + 1)) {
                    (true, true) => Ordering::Less,
                    (false, false) => Ordering::Greater,
                    (true, false) => Ordering::Equal,
                    (false, true) => unreachable!(),
                }
            });
            let (max, min) = (max.expect("Max exists"), min.expect("Min exists"));
            max - min + 1
        }
    
        fn is_solution(&self, val: usize) -> bool {
            val * (self.time - val) > self.record
        }
    }
    
    impl From<(usize, usize)> for Race {
        fn from(val: (usize, usize)) -> Self {
            Self {
                time: val.0,
                record: val.1,
            }
        }
    }
    
    #[inline]
    fn binary_search<F>(mut bottom: usize, mut top: usize, cond: F) -> Option<usize>
    where
        F: Fn(usize) -> Ordering,
    {
        while bottom < top {
            if top - bottom < 10 {
                return (bottom..=top).find(|i| cond(*i) == Ordering::Equal);
            } else {
                let mid = (top + bottom) / 2;
                match cond(mid) {
                    Ordering::Less => bottom = mid - 1,
                    Ordering::Greater => top = mid + 1,
                    Ordering::Equal => return Some(mid),
                }
            }
        }
        None
    }
    
    fn parse_input(part: Puzzle) -> Result<Vec<Race>> {
        let input = load_input(6)?;
        let mut lines = input.lines();
        let times = lines
            .next()
            .and_then(|l| l.strip_prefix("Time:"))
            .ok_or("No line 1")?;
        let records = lines
            .next()
            .and_then(|l| l.strip_prefix("Distance:"))
            .ok_or("No line 2")?;
        match part {
            Puzzle::First => {
                let times = times
                    .split_whitespace()
                    .filter(|n| !n.is_empty())
                    .map(parse_as::<usize>)
                    .collect::<Result<Vec<usize>>>()?;
                let records = records
                    .split_whitespace()
                    .filter(|n| !n.is_empty())
                    .map(parse_as::<usize>)
                    .collect::<Result<Vec<usize>>>()?;
                Ok(times.into_iter().zip(records).map(Race::from).collect())
            }
            Puzzle::Second => {
                let time = parse_as::<usize>(&times.replace(' ', ""))?;
                let record = parse_as::<usize>(&records.replace(' ', ""))?;
                Ok(vec![(time, record).into()])
            }
        }
    }
    
    Part 1
    fn first() -> Result<String> {
        let races = parse_input(Puzzle::First)?;
        let total = races
            .into_iter()
            .map(|race| race.num_solutions())
            .product::<usize>()
            .to_string();
        Ok(total)
    }
    
    Part 2
    fn second() -> Result<String> {
        let race = parse_input(Puzzle::Second)?.remove(0);
        Ok(race.num_solutions().to_string())
    }
    
    1 vote
  16. Comment on Day 5: If You Give A Seed A Fertilizer in ~comp.advent_of_code

    whispersilk
    Link
    Rust At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a...

    Rust

    At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a single core, that's respectable, but I want things to run quickly. With some imports I could have made it multi-core, but I'm still sticking to std and that wouldn't have gotten it down to where I wanted anyway.

    In the end, though, I figured out the same technique of collapsing the layers into one so that the actual comparisons can be done both intelligently and all at once that it seems most people used. Getting all the edge cases nailed out was rough, but I got it and in the end both parts combined run in just under 200 μs.

    Of course, between implementing the smart solution and just not having much time the past few days I'm now a bit behind on the other days, so I have some catching up to do.

    Parser and data structure — sorry, this one's ugly
    #[derive(Debug, Clone, Copy)]
    struct Range {
        input_start: usize,
        input_end: usize,
        output_start: usize,
        output_end: usize,
    }
    
    enum CombinedRange {
        Input(Range),
        Output(Range),
        Overlap(Range),
        None,
    }
    
    impl Range {
        fn new(input_start: usize, input_end: usize, output_start: usize, output_end: usize) -> Self {
            Self { input_start, input_end, output_start, output_end }
        }
    
    
        fn translate(&self, val: usize) -> Option<usize> {
            if val >= self.input_start && val <= self.input_end {
                if self.input_start > self.output_start {
                    Some(val - (self.input_start - self.output_start))
                } else {
                    Some(val + (self.output_start - self.input_start))
                }
            } else {
                None
            }
        }
    
        fn translate_min(&self, range: (usize, usize)) -> Option<usize> {
            if range.1 < self.input_start || range.0 > self.input_end {
                None
            } else {
                self.translate(std::cmp::max(range.0, self.input_start))
            }
        }
    
        fn overlaps(input: &Range, output: &Range) -> bool {
            input.output_end >= output.input_start && input.output_start <= output.input_end
        }
    
        fn input_for(&self, output: usize) -> usize {
            let offset = output - self.output_start;
            self.input_start + offset
        }
    
        fn output_for(&self, input: usize) -> usize {
            let offset = input - self.input_start;
            self.output_start + offset
        }
    
        // Splits an input range based on a value in its output range.
        // The provided value is part of the SECOND range returned, if applicable.
        fn split_at_output(self, second_start: usize) -> (Option<Range>, Option<Range>) {
            if second_start <= self.output_start {
                (None, Some(self))
            } else if second_start > self.output_end {
                (Some(self), None)
            } else {
                let first_len = second_start - 1 - self.output_start;
                let first = Range::new(self.input_start, self.input_start + first_len, self.output_start, self.output_start + first_len);
                let second = Range::new(first.input_end + 1, self.input_end, first.output_end + 1, self.output_end);
                (Some(first), Some(second))
            }
        }
    
        fn combine(input: Range, output: Range) -> [CombinedRange; 3] {
            use CombinedRange::*;
            use std::cmp::Ordering;
            if !Range::overlaps(&input, &output) {
                [Input(input), Output(output), None]
            } else {
                let (low, high) = (
                    std::cmp::max(input.output_start, output.input_start),
                    std::cmp::min(input.output_end, output.input_end),
                );
                let overlap = Overlap(Range::new(input.input_for(low), input.input_for(high), output.output_for(low), output.output_for(high)));
                match input.output_start.cmp(&output.input_start) {
                    Ordering::Less => {
                        let first = Input(Range::new(input.input_start, input.input_for(low - 1), input.output_start, low - 1));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                    Ordering::Equal => match input.output_end.cmp(&output.input_end) {
                        Ordering::Less => [
                            overlap,
                            Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            None,
                        ],
                        Ordering::Equal => [
                            overlap,
                            None,
                            None,
                        ],
                        Ordering::Greater => [
                            overlap,
                            Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            None,
                        ],
                    }
                    Ordering::Greater => {
                        let first = Output(Range::new(output.input_start, low - 1, output.output_start, output.output_for(low - 1)));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                }
            }
        }
    }
    
    fn parse_input() -> Result<(Vec<usize>, Vec<Range>)> {
        let input = load_input(5)?;
        let (seeds, maps) = input.split_once("\n\n").ok_or("No section divider")?;
        let seeds = seeds
            .strip_prefix("seeds: ")
            .ok_or("No seeds prefix")?
            .split(' ')
            .map(parse_as::<usize>)
            .collect::<Result<Vec<usize>>>()?;
        let map = maps
            .split("\n\n")
            .try_fold(Vec::new(), |prior, section| {
                let mut ranges = section
                    .split('\n')
                    .skip(1)
                    .filter(|line| !line.is_empty())
                    .map(|line| {
                        let nums = line
                            .split(' ')
                            .map(parse_as::<usize>)
                            .collect::<Result<Vec<usize>>>()?;
                        if let [dest, source, len] = nums.as_slice() {
                            Ok(Range::new(*source, source + len - 1, *dest, dest + len - 1))
                        } else {
                            Err(format!("Wrong number of elements on line {line}").into())
                        }
                    })
                    .collect::<Result<Vec<Range>>>()?;
                let mut new_ranges = Vec::new();
                for mut input in prior {
                    let mut outputs = ranges
                        .iter()
                        .enumerate()
                        .filter(|(_, output)| Range::overlaps(&input, output))
                        .map(|(idx, _)| idx)
                        .collect::<Vec<_>>();
                    outputs.sort_unstable();
                    let mut outputs = outputs
                        .into_iter()
                        .rev()
                        .map(|idx| ranges.remove(idx))
                        .collect::<Vec<_>>();
                    outputs.sort_unstable_by_key(|range| range.input_start);
                    if outputs.len() > 1 {
                        for output in outputs {
                            match input.split_at_output(output.input_end + 1) {
                                (Some(overlaps), Some(after)) => {
                                    input = after;
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (Some(overlaps), None) => {
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (None, Some(_)) => unreachable!(),
                                (None, None) => unreachable!(),
                            }
                        }
                    } else if !outputs.is_empty() {
                        let output = outputs[0];
                        for range in Range::combine(input, output) {
                            match range {
                                CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                CombinedRange::Output(x) => ranges.push(x),
                                CombinedRange::None => (),
                            }
                        }
                    } else {
                        new_ranges.push(input);
                    }
                }
                new_ranges.extend(ranges);
                let output: Result<Vec<_>> = Ok(new_ranges);
                output
            })?;
    
        Ok((seeds, map))
    }
    
    Part 1
    fn first() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let min_seed_location = seeds
            .into_iter()
            .map(|seed| ranges.iter().find_map(|range| range.translate(seed)).unwrap_or(seed))
            .min()
            .ok_or("There is at least one seed")?
            .to_string();
        Ok(min_seed_location)
    }
    
    Part 2
    fn second() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let mut seed_ranges = Vec::new();
        for x in (1..seeds.len()).step_by(2) {
            seed_ranges.push((seeds[x - 1], seeds[x - 1] + seeds[x] + 1));
        }
    
        let min_seed_location = seed_ranges
            .into_iter()
            .map(|seed_range| ranges.iter().find_map(|range| range.translate_min(seed_range)).unwrap_or(seed_range.0))
            .min()
            .ok_or("There is at least one seed range")?
            .to_string();
        Ok(min_seed_location)
    }
    
    1 vote
  17. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link Parent
    I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below. Having...

    I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below.

    Having taken a look at your code now, I think the speed difference is probably coming from the fact that you're storing winning_numbers and numbers in BTreeSets and performing intersections as you go. I just calculated how many numbers on each card were winners at the time of parsing and stored that, so I never paid for node allocation or the cache misses involved in searching.

    2 votes
  18. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link
    Rust Data structures and parser #[derive(Debug)] struct Card { winners: usize, } impl Card { fn num_winners(&self) -> usize { self.winners } fn score(&self) -> u32 { 1 << self.winners >> 1 } }...

    Rust

    Data structures and parser
    #[derive(Debug)]
    struct Card {
        winners: usize,
    }
    
    impl Card {
        fn num_winners(&self) -> usize {
            self.winners
        }
    
        fn score(&self) -> u32 {
            1 << self.winners >> 1
        }
    }
    
    impl TryFrom<&str> for Card {
        type Error = Box<dyn std::error::Error>;
        fn try_from(value: &str) -> Result<Self> {
            type Res<T> = std::result::Result<Vec<T>, std::num::ParseIntError>;
    
            let (_, data) = value.split_once(':').ok_or("Line is missing card id")?;
            let (cards, have) = data.split_once('|').ok_or("Line is missing '|'")?;
            let cards: Vec<_> = cards
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>())
                .collect::<Res<_>>()?;
            let winners = have
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>().map(|val| cards.iter().find(|v| **v == val)))
                .try_fold(0, |acc, x| x.map(|opt| acc + opt.map(|_| 1).unwrap_or(0)))?;
            Ok(Card { winners })
        }
    }
    
    Part 1
    fn first() -> Result<String> {
        let summed_scores = load_input(4)?
            .lines()
            .map(Card::try_from)
            .try_fold(0, |acc, x| x.map(|card| acc + card.score()))?
            .to_string();
        Ok(summed_scores)
    }
    
    Part 2
    fn second() -> Result<String> {
        let cards = load_input(4)?
            .lines()
            .map(Card::try_from)
            .collect::<Result<Vec<Card>>>()?;
        let num_cards = cards.len();
        let mut copies = vec![1; num_cards];
        for (idx, card) in cards.iter().enumerate() {
            let curr_count = copies[idx];
            let num_to_boost = card.num_winners();
            for i in (idx + 1)..=(std::cmp::min(idx + num_to_boost, num_cards)) {
                copies[i] += curr_count;
            }
        }
        let total_copies = copies.iter().sum::<u32>().to_string();
        Ok(total_copies)
    }
    

    What I've got so far is up on GitHub.

    1 vote
  19. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link Parent
    I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs...

    I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs including the time it takes to read the file from disk for each part. Without optimizations it's more like 3.1 ms.

    2 votes
  20. Comment on Day 3: Gear Ratios in ~comp.advent_of_code

    whispersilk
    (edited )
    Link
    Rust This was fun! A little bit harder than yesterday but easier than day 1, I think. My solution isn't exactly optimized — there's definitely some nested looping going on that would slow things...

    Rust

    This was fun! A little bit harder than yesterday but easier than day 1, I think. My solution isn't exactly optimized — there's definitely some nested looping going on that would slow things down on larger inputs — but it's pretty quick at this scale and the hard part is still just the grid parsing. Because of that, I'll put my parser in a block of its own and then the solutions will be their own little follow-on blocks.

    Data structures and parser
    #[derive(Debug)]
    struct Part {
        id: u32,
        row: u32,
        col_left: u32,
        col_right: u32,
    }
    
    #[derive(Debug)]
    struct Symbol {
        kind: char,
        row: u32,
        col: u32,
    }
    
    impl Part {
        fn new(id: u32, row: u32, col_left: u32, col_right: u32) -> Self {
            Self {
                id,
                row,
                col_left,
                col_right,
            }
        }
    
        fn adjacent_to(&self, other: &Symbol) -> bool {
            other.row >= self.row.saturating_sub(1)
                && other.row <= self.row + 1
                && other.col >= self.col_left.saturating_sub(1)
                && other.col <= self.col_right + 1
        }
    }
    
    fn parse_grid() -> Result<(Vec<Part>, Vec<Symbol>)> {
        let (mut row, mut col) = (0, 0);
        let (mut parts, mut symbols) = (Vec::new(), Vec::new());
        let mut curr_id = None;
        for c in load_input(3)?.chars() {
            match c {
                '\n' => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    row += 1;
                    col = 0;
                }
                '.' => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    col += 1;
                }
                '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
                    let curr = c as u32 - 48;
                    curr_id = curr_id.map(|v| v * 10 + curr).or(Some(curr));
                    col += 1;
                }
                kind => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    symbols.push(Symbol { kind, row, col });
                    col += 1;
                }
            };
        }
        parts.sort_unstable_by(|a, b| a.row.cmp(&b.row).then(a.col_left.cmp(&b.col_left)));
        symbols.sort_unstable_by(|a, b| a.row.cmp(&b.row).then(a.col.cmp(&b.col)));
        Ok((parts, symbols))
    }
    
    Part 1
    fn first() -> Result<String> {
        let (parts, symbols) = parse_grid()?;
        let sum = parts
            .iter()
            .filter_map(|part| {
                symbols
                    .iter()
                    .find(|s| part.adjacent_to(s))
                    .map(|_| part.id)
            })
            .sum::<u32>();
        Ok(sum.to_string())
    }
    
    Part 2
    fn second() -> Result<String> {
        let (parts, symbols) = parse_grid()?;
        let ratio = symbols
            .iter()
            .filter(|symbol| symbol.kind == '*')
            .filter_map(|symbol| {
                let mut iter = parts.iter().filter(|part| part.adjacent_to(symbol));
                match (iter.next(), iter.next(), iter.next()) {
                    (Some(part1), Some(part2), None) => Some(part1.id * part2.id),
                    _ => None,
                }
            })
            .sum::<u32>();
        Ok(ratio.to_string())
    }
    

    After completing yesterday's puzzles I put what I've got so far up on GitHub, and I'll do my best to keep it updated as the month goes on.

    2 votes