From 5739a2f482e4f1fddefc1ee587a9f66ebe967fd4 Mon Sep 17 00:00:00 2001 From: Alexander Johr Date: Sun, 7 Apr 2024 16:49:20 +0200 Subject: [PATCH] Fixed Bug that Merge Cells were not parsed --- lib/src/parser/parse.dart | 60 ++++++++++++++------ lib/src/sheet/sheet.dart | 24 ++++++++ test/excel_test.dart | 57 +++++++++++++++++++ test/test_resources/spannedItemExample.xlsx | Bin 0 -> 10014 bytes 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 test/test_resources/spannedItemExample.xlsx diff --git a/lib/src/parser/parse.dart b/lib/src/parser/parse.dart index 5bb899a3..3c0f7295 100644 --- a/lib/src/parser/parse.dart +++ b/lib/src/parser/parse.dart @@ -166,18 +166,25 @@ class Parser { }); } + /// Parses and processes merged cells within the spreadsheet. + /// + /// This method identifies merged cell regions in each sheet of the spreadsheet + /// and handles them accordingly. It removes all cells within a merged cell region + /// except for the top-left cell, preserving its content. void _parseMergedCells() { Map spannedCells = >{}; _excel._sheets.forEach((sheetName, node) { _excel._availSheet(sheetName); - XmlElement elementNode = node as XmlElement; + XmlElement sheetDataNode = node as XmlElement; List spanList = []; + final sheet = _excel._sheetMap[sheetName]!; - elementNode.findAllElements('mergeCell').forEach((elemen) { - String? ref = elemen.getAttribute('ref'); + final worksheetNode = sheetDataNode.parent; + worksheetNode!.findAllElements('mergeCell').forEach((element) { + String? ref = element.getAttribute('ref'); if (ref != null && ref.contains(':') && ref.split(':').length == 2) { - if (!_excel._sheetMap[sheetName]!._spannedItems.contains(ref)) { - _excel._sheetMap[sheetName]!._spannedItems.add(ref); + if (!sheet._spannedItems.contains(ref)) { + sheet._spannedItems.add(ref); } String startCell = ref.split(':')[0], endCell = ref.split(':')[1]; @@ -193,26 +200,43 @@ class Parser { start: startIndex, end: endIndex, ); - if (!_excel._sheetMap[sheetName]!._spanList.contains(spanObj)) { - _excel._sheetMap[sheetName]!._spanList.add(spanObj); + if (!sheet._spanList.contains(spanObj)) { + sheet._spanList.add(spanObj); + + _deleteAllButTopLeftCellsOfSpanObj(spanObj, sheet); } _excel._mergeChangeLookup = sheetName; } }); }); + } - // Remove those cells which are present inside the - _excel._sheetMap.forEach((sheetName, sheetObject) { - if (spannedCells.containsKey(sheetName)) { - sheetObject._sheetData.forEach((row, columnMap) { - columnMap.forEach((column, dataObject) { - if (!(spannedCells[sheetName].contains(getCellId(column, row)))) { - _excel[sheetName]._sheetData[row]?.remove(column); - } - }); - }); + /// Deletes all cells within the span of the given [_Span] object + /// except for the top-left cell. + /// + /// This method is used internally by [_parseMergedCells] to remove + /// cells within merged cell regions. + /// + /// Parameters: + /// - [spanObj]: The span object representing the merged cell region. + /// - [sheet]: The sheet object from which cells are to be removed. + void _deleteAllButTopLeftCellsOfSpanObj(_Span spanObj, Sheet sheet) { + final columnSpanStart = spanObj.columnSpanStart; + final columnSpanEnd = spanObj.columnSpanEnd; + final rowSpanStart = spanObj.rowSpanStart; + final rowSpanEnd = spanObj.rowSpanEnd; + + for (var columnI = columnSpanStart; columnI <= columnSpanEnd; columnI++) { + for (var rowI = rowSpanStart; rowI <= rowSpanEnd; rowI++) { + bool isTopLeftCellThatShouldNotBeDeleted = + columnI == columnSpanStart && rowI == rowSpanStart; + + if (isTopLeftCellThatShouldNotBeDeleted) { + continue; + } + sheet._removeCell(rowI, columnI); } - }); + } } // Reading the styles from the excel file. diff --git a/lib/src/sheet/sheet.dart b/lib/src/sheet/sheet.dart index dd81ae09..014be465 100644 --- a/lib/src/sheet/sheet.dart +++ b/lib/src/sheet/sheet.dart @@ -96,6 +96,30 @@ class Sheet { _countRowsAndColumns(); } + /// Removes a cell from the specified [rowIndex] and [columnIndex]. + /// + /// If the specified [rowIndex] or [columnIndex] does not exist, + /// no action is taken. + /// + /// If the removal of the cell results in an empty row, the entire row is removed. + /// + /// Parameters: + /// - [rowIndex]: The index of the row from which to remove the cell. + /// - [columnIndex]: The index of the column from which to remove the cell. + /// + /// Example: + /// ```dart + /// final sheet = Spreadsheet(); + /// sheet.removeCell(1, 2); + /// ``` + void _removeCell(int rowIndex, int columnIndex) { + _sheetData[rowIndex]?.remove(columnIndex); + final rowIsEmptyAfterRemovalOfCell = _sheetData[rowIndex]?.isEmpty == true; + if (rowIsEmptyAfterRemovalOfCell) { + _sheetData.remove(rowIndex); + } + } + /// /// returns `true` is this sheet is `right-to-left` other-wise `false` /// diff --git a/test/excel_test.dart b/test/excel_test.dart index 63bf3464..493e00bf 100644 --- a/test/excel_test.dart +++ b/test/excel_test.dart @@ -971,4 +971,61 @@ void main() { }); }); }); + + group('Spanned Items', () { + test("read spanned items", () { + var file = './test/test_resources/spannedItemExample.xlsx'; + var bytes = File(file).readAsBytesSync(); + var excel = Excel.decodeBytes(bytes); + + Sheet? sheet = excel.tables["Spanned Items"]!; + + testSpannedItemsSheetValues(Sheet sheet) { + final cells = + sheet.rows.expand((r) => r.where((c) => c != null)).toList(); + + expect(cells[0]?.value, equals(TextCellValue('spanned item A1:B1'))); + expect(cells[0]?.cellIndex, + equals(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0))); + + expect(cells[1]?.value, equals(TextCellValue('spanned item A2:A3'))); + expect(cells[1]?.cellIndex, + equals(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 1))); + + expect(cells[2]?.value, equals(TextCellValue('spanned item A4:B5'))); + expect(cells[2]?.cellIndex, + equals(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 3))); + } + + testSpannedItemsList(Sheet sheet) { + List spannedItems = sheet.spannedItems; + + expect(spannedItems[0], equals('A1:B1')); + expect(spannedItems[1], equals('A2:A3')); + expect(spannedItems[2], equals('A4:B5')); + } + + testSpannedItemsList(sheet); + + testSpannedItemsSheetValues(sheet); + + var fileBytes = excel.encode(); + if (fileBytes != null) { + File(Directory.current.path + '/tmp/spannedItemExampleOut.xlsx') + ..createSync(recursive: true) + ..writeAsBytesSync(fileBytes); + } + var newFile = './tmp/spannedItemExampleOut.xlsx'; + var newFileBytes = File(newFile).readAsBytesSync(); + var newExcel = Excel.decodeBytes(newFileBytes); + // delete tmp folder + new Directory('./tmp').delete(recursive: true); + + Sheet? newSheet = newExcel.tables["Spanned Items"]!; + + testSpannedItemsList(newSheet); + + testSpannedItemsSheetValues(newSheet); + }); + }); } diff --git a/test/test_resources/spannedItemExample.xlsx b/test/test_resources/spannedItemExample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..301c8976f8e181c11259b566786d64078a8d4c64 GIT binary patch literal 10014 zcmeHtg;!K-_y5q{-3%chDdAAkjdUXj0z)@Qhk$f-??RuNyG99{+ zh2a$y=+v#$Png!xGQ5)XnVME8Sg-t9EH!j$(Yu&6?1!J+I}{~{%-l1Ol32Fqx-d{< zGQ}#5g}ClNx>m@S4KAtDjjd0J7APRc+1*7QYeak8JY&XX%W%uUOe)ZT$+y;Zpt>Pd z86hss+k+iu08yFkIk}mjSBNGjrP6|4Tbl+ipDt5Pwi}HOSITKD8*#0A zOTz`7;^wDO;sWDZYYJg$ItiO9gfa(SI&8i$4E)*$#0T_NFHpG|=UYED^J4bQc=?%b z$&|}_H@OlC9!ZGNDNviJ^K7yG5s!74>2?U??|%h}yK@it5_K$QyWP49SL^AM80z!NS={;DB1yV$Y5TZTH*lWa-Xa0i|3=eB9UjIbxbHoN z-^GH{)Wj8R@5asb^Z$SJ{4eI@Uxr?lq@vctgB`Z3a24KvIx(MsFQx1$sqhY{9q>YN z4zDq`n2Bn>lZ6^zn=%ARKCnID>eKAJXzXS`@Z>vxRWu>72xF5cBrN^H%mt1RfK|6DOJSO5UssKZDfWd<33P%&6A zJF7;RkkZ z&sCVg+r+~D{h>^DC(*SZr)o~WCIdg=8Sk+CN9Ka#K*I+9{efKXUJyo4W&a*DIGV0( z0UojbNfHWW@1!Io0H6>a+VJ2r<7Ln7>Evo>=Hz7eGmcg1I5-sx;@<>}UZeGTVy#NCZWlc*@2E_^{uR9tctY4Tw1dwXJ!VsOoj>9&DFAB&)FD~xf&{YT72vpJ$#Iz342v;j#vUhgp% z-zOZ-TBDb|Y9y3C5Q`+GZf;raJI|W@;mBf)^Ynb2k zH~;jb2Gq(e-#oh+GBhC*w_+x{xhYo!DcWy^eBC^*NGllK8IwtSq{_VJ5M-L2s&?RY@pMiFI`$KVnrK*qaVG3 zvyq|yJ|Ay9tT`}1a=ckRvks{9&e`KO@G+H4 z-?C)$R1(!*VBS-yeaC&i6hdUJ9T9)rMyZ7gPOli<8ZCaMp2T#+XS6G6yJw8-nLZsW z;Y)(i-J-d1J-hhWc+0^{Pj!7$S=S>sLYU$n>v@UjClH-B_!aMO!o<_E0?AQlJxSiQ z;_b42jQ~9=e^uLF>HvM{`~r`X_h}e2CXK#h!+u^`9in76ZEySRW%QSinB>8nnOxy35Semt(z{*XXi&WXK)Kc5){oN*AjrwmuIH=Q-cfI6l#& zbW@~k7HL#mo8kAWiu-;@xM7H^M%K(#pg3Ma!}y~)@qqq`H(N=bWInMz!(wJ!PXF7S z&sZ+-y47u=*jzhDd~ssJFV~-M+rbjfYbMnF!xsy|0m=jz!fQc6Zr2wsx(!U zd*O-<>n5DXGuwlN;Hx_~159TN4;|XTHhT}q7jU#lb-&eEe^Qwn$tlcxckrXfsQ~VG zB%5Z|SqxELUz@q-Rf1^Mk$V zMh+=36h(2o0VC_PltW3V}y{Vea(d{ql5yY)Nh_ zMVaq(c9-0xE9G*e2H22HT zV%nIz2Vz+ zKV7A{r6H19^%Zvl%Ylr#^Qz%tZVdTT$H$`F9U+KI2m(?{tzM)bV6Bnd`jiYK$3(&m zWKU^LQIvq>xvk=4v8+#GgmFBGyu~{(9WNdSHkN$D)s?$WdsU-BGOUV0vdDfrNZt8j zZj!e0fzNJta_X9ZU{HCVBdW%(EP1H9A0g+vrx2PlG_i8RTC zwVn&>m9C3!c)sw2_4f9cSEM9L;)c-U%hpJ(0%Xe^v6IHV2&5ES>tdzt+x@MoX1Xh2 z{d;Fl2M@fkfVf=3cCRnB9@DVmpE)y*mQT<>t?=k*=aQD7`!Fr}d=y82IkTOoO{@^B zWW%1F(aNr5F(QD42?y$I&3X}t(^D=-lf`swl=O+GbhUDV8Hin4xlt9A+b^IJ8Mfpw z)p&kmwPzy{(0PFKf3N2rFNqs0fh}9ZyMcg=EQ=vsRBhD!9l4k|1KnNI_OWRD3rR9t zz3ofI+gqDxqa}pw*Lq~F&x?RBIsCQy^lhh6pkjdCZ0=;^vuJr}fPm}np_p=R`JP2{ zopB8%2O4`IL5SIti9@a*_gW(v#r69}P6X`Ktq2QG;`NK!667Bs>kvZyunFX*s8?1~ z_;7{Z;*GySk+rWSVoVdpiPN9frx;~w`ttsx+2+=KWQ~6;>sO+G$6#vMpc`QsoMwcqFlE~-8G7JE%ipw@mY(JX z_#C?LkG5y{$Y)@6C#9Kms2@DaiJ8t|8cPeb6J+K0Q&UKZApm1Io?~%bDbMByxDq%( z(LYWjoy*u4rqL-1&E6;NLk~-Aes3ulw0JM(qe3@hKIunlnvjJIQubGx5OT((v!}sk z>c0NGKt`Ao_rwX?x@w6!662@SRL! zn4pTkv2W6##ytev?!{;g?tHsXjbYPX*v)3v;I_IyM;q#^ix7b;(=RC8hF&;I`%9E! zFn*CoPdZUwoy|~oJheRyZhd7E!l0DYBjCsCw?Nv;cEHfSKH^s^22mDNrMm_16L8& z%q-WMfUAq$-Ojz6i{o&;_)f)Au}HFeH!H&?9aq=!Q{~|#Yt>%++sWx_`E6%AmEwUu zurCX%>+fIUDaM3);?Vhm46w1>r{nR?HkiT)7}AiVB;HCtH<2UVn?ckds}6A>TUdS1 z%k^#L+<0K}I%1gF<+Gna6z4Zu^M;=0F$BE{qpY=V_!kI(HHYtb$caJj1? z{DHtxt;d;Dbml-K-YKD~Df9~+Q*zJg({+ZXM583>^daNkj7V5*kK@blnPZkoGWSmS zT+rE~+Nd74e)Ijj0NkYb{vs)MsYB%r$qtC9i<4e&tVC3DoZD?DVg>vecHLY+^wOAP zY_+zp+Ceqv>qw>=WPG77qsW~9!}Po3%${J}Yy+|3*bYL3N|>phnX#ltb;TMo7Hp%{ zGMB+K@{Ohko!VDa-xE@n2&CbImBt42H_+ZgQpc77Tyh1Ot^jvN|H8+w(C7cB3RRBc=m{_kGFs7>!r00nD zbo@yGc?pe$^#@6BFZ0Nvx#kU-r5|B`G76pL$gcs>R0h9`%N-xz-e?9X=W`?(0 z0W^!wh{NbbFjO~Y-;xim%Jx>^JWOFvdj|v+FP!1m>N2?oQ(2qwqNubjr3D6z*UifoZroI98}(EBPu zGzgq&az#O3Di)~5XgH{)K$CnM62n3ynq)a0OR%zWL_jXKg1J^>cs1Xplr#vOyBs;2 z7AbHGAd-v8mVw?Z$XdEUP1KC>guoy#kOlX3jK^RlYp(Q$U{O6fm?cVamCY!5 z{S)RU(SXHE|0?G_%mR@*sUtET>Qgd^1 zcbs9Zbv0>=UL&`(mr8a1tsp-3*a4JdETwOYze-Sqs!>IxC;0uIj6=wxuGPyiqFtYZ zkU40T4q9(r^>}0r_rTy22M=em{$jM#aJT!RxXg0~ZpgAGK1i9PsP>7zyp;q?*`T$O zl|}B*?Sdy5??N>hVPwI!>i7A(JsQ8@_Sm-2r75?ot{_lz@J0Y#1C7CV3Cwj^-6RoRC|ONc50NRzMGd)XJqSQrFGh+aB!pQ{R@rOB>ld_%~uU zw_OLGwgo_Ch639x2A$0kMQDlt^Y=`;*TFH#iK>^oB1jn8xhJCm7#)Y_`);Q>-1{nw zD8js2ZV*ChqjT~HsawI7#=)JAyiQ>n4(0dDzC@<=(>DPpiSQ zoQtkq+vR=T3aioKxuao@DC!Qu7=s!_k5wWl-8xE+J6h!r=GG0zjr`*>p zRISD65e{Y(f+NnBqUWd%ZHBMMb*gBEU!8%KRXCnk)?h( z<`dRL$O9zv4>f*4HAuj{KjlN5S{2<~9PEVS`HVT$C`Z_Hof_;fi_=P2Au*SS9RShGp9Zp&XYo zPDu;F>QygZ!;*vfJcKJm?@=XeC>; zb!|aIX4O~3>vrVI#-N?dH|WCSZIkg|l6QC#fsD5$KSkd)KHo)osV}Y9Lq>zGwikc7 zQhL5AhSi?gnd6;oN9uiO-EvR_op){gv>LF!cuhT+>-jj>BCI1uVQ`2 z-$HD0<>Wj4DJ6VA`_rmk&$8~d!)EZ>peaLbUypR8eB#-ImcpNa@h*JH<^ERtUa zh2+Hy^|>;?ZB|Vf6xNc-ARgi=Ce#yJoZ%@2{%cvUz(=jG5$^G*@Vmr+dcO5DSFnYa zyQ__()h`10#j9Y#dlPb}!Jg>#Qw=vj=E$;d7)O9Ynpp|H^4#+EZ3vbNBNyGSfAlbn zwpPazCd5>iFwCc}0IoosAmv&cv&V|+(^{F8v0jSA76IjwTWTz)O-nJ3k{G&uH9Znl zqZzfXE{}gI2eTVAj_>%6s$o`1X3#V;f`+K`I;gjsa9m>P;kW&sA!lbfYR^1$b`s>` znYBJ3$S`Nb>9SIv_;TgeOow`MQ7T@14(s0tF5|zFWHa+2#_}aIg8vK4`#d&z; zVfLkHIh7@DBFA_uV(1TZGV3>czB8PilC^@HbHo^&q6yE&xH__sR8^HCKPKptVAY0d zFkO@P1oVEbjl(EU5L^AK80pOeJ&&gPQdME=aA1Pgz^&T2%kjfsFGTo}rojy?&Yw=y zvAA3It&hQm>2fJ#4-Q9mKHTRwC2V7=0j!$I zPRrWrO*1u?7%o0gCf+rIJb&rSDFKtm=d!4bGSl%d_m0XtomY>!DfvHty9!Bk9J`3P z){gQKf91y2P2O_(T+-o<$_BCQhHu<<#U+fqzIFEVcf#lkSIfg$=!SBs%Dw1hMF{9h zhwV5H9eZ&?u+tC9G-V4bn-jYFfT8=QOYdpnkQ*06IPavoDTn-n+9B?bIf%<(L-r$_ z3-3+Bc0^sGT^9Dqfog-wLW;A*DXp}zO`pWA+dXxDQ~3_EUdS{alAuSs2DdI%nQ!}> zF*P&(?drM%*3ev@y6&b^>+8V!73?=w8IyZEWcBkfme=#@?w@ikOJldpx2Cp_rb5Ll zt@L6hQ0)}NC$w5VnNV7UWnndze;KeP zFe&c$t`Kx+2*%v*D)jab(B+lRXxg)AMA$-UW=%+8{H$%g)ea;VX+lvnCZxU}Vdubn z{LBtCt$rd2dU43#-b*ODGOjg;V9rN4f&l#*!Jpr>fspgeS|a!W*Ge%gu#L9LJ@Dyz zYNQrsC+OL0={dISdP(Ye5f(Y~*(Sv&M-QZKusb>F@-H>a3tOwn3tE4~@L-MGJz3;` zgM||uOM5h#=Ed+_;Nkc=*xHZt#o~_ITxs0<9eD~`0BxwV1^G%@8>#1MB}&C1e`mzl zn`&JH)q|YR)eG+cojab-!jZfq_SMWJEm0KfG4|2Rl>H7QH=X{~{obhuS{B0HPXJy} z#Q&@NJ#%*cpZCG1?9U@7xyy0(XQL760yk_YHphcCOhQ+qgi6@72eEfMH*rHIgM)(5 zj>$K#9s1P1A@-i?td-J5lfD5FMPMn>dSDouSlj~k*gbUHjSWKMLRQ-21vc7Q{s?F zU}#odjcgk->^-S^>a&1(E$u}a7?ij8`Cuz& z*Gp6D;Ar7E5WYZvBn}9OoN%f1@89_RXBq!<{+BO))Rq4Z@b|9|{$u#_TngvPU%o}S zYk0RK``h$0JdL^2pS^4R_kPfCQve_u^Oy1er#o~P=WdhWH&O@o|GmUNS_XGf?v_7) zquj^;g>ttTdKchs0{0uBoa`6C?{x02>0Jf<+q9VS57WDP_%6a-arqk|it6qh{HxTw z3wT$n{RWJo{snkPsr{>Ry9@ev0q`3V9`R@afPct=yXJr2!2fEF1^kQopWC^*G78)W S0RU|H?;hNZzyB27fd2<37dG4g literal 0 HcmV?d00001