From 0afd874fc30bdb65ad2efd64a29e27a7d0903aaa Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:01:04 -0800 Subject: [PATCH] Try Pre-processing Formulas See if we can identify where comma needs to be replaced by union operator. --- .../Calculation/Calculation.php | 18 +++ .../Calculation/Discussion1950Test.php | 36 ++++++ .../Chart/Issue316Test.php | 115 ++++++++++++++++++ tests/data/Reader/XLSX/discussion.1950.xlsx | Bin 0 -> 9285 bytes tests/data/Reader/XLSX/issue.4656.d.xlsx | Bin 0 -> 9593 bytes 5 files changed, 169 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Discussion1950Test.php create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue316Test.php create mode 100644 tests/data/Reader/XLSX/discussion.1950.xlsx create mode 100644 tests/data/Reader/XLSX/issue.4656.d.xlsx diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2043d09c6..9ed1590d2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1052,6 +1052,22 @@ class Calculation extends CalculationLocale '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison ]; + /** @param string[] $matches */ + private static function unionForComma(array $matches): string + { + return $matches[2] . str_replace(',', '∪', $matches[3]); + } + + private const UNIONABLE_COMMAS = '/(([,(]|^)\s*)' // comma or open paren or start of string, followed by optional whitespace + . '([(]' // open paren + . self::CALCULATION_REGEXP_CELLREF // cell address + . '(:' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address + . '(\s*,\s*' // optioonal whitespace, comma, optional whitespace + . self::CALCULATION_REGEXP_CELLREF // cell address + . '(:' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address + . ')+' // one or more occurrences + . '\s*[)])/i'; // optional whitespace, end paren + /** * @return array|false */ @@ -1060,6 +1076,8 @@ class Calculation extends CalculationLocale if (($formula = $this->convertMatrixReferences(trim($formula))) === false) { return false; } + + $formula = preg_replace_callback(self::UNIONABLE_COMMAS, self::unionForComma(...), $formula) ?? $formula; $phpSpreadsheetFunctions = &self::getFunctionsAddress(); // If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet), diff --git a/tests/PhpSpreadsheetTests/Calculation/Discussion1950Test.php b/tests/PhpSpreadsheetTests/Calculation/Discussion1950Test.php new file mode 100644 index 000000000..1e1642141 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Discussion1950Test.php @@ -0,0 +1,36 @@ +load($infile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=SUM((A1,A2),(B1,B2))', $sheet->getCell('A5')->getValue()); + //$sheet->getCell('A5')->setValue('=SUM((A1∪A2)∪(B1∪B2)))'); + self::assertSame(10, $sheet->getCell('A5')->getCalculatedValue(), 'error out so use old calculated value'); + $spreadsheet->disconnectWorksheets(); + } + + public function testUnexpectedUnion(): void + { + // was failing in a different manner than prior test + $infile = 'tests/data/Reader/Xlsx/issue.4656.d.xlsx'; + $reader = new XlsxReader(); + $spreadsheet = $reader->load($infile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=RANK(A1,(A2,A3,A4))', $sheet->getCell('A5')->getValue()); + //$sheet->getCell('A5')->setValue('=RANK(A1,(A2∪A3∪A4))'); + self::assertSame(2, $sheet->getCell('A5')->getCalculatedValue(), 'error out so use old calculated value'); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue316Test.php b/tests/PhpSpreadsheetTests/Chart/Issue316Test.php new file mode 100644 index 000000000..d35757d7c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue316Test.php @@ -0,0 +1,115 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setPreCalculateFormulas(true) + ->setIncludeCharts(true); + } + + public function testUnionFormula(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray([ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ]); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, '(Worksheet!$B$2,Worksheet!$B$5)', null, 4), // cell union created problem on write + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + DataSeries::GROUPING_CLUSTERED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title = new Title('Test Bar Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + $worksheet->addChart($chart); + + $reloadedSpreadsheet = $this->writeAndReload( + $spreadsheet, + 'Xlsx', + $this->readCharts(...), + $this->writeCharts(...) + ); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + self::assertSame( + '(Worksheet!$B$2,Worksheet!$B$5)', + $chart2->getPlotArea() + ?->getPlotGroupByIndex(0) + ->getPlotValues()[0] + ->getDataSource() + ); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/discussion.1950.xlsx b/tests/data/Reader/XLSX/discussion.1950.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f3804475ef88d71c426118df28c29b5560ccd71 GIT binary patch literal 9285 zcmeHNg;!K-*B|MWp`;}T>6R8y5$P^L7;2;$O1crGkxr2ux=W77t4s31CaR)wox%W$u_~h8HBp-YR(JoT%FO@Vhqesnp<(Cve*{!Iw!8=NQ?&2Ygydl0Dbj#%aZrgPK8 z+3dsR1>vMyQz+{!qnLrXg-r^Z>M0)hym#k~P@#gP^$Hn=VxbmtByq<*8Ctkl?Ud-D zZwFlSeN_kWvzGgsUh5HWM%ZFCB#f1RzdhUaz{x6mTvw4Ec5ibw__^Ns(u&FYRrVZb zRd*hgGDp?(c*X&P4TtcdMtHKQ{#xaiw)fl%VrTCB{o;7hHo21EJD*n=54>}R7H;(W z_&ojMweM`rT8?IW_TiMI)_C228tl8hx9rp`Zi0f3y^1$h1cam!1_yVQ;< z*2DQ0R(s&h{*KHw$C_1LrhL3AU)zqg|Krhjn{U#0oGj(piDOx~Bx)TKoZjD(wR&<} zqubxB(3zTM-7(?;nx^#*cIt%9t4@W%?8w7mvca1>=)$mhq$btcI|{7$YBX znDM6Zh3gvau-2QHZP-5}89zHE^fsEW@^Dx^jN$YOpd|sH+i)~FJm*e<=j(Fa#03ea zG%-s)X~}m3bpGA?Rf~y%WY+z2R(pKH=*Q-(Ohj_sa?1N?r1uDQ|6y?tG01~_%SBEtCnyy)I0 zMZuWbCMOTIt5IimNbz#OGM#j#YV7n(`+*jF{%LvO^rRA)tJs!R?zTjnj8@T1ZR!yp zXL4@$^HZaKp`-a=@(&h9_jOC#>b&Ngg!|5AEYLaoRR>A2+ygM5ntjA#RRQ*7Cu-J6 zPbAk9IlqtI*10ca(o2dNWUX*<+J+Vsj(!oKYs};>d-sBhf!!8({5-m|+?u(yWt?8* z&Xun!L%)_Ab65RJh37S|b|yoIoUM?$#aFMC=MQ_J{lu%ZMbCH2H$Y6YHzy|fdmPGF zZGB#1{L}=Drc0yLossJI+EWtV3!mRs3wf68#Mfs9)(8@T)37YYY^B=V!dF#X!?Y->Xg!z`X=vg5=XYP3M!|_W5F2DOUP(+@VydgarY^!X8)SqHP zwsFW!bK|r1UYQ8O!6kHcJ3~r%l0g+TLQ*m$VzX_AM0DK8;=KbhGV4_Grti?KR!VAf z>Y!TtU1M*iOR8{388pp922rA?(+mgY-UcR43y5NnKlA^Vsqa2L=zu}NoB5ii=Bw%= zIZLfU=K<>AR~iJL$-PQ-xI&zOqn5YkJA#{NJE0@4iZ6N9DVCkSSWgK5*S`vW$TtZSNuB3ET0lN20bJFuDE+-;0yr zFnzMb#H}-rf`wK(eWk%@-xL(Oc`E2eiqJOVo1MlGVlk8)3!e2&p3Q$FZV7l z-TGx{mR{TvKMeJmw4rbmXYAoB!_a&xkBsM`SuHN^Q01{Fq5HSVFHqz{|C2F5n~zg* zNVedi0|4a6H~#S9P8JY|vlI8vf%k_qPmh(0UgiM??K2MhhIpx@M*;}5rloSW5EgZf z4s2Z=3Uh*KGKUNKL}>FwWK-%43G64QU%H_u&gy)^elI&V!^ZI6;}&+-mrdy1&&@=~&hmJ*d7~8?^UA8* zzAU|o6^L93U}F|&0LoutbqYi-R`7Y1OI_l21(@C4a4p^ZlmI(p6z<-!TL}78aGQ{d zK1h;4m59Z##jdu}hH?CqwV#4O`OZ6%g&j8jqy6~!#TntrGpzn2zQg7#*0<_C&|J?O z0^`_pJ>igRlm)e|4Hf91a0ecjbZZkg;={zERp+PfxRMizB|HVShrkVq)4{ zY2bYUhkA8uM@S3VY=%a7I{1NPLl;qquU*DjJZ^btoaut6XC7y^A~J)6ck^zHqZXuW z^rNfX1s>&is}Ec>q4e@*v(P}U7H^P7d~@@2b*orMWR*A_l!)R=)!7Wx!{am#YZ)<|Vmr65oMQRyGkIH_RhNG$KNn0`dZls0`W`;IBjYY=tg--T7mJ$qB zW#!_74M*&y)En7JZsBZ9DfpvUjw|?o_FxIau<*lZSdHd2VpO^b5qFeHT?z|pzLttL zapmTp+JA7p-rU{ zC*JaQ++T^_TSg^~Nv3_QNJ6r0jS|=rr>CLm^8SCa(Fm4#F{v{n|sY-0}m|q zK{c`>6SCUADV0ENtELdQ?8z(Pw-C5;Nj6o;+?mK~oW!vbY{_k{>lE1(xHO)hfJ&uY zU+nQmiRBn~c2KDXLw6QEeP;KkIGS!j&em_YNPMa7hzSzP3>jbbBAd6VMGR9n2XP3C zId_N+!xkvlzoMXzS446>=nlAN1up46qaZT{SkQHbG0T=}RJHMkEE^WRmwwEq5e&a8 z@6Z9lyEFz$ZBAd=UqU&e2(Y;1tPR_w_wXzwWJl1HwBViqy=J1-HKBzjx9j8ZjT`ra zZZhtS8(f-kNG_jN;wO6LL`Vbe^>y#f)v@bl>&-QS0DRpanFRS>?0bDZUB7S@@1biI z#AD(-vo&4%*@bY)JcacbzYR{2EBfHBb>~Z7{a!(J7LUCN z_g5m^jIY0Y0hfedI>O{MdwkeN>7{u^*9Oof*H3W9PZu%McV3=q$aA>1N)U?sqv^mj z(L$~vAnKMTmG{ke;WqxE?1M+rHV}A5=E&w|d7`!jEn&x3PzG^8hSwLPv5i8=VZc!! zR@DH#1NscAO>a?)O2{idIr#lX)^8#5>}I2VvHMF-O)ZITGs2;jv-#&}O?E+hnr$Y| zs%~8`HXw`GLEi|LyDj6N5;?~Qc-E+FTfQo7R#;GQZ7K^|YZZECTEX>h-q|1kub~o; z=|re*7>(aLfWe44Ih#@E13Ka0RZz%hoyge%*7^!E+a(N^h!T%`u>S3^cf=fMt>4|P z-VtaYzb9=~Xs+fir#38hJX!JysvQQF$7-M;`t6oKaN61_Q0?Ee)O@T@gyN&b@`Tu9`E) z>yobcpi-)Q6s{@K47RKL_HY>bc)r3QReL+wFR>Xrbizz9G&NA#n*WTF=!4UVkFGN{ z$DH0(kCqq=V5MK)<|!0Ve_RGHFptP&#Jqp;{F2q_xTkSWWPf_{GNONg5K!7}pyDb3 zZ;sA?$Aw!f^tI+mhcF12J?+&&0_#^ZG-#*0bK>QmB&og7@ymSs!Y3+eCe@TUl%*%H zzt$=}40a(zxaY_MN*^Mk5LZv+cLr0V#U z5q#lkC2{BXU}0EY7N3rf4xyd?Am&iBRnZD(-@?^L&5J&kt8LqLN&O}dUli?}PK4As zyWp})DBKwVN1h=9Ynj0QjicG;pwy}aw+Q+!%2Fv$S7i`2Em}(P2FXZgT}nwvtF@R$ z##QO#G6q`HL>b`)^?{G#fob>=wj+HLVEpGjmt=~b?@ z)U$j7P201%SaLZhDmEdj)rB-%dW`Ja#L}P@Ii3&9j|`e4ye{x!dx6S?IvirVFR(wQOz=G* z?Wxz00BxkLtlE z)##0+BLlP6$3>h?3A5DIc0o$k<4^PrY?Y^8mfF3TxjmRMmuqaw`hKpKp{5@_ctS*M zXxNc^HHAGvXU;xIJHVxR;Udz|8z_iD+B#3J^`-U$*iy)&R=#|^06j-$k>GM^LrUd| z!&qO77kQ^8P=8}VGWp;wlx}(0AO4miy7-gVit7E(LJLx8OV?1kwiqdPYV_j@vfFHd z#$zQ@oyOG@cGDgS4`-=^AI#RIt$g0N`+;XMQ<6HWH?m@a8c?=(N||8TDp(+Z$ng=r zEpwbju;_K!5KvG4*2zh~EpO@N{?mUO=tU8{8fnOk^#KL|fd8*R@8s-m19AG1rsite zS|fOX*Alb7z>B>BmlY1|y1|Jl)yOCQ4K9F_I^E}F|iZ|oK0X>J%QKp(vOK8aw%vNWN(V5-!Sjv06i(iLaFG9bO~ z6ML=iZsnUvL%JBs(?6+>Yx@RuE7P(0sak(9RlVld+NEnfB@o$VJB=si0X;!@kP}x| zsMLS?Woda-;B%T|T^m!;W#XhI zE>crphiA%_g}xeCZ%A#5mkQ5YJPA^P07NL>DEGf|JBqw6ZE4%H%GG-&>k5XD%k+>f zSv+FEbZVm#yz3hwXw*t^=jAa-f-cGe{6-?^so@-+?@1hMrrd3y#k}Lv`wLX#wvrD{ z*pJHn4mD=THb~If)GAZ!Iv%{e&F@twSs7N(CozA~;W~E=q>w-m7#3Q5F|8PRpliXc zt7h#M?E9Hha!AMZ3H4CN8b|W;YNq6SgIaGC?UIVMcnMa%p1>_;7I?Oj1r*_W~|DlOLNGn+5{brcc>X6R^47(%a3V6yGN_x$%4BQ&^&XLwJeY z9R4%oZVSwfI7CkUF)|^iK#pcmQzeMAv6->6F}I?z3B*Ru?gjM6w`h(HT6OW@p^t&F zKZ}hxeWUA_at+!l0@EQJO~0CkOlRdCEo?ha?{T5fl+`?!Y+}Fb#zfUQ@TJNG9fPzM zAIk1|uk)!kQ$S{Ww;%guS63M#JKeb&HFWOe zb3EK4LW?D}sPI+&DT1|$&L0EiB#E=c@S{$CX6){mW4hUJqYwFci1*;7HW*-(9GrD6 zK^5qnOlGC+miu7+>_Y!ytf2+uC2^nsWs)Jpg1k4>)dPj=vSmqT8>dE)bU;Ic7l`3e z6|bFmSDqa@JCuoc$)9opo_C!PCah-~-n>)bzY9eC@>)ql~3H zIM^1+pet_mVl?xNq+T#syYd zrsk$N^FHzk5Gr2N(7B)ZVV)FIb3H)A&i{NakpO?w0{fd1@7tA_F|JCJk@Aj~dZMn~ zgQQb+f=^{K1ekX0OvN}f<#q~+$RMZrgJ z#=BKR*Rn~_(WtD+P7>T^=M8*F(POb|4|t_OQGUxZU@!GFYGN}-KlG<)CDID<+UnW1 zS^V#fL@Fk_K2akPHOT5Uh7BR6YIP1;IoMR> zbj14f3HMV}vW*^kBNXE$YKM(J)N3n2=1H-xL6L(#o&#lJgg;c&X++2{Mdjk2CrzUn za-Xkp+!OB7D zq2|MNZeY$2TDY#d#{HANw@^3%|LL^FpUd^<_;1c$s4Dzjz~B46e*%Au^+=oeOBeWe z;P2h7Ur-!mnd!G)*YDuJ7vg_G0f2OzpWy#cmj7MO@8#KFl17l#wf|JC{awoMwXt7P zl8}7_q?F$)WxosfJ?H)X;J-tEPjr4kWhj3_|CaXrj{ZHz_=U!%`hPNx z-^Khr0RIvK0Ps`)6!Z6R{5$;bUi7c<(z}0w|KU?r70{7elOLxHZ~>jjh;Kvx5EH+ zMg$hkmQeozE?#YNC%-(@mggJ+s zH5PT2(FLP_IGC+-r+%$L(gw>3#!&(4%zv!-Ma;$e1{+D^gK)`*A56Qpc2dI z!*OgJJDZkVT@pz|n?T=SAI9+`C}>pN(nxkA0^M0K!hi`;)+(kMh=o|nQ^X#3rD+l1 zw$WjQeCTt|_fqR4%3SSf4ALdv3V(%LA3suhMLgZ{$iXUWR7Z(9@7~sQ;7i@}l{J%% z>#SMsiq2dZUACJ0@#Jgl`}ky%nqf%~^}dvUZLQ;75<7Dh=oKf7vdMX3NVT|j_s84p zfhD9~55K#29GGf*8akZi-h&T+U*#eAEYNFbZ`Gkm+ytEnZyjS`WU--((I_X|Uf(Xh zLc*->#2z+nu+|`2LrF`XKZ>(z5OH>T*GtfpQ6HPCJ&Ev+?D}J*b7Z-f{DZIDHpzx1 zKklIzheTok)czt{5JSiy3re<&C{+@mWNT;-v2oz#`FZ}YT>pbL`lm;S$BZgqfJkMQ zu#n8_O=5Fe(m8X@!D?!x1&n;k=CzsfY^>^YEyBfA`X6ORy3O|HQ|n`UZPwr}xk-eV z;K(9f{DaWyA8$F+shzW}IW%NTM=SEdwj8}PM^`on(o_ym1y1r9cAm%8_VEsNXmVCE z#G0Kx9tDo{3>)_0k1(HGf5UtKsoD2zK}rQD&7?$+cJtzCXs1wIB65`|lHY0oH}r1A zQ!X=53V)c6(Kn7-jY>DJDQM|O{A&L-i9r&Hp^rTw4KF#ITJzKmgr5vRA?cdoAAKcg5uJ0*K-v{2#pdi^k(+ryWU!tmUNtMT=yeA;(wA9JeqmO1_J=7LM28LlpkGec%5PP)+R8R_0KE_*LnrZ z79qXy9J|Kpa1I?*#QoSCs;!^vqy=%!Q;*AGJ+YE330=$N~<&alW?Q5uGm&b`{xMzHZg-f0&k(y9M#qCo{lqpe5ZU-5t3dI z)?1Upz-M5S14(e~-N|)I!fZoX1XHmjCmQR%pXFY3O71YPB6z%^$x*Ok#)B=Q@OiPd z;I^4ApL|&IURw-oyMbsFmX-F|sM=#n_LJz7zH_i87Pq`%KM>c|ALp6bjOBMsWB=-$ zEMQDmV|)fe>Saq|{W;L6n-V9$n&85D7&9OY>%v#ZnAKGRyuckDKN;qh*WFet7`wE% z^)5B^<_UZB`hBvEXv7UNsvMn?EZnLg{;3~QyI0Wcion(PN-91Kaop&OX?mGCDC5m& zU;+zexrax@*-^g2Gz@Boex-fkzxJS<%{CamPa2+tDSEF7PMoVXBCz1|FfGZp$F|Cd z;s_;@AHKwlQiD$jvv(Wtbp*@BWNR0Sa3?h87Ak9&=aOxWfD4WCx$dGzT;LVStIyxZ$nvvVGBz=*$73RA@NS(h1Am3dDnCxAJ)72YmcXYIL(e zwcF;!46MzpoVrsKQWoGrN4d(95Qz^gCHUfTuRZwgy&=v5k1ubTnw|Ig?XADL7ess1 z;mzcvw#B>zQnPB17^w{}Fx8X&#tQ!G291#|DST&j)`lEC!G3|WmLgpa4}Pa8dGO2y z$z@3!P0U(Nf_v5syj?$zJ0mgLYiBMp{Y;iZnI56cKX+_*E8q4+0UTiHS+=b-xB^u$ zBcaw<=rm^d2on2tN4WpGD^p|N*04!>)8#g0RJfL4JSG z<+qoc-`Nz}53doOELYaao(xfZUhmZ$*z4$UlHX@5Z6N% zCCq=NyFbhG_tb}ms#H;V@jv^3#|$fU@S!R--+j^z+wl?>mXtk7vXlGAXip!$YR{AJ z4fnb@2@KVvPBKAxKJUqQE9lu6XUTv`m_=+>Oybwq` zd|@Y_o||g@CL~igc10i$&TB@or56hTph5lP-=W;W5(05_;Qeg@{fz0UF>{e~Or(MPoW~4Yd#`M|huSJC z73gIa8VZAU1>4qWu)+z}a|-tt%g0$czp&}zu_pQKr~5qd66M%tXw}J_^OcsA6Su4{ zk#6Ab8D_NhQV`$K(E}CevG>q&xh8wv#4%RS%wV^=%2~dWq}%)yuF-#6Z2KTuAG+sq+z_gO zD(9RZPZdga$jYq@VZ^zFT9nR#Z`VXmpK>VBoT^|y3wLmgr(GpcP_WA`F46Zkb1OJ6 zzoz}5-UZ9)zDaMF#uKz7y~bHo|Iw`wI(D1086nH)9|0@7saL9@){?xDhSICD?Tkg` z^^d6b_4XO6!~~?5+=D*gp6a!1yi(k71%3~@ED^_eBT1!RBZv24)6Z)WlYY0MdUJY1 zOm3N2?JkP}Y&c@F;bxgL@G0x;iiE1!doA&fqg#V4Dzq7`p|8E;Xk;)n(saW=dT-MG$mY<3jCv zX_H{qg4XTwl>4@!u3rzYmix|c4(OjA87sW6!kHn%xT6;avIUjV5Zi!SJq(gBT(UY4 zI^;i2X5XqNxX{4Trca-T1pU~!u7_!_8pA^dP8kMg(ju$BRMq(E$`@PF8QpFEw%;g%M}j@TC=D&*sufhHrNtK zjq-Bd#TjbEq=!lI-xUM_t8rPO@SJxE>4xZQTXQp8iJR0AYKxq_gz2B*?eCmFF{+ZR z!vO%uSblEgf8)HPCFB)^_qX$JbpNJ17u7s!(Hh&5)%PvH z+wvj;q%0n{Of&IVmuo9LBLuv?Fvrdas#A`ezz|>?sl?Ax-t=k)#rS0ftaY=gM@@z?71=m6o2;L&vt>IIwXxk37-04@gWf44&I(_e18NxfyXsJyBmQ09HHo0QWidKQ(RfEDhX{q~~fm3%B zUbh1YFO5khwmx6mUBb8`NpN}OtqoeG_Xwen<%TeowWizwx=pv%zX&fiy4)OxZ6aNN zbW-!CAqf~pAvye73G>V<36Ofmo11Rr^|A9-3-V@;#PFs!A`x;`-d1z>+FWn@L|;{W?I4uIURY3N)OO&z|=*;6hGxja%hUY5{nWy9=(rk z(AVftuH{}j0qPP}8}i4L?;tN*FN-XwrYhy&6mJHu_(HjtL7VX5fN=oH@k=0qD!6y- zQN2$5>nKpO-Qc6vkT1)l_XCa;^zY-4m0PCqr?NM);;G9ayN2;zi!WHj={h~i6KDe( zvLDgPyjA+ZmON5GDHkxp9MQ&WO)Y6x4M|zc|2UkyQxLS?F56zwIw0x!Gzrpm>5^;r zO+&N|#CiJ)x2Q+vz=qo#GB0=k`%w&Eq^tu=uHr_t_a?XKHFIFchT|njuUkli-ED8o z)nAnNZqStn>59l3`#kyQU7q)cnWaGwzw}`}**L);Jzd60{r2WmQ-RC5H zhg;Fd{2FT#!=}5iSvA<7Uw%rkf#V=pfzxcbCuV=ep|Lr^Wl|)hd^-Oev(YwS?|G|< zqnb;H`6gr;FW`V=wG$fm?3QDkuX~m14%EN6NpVThxv?bRON;Oe(=wj)1xI~f!uoPT z))V2HK}-Q_Ult>_q^!HzpRmaKuLFV?wIincIBLtNU#(!Xhr`|O<@N4_rjv7Fwp_Vd zrIUb<>nK|kn<{xr=?#kPPgXnwss~9+V>Hp{dTm!falf)vyuFWXu9~S$fDyc(^WCV) zc-`dt_MLCuN8^k}4&6t2KgjoYFS*!FEvyE6q8}79;*|;>f(YNI#X*&9FTJ6di5IR$ z=mfPC(>+XWpJ5+Ehgeddda?8A59AJL@d6d<{cECWg|465E7w9F7q6N=9-jS>*RpQG z7Nn<&^Jb;y6X{)=iZ|!l()JKc(jJr637cVb49FDshW<~cuk6(uO_K^XKJM~qa zA51kx<)`xyR11Hvl4%zK5^$#Y|A^=KZiWd%xH=|W?meNj6Fz>EZ&x6rnqpE(hfi00 z67;=Vg=NYfL$NQ}7r1q6(gincNJ=2psR`>SVufFY*(JyGwA}_Th*tnuL5UP908(lY zm51|(rNCp)@8yN!c393IA05IFK0vk*v-O8%j$Q@pPn(uKq3f+XHHp0@lIDfqPRD|4 z9GwU_9xGA}8AhDV`Bk$T`ZNq@ode%j#JhwuchD6}xI3!==@~JTi#92S5H-p0;1+8! z&9v)csS*}O(*#+OC5^rralaHIh+_Brt5<5D`qOC%KUueLYwVZpH|{ojmQE&co^-2} zLv^7tu#`QUixsEyTSX?+)jE*+OShq2n-~VnLWdNW)20^b;-rDBJlg#Y;}2Db>M67C za9o83RfnjBniT)>Td;;>zxm^jpT#=z${zT@h==+u<~%M4W4cLI$h5h{cFpnTlgIcU zQFhg8J~rG;R5A}?JFMqXsC<5t#6|kS@J-W*mgq)Mx2s_5Es2!()Zl6G$cx<>U$@GB znM$k%%Avk#>*GT1#`tOaO4|Ts>roj!{Z}dzZ;EZrCyD#hW^;^9IqGJsS*m)m0>?zf z1_teU*ONKpwP)=Dz`jmROBWFaZ%Ku)DO(n3w7yn_LNolIm?PJxTiU0migC=ri?!HmYdeV@*wPQFuuLbL9rc)EX_pNso10MooO* z&v*nrfiSKdv;FKAFF8#g_-MK+Wo>cu&L_U*j3@Mw-4SJD^njARQ@VJA7NL(1=CVCS zh-Hs6NtQh>>wRl!J~%k&wdSt8*?;zriJqSEm6IE)Pf>>t01*8v(K|T0+CUtBRf$kBMVMTQ|!ls}YytLm&6alCzvb!WcXyahKUO zxK}D|-(&N>`M&$m2)Q~#Jj-cT(TJ4v^NKu^bocZ-ieB59DyHhDY*@WboehQ+ zD(N(?BBWVl62UyD#JaUROk0H*D|HPq++=oRa-HRY#j`m2MYttm*&Q!PgH~9UlkkU` z;jVA+aJ#?ummhQX>cO`o`3qI4`lT9my`~8YzDJ^ceym^@aABEgGEp=d9N%1qfaaRr zyZsic&c}vMH#*T#yX}7KXMfC|QmN1}rttLh#UgDMpbv}cM9wfL|Lb+&6hq;v*uryOZWObi(<8K5=l~*Y1hcEhXzeLcfA?6u8d`8vBkzQrN;4kgFf)?zBZ=!*rrZAI zII-!&gO!utQeNn_FAH`;A5L&(4h7~aR?a0I4Uf{F5DNLMBi(f-ge|TtW)X0^s;u-8 z^z)BVc5@Zu*U|gI_ji|tOV1dcW>o2BZG)Z2y;YmCA&tOAv=HZj>m zJxU)Go8KZ&Y(8&04(mzzAtZvV=~4aiy|S)_`D+b(CZR?34SZ}^Fl@kasL5UPwhh(A zLNvc}r{Qx^qsi+DVW$J@L!VlPH#O*wG-I(DlRd>5Hx{dIA|{S{n9UHyOWUy%R4k`? z$Ik@bW(OqC=S(q*(Ik8iiwxaZJZ%^(E!x$mz3jKEJc_lBxhQh1JRVj}m8Du>xS{1$O z>r}4!BUX1=X6ADt57*L~4?wrD)z{Mm13f-X#*xc*CKZ9C95`6t69v=^+=1g6h zIqc`NJRj+|>fj^98ZpFM6dQ6lVCt1{4%jX=2?DxJ{o2juEqYou)x3ebdIBu$p;(#B#|3#Fr%HH5wkmDpK3PP z;7M~H{I>rRYzQz)3e3EDeA~}4iP{S6lJjWe>_YEiq`n#G@mNscO`-wBlBPSv*$tiN zvUx>z2fs>)vQJYJM2hWJ5eH5{zRrk>(j0za6L#ixd4h_&fA>`6+sH6ol$WefEeA4G zg9_y-HG7zy1FtE}9`e&esFBQn`%)+hd7wrT2tJyCeXOO-^=9SDua23!5)ha2Oe2{e zJZ#7slh{Dk+1*YLG{b?a1@oX)i2V@qOKN(~cm&*+gs%p!6(~oM4WKBMDFD;Dahb0G z@!_TlWrg5(sJ{4vK~+_a5{^=$`4nBh*TCu4*tp!67M8oIc{F;>*u6IE41Hn!vvM0( zqwq5GtB7Ji)T&E3P=qbXv<@rqLPAaeebGtqwKJ65bwkux> z0^63yoYAX3W47fcliXMBiNN5ZSDsVe_#0}?3V9k=dZ)`J&s1^9&djRz%eB*Dc2Z0N z57fmUlHlR><}g{rkt)O_o}6c&3qie@otba$MIxrjV||s_L!kB`rad&*#LF}1^(B;7 z|4v9sFS53H6d^fLbS3>WA&u?q{>w*{%l>_&#wf$)_y_~{F-{l>7jBE{s}uUbv*h%S z%1qE!oh-n4rkNFHa`5P*si>OKjv1%f2u5Cm$1J(FC&m44;l0>~!CzRYY2C>0?>Gmz z@G$Z;hjEde;lKQ@6O6(3I=jVFJ%#3BxETmyv!cmNmt_lt@DtU%qnbaNFOahF5s46^ z{W?q6NU+f)qk6zUTngO7dmHds#`yt7>{?+yQn1*U_H0bib&E!)h@Z4>tmdNwP&MS! z?n4VA(iVd!FUY0v1tF*Iek#4LnxJE_Pg&#pMu0~OT28Texl@+