├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── lib.rs ├── main.rs ├── reader.rs ├── reader ├── csv_reader.rs └── jsonl_reader.rs ├── writer.rs └── writer ├── ascii_writer.rs └── markdown_writer.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ## 0.3.1 (2020-07-14) 6 | 7 | ### Added 8 | * Add jsonl reader. 9 | 10 | ### Changed 11 | * Use error messages generated by underlying errors. 12 | 13 | ## 0.3.0 (2020-07-11) 14 | 15 | ### Added 16 | * Add csv reader. 17 | 18 | ### Changed 19 | * Rewrite codes from scratch in Rust. 20 | 21 | ### Removed 22 | * Remove json reader. 23 | * Remove confluence writer. 24 | 25 | ## 0.2.0 (2017-03-05) 26 | 27 | ### Added 28 | * Support json as input format. 29 | 30 | ## 0.1.2 (2017-02-20) 31 | 32 | ### Fixed 33 | * Fix a bug to correctly count column widths when data includes wide characters 34 | 35 | ## 0.1.1 (2017-01-17) 36 | 37 | ### Added 38 | * Add `-f` and `--format` option to specify how to format a table 39 | * Add `-f=markdown` option to print a table in markdown 40 | * Add `-f=confluence` option to print a table in Confluence Wiki Markup 41 | 42 | ## 0.1.0 (2017-01-16) 43 | 44 | ### Added 45 | * Add fundamental features 46 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.9.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" 8 | 9 | [[package]] 10 | name = "atty" 11 | version = "0.2.14" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 14 | dependencies = [ 15 | "hermit-abi", 16 | "libc", 17 | "winapi", 18 | ] 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "0.9.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 25 | 26 | [[package]] 27 | name = "bstr" 28 | version = "0.2.13" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" 31 | dependencies = [ 32 | "lazy_static", 33 | "memchr", 34 | "regex-automata", 35 | "serde", 36 | ] 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.3.4" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 43 | 44 | [[package]] 45 | name = "clap" 46 | version = "2.27.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1b8c532887f1a292d17de05ae858a8fe50a301e196f9ef0ddb7ccd0d1d00f180" 49 | dependencies = [ 50 | "ansi_term", 51 | "atty", 52 | "bitflags", 53 | "strsim", 54 | "textwrap", 55 | "unicode-width", 56 | "vec_map", 57 | ] 58 | 59 | [[package]] 60 | name = "csv" 61 | version = "1.1.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" 64 | dependencies = [ 65 | "bstr", 66 | "csv-core", 67 | "itoa", 68 | "ryu", 69 | "serde", 70 | ] 71 | 72 | [[package]] 73 | name = "csv-core" 74 | version = "0.1.10" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 77 | dependencies = [ 78 | "memchr", 79 | ] 80 | 81 | [[package]] 82 | name = "hermit-abi" 83 | version = "0.1.14" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" 86 | dependencies = [ 87 | "libc", 88 | ] 89 | 90 | [[package]] 91 | name = "itoa" 92 | version = "0.4.6" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 95 | 96 | [[package]] 97 | name = "lazy_static" 98 | version = "1.4.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 101 | 102 | [[package]] 103 | name = "libc" 104 | version = "0.2.71" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" 107 | 108 | [[package]] 109 | name = "memchr" 110 | version = "2.3.3" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 113 | 114 | [[package]] 115 | name = "regex-automata" 116 | version = "0.1.9" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" 119 | dependencies = [ 120 | "byteorder", 121 | ] 122 | 123 | [[package]] 124 | name = "ryu" 125 | version = "1.0.5" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 128 | 129 | [[package]] 130 | name = "serde" 131 | version = "1.0.114" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" 134 | 135 | [[package]] 136 | name = "serde_json" 137 | version = "1.0.56" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" 140 | dependencies = [ 141 | "itoa", 142 | "ryu", 143 | "serde", 144 | ] 145 | 146 | [[package]] 147 | name = "strsim" 148 | version = "0.6.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" 151 | 152 | [[package]] 153 | name = "table" 154 | version = "0.3.1" 155 | dependencies = [ 156 | "clap", 157 | "csv", 158 | "serde_json", 159 | "unicode-width", 160 | ] 161 | 162 | [[package]] 163 | name = "textwrap" 164 | version = "0.9.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" 167 | dependencies = [ 168 | "unicode-width", 169 | ] 170 | 171 | [[package]] 172 | name = "unicode-width" 173 | version = "0.1.8" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 176 | 177 | [[package]] 178 | name = "vec_map" 179 | version = "0.8.2" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 182 | 183 | [[package]] 184 | name = "winapi" 185 | version = "0.3.9" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 188 | dependencies = [ 189 | "winapi-i686-pc-windows-gnu", 190 | "winapi-x86_64-pc-windows-gnu", 191 | ] 192 | 193 | [[package]] 194 | name = "winapi-i686-pc-windows-gnu" 195 | version = "0.4.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 198 | 199 | [[package]] 200 | name = "winapi-x86_64-pc-windows-gnu" 201 | version = "0.4.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 204 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "table" 3 | version = "0.3.1" 4 | authors = ["Naoto Kaneko "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = "~2.27.0" 11 | csv = "1.1" 12 | serde_json = "1.0" 13 | unicode-width = "0.1.7" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Naoto Kaneko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # table 2 | 3 | ## Installation 4 | 5 | ```sh 6 | $ brew tap naoty/misc 7 | $ brew install table 8 | ``` 9 | 10 | ## Input Format 11 | 12 | ### TSV 13 | 14 | ```sh 15 | $ echo -e "2017-01-01\t10000\n2017-01-02\t8000" | table 16 | +------------+-------+ 17 | | 2017-01-01 | 10000 | 18 | | 2017-01-02 | 8000 | 19 | +------------+-------+ 20 | ``` 21 | 22 | `-H` or `--header` option adds headers to the table. 23 | 24 | ```sh 25 | $ echo -e "day\tDAU\n2017-01-01\t10000\n2017-01-02\t8000" | table -H 26 | +------------+-------+ 27 | | day | DAU | 28 | +------------+-------+ 29 | | 2017-01-01 | 10000 | 30 | | 2017-01-02 | 8000 | 31 | +------------+-------+ 32 | ``` 33 | 34 | ### CSV 35 | 36 | ```sh 37 | $ echo -e "2017-01-01,10000\n2017-01-02,8000" | table -f csv 38 | +------------+-------+ 39 | | 2017-01-01 | 10000 | 40 | | 2017-01-02 | 8000 | 41 | +------------+-------+ 42 | ``` 43 | 44 | ### JSONL (JSON Lines) 45 | 46 | ```sh 47 | $ echo -n '{"day":"2020-01-01","DAU":10000}\n{"day":"2020-01-02","DAU":8000}' | table -f jsonl 48 | +-------+------------+ 49 | | DAU | day | 50 | +-------+------------+ 51 | | 10000 | 2020-01-01 | 52 | | 8000 | 2020-01-02 | 53 | +-------+------------+ 54 | ``` 55 | 56 | ## Output Format 57 | 58 | ### ASCII 59 | 60 | ```sh 61 | $ echo -e "day\tDAU\n2017-01-01\t10000\n2017-01-02\t8000" | table -H -f tsv:ascii 62 | +------------+-------+ 63 | | day | DAU | 64 | +------------+-------+ 65 | | 2017-01-01 | 10000 | 66 | | 2017-01-02 | 8000 | 67 | +------------+-------+ 68 | ``` 69 | 70 | ### Markdown 71 | 72 | ```sh 73 | $ echo -e "day\tDAU\n2017-01-01\t10000\n2017-01-02\t8000" | table -H -f tsv:markdown 74 | | day | DAU | 75 | | ---------- | ----- | 76 | | 2017-01-01 | 10000 | 77 | | 2017-01-02 | 8000 | 78 | ``` 79 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt, result}; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | pub mod reader; 5 | pub mod writer; 6 | 7 | #[derive(Debug, Eq, PartialEq)] 8 | pub struct Table { 9 | headers: Option>, 10 | rows: Vec>, 11 | } 12 | 13 | impl Table { 14 | pub fn new() -> Self { 15 | Table::from(None, vec![]) 16 | } 17 | 18 | pub fn with_headers(headers: Option>) -> Self { 19 | Table::from(headers, vec![]) 20 | } 21 | 22 | pub fn from(headers: Option>, rows: Vec>) -> Self { 23 | Table { rows, headers } 24 | } 25 | 26 | pub fn push_row(&mut self, row: Vec) { 27 | self.rows.push(row); 28 | } 29 | 30 | pub fn column_len(&self) -> usize { 31 | let column_len = self.rows.iter().fold(0, |num, row| num.max(row.len())); 32 | match &self.headers { 33 | Some(headers) => column_len.max(headers.len()), 34 | None => column_len, 35 | } 36 | } 37 | 38 | pub fn column_widths(&self) -> Vec { 39 | let mut max_widths = vec![0; self.column_len()]; 40 | 41 | for row in self.rows.iter() { 42 | for (i, cell) in row.iter().enumerate() { 43 | let max_width = max_widths.get(i).unwrap().clone(); 44 | max_widths[i] = max_width.max(UnicodeWidthStr::width(cell as &str)); 45 | } 46 | } 47 | 48 | if let Some(headers) = &self.headers { 49 | for (i, cell) in headers.iter().enumerate() { 50 | let max_width = max_widths.get(i).unwrap().clone(); 51 | max_widths[i] = max_width.max(UnicodeWidthStr::width(cell as &str)); 52 | } 53 | } 54 | 55 | max_widths 56 | } 57 | } 58 | 59 | pub trait Read { 60 | fn read(&mut self) -> Result; 61 | } 62 | 63 | pub trait Write { 64 | fn write(&mut self, table: Table) -> Result<()>; 65 | fn flush(&mut self) -> Result<()>; 66 | } 67 | 68 | type Result = result::Result>; 69 | 70 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 71 | pub enum Error { 72 | InvalidInput, 73 | } 74 | 75 | impl fmt::Display for Error { 76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 | match self { 78 | Error::InvalidInput => write!(f, "invalid input"), 79 | } 80 | } 81 | } 82 | 83 | impl error::Error for Error {} 84 | 85 | mod tests { 86 | #[test] 87 | fn table_column_len() { 88 | let test_cases = vec![ 89 | (vec![], 0), 90 | (vec![vec![]], 0), 91 | (vec![vec![String::from("alice")]], 1), 92 | (vec![vec![String::from("alice"), String::from("100")]], 2), 93 | ( 94 | vec![ 95 | vec![String::from("alice"), String::from("100")], 96 | vec![String::from("bob")], 97 | ], 98 | 2, 99 | ), 100 | ]; 101 | 102 | for test_case in test_cases { 103 | let table = crate::Table::from(None, test_case.0); 104 | assert_eq!(table.column_len(), test_case.1); 105 | } 106 | } 107 | 108 | #[test] 109 | fn table_column_widths() { 110 | let test_cases = vec![ 111 | (vec![], vec![]), 112 | (vec![vec![]], vec![]), 113 | (vec![vec![String::from("alice")]], vec![5]), 114 | ( 115 | vec![vec![String::from("alice"), String::from("80")]], 116 | vec![5, 2], 117 | ), 118 | ( 119 | vec![ 120 | vec![String::from("alice"), String::from("80")], 121 | vec![String::from("bob"), String::from("100")], 122 | ], 123 | vec![5, 3], 124 | ), 125 | ( 126 | vec![ 127 | vec![String::from("alice"), String::from("80")], 128 | vec![String::from("ボブ"), String::from("100点")], 129 | ], 130 | vec![5, 5], 131 | ), 132 | ]; 133 | 134 | for test_case in test_cases { 135 | let table = crate::Table::from(None, test_case.0); 136 | assert_eq!(table.column_widths(), test_case.1); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_version, App, Arg}; 2 | use std::io; 3 | use std::process; 4 | use table::{reader, writer}; 5 | 6 | const DESCRIPTION: &str = "A command to print ASCII table from stdin"; 7 | const USAGE_TEMPLATE: &str = r#" 8 | Usage: 9 | {usage} 10 | 11 | Flags: 12 | {flags} 13 | 14 | Options: 15 | {options} 16 | "#; 17 | 18 | fn main() { 19 | let matches = App::new("table") 20 | .version(crate_version!()) 21 | .author("Naoto Kaneko ") 22 | .about(DESCRIPTION) 23 | .template(USAGE_TEMPLATE.trim()) 24 | .version_short("v") 25 | .arg( 26 | Arg::with_name("format") 27 | .short("f") 28 | .long("format") 29 | .takes_value(true) 30 | .default_value("") 31 | .value_name("FORMAT") 32 | .help("Config input/output data format"), 33 | ) 34 | .arg( 35 | Arg::with_name("header") 36 | .short("H") 37 | .long("header") 38 | .help("Prints table with headers"), 39 | ) 40 | .get_matches(); 41 | 42 | let format = matches.value_of("format").unwrap_or_default(); 43 | let mut tokens = format.split(":").take(2); 44 | 45 | let mut csv_reader = reader::CsvReader::new(io::stdin(), b',', matches.is_present("header")); 46 | let mut jsonl_reader = reader::JsonlReader::new(io::stdin()); 47 | let mut tsv_reader = reader::CsvReader::new(io::stdin(), b'\t', matches.is_present("header")); 48 | let reader: &mut dyn table::Read = match tokens.next() { 49 | Some("csv") => &mut csv_reader, 50 | Some("jsonl") => &mut jsonl_reader, 51 | _ => &mut tsv_reader, 52 | }; 53 | 54 | let mut ascii_writer = writer::AsciiWriter::new(io::stdout()); 55 | let mut markdown_writer = writer::MarkdownWriter::new(io::stdout()); 56 | let writer: &mut dyn table::Write = match tokens.next() { 57 | Some("markdown") => &mut markdown_writer, 58 | _ => &mut ascii_writer, 59 | }; 60 | 61 | let result = reader 62 | .read() 63 | .and_then(|table| writer.write(table)) 64 | .and_then(|_| writer.flush()); 65 | 66 | match result { 67 | Ok(_) => { 68 | process::exit(0); 69 | } 70 | Err(error) => { 71 | eprintln!("{:?}", error); 72 | process::exit(1); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | mod csv_reader; 2 | mod jsonl_reader; 3 | pub use csv_reader::CsvReader; 4 | pub use jsonl_reader::JsonlReader; 5 | -------------------------------------------------------------------------------- /src/reader/csv_reader.rs: -------------------------------------------------------------------------------- 1 | use csv::{Reader, ReaderBuilder}; 2 | use std::io; 3 | 4 | pub struct CsvReader { 5 | reader: Reader, 6 | headers: bool, 7 | } 8 | 9 | impl CsvReader { 10 | pub fn new(reader: T, delimiter: u8, headers: bool) -> CsvReader { 11 | let reader = ReaderBuilder::new() 12 | .delimiter(delimiter) 13 | .has_headers(false) 14 | .from_reader(reader); 15 | 16 | CsvReader { reader, headers } 17 | } 18 | } 19 | 20 | impl crate::Read for CsvReader { 21 | fn read(&mut self) -> crate::Result { 22 | let mut table = crate::Table::new(); 23 | 24 | for (i, result) in self.reader.records().enumerate() { 25 | let record = result?; 26 | let row: Vec = record.iter().map(|field| String::from(field)).collect(); 27 | if i == 0 && self.headers { 28 | table.headers = Some(row); 29 | } else { 30 | table.push_row(row); 31 | } 32 | } 33 | 34 | Ok(table) 35 | } 36 | } 37 | 38 | mod tests { 39 | #[allow(unused)] 40 | use super::*; 41 | #[allow(unused)] 42 | use crate::Read; 43 | 44 | #[test] 45 | fn read() { 46 | let test_cases = vec![ 47 | ( 48 | "alice\t80", 49 | false, 50 | crate::Table::from(None, vec![vec![String::from("alice"), String::from("80")]]), 51 | ), 52 | ( 53 | "alice\t80\nbob\t100", 54 | false, 55 | crate::Table::from( 56 | None, 57 | vec![ 58 | vec![String::from("alice"), String::from("80")], 59 | vec![String::from("bob"), String::from("100")], 60 | ], 61 | ), 62 | ), 63 | ( 64 | "name\tscore\nalice\t80", 65 | true, 66 | crate::Table::from( 67 | Some(vec![String::from("name"), String::from("score")]), 68 | vec![vec![String::from("alice"), String::from("80")]], 69 | ), 70 | ), 71 | ]; 72 | 73 | for test_case in test_cases { 74 | let mut reader = CsvReader::new(test_case.0.as_bytes(), b'\t', test_case.1); 75 | let table = reader.read().expect("failed to read table"); 76 | assert_eq!(table, test_case.2); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/reader/jsonl_reader.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub struct JsonlReader { 4 | reader: T, 5 | } 6 | 7 | impl JsonlReader { 8 | pub fn new(reader: T) -> Self { 9 | JsonlReader { reader } 10 | } 11 | } 12 | 13 | impl crate::Read for JsonlReader { 14 | fn read(&mut self) -> crate::Result { 15 | let mut table = crate::Table::new(); 16 | 17 | let mut buffer = String::new(); 18 | self.reader.read_to_string(&mut buffer)?; 19 | 20 | let mut headers: Vec = vec![]; 21 | for line in buffer.split("\n") { 22 | let mut row: Vec = vec![]; 23 | 24 | let json_value: serde_json::Value = serde_json::from_str(line)?; 25 | let json_object = json_value.as_object().ok_or(crate::Error::InvalidInput)?; 26 | 27 | for (key, value) in json_object { 28 | let index = match headers.iter().position(|header| header == key) { 29 | Some(index) => index, 30 | None => { 31 | headers.push(key.clone()); 32 | headers.len() - 1 33 | } 34 | }; 35 | 36 | if index >= row.len() { 37 | row.resize_with(index + 1, Default::default); 38 | row[index] = match value { 39 | serde_json::Value::String(string) => string.clone(), 40 | _ => format!("{}", value), 41 | }; 42 | } 43 | } 44 | 45 | table.push_row(row); 46 | } 47 | 48 | table.headers = Some(headers); 49 | Ok(table) 50 | } 51 | } 52 | 53 | mod tests { 54 | #[allow(unused)] 55 | use super::*; 56 | #[allow(unused)] 57 | use crate::Read; 58 | 59 | #[test] 60 | fn read() { 61 | let test_cases = vec![ 62 | ( 63 | r#"{"name":"alice","score":80}"#, 64 | crate::Table::from( 65 | Some(vec![String::from("name"), String::from("score")]), 66 | vec![vec![String::from("alice"), String::from("80")]], 67 | ), 68 | ), 69 | ( 70 | r#"{"name":"alice","score":80} 71 | {"name":"bob","score":70}"#, 72 | crate::Table::from( 73 | Some(vec![String::from("name"), String::from("score")]), 74 | vec![ 75 | vec![String::from("alice"), String::from("80")], 76 | vec![String::from("bob"), String::from("70")], 77 | ], 78 | ), 79 | ), 80 | ]; 81 | 82 | for test_case in test_cases { 83 | let mut reader = JsonlReader::new(test_case.0.as_bytes()); 84 | let table = reader.read().expect("failed to read"); 85 | assert_eq!(table, test_case.1); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/writer.rs: -------------------------------------------------------------------------------- 1 | mod ascii_writer; 2 | mod markdown_writer; 3 | pub use ascii_writer::AsciiWriter; 4 | pub use markdown_writer::MarkdownWriter; 5 | -------------------------------------------------------------------------------- /src/writer/ascii_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | pub struct AsciiWriter { 5 | writer: T, 6 | } 7 | 8 | impl AsciiWriter { 9 | pub fn new(writer: T) -> Self { 10 | AsciiWriter { writer } 11 | } 12 | } 13 | 14 | impl crate::Write for AsciiWriter { 15 | fn write(&mut self, table: crate::Table) -> crate::Result<()> { 16 | let column_widths = table.column_widths(); 17 | 18 | let mut border = String::new(); 19 | for width in column_widths.iter() { 20 | border += &format!("+-{}-", "-".repeat(width.clone())); 21 | } 22 | border += "+\n"; 23 | 24 | self.writer.write(border.as_bytes())?; 25 | 26 | if let Some(headers) = table.headers { 27 | for (j, value) in headers.iter().enumerate() { 28 | let spaces = " ".repeat(column_widths[j] - UnicodeWidthStr::width(value as &str)); 29 | let cell = format!("| {}{} ", value, spaces); 30 | self.writer.write(cell.as_bytes())?; 31 | } 32 | 33 | self.writer.write(b"|\n")?; 34 | self.writer.write(border.as_bytes())?; 35 | } 36 | 37 | for row in table.rows.iter() { 38 | for (j, value) in row.iter().enumerate() { 39 | let spaces = " ".repeat(column_widths[j] - UnicodeWidthStr::width(value as &str)); 40 | let cell = format!("| {}{} ", value, spaces); 41 | self.writer.write(cell.as_bytes())?; 42 | } 43 | 44 | self.writer.write(b"|\n")?; 45 | } 46 | 47 | self.writer.write(border.as_bytes())?; 48 | Ok(()) 49 | } 50 | 51 | fn flush(&mut self) -> crate::Result<()> { 52 | self.writer.flush()?; 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/writer/markdown_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | pub struct MarkdownWriter { 5 | writer: T, 6 | } 7 | 8 | impl MarkdownWriter { 9 | pub fn new(writer: T) -> Self { 10 | MarkdownWriter { writer } 11 | } 12 | } 13 | 14 | impl crate::Write for MarkdownWriter { 15 | fn write(&mut self, table: crate::Table) -> crate::Result<()> { 16 | let column_widths = table.column_widths(); 17 | 18 | let mut border = String::new(); 19 | for column_width in column_widths.iter() { 20 | border += &format!("| {} ", "-".repeat(column_width.clone())); 21 | } 22 | border += "|\n"; 23 | 24 | for (i, row) in table.rows.iter().enumerate() { 25 | for (j, value) in row.iter().enumerate() { 26 | let spaces = " ".repeat(column_widths[j] - UnicodeWidthStr::width(value as &str)); 27 | let cell = format!("| {}{} ", value, spaces); 28 | self.writer.write(cell.as_bytes())?; 29 | } 30 | 31 | self.writer.write(b"|\n")?; 32 | 33 | if i == 0 { 34 | self.writer.write(border.as_bytes())?; 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | fn flush(&mut self) -> crate::Result<()> { 42 | self.writer.flush()?; 43 | Ok(()) 44 | } 45 | } 46 | --------------------------------------------------------------------------------