plate-tool/plate-tool-lib/src/csv/transfer_record.rs

108 lines
3.8 KiB
Rust

use serde::{Deserialize, Serialize};
use crate::{plate::PlateFormat, transfer::Transfer, util::num_to_letters};
/// Represents a single line of a CSV picklist.
/// In practice, this is generated from the deserialize intermediate.
/// A list of `TransferRecord`s can be used to create a `transfer::Transfer` struct;
/// see the `auto` module for this.
#[derive(Serialize, Debug, Clone)]
pub struct TransferRecord {
#[serde(rename = "Source Barcode")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Barcode")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
#[serde(rename = "Concentration")]
pub concentration: Option<f32>,
}
/// The deserialization intermediate is generated from a mangled CSV s.t.
/// the column headers are standardized to those given to the derive macro.
///
/// `TransferRecord` can *always* be generated from the intermediate.
/// Currently there's no reason why the inverse couldn't be true,
/// but there's no reason to convert back into the deserialize-only intermediate.
#[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<String>,
#[serde(rename = "destinationwell")]
destination_well: String,
#[serde(rename = "destinationformat")]
destination_format: Option<String>,
#[serde(rename = "volume")]
volume: Option<f32>,
#[serde(rename = "concentration")]
concentration: Option<f32>,
}
impl From<TransferRecordDeserializeIntermediate> 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::<u16>() {
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::<u16>() {
if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) {
destination_well = alphanumeric;
}
}
}
let volume = value.volume.unwrap_or(2.5f32);
TransferRecord {
source_plate: value.source_plate,
destination_plate: value.destination_plate,
source_well,
destination_well,
volume,
concentration: value.concentration,
}
}
}
impl TransferRecordDeserializeIntermediate {
/// Used to cull malformed picklist entries that lack required information
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<String> {
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))
}