From e4b6770f6fdf5d847470d5572030c6fb3be688a4 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Sun, 7 Jul 2024 09:04:10 +1200 Subject: [PATCH 01/10] add template formula example images --- docs/images/template-formulas-1.png | Bin 0 -> 10031 bytes docs/images/template-formulas-2.png | Bin 0 -> 11907 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/template-formulas-1.png create mode 100644 docs/images/template-formulas-2.png diff --git a/docs/images/template-formulas-1.png b/docs/images/template-formulas-1.png new file mode 100644 index 0000000000000000000000000000000000000000..3127156c978a9c4aa2b8eee0f53dd74151b4c47f GIT binary patch literal 10031 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 CFoC)N literal 0 HcmV?d00001 diff --git a/docs/images/template-formulas-2.png b/docs/images/template-formulas-2.png new file mode 100644 index 0000000000000000000000000000000000000000..099b7c8258952058eea1bf90065de07e3825688a GIT binary patch literal 11907 zcmcI~XIN8R*CwbSi1a2zdKUuHLX$3CK#B!IkswVHklu?Zy%#A0QbiE~0~n->BoygA z)JSiJBE8SS$M^f*x#pUgcYe$dAUW)__uBj1Ypr|T>qOqy(;&abe2svBfLu%St|0*d zAqx0>Ktc@s4i;*`0)GhI3^h~;%6e|D0AH@yDeEc|5Fq2pPOON4@1)N)P230wC|WLl z2-}=LJS89yzM*wj+1T3xpWPWh-UoZ-{fr9(N zSIKTzT$Z)3m!-SCcm1boi`=Dpsu4*kD3_{=bLUiP67->Js!$1s`*FhVs;{TKxybGv z_HV(#xZjLV6R4ZnEHgUTGqz3%aO<2eJGwZzf^x3&M%R0Kp!8oK4pLQy8!rrbc@COE zdfwi4POY#mv=6i&6V}SMIa$azKqaKQ;a4z(uNVD4_=Kb^e2V_X=c8g-G*+Hb)JZAo zrzu>g@^QSb^>EAzwztlI@bQ5@^_%f0^*KlM}-YiQuPcrer)PgOCA)$yPnM{T8 z5tWIV#~gzfcbt%z)N7buMYYkQJvHty-pIQNMfQP7=k>CA z$#Y!3C*DT>r)bSCTb@Y_U4HK}tZXAR=6ieG(A*#1=l`8m^D zVj50&w10+NJ0jeO9_NzMu((CE?NiuFbhCXEHItYrD9Kuk93mh%<)Y2hHvW~gIYaMH zgQu()@yce649e^7?k;>l47>c~u@d~U3SB0939rCJ@A;WxlYY?I za&?EXX#I7O2}sk}VftCy)cNasu~*{<&bHWsvhEFEhGjzbmxrA#-%EKVbtr!;KOA1R zynTfRsixMW)gZM_(cp7Bv4dZ8R#$YG^%)JdsM}uLbszI9SC2nNWj}gQt~KJ&)pYi; zCNOqql1Xmg;@#ScY}sH*&zeBs*;zii=pFjFgDQoIHS~EQM9StkBXoBm=(zlyYe{97 z#FOIFU8qR$dE3I0;(61vx|2mIHg31o8GU%|My6U0*|Uyg0RhxjuU?KG4s^Jma?+Ui z7WZUHcTV1>sp$8TRAi3oQ2=vkP?nTq$I-55&s(l0@-962kuEpt!#C36-PQ*z3$o@- zPF|OW30LGORYnr}`KZoom3vaHP>J| zedXI*yB1Wb#Ob2@VauKA-I`ame*c;f)+yKP9yeqv!!1d((7Hh?%jXM*fuq6US`O6v z&Yb6v2?I2FXskEsTNtM#EyFlv(7}F(ae#kh?D2=yWV2V@`Ky8Vjj$MV-}t9Pf|H@W zma5zfy*cc|M4=~d!M5X_WKB8ai{F<&ZF&&|z%C&%*=inPlc2n#Scwd*_S-t!wwKu1 zA#h(&dl zBuahYf>aY1O1xH^`MtIA+AofI?NK?{Tzy>Ojeo$UQ)_=@aZkRSoY$4QIV_7jj_I+` z*cLp_BCQhc%0J-Sn<8I5{P5n^c6GRL*-6vg2Y%+rEm>*9%uHS)Q!<2M@fIojgNipr zLpK7X4V@>T)n_@fiFb22I?mnd7_vL&VLI2!5`KYUdlMo1(c8^^bXsQVc#pab#Y*s_ zbe*?N_ce(ZdfAK)tSZ8!nYQHx%QT$r_P>mn1=X9?2QIyXV!x2@BqA}3>yu{hOQ|fa z_{z65IZyU&CAr2AIb--!nT&A+DXt;SM~FpC^c zWHx#P*EOtGaxC6DU+kSTxz63i=9+pO%!Ym>w2YP zF3ezeTC;C`xZS3Pqknm7<>87X-53*lg}AS|aJ0=v_k;B^ayMrS&HJNeeK}_RSJK_p zXmE&}N%7aNC1LN&8Ef~G)p|Fsg2nf#Oqk3gb!kjMu8um-e4*QHPcxUPL2OAv*1(#% zNA4L1u&=$V`^kcd>M)(6K}^Y8MThD~ifTw6n8Qw>`*iPu@h8iV03j|6p?`1IW`Z=pfswQ2Xrg2ej1VAE^4By}&aAOm3G<0W(6&kjHE-Qi94 zYR|66w|o07x5OR=DcmWZIH>!QuSjSQg}dg&9e;SBw0qo~4DBwe)HGWYumh zVA2?l>23J{(Rlmd{qYj6Y0G~_=ErExogVv`AWMh4k9qC;7&(zzLAmCE3yJwGnHyg* zEZ}u%&)Gm8kDKxv7Wk#Nhl+i|9aZRu8fJqjP>`YZq@&93UjGV`93{ggKNJen8ekxl zqW#p?MR!_XrFl4XLi;F?YTBrSKW1q-;OxXr8df=;|8>L9Wj<}DjLT1VYfWlm-h*xw zROEdcGmVlZ3YXDQcgH` zO@JJJiD?aIIHZ~{G_+<%IkbeIhIvZITe>yRX!XglqX-F+9+%O?-#+uxft&(xBHosp z3W@U)TC9rG|D;8RpWi3KsuKkv^@;?`BB4NP-cR4#+e^YIjczu21DQ5ukXuGZA-|=i zW!@y1%sDf8t|bfWZTa(*P)SjM4rJ%(sMFxH)lntqJ+ZCuGk_z~Vc=wGIMP&#@p;Uo zm^XA2GK1J=d({7I1UhDaCh`POjr134mekoQEjsG)d^>s%s;%{AFVHDU(Q z%=Lw{cxAKE=i~tSmEwwWt=2niMZ4E{&v(2{v1!@5ue9$N>s=~L!nU)u2BTlGZWNfL z5R)aT`Xzbqif+77o+{t`gYfhhw~xAB2cM;qp%ESScxFLmyEb>YH3tmSsV@hl1a2s zZf)c>Yv$r?YvGso=nU?uuL^Og3lFrrbCO{magz?2*m@UJL1~eiovRWiBQaH<>dGi+ z=vPc4gmPSH=`j*Eh;}q}s6=Ty5mScVZ_X`x6N2v4VTTI1R7MiEaG^`WjP|C*Gz_=UAbnrlu&xE+B>387mNol`eGui}y~&-5O9 z!xujk%gvu%+v(C}Bd2McVkhq-s{7^xm1*K4#}`iRqUd%E6O*E==-W^U`h>W{uOBtz zrINvZ_?5x;Lk}BIe(Sg>&^+v;q{^9TvL46bG-OGf{Ty-*^h#iLwY4-$TvkM<1Y1mv z=84AciF+**7UO)4gCDWN!oui7&Bw}$O8$${B8^nh!UKWc6FSFeFH`!7!^0YBHd4Xm zs}a3-yf>AEYBi5%5F&V5T*gOiPfyP$hsLQ!nb=oMJD1URXPe=Q5o(MMiuL$+0kd*O zv>+XXm)GbUFJIqhx?EPoz~ULW^RsTF9WD(dS9 zy87{xHn2%_Mx-*%1}57?K1AD43*`9!j(qIymlLj7z!V{8;znPzlS7eBGD&L-2 zOYE}93-V3xKAtMK$YTuL&1f*|{pV!F6R~E+H8448>CZ(kfBc~777-e>uewL}B50PP zmQ5l~5cOWdG*02Bi)BTsYLx5wSpaf*PfBW~uAc;7$EJ;Fx%ST%v9RPqV2d5ed?W6f zGc5@&E|U{VYc7?x(NogZxGI5rwae)EMhSac_rg& zKFP6YyK=hSWi(a6U!7CCk+)@qXfO3tIQV%PuoWWC4Vm5>P}s^=2huNdn^~ZpUE3Q9 zt^vMv={#1wYn5yHo z%7s4%zc6End}*{2PM`dGf`&M(Pu6fTOeuu-24{sT5E9?nUULEv$34awaZGao=&ot?l4`84HQxxkP*#A|*Av&+p&vfI#R5s4T(u_9mRUNJ6}0jp5WmiNJYP z$-<2EpW~Suw#(0>uH4)qMBDY@YRR0B75Zwig_f+Og8UK@y}3;uj1lCdy#EiM)088@ zjls^_C4+UaUdV9|p2D$q9gly?tO#vXdB_ImzO0i5gT`Sy6d zY3u2?IdQ6qUsqsj!k4Kt2@G~@*xams@b?9-ZAzXtCxV(gX| zavIfp-{Xa}+~~gHeSJ=EOckX`O(ddPCgd|2(5(ip;`_Q|fyqZNDsg1TREoT&dzi;X zCtvMKm-@?fedDx_OegnF90wdd*+xs}!&#>T2mA|9H&GjxSt7u`Eg$of>4~85e$aUa z<4Wwj=tpq&2$2$F%qRBc?=~ViH_zO8o>g(-+k_>y6gnB#zy4f1ecXuK3G_O;KQ8j) zD1jkbAp0rPcFXY4vFVv)bSJc&kcp29>O~5Lck~|VS{{Wxq^XM5*;|2ne>&{IQQO}X zhOgf#?3Q4FE(CV&zZ^Jyw#zZ+6Jzr`;Q4rfu6Or|_oHuA6X+M%TAfYUAW_)+DXFSA zapM*DI3aHGvP48w4m|bUl`FL{!3k$ISmz@BejWs4tB^y1UX-h!dUtpcUvXDu>7Xfv zx9j1JdEKyPqyi$|2p;3Xk&PCXAc8qo;iAx~nuS%AUj$HLmPpZNS0&2!7N2N^GvTH@WjR+!V_; zn>SN&T6C-|RQz>na>*t@lL0zTIsAK4F0LR>m ztv!pwwnhs=xV!E(^^qhgC2(g)=>}js6tF}10;&o37ubXm@l%z{NDfR!U{FcMQ*`!j z=jbOzIRP>>#GD;cxT^$@=yex_tbF#RnY%CE_Nz$-NiW8$SZVoD{r=Gsh;aJNPRWWYQqI+QcW4jw-}NgE1Bz6OJWG~fZ%Yk{Ls9xTHk2x)F^ z-oLQGV{T^FcX%k1ot@pkvLcY3mDRVkCCb9W(myl9!E*Cv-`SZ09*^%|Ul+#z`qj6$ z2f5Ha78iLDaCqO*k!)#cY5(f#typy~9@K#mQ3!iPlcw6}qm;s-wGYecM&^y^3qc?48V1gD8X)*;WohS#l z9r2f`Km)#CD|NQ13uAZQH!&qqQ!+9{l)9Ukz*oe2d;_k+R6|J7VF&jfoyKbDj*x=1 z?{`FWCqD51I*=humDvD)fgRY`kJ)vnx(G5A_>d!lLwitI*+YUr~Q;W}K zv^7 zhe1u=y!V+qM#;YTx6AioAdP1?Bgkj|>rMdGn{(2Jb4qZMtD(eOOgq)})y?5RR$3Bi3+R zZK6@|A7pAySVcvZNU&vCM4?>L%mCVz-`S~C`}J$?_O_d>oLs@+ppmSMOy1m_wXCcx zK;>0srKNLEPXop$CJLse%*V&Z^6+@X`1p8!cen017Mr`f>tPCo77PzRV2)MShMNsh zT~QvtZe(os?y!Bwuqu{m4@hMJXyRbuI|Nl)1};SS!D<90^2NvtY%pb1)Y{>oA{RJJ zV0_P<@Xtl1G)>Hn42vTYG_mPb*yYCC8f1pl>f>aC;r+~)|G7p{X%~ z$0ZY^L-hn=fLUE%Vd7~u4L3T-e#lSN z#AiDrKN%oj(0d2O-Ap-MrQ0#KRK)ngV1X3+HW#I2i0dsQB zeJ|Zvqxq;~d1QKsPY>ieNJ?T+_HUSSvHATICwIU?@vk5~HtXmw;oG<1l89{uU- ziVZnZ`Ps`Su1L(_4djbkDl||FiLHnzwd6DlAr4l=m;TnluIFgm>EzO8D^0yQz_3@T z{%QGzJOSlN5qM$|vSM;VEx0e86NWl-Y%z+8ATk%tuuzKmbu6qPbYRBKYy#h^sL17Y zp3BLYiRjk!=hQl-)m)9x)ddj=y*8Q2WSfDdUFel0StyHJ11vwY>X-U?c0Hl4pJ62w!!{>Y8F=3f#zmH{Km#0 zAVTBU*9UrfC=kxhgCiph2q&k3)>h)u^76s)apuypvH^PuL)ct?7(2-SjEGWeqYa2brPiM0~ zP1U46_1&6H9Z$E4jCF?8e!i4fT3S|pjYTyBl<@ocfc8m{BNwDVnZ9Nxxzo+_W4zW zvZlijOXY5?T0m53tNo|@!MVSKd!iP<3mBm-~vT=)$;yTSH*9NH9U$Q5r(8}w;ze+0Q#FR>)43H(Uzwt_@@THky@ zMFr$7a1)B&RC^-}{}jk2v(~?2z$m)8vJNMgmI%b{dqSv?HiDltRoEe>=#Cjp|2AG7 z0k(K9V2EfOb_kGTlyBEyId4C^EJ>mTcLv+yiZO#Y33cPKW!*1O!)FEH=SZ7yYMCvj z_*AShp?Iq3E5=Ttp2=8Hzdc2$6UpY;j~hnt{8w7N>{b12HN!jc)8l-xWm~>|r z-Wb?vcLul_GM-KtRq6vrzPOLX5G(lsryE8K^aPp-kc5^>@Tt%jSf$%a@NbL&S+in? zP+k95P@RszudYT3i-@eu&W7~#_OAT;6{DxGzcN4nDlR^LWqCP*f|7FO=g;81{e1^# z=Xg&qF9$oja8z}*gQH_KPlpnyTrey+=|kuT#4%?(&lUnj0 z%Hayq8m*(ijh|g*ONEO*g-Sbt(2I2Z`-3%y9cIzs#tOelRva~Z(MOh@7Y-4+Q@b39 zt4h2=NTmI5t}3CUK!+Z%b4tRs^%95^RHFO3Hhe6voye3`tFxn+0m><<>i$vwHFUtK z{m0q7#W2Ouw|Q9SJ}m)v2u-G}(7`2n#tt<$=9AXh>?-rv#i1%N%4H+Td<2O3N@!cV zs(PvpMC8L)5P{b)+KrIz0uk`Xuem0>T}OZB3g$+6yW`Pg<+hoEDu1SQJr-WFlKr}aGOHQoip5cp^*s$(-SoF6KD_eTmDJ;fY=2U7cvUo!|JX1DYx|-&0=&XE z(n$P`LkOuRa9CF%REEue9Pjp2+7<-l8{HcYXyWfJ3?KqLKnHy3{sXL~|9^FiRoYur zidH*&cKYs2F<7Ow=goxQt`=bn4TuITZnzzLk^W^!|EIL{Z;}2wdgR8;Z<8lf|MGBR zfd4DNkKQECSxhN1&@##x=&f>A8`QaEYj`xzSFEXoDF2KC*HNL|vhNFi{V8g%_Z()# z$l0S;Q!Nq!&xvE>tFd<(ljLoKTN86Zc)o-&Dvl(gAyRf0zaTH8r)FVeL@^wwyzlv9 z)Elm4PF5*Xq%6TMBsy^aF9CBZXl>bn;nTTIUFA8sXKjyG(l)N$cACgVw@NXTDyq0~ zMIyFPK22Y)1Xx0rew++|od&cdzl)F3R?pD}onK^N0QfW+K0y*1?zZ<<1UNF{OMH62 zR9x_+Fl-ViT3F~tb(x82Vj4YXG*}qE2-X_Nj31$g>ODJzIpBzi+=1isr%u7?I><+QL$me2ioQc2nzPYATv4pp}S)iLOw;77Z!kLV;8W>7w&*i(LW4bVAEl zybwL1YW`IBE;OJNwcir?;LTD|zxrjXw<{Qpx+}O+PYu>6s1997Y%o*4hAAqv*3|3# z6o#E`iAln`XEM?KQ22KI{R-?df;ts@#fmhX)0|_0h<-c*EJK$O&;|h`U09+i6_EioVYXXU;$UbZ9!-{WDN$g)_YvbT z>)Do!jIN|z0TW(P4Ub%LfZjv{G?3>s5F9CCTDio7JY(&a@s|NS<|a~?3JPa8HbWBj zG2}&->i>JnxDi2q1!yMxFF|`H7F$+Uw*32d5}@iXpPXa?8twAVPKutM-txggdR$!G z^5$j&1qFqDP*4ugT(I}@O7ryewDwR%A!4Tw?l ziZT_OC|QX@-lZ*Cb?2bHyJ^iAS%dK=f&Y^zUFHYu_)yX6pfn|TuE)O!+yCSl|95`Z zLX}y&jC4uvmsl)?N6kg)nseR0<+_fE8U)3LWT5Gxo~7*vzB!X`Dc45`-q2=Yhe9KE z1@?P9SV|#FZxTl&+3;k1@e{4rRpBZRQ#p~t_0cG9R85U0Os^jZP9TQYq{1QB$2s|t z0xnEqo#v4^dn1*B!5&Ov>2Ayf}C*Sa+eXl3?1VMbc_Ch z1*Va0f@}vU4YvKttEQ+7E$(l*{&j($7d3b8VW<}M{GG`4o+~dA4*3q({GyTY!bt>C-mi)@^kqnS z-mmBs$oP%9Oc3*-ysJ>3@6e3@Fao1U9`sGB*pcYB(WGtf>nE6&&jdWZgnWS_(T4Zn zQr;Qrz=F;ftDy1SdSf|-u}lfr)_6e(6>k}qz#j06k;>cu0gQIk;g23Ya`*AkVq|1= z4-C|^v$J#e_0{I%<8${18aNsn?%v*-SFc{JYHHH=^Yg2!tJ9H_ldEcI(1k*wRW&tQ zAP}gku~E;_(XpzwR=c2IGcq=s84zyb(?cKRNW3^8EvQ!X%kO*wB!qA>3Ic`O7~a@h z347nuCz^EFA$_vIv0W6$|2wDr-_gFA2!u`K;?mNfM2&4BH)M@LlAXj1X)_w-UUuj} zBo#;0o$&yPsm|`Oq%`au)e^bi1|jXwE7FYWW*~0S?8BWoW0_EIQbD>>)u zU}aWx+Cm)`vxOq#HycF(i=h{_!LLl;D1(CHyAnNMnosB1s?6dDr#3@GV{Sw1=}R2YTa1 zcj!7ee?)dOJiHS!mzV(*4iXp6avxLVbnBVsd${}XpQWgP+$E{)=qc?wOPwjG5Q8QD zlaBI%mQL|cIf#k&tySZoxp`_2F)c_)NfbAv?gI}lnMtkZh_#zK(^m)$k-nJrGn%2d zQ%jde;iZ92TdE-l@$!xjcc(XBe7c>mHkZT{1BTj2s8ZTZ%IR*W3S9N&em1boIPeS@ zI`5uOrN_}^(dabdLD62bZKgqPsoANxhamAANL?C-3o1kpB0z}x!9aR{fm3_;*7x?E z0gb&*git$q}%HVchxI zeEH~PyLY9sSD^rSJ1IrsaGv()U@m^;?DTl2UxTeboGnmue|@TeQsE$YI_Ny0)^A^U zsxcsQ+HXU?)?t`RW`e#e5#&3p7p1U=9V8g5v;l}xBKN&) zFaNm=ewwt_yeRx`l;VTj0y>TV*VwzYp@p6N(J$${3455mnKcKy!x74h1|7c;*A5rd(CAyap_?vU|rqL9vNjDIBPPs$8B`wUK-f(j)oyF--M@0=jeb2DMO>1+!*#7(J_Cw(9%w};2ysd zR16`nfpP3>Qa$ol8}epZ^xd^mwSM(L)5$~w{(5a-Hu|Zj6w_%&_?_(0wx$hk<~Gi`10&*s^xcd=)Xqow>5*oZxAI{&Oh9Zn zLAxX19#;%(E$+!L6$Im_0UI)Os>0}G)@dEd{Ayg_C(whRYX}|Q!0hjP#u8N8dRZP8 zFD+VA1m=32H)YeA`Cg?j(<d6nwd8g!iRVqKPG_Cg@x! zb$I60FQtB~;A9c8W5mbgUt(75@$aeX(;ypP8(QldOu}AE1=B!lDcl-j-sW^W8hfoe z>9o=m9x|ufs)$yb<41FIaK02?LBrlGJ&nT^73!vm?mHPV8R*GmwaJy=9yNT?%Pa8H zy|p=<2h9)6-lpA-%7B5i$GcXCq2_P*E+ciL{W`0mtPa&Q`$l#oPL1 z^a<~j#R7g$Q?})d6|6mjd&|TntPH=mRlD=sq zd%kMT?X5%}?{A#W+A+!O?O-{BjGWVj#G>zl-pGjeL{z2m7YTH;@u<&@um{1^O<_ zNbEy Date: Sun, 7 Jul 2024 09:23:03 +1200 Subject: [PATCH 02/10] add template formula example to readme --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0c09119..acb24c4e 100644 --- a/README.md +++ b/README.md @@ -959,9 +959,25 @@ var value = new Dictionary() }; MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` +#### 10. Formulas +##### 1. Example +Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: +![image](docs/images/template-formulas-1.png) -#### 10. Other +When the template is rendered, the `$` prefix will be removed and `$enumrowstart` and `$enumrowend` will be replaced with the start and end row numbers of the enumerable: +![image](docs/images/template-formulas-2.png) + +##### 2. Other Example Formulas: + +| | | +|------------|-----------------------------------------------------------------------------------------| +|Sum |`$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | +|Alt. Average|`$=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}})`| +|Range |`$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | + + +#### 11. Other ##### 1. Checking template parameter key From d18c37c03e51a75e61fa0e297b2bac348b8f8332 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Sun, 7 Jul 2024 09:23:03 +1200 Subject: [PATCH 03/10] add template formula example to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index acb24c4e..28722f44 100644 --- a/README.md +++ b/README.md @@ -963,9 +963,11 @@ MiniExcel.SaveAsByTemplate(path, templatePath, value); ##### 1. Example Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: + ![image](docs/images/template-formulas-1.png) When the template is rendered, the `$` prefix will be removed and `$enumrowstart` and `$enumrowend` will be replaced with the start and end row numbers of the enumerable: + ![image](docs/images/template-formulas-2.png) ##### 2. Other Example Formulas: From 89ca3073ad46549db7cc8e3a412c1505cdf6ad8c Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:11:39 +1200 Subject: [PATCH 04/10] get basics working --- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 20 +++++++++++++++++++ tests/MiniExcelTests/MiniExcelOpenXmlTests.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 79e211d6..afbb2451 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -263,6 +263,9 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable //cols:width WriteColumnsWidths(writer, props); + //sheet view + WriteSheetViews(writer); + //header writer.Write(WorksheetXml.StartSheetData); var yIndex = 1; @@ -389,6 +392,23 @@ private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable writer.Write(WorksheetXml.EndCols); } + private static void WriteSheetViews(MiniExcelStreamWriter writer) { + /* + + + + + + + */ + writer.Write("" /*TODO:WorksheetXml.StartSheetViews*/); + writer.Write("\t"); //TODO: do we need tabSelected and workbookViewId? + writer.Write("\t\t"); + writer.Write("\t\t"); + writer.Write("\t"); + writer.Write("" /*TODO:WorksheetXml.EndSheetViews*/); + } + private static void PrintHeader(MiniExcelStreamWriter writer, List props) { var xIndex = 1; diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index 5af73bb3..c217e224 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -1020,7 +1020,7 @@ public void SaveAsBasicCreateTest() Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); - File.Delete(path); + //File.Delete(path); } [Fact()] From 1c8d8986c129252b06c6dd901eb9b00a3049196b Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:40:45 +1200 Subject: [PATCH 05/10] add config option --- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 21 +++++++++++++------ src/MiniExcel/OpenXml/OpenXmlConfiguration.cs | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index afbb2451..2de008ef 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -263,9 +263,6 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable //cols:width WriteColumnsWidths(writer, props); - //sheet view - WriteSheetViews(writer); - //header writer.Write(WorksheetXml.StartSheetData); var yIndex = 1; @@ -301,6 +298,10 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable writer.Write(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); } + //sheet view + WriteSheetViews(writer); + + writer.Write(WorksheetXml.Drawing(currentSheetIndex)); writer.Write(WorksheetXml.EndWorksheet); @@ -392,7 +393,7 @@ private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable writer.Write(WorksheetXml.EndCols); } - private static void WriteSheetViews(MiniExcelStreamWriter writer) { + private void WriteSheetViews(MiniExcelStreamWriter writer) { /* @@ -401,10 +402,18 @@ private static void WriteSheetViews(MiniExcelStreamWriter writer) { */ + + // exit early if no style to write + if (!_configuration.FreezeTopRow) { + return; + } + writer.Write("" /*TODO:WorksheetXml.StartSheetViews*/); writer.Write("\t"); //TODO: do we need tabSelected and workbookViewId? - writer.Write("\t\t"); - writer.Write("\t\t"); + if (_configuration.FreezeTopRow) { + writer.Write("\t\t"); + writer.Write("\t\t"); + } writer.Write("\t"); writer.Write("" /*TODO:WorksheetXml.EndSheetViews*/); } diff --git a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs index d4d320ac..8c3e1eae 100644 --- a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs @@ -8,6 +8,7 @@ public class OpenXmlConfiguration : Configuration public bool FillMergedCells { get; set; } public TableStyles TableStyles { get; set; } = TableStyles.Default; public bool AutoFilter { get; set; } = true; + public bool FreezeTopRow { get; set; } = true; public bool EnableConvertByteArray { get; set; } = true; public bool IgnoreTemplateParameterMissing { get; set; } = true; public bool EnableWriteNullValueCell { get; set; } = true; From de044395bd19f352e2774abbae47daefe0e96328 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:11:15 +1200 Subject: [PATCH 06/10] add WritePanes method, remove FreezeTopRow, add FreezeRowCount and FreezeColumnCount options, add unit test --- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 113 +++++++++++++++--- src/MiniExcel/OpenXml/OpenXmlConfiguration.cs | 3 +- tests/MiniExcelTests/MiniExcelOpenXmlTests.cs | 39 +++++- 3 files changed, 133 insertions(+), 22 deletions(-) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 2de008ef..f6a6d75f 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -260,6 +260,9 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex))); } + //sheet view + WriteSheetViews(writer); + //cols:width WriteColumnsWidths(writer, props); @@ -298,10 +301,6 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable writer.Write(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); } - //sheet view - WriteSheetViews(writer); - - writer.Write(WorksheetXml.Drawing(currentSheetIndex)); writer.Write(WorksheetXml.EndWorksheet); @@ -394,30 +393,108 @@ private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable } private void WriteSheetViews(MiniExcelStreamWriter writer) { - /* - - - - - - - */ - // exit early if no style to write - if (!_configuration.FreezeTopRow) { + if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0) + { return; } + // start sheetViews writer.Write("" /*TODO:WorksheetXml.StartSheetViews*/); writer.Write("\t"); //TODO: do we need tabSelected and workbookViewId? - if (_configuration.FreezeTopRow) { - writer.Write("\t\t"); - writer.Write("\t\t"); - } + + // Write panes + WritePanes(writer); + + // end sheetViews writer.Write("\t"); writer.Write("" /*TODO:WorksheetXml.EndSheetViews*/); } + private void WritePanes(MiniExcelStreamWriter writer) { + /* + freeze row + + + + freeze row and column + + + + + + Freeze column + + + */ + writer.Write($"\t\t 0) + { + writer.Write($" xSplit=\"{_configuration.FreezeColumnCount}\""); + } + if (_configuration.FreezeRowCount > 0) + { + writer.Write($" ySplit=\"{_configuration.FreezeRowCount}\""); + } + var topLeftCellRef = ExcelOpenXmlUtils.ConvertXyToCell( + _configuration.FreezeColumnCount + 1, + _configuration.FreezeRowCount + 1 + ); + writer.Write($" topLeftCell=\"{topLeftCellRef}\""); + + string activePane; + if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0) + { + activePane = "bottomRight"; + } + else if (_configuration.FreezeColumnCount > 0) + { + activePane = "topRight"; + } + else + { + activePane = "bottomLeft"; + } + writer.Write($" activePane=\"{activePane}\" state=\"frozen\"/>"); + + // write pane selections + if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0) + { + // freeze row and column + /* + + + + */ + var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, 1); + writer.Write($""); + + var cellBL = ExcelOpenXmlUtils.ConvertXyToCell(1, _configuration.FreezeRowCount+1); + writer.Write($""); + + var cellBR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, _configuration.FreezeRowCount+1); + writer.Write($""); + } + else if ( _configuration.FreezeColumnCount > 0 ) + { + // freeze column + /* + + */ + var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount, 1); + writer.Write($""); + } + else + { + // freeze row + /* + + */ + writer.Write(""); + } + + } + private static void PrintHeader(MiniExcelStreamWriter writer, List props) { var xIndex = 1; diff --git a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs index 8c3e1eae..218d9f42 100644 --- a/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs +++ b/src/MiniExcel/OpenXml/OpenXmlConfiguration.cs @@ -8,7 +8,8 @@ public class OpenXmlConfiguration : Configuration public bool FillMergedCells { get; set; } public TableStyles TableStyles { get; set; } = TableStyles.Default; public bool AutoFilter { get; set; } = true; - public bool FreezeTopRow { get; set; } = true; + public int FreezeRowCount { get; set; } = 1; + public int FreezeColumnCount { get; set; } = 0; public bool EnableConvertByteArray { get; set; } = true; public bool IgnoreTemplateParameterMissing { get; set; } = true; public bool EnableWriteNullValueCell { get; set; } = true; diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index c217e224..1a543f65 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -56,10 +56,10 @@ public void SaveAsControlChracter() string path = GetTempXlsxPath(); char[] chars = new char[] {'\u0000','\u0001','\u0002','\u0003','\u0004','\u0005','\u0006','\u0007','\u0008', '\u0009', // - '\u000A', // - '\u000B','\u000C', + '\u000A', // + '\u000B','\u000C', '\u000D', // - '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', + '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', '\u0017','\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F','\u007F' }; var input = chars.Select(s => new { Test = s.ToString() }); @@ -823,6 +823,39 @@ public void SaveAsByIEnumerableIDictionary() } } + [Fact()] + public void SaveAsFrozenRowsAndColumnsTest() { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + + var config = new OpenXmlConfiguration + { + FreezeRowCount = 1, + FreezeColumnCount = 2 + }; + + MiniExcel.SaveAs( + path, + new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} + }, + configuration: config + ); + + using (var stream = File.OpenRead(path)) { + var rows = stream.Query(useHeaderRow: true).ToList(); + + Assert.Equal("MiniExcel", rows[0].Column1); + Assert.Equal(1, rows[0].Column2); + Assert.Equal("Github", rows[1].Column1); + Assert.Equal(2, rows[1].Column2); + } + + Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); + + //File.Delete(path); + } + [Fact()] public void SaveAsByDapperRows() { From cb5a8d84d5f4be5f2a4e5788a9137822731f55e6 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:49:10 +1200 Subject: [PATCH 07/10] add xml methods to WorksheetXml --- .../OpenXml/Constants/WorksheetXml.cs | 25 ++++++++ .../OpenXml/ExcelOpenXmlSheetWriter.cs | 64 +++++++------------ 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs index 5f1f7202..6b625222 100644 --- a/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel/OpenXml/Constants/WorksheetXml.cs @@ -13,9 +13,34 @@ internal class WorksheetXml internal static string Dimension(string dimensionRef) => $"{StartDimension}{dimensionRef}\"/>"; + internal const string StartSheetViews = ""; + internal const string EndSheetViews = ""; + + internal static string StartSheetView( int tabSelected=1, int workbookViewId=0 ) + => $""; + internal const string EndSheetView = ""; + internal const string StartSheetData = ""; internal const string EndSheetData = ""; + internal static string StartPane( int? xSplit, int? ySplit, string topLeftCell, string activePane, string state ) + => string.Concat( + ""); + + internal static string PaneSelection( string pane, string activeCell, string sqref) + => string.Concat( + $""); + internal static string StartRow(int rowIndex) => $""; internal const string EndRow = ""; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index f6a6d75f..a6da0ba4 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -12,6 +12,8 @@ using System.IO.Compression; using System.Linq; using System.Text; +using System.Xml; + using static MiniExcelLibs.Utils.ImageHelper; namespace MiniExcelLibs.OpenXml @@ -400,47 +402,18 @@ private void WriteSheetViews(MiniExcelStreamWriter writer) { } // start sheetViews - writer.Write("" /*TODO:WorksheetXml.StartSheetViews*/); - writer.Write("\t"); //TODO: do we need tabSelected and workbookViewId? + writer.Write(WorksheetXml.StartSheetViews); + writer.Write(WorksheetXml.StartSheetView()); // Write panes WritePanes(writer); // end sheetViews - writer.Write("\t"); - writer.Write("" /*TODO:WorksheetXml.EndSheetViews*/); + writer.Write(WorksheetXml.EndSheetView); + writer.Write(WorksheetXml.EndSheetViews); } private void WritePanes(MiniExcelStreamWriter writer) { - /* - freeze row - - - - freeze row and column - - - - - - Freeze column - - - */ - writer.Write($"\t\t 0) - { - writer.Write($" xSplit=\"{_configuration.FreezeColumnCount}\""); - } - if (_configuration.FreezeRowCount > 0) - { - writer.Write($" ySplit=\"{_configuration.FreezeRowCount}\""); - } - var topLeftCellRef = ExcelOpenXmlUtils.ConvertXyToCell( - _configuration.FreezeColumnCount + 1, - _configuration.FreezeRowCount + 1 - ); - writer.Write($" topLeftCell=\"{topLeftCellRef}\""); string activePane; if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0) @@ -455,7 +428,16 @@ Freeze column { activePane = "bottomLeft"; } - writer.Write($" activePane=\"{activePane}\" state=\"frozen\"/>"); + writer.Write( WorksheetXml.StartPane( + xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : (int?)null, + ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : (int?)null, + topLeftCell: ExcelOpenXmlUtils.ConvertXyToCell( + _configuration.FreezeColumnCount + 1, + _configuration.FreezeRowCount + 1 + ), + activePane: activePane, + state: "frozen" + ) ); // write pane selections if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0) @@ -467,13 +449,13 @@ Freeze column */ var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, 1); - writer.Write($""); + writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR)); var cellBL = ExcelOpenXmlUtils.ConvertXyToCell(1, _configuration.FreezeRowCount+1); - writer.Write($""); + writer.Write(WorksheetXml.PaneSelection("bottomLeft", cellBL, cellBL)); var cellBR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, _configuration.FreezeRowCount+1); - writer.Write($""); + writer.Write(WorksheetXml.PaneSelection("bottomRight", cellBR, cellBR)); } else if ( _configuration.FreezeColumnCount > 0 ) { @@ -482,7 +464,8 @@ Freeze column */ var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount, 1); - writer.Write($""); + writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR)); + } else { @@ -490,9 +473,10 @@ Freeze column /* */ - writer.Write(""); + writer.Write(WorksheetXml.PaneSelection("bottomLeft", null, null)); + } - + } private static void PrintHeader(MiniExcelStreamWriter writer, List props) From 7516b7e60c30e4fc10ff7830c13f6cf58e9fdba5 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:55:48 +1200 Subject: [PATCH 08/10] remove unused namespaces --- src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index a6da0ba4..4f94fc25 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -1,5 +1,4 @@ -using MiniExcelLibs.Attributes; -using MiniExcelLibs.OpenXml.Constants; +using MiniExcelLibs.OpenXml.Constants; using MiniExcelLibs.OpenXml.Models; using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; @@ -7,14 +6,10 @@ using System.Collections; using System.Collections.Generic; using System.Data; -using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; -using System.Xml; - -using static MiniExcelLibs.Utils.ImageHelper; namespace MiniExcelLibs.OpenXml { From c032b57ffd2c77c542b6aca666e6626e92098ba3 Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:59:46 +1200 Subject: [PATCH 09/10] revert disable file delete --- tests/MiniExcelTests/MiniExcelOpenXmlTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index 1a543f65..e5c59f05 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -1053,7 +1053,7 @@ public void SaveAsBasicCreateTest() Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); - //File.Delete(path); + File.Delete(path); } [Fact()] From 65d0f7f0c805ff8cfe7417ca017548b62e79560b Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:38:25 +1200 Subject: [PATCH 10/10] add freeze panes feature to table and idatareader --- .../OpenXml/ExcelOpenXmlSheetWriter.cs | 6 ++ tests/MiniExcelTests/MiniExcelOpenXmlTests.cs | 62 ++++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 4f94fc25..35d1e430 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -133,6 +133,9 @@ private void GenerateSheetByIDataReader(MiniExcelStreamWriter writer, IDataReade } maxColumnIndex = props.Count; + //sheet view + WriteSheetViews(writer); + WriteColumnsWidths(writer, props); writer.Write(WorksheetXml.StartSheetData); @@ -331,6 +334,9 @@ private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable va var prop = GetColumnInfosFromDynamicConfiguration(columnName); props.Add(prop); } + + //sheet view + WriteSheetViews(writer); WriteColumnsWidths(writer, props); diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index e5c59f05..5155b3cc 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -825,7 +825,6 @@ public void SaveAsByIEnumerableIDictionary() [Fact()] public void SaveAsFrozenRowsAndColumnsTest() { - var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); var config = new OpenXmlConfiguration { @@ -833,27 +832,56 @@ public void SaveAsFrozenRowsAndColumnsTest() { FreezeColumnCount = 2 }; - MiniExcel.SaveAs( - path, - new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} - }, - configuration: config - ); + { + // Test enumerable + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + MiniExcel.SaveAs( + path, + new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} + }, + configuration: config + ); - using (var stream = File.OpenRead(path)) { - var rows = stream.Query(useHeaderRow: true).ToList(); + using (var stream = File.OpenRead(path)) { + var rows = stream.Query(useHeaderRow: true).ToList(); - Assert.Equal("MiniExcel", rows[0].Column1); - Assert.Equal(1, rows[0].Column2); - Assert.Equal("Github", rows[1].Column1); - Assert.Equal(2, rows[1].Column2); + Assert.Equal("MiniExcel", rows[0].Column1); + Assert.Equal(1, rows[0].Column2); + Assert.Equal("Github", rows[1].Column1); + Assert.Equal(2, rows[1].Column2); + } + + Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); + //File.Delete(path); } + + { + // test table + var table = new DataTable(); + { + table.Columns.Add("a", typeof(string)); + table.Columns.Add("b", typeof(decimal)); + table.Columns.Add("c", typeof(bool)); + table.Columns.Add("d", typeof(DateTime)); + table.Rows.Add("some text", 1234567890, true, DateTime.Now); + table.Rows.Add(@"Hello World", -1234567890, false, DateTime.Now.Date); + } + var pathTable = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + MiniExcel.SaveAs(pathTable, table, configuration: config ); - Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path)); + Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable)); + + + // data reader + var reader = table.CreateDataReader(); + var pathReader = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + + MiniExcel.SaveAs(pathReader, reader, configuration: config); + Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable)); + } - //File.Delete(path); } [Fact()]