From c71b8ccb192bf630f631b5d0d9e989b4f8651d1f Mon Sep 17 00:00:00 2001 From: Mathijs Rutgers Date: Tue, 17 Sep 2024 22:44:44 +0200 Subject: [PATCH 1/2] Add CLI option to display results of competition --- .gitignore | 2 + README.md | 32 +++++++- bun.lockb | Bin 215510 -> 216984 bytes package.json | 1 + src/cli/bestWorstGoalDifference.ts | 36 +++++++++ src/cli/config.ts | 15 ++++ src/cli/filterExcludedUsers.ts | 15 ++++ src/cli/getHumanReadableDate.ts | 13 ++++ src/cli/goalsPerUser.ts | 41 ++++++++++ src/cli/index.ts | 49 ++++++++++++ src/cli/logResults.ts | 121 +++++++++++++++++++++++++++++ src/cli/matchStats.ts | 89 +++++++++++++++++++++ src/cli/matchesPerMatchDay.ts | 16 ++++ src/cli/mostFrequentMatchup.ts | 31 ++++++++ src/cli/mostGoalsAgainst.ts | 16 ++++ tsconfig.json | 1 + 16 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 src/cli/bestWorstGoalDifference.ts create mode 100644 src/cli/config.ts create mode 100644 src/cli/filterExcludedUsers.ts create mode 100644 src/cli/getHumanReadableDate.ts create mode 100644 src/cli/goalsPerUser.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/logResults.ts create mode 100644 src/cli/matchStats.ts create mode 100644 src/cli/matchesPerMatchDay.ts create mode 100644 src/cli/mostFrequentMatchup.ts create mode 100644 src/cli/mostGoalsAgainst.ts diff --git a/.gitignore b/.gitignore index 113a3e3..73e4801 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ next-env.d.ts public/images/users !public/images/users/unnamed.jpeg + +src/cli/data \ No newline at end of file diff --git a/README.md b/README.md index 83f8087..c319460 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ $ bun install Copy the .env.example file and add you own database url. ```bash -cp .env.example .env +$ cp .env.example .env ``` Before you start the server, make sure you've run the migrations: @@ -32,6 +32,36 @@ $ bun dev Open http://localhost:3000 with your browser to see the result. +## Getting results via the CLI + +You can get the results of the competition via the CLI. To do this, run the following command: + +```bash +$ bun src/cli/index.ts --dataDirPath +``` + +The `--dataDirPath` flag is required and should be the absolute path to the directory where the data files are stored. The data files are exports from the database and should be named `Match.json` and `User.json` + +### Excluding users + +You can also exclude users from the goals scored and goals conceded ranking by passing the `--excludeUsers` flag followed by a comma separated list of user ids. For example: + +```bash +$ bun src/cli/index.ts --dataDirPath --excludeUserIds 1,2,3 +``` + +Make sure no spaces are present between the user ids. + +### Defining a locale + +You can also define a locale for the output by passing the `--locale` flag followed by the locale you want to use. For example: + +```bash +$ bun src/cli/index.ts --dataDirPath --locale nl-NL +``` + +The locale should be a valid IETF BCP 47 language tag. The default locale is `en-US`. + ## Contributing Contibutions are welcome! If you want to add a cool feature or do some kind of improvement, feel free to open an [issue](https://github.com/brainstudnl/blaco/issues/new/choose) or [pull request](https://github.com/brainstudnl/blaco/compare)! diff --git a/bun.lockb b/bun.lockb index fc5dcb31527df5f185e40483d180165d576ceba0..3aa07ec9327845c825e9ad340541804642b260fb 100755 GIT binary patch delta 33112 zcmeHwd3;XS*Y7z;9&$_(Ln1xB$9Zd=6SAp2!aqvBr$}NL@8B@9&AHvrc|rd zYDv{pI?z_DbkNe$QCl6f+6oQ#`#pP~2tWG1_x;>^|G1x?kCpFPYp=cb+G~$zaGsB> z3MO2_#{kf~{TnaN3`vT+`*Yg&2mM}d?#Q<#yNk~J|&)8@i}59BRj z)gO2Td{y8nU;f2Hs^FaZ26g{y$M)gj2o32{7- zHRubZBZ)xPBnn860~KaxrKXHT0h{3!<&_|(W2J$t;YxT%zQ|ifEF(QD8@1J@B228v zL?HFjlG1Y1Q${27le0#q=V;orO48t77_SM1WE7N!y7;4-4Zs&x*0lS8pD9cMG6IKy zfxt}&BYen9t|pu07?1_+12STxlSfV-3)>U3@CS!aHIFRlDHvdAmH-)n2Z5~lKp=d| zi$Hj2C^KbZX3`i<8=IQW0BGM?yNpJ;XOYVKPDsv~LTNstPJT>kQg(LoXswg7GA1c2 zdlJ$!lP71TjLFeF@R{ycLR}fhAwc#d)}5uss(kTiUFwa`3du>Dn5rdZj?GBQ%t{V} z9Qj)2jX_XoU?7kIz5-+%QpTpGXC_N`lTx#^O|?~gvNAH0ArG{cmTHtzR7Y0!D?6KB_xLCFEKv>za6wZBl9k`-ddX?K08ciIZ_^3RPyL(+P= zhSJxJr0j7ah}?1HVu%kZEZa!(FDrgKcm^dkc}f~$sI61!3l**a(u>i_$r%_l+A}KO zYM?DoAC;Y%l4kAk@-^K}rKioTlV1Jk|J>4Hvc5&lr5{UyOuYz1x6I2f&gmTnD#Qm^>(u0LEu^Q>Oe+h3t~?PRsw0~thJ$xzkbCkDAOqSJ>;QmacAj# zl`gVS6UgTH8FI`od2wB3G#VZsnbIVuAg^-hX!3=9H)y_byQmq2#p;C|9@Ss>GY?ko9^ zfo$GG3O503f&U2k%Clb;^p^!*hDTMAPyo+a^CbhM;XwmshBH7+hb3z z!QffbM5MF-GypPUR|nfQ%!`IS<|_p`n|C(sRs^0Jii)vcO`n>QJ!+h$br~i@p8%x7 zK`79nXMvT0=ZDJzP5{ZjGD14I5y+-E1Ud8FOp^EqkQMnFNJl>&DF@0i@N|&fB$u9# z8kIho>*F#M6afA?6@kY_%bJY_&pz@5kR5UkkOlgUkp?FKDc68(s!Pez@lO=K2Bdrv z@=>ouzA?67Bx+BO`apr6Vu8aPJ65X$oQ+J?mXZc! zK1}H8Q(#^w>2qd~%2UWnsh&AKT9GNQugD0yoGgTH`dzy4; zOnPPrc8}vyr^xhe&|@51<;ae(8~Q%rUDIWlRsdP>cIXoy244v{8R*Ndo~jhC!2mto z2m_UYFCYUQc@(@qFk9hBmEI9p1M)B+BM}I++XYAmutaAjk4>I-9d;PGl&qx8%%mLc z8}NSMZ=jp|g7OBU#Tb%LkiZZg2eRNpKsKxe^aDNwq~}wCY_g$17St2Sd|%I&^7i1F zFAV4h3;_BA%L1uCWRA3d6_LwjO~1kkJwKuF0FVVO0Ksq#9ZWEGA!wwzV`Uv7r1HZu#Bk&=x7Vu3~zz`s7 zn((M}s1JCy;d|g|u-Zb&kIYOOm7Jw%3&B(V0gw@_zeuJ}OwJse9HD8>?kOWxj_J_U z=JKT6Voj@$4DUQ9J>CsujTb^*9$38jwe*UOa!)LkW(NYB>IV=jRWU)!pl0!yeQ~iVD z&jV>?1Kg_u+_6EnmeaqMf~T7)K)TrjSOe%*JeCYrH#aX5Co~kaSq9eGb|otweZ{^8 z!p=QAJ0&?YHGOna7N*~sTcpF2fNY2pTjgr?Dv;G32{}hp*KL~C5a{UD*e(Y`Fyuj; zV=CgL78HJeN+xW1T4wwbJR=daL&oi1+hgaN$!W+tx5@qB2qV!B$Oaytg^e95#h#c{ z9#lxsc`>n0 zjILj{a)Lar*1D!eS~G*<^lYmz$YZ_>z9q`=vJSU#8)Xb@dfirfPphD=$F^5v zZd|~Fr8I2@lw5EE5h^}G0K#&t~F?-cd}v|dh~f#4*q^(72t0ptFWQRe8^kV z;^9OYdaA!;$~+@Rv~0V(AVgb%hDUV^&?hJ zu*an%LeaL37FJ=fN1tQGHuIQoKuS&B$`5wym8^nh9=)wqh`&e=@t8-^&?Asu%4*%r zttVRrki}z|WIB1wo#6UOZhVY4CfMF!yez%0ThFixLOuEcs}O&Kt=Q%sG;U6Fk9i8? zl!axnZ?#?$&o%1z{e;@VBmowKBV4NcXTy(HF;fgXw9P za(JlCbOckHH8A5bL}}Xg*PI3}%I5SixA_dXFmPq8{6=o`FL2Cf*WBd^K$=w)6z59E zS(H`OJWk(a<7)`8c>1c3JsBZUa~5*P>eK zy{*`2)C=nslTcKoTi<6DM0;E{Yie4emC!8CoPe|TGPMzFozudj|7sPq@R&U?n!DR3 z(TwH}aJ1rOwGMHc7r@1UGwg;7unOEBGxI*!v^C(Q`2x86!Pylwi@NB6aIT6)ZOLB*v8xy_m2*bZpN zmTvQ1a6`bM^+MdPCUsFOYkz$N)ILLK&6jZ2*7o6Wm|HKjV&gq}td$dwP}Xxg>|wXt zdy?mJuI^0`8un9j zKF*|LaN2bUTnlS|Se*GM&ydY!9UdOz-BiZTo<{XNtDud?JOCLpLIJh?6CBl5zX*0> zjW*OLTLo=B<_^dhIrO{c&_+qg4P|H4NYOFbNq(@L+K4CMW>i zZ#g(RW`|clV-Rk!rs+wNuqz;Phk9a3Lu6?XIJ=d9T7 z9`im#o6QC5=;D3B$q2NLck7Q?h21^++g5B3k7>lp;LE0H3vQ&H3&Y*D6J3D+hlsSOxg&whDWD3|A|wWuI2Ac8Ez^E1^%EY2l1j@Ursb-L7Nc+EebTiD<-g zMsy9p*$7*<7iR=Lr2IfN5@R9lbo{A@|b#}z_I1zVvw!U?6O?j$XN*?ajvuW8E1t`=)4#puuN|f z!J*c$%*9{>xNhJoSosTMyt~LXM>?7fj$J}7anFMrW*cDzd!k45F6K7g#$5r&3fhCf z90BV+i#4AI$H1WdF)Mx#j+I0S;ObzxLsP;rmXHHZT4Cg8Vf{}lwsHyF2#0fZh52z@ zw9Pkg#;z(WTDg~|EwDLScmf=Ui=3@y!ucV^X=lLowKWsOt(2OLa{4|D=nJgE3D_J%hLu2RTm{GC z<>2j&u+XSnS672ev^h4yr{M0lxx?UUCd&xOeB;5%K98l*)c~^rc2S7aESxb;*m-2e zc!QC_q`5j{WzQ&W9d7G3GZZIV>JYfT_P(I%IBCFkNS|pHq^uPq&>*7QshkXm>)pavRI?~1Q`UE)w+S()fZehUV)e& zp_f3Bc4FQ76ssW9W6s9dZfDC5V?%TnoV0`9=kiPEDpM4Nr2%IV_U;@_kmWH?f@c$8 zCdIDXm0`7P*2>JtaF%Cw-Gkt03Of(%CJVu_T5{>0KFQezV8`g%3vPfti`30@7Cc&* z0gmm3JRRKT=ip?kGx(LWoHV^lj5nB8vM3}y4o(#e?j3N97B@g>^lUjF+naN&orP09 z<|@e8#E`|i&F{dqb!_P^tb%FW$rMiWn4#00>g6ZIc!P<78n!+4-Ppqy<= z|I9djkQF=2V?G0(0}jq2UcZ53#bmsi=9X~8l?|?iHM2#W`83Y@O0BML(_4SPs-eCS8IU8&&E9;o;st8li*^qb>Ul$*Hm;ABH%nRo33 zhhSpKzJ@anHnyGH>^j%sIDxGNhus0vS}%(61~W`5qSkF6mJ^6{^$BotIXpbmZC(V& zT4SndfJJVeGx6kun*lBYX_z;1-1@Ut?86@2&&qk&V@{bbcO|wXuCw6c?ao#G5!pg= zk?#qPs~WUo-L3*~-R(Y5W`WZRl=lZW(%RoTPA{->=6m!at6)C%`j0x*VO5udV>g2* z*r}fZ$B7iT6sEb&`xeSH%-pyD9RMy$YNF;Vz)`b|J)4~e$FSlO7Xb@dB=gyKY0RhR8t@vE5LCe$VvBO zaI`760ltq**JTs*0Ec~tbkVgLoPBf2EMrM~Wv#;<-TG9kV6n$M2pO%Se2nbBz{P

z~u@8e^7BUxP{;nZ4d2>v=y>j$X&oLa16D# z-KT#6$GUji+t4P$X+AF4$%>Qv>(6AKBv{RJGYn6|eeD`i8=Yw}6pm{!`pKza@w z$BDdpH&)3sd((^yy@F*PeW6vj%wuM+E*^vP-TGcDXSqkOY!xi`m=o5>X#>k9?pa`1 ztnio>pOAwJqYoV>100rEtkEA8%W*5tmHs4W4^EJ8;|$Xa#&nsrvX1ud)YS$YF2us( zTvMGha|h1Yv2;sMb(=T9sX>L+Wt}s-VU)Pmg6q!N)4YT;_H?+|(5;7A1*<&fxb;p@ zIPRYT$B}_u6Rx)}S~;sdT+#8jwpF+q_X{^Vix}6YwcrNWE|B{Z+z^{%WqWV3=j_&) zd2x0>hemZ1llQK5oRSMu=T4Vp**OflcmBo zq;PC_Ly8vP6ih?rSx8eLMbD3q@!sML3d|A4pIfYJPqZ?cZM9lH*~*-~RT{=T(97-m z72FtWX2Ur1fv1Y+LJX^o;1XdDcLVFXU76c(aX~HDd7QPh&!V@pu9Pjp*=YOhGR}tB zXWgE5Qg`Dl(Ux7oSzG(8;|?wg$ZIUvVZE}xmH8?JZBZPW0Gl-JnPT_hN({K3NW*l5 zfX@fl3Y^`X#v9LA*EX~=y`PmWh}jigYalthoaH#P%Q4U6jFSfD_O5QD@=mMerdCGh zoz`@~!kyMDn_A{pLp({PK-EEPr~rjEfINtl2P&)y^a9lZ@u&l0dJu>Qk@o6?T%aZ( z0~8G6Q3C5~r6C9hF#{UdK1!lt747!VrTG|@N~Fu(6#st`>1GdQw*+EGp%SRj%jRxJ zy5CpH?}n^EKb20TJ_Zz@-e|KxJW3+V$)gZQ9v?z6!$Tk%ngcSdb6fnaAuoH|zG0!Z zkAFhuoDZU{N^``yjGf=hKpwz$`6v1e=h`3DD)IS!D)L1gfclEFbV ztXkXrW68bAKRE7&wDOjnDv`b&CvQEuR}PkURVtC%CqUGCU-2h_JcvyHK;bDMkGmoD zPJXX7j#AP8j+Q}24Yj(0MQY<)O#S)4U|UoVySl^9VnyZM4oyp zH1&E^$8rM!70auH3e>_uEM=W5@VB1XUdNtTs;HEILb_5->6Jvv{gs@^Dh2{s@_kBv zH)K+69W^X%B|Kf*cB7F}zZBT{tLqMLyJ}gG@Fyjs(>~`Pg*^QN ze^|f;ARG02AS3ge;;%8?TJ($@-8aFNMJnHOr?xM%DkL8Op7Fg;r8iXRM4mQQJduMl z6j%Wm2V_!$LJt`nC2+i5U$um|)Quiik^MeC!mugXvKv5NNl+tdAM zB_&cjS@A@+!ek|%qU1#CO$D-)=}Jyy>&#MoNu*xBk`v2;UufWM3%0^?2r2`g0(x0b z9+0i@j8ZR&H1izfEdF_wPNe)r#S@vnTj5JUw$>|(-v{JzH>8~d26lBca8PAD3S?A| z1F7&XFcf%QrBkR|Ll61~Fjpz6C}cFsfOlCT&&f*pfI~LTmwAAdl{S%OR9Er<#n%SX z`T9Wo(;6vksxSn|QofaSA5@ImohrENC*&%bNMTj2~Y> z!9Oim;cPNEN+L^n2y(hL4@iE#;uiuL=f@SUROzdLJcx|}epyEp+tsGYAuL0@ln?TBs zDgBa2JI7V}J1QNRYZv$d1eATK@Drs_5-Iu&e`x5eN+Cgq0UJ|(o{8nm| zH1G!$X!uX1NMxH`16BljgJBIT0LuWY0=ewe0pg!lAAeX-Lo#=wb?ya!tM8FYR_YPC zhi|I1h%7Z2SOwS$NUNQJbflZYoT4l4F@v+2!%;NdNE4zqZK9t+4>W` z5N{qh`442qbR{o|Tzztsq3J+6HdE;l>A)<7xj@><2U2g2;vWX$pSHjYlPwb#fzz#- zM`iz8Ob*f|)VTvGTBhVBkyeC~Gfy=|lyjR(d;g8g+wrzvb)d|n4u~0pKs<;vSRcd& zXaZvWfggM*j59Bglq!9nEU`uFnI4$Hrn zxBp(={(E_g34sgmzn8c6zT)4@TPzv>Uf#-e^54tbe=l!kPbhg=i^uw_L?63{mL@#9W@sf&Ds0+(iK%AzPRxI z;R)jHmnwa^#r5X4k8AFIJ;t}av2pIZ-QU>~nE36XzRharrE2@NyPokw2Qe{C56GQ- zXnx-3Gry`_R5&c^-3e`5G~L*{eYF3OdAWIe>t{?_zIuC$mkxyIw|HYi?aLqbe>wil zwQrWs@3e8m@{S$nz4PkVyiKobH+*gYe_@@};kE17k?&5YjPOi;V_I-)6+ccfurvB;hr6t_EEP~SX{^FB#y@HsVrgzbQ7bhv0p8-L1Is|`+ z{B#InCPDBG1%HX~3J8=cj4< z4Bd4Q_k1es@m+_vN$?JZS6O_p#r|H4QAt#MQ17k}*faD&{V&6KvX=OHntp;hCO)E* znTE!lwkJGKAFh|}&_Mpb#$FBfR9vSY(Dziz(W~3$%&EyEGm=J4(3XjrbM#UA=sg$a z=<5u^&e-O`3(<7OstDW(``YYD2)kmAo|3WVi-InUV=-oxa<9Z|4(R5Xr zeRnZsB?fuXeNkzp?u`)Tuhny9u>yq;u#*y3>)z6exQt-gvh$D`e?5MyYF%|^$h*@P zg*_=O>-_)Com-gcHQm(~Pok^QVr`~ZaQ<85#}Jzor(5%b8%tmQ{rmq%1lSvPlwGMl zJ+&7@(PemYZ`_mzXU0qRe57e-)HLbvT!eTtmd8&@hxZG|D%sDFktfTWx;!o`JzfSm zkFEHu5dZ9(z_V$T&*FcFgy+0JJzom7Ka`Aje|s|rj=z9B<>RhzKs>*yWLym=tAY!a zjL*lWC>bA4QlEFJrz+WXTZS9Ue6Bc63IA4#ym|hFa;8Yhd?4ea10GyZ@Xvm@w_VBj z%#&HTg;}if>Oku8+1v=6vqFZF@z(ht^2Rp8r-W4@oU5GiQZn8n&Qmg%lKDZl0K}uT zlJU{_Q8+_K%P1M|DL)4ppF(;o8SgK*z&Vdxb_aUMcc_Xe!BJMp_&ycyfzzpSkg=kC zw~7x$cvOUpe$@gkzzOv$tGxH&e4&z6QL@^QIS)~)N?EQ}2f}w@kcO*4h=2AC>=R1n zuQJz#{C!n$b){DivXe@$hLY8X>@gHfr)sLaSPit#p+^U5DOp3De_E=U{r}eO3w1`J z5$F^YX}GphyjUJ5IL1{I2fU4j;=H%fGnOIYD<+?SI9xb9#(+2^#(`2m<3Xt)4hdga z=WY0=BH1*AF;f#aOrx&r5t!~L8k99WK^>4RXbb2e&_U24&=C;dBH9Zo;2Vvv;N(Tn2GC{@ zU&9ihd7wNHpCL5^g@8gq%|YRyNbzAgqjqjQPFEmC0>tt7Bxo&YBWMeVmk?iqz5#s; z`VMpv^gZY&5Xb9Q&{LpoAdXdz%_X3|D83hn@0Rod^#FAPbqAqautV0hdl9=bdk)79 zU*yOHO@o1Ppz&gPdBiyzrz1hVKzvi;Jc#dlEC4+UnhZ(>rGe5x8K7j)7!Y4~X#{Ev z3IR0-g@M9Bk)S%Dx}bWX`k?9{J}0jMsthvu4oNwjT!DXofc^qq1@XfHkApa$?G)Y5bsvc0nG)y3fc$S3VI5(3bY!u5;Pz52#9kHpZHp!WuWDt6(9i`;+2bca!|kx zFgHOu&J9p0kQc}WDh(fjT0pAtW7SsXM5!4C9w-~m8wu7DqwSX=kb@GAj z5zyRuHm!-s#y+k ziUlTu&Vx87zYXHU?7g4@(02Nf?9*_2ek#Y1GNY7jT+9=oR>Kb`+>Nk z{RraAMe9LNv!^|Slbs-M5Z{%12ec5h2=o|e31}(EK%qlGLs>Y;3sf3Z1&(tO;9Slp z_3J?$P}rZaw;yx>bO>}9bQJVD=p1MnY?dj7p5~1VCddzls)GVR_ks9S{Ugu@P#y3= zpt_)XppB4y0|U!(o&lN!$^wl9g@IPVfz_Y~p*IUO0_RmxD4z`TDKVcI^J((*TqJUh zeHO%Zbq#0~lzpJgr7|D%C<;0T`O6@_m~<07m&ZpzToSq3admqS#MO(d6`w4Bj&r_B z@G*$5415Cm6vPGTG>8}EeoP=yaU~lH$CXBMWy1a?tU{jd6lw z);e*bo-wA1^W7G2Jv=l#G!`idBBZ_%5XfZ}Tfsc%LoVKWL|ABeRA_|OTO>h2A0}qh zHzM^k@f^75Jg8$gnYXS%UbEo2$s@h>=+LOpXh=B&RzOPU&%cOk_TAre3_UUwmLL_P zh$XGZDfW$dzm4zyc%6A)9WszLJUTQC!$pKQKuLQ=mj)>5w8$m*xmeY}=oWYpxlH7m zQ9bTj%Dlc~3_Uz1G?I3k3B4ioqQ#`fMpel(#R=MyAj~LI7#k`_z@7uH=F#SAG@TjsVk7`(f3x@ zUGgJ7uOwIfAjs#}8-_kC3Q-FQLTY*e!V{LRMn%zyGW=Pg`Aa< zaAy=M=}tL5x&gjteZ=>R=PJWBP8;vm#zyaMz? z;&*AIQ4^%SE(SM2T5mDAiP5d5^SRia+kT(dd-bkkh(9_&Ifd#%gw!5+5BbYErsLa0u=PG z#C1ZEs1|HQ2Ku8Hvojpn^5W|qpWWTDsy-|{G%6hZzL6M3h5rosF!a0#^w-T|7ZmiB z;+tS2Sl=Xkn;Aj+L=mzI)~AYY&ESml&D<&9pIGvkwd*33o&=($$gYhF9^9LT0oOLwiO10tMtUqoE!s z5~A&3RG4D~h>U2XvN^1d{7PD&coa#2UnzEu=X7A__uuLOOY{(yip0TaR53?XXaN(m zMeAaS)OU-?Enq%IY;S?t-TB^d+3)+;@~mHY4K^56*eEM}-N?L91iPWWLJTLjPUMi= zE}kt;qws>{t~0HfXdVMix9AiD`_5N{n?Kd+(6@6(Vk%;9p#9lm7xm7Gw+R=;H!)~r z=j&)AC)H0#EawxWs)TNJQ$)tXdK>XxEYuf>d~kuzmt(7>z5V`0s{tEAd2j1Q20GuI zT^-w^?2V^C<-{Ku>gG_nB!Wg7e)pmKIN!PbbM1k43)}vP1&&ii1p0D0(W@m2t0yM3 zM9~Ri9k^)cJG|*D-}9L@zpH>c=40dtbH3EO_3szoJT>Vt))h77z|9olV~lFGIgvP@ z_f1@%dMw~->L0ce3)B7(L2;;)^Ce`@)r{a*%KS7R3Sn}slJk5F=6U%MVSL4Q!N#)@ zL4}8*gB5{BoX-r;_UnEi_KQQD_qbXh-p9oLICL84`@};({xtoGwt9}8)t+6>ipwnD z`2usr5B~mbcj5Bqp%6{mVdzz!cm%-tbn{cmKP_#0=Gs6gM9Kx}zi_>39nS8W^Qq>X z%P)QMndjA6$RCCRBEqzG;xG!p0EHt}#V_%OOYbTEh_?r8n*{jgeD(RGim_>xt99vQ zJCE&BnD&}jnt-f9Vh13wO{kouzrOTJjYD;cPTECap2pRa2wx4oW}*uq(D_RAh)$!* z)qSMjr*>8B-nmsIwKDu`MQP55qc7ZecW2|M3UW%d+sjs=sTr+c%K4u3>Ghr;T3`5k zi0ztf>Iv~26!Zq-SS$1*wj9t{yDm7e%rM(vxBpL5|zN+4W>CCL}DI zKk3xjH(?Tc7Phk86CwlubDhtOlk0r0=#dDgoNq(#ss8%e_us$qwe1wfUzE09Jd|ie zhB)7x&w8Zejon-7KL`bOSrl<+H?M@-|6e6yoh=lD+n_pgMeWvr&Egk8pz|&1D^2$m zj4}5HVUxl+AJ@g_gl}8u{U(A5kBAM-ar~CR-a{fFNq5!pn}MnhU&%xs4qS4ZqZBDSOZ zp&ICXw|LgPtM6_3y#{AZwXlp9OOY$u`SNkW3wskEf7E9@6x6)yd`o%J`(^I;88Vi` zMP+?LbZZaW&c~QzYjhuf(zh{IRVQnK$cKV{M698n^C9mITQaBb+ghs<^w6O=)G@WU zhs$S$=K-i+7d^TGstb1q2nUIuI@}VD5$nj^E>xU?l_IDkRJMpX!tKJ_H3y0P9kEBa zA{ulu#ukV1Aramc85fJgos4dGN|%T>o#E8aVp0#($oT|$fY0(_<;O2*eyiqH#G+9! zQe1~~Lya0DX#hkGMMxJwoJcAGUB#~65DpU0QK&#Uk|cHwhft~rzph3NU8ShnoD?S! zFEI-#(atBr-w6sxto6pkj_^orMVzmex8E^4b>6f$UcZ%6QM}6vd&I=SfWbm%^p$_g zG1VkWSW(tQt7r4ga7b&zI zQjwAl1v+0>KN{1i`+fSi_2IwjhO#mAQKC6xSnPOyAC#;N7Ypq$-_9u&Ra51V;z~M> zixT>HNCXp<{~wFt-~ydbxfhIY>XWC<=0;UHcUuJmoln2Nz0S3Fc*Vz#-qH&e7ijjs ztn8h&v@4_vxxHSuNBF;$W{2#q#_lRC5WV`sXW1JAXAh8@^-eW@4LLi-Zy)NWW{@Rf zU0)-zW*|1VynC>6S>n;tdk0j90xzVogqIp9e(q~j4|G0EA5s4LSAA=Jgu4Tdi9`{^ zUNcC<5mY}>jb!ip0>st+7#XT}DR<9^%T&L;?rN5iGfANHZTzK&^ZIvtJhUo`R0Bqu zRTGFf^Z=}n6`yf}y4{|dlH_Ep)bE(KrKA64S+WXp#@4Tj5Kb5W)QYO)lJ);@R~#O4 z4Ut9OwXC}aPfbS4#APl(&x$7p0S<~@Nq|%0B)QusrvyrP=A>8-*{Y#aBcyAkg{wi-s5e z_9<%mWghg@bfKnCrFSezUN0~C8C|h^!ZI^@xD{Qp$iw;Bk ze%oU6n{V^#NQKh*4F_}ml(d^q%>ETAa<`$W#aXr81aWwjQDwzQqja%-P2K6Ap=!9Y#)X`=dxE^nSo)jas`M*Wb&4J4h!|W}yd^@C z(Zigt*H_j%oxQSW>?S+fk-RMbLnJ{VIyBXOnwdA|jOT@ZAu-<=dId;=(0QF70C*t# z=bV8zs~s`)4oGQ<vKwC((8#;?bAhE%Ov`U5-S(Y)}?6JI6ce&OrFXAG`m>}&D> zJyCQSgIvz<6NGyLLUp| z4@DrLrt{+nmm5;=Y_`siDJ)KbZ@&De_@O>YC5-% zY6;H*c>wshfnz)}{IqBkZI`k@u3RF)M z8y4Tnyk88a@VL;kc@G*7gG5p$mWplT(S1~+$|#Eyl_ns?Hd0l1Cg2+Xwqz%dD&=AE z>;#x^BZ8;>N10MZgr>q?e=#8q3b$7?SS015%|_%IUixIQD-{Zh#M^{zV(6q|^=Ri; zGmdYY=ktlH!?|0bb$(al{T7$>?r&F<_e<@deII(&pGW15Gw0_}>gBd?^82Ma+zo|?+9U0{@MK^V zm!B#Zk{O$NbWYm&&Nx*jhPm^LE8BjXHgCYHpgnd<1RslqiABg7==?-W^@iUCeZFLM zJ(U$Vsz-{$P|&l*XVi0k;w69ck;oi*Il_?uLJUZi+2hz6Tnl2v9#D>=SIh!AncDfR>w|=M;F{UPq=P;(Cogcs{ z)o<#8Z&%d(Qz=B@MT9AGw|qzI;_l`O(Q5{3f|%Q-*>$pO7a%LiF+(>McQ0?@gdAHk zKB{-ht`m6g5ZE!kW4PVLf*QB?PI0Cx)mLvH=4#aaH-vIRaABvQmCWSlg&S3Bb}By= zBki`3IFpJRt!hMWlX{p{?c8c&RWs@h;mhgj_E~G77*1iynaa-#Eu^waySf@>$^%uW z;%eJ1WjkWewaVueIq>;wv5Ui42|H{+Q6mTay!wmRsFosvF$Jh z;DLr+p;S?82DIr56GX8Zgp7e6s--|D?~7j!z9vwX-ACSIC>$1k?eB^m(_@$4Dw z_PUf5-bGnW@XFS$xZ2M8mx`2?s#@B<1E@lXQ6XnjIZN7uc^ZKICY@?kXDOSzKo^7|-2X*}&F?=rINAU}} za>9JbSaX->gRxi$;ET3U9DE3Rw`Wcfe6HpQ^F(WM$zm|zAD$!*6pQDeUVX%oIk*>i zLIl%~l7=G#onLASs9ZUqk{71iPAIwEyV&wAnqL}$G8prwFsG?N;`HZ@4KeZ!gKV})ZTt2?MdTqwI z*9I)xV(48U;r;GI;%#INoClV#gNz<{s^MP2}w0bmW!LT{Q_9NG!S0)@{dj1 zSH)%RT1fc%fb(NU-J8F*G2r+SuKemgh(~mr2ix~}OpYML;=eJVx^2apf6&8fb56WR zg}c_z-hjzkV3SpAKJ=X5%NqDbi{3wUF1!Sf)YJD^(F+RL-q@8s?JM85={YO))w8=^ zKbhiLIAn(ZSBmzA^|l`{G?l+%3zyeWFqm!ZmRiR}Z@F zqThX3=k9S)0=Mj)T|l7oJA%Rf8PJayr-oZAbKQwkPSHMIudE|k6RRl9z* z_O!>=1h0DJMcjP3R(IC|c@bm}%)9M$olWlFVg~oRawF+nw%l6y)M7bE6j4hpxA$-_ z{PyK6@ZJs`74I%WU%JPJ9Sxy2PIX1#W6-BGg3 zOtiN~ej;$O;SSvMq>Rz}<6|rR^orMTgq-&!FvT7g6M3w(@lGbN0-2KirBIQy-)r?9DbPZfp@=ZR`2LATOR{0 z77yz2`asn>c2ED$KAMkmDxrDRyAF2e4AAYK>BrwyS&DQ{BA33q->lbZ`Zm>@uxCvW ziPO@3=b51qm_m|Oiu3bvhCT~#Jn?D&o$nH-6}~8u#vJMVASUXb~EP8Nz<7-ENCmzuc(a!ufGKpEm73^GW!LuXDKZ zpdY=^`Te~bS110oy-VE%hK>Zzvf89A;%DSFW^NJQD~t$Z*%lGM!f0A{3*NBg1A$k! zii{OVKe1ITSYdQEpkHaF>YsNnk3AP2l@~_!n$FMmb$;pQ`Ok-(=Zj4!7_)NZ?@x(D zXtZ#C@ULaTqkXSe8~uW#5v|GJ1l02Wdcx(hu`wF7`F13#hQ1o87+MIlz0Z3>ist(W`{Vx z(ular+n3G~Enk!iythy1MA9k@%m>8`!bP!s73PP#yqSYHp1Atm6kkEXsk>hC4GX@n zlZzKE>N`KL=vU?2u8rq+JZuLbobSWDwNAb#>%CsQy4pxwvBn6iQ6BdX*^%eOM8Bg? zt9T=3z0}cqh|X(_5v4{>PTR9-jq%PVk@1D$@&nsEq&fbEaGi<`%^o`)jJUks2x>xU zc1}iemeR1#ZM_IL6%%FR)GW~}xKxm+eai4Hh29`G95m{z_}wT|VJdS@%?hzch1m6_ NQD@IYPpOo${|oVSbz1-c delta 32211 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 Qvw8B#gW>H;j4buP0DO_DGynhq diff --git a/package.json b/package.json index a98a0af..5467714 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@mdx-js/loader": "3.0.1", "@trivago/prettier-plugin-sort-imports": "4.3.0", + "@types/bun": "1.1.9", "@types/mdx": "2.0.13", "@types/node": "20.14.2", "@types/react": "18.3.3", diff --git a/src/cli/bestWorstGoalDifference.ts b/src/cli/bestWorstGoalDifference.ts new file mode 100644 index 0000000..dc14138 --- /dev/null +++ b/src/cli/bestWorstGoalDifference.ts @@ -0,0 +1,36 @@ +/** + * Calculate the user with the best and worst goal difference. + */ +export function calculateBestAndWorstGoalDifference( + goalsScored: Map, + goalsConceded: Map, +) { + let bestUserId: number | null = null; + let worstUserId: number | null = null; + let bestGoalDifference = 0; + let worstGoalDifference = 0; + + goalsScored.forEach((scored, userId) => { + const conceded = goalsConceded.get(userId) || 0; + const goalDifference = scored - conceded; + + // If the goal difference is better than the current best, update the best. + if (goalDifference > bestGoalDifference) { + bestGoalDifference = goalDifference; + bestUserId = userId; + } + + // If the goal difference is worse than the current worst, update the worst. + if (goalDifference < worstGoalDifference) { + worstGoalDifference = goalDifference; + worstUserId = userId; + } + }); + + return { + bestUserId, + worstUserId, + bestGoalDifference, + worstGoalDifference, + }; +} diff --git a/src/cli/config.ts b/src/cli/config.ts new file mode 100644 index 0000000..8f79eb3 --- /dev/null +++ b/src/cli/config.ts @@ -0,0 +1,15 @@ +let locale = 'en-GB'; + +/** + * Set the locale to use for formatting dates. + */ +export function setLocale(newLocale?: string) { + if (newLocale) locale = newLocale; +} + +/** + * Get the locale used for formatting dates. + */ +export function getLocale() { + return locale; +} diff --git a/src/cli/filterExcludedUsers.ts b/src/cli/filterExcludedUsers.ts new file mode 100644 index 0000000..3ce07dd --- /dev/null +++ b/src/cli/filterExcludedUsers.ts @@ -0,0 +1,15 @@ +import { Match } from '@prisma/client'; + +/** + * Filter out matches where the challenger or defender is in the excluded list. + */ +export function filterExcludedUsers( + matches: Array, + excludedIds: Array, +): Array { + return matches.filter( + (match) => + !excludedIds.includes(match.challenger_id) && + !excludedIds.includes(match.defender_id), + ); +} diff --git a/src/cli/getHumanReadableDate.ts b/src/cli/getHumanReadableDate.ts new file mode 100644 index 0000000..16ebc9e --- /dev/null +++ b/src/cli/getHumanReadableDate.ts @@ -0,0 +1,13 @@ +import { getLocale } from './config'; + +const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', +}; + +export function getHumanReadableDate(date: string): string { + const locale = getLocale(); + return new Date(date).toLocaleDateString(locale, options); +} diff --git a/src/cli/goalsPerUser.ts b/src/cli/goalsPerUser.ts new file mode 100644 index 0000000..1a0f3bd --- /dev/null +++ b/src/cli/goalsPerUser.ts @@ -0,0 +1,41 @@ +import { Match } from '@prisma/client'; + +/** + * Helper function to update goals scored and conceded for a user. + * Combines goals scored and conceded update into a single operation. + */ +function updateGoalsMap( + userId: number, + scored: number, + conceded: number, + goalsMap: Map, +) { + const userStats = goalsMap.get(userId) || { scored: 0, conceded: 0 }; + userStats.scored += scored; + userStats.conceded += conceded; + goalsMap.set(userId, userStats); +} + +/** + * Calculate goals scored and conceded per user from a list of matches. + */ +export function calculateGoalsPerUser(matches: Array) { + const goalsMap = new Map(); + + matches.forEach( + ({ challenger_id, defender_id, score_challenger, score_defender }) => { + updateGoalsMap(challenger_id, score_challenger, score_defender, goalsMap); + updateGoalsMap(defender_id, score_defender, score_challenger, goalsMap); + }, + ); + + const goalsScored = new Map(); + const goalsConceded = new Map(); + + goalsMap.forEach((stats, userId) => { + goalsScored.set(userId, stats.scored); + goalsConceded.set(userId, stats.conceded); + }); + + return { goalsScored, goalsConceded }; +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..6f3cc15 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,49 @@ +import { parseArgs } from 'util'; +import { setLocale } from './config'; +import { filterExcludedUsers } from './filterExcludedUsers'; +import { calculateGoalsPerUser } from './goalsPerUser'; +import { logResults } from './logResults'; + +async function loadFiles(dataDirPath?: string) { + const [matches, users] = await Promise.all([ + import(`${dataDirPath || ''}/Match.json`).then((mod) => mod.default), + import(`${dataDirPath || ''}/User.json`).then((mod) => mod.default), + ]); + + return { matches, users }; +} + +/** + * Import the data files, and log the results. + * This is the main function that is called when the script is run. + */ +async function calculateAndLogResults() { + const { + values: { dataDirPath, excludeUserIds = '', locale }, + } = parseArgs({ + args: Bun.argv, + options: { + dataDirPath: { type: 'string' }, + excludeUserIds: { type: 'string' }, + locale: { type: 'string' }, + }, + strict: true, + allowPositionals: true, + }); + + setLocale(locale); + + console.log('Excluding users for ranking:', excludeUserIds); + + const { matches, users } = await loadFiles(dataDirPath); + const excludedUsers = excludeUserIds.split(',').map((id) => parseInt(id, 10)); + const filteredMatches = filterExcludedUsers(matches, excludedUsers); + + // Only calculate the goals scored and conceded for the filtered matches. + // This is to ensure that the excluded users are not included in the results for whatever reason + const { goalsScored, goalsConceded } = calculateGoalsPerUser(filteredMatches); + + logResults({ matches, users, goalsScored, goalsConceded }); +} + +calculateAndLogResults(); diff --git a/src/cli/logResults.ts b/src/cli/logResults.ts new file mode 100644 index 0000000..f999434 --- /dev/null +++ b/src/cli/logResults.ts @@ -0,0 +1,121 @@ +import { Match, User } from '@prisma/client'; +import { calculateBestAndWorstGoalDifference } from './bestWorstGoalDifference'; +import { getHumanReadableDate } from './getHumanReadableDate'; +import { calculateMatchStats } from './matchStats'; +import { calculateMatchesPerMatchDay } from './matchesPerMatchDay'; +import { calculateMostFrequentMatchup } from './mostFrequentMatchup'; +import { calculateMostGoalsAgainst } from './mostGoalsAgainst'; + +/** + * Get the user name from the user ID. + */ +function getUserName( + userId: number | null, + userLookup: Map, +): string { + return userId !== null ? userLookup.get(userId) || 'Unknown' : 'Unknown'; +} + +/** + * Get the user ID with the most statistics from a map of user IDs and statistics. + */ +function getEntryWithMostStatistics( + entries: Map, +): [number, number] { + return [...entries.entries()].reduce((a, b) => (b[1] > a[1] ? b : a), [0, 0]); +} + +type LogResultsArgs = { + matches: Array; + users: Array; + goalsScored: Map; + goalsConceded: Map; +}; + +export function logResults({ + matches, + users, + goalsConceded, + goalsScored, +}: LogResultsArgs) { + const userLookup = new Map(); + users.forEach((user) => userLookup.set(user.id, user.name)); + + console.log(`Number of matches played: ${matches.length}`); + + const { + matchDays, + matchesPlayed, + matchesLost, + largestMatchResult, + largestResult, + } = calculateMatchStats(matches); + console.log(`Number of match days: ${matchDays.size}`); + + const { worstUserId, worstGoalDifference, bestUserId, bestGoalDifference } = + calculateBestAndWorstGoalDifference(goalsScored, goalsConceded); + console.log( + `Worst goal difference: ${getUserName(worstUserId, userLookup)} with goal difference ${worstGoalDifference}`, + ); + + const [mostMatchesPlayedId, mostMatchesPlayedCount] = + getEntryWithMostStatistics(matchesPlayed); + const [mostMatchesLostId, mostMatchesLostCount] = + getEntryWithMostStatistics(matchesLost); + + console.log( + `User with most matches played: ${userLookup.get(mostMatchesPlayedId)} with ${mostMatchesPlayedCount} matches`, + ); + console.log( + `User with most matches lost: ${userLookup.get(mostMatchesLostId)} with ${mostMatchesLostCount} losses`, + ); + + console.log(`Number of active players: ${matchesPlayed.size}`); + + const mostFrequentMatchup = calculateMostFrequentMatchup(matches); + if (mostFrequentMatchup.matchup) { + const [user1, user2] = mostFrequentMatchup.matchup; + console.log( + `Archrivals: ${userLookup.get(user1)} vs ${userLookup.get(user2)} with ${mostFrequentMatchup.count} matches`, + ); + } else { + console.log(`No frequent matchups found.`); + } + + // const largestMatchResult = calculateMostGoalsAgainst(goalsConceded); + + if (largestMatchResult) { + console.log( + `Largest result difference: ${largestResult} goals on the following match days:`, + ); + + largestMatchResult.forEach((match) => { + console.log( + `\t${getHumanReadableDate(match.date)}: ${userLookup.get(match.challenger)} ${match.challengerGoals} - ${match.defenderGoals} ${userLookup.get(match.defender)}`, + ); + }); + } + + const king = getUserName( + users.find((user) => user.level === 1)?.id || null, + userLookup, + ); + + console.log(`King of BLACO: ${king}`); + + console.log( + `Best goal difference: ${getUserName(bestUserId, userLookup)} with goal difference ${bestGoalDifference}`, + ); + + const { mostGoalsAgainst, mostGoalsAgainstUser } = + calculateMostGoalsAgainst(goalsConceded); + + console.log( + `User with most goals conceded: ${getUserName(mostGoalsAgainstUser, userLookup)} with ${mostGoalsAgainst} goals against`, + ); + + console.log(`Matches per match day:`); + calculateMatchesPerMatchDay(matches).forEach((count, date) => { + console.log(`\t${date}: ${count}`); + }); +} diff --git a/src/cli/matchStats.ts b/src/cli/matchStats.ts new file mode 100644 index 0000000..0a6d0ce --- /dev/null +++ b/src/cli/matchStats.ts @@ -0,0 +1,89 @@ +import { Match } from '@prisma/client'; + +type MatchStats = { + matchesPlayed: Map; + matchesLost: Map; + matchDays: Set; + largestResult: number; + largestMatchResult: Array | null; +}; + +export type LargestMatchResult = { + challenger: number; + defender: number; + date: string; + challengerGoals: number; + defenderGoals: number; +}; + +/** + * Calculate match statistics from a list of matches. + */ +export function calculateMatchStats(matches: Array): MatchStats { + const matchesPlayed: Map = new Map(); + const matchesLost: Map = new Map(); + const matchDays: Set = new Set(); + + let largestResult = 0; + let largestMatchResult: Array | null = null; + + matches.forEach((match) => { + // Add the match day to the set + const matchDay = match.played_at.toString().split('T')[0]; + matchDays.add(matchDay); + + // Increment the number of matches played for each challenger and defender + const challengerMatches = matchesPlayed.get(match.challenger_id) || 0; + matchesPlayed.set(match.challenger_id, challengerMatches + 1); + + const defenderMatches = matchesPlayed.get(match.defender_id) || 0; + matchesPlayed.set(match.defender_id, defenderMatches + 1); + + // Increment the number of matches lost for the defender and challenger + if (match.score_challenger > match.score_defender) { + const defenderLosses = matchesLost.get(match.defender_id) || 0; + matchesLost.set(match.defender_id, defenderLosses + 1); + } else if (match.score_defender > match.score_challenger) { + const challengerLosses = matchesLost.get(match.challenger_id) || 0; + matchesLost.set(match.challenger_id, challengerLosses + 1); + } + + // Update the largest result difference and store the match details + const resultDifference = Math.abs( + match.score_challenger - match.score_defender, + ); + + // If the score is the same, add the match to the list of largest results + // If the score is larger, update the largest result and store the match details + if (resultDifference === largestResult) { + if (largestMatchResult) { + largestMatchResult.push({ + challenger: match.challenger_id, + defender: match.defender_id, + date: match.played_at.toString(), + challengerGoals: match.score_challenger, + defenderGoals: match.score_defender, + }); + } + } else if (resultDifference > largestResult) { + largestResult = resultDifference; + largestMatchResult = [ + { + challenger: match.challenger_id, + defender: match.defender_id, + date: match.played_at.toString(), + challengerGoals: match.score_challenger, + defenderGoals: match.score_defender, + }, + ]; + } + }); + + return { + matchesPlayed, + matchesLost, + matchDays, + largestResult, + largestMatchResult, + }; +} diff --git a/src/cli/matchesPerMatchDay.ts b/src/cli/matchesPerMatchDay.ts new file mode 100644 index 0000000..cdce42a --- /dev/null +++ b/src/cli/matchesPerMatchDay.ts @@ -0,0 +1,16 @@ +import { Match } from '@prisma/client'; +import { getHumanReadableDate } from './getHumanReadableDate'; + +export function calculateMatchesPerMatchDay( + matches: Array, +): Map { + const matchDays = new Map(); + + matches.forEach((match) => { + const matchDay = match.played_at.toString().split(' ')[0]; + const count = matchDays.get(getHumanReadableDate(matchDay)) || 0; + matchDays.set(getHumanReadableDate(matchDay), count + 1); + }); + + return matchDays; +} diff --git a/src/cli/mostFrequentMatchup.ts b/src/cli/mostFrequentMatchup.ts new file mode 100644 index 0000000..398ba71 --- /dev/null +++ b/src/cli/mostFrequentMatchup.ts @@ -0,0 +1,31 @@ +import { Match } from '@prisma/client'; + +export type MostFrequentMatchup = { + matchup: [number, number] | null; + count: number; +}; + +export function calculateMostFrequentMatchup( + matches: Array, +): MostFrequentMatchup { + const matchupCounts = new Map(); + + matches.forEach(({ challenger_id, defender_id }) => { + const key = [challenger_id, defender_id].sort((a, b) => a - b).join('-'); + const count = (matchupCounts.get(key) || 0) + 1; + matchupCounts.set(key, count); + }); + + let mostFrequentMatchup: [number, number] | null = null; + let highestCount = 0; + + matchupCounts.forEach((count, key) => { + if (count > highestCount) { + highestCount = count; + const [user1, user2] = key.split('-').map(Number) as [number, number]; + mostFrequentMatchup = [user1, user2]; + } + }); + + return { matchup: mostFrequentMatchup, count: highestCount }; +} diff --git a/src/cli/mostGoalsAgainst.ts b/src/cli/mostGoalsAgainst.ts new file mode 100644 index 0000000..4ae54f7 --- /dev/null +++ b/src/cli/mostGoalsAgainst.ts @@ -0,0 +1,16 @@ +export function calculateMostGoalsAgainst(goalsConceded: Map) { + let mostGoalsAgainstUser: number | null = null; + let mostGoalsAgainst = 0; + + goalsConceded.forEach((conceded, userId) => { + if (conceded > mostGoalsAgainst) { + mostGoalsAgainst = conceded; + mostGoalsAgainstUser = userId; + } + }); + + return { + mostGoalsAgainstUser, + mostGoalsAgainst, + }; +} diff --git a/tsconfig.json b/tsconfig.json index ec8fdef..4daaaaf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "target": "ES2015", "paths": { "@blaco/*": ["./src/*"] } From 6bfb1ef931767693783915f535da0bd77bd55618 Mon Sep 17 00:00:00 2001 From: Mathijs Rutgers Date: Thu, 19 Sep 2024 13:16:40 +0200 Subject: [PATCH 2/2] Update with table output --- bun.lockb | Bin 216984 -> 217728 bytes package.json | 1 + src/cli/logResults.ts | 108 ++++++++++++++++++++++++------------------ src/cli/matchStats.ts | 2 - 4 files changed, 62 insertions(+), 49 deletions(-) diff --git a/bun.lockb b/bun.lockb index 3aa07ec9327845c825e9ad340541804642b260fb..0ad654eb6aad36e844aef1054e5d91841defc17c 100755 GIT binary patch delta 32976 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 YRv!-9VGZ0Nw#JnzbNK5{rPh@HKPW6i9RL6T diff --git a/package.json b/package.json index 5467714..1a65e17 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/node": "20.14.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "cli-table3": "0.6.5", "eslint": "8.57.0", "eslint-config-next": "14.2.3", "typescript": "5.4.5" diff --git a/src/cli/logResults.ts b/src/cli/logResults.ts index f999434..263fa8b 100644 --- a/src/cli/logResults.ts +++ b/src/cli/logResults.ts @@ -1,4 +1,5 @@ import { Match, User } from '@prisma/client'; +import Table from 'cli-table3'; import { calculateBestAndWorstGoalDifference } from './bestWorstGoalDifference'; import { getHumanReadableDate } from './getHumanReadableDate'; import { calculateMatchStats } from './matchStats'; @@ -41,59 +42,47 @@ export function logResults({ const userLookup = new Map(); users.forEach((user) => userLookup.set(user.id, user.name)); - console.log(`Number of matches played: ${matches.length}`); + const statistics = new Table({ head: ['Description', 'User'] }); - const { - matchDays, - matchesPlayed, - matchesLost, - largestMatchResult, - largestResult, - } = calculateMatchStats(matches); - console.log(`Number of match days: ${matchDays.size}`); + statistics.push(['Number of matches played', matches.length]); + + const { matchDays, matchesPlayed, matchesLost, largestMatchResult } = + calculateMatchStats(matches); + + statistics.push([`Number of match days`, matchDays.size]); const { worstUserId, worstGoalDifference, bestUserId, bestGoalDifference } = calculateBestAndWorstGoalDifference(goalsScored, goalsConceded); - console.log( - `Worst goal difference: ${getUserName(worstUserId, userLookup)} with goal difference ${worstGoalDifference}`, - ); + + statistics.push([ + `Worst goal difference`, + `${getUserName(worstUserId, userLookup)} with goal difference ${worstGoalDifference}`, + ]); const [mostMatchesPlayedId, mostMatchesPlayedCount] = getEntryWithMostStatistics(matchesPlayed); const [mostMatchesLostId, mostMatchesLostCount] = getEntryWithMostStatistics(matchesLost); - console.log( - `User with most matches played: ${userLookup.get(mostMatchesPlayedId)} with ${mostMatchesPlayedCount} matches`, - ); - console.log( - `User with most matches lost: ${userLookup.get(mostMatchesLostId)} with ${mostMatchesLostCount} losses`, - ); + statistics.push([ + `User with most matches played`, + `${userLookup.get(mostMatchesPlayedId)} with ${mostMatchesPlayedCount} matches`, + ]); + + statistics.push([ + `User with most matches lost`, + `${userLookup.get(mostMatchesLostId)} with ${mostMatchesLostCount} losses`, + ]); - console.log(`Number of active players: ${matchesPlayed.size}`); + statistics.push([`Number of active players`, matchesPlayed.size]); const mostFrequentMatchup = calculateMostFrequentMatchup(matches); if (mostFrequentMatchup.matchup) { const [user1, user2] = mostFrequentMatchup.matchup; - console.log( - `Archrivals: ${userLookup.get(user1)} vs ${userLookup.get(user2)} with ${mostFrequentMatchup.count} matches`, - ); - } else { - console.log(`No frequent matchups found.`); - } - - // const largestMatchResult = calculateMostGoalsAgainst(goalsConceded); - - if (largestMatchResult) { - console.log( - `Largest result difference: ${largestResult} goals on the following match days:`, - ); - - largestMatchResult.forEach((match) => { - console.log( - `\t${getHumanReadableDate(match.date)}: ${userLookup.get(match.challenger)} ${match.challengerGoals} - ${match.defenderGoals} ${userLookup.get(match.defender)}`, - ); - }); + statistics.push([ + `Archrivals`, + `${userLookup.get(user1)} vs ${userLookup.get(user2)} with ${mostFrequentMatchup.count} matches`, + ]); } const king = getUserName( @@ -101,21 +90,46 @@ export function logResults({ userLookup, ); - console.log(`King of BLACO: ${king}`); + statistics.push([`King of BLACO`, king]); - console.log( - `Best goal difference: ${getUserName(bestUserId, userLookup)} with goal difference ${bestGoalDifference}`, - ); + statistics.push([ + `Best goal difference`, + `${getUserName(bestUserId, userLookup)} with goal difference ${bestGoalDifference}`, + ]); const { mostGoalsAgainst, mostGoalsAgainstUser } = calculateMostGoalsAgainst(goalsConceded); - console.log( - `User with most goals conceded: ${getUserName(mostGoalsAgainstUser, userLookup)} with ${mostGoalsAgainst} goals against`, - ); + statistics.push([ + `User with most goals conceded`, + `${getUserName(mostGoalsAgainstUser, userLookup)} with ${mostGoalsAgainst} goals against`, + ]); + + console.log(statistics.toString()); + + if (largestMatchResult) { + const matchesTable = new Table({ + head: ['Date', 'Largest result difference'], + }); + + largestMatchResult.forEach((match) => { + matchesTable.push([ + `${getHumanReadableDate(match.date)}`, + `${userLookup.get(match.challenger)} ${match.challengerGoals} - ${match.defenderGoals} ${userLookup.get(match.defender)}`, + ]); + }); + + console.log(matchesTable.toString()); + } + + // Display the number of matches played per match day + const matchesPerMatchDayTable = new Table({ + head: ['Date', 'Number of matches'], + }); - console.log(`Matches per match day:`); calculateMatchesPerMatchDay(matches).forEach((count, date) => { - console.log(`\t${date}: ${count}`); + matchesPerMatchDayTable.push([getHumanReadableDate(date), count]); }); + + console.log(matchesPerMatchDayTable.toString()); } diff --git a/src/cli/matchStats.ts b/src/cli/matchStats.ts index 0a6d0ce..c29ca86 100644 --- a/src/cli/matchStats.ts +++ b/src/cli/matchStats.ts @@ -4,7 +4,6 @@ type MatchStats = { matchesPlayed: Map; matchesLost: Map; matchDays: Set; - largestResult: number; largestMatchResult: Array | null; }; @@ -83,7 +82,6 @@ export function calculateMatchStats(matches: Array): MatchStats { matchesPlayed, matchesLost, matchDays, - largestResult, largestMatchResult, }; }