diff --git a/.gitignore b/.gitignore index 0affb12..7d56b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .direnv .coverage +samples/*.csv +samples/*.db +samples_regenerated.cfwf diff --git a/README.md b/README.md index e80be2b..c78829c 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,86 @@ # cfwf -## TODO cfwf project initialisation - -Enable **Read and write permissions** on the -[Github action workflow permission](https://github.com/badele/cfwf/settings/actions) -(for pushing the release and changelog) - -## Included with this project - -- nix/flake - reproducible, declarative and reliable developpement systems -- pre-commit -- cocogitto - conventional commits and auto versioning - -## Git workflow - -- `nix develop` or automatically loaded with `direnv` tool -- Conventional commits - `cog feat "message" scope` - - pre-commit hook - - markdownlint - markdown linter - - nixpkgs-fmt - nix linter -- github - - CI - - conventional commits - - lint - - test - - coverage - - Manually releasing a new version - [release action](https://github.com/badele/cfwf/actions/workflows/Release.yml) +## Introduction + +**cfwf** is a library designed to facilitate the conversion of tables or +dictionaries into a human-readable format compatible with a simple text file +reader. + +### Project Motivation + +This project was initiated due to a dissatisfaction with the CSV file format, +which is often considered unreadable without the use of dedicated tools. From my +past experiences handling telematic files such as Minitel or BBS, sought to +develop a solution that would provide better readability and usability for +tabular data. + +## Features + +- **Conversion of Single or Multiple Tables:** The cfwf library enables the + conversion of one or more tables into a single CFWF dataset file. + +- **Commentary and Metadata Inclusion:** Posibility to add comments and metadata + to the entire CFWF file, also for each table + +## Example + +```text + ___ _____ ______ _____ + / _ \ |_ _|| ___ \ |_ _| + / /_\ \ | | | |_/ / | | ___ _ _ _ __ + | _ | | | | __/ | | / _ \ | | | || '__| + | | | | | | | | | | | (_) || |_| || | + \_| |_/ \_/ \_| \_/ \___/ \__,_||_| + + ┈┈┈ +Here is a list of the best tennis players in the ATP rankings +from 2012 to 2022, as well as the list of winners of the +4 major Grand Slam tournaments. + ┈┈┈ + +players +The best players (number of winning matches) beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +winner_ioc name_first name_last hand height birth nbwins +────────── ────────── ───────── ──── ────── ───── ────── +SRB Novak Djokovic R 188 1987 64 +ESP Rafael Nadal L 185 1986 46 +SUI Roger Federer R 185 1981 33 +GBR Andy Murray R 190 1987 25 +GER Alexander Zverev R 198 1997 19 +AUT Dominic Thiem R 185 1993 17 +RUS Daniil Medvedev R 198 1996 16 +ESP David Ferrer R 175 1982 16 +CRO Marin Cilic R 198 1988 14 +RUS Andrey Rublev R 188 1997 13 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The Australian Open is a tennis tournament held annually at Melbourne Park +in Melbourne, Victoria, Australia. The tournament is the first of the +four Grand Slam tennis events held each year. + +australian_open +Australian Open winners beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +year tourney name birth nat winner +──── ─────────────── ───────── ────────────── +2022 Australian Open ESP Rafael Nadal +2021 Australian Open SRB Novak Djokovic +2020 Australian Open SRB Novak Djokovic +2019 Australian Open SRB Novak Djokovic +2018 Australian Open SUI Roger Federer +2017 Australian Open SUI Roger Federer +2016 Australian Open SRB Novak Djokovic +2015 Australian Open SRB Novak Djokovic +2014 Australian Open SUI Stan Wawrinka +2013 Australian Open SRB Novak Djokovic +2012 Australian Open SRB Novak Djokovic +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## Sample dataset + +The samples dataset provided by the +[JeffSackmann ATP tennis project](https://github.com/JeffSackmann/tennis_atp) diff --git a/deps.ts b/deps.ts index e75ed2b..0c61580 100644 --- a/deps.ts +++ b/deps.ts @@ -1,5 +1,6 @@ // Add your dependencies in here import * as modfmt from "https://deno.land/std@0.204.0/fmt/printf.ts"; +import * as modyaml from "https://deno.land/std@0.204.0/yaml/mod.ts"; import filget, { text } from "https://deno.land/x/deno_figlet@1.0.0/mod.ts"; -export { filget, modfmt, text }; +export { filget, modfmt, modyaml, text }; diff --git a/flake.nix b/flake.nix index 5375899..4ad5984 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,8 @@ deno lcov + + sqlite ]; shellHook = '' export PROJ="cfwf" diff --git a/justfile b/justfile index 718793d..8db89e7 100644 --- a/justfile +++ b/justfile @@ -11,7 +11,8 @@ BROWSER := "chromium" cog check # Execute test tasks -@test: +@test: generate-samples + deno run -A samples/scripts/generate_samples.ts rm -rf ./.coverage deno test --doc --unstable --allow-all --parallel --coverage=./.coverage --trace-ops @@ -32,6 +33,11 @@ coverage-browse browser="chromium": coverage genhtml -o ./.coverage/html_cov ./.coverage/cov.lcov {{ browser }} ./.coverage/html_cov/index.html +@import-atp: + ./samples/scripts/import-atp.sh + +@generate-samples: + deno run -A samples/scripts/generate_samples.ts # Run command interactively, view the result in realtime @view: diff --git a/mod.ts b/mod.ts index d7b9d8b..74f9042 100644 --- a/mod.ts +++ b/mod.ts @@ -1 +1,5 @@ -export { Align, arrayToCFWF } from "./src/cfwf_writer.ts"; +export * from "./src/cfwf.ts"; +export * from "./src/table.ts"; +export * from "./src/cfwf_reader.ts"; +export * from "./src/cfwf_writer.ts"; +export * from "./src/types.ts"; diff --git a/sample.cfwf b/sample.cfwf deleted file mode 100644 index 9f2add4..0000000 --- a/sample.cfwf +++ /dev/null @@ -1,20 +0,0 @@ - - _____ _ - / __ \ | | - | / \/ _ __ _ _ _ __ | |_ ___ ___ _ _ _ __ _ __ ___ _ __ ___ _ _ - | | | '__|| | | || '_ \ | __| / _ \ / __|| | | || '__|| '__| / _ \| '_ \ / __|| | | | - | \__/\| | | |_| || |_) || |_ | (_) || (__ | |_| || | | | | __/| | | || (__ | |_| | - \____/|_| \__, || .__/ \__| \___/ \___| \__,_||_| |_| \___||_| |_| \___| \__, | - __/ || | __/ | - |___/ |_| |___/ - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -date provider ticker title price cur trend value trend percent -────────── ──────── ───────── ───────────────────────────────────────────────── ────────────── ─── ────────────── ───────────── -2023-11-04 yahoo ADA-EUR Cardano EUR 0.30382475 EUR 0.0089045465 3.02 -2023-11-04 yahoo BTC-EUR Bitcoin EUR 32382.29300000 EUR 523.8554700000 1.64 -2023-11-04 yahoo DOGE-EUR Dogecoin EUR 0.06409328 EUR 0.0016048551 2.57 -2023-11-04 yahoo ETH-EUR Ethereum EUR 1715.40040000 EUR 50.5688480000 3.04 -2023-11-04 yahoo MATIC-EUR Polygon EUR 0.62594900 EUR 0.0191679000 3.16 -2023-11-04 yahoo 500.PA Amundi Index Solutions - Amundi S&P 500 UCITS ETF 78.79000000 EUR 0.3099975600 0.40 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \ No newline at end of file diff --git a/samples.cfwf b/samples.cfwf new file mode 100644 index 0000000..619d0a3 --- /dev/null +++ b/samples.cfwf @@ -0,0 +1,146 @@ + + ___ _____ ______ _____ + / _ \ |_ _|| ___ \ |_ _| + / /_\ \ | | | |_/ / | | ___ _ _ _ __ + | _ | | | | __/ | | / _ \ | | | || '__| + | | | | | | | | | | | (_) || |_| || | + \_| |_/ \_/ \_| \_/ \___/ \__,_||_| + + ┈┈┈ +Here is a list of the best tennis players in the ATP rankings +from 2012 to 2022, as well as the list of winners of the +4 major Grand Slam tournaments. + ┈┈┈ + +players +The best players (number of winning matches) beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +winner_ioc name_first name_last hand height birth nbwins +────────── ────────── ───────── ──── ────── ───── ────── +SRB Novak Djokovic R 188 1987 64 +ESP Rafael Nadal L 185 1986 46 +SUI Roger Federer R 185 1981 33 +GBR Andy Murray R 190 1987 25 +GER Alexander Zverev R 198 1997 19 +AUT Dominic Thiem R 185 1993 17 +RUS Daniil Medvedev R 198 1996 16 +ESP David Ferrer R 175 1982 16 +CRO Marin Cilic R 198 1988 14 +RUS Andrey Rublev R 188 1997 13 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The Australian Open is a tennis tournament held annually at Melbourne Park +in Melbourne, Victoria, Australia. The tournament is the first of the +four Grand Slam tennis events held each year. + +australian_open +Australian Open winners beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +year tourney name birth nat winner +──── ─────────────── ───────── ────────────── +2022 Australian Open ESP Rafael Nadal +2021 Australian Open SRB Novak Djokovic +2020 Australian Open SRB Novak Djokovic +2019 Australian Open SRB Novak Djokovic +2018 Australian Open SUI Roger Federer +2017 Australian Open SUI Roger Federer +2016 Australian Open SRB Novak Djokovic +2015 Australian Open SRB Novak Djokovic +2014 Australian Open SUI Stan Wawrinka +2013 Australian Open SRB Novak Djokovic +2012 Australian Open SRB Novak Djokovic +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The major tennis tournament held over two weeks at the Stade Roland Garros +in Paris, France, beginning in late May each year. The tournament and venue are +named after the French aviator Roland Garros. The French Open is the premier +clay court championship in the world and the only Grand Slam tournament +currently held on this surface. + +roland_garros +Roland Garros winners beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +year tourney name birth nat winner +──── ───────────── ───────── ────────────── +2022 Roland Garros ESP Rafael Nadal +2021 Roland Garros SRB Novak Djokovic +2020 Roland Garros ESP Rafael Nadal +2019 Roland Garros ESP Rafael Nadal +2018 Roland Garros ESP Rafael Nadal +2017 Roland Garros ESP Rafael Nadal +2016 Roland Garros SRB Novak Djokovic +2015 Roland Garros SUI Stan Wawrinka +2014 Roland Garros ESP Rafael Nadal +2013 Roland Garros ESP Rafael Nadal +2012 Roland Garros ESP Rafael Nadal +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The US Open Tennis Championships, commonly called the US Open, is a hardcourt tennis +tournament held annually in Queens, New York. Since 1987, the US Open has been +chronologically the fourth and final Grand Slam tournament of the year. + +us_open +US Open winners beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +year tourney name birth nat winner +──── ──────────── ───────── ─────────────── +2022 Us Open ESP Carlos Alcaraz +2021 Us Open RUS Daniil Medvedev +2020 Us Open AUT Dominic Thiem +2019 US Open ESP Rafael Nadal +2018 US Open SRB Novak Djokovic +2017 US Open ESP Rafael Nadal +2016 US Open SUI Stan Wawrinka +2015 US Open SRB Novak Djokovic +2014 US Open CRO Marin Cilic +2013 US Open ESP Rafael Nadal +2012 US Open GBR Andy Murray +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The Championships, commonly known simply as Wimbledon, is the oldest tennis tournament +in the world and is regarded by many as the most prestigious. +It has been held at the All England Lawn Tennis and Croquet Club in Wimbledon, London, since 1877 + +wimbledon +wimbledon winners beetween 2012-2022 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +year tourney name birth nat winner +──── ──────────── ───────── ────────────── +2022 Wimbledon SRB Novak Djokovic +2021 Wimbledon SRB Novak Djokovic +2019 Wimbledon SRB Novak Djokovic +2018 Wimbledon SRB Novak Djokovic +2017 Wimbledon SUI Roger Federer +2016 Wimbledon GBR Andy Murray +2015 Wimbledon SRB Novak Djokovic +2014 Wimbledon SRB Novak Djokovic +2013 Wimbledon GBR Andy Murray +2012 Wimbledon SUI Roger Federer +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +_infos_: + font: doom + generated_with: 'Generated with https://github.com/badele/cfwf@0.0.1' + removetitlelines: 2 + sources: ['https://github.com/JeffSackmann/tennis_atp'] + title: ' ATP Tour' +australian_open: + aligns: [right, left, center, center] + sources: ['https://fr.wikipedia.org/wiki/Open_d%27Australie', 'https://github.com/JeffSackmann/tennis_atp'] +players: + aligns: [left, right, left, center, right, right, right] + sources: 'https://github.com/JeffSackmann/tennis_atp' +roland_garros: + aligns: [right, left, center, center] + sources: ['https://fr.wikipedia.org/wiki/Internationaux_de_France_de_tennis', 'https://github.com/JeffSackmann/tennis_atp'] +us_open: + aligns: [right, left, center, center] + sources: ['https://fr.wikipedia.org/wiki/US_Open_de_tennis', 'https://github.com/JeffSackmann/tennis_atp'] +wimbledon: + aligns: [right, left, center, center] + sources: ['https://fr.wikipedia.org/wiki/Tournoi_de_Wimbledon', 'https://github.com/JeffSackmann/tennis_atp'] diff --git a/samples/scripts/generate_samples.ts b/samples/scripts/generate_samples.ts new file mode 100644 index 0000000..5beae9c --- /dev/null +++ b/samples/scripts/generate_samples.ts @@ -0,0 +1,116 @@ +import * as modcsv from "https://deno.land/std@0.204.0/csv/mod.ts"; +import { CFWF } from "../../src/cfwf.ts"; +import { parseCSV } from "../../src/utils.ts"; + +// deno-lint-ignore no-explicit-any +const metas: any = { + title: " ATP Tour", + comment: "Here is a list of the best tennis players in the ATP rankings\n\ +from 2012 to 2022, as well as the list of winners of the\n\ +4 major Grand Slam tournaments.", + sources: [ + "https://github.com/JeffSackmann/tennis_atp", + ], +}; + +const samples = new CFWF(metas); + +const players_txt = Deno.readTextFileSync("samples/players.csv"); +const players = parseCSV(players_txt); + +const australian_txt = Deno.readTextFileSync("samples/australian_open.csv"); +const australian = parseCSV(australian_txt); + +const rgarros_txt = Deno.readTextFileSync("samples/roland_garros.csv"); +const rgarros = parseCSV(rgarros_txt); + +const usopen_txt = Deno.readTextFileSync("samples/us_open.csv"); +const usopen = parseCSV(usopen_txt); + +const wimbledon_txt = Deno.readTextFileSync("samples/wimbledon.csv"); +const wimbledon = parseCSV(wimbledon_txt); + +samples.addArray( + "players", + "The best players (number of winning matches) beetween 2012-2022 ", + "", + players.columns, + players.values, + { + aligns: ["left", "right", "left", "center", "right", "right", "right"], + sources: "https://github.com/JeffSackmann/tennis_atp", + }, +); + +samples.addArray( + "australian_open", + "Australian Open winners beetween 2012-2022", +"The Australian Open is a tennis tournament held annually at Melbourne Park\n\ +in Melbourne, Victoria, Australia. The tournament is the first of the \n\ +four Grand Slam tennis events held each year.", + ["year", "tourney name", "birth nat", "winner"], + australian.values, + { + aligns: ["right", "left", "center", "center"], + sources: [ + "https://fr.wikipedia.org/wiki/Open_d%27Australie", + "https://github.com/JeffSackmann/tennis_atp", + ], + }, +); + +samples.addArray( + "roland_garros", + "Roland Garros winners beetween 2012-2022", +"The major tennis tournament held over two weeks at the Stade Roland Garros \n\ +in Paris, France, beginning in late May each year. The tournament and venue are\n\ +named after the French aviator Roland Garros. The French Open is the premier\n\ +clay court championship in the world and the only Grand Slam tournament\n\ +currently held on this surface.", + ["year", "tourney name", "birth nat", "winner"], + rgarros.values, + { + aligns: ["right", "left", "center", "center"], + sources: [ + "https://fr.wikipedia.org/wiki/Internationaux_de_France_de_tennis", + "https://github.com/JeffSackmann/tennis_atp", + ], + }, +); + +samples.addArray( + "us_open", + "US Open winners beetween 2012-2022", +"The US Open Tennis Championships, commonly called the US Open, is a hardcourt tennis\n\ +tournament held annually in Queens, New York. Since 1987, the US Open has been \n\ +chronologically the fourth and final Grand Slam tournament of the year.", + ["year", "tourney name", "birth nat", "winner"], + usopen.values, + { + aligns: ["right", "left", "center", "center"], + sources: [ + "https://fr.wikipedia.org/wiki/US_Open_de_tennis", + "https://github.com/JeffSackmann/tennis_atp", + ], + }, +); + +samples.addArray( + "wimbledon", + "wimbledon winners beetween 2012-2022", +"The Championships, commonly known simply as Wimbledon, is the oldest tennis tournament\n\ +in the world and is regarded by many as the most prestigious.\n\ +It has been held at the All England Lawn Tennis and Croquet Club in Wimbledon, London, since 1877", + ["year", "tourney name", "birth nat", "winner"], + wimbledon.values, + { + aligns: ["right", "left", "center", "center"], + sources: [ + "https://fr.wikipedia.org/wiki/Tournoi_de_Wimbledon", + "https://github.com/JeffSackmann/tennis_atp", + ], + }, +); + +const content = await samples.toCFWF({}); +Deno.writeTextFileSync("samples.cfwf", content); diff --git a/samples/scripts/import-atp.sh b/samples/scripts/import-atp.sh new file mode 100755 index 0000000..119bef2 --- /dev/null +++ b/samples/scripts/import-atp.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# SRCDATAS="/home/badele/ghq/github.com/JeffSackmann/tennis_atp" + +rm -f samples/samples.db +sqlite3 -bail samples/samples.db < samples/scripts/import-atp.sql diff --git a/samples/scripts/import-atp.sql b/samples/scripts/import-atp.sql new file mode 100644 index 0000000..6e58637 --- /dev/null +++ b/samples/scripts/import-atp.sql @@ -0,0 +1,75 @@ +.head on +.nullvalue NULL + +-- PRAGMA synchronous = OFF; + +-- BEGIN TRANSACTION; + +.read './samples/scripts/init_schema.sql' + +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_players.csv' players + +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_rankings_00s.csv' rankings +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_rankings_10s.csv' rankings +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_rankings_20s.csv' rankings + +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2022.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2021.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2020.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2019.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2018.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2017.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2016.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2015.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2014.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2013.csv' matches +.import --csv '/home/badele/ghq/github.com/JeffSackmann/tennis_atp/atp_matches_2012.csv' matches + +-- SELECT ranking_date,substr(ranking_date,1,4) as year,ioc,name_first,name_last,points +-- FROM atp_rankings r INNER JOIN atp_players p ON (r.player=p.player_id) +-- WHERE rank=1 AND ranking_date>=20000101 +-- GROUP BY year HAVING ranking_date=max(ranking_date) +-- ORDER by ranking_date DESC +-- LIMIT 11; + +.separator "," + +--Top players +.output samples/players.csv +SELECT winner_ioc, p.name_first, p.name_last, hand, height, substr(dob,1,4) as birth,count(*) as nbwins from matches m +INNER JOIN players p ON (m.winner_id=p.player_id) +WHERE round="F" +GROUP BY winner_id +ORDER BY count(*) DESC +LIMIT 10; + +-- Australian Open +.output samples/australian_open.csv +SELECT substr(tourney_id,1,4) as year, tourney_name, winner_ioc, winner_name from matches +WHERE tourney_name = "Australian Open" AND round="F" +ORDER BY tourney_id desc +LIMIT 11; + +-- Roland Garros +.output samples/roland_garros.csv +SELECT substr(tourney_id,1,4) as year, tourney_name, winner_ioc, winner_name from matches +WHERE tourney_name = "Roland Garros" AND round="F" +ORDER BY tourney_id desc +LIMIT 11; + +-- Wimbledon +.output samples/wimbledon.csv +SELECT substr(tourney_id,1,4) as year, tourney_name, winner_ioc, winner_name from matches +WHERE tourney_name = "Wimbledon" AND round="F" +ORDER BY tourney_id desc +LIMIT 11; + +-- Us Open +.output samples/us_open.csv +SELECT substr(tourney_id,1,4) as year, tourney_name, winner_ioc, winner_name from matches +WHERE (tourney_name = "Us Open" OR tourney_name = "US Open") AND round="F" +ORDER BY tourney_id desc +LIMIT 11; + + + diff --git a/samples/scripts/init_schema.sql b/samples/scripts/init_schema.sql new file mode 100644 index 0000000..501be13 --- /dev/null +++ b/samples/scripts/init_schema.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS "players"( +"player_id" TEXT, "name_first" TEXT, "name_last" TEXT, "hand" TEXT, + "dob" TEXT, "ioc" TEXT, "height" TEXT, "wikidata_id" TEXT); + +CREATE TABLE IF NOT EXISTS "rankings"( +"ranking_date" TEXT, "rank" TEXT, "player" TEXT, "points" TEXT); + +CREATE TABLE IF NOT EXISTS "matches"( +"tourney_id" TEXT, "tourney_name" TEXT, "surface" TEXT, "draw_size" TEXT, + "tourney_level" TEXT, "tourney_date" TEXT, "match_num" INTEGER, "winner_id" TEXT, + "winner_seed" TEXT, "winner_entry" TEXT, "winner_name" TEXT, "winner_hand" TEXT, + "winner_ht" TEXT, "winner_ioc" TEXT, "winner_age" TEXT, "loser_id" TEXT, + "loser_seed" TEXT, "loser_entry" TEXT, "loser_name" TEXT, "loser_hand" TEXT, + "loser_ht" TEXT, "loser_ioc" TEXT, "loser_age" TEXT, "score" TEXT, + "best_of" TEXT, "round" TEXT, "minutes" TEXT, "w_ace" TEXT, + "w_df" TEXT, "w_svpt" TEXT, "w_1stIn" TEXT, "w_1stWon" TEXT, + "w_2ndWon" TEXT, "w_SvGms" TEXT, "w_bpSaved" TEXT, "w_bpFaced" TEXT, + "l_ace" TEXT, "l_df" TEXT, "l_svpt" TEXT, "l_1stIn" TEXT, + "l_1stWon" TEXT, "l_2ndWon" TEXT, "l_SvGms" TEXT, "l_bpSaved" TEXT, + "l_bpFaced" TEXT, "winner_rank" TEXT, "winner_rank_points" TEXT, "loser_rank" TEXT, + "loser_rank_points" TEXT); diff --git a/src/cfwf.ts b/src/cfwf.ts new file mode 100644 index 0000000..159aef3 --- /dev/null +++ b/src/cfwf.ts @@ -0,0 +1,384 @@ +import { Align, readerOptions, writerOptions } from "./types.ts"; +import { AvailableFonts } from "https://deno.land/x/deno_figlet@1.0.0/src/types.ts"; +import { Table } from "./table.ts"; +import { align, getMaxWidth, max, searchMarker } from "./utils.ts"; +import { modfmt, modyaml, text } from "../deps.ts"; +import { version } from "./version.ts"; + +const footertitle = "Generated with https://github.com/badele/cfwf"; + +export class CFWF { + // deno-lint-ignore no-explicit-any + infos: any; + tables: Record; + + // Title + generatedtitle: string[]; + maxwidthtitle: number; + + // deno-lint-ignore no-explicit-any + constructor(infos: any) { + if (!infos.font && infos.title) infos.font = "doom"; + // if (infos.generatedtitle) delete infos.title; + // if (!infos.title && !infos.generatedtitle) infos.title = "VIDE"; + if (!infos.comment) infos.comment = ""; + if (!infos.removetitlelines) infos.removetitlelines = 2; + + this.infos = infos; + this.tables = {}; + + // Title + this.generatedtitle = []; + this.maxwidthtitle = 0; + } + + async _generateTitle(): Promise { + const txttitle = await text( + this.infos.title, + this.infos.font as AvailableFonts, + ); + const ageneratedtitle = txttitle.split("\n").slice( + 0, + -this.infos.removetitlelines, + ); + const maxwidthtitle = getMaxWidth(ageneratedtitle); + + this.generatedtitle = ageneratedtitle; + this.maxwidthtitle = maxwidthtitle; + } + + addArray( + tablename: string, + subtitle: string, + comment: string, + columns: string[], + // deno-lint-ignore no-explicit-any + rows: any[], + // deno-lint-ignore no-explicit-any + infos: any, + ): void { + this.tables[tablename] = new Table( + tablename, + subtitle, + comment, + columns, + rows, + infos, + ); + } + + fromCFWF(content: string, { + charseparator = "┈", + chartabletop = "━", + chartablemiddle = "─", + chartablebottom = "━", + }: readerOptions): void { + let lastmarkerpos = 0; + let tabletopmarkerpos = 0; + let tablebottommarkerpos = 0; + let tabletablenamepos = 0; + let tablesubtitlepos = 0; + + // deno-lint-ignore no-explicit-any + let fromYAML: any = {}; + const tables: Table[] = []; + + const lines = content.split("\n"); + + // Search title and comment + const titlemarkerpos = searchMarker(lines, charseparator); + const commentmarkerpos = searchMarker( + lines, + charseparator, + titlemarkerpos + 1, + ); + lastmarkerpos = commentmarkerpos + 1; + + let endfile = false; + while (!endfile) { + const table: Table = { + tablename: "", + subtitle: "", + comment: "", + columns: [], + // aligns: [], + rows: [], + metas: {}, + }; + + tabletopmarkerpos = searchMarker(lines, chartabletop, lastmarkerpos); + if (tabletopmarkerpos > -1) { + // search table + tablebottommarkerpos = searchMarker( + lines, + chartablebottom, + tabletopmarkerpos + 1, + ); + + tabletablenamepos = tabletopmarkerpos - 3; + tablesubtitlepos = tabletopmarkerpos - 2; + + table.tablename = lines[tabletablenamepos]; + table.subtitle = lines[tablesubtitlepos]; + table.comment = lines.slice(lastmarkerpos + 1, tabletablenamepos - 1) + .join("\n"); + + const headerline = lines[tabletopmarkerpos + 1]; + const widthline = lines[tabletopmarkerpos + 2]; + + const beginspace = widthline.indexOf(" "); + const endspace = widthline.indexOf(chartablemiddle, beginspace); + const paddingsize = endspace - beginspace; + const headerseps = widthline.split(" ".repeat(paddingsize)); + + const columns = []; + let beginpos = 0; + const headersizes = []; + for (let idx = 0; idx < headerseps.length; idx++) { + const headerlength = headerseps[idx].length; + headersizes.push([beginpos, headerlength]); + columns.push( + headerline.slice(beginpos, beginpos + headerlength).trim(), + ); + beginpos += headerlength + paddingsize; + } + table.columns = columns; + + const rows = []; + for ( + let idx = tabletopmarkerpos + 3; + idx < tablebottommarkerpos; + idx++ + ) { + const cols = []; + for (let hdx = 0; hdx < headersizes.length; hdx++) { + const svalue = lines[idx].substring( + headersizes[hdx][0], + headersizes[hdx][0] + headersizes[hdx][1], + ).trim(); + const nvalue = Number(svalue); + if (nvalue) { + cols.push(nvalue); + } else { + cols.push(svalue); + } + } + rows.push(cols); + } + table.rows = rows; + tables.push(table); + + lastmarkerpos = tablebottommarkerpos + 1; + } else { + endfile = true; + fromYAML = modyaml.parse(lines.slice(lastmarkerpos, -1).join("\n")); + this.infos = fromYAML._infos_; + + this.generatedtitle = lines.slice(0, titlemarkerpos); + this.infos.comment = lines.slice( + titlemarkerpos + 1, + commentmarkerpos, + ).join("\n"); + } + } + + this._generateTitle(); + for (let idx = 0; idx < tables.length; idx++) { + const table = tables[idx]; + + this.addArray( + table.tablename, + table.subtitle, + table.comment, + table.columns, + // global[table.tablename].aligns, + table.rows, + table.metas = fromYAML[table.tablename], + ); + } + } + + async toCFWF({ + padding = 3, + charseparator = "┈", + chartabletop = "━", + chartablemiddle = "─", + chartablebottom = "━", + }: writerOptions = {}): Promise { + // deno-lint-ignore no-explicit-any + const infos: any = {}; + + const lines: string[] = []; + let maxwidthcomment = 0; + + // Compute max title width size + if (this.infos.title) { + await this._generateTitle(); + } else { + if (this.infos.generatedtitle) { + this.generatedtitle = this.infos.generatedtitle.split("\n"); + this.maxwidthtitle = getMaxWidth(this.generatedtitle); + } + } + + // Compute max comment width size + if (this.infos.comment) { + const acomment = this.infos.comment.split("\n"); + maxwidthcomment = getMaxWidth(acomment); + } + + // Compute title or comment max size + const maxwidthline = max(this.maxwidthtitle, maxwidthcomment); + if (this.generatedtitle && this.generatedtitle.length > 0) { + lines.push(this.generatedtitle.join("\n")); + lines.push(align("center", charseparator.repeat(3), maxwidthline)); + } + + if (this.infos.comment && this.infos.comment.length > 0) { + lines.push(this.infos.comment); + lines.push(align("center", charseparator.repeat(3), maxwidthline)); + } + + // Generate tables + let table: Table; + const tablenames = Object.keys(this.tables); + tablenames.forEach((tablename) => { + table = this.tables[tablename]; + + //search the needed number of float size for each columns + const colnbfloat: Record = {}; + for (let ridx = 0; ridx < table.rows.length; ridx++) { + const row = table.rows[ridx]; + for (let cidx = 0; cidx < row.length; cidx++) { + const col = row[cidx]; + if (typeof col === "number") { + const colvalue = col.toString(); + const dotpos = colvalue.indexOf("."); + colnbfloat[cidx] = max( + colnbfloat[cidx], + (dotpos != -1) ? colvalue.length - dotpos - 1 : 0, + ); + } + } + } + + // Compute number floats needed for each columns + // deno-lint-ignore no-explicit-any + const srows: any[] = []; + for (let ridx = 0; ridx < table.rows.length; ridx++) { + const row = table.rows[ridx]; + // deno-lint-ignore no-explicit-any + const cols: any[] = []; + for (let cidx = 0; cidx < row.length; cidx++) { + // const colname = table.columns[cidx]; + const col = row[cidx]; + if (typeof col === "number" && colnbfloat[cidx] > 0) { + cols.push(modfmt.sprintf("%.*f", colnbfloat[cidx], col)); + } else { + cols.push(col.toString()); + } + } + srows.push(cols); + } + + // Init columns size with the header size + const columnssize: number[] = []; + for (let idx = 0; idx < table.columns.length; idx++) { + columnssize.push(table.columns[idx].length); + } + + // compute the content columns size + for (let rowidx = 0; rowidx < srows.length; rowidx++) { + const itemrow = srows[rowidx]; + for (let colidx = 0; colidx < itemrow.length; colidx++) { + const content = itemrow[colidx]; + columnssize[colidx] = max(content.length, columnssize[colidx]); + } + } + + // compute total line size + let headerlinesize = 0; + for (let idx = 0; idx < table.columns.length; idx++) { + if (idx > 0) headerlinesize += padding; + headerlinesize += columnssize[idx]; + } + + // add tablename and subtitle + if (table.comment) { + lines.push(""); + lines.push(table.comment); + } + + if (table.subtitle) { + lines.push(""); + lines.push(table.tablename); + lines.push(table.subtitle); + lines.push(""); + } + + // Add top line header + if (chartabletop) lines.push(chartabletop.repeat(headerlinesize)); + + // add header columns + const headers: string[] = []; + const middlelineheader: string[] = []; + + for (let idx = 0; idx < table.columns.length; idx++) { + const cname = table.columns[idx]; + const csize = columnssize[idx]; + + if (idx > 0) { + headers.push(" ".repeat(padding)); + middlelineheader.push(" ".repeat(padding)); + } + + headers.push(align(table.metas.aligns[idx] as Align, cname, csize)); + middlelineheader.push(chartablemiddle.repeat(csize)); + } + + // add middle header line separator + lines.push(headers.join("")); + lines.push(middlelineheader.join("")); + + // Add rows datas + for (let rowidx = 0; rowidx < srows.length; rowidx++) { + const line = []; + const coldata = srows[rowidx]; + + for (let colidx = 0; colidx < coldata.length; colidx++) { + const csize = columnssize[colidx]; + + if (colidx > 0) line.push(" ".repeat(padding)); + // line.push(coldata[colidx].padEnd(csize)); + line.push( + align(table.metas.aligns[colidx] as Align, coldata[colidx], csize), + ); + } + lines.push(line.join("")); + + infos[tablename] = {}; + infos[tablename]["aligns"] = table.metas.aligns; + + // Add another metas to export + Object.keys(table.metas).forEach((key) => + infos[tablename][key] = table.metas[key] + ); + } + + // Add bottom line header + if (chartablebottom) lines.push(chartablebottom.repeat(headerlinesize)); + }); + + // Add another metas to export + infos._infos_ = structuredClone(this.infos); + infos._infos_.removetitlelines = this.infos.removetitlelines; + infos._infos_.generated_with = `${footertitle}@${version}`; + delete infos._infos_.comment; + + lines.push(""); + const smeta = modyaml.stringify(infos, { flowLevel: 2, sortKeys: true }); + lines.push(smeta); + + return lines.join("\n"); + } +} diff --git a/src/cfwf_test.ts b/src/cfwf_test.ts new file mode 100644 index 0000000..e72fea0 --- /dev/null +++ b/src/cfwf_test.ts @@ -0,0 +1,457 @@ +// mod_test.ts +import { CFWF } from "./cfwf.ts"; +import { assertEquals, assertGreater, assertThrows } from "../test_deps.ts"; +import { modyaml } from "../deps.ts"; +import { searchMarker } from "./utils.ts"; +import { version } from "./version.ts"; +import { writerOptions } from "./types.ts"; + +const { test } = Deno; + +const title = "this is a title"; +const comment = "this is a comment"; +const footer = "Generated with https://github.com/badele/cfwf"; + +const datas_array = [ + [1, 1, 1, 1, 1], + [2, 22, 22, 222, 22], + [3, 12.43, 333, 33333, 333], + [4, .123, 4444, 4444444, 4444], + [5, "", 55555, 555555555, 55555], +]; + +const datas_array_options = { + columns: [ + "Id", + "larger column", + "right", + "center", + "left", + ], + aligns: [ + "right", + "right", + "right", + "center", + "DEFAULTLEFT", + ], + title: "Test", +}; + +const datas_dict = [ + { "Id": 1, "larger column": 1, "right": 1, "center": 1, "left": 1 }, + { "Id": 2, "larger column": 22, "right": 22, "center": 222, "left": 22 }, + { + "Id": 3, + "larger column": 12.43, + "right": 333, + "center": 33333, + "left": 333, + }, + { + "Id": 4, + "larger column": .123, + "right": 4444, + "center": 4444444, + "left": 4444, + }, + { + "Id": 5, + "larger column": "", + "right": 55555, + "center": 555555555, + "left": 55555, + }, +]; + +const datas_dict_options = { + columns: [], + aligns: [ + "right", + "right", + "right", + "center", + "DEFAULTLEFT", + ], + title: "Test", +}; + +test("Generated title", async () => { + const samples = new CFWF({ + generatedtitle: "this is a title\nis\ngenerated by other tools", + comment: "", + }); + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + + assertEquals("generated by other tools", lines[2]); + assertEquals(10, lines.length); + + // deno-lint-ignore no-explicit-any + const dsinfos: any = modyaml.parse(lines.slice(4).join("\n")); + const keys = Object.keys(dsinfos._infos_); + assertEquals(keys, [ + "generated_with", + "generatedtitle", + "removetitlelines", + ]); +}); + +test("Empty data", async () => { + const samples = new CFWF({}); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + + assertEquals("", lines[0]); + assertEquals(5, lines.length); + + // deno-lint-ignore no-explicit-any + const dsinfos: any = modyaml.parse(lines.join("\n")); + const keys = Object.keys(dsinfos._infos_); + assertEquals([ + "generated_with", + "removetitlelines", + ], keys); + assertEquals(`${footer}@${version}`, dsinfos._infos_.generated_with); +}); + +test("Title", async () => { + // test title + const samples = new CFWF({ + title: title, + }); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + + assertEquals(16, lines.length); + assertGreater(lines[8].indexOf("┈"), 0); + + // deno-lint-ignore no-explicit-any + const dsinfos: any = modyaml.parse(lines.slice(9).join("\n")); + const keys = Object.keys(dsinfos._infos_); + assertEquals(keys, [ + "font", + "generated_with", + "removetitlelines", + "title", + ]); + assertEquals(title, dsinfos._infos_.title); + assertEquals("doom", dsinfos._infos_.font); + assertEquals(`${footer}@${version}`, dsinfos._infos_.generated_with); +}); + +test("Title & comment", async () => { + // test title and comment + const samples = new CFWF({ + title: title, + comment: comment, + }); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + + assertEquals(18, lines.length); + assertGreater(lines[8].indexOf("┈"), 0); + assertGreater(lines[10].indexOf("┈"), 0); + + // deno-lint-ignore no-explicit-any + const dsinfos: any = modyaml.parse(lines.slice(11).join("\n")); + const keys = Object.keys(dsinfos._infos_); + assertEquals(keys, [ + "font", + "generated_with", + "removetitlelines", + "title", + ]); + assertEquals(title, dsinfos._infos_.title); + assertEquals("doom", dsinfos._infos_.font); + assertEquals(`${footer}@${version}`, dsinfos._infos_.generated_with); +}); + +test("Columns and headers columns size from samples.cfwf", () => { + const options: writerOptions = { + charseparator: "┈", + chartabletop: "━", + chartablemiddle: "─", + chartablebottom: "━", + }; + + const content = Deno.readTextFileSync("samples.cfwf"); + const lines = content.split("\n"); + + // Search title and comment marker + const titlemarkerpos = searchMarker(lines, options.charseparator || ""); + const commentmarkerpos = searchMarker( + lines, + options.charseparator || "", + titlemarkerpos + 1, + ); + assertEquals(8, titlemarkerpos); + assertEquals(12, commentmarkerpos); + + let beginheadermarker = searchMarker(lines, options.chartabletop || ""); + let endheadermarker = searchMarker( + lines, + options.chartablebottom || "", + beginheadermarker + 1, + ); + + // players + let table = lines.slice(beginheadermarker, endheadermarker + 1); + assertEquals(table[table.length - 1].length, table[0].length); + assertEquals( + "winner_ioc name_first name_last hand height birth nbwins", + table[1], + ); + assertEquals( + "────────── ────────── ───────── ──── ────── ───── ──────", + table[2], + ); + assertEquals( + "SRB Novak Djokovic R 188 1987 64", + table[3], + ); + + // australian + beginheadermarker = searchMarker( + lines, + options.chartabletop || "", + endheadermarker + 1, + ); + endheadermarker = searchMarker( + lines, + options.chartablebottom || "", + beginheadermarker + 1, + ); + + table = lines.slice(beginheadermarker, endheadermarker + 1); + + assertEquals(table[table.length - 1].length, table[0].length); + assertEquals( + "year tourney name birth nat winner ", + table[1], + ); + assertEquals( + "──── ─────────────── ───────── ──────────────", + table[2], + ); + assertEquals( + "2022 Australian Open ESP Rafael Nadal ", + table[3], + ); +}); + +test("No row", () => { + const samples = new CFWF({ + title: title, + comment: comment, + }); + + assertThrows( + () => { + samples.addArray( + "number", + "test numbers", + "", + datas_array_options.columns, + [], + datas_array_options, + ); + }, + Error, + "Table has no data", + ); +}); + +test("Number col and columns", () => { + const samples = new CFWF({ + title: title, + comment: comment, + }); + + assertThrows( + () => { + samples.addArray( + "number", + "test numbers", + "", + ["one columns"], + datas_array, + datas_array_options, + ); + }, + Error, + "They have 5 columns and 1 renamed columns", + ); +}); + +test("Aligns provided", () => { + const samples = new CFWF({ + title: title, + comment: comment, + }); + + const newoptions = structuredClone(datas_array_options); + newoptions.aligns = ["onealign"]; + assertThrows( + () => { + samples.addArray( + "number", + "test numbers", + "", + datas_array_options.columns, + datas_array, + newoptions, + ); + }, + Error, + "They have 5 columns and 1 aligns", + ); + + delete newoptions.aligns; + assertThrows( + () => { + samples.addArray( + "number", + "test numbers", + "", + datas_array_options.columns, + datas_array, + newoptions, + ); + }, + Error, + "No aligns metadatas found", + ); +}); + +test("Number & array", async () => { + const options: writerOptions = { + charseparator: "┈", + chartabletop: "━", + chartablemiddle: "─", + chartablebottom: "━", + }; + + const samples = new CFWF({ + title: title, + comment: comment, + }); + + samples.addArray( + "number", + "test numbers", + "", + datas_array_options.columns, + datas_array, + datas_array_options, + ); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + const beginheadermarker = searchMarker(lines, options.chartabletop || ""); + + assertEquals( + "Id larger column right center left ", + lines[beginheadermarker + 1], + ); + assertEquals( + "── ───────────── ───── ───────── ─────", + lines[beginheadermarker + 2], + ); + assertEquals( + " 3 12.430 333 33333 333 ", + lines[beginheadermarker + 5], + ); +}); + +test("Number & dict", async () => { + const options: writerOptions = { + charseparator: "┈", + chartabletop: "━", + chartablemiddle: "─", + chartablebottom: "━", + }; + + const samples = new CFWF({ + title: title, + comment: comment, + }); + + samples.addArray( + "number", + "test numbers", + "", + datas_dict_options.columns, + datas_dict, + datas_dict_options, + ); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + const beginheadermarker = searchMarker(lines, options.chartabletop || ""); + + assertEquals( + "Id larger column right center left ", + lines[beginheadermarker + 1], + ); + assertEquals( + "── ───────────── ───── ───────── ─────", + lines[beginheadermarker + 2], + ); + assertEquals( + " 3 12.430 333 33333 333 ", + lines[beginheadermarker + 5], + ); +}); + +test("Feature yaml infos order", async () => { + const samples = new CFWF({}); + + const content = await samples.toCFWF({}); + const lines = content.split("\n"); + + // deno-lint-ignore no-explicit-any + const dsinfos: any = modyaml.parse(lines.join("\n")); + const keys = Object.keys(dsinfos._infos_); + assertEquals(keys, [ + "generated_with", + "removetitlelines", + ]); +}); + +test("Feature (no key element can be found in the doc with marker", () => { + const content = Deno.readTextFileSync("samples.cfwf"); + const lines = content.split("\n"); + + let idx = 0; + while (lines[idx].indexOf("_infos_:") < 0) { + idx++; + } + + const syaml = lines.slice(idx); + // deno-lint-ignore no-explicit-any + const pyaml = modyaml.parse(syaml.join("\n")) as any; + + const beforekeys = Object.keys(pyaml._infos_); + + // Remove keys can be found in the document with the marker + delete pyaml._infos_.comment; + + const afterkeys = Object.keys(pyaml._infos_); + assertEquals(afterkeys, beforekeys); +}); + +test("Reader", async () => { + const samples = new CFWF({}); + const content = Deno.readTextFileSync("samples.cfwf"); + + samples.fromCFWF(content, {}); + const generatedcontent = await samples.toCFWF(); + Deno.writeTextFileSync("samples_regenerated.cfwf", generatedcontent); + + const newcontent = Deno.readTextFileSync("samples_regenerated.cfwf"); + assertEquals(newcontent, content); +}); diff --git a/src/cfwf_writer.ts b/src/cfwf_writer.ts deleted file mode 100644 index 34dc4d6..0000000 --- a/src/cfwf_writer.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { modfmt, text } from "../deps.ts"; - -export enum Align { - Left, - Center, - Right, -} - -function max(a: number, b: number): number { - return (a > b) ? a : b; -} - -function align(align: Align, text: string, maxsize: number): string { - switch (align) { - case Align.Center: { - const begin = (maxsize - text.length) / 2; - return text.padStart(begin + text.length).padEnd(maxsize); - } - case Align.Right: - return text.padStart(maxsize); - - case Align.Left: - return text.padEnd(maxsize); - } -} - -export type exportOptions = { - headers?: string[]; - aligns?: Align[]; - padding?: number; - charlinetop?: string; - charlinemiddle?: string; - charlinebottom?: string; - title?: string; -}; - -export async function arrayToCFWF( - // deno-lint-ignore no-explicit-any - rows: any, - { - headers = [], - aligns = [], - padding = 3, - charlinetop = "━", - charlinemiddle = "─", - charlinebottom = "━", - title = "", - }: exportOptions = {}, -): Promise { - if (rows.length == 0) return ""; - if (rows[0].length != headers.length) { - throw new Error( - `They have ${ - rows[0].length - } columns and ${headers.length} header columns`, - ); - } - if (aligns.length != headers.length) { - throw new Error( - `They have ${headers.length} columns and ${aligns.length} aligns`, - ); - } - - //search the needed number of float size for each columns - const colnbfloat: Record = {}; - // deno-lint-ignore no-explicit-any - rows.forEach((row: any) => { - for (let idx = 0; idx < row.length; idx++) { - const col = row[idx]; - if (typeof col === "number") { - const colvalue = col.toString(); - const dotpos = colvalue.indexOf("."); - colnbfloat[idx] = max( - colnbfloat[idx], - (dotpos != -1) ? colvalue.length - dotpos - 1 : 0, - ); - } - } - }); - - // convert all entries to string - // deno-lint-ignore no-explicit-any - const srows: any[] = []; - // deno-lint-ignore no-explicit-any - rows.forEach((row: any) => { - // deno-lint-ignore no-explicit-any - const cols: any[] = []; - for (let idx = 0; idx < row.length; idx++) { - const col = row[idx]; - if (typeof col === "number" && colnbfloat[idx] > 0) { - cols.push(modfmt.sprintf("%.*f", colnbfloat[idx], col)); - } else { - cols.push(col.toString()); - } - } - srows.push(cols); - }); - const lines: string[] = []; - const columnssize: number[] = []; - - // Init columns size with the header size - for (let idx = 0; idx < headers.length; idx++) { - columnssize.push(headers[idx].length); - } - - // compute the content columns size - for (let rowidx = 0; rowidx < srows.length; rowidx++) { - const itemrow = srows[rowidx]; - for (let colidx = 0; colidx < itemrow.length; colidx++) { - const content = itemrow[colidx]; - columnssize[colidx] = max(content.length, columnssize[colidx]); - } - } - - // compute total line size - let headerlinesize = 0; - for (let idx = 0; idx < headers.length; idx++) { - if (idx > 0) headerlinesize += padding; - headerlinesize += columnssize[idx]; - } - - // Add top line header - if (charlinetop) lines.push(charlinetop.repeat(headerlinesize)); - - // add header columns - const header: string[] = []; - const middlelineheader: string[] = []; - for (let idx = 0; idx < headers.length; idx++) { - const cname = headers[idx]; - const csize = columnssize[idx]; - - if (idx > 0) { - header.push(" ".repeat(padding)); - middlelineheader.push(" ".repeat(padding)); - } - - header.push(align(aligns[idx], cname, csize)); - middlelineheader.push(charlinemiddle.repeat(csize)); - } - lines.push(header.join("")); - lines.push(middlelineheader.join("")); - - // Add rows datas - for (let rowidx = 0; rowidx < srows.length; rowidx++) { - const line = []; - const coldata = srows[rowidx]; - - for (let colidx = 0; colidx < coldata.length; colidx++) { - const csize = columnssize[colidx]; - - if (colidx > 0) line.push(" ".repeat(padding)); - // line.push(coldata[colidx].padEnd(csize)); - line.push(align(aligns[colidx], coldata[colidx], csize)); - } - lines.push(line.join("")); - } - - // Add bottom line header - if (charlinebottom) lines.push(charlinebottom.repeat(headerlinesize)); - - // add title - if (title) { - const txttitle = await text(title, "doom"); - const atitle = txttitle.split("\n"); - let maxtitlesize = 0; - let titlepadding = 0; - for (let idx = 0; idx < atitle.length; idx++) { - maxtitlesize = max(atitle[idx].length, maxtitlesize); - titlepadding = headerlinesize - maxtitlesize; - } - - for (let idx = 0; idx < atitle.length; idx++) { - atitle[idx] = atitle[idx].padStart(headerlinesize - (titlepadding / 2)); - } - lines.unshift(atitle.join("\n")); - } - - return lines.join("\n"); -} diff --git a/src/cfwf_writer_test.ts b/src/cfwf_writer_test.ts deleted file mode 100644 index 22234f1..0000000 --- a/src/cfwf_writer_test.ts +++ /dev/null @@ -1,158 +0,0 @@ -// mod_test.ts -import { assertEquals, assertRejects } from "../test_deps.ts"; -import { Align, arrayToCFWF } from "../mod.ts"; - -const { test } = Deno; - -const datas = [ - [1, 1, 1, 1, 1], - [2, 22, 22, 222, 22], - [3, 12.43, 333, 33333, 333], - [4, .123, 4444, 4444444, 4444], - [5, "", 55555, 555555555, 55555], -]; -const datas_options = { - headers: [ - "Id", - "larger column", - "col", - "column", - "other column", - ], - aligns: [ - Align.Right, - Align.Right, - Align.Right, - Align.Center, - Align.Left, - ], - title: "Test", -}; - -// const cryptos = [ -// [ -// "2023-11-04", -// "yahoo", -// "ADA-EUR", -// "Cardano EUR", -// 0.30382475, -// "EUR", -// 0.0089045465, -// 3.02, -// ], -// [ -// "2023-11-04", -// "yahoo", -// "BTC-EUR", -// "Bitcoin EUR", -// 32382.293, -// "EUR", -// 523.85547, -// 1.64, -// ], -// [ -// "2023-11-04", -// "yahoo", -// "DOGE-EUR", -// "Dogecoin EUR", -// 0.06409328, -// "EUR", -// 0.0016048551, -// 2.57, -// ], -// [ -// "2023-11-04", -// "yahoo", -// "ETH-EUR", -// "Ethereum EUR", -// 1715.4004, -// "EUR", -// 50.568848, -// 3.04, -// ], -// [ -// "2023-11-04", -// "yahoo", -// "MATIC-EUR", -// "Polygon EUR", -// 0.625949, -// "EUR", -// 0.0191679, -// 3.16, -// ], -// [ -// "2023-11-04", -// "yahoo", -// "500.PA", -// "Amundi Index Solutions - Amundi S&P 500 UCITS ETF", -// 78.79, -// "EUR", -// 0.30999756, -// 0.4, -// ], -// ]; - -test("Test empty array", async () => { - const result = await arrayToCFWF([]); - assertEquals(result, ""); -}); - -test("Test columns and headers columns size", async () => { - await assertRejects( - async () => { - await arrayToCFWF(datas, { headers: datas_options.headers.slice(0, -1) }); - }, - Error, - "They have 5 columns and 4 header columns", - ); -}); - -test("Test headers and aligns columns size", async () => { - await assertRejects( - async () => { - await arrayToCFWF(datas, { - headers: datas_options.headers, - aligns: datas_options.aligns.slice(0, -1), - }); - }, - Error, - "They have 5 columns and 4 aligns", - ); -}); - -test("Test header", async () => { - const result = await arrayToCFWF(datas, datas_options); - const aresult = result.split("\n"); - - // Test header - assertEquals(aresult[10][0], "━"); - - assertEquals( - aresult[11], - "Id larger column col column other column", - ); - - assertEquals( - aresult[12], - "── ───────────── ───── ───────── ────────────", - ); - - // test size from header - for (let idx = 10; idx < aresult.length; idx++) { - assertEquals(aresult[idx].length, 53); - } -}); - -test("Test number parameters", async () => { - const result = await arrayToCFWF(datas, datas_options); - const aresult = result.split("\n"); - - assertEquals( - aresult[13], - " 1 1.000 1 1 1 ", - ); - assertEquals( - aresult[16], - " 4 0.123 4444 4444444 4444 ", - ); -}); diff --git a/src/table.ts b/src/table.ts new file mode 100644 index 0000000..7bbc275 --- /dev/null +++ b/src/table.ts @@ -0,0 +1,101 @@ +export class Table { + tablename: string; + subtitle: string; + comment: string; + columns: string[]; + // aligns: string[]; + // deno-lint-ignore no-explicit-any + rows: any[]; + // metas, customised fields and also some value cannot be computer from + // fulltext document + // deno-lint-ignore no-explicit-any + metas: any; + + constructor( + tablename: string, + subtitle: string, + comment: string, + columns: string[], + // aligns: string[], + // deno-lint-ignore no-explicit-any + rows: any[], + // deno-lint-ignore no-explicit-any + metas: any, + ) { + if (rows.length == 0) throw new Error("Table has no data"); + + let nbcols = 0; + const isarray = rows[0] instanceof Array; + let keys: string[]; + + if (isarray) { + nbcols = rows[0].length; + keys = columns; + } else { + // Get headers information from row content + keys = Object.keys(rows[0]); + nbcols = keys.length; + + if (columns.length == 0) columns = keys; + + // convert arraykey to array + for (let rdx = 0; rdx < rows.length; rdx++) { + rows[rdx] = Object.values(rows[rdx]); + } + } + + if (nbcols > 0 && (nbcols != columns.length)) { + throw new Error( + `They have ${nbcols} columns and ${columns.length} renamed columns`, + ); + } + + if (!metas.aligns) { + throw new Error( + `No aligns metadatas found`, + ); + } + + if (nbcols != metas.aligns.length) { + throw new Error( + `They have ${nbcols} columns and ${metas.aligns.length} aligns`, + ); + } + + // let objrows = rows; + // if (!isarray) { + // if (keys != columns) { + // for (let rdx = 0; rdx < rows.length; rdx++) { + // const row = rows[rdx]; + // for (let kdx = 0; kdx < keys.length; kdx++) { + // if (keys[kdx] != columns[kdx]) { + // row[columns[kdx]] = row[keys[kdx]]; + // delete row[keys[kdx]]; + // } + // // console.log(`${keys[kdx]} => ${columns[kdx]}`); + // } + // } + // } + // } + + // objrows = []; + // for (let ridx = 0; ridx < rows.length; ridx++) { + // // deno-lint-ignore no-explicit-any + // const fields: any = {}; + // const cols = rows[ridx]; + // for (let cidx = 0; cidx < cols.length; cidx++) { + // // item[headers[cidx]["name"]] = rows[ridx][cidx]; + // fields[keys[cidx]] = cols[cidx]; + // } + // objrows.push(fields); + // } + + this.tablename = tablename; + this.subtitle = subtitle; + this.comment = comment; + // this.aligns = aligns; + this.columns = columns; + this.rows = rows; + this.metas = metas; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f07eda0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,27 @@ +export type Align = "left" | "center" | "right"; + +export type ParsedCSV = { + columns: string[]; + // deno-lint-ignore no-explicit-any + values: any[]; +}; + +export type readerOptions = { + charseparator?: string; + chartabletop?: string; + chartablemiddle?: string; + chartablebottom?: string; + maxmarkerlines?: number; +}; + +export type writerOptions = { + padding?: number; + charseparator?: string; + chartabletop?: string; + chartablemiddle?: string; + chartablebottom?: string; + removetitleline?: number; + // deno-lint-ignore no-explicit-any + metas?: any; + font?: string; +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..f5d6212 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,59 @@ +import { Align, ParsedCSV } from "./types.ts"; +import * as modcsv from "https://deno.land/std@0.204.0/csv/mod.ts"; + +export function parseCSV(content: string): ParsedCSV { + let csv: ParsedCSV = { + columns: [], + values: [], + }; + const lines = modcsv.parse(content); + + csv = { + columns: lines[0], + values: lines.slice(1), + }; + + return csv; +} + +export function max(a: number, b: number): number { + return (a > b) ? a : b; +} + +export function searchMarker( + lines: string[], + marker: string, + fromline = 0, +): number { + for (let idx = fromline; idx < lines.length; idx++) { + if (lines[idx].indexOf(marker) > -1) return idx; + } + + return -1; +} + +export function getMaxWidth(lines: string[]): number { + let maxmaintitlesize = 0; + for (let idx = 0; idx < lines.length; idx++) { + maxmaintitlesize = max(maxmaintitlesize, lines[idx].length); + } + return maxmaintitlesize; +} + +export function align(align: Align, text: string, maxsize: number): string { + switch (align) { + case "center": { + const begin = (maxsize - text.length) / 2; + return text.padStart(begin + text.length).padEnd(maxsize); + } + case "right": + return text.padStart(maxsize); + + case "left": + return text.padEnd(maxsize); + + // left + default: + return text.padEnd(maxsize); + } +} diff --git a/src/utils_test.ts b/src/utils_test.ts new file mode 100644 index 0000000..e612a89 --- /dev/null +++ b/src/utils_test.ts @@ -0,0 +1,38 @@ +// mod_test.ts +import { assertEquals } from "../test_deps.ts"; +import { align, max } from "./utils.ts"; +import { parseCSV } from "./utils.ts"; + +const { test } = Deno; + +test("Test max", () => { + assertEquals(max(0, -1), 0); + assertEquals(max(0, 0), 0); + assertEquals(max(0, 1), 1); + assertEquals(max(-1, 0), 0); + assertEquals(max(0, 0), 0); + assertEquals(max(1, 0), 1); +}); + +test("Test align", () => { + assertEquals(align("left", "left", 10), "left "); + assertEquals(align("center", "center", 10), " center "); + assertEquals(align("center", "center", 11), " center "); + assertEquals(align("right", "right", 10), " right"); +}); + +test("parseCSV", () => { + const players_txt = Deno.readTextFileSync("samples/players.csv"); + const players = parseCSV(players_txt); + + assertEquals(10, players.values.length); + assertEquals(players.columns, [ + "winner_ioc", + "name_first", + "name_last", + "hand", + "height", + "birth", + "nbwins", + ]); +}); diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..dbeaefe --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const version = "0.0.1"; diff --git a/test_deps.ts b/test_deps.ts index fce4710..d88e584 100644 --- a/test_deps.ts +++ b/test_deps.ts @@ -1,6 +1,7 @@ // Add your test dependencies in here export { assertEquals, + assertGreater, assertRejects, assertThrows, } from "https://deno.land/std@0.203.0/assert/mod.ts";