diff --git a/plate-tool-lib/Cargo.toml b/plate-tool-lib/Cargo.toml index 609d4c3..7b8bec1 100644 --- a/plate-tool-lib/Cargo.toml +++ b/plate-tool-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plate-tool-lib" -version = "0.3.0" +version = "0.3.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/plate-tool-lib/src/csv/conversion.rs b/plate-tool-lib/src/csv/conversion.rs index ba10250..825b77e 100644 --- a/plate-tool-lib/src/csv/conversion.rs +++ b/plate-tool-lib/src/csv/conversion.rs @@ -1,7 +1,7 @@ use crate::transfer::Transfer; use crate::util::*; -use super::TransferRecord; +use super::{TransferRecord, transfer_record::TransferRecordDeserializeIntermediate, mangle_headers::mangle_headers}; use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -47,29 +47,28 @@ pub fn records_to_csv(trs: Vec) -> Result pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> { lazy_static! { static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap(); + static ref REGEX_ALT: Regex = Regex::new(r"(\d+)").unwrap(); } if let Some(c1) = REGEX.captures(input) { if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::().ok()) { - Some((row, col)) + return Some((row, col)) } else { - None + return None } - } else { - None } + None } pub fn read_csv(data: &str) -> Vec { - let (header, data) = data.split_at(data.find('\n').unwrap()); - let modified: String = header.to_lowercase() + data; - + let modified = mangle_headers(data); let mut rdr = csv::Reader::from_reader(modified.as_bytes()); let mut records: Vec = Vec::new(); - for record in rdr.deserialize::() { + for record in rdr.deserialize::() { match record { Ok(r) => { - //log::debug!("{:?}", r); - records.push(r); + if !r.is_empty() { + records.push(r.into()); + } } Err(e) => { log::debug!("{:?}", e); diff --git a/plate-tool-lib/src/csv/mangle_headers.rs b/plate-tool-lib/src/csv/mangle_headers.rs new file mode 100644 index 0000000..b9a1691 --- /dev/null +++ b/plate-tool-lib/src/csv/mangle_headers.rs @@ -0,0 +1,58 @@ +pub fn mangle_headers(data: &str) -> String { + let (header, rows) = data.split_at(data.find('\n').unwrap()); + let fields = header.trim().split(","); + let mut modified_headers: Vec = Vec::new(); + for field in fields { + if let Some(f) = detect_field(field) { + modified_headers.push(f.to_string()); + } + } + + modified_headers.join(",") + "\n" + rows +} + +fn detect_field(field: &str) -> Option { + match field.trim().to_lowercase() { + x if x.contains("source") || x.contains("src") => match x { + _ if x.contains("plate") => Some(Field::SourcePlate), + _ if x.contains("well") => Some(Field::SourceWell), + _ if x.contains("format") || x.contains("fmt") => Some(Field::SourceFormat), + _ => None, + }, + x if x.contains("destination") || x.contains("dest") => match x { + _ if x.contains("plate") => Some(Field::DestinationPlate), + _ if x.contains("well") => Some(Field::DestinationWell), + _ if x.contains("format") || x.contains("fmt") => Some(Field::DestinationFormat), + _ => None, + }, + x if x.contains("volume") => Some(Field::Volume), + x if x.contains("concentration") => Some(Field::Concentration), + _ => None, + } +} + +enum Field { + SourcePlate, + DestinationPlate, + SourceWell, + DestinationWell, + SourceFormat, + DestinationFormat, + Volume, + Concentration, +} + +impl ToString for Field { + fn to_string(&self) -> String { + match self { + Field::SourcePlate => "sourceplate".to_string(), + Field::DestinationPlate => "destinationplate".to_string(), + Field::SourceWell => "sourcewell".to_string(), + Field::DestinationWell => "destinationwell".to_string(), + Field::SourceFormat => "sourceformat".to_string(), + Field::DestinationFormat => "destinationformat".to_string(), + Field::Volume => "volume".to_string(), + Field::Concentration => "concentration".to_string(), + } + } +} diff --git a/plate-tool-lib/src/csv/mod.rs b/plate-tool-lib/src/csv/mod.rs index d72fe26..957fe10 100644 --- a/plate-tool-lib/src/csv/mod.rs +++ b/plate-tool-lib/src/csv/mod.rs @@ -1,6 +1,7 @@ mod transfer_record; mod conversion; mod auto; +mod mangle_headers; pub use transfer_record::volume_default; pub use transfer_record::TransferRecord; diff --git a/plate-tool-lib/src/csv/transfer_record.rs b/plate-tool-lib/src/csv/transfer_record.rs index ac54c87..9cfc4b7 100644 --- a/plate-tool-lib/src/csv/transfer_record.rs +++ b/plate-tool-lib/src/csv/transfer_record.rs @@ -1,33 +1,100 @@ use serde::{Deserialize, Serialize}; -use crate::transfer::Transfer; +use crate::{plate::PlateFormat, transfer::Transfer, util::num_to_letters}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone)] pub struct TransferRecord { - #[serde(rename = "Source Plate", alias = "source plate", alias = "src plate")] + #[serde(rename = "Source Plate")] pub source_plate: String, - #[serde(rename = "Source Well", alias = "source well", alias = "src well")] + #[serde(rename = "Source Well")] pub source_well: String, - #[serde( - rename = "Dest Plate", - alias = "dest plate", - alias = "destination plate" - )] + #[serde(rename = "Dest Plate")] pub destination_plate: String, - #[serde( - rename = "Destination Well", - alias = "destination well", - alias = "dest well" - )] + #[serde(rename = "Destination Well")] pub destination_well: String, - #[serde(rename = "Transfer Volume", alias = "transfer volume")] - #[serde(default = "volume_default")] + #[serde(rename = "Transfer Volume")] pub volume: f32, - #[serde(rename = "Concentration", alias = "concentration")] + #[serde(rename = "Concentration")] pub concentration: Option, } +#[derive(Deserialize, Debug, Clone)] +pub struct TransferRecordDeserializeIntermediate { + #[serde(rename = "sourceplate")] + source_plate: String, + #[serde(rename = "destinationplate")] + destination_plate: String, + #[serde(rename = "sourcewell")] + source_well: String, + #[serde(rename = "sourceformat")] + source_format: Option, + #[serde(rename = "destinationwell")] + destination_well: String, + #[serde(rename = "destinationformat")] + destination_format: Option, + #[serde(rename = "volume")] + volume: Option, + #[serde(rename = "concentration")] + concentration: Option, +} + +impl From for TransferRecord { + fn from(value: TransferRecordDeserializeIntermediate) -> Self { + let mut source_well: String = value.source_well; + if let Some(pformat) = value + .source_format + .and_then(|x| PlateFormat::try_from(x.as_str()).ok()) + { + if let Ok(well_number) = source_well.parse::() { + if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) { + source_well = alphanumeric; + } + } + } + + let mut destination_well: String = value.destination_well; + if let Some(pformat) = value + .destination_format + .and_then(|x| PlateFormat::try_from(x.as_str()).ok()) + { + if let Ok(well_number) = destination_well.parse::() { + if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) { + destination_well = alphanumeric; + } + } + } + + let volume = value.volume.unwrap_or(volume_default()); + + TransferRecord { + source_plate: value.source_plate, + destination_plate: value.destination_plate, + source_well, + destination_well, + volume, + concentration: value.concentration, + } + } +} + +impl TransferRecordDeserializeIntermediate { + pub fn is_empty(&self) -> bool { + self.source_plate.is_empty() + || self.destination_plate.is_empty() + || self.source_well.is_empty() + || self.destination_well.is_empty() + } +} + +fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option { + let column_height: u16 = pformat.size().0 as u16; + let column = input.div_ceil(column_height); + let row = input % column_height; + let row_str = num_to_letters(row as u8)?; + + Some(format!("{}{}", row_str, column)) +} + pub fn volume_default() -> f32 { Transfer::default().volume } - diff --git a/plate-tool-lib/src/plate.rs b/plate-tool-lib/src/plate.rs index 0c9040e..2a5158c 100644 --- a/plate-tool-lib/src/plate.rs +++ b/plate-tool-lib/src/plate.rs @@ -77,6 +77,20 @@ impl TryFrom<&str> for PlateFormat { } } } +impl From<&PlateFormat> for u16 { + fn from(value: &PlateFormat) -> Self { + match value { + PlateFormat::W6 => 6, + PlateFormat::W12 => 12, + PlateFormat::W24 => 24, + PlateFormat::W48 => 48, + PlateFormat::W96 => 96, + PlateFormat::W384 => 384, + PlateFormat::W1536 => 1536, + PlateFormat::W3456 => 3456, + } + } +} impl PlateFormat { pub fn size(&self) -> (u8, u8) {