cgats 0.2.0

Parse, transform, and write CGATS color files
Documentation
#![warn(missing_docs)]
#![allow(clippy::tabs_in_doc_comments)]
//! This crate contains utilities for parsing and creating CGATS color data files.

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;

/// A generic heap allocated error
pub type BoxErr = Box<dyn std::error::Error>;
/// A generic Result with a [`BoxErr`]
pub type Result<T> = std::result::Result<T, BoxErr>;

pub use DataPoint::*;
/// The building block of CGATS data
#[derive(Debug, Clone)]
pub enum DataPoint {
    /// String type
    Alpha(String),
    /// Float type
    Float(f32),
    /// Integer type
    Int(i32),
}

impl DataPoint {
    /// Create a new string type
    pub fn new_alpha<S: ToString>(alpha: S) -> Self {
        Alpha(alpha.to_string())
    }

    /// Create a new float type
    pub fn new_float<F: Into<f32>>(float: F) -> Self {
        Float(float.into())
    }

    /// Create a new integer type
    pub fn new_int<I: Into<i32>>(int: I) -> Self {
        Int(int.into())
    }

    /// Convert to integer type
    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()),
        })
    }

    /// Convert to float type
    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),
        })
    }

    /// Extract the float value. Panics if the type cannot be converted to a Float.
    pub fn to_float_unchecked(&self) -> f32 {
        match self.to_float() {
            Ok(Float(f)) => f,
            _ => panic!("unchecked conversion to float failed"),
        }
    }

    /// Convert to string type
    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")
            }
        }
    }
}

/// CGATS header data
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetaData {
    /// A Key:Value pair
    KeyVal {
        /// The key associated with the value
        key: String,
        /// The value associated with the key
        val: String,
    },
    /// A comment line
    Comment(String),
    /// A blank line
    Blank,
}

impl MetaData {
    /// Return the key, if it has one
    pub fn key(&self) -> Option<&str> {
        if let MetaData::KeyVal{ key, .. } = self {
            Some(key)
        } else {
            None
        }
    }

    /// Return the value, if it has one
    pub fn value(&self) -> Option<&str> {
        Some(match self {
            MetaData::KeyVal { val, .. } => val,
            MetaData::Comment(val) => val,
            MetaData::Blank => return None,
        })
    }

    /// Return a mutable reference to the value, if it has one
    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");
}

/// The CGATS data
#[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 {
    /// Creates a new empty [`Cgats`] object
    pub fn new() -> Self {
        Cgats {
            vendor: Vendor::Cgats,
            metadata: Vec::new(),
            data_format: DataFormat::new(),
            data: Vec::new()
        }
    }

    /// Creates a new empty [`Cgats`] object with the specified capacity
    pub fn with_capacity(cap: usize) -> Self {
        Cgats {
            data: Vec::with_capacity(cap),
            ..Self::new()
        }
    }

    /// Returns a summary string:
    ///
    /// [`Vendor`][[`ColorType`]s; `n`]
    ///
    /// ```
    /// use cgats::Cgats;
    ///
    /// let 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.summary(), "Cgats[Rgb; 3]");
    /// ```
    pub fn summary(&self) -> String {
        format!(
            "{}[{}; {}]",
            self.vendor(),
            self.color_types().iter().join(", "),
            self.n_rows()
        )
    }

    /// Returns a reference the [`Vendor`]
    pub fn vendor(&self) -> &Vendor {
        &self.vendor
    }

    /// Find the [`MetaData`] value by key
    /// ```
    /// use cgats::Cgats;
    ///
    /// let cgats: Cgats =
    /// "CGATS.17
    /// NUMBER_OF_FIELDS 4
    /// BEGIN_DATA_FORMAT
    /// SAMPLE_ID	RGB_R	RGB_G	RGB_B
    /// END_DATA_FORMAT
    /// NUMBER_OF_SETS 3
    /// BEGIN_DATA
    /// 1	0	0	0
    /// 2	128	128	128
    /// 3	255	255	255
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// assert_eq!(cgats.get_metadata("NUMBER_OF_FIELDS"), Some("4"));
    /// assert_eq!(cgats.get_metadata("NUMBER_OF_SETS"), Some("3"));
    /// ```
    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)
    }

    /// Returns a mutable reference to a metadata value by key
    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)
    }

    /// Insert a MetaData Key:Value, overwriting if it exists
    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()});
        }
    }

    /// Iterator over the [`DataPoint`]s in a [`Cgats`] object
    pub fn iter(&self) -> impl Iterator<Item=&DataPoint> {
        self.data.iter()
    }

    /// Iterator over the [`DataPoint`]s with their corresponding `Field` keys
    ///
    /// ```
    /// use cgats::{Cgats, Field::*, DataPoint::*};
    ///
    /// let cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// SAMPLE_ID	DE2000
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 1	1.23
    /// 2	3.21
    /// END_DATA"
    /// .parse().unwrap();
    ///
    ///let mut iter = cgats.iter_with_fields();
    ///
    /// assert_eq!(iter.next(), Some((&SAMPLE_ID, &Int(1))));
    /// assert_eq!(iter.next(), Some((&DE_2000, &Float(1.23))));
    /// assert_eq!(iter.next(), Some((&SAMPLE_ID, &Int(2))));
    /// assert_eq!(iter.next(), Some((&DE_2000, &Float(3.21))));
    /// assert_eq!(iter.next(), None);
    /// ```
    pub fn iter_with_fields(&self) -> impl Iterator<Item=(&Field, &DataPoint)> {
        self.data_format
            .fields
            .iter()
            .cycle()
            .zip(self.iter())
    }

    /// Mutable iterator over the [`DataPoint`]s with their corresponding `Field` keys.
    /// [`Field`] values are cloned.
    ///
    /// ```
    /// use cgats::{Cgats, Field::*, DataPoint::*};
    ///
    /// let mut cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// SAMPLE_ID	DE2000
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 1	1.23
    /// 2	3.21
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// {
    ///     let mut iter = cgats.iter_mut_with_fields();
    ///    
    ///     assert_eq!(iter.next(), Some((SAMPLE_ID, &mut Int(1))));
    ///     assert_eq!(iter.next(), Some((DE_2000, &mut Float(1.23))));
    ///     assert_eq!(iter.next(), Some((SAMPLE_ID, &mut Int(2))));
    ///    
    ///     if let Some((_field, data_point)) = iter.next() {
    ///         *data_point = Float(4.56);
    ///     }
    ///    
    ///     assert_eq!(iter.next(), None);
    /// }
    ///
    /// assert_eq!(cgats[3], Float(4.56));
    /// ```
    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())
    }

    /// Iterator over the fields of the [`DataFormat`]
    pub fn fields(&self) -> impl Iterator<Item=&Field> {
        self.data_format.fields()
    }

    /// Mutable iterator over the fields of the [`DataFormat`]
    pub fn fields_mut(&mut self) -> impl Iterator<Item=&mut Field> {
        self.data_format.fields_mut()
    }

    /// Mutable iterator over the [`DataPoint`]s in a [`Cgats`] object
    pub fn iter_mut(&mut self) -> impl Iterator<Item=&mut DataPoint> {
        self.data.iter_mut()
    }

    /// Read a file into a [`Cgats`] object
    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()
    }

    /// Write a [`Cgats`] object to a file
    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let buf = &mut BufWriter::new(File::create(path)?);
        self.write(buf)
    }

    /// Write a [`Cgats`] object to a [`Write`] object
    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
        Ok(write!(writer, "{}", self)?)
    }

    /// Returns the total number of data points in the set (rows x cols)
    pub fn len(&self) -> usize {
        self.data.len()
    }

    /// Returns true if `self.len() == 0`
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Returns the total number of rows (samples) in the set
    pub fn n_rows(&self) -> usize {
        self.data.len().checked_div(self.n_cols()).unwrap_or(0)
    }

    /// Returns the total number of columns (fields) in the set
    pub fn n_cols(&self) -> usize {
        self.data_format.len()
    }

    /// Iterator over the data points in a given sample row
    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())
    }

    /// Returns the first row containing the given values
    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)?)
    }

    /// Returns a row index to the first row containing the given 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
    }

    /// Remove a row from a sample set by row index and return the row. Returns none if the row
    /// index is greater than or equal to the number of rows.
    /// ```
    /// use cgats::Cgats;
    ///
    /// let mut cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// SAMPLE_ID	DE2000
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 1	1.23
    /// 2	3.21
    /// 3	4.56
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// {
    ///     let mut row = cgats.remove_row(1).unwrap();
    ///     assert_eq!(row.next().unwrap(), 2);
    ///     assert_eq!(row.next().unwrap(), 3.21);
    ///     assert_eq!(row.next(), None);
    /// }
    ///
    /// {
    ///     let mut row = cgats.remove_row(1).unwrap();
    ///     assert_eq!(row.next().unwrap(), 3);
    ///     assert_eq!(row.next().unwrap(), 4.56);
    ///     assert_eq!(row.next(), None);
    /// }
    ///
    /// {
    ///     assert!(cgats.remove_row(1).is_none());
    /// }
    /// ```
    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))
        }
    }

    /// Move a row from one row index to another. Returns an error if the indices are out of range
    /// ```
    /// use cgats::Cgats;
    ///
    /// let mut cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// SAMPLE_ID	DE2000
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 1	1.23
    /// 2	3.21
    /// 3	4.56
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// cgats.move_row(2, 1).unwrap();
    ///
    /// {
    ///     let mut last_row = cgats.get_row(2).unwrap();
    ///     assert_eq!(last_row.next().unwrap(), &2);
    ///     assert_eq!(last_row.next().unwrap(), &3.21);
    ///     assert_eq!(last_row.next(), None);
    /// }
    ///
    /// cgats.move_row(0, 2).unwrap();
    ///
    /// {
    ///     let mut last_row = cgats.get_row(2).unwrap();
    ///     assert_eq!(last_row.next().unwrap(), &1);
    ///     assert_eq!(last_row.next().unwrap(), &1.23);
    ///     assert_eq!(last_row.next(), None);
    /// }
    /// ```
    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())
        }
    }

    /// Re-order rows to transpose for chart layouts with a given current chart width (`LGOROWLENGTH`)
    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);
    }

        /// Iterator of mutable references to data points in a given sample row
    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)
    }

    /// Iterator over the rows of [`DataPoint`]s
    pub fn rows(&self) -> impl Iterator<Item=impl Iterator<Item=&DataPoint>> {
        (0..self.n_rows()).into_iter()
            .filter_map(|row| self.get_row(row))
    }

    /// Returns an iterator over the data points in a given column (field)
    pub fn get_col(&self, col: usize) -> impl Iterator<Item=&DataPoint> {
        self.iter().skip(col).step_by(self.n_cols())
    }

    /// Find a column by it's [`Field`]. Returns [`None`] if the [`Field`] does not exist.
    pub fn get_col_by_field(&self, field: &Field) -> Option<impl Iterator<Item = &DataPoint>> {
        Some(self.get_col(self.index_by_field(field)?))
    }

    /// Iterator of mutable references to data points in a given column (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)
    }

    /// Find a mutable column by it's [`Field`]. Returns [`None`] if the [`Field`] does not exist.
    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)?))
    }

    /// Iterator over the columns of [`DataPoint`]s
    pub fn cols(&self) -> impl Iterator<Item=impl Iterator<Item=&DataPoint>> {
        (0..self.n_cols()).into_iter()
            .map(|col| self.get_col(col))
    }

    /// Iterator over columns of [`DataPoint`]s with their corresponding [`Field`]s
    pub fn cols_with_fields(&self) -> impl Iterator<Item=(&Field, impl Iterator<Item=&DataPoint>)> {
        self.data_format
            .fields
            .iter()
            .zip(self.cols())
    }

    /// Returns a list of [`ColorType`]s based on the contents of the `DATA_FORMAT`
    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,
            })
        }
    }

    /// Determines if the data contains a [`ColorType`]
    pub fn has_color_type(&self, color_type: &ColorType) -> bool {
        self.color_types().contains(color_type)
    }

    /// Re-number the `SAMPLE_ID` field starting from 0
    pub fn reindex_sample_id(&mut self) {
        self.reindex_sample_id_at(0)
    }

    /// Re-number the `SAMPLE_ID` field starting from a given integer
    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")
        }
    }

    /// Returns the position of a column with a given [`Field`]
    ///
    /// Returns [`None`] if the [`DataFormat`] does not contain the [`Field`]
    pub fn index_by_field(&self, field: &Field) -> Option<usize> {
        self.data_format.index_by_field(field)
    }

    /// Insert a column of [`DataPoint`]s
    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())
        }
    }

    /// Append a column of [`DataPoint`]s
    pub fn push_column(&mut self, field: Field, iter: impl Iterator<Item=DataPoint>) -> Result<()> {
        self.insert_column(self.n_cols(), field, iter)
    }

    /// Insert a row of [`DataPoint`]s at a row index
    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(())
        }
    }

    /// Append a row of [`DataPoint`]s
    pub fn push_row(&mut self, iter: impl Iterator<Item=DataPoint>) -> Result<()> {
        self.insert_row(self.n_rows(), iter)
    }

    /// Append the rows from another [`Cgats`]. Returns an error if the [`DataFormat`]s do not
    /// match.
    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(())
    }

    /// Concatenate the rows from multiple [`Cgats`]
    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)
    }

    /// Convert the CGATS to a ColorBurst linearization format.
    pub fn to_colorburst(&self) -> Result<Self> {
        self.to_colorburst_by_value()
            .or_else(|_|self.to_colorburst_in_order())
    }

    /// Convert the CGATS to a ColorBurst linearization format.
    /// Assumes the patches are in the correct 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)
    }

    /// Convert the CGATS to a ColorBurst linearization format.
    /// Reorders patches by value.
    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()
    }

    /// Convert ColorBurst linearization format to a conventional CGATS format
    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;
    /// Index into [`Cgats`] by [`usize`]
    /// ```
    /// use cgats::Cgats;
    ///
    /// let mut cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// RGB_R	RGB_G	RGB_B
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 0	1	2
    /// 126	127	128
    /// 253	254	255
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// assert_eq!(cgats[0], 0);
    /// assert_eq!(cgats[5], 128);
    /// assert_eq!(cgats[8], 255);
    /// ```
    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;
    /// Index into [`Cgats`] by (column, row)
    ///
    /// ```
    /// use cgats::Cgats;
    ///
    /// let mut cgats: Cgats =
    /// "CGATS.17
    /// BEGIN_DATA_FORMAT
    /// RGB_R	RGB_G	RGB_B
    /// END_DATA_FORMAT
    /// BEGIN_DATA
    /// 0	1	2
    /// 126	127	128
    /// 253	254	255
    /// END_DATA"
    /// .parse().unwrap();
    ///
    /// assert_eq!(cgats[(0, 0)], 0);
    /// assert_eq!(cgats[(2, 1)], 128);
    /// assert_eq!(cgats[(2, 2)], 255);
    /// ```
    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)
    }
}

/// Representation of the DATA_FORMAT fields
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataFormat {
    fields: Vec<Field>,
}

use Field::*;
impl DataFormat {
    /// Creates a new DATA_FORMAT
    pub fn new() -> Self {
        DataFormat { fields: Vec::new() }
    }

    /// Returns the number of fields in the DATA_FORMAT
    pub fn len(&self) -> usize {
        self.fields.len()
    }

    /// Returns true if self.len() == 0
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Iterator over the [`Field`]s in a [`DataFormat`]
    pub fn fields(&self) -> impl Iterator<Item=&Field> {
        self.fields.iter()
    }

    /// Mutable iterator over the [`Field`]s in a [`DataFormat`]
    pub fn fields_mut(&mut self) -> impl Iterator<Item=&mut Field> {
        self.fields.iter_mut()
    }

    /// Returns column index of a given [`Field`].
    /// Returns [`None`] if the [`DataFormat`] does not contain the [`Field`].
    pub fn index_by_field(&self, field: &Field) -> Option<usize> {
        self.fields.iter().position(|f| f == field)
    }

    /// Returns an iterator over the fields of a DATA_FORMAT
    pub fn iter(&self) -> impl Iterator<Item=&Field> {
        self.fields.iter()
    }

    /// Implied ColorBurst [`DataFormat`]
    pub fn colorburst() -> Self {
        DataFormat {
            fields: vec![D_RED, D_GREEN, D_BLUE, D_VIS, LAB_L, LAB_A, LAB_B],
        }
    }

    /// Check if the [`DataFormat`] contains a [`Field`]
    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(),
        }
    }
}

/// Trait to format data for writing to CGATS files
pub trait CgatsFmt {
    /// Format data to a [`fmt::Formatter`]
    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 to join strings with another string in between
trait Join: Iterator {
    /// Join an iterator of strings with another string in between
    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(())
}