From 05c9fefa135bb7aa5633b33b3584776a10496515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Sun, 7 Dec 2025 17:34:49 +0000 Subject: [PATCH] feat: 2025 d02 --- .github/badges/completion2025.json | 2 +- 2025/day02/Cargo.toml | 31 ++++++ 2025/day02/benches/benchmarks.rs | 18 ++++ 2025/day02/src/common.rs | 164 +++++++++++++++++++++++++++++ 2025/day02/src/lib.rs | 64 +++++++++++ 2025/day02/src/main.rs | 22 ++++ Cargo.toml | 3 +- common/src/helpers.rs | 22 ++++ common/src/parsing/combinators.rs | 35 +++++- common/src/parsing/impls.rs | 16 ++- common/src/parsing/mod.rs | 6 +- solution-runner/Cargo.toml | 1 + solution-runner/src/main.rs | 1 + 13 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 2025/day02/Cargo.toml create mode 100644 2025/day02/benches/benchmarks.rs create mode 100644 2025/day02/src/common.rs create mode 100644 2025/day02/src/lib.rs create mode 100644 2025/day02/src/main.rs diff --git a/.github/badges/completion2025.json b/.github/badges/completion2025.json index 0dbb905..2a97368 100644 --- a/.github/badges/completion2025.json +++ b/.github/badges/completion2025.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "label": "2025", - "message": "02/50", + "message": "04/50", "color": "red", "style": "for-the-badge" } diff --git a/2025/day02/Cargo.toml b/2025/day02/Cargo.toml new file mode 100644 index 0000000..26d2867 --- /dev/null +++ b/2025/day02/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "day02_2025" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day02_2025" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } +winnow = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } + +[[bench]] +name = "benchmarks" +harness = false + +[lints] +workspace = true \ No newline at end of file diff --git a/2025/day02/benches/benchmarks.rs b/2025/day02/benches/benchmarks.rs new file mode 100644 index 0000000..f4502ee --- /dev/null +++ b/2025/day02/benches/benchmarks.rs @@ -0,0 +1,18 @@ +// Copyright 2023 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::define_aoc_benchmark; +use day02_2025::Day02; + +define_aoc_benchmark!("inputs/2025/day02", Day02); diff --git a/2025/day02/src/common.rs b/2025/day02/src/common.rs new file mode 100644 index 0000000..0ea7370 --- /dev/null +++ b/2025/day02/src/common.rs @@ -0,0 +1,164 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::helpers::{Digits, digits_to_number}; +use aoc_common::parsing::combinators::parse_range_inclusive; +use std::ops::{Deref, RangeInclusive}; +use std::str::FromStr; +use winnow::Parser; + +#[derive(Clone, Debug)] +pub struct IdRange(RangeInclusive); + +impl IdRange { + // no point in checking each value individually, + // we just iterate through all valid prefixes + pub fn invalid_ids_p1(&self) -> Vec { + if self.is_empty() { + return Vec::new(); + } + + let mut invalid_ids = Vec::new(); + + // if our prefix digit count is odd, we start from the smallest next number with even count of digits + let mut start_digits = self.0.start().to_digits(); + if !start_digits.len().is_multiple_of(2) { + let start = next_smallest_number_with_more_digits(&start_digits); + start_digits = start.to_digits(); + } + + let mid_point = start_digits.len() / 2; + + let mut prefix_digits = start_digits[..mid_point].to_vec(); + let mut prefix = digits_to_number(&prefix_digits); + loop { + let candidate_id = prefix_digits_to_id(&prefix_digits); + if self.0.contains(&candidate_id) { + invalid_ids.push(candidate_id); + } else if candidate_id > *self.0.start() { + break; + } + + prefix += 1; + prefix_digits = prefix.to_digits(); + } + + invalid_ids + } + + pub fn invalid_ids_p2(&self) -> Vec { + // don't try to be too fancy here, + // just check every sequence + // (because I gave up trying to be fancy) + let mut invalid_ids = Vec::new(); + + for candidate in *self.0.start()..=*self.0.end() { + let digits = candidate.to_digits(); + let digits_len = digits.len(); + if invalid_ids.contains(&candidate) { + continue; + } + + for seq_len in 1..=digits_len / 2 { + // can't possibly be a repeating sequence + if digits_len % seq_len != 0 { + continue; + } + + let repeats = digits_len / seq_len; + // task requires at least 2 repeats + if repeats < 2 { + continue; + } + + let pattern = &digits[..seq_len]; + if pattern.repeat(repeats) == digits { + invalid_ids.push(candidate) + } + } + } + + invalid_ids + } +} + +// converts, e.g. [1,2,3] into 123123 +fn prefix_digits_to_id(digits: &[usize]) -> usize { + usize::from_digits(&digits.repeat(2)) +} + +/// Returns the next smallest number with higher count of digits, +/// e.g. [9,9,9] returns 1000 +/// [1,2,3,4] returns 10000 +fn next_smallest_number_with_more_digits(digits: &[usize]) -> usize { + 10usize.pow(digits.len() as u32 + 1) +} + +impl Deref for IdRange { + type Target = RangeInclusive; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for IdRange { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(IdRange( + parse_range_inclusive + .parse(s) + .map_err(|err| anyhow::format_err!("{err}"))?, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_ids_p1() { + let range = IdRange::from_str("11-44").unwrap(); + assert_eq!(range.invalid_ids_p1(), vec![11, 22, 33, 44]); + + let range = IdRange::from_str("47-126").unwrap(); + assert_eq!(range.invalid_ids_p1(), vec![55, 66, 77, 88, 99]); + + let range = IdRange::from_str("99-1011").unwrap(); + assert_eq!(range.invalid_ids_p1(), vec![99, 1010]); + + let range = IdRange::from_str("999-1011").unwrap(); + assert_eq!(range.invalid_ids_p1(), vec![1010]) + } + + #[test] + fn invalid_ids_p2() { + let range = IdRange::from_str("11-22").unwrap(); + assert_eq!(range.invalid_ids_p2(), vec![11, 22]); + + let range = IdRange::from_str("95-115").unwrap(); + assert_eq!(range.invalid_ids_p2(), vec![99, 111]); + + let range = IdRange::from_str("123123122-123123124").unwrap(); + assert_eq!(range.invalid_ids_p2(), vec![123123123]); + + let range = IdRange::from_str("998-1012").unwrap(); + assert_eq!(range.invalid_ids_p2(), vec![999, 1010]); + + let range = IdRange::from_str("824824821-824824827").unwrap(); + assert_eq!(range.invalid_ids_p2(), vec![824824824]) + } +} diff --git a/2025/day02/src/lib.rs b/2025/day02/src/lib.rs new file mode 100644 index 0000000..b5034b3 --- /dev/null +++ b/2025/day02/src/lib.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::IdRange; +use aoc_common::parsing::CommaSeparatedParser; +use aoc_solution::Aoc; + +mod common; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = CommaSeparatedParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day02; + +pub fn part1(input: Vec) -> usize { + input + .into_iter() + .flat_map(|range| range.invalid_ids_p1().into_iter()) + .sum() +} + +pub fn part2(input: Vec) -> usize { + input + .into_iter() + .flat_map(|range| range.invalid_ids_p2().into_iter()) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + use aoc_solution::parser::AocInputParser; + + fn sample_input() -> Vec { + CommaSeparatedParser::parse_input( + r#"11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124"#, + ).unwrap() + } + + #[test] + fn part1_sample_input() { + let expected = 1227775554; + assert_eq!(expected, part1(sample_input())) + } + + #[test] + fn part2_sample_input() { + let expected = 4174379265; + assert_eq!(expected, part2(sample_input())) + } +} diff --git a/2025/day02/src/main.rs b/2025/day02/src/main.rs new file mode 100644 index 0000000..21c0a4e --- /dev/null +++ b/2025/day02/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::helpers::root_path; +use aoc_solution::AocSolutionSolver; +use day02_2025::Day02; + +#[cfg(not(tarpaulin_include))] +fn main() { + Day02::try_solve_from_file(root_path("inputs/2025/day02")) +} diff --git a/Cargo.toml b/Cargo.toml index 5d091eb..4797962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,8 @@ members = [ "2024/day09", "2024/day10", "2024/day11", - "2025/day01" + "2025/day01", + "2025/day02" ] [workspace.package] diff --git a/common/src/helpers.rs b/common/src/helpers.rs index 0041cb2..beabc5f 100644 --- a/common/src/helpers.rs +++ b/common/src/helpers.rs @@ -22,6 +22,28 @@ pub fn root_path>(segment: P) -> PathBuf { PathBuf::from(COMMON_ROOT).join("..").join(segment) } +pub trait Digits { + fn to_digits(&self) -> Vec; + + fn to_digits_reversed(&self) -> Vec; + + fn from_digits(digits: &[usize]) -> Self; +} + +impl Digits for usize { + fn to_digits(&self) -> Vec { + split_into_digits(*self) + } + + fn to_digits_reversed(&self) -> Vec { + split_into_digits_reversed(*self) + } + + fn from_digits(digits: &[usize]) -> Self { + digits_to_number(digits) + } +} + #[inline] pub fn split_into_digits(number: usize) -> Vec { let mut digits = Vec::new(); diff --git a/common/src/parsing/combinators.rs b/common/src/parsing/combinators.rs index 2429211..954dfbe 100644 --- a/common/src/parsing/combinators.rs +++ b/common/src/parsing/combinators.rs @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::ops::RangeInclusive; use std::str::FromStr; +use winnow::ModalResult; +use winnow::Parser; use winnow::ascii::digit1; +use winnow::combinator::separated_pair; use winnow::stream::AsChar; use winnow::token::take_while; -use winnow::ModalResult; -use winnow::Parser; pub fn parse_number(input: &mut &str) -> ModalResult { digit1.parse_to().parse_next(input) @@ -28,3 +30,32 @@ pub fn parse_digit(input: &mut &str) -> ModalResult { .parse_to() .parse_next(input) } + +pub fn parse_range_inclusive(input: &mut &str) -> ModalResult> { + separated_pair(parse_number, "-", parse_number) + .map(|(start, end)| RangeInclusive::new(start, end)) + .parse_next(input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn range_inclusive_parser() { + assert_eq!( + RangeInclusive::new(11usize, 22), + parse_range_inclusive::.parse("11-22").unwrap() + ); + assert_eq!( + RangeInclusive::new(1188511880usize, 1188511890), + parse_range_inclusive:: + .parse("1188511880-1188511890") + .unwrap() + ); + assert_eq!( + RangeInclusive::new(998usize, 1012), + parse_range_inclusive::.parse("998-1012").unwrap() + ); + } +} diff --git a/common/src/parsing/impls.rs b/common/src/parsing/impls.rs index 19859f1..3473895 100644 --- a/common/src/parsing/impls.rs +++ b/common/src/parsing/impls.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::{Error, Result}; +use anyhow::{Error, Result, bail}; use std::fmt::Debug; use std::ops::RangeInclusive; use std::str::FromStr; /// Parse input in the form of x=.. to `RangeInclusive` -pub fn parse_raw_range(raw: &str) -> Result> { +pub fn parse_assigned_range(raw: &str) -> Result> { let mut bounds = raw.split('='); let _axis = bounds .next() @@ -40,6 +40,18 @@ pub fn parse_raw_range(raw: &str) -> Result> { Ok(RangeInclusive::new(lower_bound, upper_bound)) } +/// Parse input in the form of - to `RangeInclusive` +pub fn parse_value_range(raw: &str) -> Result> { + let Some((lower_bound, upper_bound)) = raw.split_once("-") else { + bail!("invalid range definition") + }; + + Ok(RangeInclusive::new( + lower_bound.parse()?, + upper_bound.parse()?, + )) +} + /// Parses input in the form of: /// /// value1 diff --git a/common/src/parsing/mod.rs b/common/src/parsing/mod.rs index 22c0903..636caf7 100644 --- a/common/src/parsing/mod.rs +++ b/common/src/parsing/mod.rs @@ -27,7 +27,7 @@ pub mod impls; pub struct FromStrParser(PhantomData); /// Parse input in the form of x=.. to `RangeInclusive` -pub struct RangeParser; +pub struct AssignedRangeParser; /// Parses input in the form of: /// @@ -93,11 +93,11 @@ where } } -impl AocInputParser for RangeParser { +impl AocInputParser for AssignedRangeParser { type Output = RangeInclusive; fn parse_input(raw: &str) -> Result { - parse_raw_range(raw) + parse_assigned_range(raw) } } diff --git a/solution-runner/Cargo.toml b/solution-runner/Cargo.toml index 7307a50..175ff3b 100644 --- a/solution-runner/Cargo.toml +++ b/solution-runner/Cargo.toml @@ -110,3 +110,4 @@ day09_2024 = { path = "../2024/day09" } day10_2024 = { path = "../2024/day10" } day11_2024 = { path = "../2024/day11" } day01_2025 = { path = "../2025/day01" } +day02_2025 = { path = "../2025/day02" } diff --git a/solution-runner/src/main.rs b/solution-runner/src/main.rs index d0531b3..a623533 100644 --- a/solution-runner/src/main.rs +++ b/solution-runner/src/main.rs @@ -132,6 +132,7 @@ fn main() { define_solution!(args, 2024, 10, "inputs/2024/day10", day10_2024::Day10); define_solution!(args, 2024, 11, "inputs/2024/day11", day11_2024::Day11); define_solution!(args, 2025, 1, "inputs/2025/day01", day01_2025::Day01); + define_solution!(args, 2025, 2, "inputs/2025/day02", day02_2025::Day02); // AUTOGENERATED SOLUTIONS END println!("no solution found for year {}, day {}", args.year, args.day);