diff --git a/README.md b/README.md index 1d8db18..405338f 100644 --- a/README.md +++ b/README.md @@ -5,32 +5,38 @@ CSV processing library inspired by [csvsc](https://crates.io/crates/csvsc) [![Crates.io](https://img.shields.io/crates/v/csv-pipeline.svg)](https://crates.io/crates/csv-pipeline) [![Documentation](https://docs.rs/csv-pipeline/badge.svg)](https://docs.rs/csv-pipeline) -## Basic Example +## Example + ```rs use csv_pipeline::{Pipeline, Transformer}; -// First create a pipeline from a CSV file path -let csv = Pipeline::from_path("test/Countries.csv") +let source = "\ + Person,Score\n\ + A,1\n\ + A,8\n\ + B,3\n\ + B,4\n"; +let reader = csv::Reader::from_reader(source.as_bytes()); +let csv = Pipeline::from_reader(reader) .unwrap() - // Add a column with values computed from a closure - .add_col("Language", |headers, row| { - match headers.get_field(row, "Country") { - Some("Norway") => Ok("Norwegian".into()), - _ => Ok("Unknown".into()), - } + .map(|_headers, row| Ok(row)) + // Transform into a new csv + .transform_into(|| { + vec![ + // Keep every Person + Transformer::new("Person").keep_unique(), + // Sum the scores into a "Total score" column + Transformer::new("Total score").from_col("Score").sum(0), + ] }) - // Make the "Country" column uppercase - .rename_col("Country", "COUNTRY") - .map_col("COUNTRY", |id_str| Ok(id_str.to_uppercase())) - // Collect the csv into a string .collect_into_string() .unwrap(); assert_eq!( csv, - "ID,COUNTRY,Language\n\ - 1,NORWAY,Norwegian\n\ - 2,TUVALU,Unknown\n" + "Person,Total score\n\ + A,9\n\ + B,7\n" ); ``` diff --git a/src/lib.rs b/src/lib.rs index d952378..88e6e49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,13 +63,7 @@ //! // Keep every Person //! Transformer::new("Person").keep_unique(), //! // Sum the scores into a "Total score" column -//! Transformer::new("Total score").from_col("Score").reduce( -//! |accumulator, current| { -//! let score: u64 = current.parse().unwrap(); -//! Ok(accumulator + score) -//! }, -//! 0, -//! ), +//! Transformer::new("Total score").from_col("Score").sum(0), //! ] //! }) //! .collect_into_string() diff --git a/src/pipeline.rs b/src/pipeline.rs index 2db042b..ea760d4 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -136,7 +136,7 @@ impl<'a> Pipeline<'a> { /// ## Example /// /// ``` - /// use csv_pipeline::{Pipeline, StringTarget}; + /// use csv_pipeline::Pipeline; /// /// let csv = Pipeline::from_path("test/AB.csv") /// .unwrap() @@ -159,7 +159,7 @@ impl<'a> Pipeline<'a> { /// ## Example /// /// ``` - /// use csv_pipeline::{Pipeline, StringTarget}; + /// use csv_pipeline::Pipeline; /// /// let csv = Pipeline::from_path("test/AB.csv") /// .unwrap() @@ -195,7 +195,7 @@ impl<'a> Pipeline<'a> { /// ## Example /// /// ``` - /// use csv_pipeline::{Pipeline, StringTarget}; + /// use csv_pipeline::Pipeline; /// /// let csv = Pipeline::from_path("test/AB.csv") /// .unwrap() @@ -258,12 +258,12 @@ impl<'a> Pipeline<'a> { /// ## Example /// /// ``` - /// use csv_pipeline::{Pipeline, StringTarget}; + /// use csv_pipeline::{Pipeline, Target}; /// /// let mut csv = String::new(); /// Pipeline::from_path("test/AB.csv") /// .unwrap() - /// .flush(StringTarget::new(&mut csv)) + /// .flush(Target::string(&mut csv)) /// .run() /// .unwrap(); /// diff --git a/src/transform.rs b/src/transform.rs index 1855c2b..6734611 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -2,6 +2,8 @@ use crate::{Error, Headers, Row}; use core::fmt::Display; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; +use std::ops::AddAssign; +use std::str::FromStr; /// For grouping and reducing rows. pub trait Transform { @@ -50,13 +52,24 @@ impl Transformer { value: "".to_string(), }) } + /// Sum the values in this column + pub fn sum<'a, N>(self, init: N) -> Box + where + N: Display + AddAssign + FromStr + Clone + 'a, + { + Box::new(Sum { + name: self.name, + from_col: self.from_col, + value: init, + }) + } /// Reduce the values from this column into a single value using a closure pub fn reduce<'a, R, V>(self, reduce: R, init: V) -> Box where R: FnMut(V, &str) -> Result + 'a, V: Display + Clone + 'a, { - Box::new(Closure { + Box::new(Reduce { name: self.name, from_col: self.from_col, reduce, @@ -65,35 +78,6 @@ impl Transformer { } } -struct Closure { - name: String, - from_col: String, - reduce: F, - value: V, -} -impl Transform for Closure -where - F: FnMut(V, &str) -> Result, - V: Display + Clone, -{ - fn add_row(&mut self, headers: &Headers, row: &Row) -> Result<(), Error> { - let field = headers - .get_field(row, &self.from_col) - .ok_or(Error::MissingColumn(self.from_col.clone()))? - .to_string(); - self.value = (self.reduce)(self.value.clone(), &field)?; - Ok(()) - } - - fn value(&self) -> String { - self.value.to_string() - } - - fn name(&self) -> String { - self.name.clone() - } -} - struct KeepUnique { name: String, from_col: String, @@ -139,3 +123,62 @@ pub(crate) fn compute_hash<'a>( } Ok(hasher.finish()) } + +struct Reduce { + name: String, + from_col: String, + reduce: F, + value: V, +} +impl Transform for Reduce +where + F: FnMut(V, &str) -> Result, + V: Display + Clone, +{ + fn add_row(&mut self, headers: &Headers, row: &Row) -> Result<(), Error> { + let field = headers + .get_field(row, &self.from_col) + .ok_or(Error::MissingColumn(self.from_col.clone()))? + .to_string(); + self.value = (self.reduce)(self.value.clone(), &field)?; + Ok(()) + } + + fn value(&self) -> String { + self.value.to_string() + } + + fn name(&self) -> String { + self.name.clone() + } +} + +struct Sum { + name: String, + from_col: String, + value: N, +} +impl Transform for Sum +where + V: Display + AddAssign + FromStr + Clone, +{ + fn add_row(&mut self, headers: &Headers, row: &Row) -> Result<(), Error> { + let field = headers + .get_field(row, &self.from_col) + .ok_or(Error::MissingColumn(self.from_col.clone()))? + .to_string(); + let new: V = match field.parse() { + Ok(v) => v, + Err(_) => return Err(Error::InvalidField(field)), + }; + self.value += new; + Ok(()) + } + + fn value(&self) -> String { + self.value.to_string() + } + fn name(&self) -> String { + self.name.clone() + } +}