diff --git a/plate-tool-lib/src/transfer_region.rs b/plate-tool-lib/src/transfer_region.rs index 587d319..ca99f5a 100644 --- a/plate-tool-lib/src/transfer_region.rs +++ b/plate-tool-lib/src/transfer_region.rs @@ -1,9 +1,16 @@ use serde::{Deserialize, Serialize}; +use regex::Regex; + use super::plate::Plate; +use crate::plate::PlateType; +use crate::util; use crate::Well; -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +use std::fmt; +use std::sync::LazyLock; + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] pub struct CustomRegion { src: Vec, dest: Vec, @@ -15,7 +22,7 @@ impl CustomRegion { } } -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] pub enum Region { Rect(Well, Well), Point(Well), @@ -26,6 +33,28 @@ impl Default for Region { Region::Point(Well { row: 1, col: 1 }) } } +impl Display for Region { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Region::Rect(ul, br) => { + write!( + f, + "{}{}:{}{}", + util::num_to_letters(ul.col).unwrap(), + ul.row, + util::num_to_letters(br.col).unwrap(), + br.row + ) + } + Region::Point(w) => { + write!(f, "{}{}", util::num_to_letters(w.col).unwrap(), w.row) + } + Region::Custom(..) => { + write!(f, "Custom Region") + } + } + } +} impl TryFrom for (Well, Well) { type Error = &'static str; fn try_from(region: Region) -> Result { @@ -53,9 +82,76 @@ impl Region { dest: dest_pts, }) } + + pub fn new_from_wells(w1: Well, w2: Option) -> Self { + if w2.is_none() { + Self::Point(w1) + } else { + let w2 = w2.unwrap(); + let (w1, w2) = standardize_rectangle(&w1, &w2); + Self::Rect(w1, w2) + } + } + + pub fn try_parse_str(input: &str) -> Option { + static POINT_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap()); + static RECT_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"([A-Z,a-z]+)(\d+):([A-Z,a-z]+)(\d+)").unwrap()); + + log::info!("{:?}", input); + if let Some(captures) = RECT_REGEX.captures(input) { + if let (Some(row1), Some(col1), Some(row2), Some(col2)) = ( + crate::util::letters_to_num(&captures[1]), + captures[2].parse::().ok(), + crate::util::letters_to_num(&captures[3]), + captures[4].parse::().ok(), + ) { + return Some(Region::Rect( + Well { + row: row1, + col: col1, + }, + Well { + row: row2, + col: col2, + }, + )); + } else { + return None; + } + } else if let Some(captures) = POINT_REGEX.captures(input) { + if let (Some(row), Some(col)) = ( + crate::util::letters_to_num(&captures[1]), + captures[2].parse::().ok(), + ) { + return Some(Region::Point(Well { row, col })); + } else { + return None; + } + } else { + None + } + } + + pub fn contains_well(&self, w: &Well, pt: Option) -> bool { + match self { + Self::Point(x) => x == w, + Self::Rect(ul, br) => { + w.row <= br.row && w.row >= ul.row && w.col <= br.col && w.col >= ul.col + } + Self::Custom(xs) => match pt { + Some(PlateType::Source) => xs.src.contains(w), + Some(PlateType::Destination) => xs.dest.contains(w), + None => { + unreachable!("Cannot check if custom contains well without knowing the type") + } + }, + } + } } -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] +#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug, Hash)] pub struct TransferRegion { pub source_plate: Plate, pub source_region: Region, // Even if it is just a point, we don't want corners. @@ -233,8 +329,10 @@ impl TransferRegion { let j = j .saturating_sub(s_ul.col) .saturating_div(il_source.1.unsigned_abs()); - let checked_il_dest = (u8::max(il_dest.0.unsigned_abs(), 1u8), - u8::max(il_dest.1.unsigned_abs(), 1u8)); + let checked_il_dest = ( + u8::max(il_dest.0.unsigned_abs(), 1u8), + u8::max(il_dest.1.unsigned_abs(), 1u8), + ); let row_modulus = number_used_src_wells.0 * checked_il_dest.0; let column_modulus = number_used_src_wells.1 * checked_il_dest.1; @@ -256,12 +354,12 @@ impl TransferRegion { .filter(|Well { row: x, col: y }| { // How many times have we replicated? < How many are we allowed // to replicate? - x.checked_sub(d_ul.row).unwrap().div_euclid( - row_modulus - ) < count.0 - && y.checked_sub(d_ul.col).unwrap().div_euclid( - column_modulus - ) < count.1 + x.checked_sub(d_ul.row).unwrap().div_euclid(row_modulus) + < count.0 + && y.checked_sub(d_ul.col) + .unwrap() + .div_euclid(column_modulus) + < count.1 }) .collect(), ) @@ -319,8 +417,8 @@ impl TransferRegion { return Err("Source region is out-of-bounds! (Too wide)"); } - if il_dest == (0,0) { - return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate.") + if il_dest == (0, 0) { + return Err("Refusing to pool both dimensions in a rectangular transfer!\nPlease select a point in the destination plate."); } } Region::Custom(_) => return Ok(()), @@ -372,7 +470,7 @@ fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) { #[cfg(debug_assertions)] use std::fmt; -use std::ops::Mul; +use std::{fmt::Display, ops::Mul}; #[cfg(debug_assertions)] // There should be no reason to print a transfer otherwise impl fmt::Display for TransferRegion {