#![warn(missing_docs)]
#![allow(clippy::tabs_in_doc_comments)]
macro_rules! err {
($e: expr) => { Err($e.to_string().into()) }
}
use std::{
fmt,
fs::File,
io::{BufWriter, Read, Write},
ops::{Index, IndexMut},
path::Path,
};
mod cmp;
mod color;
mod delta;
mod field;
mod parse;
mod math;
mod vendor;
pub use color::ColorType;
pub use delta::*;
pub use math::*;
pub use vendor::Vendor;
pub use field::Field;
pub type BoxErr = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, BoxErr>;
pub use DataPoint::*;
#[derive(Debug, Clone)]
pub enum DataPoint {
Alpha(String),
Float(f32),
Int(i32),
}
impl DataPoint {
pub fn new_alpha<S: ToString>(alpha: S) -> Self {
Alpha(alpha.to_string())
}
pub fn new_float<F: Into<f32>>(float: F) -> Self {
Float(float.into())
}
pub fn new_int<I: Into<i32>>(int: I) -> Self {
Int(int.into())
}
pub fn to_int(&self) -> Result<Self> {
Ok(match self {
Alpha(a) => Int(a.parse()?),
Float(f) => Int(f.round() as i32),
Int(i) => Int(i.to_owned()),
})
}
pub fn to_float(&self) -> Result<Self> {
Ok(match self {
Alpha(a) => Float(a.parse()?),
Float(f) => Float(f.to_owned()),
Int(i) => Float(*i as f32),
})
}
pub fn to_float_unchecked(&self) -> f32 {
match self.to_float() {
Ok(Float(f)) => f,
_ => panic!("unchecked conversion to float failed"),
}
}
pub fn to_alpha(&self) -> Self {
DataPoint::new_alpha(self)
}
}
impl AsRef<Self> for DataPoint {
fn as_ref(&self) -> &Self {
self
}
}
use std::cmp::Ordering::*;
impl PartialOrd for DataPoint {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self.to_float(), other.to_float()) {
(Ok(f_self), Ok(f_other)) => f_self.partial_cmp(&f_other),
_ => match (self, other) {
(Alpha(a_self), Alpha(a_other)) => a_self.partial_cmp(a_other),
(Alpha(_), Float(_)|Int(_)) => Some(Greater),
(Float(_)|Int(_), Alpha(_)) => Some(Less),
_ => unreachable!("DataPoint PartialOrd")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetaData {
KeyVal {
key: String,
val: String,
},
Comment(String),
Blank,
}
impl MetaData {
pub fn key(&self) -> Option<&str> {
if let MetaData::KeyVal{ key, .. } = self {
Some(key)
} else {
None
}
}
pub fn value(&self) -> Option<&str> {
Some(match self {
MetaData::KeyVal { val, .. } => val,
MetaData::Comment(val) => val,
MetaData::Blank => return None,
})
}
pub fn value_mut(&mut self) -> Option<&mut String> {
Some(match self {
MetaData::KeyVal { val, .. } => val,
MetaData::Comment(val) => val,
MetaData::Blank => return None,
})
}
}
impl CgatsFmt for MetaData {
fn cgats_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MetaData::KeyVal { key, val } => writeln!(f, "{}\t{}", key, val),
MetaData::Comment(comment) => writeln!(f, "{}", comment),
MetaData::Blank => writeln!(f),
}
}
}
impl fmt::Display for DataPoint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.cgats_fmt(f)
}
}
impl CgatsFmt for DataPoint {
fn cgats_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Alpha(a) => write!(f, "{a}"),
Int(i) => write!(f, "{i}"),
Float(n) => match f.precision() {
Some(p) => write!(f, "{n:.p$}"),
None => write!(f, "{n}"),
}
}
}
}
#[test]
fn display_datapoint() {
let f = Float(1.2345);
assert_eq!(format!("{:0.2}", f), "1.23");
assert_eq!(format!("{:0.3}", f), "1.235");
let i = Int(42);
assert_eq!(format!("{:0.3}", i), "42");
assert_eq!(format!("{:0.1}", i), "42");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cgats {
vendor: Vendor,
metadata: Vec<MetaData>,
data_format: DataFormat,
data: Vec<DataPoint>,
}
#[test]
fn new_cgats() {
let cgats: Cgats =
"CGATS.17
BEGIN_DATA_FORMAT
END_DATA_FORMAT
BEGIN_DATA
END_DATA"
.parse().unwrap();
assert_eq!(cgats, Cgats::new());
}
impl Cgats {
pub fn new() -> Self {
Cgats {
vendor: Vendor::Cgats,
metadata: Vec::new(),
data_format: DataFormat::new(),
data: Vec::new()
}
}
pub fn with_capacity(cap: usize) -> Self {
Cgats {
data: Vec::with_capacity(cap),
..Self::new()
}
}
pub fn summary(&self) -> String {
format!(
"{}[{}; {}]",
self.vendor(),
self.color_types().iter().join(", "),
self.n_rows()
)
}
pub fn vendor(&self) -> &Vendor {
&self.vendor
}
pub fn get_metadata<'a>(&'a self, key: &str) -> Option<&'a str> {
self.metadata.iter()
.find(|meta| meta.key() == Some(key))
.and_then(MetaData::value)
}
pub fn get_metadata_mut(&mut self, key: &str) -> Option<&mut String> {
self.metadata.iter_mut()
.find(|meta| meta.key() == Some(key))
.and_then(MetaData::value_mut)
}
pub fn insert_metadata_keyval<S: ToString, T: ToString>(&mut self, key: S, val: T) {
if let Some(value) = self.get_metadata_mut(&key.to_string()) {
*value = val.to_string();
} else {
self.metadata.push(MetaData::KeyVal{key: key.to_string(), val: val.to_string()});
}
}
pub fn iter(&self) -> impl Iterator<Item=&DataPoint> {
self.data.iter()
}
pub fn iter_with_fields(&self) -> impl Iterator<Item=(&Field, &DataPoint)> {
self.data_format
.fields
.iter()
.cycle()
.zip(self.iter())
}
pub fn iter_mut_with_fields(&mut self) -> impl Iterator<Item=(Field, &mut DataPoint)> {
self.data_format
.fields
.clone()
.into_iter()
.cycle()
.zip(self.iter_mut())
}
pub fn fields(&self) -> impl Iterator<Item=&Field> {
self.data_format.fields()
}
pub fn fields_mut(&mut self) -> impl Iterator<Item=&mut Field> {
self.data_format.fields_mut()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item=&mut DataPoint> {
self.data.iter_mut()
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut string = String::new();
File::open(path)?.read_to_string(&mut string)?;
string.parse()
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let buf = &mut BufWriter::new(File::create(path)?);
self.write(buf)
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
Ok(write!(writer, "{}", self)?)
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn n_rows(&self) -> usize {
self.data.len().checked_div(self.n_cols()).unwrap_or(0)
}
pub fn n_cols(&self) -> usize {
self.data_format.len()
}
pub fn get_row(&self, row: usize) -> Option<impl Iterator<Item=&DataPoint>> {
let index = row * self.n_cols();
Some(self.data.get(index..index+self.n_cols())?.iter())
}
pub fn get_row_by_values<F: AsRef<Field>, D: AsRef<DataPoint>>(
&self,
fields_values: &[(F, D)]
) -> Option<impl Iterator<Item=&DataPoint>> {
self.get_row(self.get_row_index_by_values(fields_values)?)
}
pub fn get_row_index_by_values<F: AsRef<Field>, D: AsRef<DataPoint>>(
&self,
fields_values: &[(F, D)],
) -> Option<usize> {
if !fields_values.iter().all(|(field, _value)| self.data_format.contains(field.as_ref())) {
return None;
}
let mut row_index = None;
for (index, row) in self.rows().enumerate() {
let this_row = self.data_format.fields.iter().cycle().zip(row).collect::<Vec<_>>();
if fields_values.iter().all(|fv| this_row.contains(&(fv.0.as_ref(), fv.1.as_ref())))
{
row_index = Some(index);
break;
}
}
row_index
}
pub fn remove_row(&mut self, row: usize) -> Option<impl Iterator<Item = DataPoint> + '_> {
if row >= self.n_rows() {
None
} else {
let range = (row * self.n_cols())..((row * self.n_cols()) + self.n_cols());
Some(self.data.drain(range))
}
}
pub fn move_row(&mut self, from_row: usize, to_row: usize) -> Result<()> {
if from_row == to_row {
Ok(())
} else {
#[allow(clippy::needless_collect)]
let row = self.remove_row(from_row)
.ok_or(format!("could not remove row {from_row}"))?
.collect::<Vec<_>>();
self.insert_row(to_row, row.into_iter())
}
}
pub fn transpose_chart(&mut self, chart_width: usize) {
let mut transpose_map: Vec<usize> = Vec::new();
let mut new_data: Vec<DataPoint> = Vec::with_capacity(self.len());
let chart_height = chart_height(self.n_rows(), chart_width);
for x in 0..chart_width {
for y in 0..chart_height {
transpose_map.push(row_index_by_chart_pos(chart_width, (x, y)));
}
}
for row_index in transpose_map {
if let Some(row) = self.get_row(row_index) {
for data_point in row {
new_data.push(data_point.clone());
}
}
}
self.data = new_data;
self.insert_metadata_keyval("LGOROWLENGTH", chart_height);
}
pub fn get_row_mut(&mut self, row: usize) -> impl Iterator<Item=&mut DataPoint> {
let n_rows = self.n_rows();
self.iter_mut().skip(row).step_by(n_rows)
}
pub fn rows(&self) -> impl Iterator<Item=impl Iterator<Item=&DataPoint>> {
(0..self.n_rows()).into_iter()
.filter_map(|row| self.get_row(row))
}
pub fn get_col(&self, col: usize) -> impl Iterator<Item=&DataPoint> {
self.iter().skip(col).step_by(self.n_cols())
}
pub fn get_col_by_field(&self, field: &Field) -> Option<impl Iterator<Item = &DataPoint>> {
Some(self.get_col(self.index_by_field(field)?))
}
pub fn get_col_mut(&mut self, col: usize) -> impl Iterator<Item=&mut DataPoint> {
let n_cols = self.n_cols();
self.iter_mut().skip(col).step_by(n_cols)
}
pub fn get_col_mut_by_field(
&mut self,
field: &Field,
) -> Option<impl Iterator<Item = &mut DataPoint>> {
Some(self.get_col_mut(self.index_by_field(field)?))
}
pub fn cols(&self) -> impl Iterator<Item=impl Iterator<Item=&DataPoint>> {
(0..self.n_cols()).into_iter()
.map(|col| self.get_col(col))
}
pub fn cols_with_fields(&self) -> impl Iterator<Item=(&Field, impl Iterator<Item=&DataPoint>)> {
self.data_format
.fields
.iter()
.zip(self.cols())
}
pub fn color_types(&self) -> Vec<ColorType> {
let mut color_types = Vec::new();
if let Some(color_type) = self.cb_color_type() {
color_types.push(color_type);
}
color_types.append(&mut self.data_format.color_types());
color_types
}
fn cb_color_type(&self) -> Option<ColorType> {
if self.vendor != Vendor::ColorBurst || self.n_rows() % 21 != 0 {
None
} else {
Some(match self.n_rows() / 21 {
0..=2 => return None,
3 => ColorType::Rgb,
4 => ColorType::Cmyk,
5 => ColorType::FiveClr,
6 => ColorType::SixClr,
7 => ColorType::SevenClr,
8 => ColorType::EightClr,
_ => ColorType::NClr,
})
}
}
pub fn has_color_type(&self, color_type: &ColorType) -> bool {
self.color_types().contains(color_type)
}
pub fn reindex_sample_id(&mut self) {
self.reindex_sample_id_at(0)
}
pub fn reindex_sample_id_at(&mut self, start: usize) {
if let Some(i) = self.index_by_field(&SAMPLE_ID) {
self.get_col_mut(i)
.enumerate()
.for_each(|(i, dp)| *dp = Int((i + start) as i32));
} else {
self.insert_column(0, SAMPLE_ID, (start..(self.n_rows() + start)).map(|i| Int(i as i32)))
.expect("column must have same number of rows")
}
}
pub fn index_by_field(&self, field: &Field) -> Option<usize> {
self.data_format.index_by_field(field)
}
pub fn insert_column(
&mut self,
index: usize,
label: Field,
iter: impl Iterator<Item=DataPoint>
) -> Result<()> {
let iter = iter.collect::<Vec<DataPoint>>();
if self.is_empty() || iter.len() == self.n_rows() {
self.data_format.fields.insert(index, label);
let cols = self.n_cols();
let mut insert = index;
for dp in iter {
self.data.insert(insert, dp);
insert += cols;
}
Ok(())
} else {
Err(format!(
"column contains {} items but number of rows is {}",
iter.len(),
self.n_rows(),
).into())
}
}
pub fn push_column(&mut self, field: Field, iter: impl Iterator<Item=DataPoint>) -> Result<()> {
self.insert_column(self.n_cols(), field, iter)
}
pub fn insert_row(&mut self, index: usize, iter: impl Iterator<Item=DataPoint>) -> Result<()> {
let iter = iter.collect::<Vec<DataPoint>>();
if iter.len() != self.n_cols() {
Err(format!("row contains {} items but number of cols is {}", iter.len(), self.n_cols()).into())
} else {
let mut insert = index * self.n_cols();
for dp in iter.into_iter() {
self.data.insert(insert, dp);
insert += 1;
}
Ok(())
}
}
pub fn push_row(&mut self, iter: impl Iterator<Item=DataPoint>) -> Result<()> {
self.insert_row(self.n_rows(), iter)
}
pub fn append(&mut self, other: &Cgats) -> Result<()> {
if self.data_format != other.data_format {
return err!("DATA_FORMAT does not match");
}
for row in other.rows() {
self.push_row(row.cloned())?;
}
if self.data_format.contains(&Field::SAMPLE_ID) {
self.reindex_sample_id();
}
Ok(())
}
pub fn concatenate<'a>(root: &Cgats, other: impl Iterator<Item=&'a Cgats>) -> Result<Self> {
let mut root = root.clone();
for cgats in other {
root.append(cgats)?;
}
if root.data_format.contains(&Field::SAMPLE_ID) {
root.reindex_sample_id();
}
Ok(root)
}
pub fn to_colorburst(&self) -> Result<Self> {
self.to_colorburst_by_value()
.or_else(|_|self.to_colorburst_in_order())
}
fn to_colorburst_in_order(&self) -> Result<Self> {
let d_red = self.get_col_by_field(&Field::D_RED).ok_or("D_RED field not found")?;
let d_green = self.get_col_by_field(&Field::D_GREEN).ok_or("D_GREEN field not found")?;
let d_blue = self.get_col_by_field(&Field::D_BLUE).ok_or("D_BLUE field not found")?;
let d_vis = self.get_col_by_field(&Field::D_VIS).ok_or("D_VIS field not found")?;
let lab_l = self.get_col_by_field(&Field::LAB_L).ok_or("LAB_L field not found")?;
let lab_a = self.get_col_by_field(&Field::LAB_A).ok_or("LAB_A field not found")?;
let lab_b = self.get_col_by_field(&Field::LAB_B).ok_or("LAB_B field not found")?;
let mut cgats = Cgats::with_capacity(self.n_rows() * 7);
cgats.vendor = Vendor::ColorBurst;
cgats.push_column(Field::D_RED, d_red.cloned())?;
cgats.push_column(Field::D_GREEN, d_green.cloned())?;
cgats.push_column(Field::D_BLUE, d_blue.cloned())?;
cgats.push_column(Field::D_VIS, d_vis.cloned())?;
cgats.push_column(Field::LAB_L, lab_l.cloned())?;
cgats.push_column(Field::LAB_A, lab_a.cloned())?;
cgats.push_column(Field::LAB_B, lab_b.cloned())?;
Ok(cgats)
}
fn to_colorburst_by_value(&self) -> Result<Self> {
let mut cgats = self.clone();
cgats.vendor = Vendor::ColorBurst;
let fields = match self.color_types()
.into_iter()
.find(move |color| COLORBURST_COLOR_TYPES.contains(color))
.ok_or("unable to determine color type")?
{
ColorType::Rgb => color::FORMAT_RGB.as_slice(),
ColorType::Cmyk => color::FORMAT_CMYK.as_slice(),
ColorType::FiveClr => color::FORMAT_5CLR.as_slice(),
ColorType::SixClr => color::FORMAT_6CLR.as_slice(),
ColorType::SevenClr => color::FORMAT_7CLR.as_slice(),
ColorType::EightClr => color::FORMAT_8CLR.as_slice(),
n => return err!(format!("unsupported color type: {n}")),
};
cgats.data.clear();
for color in fields {
for value in COLORBURST_INPUT {
let mut row = Vec::with_capacity(fields.len());
row.push((color, Float(value)));
for channel in fields {
if color != channel {
row.push((channel, Float(0.0)));
}
}
let row = self.get_row_by_values(row.as_slice())
.ok_or(format!("unable to find {color} with value {value}"))?;
cgats.push_row(row.cloned())?;
}
}
cgats.to_colorburst_in_order()
}
pub fn colorburst_to_cgats(&self) -> Result<Self> {
if self.vendor != Vendor::ColorBurst {
err!("not a ColorBurst linearization format")
} else if self.n_rows() % 21 != 0 {
err!("row count must be a multiple of 21 for ColorBurst")
} else {
let mut cgats = self.clone();
cgats.vendor = Vendor::Cgats;
cgats.reindex_sample_id();
let n_channels = self.n_rows() / COLORBURST_INPUT.len();
for i in 0..n_channels {
let field = Field::from_channel_index(n_channels, i)?;
let mut column = Vec::<DataPoint>::with_capacity(self.n_rows());
let mut index = i * 21;
for _val in 0..index {
column.push(DataPoint::new_float(0.0));
}
for val in COLORBURST_INPUT {
column.push(DataPoint::new_float(val));
index += 1;
}
for _val in index..cgats.n_rows() {
column.push(DataPoint::new_float(0.0));
}
let insert_index = cgats.index_by_field(&SAMPLE_ID).expect("SAMPLE_ID field not found");
cgats.insert_column(insert_index + i + 1, field, column.into_iter())?;
}
Ok(cgats)
}
}
}
#[test]
fn transpose() {
let mut cgats = Cgats::from_file("test_files/transpose.txt").unwrap();
cgats.transpose_chart(2);
let mut transposed = Cgats::from_file("test_files/transposed.txt").unwrap();
assert_eq!(cgats, transposed);
cgats.transpose_chart(4);
transposed.transpose_chart(4);
assert_eq!(cgats, transposed);
let mut cgats = Cgats::from_file("test_files/transpose_uneven.txt").unwrap();
cgats.transpose_chart(4);
let mut transposed = Cgats::from_file("test_files/transposed_uneven.txt").unwrap();
assert_eq!(cgats, transposed);
cgats.transpose_chart(3);
transposed.transpose_chart(3);
assert_eq!(cgats, transposed);
}
#[test]
fn test_chart_height() {
assert_eq!(chart_height(5, 5), 1);
assert_eq!(chart_height(5, 6), 1);
assert_eq!(chart_height(5, 100), 1);
assert_eq!(chart_height(9, 3), 3);
assert_eq!(chart_height(10, 4), 3);
assert_eq!(chart_height(10, 3), 4);
assert_eq!(chart_height(12, 4), 3);
assert_eq!(chart_height(12, 3), 4);
}
fn chart_height(patches: usize, width: usize) -> usize {
let full_rows = patches / width;
if patches % width == 0 {
full_rows
} else {
full_rows + 1
}
}
fn row_index_by_chart_pos(chart_width: usize, chart_xy: (usize, usize)) -> usize {
chart_xy.1 * chart_width + chart_xy.0
}
#[test]
fn row_by_values() {
let cgats = Cgats::from_file("test_files/cgats1.tsv").unwrap();
let cyan_100 = &[
(&CMYK_C, &Int(100)),
(&CMYK_M, &Int(0)),
(&CMYK_Y, &Int(0)),
(&CMYK_K, &Int(0)),
];
let magenta_100 = &[
(&CMYK_C, &Int(0)),
(&CMYK_M, &Int(100)),
(&CMYK_Y, &Int(0)),
(&CMYK_K, &Int(0)),
];
let yellow_100 = &[
(&CMYK_C, &Int(0)),
(&CMYK_M, &Int(0)),
(&CMYK_Y, &Int(100)),
(&CMYK_K, &Int(0)),
];
let row = cgats.get_row_by_values(cyan_100).unwrap().collect::<Vec<_>>();
assert_eq!(row[1], "Cyan");
let row = cgats.get_row_by_values(magenta_100).unwrap().collect::<Vec<_>>();
assert_eq!(row[1], "Magenta");
let row = cgats.get_row_by_values(yellow_100).unwrap().collect::<Vec<_>>();
assert_eq!(row[1], "Yellow");
}
#[test]
fn cb_to_cgats() {
let cb = Cgats::from_file("test_files/colorburst0.txt").unwrap();
let cgats = cb.colorburst_to_cgats().unwrap();
println!("{cgats}");
let mut magenta = cgats.get_col_by_field(&Field::CMYK_M).unwrap().skip(21);
assert_eq!(magenta.next().unwrap(), &0.0);
assert_eq!(magenta.next().unwrap(), &5.0);
assert_eq!(magenta.next().unwrap(), &10.0);
let cb = Cgats::from_file("test_files/colorburst1.lin").unwrap();
let cgats = cb.colorburst_to_cgats().unwrap();
let mut green = cgats.get_col_by_field(&Field::SIXCLR_6).unwrap().skip(21 * 5);
assert_eq!(green.next().unwrap(), &0.0);
assert_eq!(green.next().unwrap(), &5.0);
assert_eq!(green.next().unwrap(), &10.0);
println!("{cgats}");
}
#[test]
fn cgats_to_cb() {
let cgats = Cgats::from_file("test_files/cblin_i1.txt").unwrap();
let cb = cgats.to_colorburst_by_value().unwrap();
eprintln!("{cb:0.4}");
assert_eq!(cb.get_row(20).unwrap().next().unwrap(), &1.23);
let cgats = Cgats::from_file("test_files/colorburst4.txt").unwrap();
let cb = cgats.to_colorburst().unwrap();
eprintln!("{cb:0.4}");
assert_eq!(cb.get_row(20).unwrap().next().unwrap(), &1.06);
}
const COLORBURST_INPUT: [f32; 21] = [
0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0,
55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 100.0,
];
const COLORBURST_COLOR_TYPES: &[ColorType] = &[
ColorType::Rgb,
ColorType::Cmyk,
ColorType::FiveClr,
ColorType::SixClr,
ColorType::SevenClr,
ColorType::EightClr,
ColorType::NClr,
];
impl Default for Cgats {
fn default() -> Self {
Self::new()
}
}
impl Index<usize> for Cgats {
type Output = DataPoint;
fn index(&self, index: usize) -> &Self::Output {
&self.data[index]
}
}
impl IndexMut<usize> for Cgats {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.data[index]
}
}
impl Index<(usize, usize)> for Cgats {
type Output = DataPoint;
fn index(&self, index: (usize, usize)) -> &Self::Output {
let (col, row) = index;
let index = row * self.n_cols() + col;
&self[index]
}
}
impl IndexMut<(usize, usize)> for Cgats {
fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output {
let (col, row) = index;
let index = row * self.n_cols() + col;
&mut self[index]
}
}
impl CgatsFmt for Cgats {
fn cgats_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use std::fmt::Write;
self.vendor.cgats_fmt(f)?;
for meta in self.metadata.iter() {
meta.cgats_fmt(f)?;
}
if self.vendor != Vendor::ColorBurst {
self.data_format.cgats_fmt(f)?;
}
writeln!(f, "BEGIN_DATA")?;
for row in self.rows() {
let result = &mut String::new();
for dp in row {
match f.precision() {
Some(p) => write!(result, "{dp:.p$}")?,
None => write!(result, "{dp}")?,
}
result.push('\t');
}
result.pop();
writeln!(f, "{}", result)?;
}
writeln!(f, "END_DATA")
}
}
impl fmt::Display for Cgats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.cgats_fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataFormat {
fields: Vec<Field>,
}
use Field::*;
impl DataFormat {
pub fn new() -> Self {
DataFormat { fields: Vec::new() }
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn fields(&self) -> impl Iterator<Item=&Field> {
self.fields.iter()
}
pub fn fields_mut(&mut self) -> impl Iterator<Item=&mut Field> {
self.fields.iter_mut()
}
pub fn index_by_field(&self, field: &Field) -> Option<usize> {
self.fields.iter().position(|f| f == field)
}
pub fn iter(&self) -> impl Iterator<Item=&Field> {
self.fields.iter()
}
pub fn colorburst() -> Self {
DataFormat {
fields: vec![D_RED, D_GREEN, D_BLUE, D_VIS, LAB_L, LAB_A, LAB_B],
}
}
pub fn contains(&self, field: &Field) -> bool {
self.fields.contains(field)
}
}
impl Default for DataFormat {
fn default() -> Self {
Self::new()
}
}
impl FromIterator<Field> for DataFormat {
fn from_iter<I: IntoIterator<Item=Field>>(iter: I) -> Self {
DataFormat {
fields: iter.into_iter().collect(),
}
}
}
pub trait CgatsFmt {
fn cgats_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result;
}
impl CgatsFmt for DataFormat {
fn cgats_fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "BEGIN_DATA_FORMAT\n{}\nEND_DATA_FORMAT", self.fields.iter().join("\t"))
}
}
trait Join: Iterator {
fn join(&mut self, sep: &str) -> String
where
Self::Item: std::fmt::Display,
{
use std::fmt::Write;
match self.next() {
None => String::new(),
Some(first) => {
let (lower, _) = self.size_hint();
let mut result = String::with_capacity(sep.len() * lower);
write!(&mut result, "{}", first).expect("unable to join");
for elt in self {
result.push_str(sep);
write!(&mut result, "{}", elt).expect("unable to join");
}
result
}
}
}
}
impl<T, D> Join for T
where
T: Iterator<Item=D>,
D: std::fmt::Display,
{}
#[test]
fn row_col() {
use Field::*;
let cgats = Cgats {
vendor: Vendor::Cgats,
metadata: Vec::new(),
data_format: DataFormat {
fields: vec![
RGB_R, RGB_B, RGB_B,
],
},
data: vec![
Float(0.0), Float(1.0), Float(2.0),
Float(3.0), Float(4.0), Float(5.0),
Float(6.0), Float(7.0), Float(8.0),
],
};
assert_eq!(
cgats.get_row(1).unwrap().collect::<Vec<_>>(),
[&Float(3.0), &Float(4.0), &Float(5.0)],
);
assert_eq!(
cgats.get_col(1).collect::<Vec<_>>(),
[&Float(1.0), &Float(4.0), &Float(7.0)],
);
eprintln!("{}", cgats);
}
#[test]
fn format() -> Result<()> {
let cgats = Cgats::from_file("test_files/curve0.txt")?;
eprintln!("{}", cgats);
Ok(())
}
#[test]
fn reindex() -> Result<()> {
let mut cgats: Cgats =
"CGATS.17
BEGIN_DATA_FORMAT
SAMPLE_ID RGB_R RGB_G RGB_B
END_DATA_FORMAT
BEGIN_DATA
1 0 0 0
2 128 128 128
3 255 255 255
END_DATA"
.parse().unwrap();
assert_eq!(cgats.get_col(0).collect::<Vec<_>>(), vec![&1, &2, &3]);
assert_eq!(cgats.index_by_field(&SAMPLE_ID), Some(0));
cgats.reindex_sample_id();
assert_eq!(cgats.get_col(0).collect::<Vec<_>>(), vec![&0, &1, &2]);
assert_eq!(cgats.index_by_field(&SAMPLE_ID), Some(0));
cgats.reindex_sample_id_at(5);
assert_eq!(cgats.get_col(0).collect::<Vec<_>>(), vec![&5, &6, &7]);
assert_eq!(cgats.index_by_field(&SAMPLE_ID), Some(0));
let mut cgats: Cgats =
"CGATS.17
BEGIN_DATA_FORMAT
RGB_R RGB_G RGB_B
END_DATA_FORMAT
BEGIN_DATA
0 0 0
128 128 128
255 255 255
END_DATA"
.parse().unwrap();
assert_eq!(cgats.get_col(0).collect::<Vec<_>>(), vec![&0, &128, &255]);
assert_eq!(cgats.index_by_field(&SAMPLE_ID), None);
cgats.reindex_sample_id();
assert_eq!(cgats.get_col(0).collect::<Vec<_>>(), vec![&0, &1, &2]);
assert_eq!(cgats.index_by_field(&SAMPLE_ID), Some(0));
Ok(())
}
#[test]
fn insert_row() -> Result<()> {
let mut cgats: Cgats =
"CGATS.17
BEGIN_DATA_FORMAT
SAMPLE_ID RGB_R RGB_G RGB_B
END_DATA_FORMAT
BEGIN_DATA
1 0 0 0
2 128 128 128
3 255 255 255
END_DATA"
.parse().unwrap();
assert_eq!(cgats.get_row(2).unwrap().collect::<Vec<_>>(), vec![&3, &255, &255, &255]);
let row = vec![
DataPoint::Int(0), DataPoint::Int(190), DataPoint::Int(191), DataPoint::Int(192)
];
cgats.insert_row(0, row.into_iter())?;
eprintln!("{}", cgats);
assert_eq!(cgats.get_row(0).unwrap().collect::<Vec<_>>(), vec![&0, &190, &191, &192]);
let row = vec![
DataPoint::Int(5), DataPoint::Int(67), DataPoint::Int(68), DataPoint::Int(69)
];
cgats.push_row(row.clone().into_iter())?;
eprintln!("{}", cgats);
assert_eq!(cgats.get_row(4).unwrap().collect::<Vec<_>>(), vec![&5, &67, &68, &69]);
cgats.reindex_sample_id();
eprintln!("{}", cgats);
let mut cgats: Cgats =
"CGATS.17
BEGIN_DATA_FORMAT
SAMPLE_ID RGB_R RGB_G RGB_B
END_DATA_FORMAT
BEGIN_DATA
END_DATA"
.parse().unwrap();
dbg!(cgats.n_rows(), cgats.n_cols());
eprintln!("{}", cgats);
cgats.push_row(row.into_iter())?;
eprintln!("{}", cgats);
assert_eq!(cgats.get_row(0).unwrap().collect::<Vec<_>>(), vec![&5, &67, &68, &69]);
Ok(())
}