From 9b62590517fa4ab0936feb4fd59820ce7d4648f4 Mon Sep 17 00:00:00 2001 From: Xiretza Date: Sun, 4 Dec 2022 17:41:57 +0100 Subject: [PATCH] rust: move section ranges to library --- 2022/day4/rust/Cargo.toml | 1 + 2022/day4/rust/src/main.rs | 57 +--------- Cargo.lock | 7 ++ Cargo.toml | 1 + common/rust/Cargo.toml | 8 ++ common/rust/src/lib.rs | 5 + common/rust/src/section_range.rs | 184 +++++++++++++++++++++++++++++++ 7 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 common/rust/Cargo.toml create mode 100644 common/rust/src/lib.rs create mode 100644 common/rust/src/section_range.rs diff --git a/2022/day4/rust/Cargo.toml b/2022/day4/rust/Cargo.toml index e996c8f..c380d2c 100644 --- a/2022/day4/rust/Cargo.toml +++ b/2022/day4/rust/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aoc = { path = "../../../common/rust" } diff --git a/2022/day4/rust/src/main.rs b/2022/day4/rust/src/main.rs index 5ae7f37..e004d1f 100644 --- a/2022/day4/rust/src/main.rs +++ b/2022/day4/rust/src/main.rs @@ -1,42 +1,8 @@ #![warn(clippy::pedantic)] -#![feature(is_some_and)] -use std::{ - io::{stdin, Read}, - ops::{BitAnd, RangeInclusive}, -}; +use std::io::{stdin, Read}; -/// A range of sections. Always contains at least one element. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct SectionRange { - start: u64, - end: u64, -} - -impl TryFrom> for SectionRange { - type Error = (); - - fn try_from(range: RangeInclusive) -> Result { - let start = *range.start(); - let end = *range.end(); - if start <= end { - Ok(Self { start, end }) - } else { - Err(()) - } - } -} - -impl BitAnd for SectionRange { - type Output = Option; - - fn bitand(self, other: Self) -> Self::Output { - let start = self.start.max(other.start); - let end = self.end.min(other.end); - - Self::try_from(start..=end).ok() - } -} +use aoc::SectionRange; fn main() { let mut data = String::new(); @@ -44,19 +10,10 @@ fn main() { let pairs: Vec<_> = data .lines() - .map(|line| { - let make_range = |s: &str| { - let (start, end) = s.split_once('-').unwrap(); - - let start = start.parse().unwrap(); - let end = end.parse().unwrap(); - - SectionRange::try_from(start..=end).unwrap() - }; - + .map(|line| -> (SectionRange, SectionRange) { let (left, right) = line.split_once(',').unwrap(); - let left = make_range(left); - let right = make_range(right); + let left = left.parse().unwrap(); + let right = right.parse().unwrap(); (left, right) }) @@ -66,9 +23,7 @@ fn main() { "{}", pairs .iter() - .filter(|&&(l, r)| { - (l & r).is_some_and(|intersection| intersection == l || intersection == r) - }) + .filter(|&&(l, r)| l.encompasses(&r) || r.encompasses(&l)) .count() ); diff --git a/Cargo.lock b/Cargo.lock index 3e16e9c..dc9d942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,10 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aoc" +version = "0.1.0" + [[package]] name = "arrayvec" version = "0.7.2" @@ -476,6 +480,9 @@ version = "0.1.0" [[package]] name = "rust_2022_04" version = "0.1.0" +dependencies = [ + "aoc", +] [[package]] name = "ryu" diff --git a/Cargo.toml b/Cargo.toml index 74c5e3d..4daf70a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "common/rust", "2021/day1/rust", "2021/day3/rust", "2021/day5/rust", diff --git a/common/rust/Cargo.toml b/common/rust/Cargo.toml new file mode 100644 index 0000000..706d2e1 --- /dev/null +++ b/common/rust/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "aoc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/common/rust/src/lib.rs b/common/rust/src/lib.rs new file mode 100644 index 0000000..b083d9b --- /dev/null +++ b/common/rust/src/lib.rs @@ -0,0 +1,5 @@ +#![warn(clippy::pedantic)] + +mod section_range; + +pub use section_range::{EmptyRange, InvalidSectionString, SectionRange}; diff --git a/common/rust/src/section_range.rs b/common/rust/src/section_range.rs new file mode 100644 index 0000000..3d8778a --- /dev/null +++ b/common/rust/src/section_range.rs @@ -0,0 +1,184 @@ +use std::{ + error::Error, + fmt, + num::NonZeroU64, + ops::{BitAnd, RangeInclusive}, + str::FromStr, +}; + +/// Error returned when an attempt is made to construct an empty `SectionRange`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EmptyRange; + +impl fmt::Display for EmptyRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Range is empty") + } +} + +impl Error for EmptyRange {} + +/// Error returned when a malformed string is attempted to be parsed as a `SectionRange`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InvalidSectionString(String); + +impl fmt::Display for InvalidSectionString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid section range: {}", self.0) + } +} + +impl Error for InvalidSectionString {} + +/// A range of sections. Always contains at least one element. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SectionRange { + start: u64, + end: u64, +} + +impl SectionRange { + /// Constructs a new section range from a start section and an end section, both inclusive. + /// + /// # Errors + /// + /// Returns an error if the range would be empty, i.e. `start > end`. + /// + /// # Examples + /// + /// ```rust + /// # use aoc::{EmptyRange, SectionRange}; + /// let range = SectionRange::try_new(3, 10); + /// assert!(range.is_ok()); + /// + /// let range = SectionRange::try_new(3, 2); + /// assert_eq!(range, Err(EmptyRange)); + /// ``` + pub fn try_new(start: u64, end: u64) -> Result { + if start <= end { + Ok(Self { start, end }) + } else { + Err(EmptyRange) + } + } + + /// Returns the number of sections contained by the range. Since the range always contains at + /// least one element, the length is never zero. + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn len(&self) -> NonZeroU64 { + debug_assert!(self.start <= self.end); + NonZeroU64::new(self.end - self.start + 1).unwrap() + } + + /// Returns true if and only if the range contains the given section. + #[must_use] + pub fn contains(&self, section: u64) -> bool { + debug_assert!(self.start <= self.end); + section >= self.start && section <= self.end + } + + /// Returns true if and only if the range contains the entirety of `other`. + #[must_use] + pub fn encompasses(&self, other: &SectionRange) -> bool { + let Some(intersection) = *self & *other else { + return false; + }; + intersection == *other + } +} + +impl FromStr for SectionRange { + type Err = InvalidSectionString; + + fn from_str(s: &str) -> Result { + // poor man's try block + fn inner(s: &str) -> Option { + let (start, end) = s.split_once('-')?; + + let start = start.parse().ok()?; + let end = end.parse().ok()?; + + SectionRange::try_from(start..=end).ok() + } + + inner(s).ok_or_else(|| InvalidSectionString(s.to_owned())) + } +} + +impl From for RangeInclusive { + fn from(r: SectionRange) -> Self { + r.start..=r.end + } +} + +impl TryFrom> for SectionRange { + type Error = EmptyRange; + + fn try_from(range: RangeInclusive) -> Result { + Self::try_new(*range.start(), *range.end()) + } +} + +impl BitAnd for SectionRange { + type Output = Option; + + fn bitand(self, other: Self) -> Self::Output { + let start = self.start.max(other.start); + let end = self.end.min(other.end); + + Self::try_from(start..=end).ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn section_range_construction() { + let range = SectionRange::try_from(3..=10).unwrap(); + assert_eq!(range, SectionRange { start: 3, end: 10 }); + assert_eq!(range.len().get(), 8); + + let range = SectionRange::try_from(3..=3).unwrap(); + assert_eq!(range, SectionRange { start: 3, end: 3 }); + assert_eq!(range.len().get(), 1); + + #[allow(clippy::reversed_empty_ranges)] + let range = SectionRange::try_from(3..=2); + assert_eq!(range, Err(EmptyRange)); + } + + #[test] + fn section_range_intersection() { + fn check_intersection(a: SectionRange, b: SectionRange, expected: Option) { + let x = a & b; + let y = b & a; + + assert_eq!(x, y, "intersection not commutative"); + assert_eq!(x, expected); + } + + let range1 = SectionRange::try_from(3..=5).unwrap(); + let range2 = SectionRange::try_from(4..=10).unwrap(); + let range3 = SectionRange::try_from(6..=9).unwrap(); + + check_intersection(range1, range2, Some(SectionRange { start: 4, end: 5 })); + check_intersection(range1, range3, None); + check_intersection(range2, range3, Some(range3)); + + assert!(range1.encompasses(&range1)); + assert!(range2.encompasses(&range2)); + assert!(range3.encompasses(&range3)); + + assert!(!range1.encompasses(&range2)); + assert!(!range1.encompasses(&range3)); + + assert!(!range2.encompasses(&range1)); + assert!(range2.encompasses(&range3)); + + assert!(!range3.encompasses(&range1)); + assert!(!range3.encompasses(&range2)); + } +}