use std::{
fmt::{Display, Write as _},
num::{NonZeroU64, ParseIntError},
str::FromStr,
};
use num_traits::AsPrimitive as _;
use thiserror::Error;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Size(NonZeroU64, NonZeroU64);
impl Default for Size {
fn default() -> Self {
let n = NonZeroU64::new(4).unwrap();
Self(n, n)
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum SizeError {
#[error("InvalidSize: width ({0}) and height ({1}) must be greater than 0")]
InvalidSize(u64, u64),
}
impl Size {
pub fn new(width: u64, height: u64) -> Result<Self, SizeError> {
Ok(Self(
NonZeroU64::new(width).ok_or(SizeError::InvalidSize(width, height))?,
NonZeroU64::new(height).ok_or(SizeError::InvalidSize(width, height))?,
))
}
#[must_use]
pub fn width(&self) -> u64 {
self.0.get()
}
#[must_use]
pub fn height(&self) -> u64 {
self.1.get()
}
#[must_use]
pub fn area(&self) -> u64 {
self.width() * self.height()
}
#[must_use]
pub fn num_pieces(&self) -> u64 {
self.area() - 1
}
#[must_use]
pub fn is_within_bounds(&self, (x, y): (u64, u64)) -> bool {
x < self.width() && y < self.height()
}
#[must_use]
pub fn transpose(&self) -> Self {
Self(self.1, self.0)
}
#[must_use]
pub fn shrink_to_square(&self) -> Self {
let s = self.0.min(self.1);
Self(s, s)
}
#[must_use]
pub fn expand_to_square(&self) -> Self {
let s = self.0.max(self.1);
Self(s, s)
}
#[must_use]
pub fn is_square(&self) -> bool {
self.width() == self.height()
}
#[must_use]
pub fn num_states(&self) -> u128 {
let (w, h) = (*self).into();
if w == 1 {
h as u128
} else if h == 1 {
w as u128
} else {
(1..=self.area().as_()).product::<u128>() / 2
}
}
}
impl From<Size> for (u64, u64) {
fn from(value: Size) -> Self {
(value.width(), value.height())
}
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string())?;
f.write_char('x')?;
f.write_str(&self.1.to_string())
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ParseSizeError {
#[error("ParseError: failed to parse size string")]
ParseError,
#[error("ParseWidthError: {0}")]
ParseWidthError(ParseIntError),
#[error("ParseWidthError: {0}")]
ParseHeightError(ParseIntError),
#[error("SizeError: {0}")]
SizeError(SizeError),
}
impl FromStr for Size {
type Err = ParseSizeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(s) = s.trim().parse::<u64>() {
Self::new(s, s).map_err(ParseSizeError::SizeError)
} else {
let (w, h) = s.split_once('x').ok_or(ParseSizeError::ParseError)?;
let (w, h) = (
w.trim()
.parse::<u64>()
.map_err(ParseSizeError::ParseWidthError)?,
h.trim()
.parse::<u64>()
.map_err(ParseSizeError::ParseHeightError)?,
);
Self::new(w, h).map_err(ParseSizeError::SizeError)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn size(width: u64, height: u64) -> Size {
Size::new(width, height).unwrap()
}
#[test]
fn test_new() {
assert_eq!(Size::new(2, 2), Ok(size(2, 2)));
assert_eq!(Size::new(2, 3), Ok(size(2, 3)));
assert_eq!(Size::new(3, 2), Ok(size(3, 2)));
assert_eq!(Size::new(3, 3), Ok(size(3, 3)));
assert_eq!(Size::new(1, 1), Ok(size(1, 1)));
assert_eq!(Size::new(1, 2), Ok(size(1, 2)));
assert_eq!(Size::new(2, 1), Ok(size(2, 1)));
assert_eq!(Size::new(0, 0), Err(SizeError::InvalidSize(0, 0)));
assert_eq!(Size::new(0, 1), Err(SizeError::InvalidSize(0, 1)));
assert_eq!(Size::new(1, 0), Err(SizeError::InvalidSize(1, 0)));
}
#[test]
fn test_width() {
assert_eq!(size(2, 3).width(), 2);
}
#[test]
fn test_height() {
assert_eq!(size(2, 3).height(), 3);
}
#[test]
fn test_area() {
assert_eq!(size(2, 3).area(), 6);
}
#[test]
fn test_num_pieces() {
assert_eq!(size(2, 3).num_pieces(), 5);
}
#[test]
fn test_is_within_bounds() {
assert!(size(2, 3).is_within_bounds((0, 0)));
assert!(size(2, 3).is_within_bounds((1, 2)));
assert!(!size(2, 3).is_within_bounds((2, 3)));
assert!(!size(2, 3).is_within_bounds((3, 2)));
}
#[test]
fn test_transpose() {
assert_eq!(size(2, 3).transpose(), size(3, 2));
}
#[test]
fn test_shrink_to_square() {
assert_eq!(size(2, 5).shrink_to_square(), size(2, 2));
assert_eq!(size(5, 2).shrink_to_square(), size(2, 2));
assert_eq!(size(5, 5).shrink_to_square(), size(5, 5));
}
#[test]
fn test_expand_to_square() {
assert_eq!(size(2, 5).expand_to_square(), size(5, 5));
assert_eq!(size(5, 2).expand_to_square(), size(5, 5));
assert_eq!(size(5, 5).expand_to_square(), size(5, 5));
}
#[test]
fn test_is_square() {
assert!(!size(2, 5).is_square());
assert!(!size(5, 2).is_square());
assert!(size(5, 5).is_square());
}
#[test]
fn test_num_states() {
assert_eq!(size(1, 1).num_states(), 1);
assert_eq!(size(1, 5).num_states(), 5);
assert_eq!(size(5, 1).num_states(), 5);
assert_eq!(size(2, 2).num_states(), 12);
assert_eq!(size(2, 3).num_states(), 360);
assert_eq!(size(3, 2).num_states(), 360);
assert_eq!(size(3, 3).num_states(), 181440);
assert_eq!(size(4, 4).num_states(), 10461394944000);
assert_eq!(size(5, 5).num_states(), 7755605021665492992000000);
assert_eq!(
size(17, 2).num_states(),
147616399519802070423809304821760000000
);
}
#[test]
fn test_into_usize_usize() {
let (w, h) = size(2, 3).into();
assert_eq!((w, h), (2, 3));
}
#[test]
fn test_display() {
assert_eq!(size(2, 3).to_string(), "2x3");
}
#[test]
fn test_parse() {
assert_eq!("2x3".parse::<Size>(), Ok(size(2, 3)));
assert_eq!(" 2x3 ".parse::<Size>(), Ok(size(2, 3)));
assert_eq!("2 x 3".parse::<Size>(), Ok(size(2, 3)));
assert_eq!(" 2 x3 ".parse::<Size>(), Ok(size(2, 3)));
assert_eq!("2".parse::<Size>(), Ok(size(2, 2)));
assert_eq!(" 2 ".parse::<Size>(), Ok(size(2, 2)));
}
}