From 8219e4e61ee6d0b8b953972971112242edfb78cb Mon Sep 17 00:00:00 2001 From: "Clelia (Astra) Bertelli" Date: Sat, 4 Apr 2026 19:39:06 +0200 Subject: [PATCH 1/3] feat: write csv --- Cargo.toml | 6 +- benchmark/bench.ts | 114 ++++++++++++++++++- index.d.ts | 10 ++ index.js | 4 + package.json | 1 - src/lib.rs | 109 +++++++++++++++++- testfiles/comparative_bench.py | 85 +++++++++++++- testfiles/generated-100.csv | 200 ++++++++++++++++----------------- 8 files changed, 415 insertions(+), 114 deletions(-) mode change 100755 => 100644 testfiles/comparative_bench.py diff --git a/Cargo.toml b/Cargo.toml index 756019f..49bad94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.102" csv = "1.4.0" -napi = { version = "3.0.0", features = ["anyhow", "serde_json"] } +itoa = "1.0.18" +napi = { version = "3.0.0", features = ["anyhow"] } napi-derive = "3.0.0" -serde = "1.0.228" -serde_json = "1.0.149" +ryu = "1.0.23" [build-dependencies] napi-build = "2" diff --git a/benchmark/bench.ts b/benchmark/bench.ts index dd47ba0..9069203 100644 --- a/benchmark/bench.ts +++ b/benchmark/bench.ts @@ -1,6 +1,9 @@ +/* eslint-disable */ + import { Bench } from 'tinybench' -import { readCsv } from '../index.js' +import { DataFrame, readCsv, toBoolColumn, toFloatColumn, toIntColumn, toStringColumn } from '../index.js' +import { unlinkSync } from 'fs' const b = new Bench() @@ -28,6 +31,115 @@ b.add('Read a 1000000-lines CSV', () => { console.log('Ended 1.000.000 lines bench') }) +let df: DataFrame | undefined = undefined +let df_100: DataFrame | undefined = undefined +let df_1000: DataFrame | undefined = undefined +let df_100000: DataFrame | undefined = undefined + +b.add( + 'Write a 100-lines CSV', + () => { + console.log('Started writing 100 lines bench') + df_100!.writeCsv('testfiles/written-100.csv') + console.log('Ended writing 100 lines bench') + }, + { + beforeAll: () => { + const col1 = toStringColumn(new Array(100).fill('something')) + const col2 = toFloatColumn(new Array(100).fill(1.0)) + const col3 = toIntColumn(new Array(100).fill(3)) + const col4 = toBoolColumn(new Array(100).fill(true)) + df_100 = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, + }) + }, + afterEach: () => { + unlinkSync('testfiles/written-100.csv') + }, + }, +) + +b.add( + 'Write a 1000-lines CSV', + () => { + console.log('Started writing 1000 lines bench') + df_1000!.writeCsv('testfiles/written-1000.csv') + console.log('Ended writing 1000 lines bench') + }, + { + beforeAll: () => { + const col1 = toStringColumn(new Array(1000).fill('something')) + const col2 = toFloatColumn(new Array(1000).fill(1.0)) + const col3 = toIntColumn(new Array(1000).fill(3)) + const col4 = toBoolColumn(new Array(1000).fill(true)) + df_1000 = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, + }) + }, + afterEach: () => { + unlinkSync('testfiles/written-1000.csv') + }, + }, +) + +b.add( + 'Write a 100000-lines CSV', + () => { + console.log('Started writing 100.000 lines bench') + df_100000!.writeCsv('testfiles/written-100000.csv') + console.log('Ended writing 100.000 lines bench') + }, + { + beforeAll: () => { + const col1 = toStringColumn(new Array(100_000).fill('something')) + const col2 = toFloatColumn(new Array(100_000).fill(1.0)) + const col3 = toIntColumn(new Array(100_000).fill(3)) + const col4 = toBoolColumn(new Array(100_000).fill(true)) + df_100000 = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, + }) + }, + afterEach: () => { + unlinkSync('testfiles/written-100000.csv') + }, + }, +) + +b.add( + 'Write a 1000000-lines CSV', + () => { + console.log('Started writing 1.000.000 lines bench') + df!.writeCsv('testfiles/written-1000000.csv') + console.log('Ended writing 1.000.000 lines bench') + }, + { + beforeAll: () => { + const col1 = toStringColumn(new Array(1_000_000).fill('something')) + const col2 = toFloatColumn(new Array(1_000_000).fill(1.0)) + const col3 = toIntColumn(new Array(1_000_000).fill(3)) + const col4 = toBoolColumn(new Array(1_000_000).fill(true)) + df = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, + }) + }, + afterEach: () => { + unlinkSync('testfiles/written-1000000.csv') + }, + }, +) + await b.run() console.table(b.table()) diff --git a/index.d.ts b/index.d.ts index fdf0cdb..25b9470 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,9 +3,11 @@ export declare class DataFrame { len: number constructor(columns: Record, len: number) + static fromColumns(columns: Record): DataFrame colDtype(col: string): DataType | null get(col: string): ColumnData | null get columns(): Record + writeCsv(path: string): void } export declare function asBooleanArray(column: ColumnData): Array | null @@ -30,3 +32,11 @@ export declare const enum DataType { } export declare function readCsv(path: string): DataFrame + +export declare function toBoolColumn(data: Array): ColumnData + +export declare function toFloatColumn(data: Array): ColumnData + +export declare function toIntColumn(data: Array): ColumnData + +export declare function toStringColumn(data: Array): ColumnData diff --git a/index.js b/index.js index 834c7fe..c53cb9c 100644 --- a/index.js +++ b/index.js @@ -583,3 +583,7 @@ module.exports.asIntArray = nativeBinding.asIntArray module.exports.asStringArray = nativeBinding.asStringArray module.exports.DataType = nativeBinding.DataType module.exports.readCsv = nativeBinding.readCsv +module.exports.toBoolColumn = nativeBinding.toBoolColumn +module.exports.toFloatColumn = nativeBinding.toFloatColumn +module.exports.toIntColumn = nativeBinding.toIntColumn +module.exports.toStringColumn = nativeBinding.toStringColumn diff --git a/package.json b/package.json index b9c794c..5940801 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ ], "napi": { "binaryName": "sunbears", - "name": "sunbears", "targets": [ "x86_64-pc-windows-msvc", "x86_64-apple-darwin", diff --git a/src/lib.rs b/src/lib.rs index f34be11..1859d70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,14 @@ #![deny(clippy::all)] -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, fmt, fs::File, io::BufWriter}; use anyhow::{anyhow, Result}; -use csv::Reader; +use csv::{Reader, Writer}; use napi_derive::napi; +// ---------------------------------------- DATA MODELS ---------------------------------------- + #[napi] #[derive(Clone)] pub enum ColumnData { @@ -16,6 +18,17 @@ pub enum ColumnData { Boolean(Vec), } +impl ColumnData { + fn len(&self) -> usize { + match self { + Self::Boolean(b) => b.len(), + Self::Float(f) => f.len(), + Self::Integer(i) => i.len(), + Self::String(s) => s.len(), + } + } +} + #[napi] #[derive(PartialEq, Debug, Clone)] pub enum DataType { @@ -37,6 +50,8 @@ impl fmt::Display for DataType { } } +// ---------------------------------------- HELPER FUNCTIONS FOR DATA MODELS ---------------------------------------- + #[napi] pub fn as_float_array(column: ColumnData) -> Option> { match column { @@ -69,6 +84,28 @@ pub fn as_string_array(column: ColumnData) -> Option> { } } +#[napi] +pub fn to_string_column(data: Vec) -> ColumnData { + ColumnData::String(data) +} + +#[napi] +pub fn to_float_column(data: Vec) -> ColumnData { + ColumnData::Float(data) +} + +#[napi] +pub fn to_int_column(data: Vec) -> ColumnData { + ColumnData::Integer(data) +} + +#[napi] +pub fn to_bool_column(data: Vec) -> ColumnData { + ColumnData::Boolean(data) +} + +// ---------------------------------------- DATAFRAME ---------------------------------------- + #[napi(js_name = "DataFrame")] pub struct DataFrame { columns: HashMap, @@ -78,8 +115,26 @@ pub struct DataFrame { #[napi] impl DataFrame { #[napi(constructor)] - pub fn new(columns: HashMap, len: u32) -> Self { - Self { columns, len } + pub fn new(columns: HashMap, len: u32) -> Result { + if !columns.iter().all(|(_, c)| c.len() as u32 == len) { + return Err(anyhow!("No all columns are the same length")); + } + Ok(Self { columns, len }) + } + + #[napi(factory)] + pub fn from_columns(columns: HashMap) -> Result { + let keys: Vec<&String> = columns.keys().collect(); + let col = &columns[keys[0]]; + let l = col.len(); + if !columns.iter().all(|(_, c)| c.len() == l) { + return Err(anyhow!("No all columns are the same length")); + } + + Ok(Self { + columns, + len: l as u32, + }) } #[napi] @@ -107,8 +162,52 @@ impl DataFrame { pub fn columns(&self) -> HashMap { self.columns.clone() } + + #[napi] + pub fn write_csv(&self, path: String) -> Result<()> { + let file = File::create(&path)?; + let buf = BufWriter::with_capacity(1 << 20, file); // 1 MB buffer size + let mut writer = Writer::from_writer(buf); + let mut float_buf = ryu::Buffer::new(); + let mut int_buf = itoa::Buffer::new(); + let keys: Vec<&String> = self.columns.keys().collect(); + writer.write_record(keys.iter().map(|k| k.as_str()))?; + for i in 0..self.len as usize { + let mut row = Vec::with_capacity(keys.len()); + for key in &keys { + let col = &self.columns[*key]; + match col { + ColumnData::Boolean(b) => { + let v = if b[i] { + "true".to_string() + } else { + "false".to_string() + }; + row.push(v); + } + ColumnData::Float(f) => { + let v = float_buf.format(f[i]).to_string(); + row.push(v); + } + ColumnData::Integer(j) => { + let v = int_buf.format(j[i]).to_string(); + row.push(v); + } + ColumnData::String(s) => { + row.push(s[i].clone()); + } + } + } + writer.write_record(&row)?; + } + writer.flush()?; + + Ok(()) + } } +// ---------------------------------------- CSV READING ---------------------------------------- + fn infer_dtype(s: &str) -> DataType { if s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("false") { return DataType::Boolean; @@ -207,7 +306,7 @@ pub fn read_csv(path: String) -> Result { cols.insert(col_idx[*c].to_owned(), data); } - let df = DataFrame::new(cols, i); + let df = DataFrame::new(cols, i)?; Ok(df) } diff --git a/testfiles/comparative_bench.py b/testfiles/comparative_bench.py old mode 100755 new mode 100644 index 00ceed6..4498365 --- a/testfiles/comparative_bench.py +++ b/testfiles/comparative_bench.py @@ -6,12 +6,36 @@ # /// +import os import timeit +from glob import glob import pandas as pd import polars as pl +def generate_df_with_n_rows(n: int, pandas: bool) -> pl.DataFrame | pd.DataFrame: + data = { + "col1": ["something"] * n, + "col2": [1.0] * n, + "col3": [3] * n, + "col4": [True] * n, + } + if pandas: + return pd.DataFrame(data) + return pl.DataFrame(data) + + +pl_100_df = generate_df_with_n_rows(100, False) +pl_1000_df = generate_df_with_n_rows(1000, False) +pl_100000_df = generate_df_with_n_rows(100000, False) +pl_1000000_df = generate_df_with_n_rows(1000000, False) +pd_100_df = generate_df_with_n_rows(100, True) +pd_1000_df = generate_df_with_n_rows(1000, True) +pd_100000_df = generate_df_with_n_rows(100000, True) +pd_1000000_df = generate_df_with_n_rows(1000000, True) + + def read_100_with_polars() -> None: """ Read a 100-rows CSV with Polars. @@ -69,6 +93,44 @@ def read_1000000_with_pandas() -> None: pd.read_csv("testfiles/generated-1000000.csv") +def write_100_with_pandas() -> None: + pd_100_df.to_csv("testfiles/pd-100.csv", index=False) + + +def write_1000_with_pandas() -> None: + pd_1000_df.to_csv("testfiles/pd-1000.csv", index=False) + + +def write_100000_with_pandas() -> None: + pd_100000_df.to_csv("testfiles/pd-100000.csv", index=False) + + +def write_1000000_with_pandas() -> None: + pd_1000000_df.to_csv("testfiles/pd-1000000.csv", index=False) + + +def write_100_with_polars() -> None: + pl_100_df.write_csv("testfiles/pl-100.csv") + + +def write_1000_with_polars() -> None: + pl_1000_df.write_csv("testfiles/pl-1000.csv") + + +def write_100000_with_polars() -> None: + pl_100000_df.write_csv("testfiles/pl-100000.csv") + + +def write_1000000_with_polars() -> None: + pl_1000000_df.write_csv("testfiles/pl-1000000.csv") + + +def cleanup() -> None: + files = glob("testfiles/p?-1*.csv") + for file in files: + os.remove(file) + + def main() -> None: t100_pd = timeit.timeit(read_100_with_pandas, number=200) t1000_pd = timeit.timeit(read_1000_with_pandas, number=100) @@ -79,11 +141,24 @@ def main() -> None: t100000_pl = timeit.timeit(read_100000_with_polars, number=20) t1000000_pl = timeit.timeit(read_1000000_with_polars, number=20) + wt100_pd = timeit.timeit(write_100_with_pandas, number=200) + wt1000_pd = timeit.timeit(write_1000_with_pandas, number=100) + wt100000_pd = timeit.timeit(write_100000_with_pandas, number=20) + wt1000000_pd = timeit.timeit(write_1000000_with_pandas, number=20) + wt100_pl = timeit.timeit(write_100_with_polars, number=200) + wt1000_pl = timeit.timeit(write_1000_with_polars, number=100) + wt100000_pl = timeit.timeit(write_100000_with_polars, number=20) + wt1000000_pl = timeit.timeit(write_1000000_with_polars, number=20) + rows = [ - ("100 lines", t100_pd, t100_pl), - ("1000 lines", t1000_pd, t1000_pl), - ("100000 lines", t100000_pd, t100000_pl), - ("1000000 lines", t1000000_pd, t1000000_pl), + ("Read 100 lines", t100_pd, t100_pl), + ("Read 1000 lines", t1000_pd, t1000_pl), + ("Read 100000 lines", t100000_pd, t100000_pl), + ("Read 1000000 lines", t1000000_pd, t1000000_pl), + ("Write 100 lines", wt100_pd, wt100_pl), + ("Write 1000 lines", wt1000_pd, wt1000_pl), + ("Write 100000 lines", wt100000_pd, wt100000_pl), + ("Write 1000000 lines", wt1000000_pd, wt1000000_pl), ] col0, col1, col2 = "Dataset", "Pandas (s)", "Polars (s)" @@ -101,6 +176,8 @@ def main() -> None: print(f"| {name:<{w0}} | {pd_t:<{w1}.6f} | {pl_t:<{w2}.6f} |") print(sep) + cleanup() + if __name__ == "__main__": main() diff --git a/testfiles/generated-100.csv b/testfiles/generated-100.csv index a91b020..cfd9b5d 100644 --- a/testfiles/generated-100.csv +++ b/testfiles/generated-100.csv @@ -1,101 +1,101 @@ name,age,is_working,height,phone_number,years_of_experience -Enid,45,false,151.5466823228966,0043-1234,2.5778831651313947 -Anne,27,false,157.32057957197924,0043-1234,3.920121676134699 -Anne,33,false,172.136358978696,0043-1234,0.8587354612575759 -Charles,37,false,171.33244976651685,0043-1234,2.903354645818903 -Anne,34,true,174.21281615745679,0043-1234,3.152821943088975 -Charles,24,false,161.73810910236352,0043-1234,1.24912967135455 -Enid,26,false,163.20241491839687,0043-1234,1.3843500318447932 -Enid,50,true,174.58942179951632,0043-1234,0.29440806510794815 -Charles,40,true,160.32882761029387,+1 1234,4.750086123138027 -Francis,20,false,177.84463400681972,0043-1234,3.590579920066384 -Anne,28,false,180.7559581929724,0043-1234,3.8079258128713915 -Enid,43,true,162.2578081838651,+1 1234,3.943149128054852 -Francis,45,true,161.15792835686182,+1 1234,3.275121349491939 -Charles,28,true,173.52374548303726,0043-1234,2.2907905374973434 -Charles,27,false,167.53436253284644,+1 1234,3.036343574334585 -Francis,41,false,151.6681706051119,+1 1234,4.704139386820414 -Charles,43,true,158.46248923827525,+1 1234,3.6361156343617385 -Charles,43,true,176.16091128359244,+1 1234,4.526527468976222 -Francis,29,false,171.5240388288034,+1 1234,1.3341152606202553 -Dylan,36,false,177.03758652443665,+1 1234,3.5017422747629494 -Dylan,45,true,168.21283391386254,0043-1234,2.1148839392310013 -Enid,34,true,152.4336293492877,0043-1234,4.938747197130386 -Charles,37,true,158.39485545404779,0043-1234,2.076732739300934 -Bob,36,true,158.75970242956387,0043-1234,2.9789814987774665 -Francis,44,false,177.36154981808326,0043-1234,4.197012112866403 -Francis,21,true,163.58058955398462,0043-1234,0.1290727333852506 -Charles,23,true,176.4620090811987,+1 1234,1.9565531871692177 -Francis,46,true,173.67591891696253,0043-1234,3.475410636322101 -Francis,43,false,174.1497585797949,+1 1234,2.4334043109864156 -Dylan,35,false,163.3179439068544,+1 1234,4.456175047352048 -Bob,26,false,156.89588160262397,0043-1234,1.7440901131079198 -Bob,25,true,174.02640553149135,+1 1234,2.96007370677206 -Dylan,27,false,163.06869544510883,0043-1234,1.1350907683849853 -Francis,36,true,163.92732636598666,0043-1234,3.837165706004641 -Dylan,22,false,169.22480371321228,0043-1234,3.241646789199385 -Dylan,39,false,159.94975264421598,0043-1234,1.4793931230161022 -Enid,27,true,180.63096161699352,0043-1234,1.731196161419024 -Bob,31,true,180.27010334955744,0043-1234,3.3868422932758686 -Bob,38,false,166.5457493309321,0043-1234,2.5500213541433325 -Francis,37,true,159.4830413538787,0043-1234,1.0761979352612872 -Francis,45,false,150.8110611015065,+1 1234,3.766570934362565 -Enid,29,true,159.5999294435609,+1 1234,3.444842149699685 -Francis,44,true,179.9218787546004,0043-1234,1.0536416152316959 -Dylan,32,true,159.9604202358432,0043-1234,3.6030971831385328 -Francis,45,false,172.2728449812043,+1 1234,3.1972307584450004 -Dylan,23,true,178.38682904606802,+1 1234,1.1139663330328458 -Dylan,43,true,165.91595703550433,0043-1234,1.663690739056932 -Charles,37,true,164.97907400959514,+1 1234,1.059942097552784 -Francis,21,true,152.51438354447447,+1 1234,3.1689985168144226 -Francis,27,true,163.12175423186218,0043-1234,3.8489905588458333 -Charles,25,false,178.02960891566104,0043-1234,4.6216027506403865 -Enid,23,false,166.83457465997702,0043-1234,3.8774566196190325 -Francis,31,false,165.31805063687807,+1 1234,3.4656451539345836 -Bob,43,false,168.21950087249613,+1 1234,1.0603604183311441 -Charles,32,false,177.4090685505179,0043-1234,4.604355730257678 -Enid,34,false,178.72611537672864,0043-1234,1.5270268273472656 -Anne,29,false,166.57616492581164,+1 1234,0.8760047145100074 -Anne,22,false,165.40220309671685,0043-1234,4.8832531600674445 -Francis,46,true,167.74924727046664,0043-1234,1.868036709110598 -Francis,39,false,171.34656089350054,0043-1234,2.839303404514995 -Anne,43,false,171.70729294284888,+1 1234,1.026264394218072 -Enid,27,true,170.97086309150654,0043-1234,2.4077883659503927 -Anne,21,true,155.54580394718937,0043-1234,1.9208241110559379 -Bob,36,true,180.10669124042226,+1 1234,1.9623607711430755 -Anne,21,false,150.30057047559959,0043-1234,1.5292597422560075 -Enid,42,false,155.3581224019092,0043-1234,3.2138244136599496 -Dylan,43,false,167.56206691233936,0043-1234,0.8422819556789753 -Bob,35,false,162.86264479579643,+1 1234,0.5569161225719821 -Enid,33,true,154.0846693644162,+1 1234,1.4359544202248402 -Bob,41,true,172.57865473074597,+1 1234,2.055424188216807 -Enid,26,false,152.15799419687647,+1 1234,4.719488876086064 -Francis,23,false,169.878528671469,0043-1234,2.187081578657277 -Enid,38,false,154.97149898230037,+1 1234,4.359584669131257 -Anne,33,true,174.05165187931777,0043-1234,2.0777051150234804 -Anne,24,false,158.67705046306142,0043-1234,1.9469860232357636 -Anne,35,true,177.64575406781483,0043-1234,2.5616839816914085 -Dylan,21,true,179.70924371031091,0043-1234,4.463147220830667 -Charles,38,false,153.8473652863645,+1 1234,1.8976826379764433 -Anne,26,false,162.19056921456087,+1 1234,2.2791663503653314 -Anne,44,false,168.74112625639967,0043-1234,3.998571722571157 -Anne,20,true,155.2211316180539,0043-1234,3.037807748091206 -Dylan,49,false,163.3226306089724,+1 1234,4.190044181325576 -Enid,31,true,165.3500162565441,+1 1234,1.5201238059560775 -Anne,23,false,180.67216842351866,0043-1234,0.7375719808853958 -Dylan,50,false,159.00118638113378,+1 1234,1.7967453440495245 -Charles,48,false,151.18747081584493,+1 1234,4.6827749333675595 -Charles,38,false,164.73927282624376,+1 1234,1.9827473672350309 -Anne,41,false,154.90888085576222,0043-1234,1.458068273448839 -Bob,31,false,168.7056602769985,0043-1234,4.755019954654738 -Bob,40,true,164.39532455208624,+1 1234,4.7388916895511795 -Francis,43,true,179.21558184516408,+1 1234,4.786612934599447 -Anne,23,true,174.06420167103988,+1 1234,3.441708127758816 -Enid,25,false,152.23513887479515,+1 1234,0.40725044518799813 -Dylan,27,false,162.08722522981748,+1 1234,2.2732454408147174 -Enid,44,false,170.37728837288006,+1 1234,2.5495128532876676 -Francis,25,true,156.60365018377416,+1 1234,1.888663703839749 -Anne,28,false,176.65580422987716,0043-1234,0.5165139300906969 -Charles,43,false,169.77672722144214,+1 1234,2.2958058569771893 -Enid,35,true,180.90442290293274,+1 1234,1.2124514098332435 -Anne,50,false,152.19948439286932,0043-1234,3.628541290339011 +Bob,47,true,151.45953325802017,+1 1234,3.1754137540305685 +Enid,35,true,156.96363408367307,0043-1234,4.654014710057664 +Charles,21,true,176.69455091397938,+1 1234,0.3473955008603544 +Francis,35,false,157.17456348482008,0043-1234,3.1344146354361535 +Charles,33,true,156.21328657509966,+1 1234,3.4517738239740483 +Dylan,26,false,173.8679538956362,0043-1234,3.6635447706498443 +Anne,34,false,164.2608447683769,+1 1234,0.7537754389273914 +Dylan,33,true,157.33586443194918,+1 1234,3.4604562795588367 +Bob,29,false,154.12191803390866,+1 1234,4.486647349988025 +Francis,44,false,179.30615773520552,+1 1234,4.8437908498413105 +Dylan,40,false,156.79165717884584,+1 1234,2.650115801581247 +Dylan,44,false,177.01039410937136,0043-1234,2.923456791793171 +Anne,38,false,178.20599563705383,0043-1234,0.30013253730260714 +Francis,44,true,176.85789228219792,0043-1234,1.047430767785612 +Anne,32,false,155.68527741883187,+1 1234,0.20105922544326926 +Charles,45,false,150.12252665335032,0043-1234,4.344676639131244 +Dylan,43,false,166.2788082136338,0043-1234,1.2383856333312144 +Anne,28,true,171.32966948826453,0043-1234,2.883743445724254 +Francis,47,true,159.31696368618415,+1 1234,1.19261883793782 +Charles,24,true,159.65070640585435,+1 1234,2.7288754892959144 +Charles,45,true,155.98218993237,0043-1234,4.54736885383237 +Bob,44,true,161.0634997283491,+1 1234,2.971902157066029 +Charles,29,false,171.34690740527262,0043-1234,1.4012903775469783 +Enid,40,false,158.3401494817078,+1 1234,1.4876634078235775 +Francis,38,true,173.66119793907285,0043-1234,3.564453978577891 +Dylan,22,false,156.98528041495285,+1 1234,2.974725359960889 +Enid,34,false,167.38570483970952,0043-1234,0.6721973806883978 +Bob,21,false,156.17954353421925,0043-1234,3.8553097636651517 +Francis,45,false,178.20182583943927,0043-1234,2.2544319649657933 +Charles,30,false,164.0968048609711,+1 1234,2.6201242804916007 +Dylan,43,false,160.19389696515137,+1 1234,2.3726278108047953 +Anne,33,true,156.93261540867846,0043-1234,3.9897536411428494 +Enid,20,false,171.42914322664822,0043-1234,2.72133090773373 +Dylan,22,true,168.37220009414105,0043-1234,4.987508423751961 +Charles,34,false,170.9445443977941,+1 1234,1.4439723678299758 +Francis,45,false,161.84893323660913,+1 1234,4.854106925491763 +Bob,45,false,151.80090766153148,+1 1234,3.43332451957512 +Charles,25,false,167.45556215195978,+1 1234,3.3843718562029643 +Enid,21,true,173.6054592648578,0043-1234,4.64159918062864 +Anne,47,false,159.42475988583843,+1 1234,2.487469995725209 +Anne,44,true,160.1895400431365,0043-1234,2.5216068731974755 +Francis,40,false,169.08677799147452,0043-1234,4.275751280701256 +Charles,34,true,175.32180596355994,+1 1234,0.9907103870944589 +Anne,43,true,161.45674694008093,0043-1234,2.8081865017689136 +Charles,43,false,171.2883040317275,+1 1234,1.1742446157241133 +Enid,49,true,155.39319007837577,0043-1234,1.839558617073932 +Francis,38,true,179.35959608419054,+1 1234,3.6117230005024226 +Anne,30,false,164.16341317102953,+1 1234,1.2476441168209382 +Charles,33,false,150.5751792305759,0043-1234,1.2793011280762348 +Charles,43,false,154.42972714337193,0043-1234,2.2794623966023186 +Anne,37,true,179.18563264356342,0043-1234,2.82187125538233 +Bob,42,true,155.83800890030008,0043-1234,0.7743620683671733 +Bob,30,false,176.01007974401946,0043-1234,4.280908551187071 +Bob,38,false,151.5563470177039,+1 1234,4.794970114578856 +Francis,25,false,157.3288992904926,+1 1234,2.608656212152166 +Dylan,20,true,172.71124650981716,+1 1234,1.2709680849811693 +Bob,35,false,155.4850079163832,0043-1234,2.3152581750637937 +Francis,35,true,167.35922231025305,0043-1234,2.7734919199829258 +Bob,44,false,171.51234008620193,0043-1234,0.6616790834760764 +Enid,48,true,177.60048983965945,0043-1234,0.38754882607048047 +Enid,35,false,160.85427573559352,0043-1234,2.3331556099530197 +Charles,50,true,167.05744796443096,0043-1234,0.7132967400131878 +Francis,29,true,162.12672191985797,+1 1234,0.31650421949768837 +Charles,34,true,159.7528183953533,0043-1234,1.1947660331044456 +Charles,33,false,169.3237965323671,+1 1234,4.888513786265316 +Charles,39,false,178.44355079970785,+1 1234,2.0183826124535234 +Bob,44,false,158.45487336757313,0043-1234,2.251648216081265 +Dylan,46,false,165.58999721056313,+1 1234,3.3355068111943007 +Anne,42,true,158.70655188053848,0043-1234,4.421426749330608 +Charles,49,false,158.4407512291543,0043-1234,2.3614456630305733 +Dylan,29,false,151.45147196532412,0043-1234,2.0923131034703504 +Anne,30,true,172.59490165766525,+1 1234,4.861997318570792 +Bob,26,false,174.25013845375165,0043-1234,1.9359385414270311 +Francis,23,true,160.5222808153053,+1 1234,3.4416806110151366 +Francis,42,false,178.84335870452702,+1 1234,2.440289344276025 +Enid,22,true,162.30684934576925,+1 1234,4.853840587063166 +Charles,50,true,168.51348019045034,+1 1234,3.6012951528236563 +Dylan,34,false,155.74485287466356,+1 1234,2.1617473597550108 +Dylan,40,true,180.53831889888474,+1 1234,1.6075604159622656 +Francis,23,true,172.15296152189546,+1 1234,2.495813284957101 +Anne,39,true,179.41801574042452,0043-1234,2.568456093885347 +Bob,30,true,163.48519789953298,0043-1234,2.952322482040201 +Charles,33,false,163.1993796216252,0043-1234,2.5812320871532055 +Francis,30,false,166.3156687188668,+1 1234,0.9077353813017766 +Bob,29,false,164.66204879997798,+1 1234,3.2724623711420806 +Charles,34,false,163.988363537854,0043-1234,4.58050950303847 +Bob,20,false,153.25165056081175,+1 1234,1.0189211371790443 +Enid,49,true,178.42379312588008,+1 1234,3.188040823032764 +Enid,44,false,161.84518728926662,0043-1234,3.4319945588813745 +Enid,24,false,166.84991853352767,0043-1234,0.43218240720984324 +Bob,50,true,165.04813421872913,+1 1234,2.882511445335222 +Anne,32,true,151.98497967762685,0043-1234,2.546661674760527 +Anne,34,false,161.37099655463751,+1 1234,2.259134253389573 +Enid,45,false,158.90725582881538,+1 1234,0.47778753983341593 +Anne,42,false,153.90656352203868,0043-1234,3.9371191883450933 +Charles,46,false,155.0456715448628,+1 1234,2.084386640223091 +Francis,33,false,159.25809781776275,+1 1234,1.4132760683508596 +Dylan,47,true,166.00369832853445,+1 1234,3.201425487808131 +Bob,24,false,175.74828242343494,+1 1234,1.3640237054587827 +Anne,31,true,152.53259748715433,+1 1234,0.2773293315865535 From 98e9c458665c1f2658ba6384cfe1a6a85840a3a7 Mon Sep 17 00:00:00 2001 From: "Clelia (Astra) Bertelli" Date: Sat, 4 Apr 2026 21:54:09 +0200 Subject: [PATCH 2/3] chore: benches for csv-stringify --- __test__/index.spec.ts | 23 +++++ benchmark/bench-alt.ts | 160 +++++++++++++++++++++++++---- package.json | 1 + testfiles/generated-100.csv | 200 ++++++++++++++++++------------------ yarn.lock | 8 ++ 5 files changed, 273 insertions(+), 119 deletions(-) diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 5f442bb..99af54c 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -9,6 +9,10 @@ import { asFloatArray, asIntArray, asStringArray, + toIntColumn, + toFloatColumn, + toStringColumn, + toBoolColumn, } from '../index' test('readCsv reads a CSV and returns a DataFrame with correct datatypes', (t) => { @@ -80,3 +84,22 @@ test('Column to array functions work', (t) => { t.truthy(lastArray) t.deepEqual(lastArray, columns['last_one'].field0) }) + +test('Column conversion functions work correctly', (t) => { + const intArr = [0, 1, 2, 3] + const floatArr = [0.1, 1.2, 2.3, 3.4] + const stringArr = ['hello', 'world', 'how', 'are'] + const boolArr = [true, true, false, false] + const intCol = toIntColumn(intArr) + t.deepEqual(intCol.field0, intArr) + t.is(intCol.type, 'Integer') + const floatCol = toFloatColumn(floatArr) + t.deepEqual(floatCol.field0, floatArr) + t.is(floatCol.type, 'Float') + const stringCol = toStringColumn(stringArr) + t.deepEqual(stringCol.field0, stringArr) + t.is(stringCol.type, 'String') + const boolCol = toBoolColumn(boolArr) + t.deepEqual(boolCol.field0, boolArr) + t.is(boolCol.type, 'Boolean') +}) diff --git a/benchmark/bench-alt.ts b/benchmark/bench-alt.ts index 92d79c6..05e9187 100644 --- a/benchmark/bench-alt.ts +++ b/benchmark/bench-alt.ts @@ -1,28 +1,13 @@ +/* eslint-disable */ + import { Bench } from 'tinybench' import { parse } from 'csv-parse' -import fs from 'node:fs' +import fs, { unlinkSync, writeFileSync } from 'node:fs' import { finished } from 'stream/promises' +import { stringify } from 'csv-stringify/sync' const b = new Bench() -// const processFile = async () => { -// const records = []; -// const parser = fs.createReadStream(`${os.tmpdir()}/input.csv`).pipe( -// parse({ -// // CSV options if any -// }), -// ); -// parser.on("readable", function () { -// let record; -// while ((record = parser.read()) !== null) { -// // Work with each record -// records.push(record); -// } -// }); -// await finished(parser); -// return records; -// }; - b.add('Read a 100-lines CSV', async () => { console.log('Started 100 lines bench') const parser = fs.createReadStream(`testfiles/generated-100.csv`).pipe(parse({ toLine: 100 })) @@ -73,6 +58,143 @@ b.add('Read a 1000000-lines CSV', async () => { console.log('Ended 1.000.000 lines bench') }) +let records_100: any[][] | undefined +let records_1000: any[][] | undefined +let records_100000: any[][] | undefined +let records_1000000: any[][] | undefined + +b.add( + 'Write a 100-lines CSV', + async () => { + console.log('Started writing 100 lines bench') + const output = stringify(records_100!) + writeFileSync('testfiles/written-alt-100.csv', output) + console.log('Ended writing 100 lines bench') + }, + { + beforeAll: () => { + const col1 = new Array(100).fill('something') + const col2 = new Array(100).fill(1.0) + const col3 = new Array(100).fill(3) + const col4 = new Array(100).fill(true) + const cols = [col1, col2, col3, col4] + let i = 0 + const matrix: any[][] = [] + while (i < 100) { + let row = [] + for (const col of cols) { + row.push(col[i]) + } + matrix.push(row) + i++ + } + records_100 = matrix + }, + afterEach: () => { + unlinkSync('testfiles/written-alt-100.csv') + }, + }, +) + +b.add( + 'Write a 1000-lines CSV', + async () => { + console.log('Started writing 1000 lines bench') + const output = stringify(records_1000!) + writeFileSync('testfiles/written-alt-1000.csv', output) + console.log('Ended writing 1000 lines bench') + }, + { + beforeAll: () => { + const col1 = new Array(1000).fill('something') + const col2 = new Array(1000).fill(1.0) + const col3 = new Array(1000).fill(3) + const col4 = new Array(1000).fill(true) + const cols = [col1, col2, col3, col4] + let i = 0 + const matrix: any[][] = [] + while (i < 1000) { + const row = [] + for (const col of cols) { + row.push(col[i]) + } + matrix.push(row) + i++ + } + records_1000 = matrix + }, + afterEach: () => { + unlinkSync('testfiles/written-alt-1000.csv') + }, + }, +) + +b.add( + 'Write a 100.000-lines CSV', + async () => { + console.log('Started writing 100.000 lines bench') + const output = stringify(records_100000!) + writeFileSync('testfiles/written-alt-100000.csv', output) + console.log('Ended writing 100.000 lines bench') + }, + { + beforeAll: () => { + const col1 = new Array(100000).fill('something') + const col2 = new Array(100000).fill(1.0) + const col3 = new Array(100000).fill(3) + const col4 = new Array(100000).fill(true) + const cols = [col1, col2, col3, col4] + let i = 0 + const matrix: any[][] = [] + while (i < 100000) { + const row = [] + for (const col of cols) { + row.push(col[i]) + } + matrix.push(row) + i++ + } + records_100000 = matrix + }, + afterEach: () => { + unlinkSync('testfiles/written-alt-100000.csv') + }, + }, +) + +b.add( + 'Write a 1.000.000-lines CSV', + async () => { + console.log('Started writing 1.000.000 lines bench') + const output = stringify(records_1000000!) + writeFileSync('testfiles/written-alt-1000000.csv', output) + console.log('Ended writing 1.000.000 lines bench') + }, + { + beforeAll: () => { + const col1 = new Array(1000000).fill('something') + const col2 = new Array(1000000).fill(1.0) + const col3 = new Array(1000000).fill(3) + const col4 = new Array(1000000).fill(true) + const cols = [col1, col2, col3, col4] + let i = 0 + const matrix: any[][] = [] + while (i < 1000000) { + const row = [] + for (const col of cols) { + row.push(col[i]) + } + matrix.push(row) + i++ + } + records_1000000 = matrix + }, + afterEach: () => { + unlinkSync('testfiles/written-alt-1000000.csv') + }, + }, +) + await b.run() console.table(b.table()) diff --git a/package.json b/package.json index 5940801..19b05b4 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "ava": "^7.0.0", "chalk": "^5.6.2", "csv-parse": "^6.2.1", + "csv-stringify": "^6.7.0", "husky": "^9.1.7", "lint-staged": "^16.1.6", "npm-run-all2": "^8.0.4", diff --git a/testfiles/generated-100.csv b/testfiles/generated-100.csv index cfd9b5d..ee8d7da 100644 --- a/testfiles/generated-100.csv +++ b/testfiles/generated-100.csv @@ -1,101 +1,101 @@ name,age,is_working,height,phone_number,years_of_experience -Bob,47,true,151.45953325802017,+1 1234,3.1754137540305685 -Enid,35,true,156.96363408367307,0043-1234,4.654014710057664 -Charles,21,true,176.69455091397938,+1 1234,0.3473955008603544 -Francis,35,false,157.17456348482008,0043-1234,3.1344146354361535 -Charles,33,true,156.21328657509966,+1 1234,3.4517738239740483 -Dylan,26,false,173.8679538956362,0043-1234,3.6635447706498443 -Anne,34,false,164.2608447683769,+1 1234,0.7537754389273914 -Dylan,33,true,157.33586443194918,+1 1234,3.4604562795588367 -Bob,29,false,154.12191803390866,+1 1234,4.486647349988025 -Francis,44,false,179.30615773520552,+1 1234,4.8437908498413105 -Dylan,40,false,156.79165717884584,+1 1234,2.650115801581247 -Dylan,44,false,177.01039410937136,0043-1234,2.923456791793171 -Anne,38,false,178.20599563705383,0043-1234,0.30013253730260714 -Francis,44,true,176.85789228219792,0043-1234,1.047430767785612 -Anne,32,false,155.68527741883187,+1 1234,0.20105922544326926 -Charles,45,false,150.12252665335032,0043-1234,4.344676639131244 -Dylan,43,false,166.2788082136338,0043-1234,1.2383856333312144 -Anne,28,true,171.32966948826453,0043-1234,2.883743445724254 -Francis,47,true,159.31696368618415,+1 1234,1.19261883793782 -Charles,24,true,159.65070640585435,+1 1234,2.7288754892959144 -Charles,45,true,155.98218993237,0043-1234,4.54736885383237 -Bob,44,true,161.0634997283491,+1 1234,2.971902157066029 -Charles,29,false,171.34690740527262,0043-1234,1.4012903775469783 -Enid,40,false,158.3401494817078,+1 1234,1.4876634078235775 -Francis,38,true,173.66119793907285,0043-1234,3.564453978577891 -Dylan,22,false,156.98528041495285,+1 1234,2.974725359960889 -Enid,34,false,167.38570483970952,0043-1234,0.6721973806883978 -Bob,21,false,156.17954353421925,0043-1234,3.8553097636651517 -Francis,45,false,178.20182583943927,0043-1234,2.2544319649657933 -Charles,30,false,164.0968048609711,+1 1234,2.6201242804916007 -Dylan,43,false,160.19389696515137,+1 1234,2.3726278108047953 -Anne,33,true,156.93261540867846,0043-1234,3.9897536411428494 -Enid,20,false,171.42914322664822,0043-1234,2.72133090773373 -Dylan,22,true,168.37220009414105,0043-1234,4.987508423751961 -Charles,34,false,170.9445443977941,+1 1234,1.4439723678299758 -Francis,45,false,161.84893323660913,+1 1234,4.854106925491763 -Bob,45,false,151.80090766153148,+1 1234,3.43332451957512 -Charles,25,false,167.45556215195978,+1 1234,3.3843718562029643 -Enid,21,true,173.6054592648578,0043-1234,4.64159918062864 -Anne,47,false,159.42475988583843,+1 1234,2.487469995725209 -Anne,44,true,160.1895400431365,0043-1234,2.5216068731974755 -Francis,40,false,169.08677799147452,0043-1234,4.275751280701256 -Charles,34,true,175.32180596355994,+1 1234,0.9907103870944589 -Anne,43,true,161.45674694008093,0043-1234,2.8081865017689136 -Charles,43,false,171.2883040317275,+1 1234,1.1742446157241133 -Enid,49,true,155.39319007837577,0043-1234,1.839558617073932 -Francis,38,true,179.35959608419054,+1 1234,3.6117230005024226 -Anne,30,false,164.16341317102953,+1 1234,1.2476441168209382 -Charles,33,false,150.5751792305759,0043-1234,1.2793011280762348 -Charles,43,false,154.42972714337193,0043-1234,2.2794623966023186 -Anne,37,true,179.18563264356342,0043-1234,2.82187125538233 -Bob,42,true,155.83800890030008,0043-1234,0.7743620683671733 -Bob,30,false,176.01007974401946,0043-1234,4.280908551187071 -Bob,38,false,151.5563470177039,+1 1234,4.794970114578856 -Francis,25,false,157.3288992904926,+1 1234,2.608656212152166 -Dylan,20,true,172.71124650981716,+1 1234,1.2709680849811693 -Bob,35,false,155.4850079163832,0043-1234,2.3152581750637937 -Francis,35,true,167.35922231025305,0043-1234,2.7734919199829258 -Bob,44,false,171.51234008620193,0043-1234,0.6616790834760764 -Enid,48,true,177.60048983965945,0043-1234,0.38754882607048047 -Enid,35,false,160.85427573559352,0043-1234,2.3331556099530197 -Charles,50,true,167.05744796443096,0043-1234,0.7132967400131878 -Francis,29,true,162.12672191985797,+1 1234,0.31650421949768837 -Charles,34,true,159.7528183953533,0043-1234,1.1947660331044456 -Charles,33,false,169.3237965323671,+1 1234,4.888513786265316 -Charles,39,false,178.44355079970785,+1 1234,2.0183826124535234 -Bob,44,false,158.45487336757313,0043-1234,2.251648216081265 -Dylan,46,false,165.58999721056313,+1 1234,3.3355068111943007 -Anne,42,true,158.70655188053848,0043-1234,4.421426749330608 -Charles,49,false,158.4407512291543,0043-1234,2.3614456630305733 -Dylan,29,false,151.45147196532412,0043-1234,2.0923131034703504 -Anne,30,true,172.59490165766525,+1 1234,4.861997318570792 -Bob,26,false,174.25013845375165,0043-1234,1.9359385414270311 -Francis,23,true,160.5222808153053,+1 1234,3.4416806110151366 -Francis,42,false,178.84335870452702,+1 1234,2.440289344276025 -Enid,22,true,162.30684934576925,+1 1234,4.853840587063166 -Charles,50,true,168.51348019045034,+1 1234,3.6012951528236563 -Dylan,34,false,155.74485287466356,+1 1234,2.1617473597550108 -Dylan,40,true,180.53831889888474,+1 1234,1.6075604159622656 -Francis,23,true,172.15296152189546,+1 1234,2.495813284957101 -Anne,39,true,179.41801574042452,0043-1234,2.568456093885347 -Bob,30,true,163.48519789953298,0043-1234,2.952322482040201 -Charles,33,false,163.1993796216252,0043-1234,2.5812320871532055 -Francis,30,false,166.3156687188668,+1 1234,0.9077353813017766 -Bob,29,false,164.66204879997798,+1 1234,3.2724623711420806 -Charles,34,false,163.988363537854,0043-1234,4.58050950303847 -Bob,20,false,153.25165056081175,+1 1234,1.0189211371790443 -Enid,49,true,178.42379312588008,+1 1234,3.188040823032764 -Enid,44,false,161.84518728926662,0043-1234,3.4319945588813745 -Enid,24,false,166.84991853352767,0043-1234,0.43218240720984324 -Bob,50,true,165.04813421872913,+1 1234,2.882511445335222 -Anne,32,true,151.98497967762685,0043-1234,2.546661674760527 -Anne,34,false,161.37099655463751,+1 1234,2.259134253389573 -Enid,45,false,158.90725582881538,+1 1234,0.47778753983341593 -Anne,42,false,153.90656352203868,0043-1234,3.9371191883450933 -Charles,46,false,155.0456715448628,+1 1234,2.084386640223091 -Francis,33,false,159.25809781776275,+1 1234,1.4132760683508596 -Dylan,47,true,166.00369832853445,+1 1234,3.201425487808131 -Bob,24,false,175.74828242343494,+1 1234,1.3640237054587827 -Anne,31,true,152.53259748715433,+1 1234,0.2773293315865535 +Dylan,45,false,177.2937353500214,+1 1234,3.7395089769984757 +Anne,29,true,161.43632513042851,+1 1234,0.36354362226719594 +Enid,45,true,169.26614382734923,+1 1234,4.2164240040716425 +Charles,34,true,164.7544307613378,0043-1234,3.939167269692997 +Anne,28,false,164.27186559954328,0043-1234,3.839467788131323 +Charles,22,false,156.89122287871254,+1 1234,2.1053501674828636 +Francis,47,false,180.1522783668909,+1 1234,2.953252586378926 +Dylan,39,false,170.16585663529074,0043-1234,3.351852404327512 +Anne,48,true,177.01298914183735,0043-1234,2.2181216686039233 +Francis,34,true,177.88949138630483,0043-1234,2.4997660878334007 +Bob,29,true,159.70867124708442,+1 1234,1.6396974755188196 +Dylan,36,true,153.73302821256667,+1 1234,1.5295004562438872 +Enid,46,false,158.87341412984307,0043-1234,4.3126801415737965 +Charles,36,false,178.95300693394546,+1 1234,3.710321498779153 +Francis,48,false,169.95955747477666,0043-1234,4.12969563091761 +Charles,28,false,157.62970087430813,+1 1234,2.1601409432170415 +Dylan,33,true,166.91110338212283,0043-1234,1.9600887684213986 +Anne,45,false,175.93327175539662,+1 1234,2.589232422762504 +Bob,41,true,154.1023489575612,+1 1234,4.0355601408003965 +Francis,49,true,162.5716054403943,+1 1234,2.661137320479308 +Charles,35,false,173.1348796842183,0043-1234,4.885151236296737 +Enid,38,false,165.6949344576567,0043-1234,3.9011587382926107 +Dylan,28,false,179.44231748350433,+1 1234,3.9964294460437246 +Charles,44,false,151.3649441961405,+1 1234,3.1525987096065142 +Anne,32,true,152.2625478182052,0043-1234,1.3707633603574176 +Dylan,29,true,155.28535319179414,0043-1234,0.5854756508752057 +Charles,30,false,151.97194189601123,0043-1234,3.6476858594934254 +Dylan,42,true,158.12144349439083,0043-1234,3.30685208996677 +Enid,22,false,156.45979773689683,0043-1234,1.755551245375105 +Dylan,21,true,160.428467408276,0043-1234,3.4783373486708324 +Bob,48,false,165.12211853586453,+1 1234,1.2774730114831847 +Dylan,38,false,150.31152839532768,+1 1234,4.281663297291281 +Francis,41,false,157.40137141444785,+1 1234,3.893970068912335 +Anne,26,true,177.82250228101697,0043-1234,3.278551647168227 +Anne,22,false,154.53178756594167,0043-1234,2.2604370191900394 +Dylan,42,false,169.8258765382244,0043-1234,4.331307823331232 +Anne,44,false,154.15463855071386,+1 1234,1.67723611063542 +Enid,49,false,157.2957814826104,0043-1234,4.085845611798819 +Dylan,21,true,175.55982377196557,+1 1234,2.1365490937015057 +Charles,32,false,176.7741132620447,+1 1234,3.21489136636146 +Charles,36,false,162.8615895897245,+1 1234,0.395513920423757 +Bob,21,false,174.3620985231938,+1 1234,4.954330167572601 +Charles,30,true,173.27221024700728,0043-1234,3.6836957527486036 +Bob,42,false,163.31765212369828,0043-1234,2.424331643375403 +Francis,30,false,176.05818599637183,+1 1234,1.153032160989889 +Charles,49,true,159.4323710027746,+1 1234,2.557083495978189 +Enid,39,false,165.36366002675086,0043-1234,0.2984133096061794 +Anne,27,false,155.8000606207464,0043-1234,3.531672450770336 +Bob,26,true,172.3935102951727,+1 1234,1.023576438214909 +Anne,37,true,174.77577712670893,0043-1234,2.835577806636306 +Anne,24,false,155.65487822863184,0043-1234,0.3187930791112752 +Anne,40,false,162.12562316177522,+1 1234,1.7350639735149092 +Anne,21,true,150.71402348535784,0043-1234,2.3248785179166696 +Enid,30,true,153.4571823993667,0043-1234,4.9941399111298495 +Enid,35,true,175.36210449296527,+1 1234,3.2126966762649767 +Enid,41,true,150.49374365866456,0043-1234,3.8882847463826886 +Anne,48,false,156.5789744153986,0043-1234,2.078716385500277 +Francis,30,false,155.94894399355468,0043-1234,2.2453235939805225 +Enid,21,true,161.79570461973267,+1 1234,0.9857979565399588 +Bob,38,false,178.13654463298386,+1 1234,0.3458473792726764 +Enid,44,false,155.8156336358623,0043-1234,3.7846589905054135 +Bob,21,true,151.11171882251037,+1 1234,4.64440002550114 +Dylan,24,true,179.94582846709443,+1 1234,1.946670334360043 +Enid,41,false,172.46086158083503,0043-1234,1.991021907080526 +Charles,27,false,173.10753438669792,0043-1234,3.4357662603475596 +Enid,46,true,156.56942999427383,+1 1234,3.6203074539365874 +Dylan,34,false,163.40120828036834,0043-1234,1.614560100765366 +Enid,21,true,162.39142861148989,0043-1234,3.985190937354127 +Charles,21,true,154.77041889806156,0043-1234,3.5550704246136293 +Francis,31,false,169.3900781863792,+1 1234,2.1458874610164953 +Bob,37,true,175.66086505776408,+1 1234,1.3627143016094636 +Dylan,46,false,173.36399058760173,0043-1234,0.6530939777745282 +Dylan,46,false,157.5918226857898,+1 1234,3.6675576486977484 +Dylan,38,true,176.31043529921456,+1 1234,0.34960041537135056 +Francis,34,false,171.72354332864336,0043-1234,0.06831696744434446 +Charles,35,true,180.4209774466616,0043-1234,1.8118930487751266 +Francis,32,false,155.49344764462552,+1 1234,3.158533178625648 +Dylan,23,false,159.53635452666364,0043-1234,4.865488260045666 +Enid,25,true,153.90874615398994,0043-1234,1.150558527487478 +Bob,21,false,166.10523329179568,+1 1234,1.6362970447982579 +Charles,39,true,150.24816244580705,+1 1234,1.992102537033306 +Charles,45,false,166.61768885418022,+1 1234,2.3148612852415056 +Anne,47,false,169.22367556246775,+1 1234,1.8401685572931412 +Anne,34,true,170.79235543511984,+1 1234,2.9546698637553597 +Enid,30,true,170.9480940778325,+1 1234,2.3324592910836954 +Anne,27,false,171.47189294772073,+1 1234,2.4043924963788843 +Enid,42,false,169.7445980827772,0043-1234,3.0476170017747477 +Francis,28,true,152.20792061854058,+1 1234,1.616278383146097 +Charles,21,false,162.13680963574595,+1 1234,4.286301454151409 +Anne,33,true,160.76877550655146,+1 1234,4.926745846160673 +Charles,38,false,167.7313123915221,+1 1234,0.608997473198645 +Anne,27,true,158.33979250322193,+1 1234,4.122684083167064 +Bob,50,false,168.95793020629773,+1 1234,3.185683892095637 +Charles,46,true,165.090628769717,0043-1234,0.6157331539810396 +Charles,20,true,177.8550453997302,+1 1234,1.6341388760814843 +Anne,20,false,155.77016345207093,+1 1234,1.7272457955097102 +Francis,49,false,162.05707436134603,+1 1234,0.8409368550023206 +Anne,30,false,171.7690343975572,0043-1234,1.5643851065176424 +Bob,25,true,177.0414046373274,0043-1234,3.1217867887313524 +Charles,24,true,153.39598959081485,+1 1234,2.5078217842666755 diff --git a/yarn.lock b/yarn.lock index 301f211..949950c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,7 @@ __metadata: ava: "npm:^7.0.0" chalk: "npm:^5.6.2" csv-parse: "npm:^6.2.1" + csv-stringify: "npm:^6.7.0" husky: "npm:^9.1.7" lint-staged: "npm:^16.1.6" npm-run-all2: "npm:^8.0.4" @@ -1840,6 +1841,13 @@ __metadata: languageName: node linkType: hard +"csv-stringify@npm:^6.7.0": + version: 6.7.0 + resolution: "csv-stringify@npm:6.7.0" + checksum: 10c0/2f1272e0462c31935650b42f34cb5dafbbc9a3b479074283ec4ed6491756c791d2d6230226c530198d85002d4d3f3b69da14b1f424ab93ad3263124ea25e5ea3 + languageName: node + linkType: hard + "currently-unhandled@npm:^0.4.1": version: 0.4.1 resolution: "currently-unhandled@npm:0.4.1" From 8be984d20ded84b9da531935c4efe43efab4294e Mon Sep 17 00:00:00 2001 From: "Clelia (Astra) Bertelli" Date: Sun, 5 Apr 2026 18:43:22 +0200 Subject: [PATCH 3/3] chore: tests, error handling improvements, readme up-to-date --- .gitignore | 1 + LICENSE | 2 +- README.md | 105 +++++++++++++---- __test__/index.spec.ts | 90 ++++++++++++++- src/lib.rs | 66 ++++++----- testfiles/comparative_bench.py | 0 testfiles/err-typed-col-1.csv | 3 + testfiles/err-typed-col-2.csv | 3 + testfiles/generated-100.csv | 200 ++++++++++++++++----------------- tsconfig.json | 3 +- 10 files changed, 321 insertions(+), 152 deletions(-) mode change 100644 => 100755 testfiles/comparative_bench.py create mode 100644 testfiles/err-typed-col-1.csv create mode 100644 testfiles/err-typed-col-2.csv diff --git a/.gitignore b/.gitignore index d2fea82..0ec9a24 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,4 @@ Cargo.lock # test files testfiles/*.csv !testfiles/generated-100.csv +!testfiles/err-*-col*.csv diff --git a/LICENSE b/LICENSE index 6c7f562..1817579 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 N-API for Rust +Copyright (c) 2026 Clelia Astra Bertelli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8092a69..649ee13 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ npm install @cle-does-things/sunbears ## Usage +### `readCsv` + The main function for `sunbears` is `readCsv`, which loads the data contained in a CSV file as a `DataFrame`, a columnar data format. ```typescript @@ -24,6 +26,7 @@ The `DataFrame` class exposes two methods: - `colDtype`: retrieve the data type of the records contained within a column (integer, float, boolean or string) - `get`: get a column +- `writeCsv`: write the dataframe to CSV (see the next paragraph) ```typescript const dt = df.colDtype('name') @@ -61,36 +64,88 @@ const filteredNames = asStringArray(readCsv('test.csv').get('name'))?.filter((n) const mappedNames = asStringArray(readCsv('test.csv').get('name'))?.map((n) => n.toUpperCase()) ``` -## Benchmarking +### `DataFrame.writeCsv` -`sunbears` was benchmarked using the `tinybench`-based script you can find [here](./benchmark/bench.ts). The script reports latency statistics related to the `readCsv` function reading increasingly large CSV files (100, 1000, 100.000 and 1.000.000 rows). +The `writeCsv` method writes a DataFrame to CSV. -The latest benchmark run was: +You can construct a DataFrame simply starting from arrays, using the following helper functions: -| Task | Latency avg (s) | Latency med (s) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | -| ------------------------ | ---------------- | -------------------- | ---------------------- | ---------------------- | ------- | -| Read a 100-lines CSV | 0.000073 ± 0.20% | 0.000073 ± 0.0000015 | 13814 ± 0.11% | 13841 ± 285 | 13727 | -| Read a 1000-lines CSV | 0.000423 ± 0.47% | 0.000412 ± 0.000010 | 2383 ± 0.31% | 2427 ± 60 | 2362 | -| Read a 100000-lines CSV | 0.041591 ± 2.07% | 0.040895 ± 0.001126 | 24 ± 1.86% | 24 ± 1 | 64 | -| Read a 1000000-lines CSV | 0.424404 ± 0.73% | 0.421294 ± 0.008276 | 2 ± 0.70% | 2 ± 0 | 64 | +```typescript +import { DataFrame, toIntColumn, toFloatColumn, toStringColumn, toBoolColumn } from '@cle-does-things/sunbears' -Here is how the tool compares to the `read_csv` function in Pandas and Polars ([script](./testfiles/comparative_bench.py)): +const col1 = toStringColumn(['hello', 'world']) +const col2 = toFloatColumn([1.2, 2.3]) +const col3 = toIntColumn([4, 5]) +const col4 = toBoolColumn([true, false]) +``` -| Dataset | Pandas (s) | Polars (s) | -| ------------- | ---------- | ---------- | -| 100 lines | 0.030637 | 0.022748 | -| 1000 lines | 0.033386 | 0.017368 | -| 100000 lines | 0.453596 | 0.027075 | -| 1000000 lines | 3.986837 | 0.198708 | +You can then use the `fromColumns` factory for the `DataFrame` class to turn column data into a DataFrame: if the columns do not have the same length, an error will be thrown. -And here it how it compares with `csv-parse` ([script](./benchmark/bench-alt.ts)): +```typescript +const df = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, +}) +``` + +Writing to the CSV file is then trivial: + +```typescript +df.writeCsv('test.csv') +``` -| Task | Latency avg (s) | Latency med (s) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | -| -------------------------- | ---------------- | ------------------- | ---------------------- | ---------------------- | ------- | -| Read a 100-lines CSV | 0.000188 ± 0.98% | 0.000179 ± 0.000003 | 5,442 ± 0.23% | 5,580 ± 94 | 5,312 | -| Read a 1,000-lines CSV | 0.001235 ± 0.69% | 0.001189 ± 0.000032 | 816 ± 0.58% | 841 ± 23 | 810 | -| Read a 100,000-lines CSV | 0.11656 ± 0.26% | 0.11648 ± 0.000642 | 9 ± 0.25% | 9 ± 0 | 64 | -| Read a 1,000,000-lines CSV | 1.19114 ± 0.37% | 1.18707 ± 0.007191 | 1 ± 0.36% | 1 ± 0 | 64 | +The file will look like this: + +```csv +col1,col2,col3,col4 +hello,1.2,4,true +world,2.3,5,false +``` + +## Benchmarking + +`sunbears` was benchmarked using the `tinybench`-based script you can find [here](./benchmark/bench.ts). The script reports latency statistics related to the `readCsv` and `writeCsv` functions reading/writing increasingly large CSV files (100, 1000, 100.000 and 1.000.000 rows). + +The latest benchmark run was: + +| Task | Latency avg (s) | Latency med (s) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | +| ------------------------- | ---------------- | -------------------- | ---------------------- | ---------------------- | ------- | +| Read a 100-lines CSV | 0.000054 ± 0.24% | 0.000050 ± 0.0000023 | 18964 ± 0.16% | 19967 ± 921 | 18654 | +| Read a 1000-lines CSV | 0.000289 ± 0.54% | 0.000279 ± 0.0000090 | 3518 ± 0.32% | 3583 ± 116 | 3464 | +| Read a 100000-lines CSV | 0.028000 ± 1.38% | 0.027537 ± 0.000254 | 36 ± 1.19% | 36 ± 0 | 64 | +| Read a 1000000-lines CSV | 0.310751 ± 0.75% | 0.308330 ± 0.004228 | 3 ± 0.70% | 3 ± 0 | 64 | +| Write a 100-lines CSV | 0.000076 ± 0.46% | 0.000069 ± 0.0000052 | 13665 ± 0.28% | 14467 ± 1148 | 13140 | +| Write a 1000-lines CSV | 0.000213 ± 0.43% | 0.000209 ± 0.0000056 | 4724 ± 0.16% | 4785 ± 130 | 4700 | +| Write a 100000-lines CSV | 0.013886 ± 0.86% | 0.013756 ± 0.000171 | 72 ± 0.78% | 73 ± 1 | 73 | +| Write a 1000000-lines CSV | 0.146282 ± 1.39% | 0.146108 ± 0.005752 | 7 ± 1.31% | 7 ± 0 | 64 | + +Here is how the tool compares to the `read_csv` and `to_csv`/`write_csv` functions in Pandas and Polars ([script](./testfiles/comparative_bench.py)): + +| Dataset | Pandas (s) | Polars (s) | +| ------------------- | ---------- | ---------- | +| Read 100 lines | 0.038291 | 0.033831 | +| Read 1000 lines | 0.037794 | 0.016517 | +| Read 100000 lines | 0.471109 | 0.029076 | +| Read 1000000 lines | 4.153507 | 0.216254 | +| Write 100 lines | 0.035926 | 0.043052 | +| Write 1000 lines | 0.067816 | 0.017617 | +| Write 100000 lines | 0.892885 | 0.031329 | +| Write 1000000 lines | 8.549390 | 0.331897 | + +And here it how it compares with `csv-parse` and `csv-stringify`+`writeFileSync` ([script](./benchmark/bench-alt.ts)): + +| Task | Latency avg (s) | Latency med (s) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | +| ------------------------- | ---------------- | -------------------- | ---------------------- | ---------------------- | ------- | +| Read a 100-lines CSV | 0.000207 ± 2.31% | 0.000191 ± 0.0000087 | 5086 ± 0.31% | 5224 ± 244 | 4842 | +| Read a 1000-lines CSV | 0.001244 ± 0.42% | 0.001233 ± 0.0000229 | 806 ± 0.33% | 811 ± 15 | 805 | +| Read a 100000-lines CSV | 0.120565 ± 0.63% | 0.119515 ± 0.001141 | 8 ± 0.60% | 8 ± 0 | 64 | +| Read a 1000000-lines CSV | 1.216019 ± 0.46% | 1.209978 ± 0.006709 | 1 ± 0.44% | 1 ± 0 | 64 | +| Write a 100-lines CSV | 0.000087 ± 0.52% | 0.000080 ± 0.0000078 | 12010 ± 0.31% | 12526 ± 1267 | 11503 | +| Write a 1000-lines CSV | 0.000290 ± 1.14% | 0.000275 ± 0.0000192 | 3555 ± 0.42% | 3635 ± 258 | 3451 | +| Write a 100000-lines CSV | 0.027303 ± 2.96% | 0.027014 ± 0.000900 | 37 ± 1.87% | 37 ± 1 | 64 | +| Write a 1000000-lines CSV | 0.273814 ± 2.08% | 0.265154 ± 0.005824 | 4 ± 1.77% | 4 ± 0 | 64 | ## Development @@ -176,3 +231,7 @@ git push GitHub actions will do the rest job for you. > WARN: Don't run `npm publish` manually. + +## License + +MIT diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 99af54c..3f62df5 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -14,6 +14,7 @@ import { toStringColumn, toBoolColumn, } from '../index' +import { unlinkSync } from 'fs' test('readCsv reads a CSV and returns a DataFrame with correct datatypes', (t) => { const csvPath = 'testfiles/generated-100.csv' @@ -40,6 +41,23 @@ test('readCsv reads a CSV and returns a DataFrame with correct datatypes', (t) = t.truthy(df.get('years_of_experience')) }) +test('readCsv throws expected type-dependent errors', (t) => { + t.throws( + () => { + readCsv('testfiles/err-typed-col-1.csv') + }, + undefined, + 'Expecting integer at line 2 for column colErr', + ) + t.throws( + () => { + readCsv('testfiles/err-typed-col-2.csv') + }, + undefined, + 'Expecting integer at line 2 for column colErr', + ) +}) + test('DataFrame class methods work correctly', (t) => { const columns: Record = { test: { type: 'String', field0: ['hello', 'world'] }, @@ -60,7 +78,7 @@ test('DataFrame class methods work correctly', (t) => { t.deepEqual(df.get('last_one'), columns['last_one']) }) -test('Column to array functions work', (t) => { +test('Column to array functions work correctly', (t) => { const columns: Record = { name: { type: 'String', field0: ['hello', 'world'] }, other: { type: 'Boolean', field0: [false, true] }, @@ -85,7 +103,7 @@ test('Column to array functions work', (t) => { t.deepEqual(lastArray, columns['last_one'].field0) }) -test('Column conversion functions work correctly', (t) => { +test('Array to column functions work correctly', (t) => { const intArr = [0, 1, 2, 3] const floatArr = [0.1, 1.2, 2.3, 3.4] const stringArr = ['hello', 'world', 'how', 'are'] @@ -103,3 +121,71 @@ test('Column conversion functions work correctly', (t) => { t.deepEqual(boolCol.field0, boolArr) t.is(boolCol.type, 'Boolean') }) + +test('DataFrame constructor and factory work correctly', (t) => { + const columns: Record = { + name: { type: 'String', field0: ['hello', 'world'] }, + other: { type: 'Boolean', field0: [false, true] }, + another: { type: 'Integer', field0: [1, 2] }, + last_one: { type: 'Float', field0: [0.3, 2.6] }, + } + const df = new DataFrame(columns, 2) + t.is(df.len, 2) + for (const k of Object.keys(columns)) { + t.deepEqual(df.get(k), columns[k]) + } + const df1 = DataFrame.fromColumns(columns) + t.is(df1.len, 2) + for (const k of Object.keys(columns)) { + t.deepEqual(df1.get(k), columns[k]) + } +}) + +test('DataFrame constructor and factory throw expected errors', (t) => { + const columns: Record = { + name: { type: 'String', field0: ['hello', 'world'] }, + other: { type: 'Boolean', field0: [false, true] }, + another: { type: 'Integer', field0: [1, 2] }, + last_one: { type: 'Float', field0: [0.3, 2.6] }, + } + const columns1: Record = { + name: { type: 'String', field0: ['hello'] }, + other: { type: 'Boolean', field0: [false, true] }, + another: { type: 'Integer', field0: [1, 2] }, + last_one: { type: 'Float', field0: [0.3, 2.6] }, + } + t.throws( + () => { + new DataFrame(columns, 3) + }, + undefined, + 'Not all columns are of the declared length', + ) + t.throws( + () => { + DataFrame.fromColumns(columns1) + }, + undefined, + 'Not all columns are the same length', + ) +}) + +test('Write to CSV works correctly', (t) => { + const col1 = toStringColumn(['hello', 'world']) + const col2 = toFloatColumn([1.2, 2.3]) + const col3 = toIntColumn([4, 5]) + const col4 = toBoolColumn([true, false]) + const dfw = DataFrame.fromColumns({ + col1: col1, + col2: col2, + col3: col3, + col4: col4, + }) + dfw.writeCsv('testfiles/write-test.csv') + const dfr = readCsv('testfiles/write-test.csv') + const colw = dfw.columns + for (const k of Object.keys(colw)) { + t.deepEqual(dfr.get(k), colw[k]) + } + unlinkSync('testfiles/write-test.csv') +}) diff --git a/src/lib.rs b/src/lib.rs index 1859d70..3e7c43c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ impl DataFrame { #[napi(constructor)] pub fn new(columns: HashMap, len: u32) -> Result { if !columns.iter().all(|(_, c)| c.len() as u32 == len) { - return Err(anyhow!("No all columns are the same length")); + return Err(anyhow!("Not all columns are of the declared length")); } Ok(Self { columns, len }) } @@ -128,7 +128,7 @@ impl DataFrame { let col = &columns[keys[0]]; let l = col.len(); if !columns.iter().all(|(_, c)| c.len() == l) { - return Err(anyhow!("No all columns are the same length")); + return Err(anyhow!("Not all columns are the same length")); } Ok(Self { @@ -265,24 +265,35 @@ pub fn read_csv(path: String) -> Result { let vc = &col_to_vec[*c]; let data: ColumnData = match d { DataType::Boolean => { - let v = vc - .iter() - .enumerate() - .map(|(i, s)| { - str_to_bool(s).unwrap_or_else(|| panic!("Expecting bool at line {:?} for col {}", i, c)) - }) - .collect(); + let mut v: Vec = vec![]; + for el in vc { + match str_to_bool(el) { + Some(val) => v.push(val), + None => { + return Err(anyhow!( + "Expecting bool at line {:?} for col {}", + i + 1, + col_idx[*c] + )) + } + } + } ColumnData::Boolean(v) } DataType::Float => { - let v = vc - .iter() - .enumerate() - .map(|(i, s)| { - s.parse::() - .unwrap_or_else(|_| panic!("Expecting float at line {:?} for col {}", i, c)) - }) - .collect(); + let mut v: Vec = vec![]; + for s in vc { + match s.parse::() { + Ok(val) => v.push(val), + Err(_) => { + return Err(anyhow!( + "Expecting float at line {:?} for col {}", + i + 1, + col_idx[*c] + )) + } + } + } ColumnData::Float(v) } @@ -291,14 +302,19 @@ pub fn read_csv(path: String) -> Result { ColumnData::String(v) } DataType::Integer => { - let v = vc - .iter() - .enumerate() - .map(|(i, s)| { - s.parse::() - .unwrap_or_else(|_| panic!("Expecting integer at line {:?} for col {}", i, c)) - }) - .collect(); + let mut v: Vec = vec![]; + for s in vc { + match s.parse::() { + Ok(val) => v.push(val), + Err(_) => { + return Err(anyhow!( + "Expecting integer at line {:?} for col {}", + i + 1, + col_idx[*c] + )) + } + } + } ColumnData::Integer(v) } diff --git a/testfiles/comparative_bench.py b/testfiles/comparative_bench.py old mode 100644 new mode 100755 diff --git a/testfiles/err-typed-col-1.csv b/testfiles/err-typed-col-1.csv new file mode 100644 index 0000000..8909933 --- /dev/null +++ b/testfiles/err-typed-col-1.csv @@ -0,0 +1,3 @@ +col1,col2,colErr +1,true,1 +2,false,2.3 diff --git a/testfiles/err-typed-col-2.csv b/testfiles/err-typed-col-2.csv new file mode 100644 index 0000000..ecebe36 --- /dev/null +++ b/testfiles/err-typed-col-2.csv @@ -0,0 +1,3 @@ +col1,col2,colErr +1,true,12345678 +2,false,+1 345-6-78 diff --git a/testfiles/generated-100.csv b/testfiles/generated-100.csv index ee8d7da..ea61a40 100644 --- a/testfiles/generated-100.csv +++ b/testfiles/generated-100.csv @@ -1,101 +1,101 @@ name,age,is_working,height,phone_number,years_of_experience -Dylan,45,false,177.2937353500214,+1 1234,3.7395089769984757 -Anne,29,true,161.43632513042851,+1 1234,0.36354362226719594 -Enid,45,true,169.26614382734923,+1 1234,4.2164240040716425 -Charles,34,true,164.7544307613378,0043-1234,3.939167269692997 -Anne,28,false,164.27186559954328,0043-1234,3.839467788131323 -Charles,22,false,156.89122287871254,+1 1234,2.1053501674828636 -Francis,47,false,180.1522783668909,+1 1234,2.953252586378926 -Dylan,39,false,170.16585663529074,0043-1234,3.351852404327512 -Anne,48,true,177.01298914183735,0043-1234,2.2181216686039233 -Francis,34,true,177.88949138630483,0043-1234,2.4997660878334007 -Bob,29,true,159.70867124708442,+1 1234,1.6396974755188196 -Dylan,36,true,153.73302821256667,+1 1234,1.5295004562438872 -Enid,46,false,158.87341412984307,0043-1234,4.3126801415737965 -Charles,36,false,178.95300693394546,+1 1234,3.710321498779153 -Francis,48,false,169.95955747477666,0043-1234,4.12969563091761 -Charles,28,false,157.62970087430813,+1 1234,2.1601409432170415 -Dylan,33,true,166.91110338212283,0043-1234,1.9600887684213986 -Anne,45,false,175.93327175539662,+1 1234,2.589232422762504 -Bob,41,true,154.1023489575612,+1 1234,4.0355601408003965 -Francis,49,true,162.5716054403943,+1 1234,2.661137320479308 -Charles,35,false,173.1348796842183,0043-1234,4.885151236296737 -Enid,38,false,165.6949344576567,0043-1234,3.9011587382926107 -Dylan,28,false,179.44231748350433,+1 1234,3.9964294460437246 -Charles,44,false,151.3649441961405,+1 1234,3.1525987096065142 -Anne,32,true,152.2625478182052,0043-1234,1.3707633603574176 -Dylan,29,true,155.28535319179414,0043-1234,0.5854756508752057 -Charles,30,false,151.97194189601123,0043-1234,3.6476858594934254 -Dylan,42,true,158.12144349439083,0043-1234,3.30685208996677 -Enid,22,false,156.45979773689683,0043-1234,1.755551245375105 -Dylan,21,true,160.428467408276,0043-1234,3.4783373486708324 -Bob,48,false,165.12211853586453,+1 1234,1.2774730114831847 -Dylan,38,false,150.31152839532768,+1 1234,4.281663297291281 -Francis,41,false,157.40137141444785,+1 1234,3.893970068912335 -Anne,26,true,177.82250228101697,0043-1234,3.278551647168227 -Anne,22,false,154.53178756594167,0043-1234,2.2604370191900394 -Dylan,42,false,169.8258765382244,0043-1234,4.331307823331232 -Anne,44,false,154.15463855071386,+1 1234,1.67723611063542 -Enid,49,false,157.2957814826104,0043-1234,4.085845611798819 -Dylan,21,true,175.55982377196557,+1 1234,2.1365490937015057 -Charles,32,false,176.7741132620447,+1 1234,3.21489136636146 -Charles,36,false,162.8615895897245,+1 1234,0.395513920423757 -Bob,21,false,174.3620985231938,+1 1234,4.954330167572601 -Charles,30,true,173.27221024700728,0043-1234,3.6836957527486036 -Bob,42,false,163.31765212369828,0043-1234,2.424331643375403 -Francis,30,false,176.05818599637183,+1 1234,1.153032160989889 -Charles,49,true,159.4323710027746,+1 1234,2.557083495978189 -Enid,39,false,165.36366002675086,0043-1234,0.2984133096061794 -Anne,27,false,155.8000606207464,0043-1234,3.531672450770336 -Bob,26,true,172.3935102951727,+1 1234,1.023576438214909 -Anne,37,true,174.77577712670893,0043-1234,2.835577806636306 -Anne,24,false,155.65487822863184,0043-1234,0.3187930791112752 -Anne,40,false,162.12562316177522,+1 1234,1.7350639735149092 -Anne,21,true,150.71402348535784,0043-1234,2.3248785179166696 -Enid,30,true,153.4571823993667,0043-1234,4.9941399111298495 -Enid,35,true,175.36210449296527,+1 1234,3.2126966762649767 -Enid,41,true,150.49374365866456,0043-1234,3.8882847463826886 -Anne,48,false,156.5789744153986,0043-1234,2.078716385500277 -Francis,30,false,155.94894399355468,0043-1234,2.2453235939805225 -Enid,21,true,161.79570461973267,+1 1234,0.9857979565399588 -Bob,38,false,178.13654463298386,+1 1234,0.3458473792726764 -Enid,44,false,155.8156336358623,0043-1234,3.7846589905054135 -Bob,21,true,151.11171882251037,+1 1234,4.64440002550114 -Dylan,24,true,179.94582846709443,+1 1234,1.946670334360043 -Enid,41,false,172.46086158083503,0043-1234,1.991021907080526 -Charles,27,false,173.10753438669792,0043-1234,3.4357662603475596 -Enid,46,true,156.56942999427383,+1 1234,3.6203074539365874 -Dylan,34,false,163.40120828036834,0043-1234,1.614560100765366 -Enid,21,true,162.39142861148989,0043-1234,3.985190937354127 -Charles,21,true,154.77041889806156,0043-1234,3.5550704246136293 -Francis,31,false,169.3900781863792,+1 1234,2.1458874610164953 -Bob,37,true,175.66086505776408,+1 1234,1.3627143016094636 -Dylan,46,false,173.36399058760173,0043-1234,0.6530939777745282 -Dylan,46,false,157.5918226857898,+1 1234,3.6675576486977484 -Dylan,38,true,176.31043529921456,+1 1234,0.34960041537135056 -Francis,34,false,171.72354332864336,0043-1234,0.06831696744434446 -Charles,35,true,180.4209774466616,0043-1234,1.8118930487751266 -Francis,32,false,155.49344764462552,+1 1234,3.158533178625648 -Dylan,23,false,159.53635452666364,0043-1234,4.865488260045666 -Enid,25,true,153.90874615398994,0043-1234,1.150558527487478 -Bob,21,false,166.10523329179568,+1 1234,1.6362970447982579 -Charles,39,true,150.24816244580705,+1 1234,1.992102537033306 -Charles,45,false,166.61768885418022,+1 1234,2.3148612852415056 -Anne,47,false,169.22367556246775,+1 1234,1.8401685572931412 -Anne,34,true,170.79235543511984,+1 1234,2.9546698637553597 -Enid,30,true,170.9480940778325,+1 1234,2.3324592910836954 -Anne,27,false,171.47189294772073,+1 1234,2.4043924963788843 -Enid,42,false,169.7445980827772,0043-1234,3.0476170017747477 -Francis,28,true,152.20792061854058,+1 1234,1.616278383146097 -Charles,21,false,162.13680963574595,+1 1234,4.286301454151409 -Anne,33,true,160.76877550655146,+1 1234,4.926745846160673 -Charles,38,false,167.7313123915221,+1 1234,0.608997473198645 -Anne,27,true,158.33979250322193,+1 1234,4.122684083167064 -Bob,50,false,168.95793020629773,+1 1234,3.185683892095637 -Charles,46,true,165.090628769717,0043-1234,0.6157331539810396 -Charles,20,true,177.8550453997302,+1 1234,1.6341388760814843 -Anne,20,false,155.77016345207093,+1 1234,1.7272457955097102 -Francis,49,false,162.05707436134603,+1 1234,0.8409368550023206 -Anne,30,false,171.7690343975572,0043-1234,1.5643851065176424 -Bob,25,true,177.0414046373274,0043-1234,3.1217867887313524 -Charles,24,true,153.39598959081485,+1 1234,2.5078217842666755 +Anne,24,false,172.06195385839587,+1 1234,3.767044082274923 +Enid,36,true,164.17546867223956,+1 1234,3.9987730641281765 +Francis,45,true,167.66954681406762,0043-1234,0.4383413027655949 +Enid,34,true,161.30458950930952,+1 1234,1.0983771829465512 +Charles,25,false,151.67913999785412,0043-1234,2.434259647739991 +Dylan,31,true,161.16597974594205,0043-1234,3.276539501458718 +Charles,43,false,153.78110220498175,+1 1234,4.134995277849777 +Charles,25,true,166.40188604691568,0043-1234,3.4263596384326083 +Bob,23,false,158.38593329361092,0043-1234,1.0320864141187804 +Bob,22,false,174.30421224335603,0043-1234,0.30139282444920124 +Charles,25,false,167.36986172098074,+1 1234,3.6070863321158333 +Bob,47,true,155.77584934965734,+1 1234,0.3358753495974762 +Anne,47,true,152.07245609048525,0043-1234,3.978476753629136 +Anne,35,false,163.8284506948372,0043-1234,4.7375086847218375 +Anne,49,true,180.1237482982292,+1 1234,1.4734837130167757 +Francis,31,false,171.39476207206496,+1 1234,2.057482207163191 +Charles,20,false,150.85891722659494,+1 1234,1.8076413190188547 +Charles,26,true,157.73556835468887,0043-1234,3.2390834201030048 +Bob,28,false,180.22280208552263,0043-1234,0.11382435925677648 +Francis,50,true,162.39526970619806,+1 1234,0.8249470790353828 +Charles,39,true,154.01212102360162,+1 1234,1.6846034197031907 +Francis,27,true,173.5486689216849,+1 1234,3.9985312398191337 +Francis,23,false,177.5849194905182,0043-1234,2.6625711310679345 +Francis,20,true,176.23107677217902,+1 1234,2.36102935780334 +Francis,34,true,180.7011235918709,0043-1234,1.482859764730454 +Anne,28,true,172.2611271479767,+1 1234,0.8026755462524306 +Francis,35,true,163.02339898361976,+1 1234,0.13368333645175268 +Dylan,21,false,164.56067064798725,+1 1234,1.1321786041675934 +Anne,41,true,179.442405598062,0043-1234,2.4910230451061053 +Dylan,40,true,169.0704375228679,+1 1234,3.767070661054649 +Bob,45,false,158.52554354403676,0043-1234,3.389188917679733 +Anne,49,true,178.3749017440682,+1 1234,1.2717571652842483 +Francis,42,false,179.89635183986815,+1 1234,0.7228721084532994 +Francis,50,false,162.16345519279804,0043-1234,2.1467121727357386 +Anne,25,false,151.85714506267925,0043-1234,2.0106703515331454 +Enid,43,true,154.44252150760803,0043-1234,0.10012231117163695 +Francis,33,false,169.75224887442954,0043-1234,2.1155764577654237 +Dylan,21,true,163.42614142253774,+1 1234,4.1636259380519896 +Charles,39,false,177.19248309034617,0043-1234,0.7043319312506218 +Dylan,27,false,163.78268872264073,0043-1234,0.7933675094289101 +Enid,22,true,151.69475029878475,0043-1234,3.427832466709869 +Anne,24,true,167.0624790612009,0043-1234,3.208757157964802 +Enid,22,true,161.48780558514278,+1 1234,1.8090922295976357 +Francis,49,true,161.18349998788094,+1 1234,1.0264327081269466 +Enid,30,true,155.8820687964176,0043-1234,1.0565103129209166 +Anne,50,false,163.58681127771385,0043-1234,2.834026981707148 +Enid,25,true,165.3165808966044,0043-1234,2.907435266125858 +Dylan,33,true,178.4105287916339,0043-1234,3.1364447532695845 +Francis,24,false,153.3751836951103,0043-1234,3.573562444922654 +Francis,33,true,175.3551671500021,0043-1234,0.8777011127810928 +Bob,28,false,167.79756345857308,0043-1234,2.5037076919560795 +Dylan,31,true,151.22287551814932,+1 1234,3.909823087240767 +Anne,24,true,159.32157999459298,0043-1234,3.304525677644712 +Dylan,45,false,179.506794367215,0043-1234,1.94228733904784 +Enid,47,false,153.09902751239227,0043-1234,1.2257610300558768 +Enid,24,false,155.81295425867603,0043-1234,2.5198447985382337 +Dylan,35,true,177.8206091922323,0043-1234,3.11915384637018 +Charles,36,false,169.68986298923735,+1 1234,2.040447345083472 +Bob,26,true,175.03218264042204,+1 1234,0.969361969266481 +Bob,33,false,172.40702314693513,0043-1234,3.0529996726430175 +Dylan,21,true,180.24598488538015,+1 1234,2.4544469347421005 +Charles,47,false,152.5271909335372,+1 1234,4.862982108147397 +Bob,31,true,174.82341433179184,+1 1234,4.772525888375782 +Dylan,47,false,159.68297409447837,0043-1234,2.910096160363199 +Dylan,26,false,163.5897866306247,0043-1234,4.5364911850626335 +Dylan,23,true,150.732788122647,0043-1234,4.315366231031446 +Enid,23,false,171.53990655114706,+1 1234,0.5693450705816476 +Enid,48,false,163.53419005719897,0043-1234,1.2921866236585322 +Francis,47,false,151.76618196211368,+1 1234,2.3720314158733604 +Charles,27,true,159.52354929775623,+1 1234,4.867960488247686 +Enid,49,false,162.074604287669,0043-1234,3.7767032840851327 +Bob,40,true,180.6973555225904,+1 1234,4.976864922452478 +Dylan,43,true,173.8940011972604,+1 1234,3.840024105102519 +Enid,35,false,170.53003190168505,0043-1234,3.146912731530201 +Charles,45,false,175.9191299427927,0043-1234,0.13449729759980267 +Francis,37,false,178.81323593962227,+1 1234,1.8046065918879781 +Francis,37,true,171.64751648501152,0043-1234,1.2121177903237317 +Dylan,21,true,174.23500345515865,0043-1234,2.40193633389855 +Enid,23,true,166.64177130219286,0043-1234,1.3485286041527245 +Enid,28,true,167.84237767602943,+1 1234,3.5604680454430335 +Anne,47,false,173.3829667919652,0043-1234,3.4355520047255492 +Charles,42,true,172.32056905611879,0043-1234,4.572668974525715 +Francis,26,true,153.87122280420334,0043-1234,1.1098493391586837 +Bob,38,false,154.80156174298156,+1 1234,1.09022613940166 +Charles,30,true,175.78649302190107,+1 1234,2.8155094550286215 +Anne,24,false,173.6683115337848,0043-1234,3.411235438552893 +Anne,47,true,164.74234862130373,+1 1234,4.395743541956674 +Charles,39,true,160.344588323495,0043-1234,4.617831663085374 +Anne,41,false,154.21935376553893,0043-1234,3.8765168318277285 +Enid,38,true,161.36130856960816,0043-1234,4.292390362269066 +Dylan,22,true,151.97403458363888,0043-1234,1.511052542912881 +Charles,30,true,160.8150715590866,+1 1234,3.4227094005312617 +Francis,36,false,153.92000405422075,0043-1234,2.57570905818981 +Francis,49,true,162.03322009490762,+1 1234,1.3991670356908592 +Enid,39,false,180.7453190324777,0043-1234,4.170054416280345 +Dylan,30,true,157.385569900398,0043-1234,4.456103378715245 +Enid,24,false,151.93522138656664,0043-1234,3.456877765620685 +Charles,31,false,178.33999739259212,+1 1234,4.708568405839408 +Anne,50,false,151.1441208606709,0043-1234,3.0190218516256966 +Anne,20,false,180.56604162487267,+1 1234,2.47731278312586 diff --git a/tsconfig.json b/tsconfig.json index 5056788..df7d9d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["node"] }, "include": ["."], "exclude": ["node_modules", "bench", "__test__"]