Repository: inosion/madato (language: python)

Issue:
Support CSV
Support converting from CSV, and potentially to CSV. It might be the most common format to convert to markdown.

Currently you get a message ```Error: "CSV not implemented"```, so I guess this was a planned feature at least at some point.


Discussion / hints:
Acknowledged @ninpnin - I will see if I can add that feature set this coming weekend. 

Fix patch:
diff --git a/madato/Cargo.toml b/madato/Cargo.toml
index ca297f3..f2e8037 100644
--- a/madato/Cargo.toml
+++ b/madato/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "madato"
-version = "0.5.4"
-authors = ["Ramon Buckland <ramon.buckland@inosion.com>"]
+version = "0.6.0"
+authors = ["Ramon Buckland <ramon@thebuckland.com>"]
 license = "MIT/Apache-2.0"
 description = "A library and command line tool for working tabular data (XLS, ODS, CSV, YAML), and Markdown"
 readme  = "README.md"
 repository = "https://github.com/inosion/madato"
-keywords = ["markdown","excel","yaml","tables","openoffice"]
+keywords = ["markdown","excel","yaml","tables","openoffice","csv"]
+edition = "2021"
 exclude = [
     "doc/*",
     "assets/*",
@@ -23,7 +24,11 @@ serde_derive    = "1.0"
 serde_yaml      = "0.8"
 serde_json      = "1.0"
 indexmap        = "1.0.1"
+csv = "1.3.0"
 
 [dependencies.linked-hash-map]
 version = "0.5.1"
 features = ["serde_impl"]
+
+[dev-dependencies]
+tempfile = "3.10.1"
diff --git a/madato/src/csv.rs b/madato/src/csv.rs
new file mode 100644
index 0000000..e4f6672
--- /dev/null
+++ b/madato/src/csv.rs
@@ -0,0 +1,143 @@
+use super::mk_table;
+use crate::types::*;
+
+use linked_hash_map::LinkedHashMap;
+use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+
+fn load_csv(csv: &str) -> Result<Table<String, String>, Box<dyn Error>> {
+    let mut rdr = csv::Reader::from_reader(csv.as_bytes());
+    let headers = rdr.headers()?.clone();
+    let mut table: Table<String, String> = Vec::new();
+
+    for result in rdr.records() {
+        let record = result?;
+        let mut row: TableRow<String, String> = LinkedHashMap::new();
+
+        for (header, field) in headers.iter().zip(record.iter()) {
+            row.insert(header.into(), field.into());
+        }
+
+        table.push(row);
+    }
+
+    Ok(table)
+}
+
+pub fn mk_md_table_from_csv(csv: &str, render_options: &Option<RenderOptions>) -> String {
+    mk_table(
+        &load_csv(csv).expect("Failed to load the CSV"),
+        render_options,
+    )
+}
+
+/// Given results of tables, throw them back out as csv
+pub fn mk_csv_from_table_result(
+    tables: Vec<Result<NamedTable<String, String>, ErroredTable>>,
+) -> String {
+    let table_map: LinkedHashMap<String, Table<String, String>> =
+        tables.into_iter().filter_map(Result::ok).collect();
+
+    // if we only have one table, strip off the key (get just the value)
+    if table_map.len() == 1 {
+        let (_, table) = table_map.into_iter().next().unwrap();
+        let mut wtr = csv::Writer::from_writer(vec![]);
+        if let Some(first_row) = table.iter().next() {
+            wtr.write_record(first_row.keys())
+                .expect("Failed to serialize headers");
+        }
+        for row in table {
+            wtr.write_record(row.values())
+                .expect("Failed to serialize row");
+        }
+        String::from_utf8(wtr.into_inner().expect("Failed to write CSV"))
+            .expect("Failed to convert CSV to string")
+    } else {
+        let mut csvs = Vec::new();
+        for (_, table) in table_map {
+            let mut wtr = csv::Writer::from_writer(vec![]);
+            if let Some(first_row) = table.iter().next() {
+                wtr.write_record(first_row.keys())
+                    .expect("Failed to serialize headers");
+            }
+            for row in table {
+                wtr.write_record(row.values())
+                    .expect("Failed to serialize row");
+            }
+            csvs.push(
+                String::from_utf8(wtr.into_inner().expect("Failed to write CSV"))
+                    .expect("Failed to convert CSV to string"),
+            );
+        }
+        csvs.join("\n")
+    }
+}
+
+pub fn csv_file_to_md(
+    filename: String,
+    render_options: &Option<RenderOptions>,
+) -> Result<String, String> {
+    let mut file = File::open(filename).expect("Unable to open the file");
+    let mut contents = String::new();
+    file.read_to_string(&mut contents)
+        .expect("Unable to read the file");
+
+    Ok(mk_md_table_from_csv(&contents, render_options))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_load_csv() {
+        let csv = "header1,header2\nvalue1,value2";
+        let table = load_csv(csv).unwrap();
+        assert_eq!(table.len(), 1);
+        let row = &table[0];
+        assert_eq!(row.get("header1"), Some(&"value1".to_string()));
+        assert_eq!(row.get("header2"), Some(&"value2".to_string()));
+    }
+
+    #[test]
+    fn test_mk_md_table_from_csv() {
+        let csv = "header1,header2\nvalue1,value2";
+        let md_table = mk_md_table_from_csv(csv, &None);
+        assert!(md_table.contains("|header1|header2|"));
+        assert!(md_table.contains("|value1 |value2 |"));
+    }
+
+    #[test]
+    fn test_mk_csv_from_table_result() {
+        let table: NamedTable<String, String> = (
+            "table1".to_string(),
+            vec![[
+                ("header1".to_string(), "value1".to_string()),
+                ("header2".to_string(), "value2".to_string()),
+            ]
+            .iter()
+            .cloned()
+            .collect()],
+        );
+        let tables = vec![Ok(table)];
+        let csv = mk_csv_from_table_result(tables);
+        assert!(csv.contains("header1,header2"));
+        assert!(csv.contains("value1,value2"));
+    }
+
+    #[test]
+    fn test_csv_file_to_md() {
+        use std::io::Write;
+        use tempfile::NamedTempFile;
+
+        let csv = "header1,header2\nvalue1,value2";
+        let mut tmp_file = NamedTempFile::new().unwrap();
+        write!(tmp_file, "{}", csv).unwrap();
+
+        let md_table =
+            csv_file_to_md(tmp_file.path().to_str().unwrap().to_string(), &None).unwrap();
+        assert!(md_table.contains("|header1|header2|"));
+        assert!(md_table.contains("|value1 |value2 |"));
+    }
+}
diff --git a/madato/src/lib.rs b/madato/src/lib.rs
index 48151f8..a7784f7 100644
--- a/madato/src/lib.rs
+++ b/madato/src/lib.rs
@@ -14,6 +14,7 @@ extern crate serde_yaml;
 
 #[macro_use]
 pub mod utils;
+pub mod csv;
 pub mod types;
 pub mod yaml;
 
@@ -152,8 +153,8 @@ pub fn mk_md_data(
         ),
     };
 
-    let ret: Vec<String> =
-        iter.map(|hm| {
+    let ret: Vec<String> = iter
+        .map(|hm| {
             heading_data.iter().fold(String::from("|"), |res, k| {
                 let s = match hm.get(&k.0) {
                     Some(x) => x.to_string(),
@@ -162,7 +163,8 @@ pub fn mk_md_data(
 
                 format!("{}{: ^width$}|", res, s, width = k.1)
             })
-        }).collect::<Vec<String>>();
+        })
+        .collect::<Vec<String>>();
 
     // make a new String of all the concatenated fields
     ret.join("\n")
@@ -186,13 +188,15 @@ fn filter_tablerows(row: &TableRow<String, String>, vfilters: &Vec<KVFilter>) ->
 fn tablerow_filter(row: &TableRow<String, String>, filt: &KVFilter) -> bool {
     row.keys()
         .filter(|k| {
-            filt.key_re.is_match(k) && match row.get(k.clone()) {
-                Some(v) => filt.value_re.is_match(v),
-                None => false,
-            }
+            filt.key_re.is_match(k)
+                && match row.get(*k) {
+                    Some(v) => filt.value_re.is_match(v),
+                    None => false,
+                }
         })
         .collect::<Vec<_>>()
-        .len() > 0
+        .len()
+        > 0
 }
 
 #[test]
diff --git a/madato/src/yaml.rs b/madato/src/yaml.rs
index 0ca4ed9..0549ab9 100644
--- a/madato/src/yaml.rs
+++ b/madato/src/yaml.rs
@@ -1,29 +1,29 @@
 use super::mk_table;
 use linked_hash_map::LinkedHashMap;
 
+use crate::types::*;
 use std::fs::File;
 use std::io::prelude::*;
-use types::*;
 
 #[allow(unused_imports)]
-use utils::StripMargin;
+use crate::utils::StripMargin;
 
 #[test]
 fn can_yaml_to_md() {
     let yml_data = "
     |- data1: somevalue
     |  data2: someother value here
-    |  col3: 100 
+    |  col3: 100
     |  col4: gar gar
     |- data1: that
     |  data2: nice
-    |  col3: 190x 
+    |  col3: 190x
     |- data1: this
     |  data2: someother value here
-    |  col3: 100 
+    |  col3: 100
     |  col4: ta da
     |"
-        .strip_margin();
+    .strip_margin();
 
     // the | below is the margin
     let expected = "
@@ -43,17 +43,17 @@ fn can_yaml_to_md_with_headings() {
     let yml_data = "
     |- data1: somevalue
     |  data2: someother value here
-    |  col3: 100 
+    |  col3: 100
     |  col4: gar gar
     |- data1: that
     |  data2: nice
-    |  col3: 190x 
+    |  col3: 190x
     |- data1: this
     |  data2: someother value here
-    |  col3: 100 
+    |  col3: 100
     |  col4: ta da
     |"
-        .strip_margin();
+    .strip_margin();
     let headings = vec![s!("data1"), s!("data2"), s!("col4")];
 
     // the | below is the margin
@@ -108,7 +108,7 @@ pub fn md_table_yaml_and_headings(headings: &str, yaml: &str) -> String {
 /// - data1: this
 ///   data2: someother value here
 ///   col3: 100
-///   col4: ta da    
+///   col4: ta da
 /// ```
 ///
 /// gives
diff --git a/madato_cal/Cargo.toml b/madato_cal/Cargo.toml
index 714c6b4..c333aba 100644
--- a/madato_cal/Cargo.toml
+++ b/madato_cal/Cargo.toml
@@ -1,13 +1,14 @@
 [package]
 name = "madato_cal"
-version = "0.5.4"
-authors = ["Ramon Buckland <ramon.buckland@inosion.com>"]
+version = "0.6.0"
+authors = ["Ramon Buckland <ramon@thebuckland.com>"]
 license = "MIT/Apache-2.0"
 description = "XLS, ODS Support for the madato lib and command line"
 readme  = "README.md"
 repository = "https://github.com/inosion/madato"
 keywords = ["markdown","excel","yaml","tables","openoffice"]
 include = [ "src/lib.js", "Cargo.toml" ]
+edition = "2021"
 
 [lib]
 crate-type = ["rlib"]
@@ -15,4 +16,4 @@ path = "src/lib.rs"
 
 [dependencies]
 madato          = { path = "../madato/" }
-calamine        = "0.18"
+calamine        = "0.24"
diff --git a/madato_cal/src/lib.rs b/madato_cal/src/lib.rs
index bb49ad1..14f5a41 100644
--- a/madato_cal/src/lib.rs
+++ b/madato_cal/src/lib.rs
@@ -1,10 +1,9 @@
 // #![feature(slice_patterns)]
 
-
 extern crate calamine;
 extern crate madato;
 
-use calamine::{open_workbook_auto, DataType, Reader};
+use calamine::{open_workbook_auto, Data, Reader};
 
 use madato::types::{ErroredTable, NamedTable, RenderOptions, TableRow};
 
@@ -19,7 +18,11 @@ pub fn spreadsheet_to_md(
     let results =
         read_excel_to_named_tables(filename, render_options.clone().and_then(|r| r.sheet_name));
     if results.len() <= 1 {
-        Ok(madato::named_table_to_md(results[0].clone(), false, render_options))
+        Ok(madato::named_table_to_md(
+            results[0].clone(),
+            false,
+            render_options,
+        ))
     } else {
         Ok(results
             .iter()
@@ -55,9 +58,8 @@ pub fn read_excel_to_named_tables(
         .map(|name| {
             let maybe_sheet = workbook.worksheet_range(name);
             match maybe_sheet {
-                None => Err((name.clone(), format!("sheet {} is empty", name))),
-                Some(Err(err)) => Err((name.clone(), format!("{}", err))),
-                Some(Ok(sheet)) => Ok((name.clone(), {
+                Err(err) => Err((name.clone(), format!("{}", err))),
+                Ok(sheet) => Ok((name.clone(), {
                     let first_row: Vec<(usize, String)> = sheet
                         .rows()
                         .next()
@@ -65,7 +67,7 @@ pub fn read_excel_to_named_tables(
                         .iter()
                         .enumerate()
                         .map(|(i, c)| match c {
-                            DataType::Empty => (i, format!("NULL{}", i)),
+                            Data::Empty => (i, format!("NULL{}", i)),
                             _ => (i, c.to_string()),
                         })
                         .collect();
@@ -96,7 +98,7 @@ pub fn list_sheet_names(filename: String) -> Result<Vec<String>, String> {
     Ok(workbook.sheet_names().to_owned())
 }
 
-pub fn md_santise(data: &DataType) -> String {
+pub fn md_santise(data: &Data) -> String {
     data.to_string()
         .replace("|", "\\|")
         .replace("\r\n", "<br/>")
diff --git a/madato_cmd/Cargo.toml b/madato_cmd/Cargo.toml
index f547df7..789324a 100644
--- a/madato_cmd/Cargo.toml
+++ b/madato_cmd/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "madato_cmd"
-version = "0.5.4"
-authors = ["Ramon Buckland <ramon.buckland@inosion.com>"]
+version = "0.6.0"
+authors = ["Ramon Buckland <ramon@thebuckland.com>"]
 license = "MIT/Apache-2.0"
 description = "Command Line Binary for madato"
 readme  = "README.md"
 repository = "https://github.com/inosion/madato"
-keywords = ["markdown","excel","yaml","tables","openoffice", "javascript", "wasm"]
+keywords = ["markdown","excel","yaml","tables","openoffice", "javascript", "wasm","csv"]
+edition = "2021"
 
 [[bin]]
 name = "madato"
diff --git a/madato_cmd/src/main.rs b/madato_cmd/src/main.rs
index 5dcc427..c630c36 100644
--- a/madato_cmd/src/main.rs
+++ b/madato_cmd/src/main.rs
@@ -5,6 +5,7 @@ extern crate madato;
 extern crate madato_cal;
 
 use docopt::Docopt;
+use madato::csv::*;
 use madato::types::*;
 use madato::yaml::*;
 use madato_cal::*;
@@ -12,7 +13,7 @@ use madato_cal::*;
 const USAGE: &str = "
 madato utility - Tabular Data Helper
 
-SpreadSheet <--> YAML <--> JSON <--> Markdown 
+SpreadSheet <--> YAML <--> JSON <--> Markdown
 
 Usage:
   madato table -t <type> [-s <sheetname>] [-o <outputtype>] [-f <filters>...] [-c <column>...] <filename>
@@ -28,7 +29,7 @@ Options:
 
   -t --type <type>              Input Type. XLSX(xls, xlsx, xlsm, xlsb, ods), YAML(table/row structure) or CSV
   -s --sheetname <sheetname>    When a Spreadsheet, restrict to just one of the sheets.
-  -o --outputtype <outputtype>  JSON, MD (Markdown) or YAML. [default: MD]
+  -o --outputtype <outputtype>  JSON, MD (Markdown), CSV or YAML. [default: MD]
   -f --filters <filters>        Filter data in the results based on a simple, key=value
   -c --columns <column>         List of Columns to output 'only'
   -h --help                     Show this screen.
@@ -41,8 +42,10 @@ Quick examples
   madato table -t XLSX -o YAML workbook3.xlsx
   madato table -t YAML -o MD   my_structured_data.yaml
   madato table -t XLSX -o YAML --filters 'Col1=Year.* Col[4-9]=.*' workbook3.xlsx
+  madato table -t XLSX -o CSV test/sample_multi_sheet.xlsx
+  madato table -t CSV -o MD test/potatoes.csv
 
-Filtering Example:
+  Filtering Example:
 
   Basic Filtering support occurs on a row by row basis where the key=value pair need to match.
   Both support a regular expression over the key and or the value.
@@ -58,13 +61,12 @@ Column Limit:
   Limit the Columns that are printed. (note, filtering occurs to ALL columns, before the output limit)
 
   - two colums '-c id -c amount'
-  - multiple columns '-c col1 -c col2 -c col3'  
+  - multiple columns '-c col1 -c col2 -c col3'
   - a column name can appear more than once eg: '-c col2 -c col2 -c col3'
 ";
 
 #[derive(Debug, Deserialize)]
 struct Args {
-    cmd_table: bool,
     cmd_sheetlist: bool,
     arg_filename: String,
 
@@ -86,6 +88,7 @@ enum FileType {
 enum OutputType {
     YAML,
     JSON,
+    CSV,
     MD,
 }
 
@@ -143,17 +146,23 @@ fn main() -> Result<(), String> {
         OutputType::MD => match args.flag_type {
             Some(FileType::YAML) => yaml_file_to_md(args.arg_filename, &render_options),
             Some(FileType::XLSX) => spreadsheet_to_md(args.arg_filename, &render_options),
-            Some(FileType::CSV) => Err(String::from("CSV not implemented")),
-            _ => Err(String::from("No file type specified"))
+            Some(FileType::CSV) => csv_file_to_md(args.arg_filename, &render_options),
+            _ => Err(String::from(
+                "No input file type was specified. Please use -t XLSX, -t YAML or -t CSV",
+            )),
         },
         OutputType::YAML => {
             let tables = read_excel_to_named_tables(args.arg_filename, args.flag_sheetname);
             Ok(mk_yaml_from_table_result(tables))
-        },
+        }
         OutputType::JSON => {
             let tables = read_excel_to_named_tables(args.arg_filename, args.flag_sheetname);
             Ok(mk_json_from_table_result(tables))
         }
+        OutputType::CSV => {
+            let tables = read_excel_to_named_tables(args.arg_filename, args.flag_sheetname);
+            Ok(mk_csv_from_table_result(tables))
+        }
     };
 
     match output_string {


Test patch:
diff --git a/madato_wasm/test/www-sample/package.json b/madato_wasm/test/www-sample/package.json
index a9e64d0..e461408 100644
--- a/madato_wasm/test/www-sample/package.json
+++ b/madato_wasm/test/www-sample/package.json
@@ -4,7 +4,7 @@
   },
   "dependencies": {
     "js-yaml": "^3.12.0",
-    "markdown-it":"^8.4.1"
+    "markdown-it":"^12.3.2"
   },
   "devDependencies": {
     "webpack": "^4.0.1",
diff --git a/test/potatoes.csv b/test/potatoes.csv
new file mode 100644
index 0000000..0b26a08
--- /dev/null
+++ b/test/potatoes.csv
@@ -0,0 +1,21 @@
+"Type","Color","Origin","Size","Texture","Taste","Cooking Method","Season","Storage","Nutrition","Calories","Protein","Carbs","Fat","Fiber"
+"Russet","Brown","USA","Large","Starchy","Mild","Baking","Fall","Cool, Dark Place","High","168","4.6","37.9","0.2","3.8"
+"Yukon Gold","Yellow","Canada","Medium","Waxy","Buttery","Boiling","Spring","Cool, Dark Place","Medium","77","2","17.5","0.1","2"
+"Red","Red","USA","Small","Waxy","Sweet","Roasting","Summer","Cool, Dark Place","Low","154","3.4","33.9","0.3","3.3"
+"White","White","USA","Medium","Starchy","Mild","Frying","Fall","Cool, Dark Place","Medium","130","2.7","29.4","0.2","2.2"
+"Sweet","Orange","USA","Large","Starchy","Sweet","Baking","Fall","Cool, Dark Place","High","103","2.3","23.6","0.2","3.8"
+"Blue","Blue","Peru","Small","Waxy","Earthy","Roasting","Winter","Cool, Dark Place","Medium","131","2.5","30","0.2","2.4"
+"Fingerling","Yellow","France","Small","Waxy","Nutty","Roasting","Spring","Cool, Dark Place","Low","81","1.9","18.1","0.1","1.6"
+"Adirondack Blue","Blue","USA","Medium","Starchy","Earthy","Boiling","Winter","Cool, Dark Place","High","131","2.5","30","0.2","2.4"
+"Adirondack Red","Red","USA","Medium","Starchy","Sweet","Boiling","Winter","Cool, Dark Place","High","154","3.4","33.9","0.3","3.3"
+"Anya","Cream","UK","Small","Waxy","Nutty","Roasting","Spring","Cool, Dark Place","Low","81","1.9","18.1","0.1","1.6"
+"Arran Victory","Purple","Scotland","Large","Starchy","Earthy","Baking","Winter","Cool, Dark Place","High","168","4.6","37.9","0.2","3.8"
+"Belle de Fontenay","Yellow","France","Medium","Waxy","Buttery","Boiling","Spring","Cool, Dark Place","Medium","77","2","17.5","0.1","2"
+"Bergerac","Red","France","Large","Starchy","Sweet","Roasting","Summer","Cool, Dark Place","Low","154","3.4","33.9","0.3","3.3"
+"Charlotte","Cream","France","Medium","Waxy","Mild","Frying","Fall","Cool, Dark Place","Medium","130","2.7","29.4","0.2","2.2"
+"Desiree","Red","Netherlands","Large","Starchy","Sweet","Baking","Fall","Cool, Dark Place","High","103","2.3","23.6","0.2","3.8"
+"Estima","Cream","UK","Medium","Waxy","Earthy","Roasting","Winter","Cool, Dark Place","Medium","131","2.5","30","0.2","2.4"
+"King Edward","Cream","UK","Large","Starchy","Nutty","Roasting","Spring","Cool, Dark Place","Low","81","1.9","18.1","0.1","1.6"
+"Maris Piper","Cream","UK","Large","Starchy","Earthy","Boiling","Winter","Cool, Dark Place","High","131","2.5","30","0.2","2.4"
+"Nicola","Yellow","Germany","Medium","Waxy","Sweet","Boiling","Winter","Cool, Dark Place","High","154","3.4","33.9","0.3","3.3"
+"Pink Fir Apple","Pink","France","Small","Waxy","Nutty","Roasting","Spring","Cool, Dark Place","Low","81","1.9","18.1","0.1","1.6"
\ No newline at end of file
diff --git a/test/sample_potatoes.md b/test/sample_potatoes.md
new file mode 100644
index 0000000..95b666f
--- /dev/null
+++ b/test/sample_potatoes.md
@@ -0,0 +1,22 @@
+|      Type       |Color |  Origin   | Size |Texture| Taste |Cooking Method|Season|    Storage     |Nutrition|Calories|Protein|Carbs|Fat|Fiber|
+|-----------------|------|-----------|------|-------|-------|--------------|------|----------------|---------|--------|-------|-----|---|-----|
+|     Russet      |Brown |    USA    |Large |Starchy| Mild  |    Baking    | Fall |Cool, Dark Place|  High   |  168   |  4.6  |37.9 |0.2| 3.8 |
+|   Yukon Gold    |Yellow|  Canada   |Medium| Waxy  |Buttery|   Boiling    |Spring|Cool, Dark Place| Medium  |   77   |   2   |17.5 |0.1|  2  |
+|       Red       | Red  |    USA    |Small | Waxy  | Sweet |   Roasting   |Summer|Cool, Dark Place|   Low   |  154   |  3.4  |33.9 |0.3| 3.3 |
+|      White      |White |    USA    |Medium|Starchy| Mild  |    Frying    | Fall |Cool, Dark Place| Medium  |  130   |  2.7  |29.4 |0.2| 2.2 |
+|      Sweet      |Orange|    USA    |Large |Starchy| Sweet |    Baking    | Fall |Cool, Dark Place|  High   |  103   |  2.3  |23.6 |0.2| 3.8 |
+|      Blue       | Blue |   Peru    |Small | Waxy  |Earthy |   Roasting   |Winter|Cool, Dark Place| Medium  |  131   |  2.5  | 30  |0.2| 2.4 |
+|   Fingerling    |Yellow|  France   |Small | Waxy  | Nutty |   Roasting   |Spring|Cool, Dark Place|   Low   |   81   |  1.9  |18.1 |0.1| 1.6 |
+| Adirondack Blue | Blue |    USA    |Medium|Starchy|Earthy |   Boiling    |Winter|Cool, Dark Place|  High   |  131   |  2.5  | 30  |0.2| 2.4 |
+| Adirondack Red  | Red  |    USA    |Medium|Starchy| Sweet |   Boiling    |Winter|Cool, Dark Place|  High   |  154   |  3.4  |33.9 |0.3| 3.3 |
+|      Anya       |Cream |    UK     |Small | Waxy  | Nutty |   Roasting   |Spring|Cool, Dark Place|   Low   |   81   |  1.9  |18.1 |0.1| 1.6 |
+|  Arran Victory  |Purple| Scotland  |Large |Starchy|Earthy |    Baking    |Winter|Cool, Dark Place|  High   |  168   |  4.6  |37.9 |0.2| 3.8 |
+|Belle de Fontenay|Yellow|  France   |Medium| Waxy  |Buttery|   Boiling    |Spring|Cool, Dark Place| Medium  |   77   |   2   |17.5 |0.1|  2  |
+|    Bergerac     | Red  |  France   |Large |Starchy| Sweet |   Roasting   |Summer|Cool, Dark Place|   Low   |  154   |  3.4  |33.9 |0.3| 3.3 |
+|    Charlotte    |Cream |  France   |Medium| Waxy  | Mild  |    Frying    | Fall |Cool, Dark Place| Medium  |  130   |  2.7  |29.4 |0.2| 2.2 |
+|     Desiree     | Red  |Netherlands|Large |Starchy| Sweet |    Baking    | Fall |Cool, Dark Place|  High   |  103   |  2.3  |23.6 |0.2| 3.8 |
+|     Estima      |Cream |    UK     |Medium| Waxy  |Earthy |   Roasting   |Winter|Cool, Dark Place| Medium  |  131   |  2.5  | 30  |0.2| 2.4 |
+|   King Edward   |Cream |    UK     |Large |Starchy| Nutty |   Roasting   |Spring|Cool, Dark Place|   Low   |   81   |  1.9  |18.1 |0.1| 1.6 |
+|   Maris Piper   |Cream |    UK     |Large |Starchy|Earthy |   Boiling    |Winter|Cool, Dark Place|  High   |  131   |  2.5  | 30  |0.2| 2.4 |
+|     Nicola      |Yellow|  Germany  |Medium| Waxy  | Sweet |   Boiling    |Winter|Cool, Dark Place|  High   |  154   |  3.4  |33.9 |0.3| 3.3 |
+| Pink Fir Apple  | Pink |  France   |Small | Waxy  | Nutty |   Roasting   |Spring|Cool, Dark Place|   Low   |   81   |  1.9  |18.1 |0.1| 1.6 |