From e5854ab3d7a4b4e735c5b0223e855817e47364a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Mon, 3 Nov 2025 23:44:22 +0100 Subject: [PATCH] UPDATE: Adds ARABIC and ROMAN (#509) --- .../src/expressions/parser/static_analysis.rs | 4 + base/src/functions/math_util.rs | 200 ++++++++++++++++++ base/src/functions/mathematical.rs | 89 ++++++++ base/src/functions/mod.rs | 14 +- xlsx/tests/calc_tests/ARABIC_ROMAN.xlsx | Bin 0 -> 12737 bytes 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 base/src/functions/math_util.rs create mode 100644 xlsx/tests/calc_tests/ARABIC_ROMAN.xlsx diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 835a4ba..e7402f4 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -866,6 +866,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Vector; arg_count], Function::Base => args_signature_scalars(arg_count, 2, 1), Function::Decimal => args_signature_scalars(arg_count, 2, 0), + Function::Roman => args_signature_scalars(arg_count, 1, 1), + Function::Arabic => args_signature_scalars(arg_count, 1, 0), } } @@ -1120,5 +1122,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Lcm => not_implemented(args), Function::Base => scalar_arguments(args), Function::Decimal => scalar_arguments(args), + Function::Roman => scalar_arguments(args), + Function::Arabic => scalar_arguments(args), } } diff --git a/base/src/functions/math_util.rs b/base/src/functions/math_util.rs new file mode 100644 index 0000000..f3b1e0b --- /dev/null +++ b/base/src/functions/math_util.rs @@ -0,0 +1,200 @@ +/// Parse Roman (classic or Excel variants) → number +pub fn from_roman(s: &str) -> Result { + if s.is_empty() { + return Err("empty numeral".into()); + } + fn val(c: char) -> Option { + Some(match c { + 'I' => 1, + 'V' => 5, + 'X' => 10, + 'L' => 50, + 'C' => 100, + 'D' => 500, + 'M' => 1000, + _ => return None, + }) + } + + // Accept the union of subtractive pairs used by the tables above (Excel-compatible). + fn allowed_subtractive(a: char, b: char) -> bool { + matches!( + (a, b), + // classic: + ('I','V')|('I','X')|('X','L')|('X','C')|('C','D')|('C','M') + // Excel forms: + |('V','L')|('L','D')|('L','M') // VL, LD, LM + |('X','D')|('X','M') // XD, XM + |('V','M') // VM + |('I','L')|('I','C')|('I','D')|('I','M') // IL, IC, ID, IM + |('V','D')|('V','C') // VD, VC + ) + } + + let chars: Vec = s.chars().map(|c| c.to_ascii_uppercase()).collect(); + let mut total = 0u32; + let mut i = 0usize; + + // Repetition rules similar to classic Romans: + // V, L, D cannot repeat; I, X, C, M max 3 in a row. + let mut last_char: Option = None; + let mut run_len = 0usize; + + while i < chars.len() { + let c = chars[i]; + let v = val(c).ok_or_else(|| format!("invalid character '{c}'"))?; + + if Some(c) == last_char { + run_len += 1; + match c { + 'V' | 'L' | 'D' => return Err(format!("invalid repetition of '{c}'")), + _ if run_len >= 3 => return Err(format!("invalid repetition of '{c}'")), + _ => {} + } + } else { + last_char = Some(c); + run_len = 0; + } + + if i + 1 < chars.len() { + let c2 = chars[i + 1]; + let v2 = val(c2).ok_or_else(|| format!("invalid character '{c2}'"))?; + if v < v2 { + if !allowed_subtractive(c, c2) { + return Err(format!("invalid subtractive pair '{c}{c2}'")); + } + // Disallow stacked subtractives like IIV, XXL: + if run_len > 0 { + return Err(format!("malformed numeral near position {i}")); + } + total += v2 - v; + i += 2; + last_char = None; + run_len = 0; + continue; + } + } + + total += v; + i += 1; + } + Ok(total) +} + +/// Classic Roman (strict) encoder used as a base for all forms. +fn to_roman(mut n: u32) -> Result { + if !(1..=3999).contains(&n) { + return Err("value out of range (must be 1..=3999)".into()); + } + + const MAP: &[(u32, &str)] = &[ + (1000, "M"), + (900, "CM"), + (500, "D"), + (400, "CD"), + (100, "C"), + (90, "XC"), + (50, "L"), + (40, "XL"), + (10, "X"), + (9, "IX"), + (5, "V"), + (4, "IV"), + (1, "I"), + ]; + + let mut out = String::with_capacity(15); + for &(val, sym) in MAP { + while n >= val { + out.push_str(sym); + n -= val; + } + if n == 0 { + break; + } + } + Ok(out) +} + +/// Excel/Google Sheets compatible ROMAN(number, [form]) encoder. +/// `form`: 0..=4 (0=Classic, 4=Simplified). +pub fn to_roman_with_form(n: u32, form: i32) -> Result { + let mut s = to_roman(n)?; + if form == 0 { + return Ok(s); + } + if !(0..=4).contains(&form) { + return Err("form must be between 0 and 4".into()); + } + + // Base rules (apply for all f >= 1) + let base_rules: &[(&str, &str)] = &[ + // C(D|M)XC -> L$1XL + ("CDXC", "LDXL"), + ("CMXC", "LMXL"), + // C(D|M)L -> L$1 + ("CDL", "LD"), + ("CML", "LM"), + // X(L|C)IX -> V$1IV + ("XLIX", "VLIV"), + ("XCIX", "VCIV"), + // X(L|C)V -> V$1 + ("XLV", "VL"), + ("XCV", "VC"), + ]; + + // Level 2 extra rules + let lvl2_rules: &[(&str, &str)] = &[ + // V(L|C)IV -> I$1 + ("VLIV", "IL"), + ("VCIV", "IC"), + // L(D|M)XL -> X$1 + ("LDXL", "XD"), + ("LMXL", "XM"), + // L(D|M)VL -> X$1V + ("LDVL", "XDV"), + ("LMVL", "XMV"), + // L(D|M)IL -> X$1IX + ("LDIL", "XDIX"), + ("LMIL", "XMIX"), + ]; + + // Level 3 extra rules + let lvl3_rules: &[(&str, &str)] = &[ + // X(D|M)V -> V$1 + ("XDV", "VD"), + ("XMV", "VM"), + // X(D|M)IX -> V$1IV + ("XDIX", "VDIV"), + ("XMIX", "VMIV"), + ]; + + // Level 4 extra rules + let lvl4_rules: &[(&str, &str)] = &[ + // V(D|M)IV -> I$1 + ("VDIV", "ID"), + ("VMIV", "IM"), + ]; + + // Helper to apply a batch of (from -> to) globally, in order. + fn apply_rules(mut t: String, rules: &[(&str, &str)]) -> String { + for (from, to) in rules { + if t.contains(from) { + t = t.replace(from, to); + } + } + t + } + + s = apply_rules(s, base_rules); + if form >= 2 { + s = apply_rules(s, lvl2_rules); + } + if form >= 3 { + s = apply_rules(s, lvl3_rules); + } + if form >= 4 { + s = apply_rules(s, lvl4_rules); + } + Ok(s) +} diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 5be0521..1333a11 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -2,6 +2,7 @@ use crate::cast::NumberOrArray; use crate::constants::{LAST_COLUMN, LAST_ROW}; use crate::expressions::parser::ArrayNode; use crate::expressions::types::CellReferenceIndex; +use crate::functions::math_util::{from_roman, to_roman_with_form}; use crate::number_format::to_precision; use crate::single_number_fn; use crate::{ @@ -1376,4 +1377,92 @@ impl Model { } CalcResult::Number((x + random() * (y - x)).floor()) } + + pub(crate) fn fn_roman(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() || args.len() > 2 { + return CalcResult::new_args_number_error(cell); + } + let number = match self.get_number(&args[0], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + + if number == 0.0 { + return CalcResult::String(String::new()); + } + if !(0.0..=3999.0).contains(&number) { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Number must be between 0 and 3999".to_string(), + }; + } + let form = if args.len() == 2 { + let mut t = match self.get_number(&args[1], cell) { + Ok(f) => f as i32, + Err(s) => return s, + }; + // If the value is a boolean TRUE/FALSE, convert to 0/4 + if t == 0 || t == 1 { + if let CalcResult::Boolean(b) = self.evaluate_node_in_context(&args[1], cell) { + if b { + // classic form + t = 0; + } else { + // simplified form + t = 4; + } + } + } + t + } else { + 0 + }; + if !(0..=4).contains(&form) { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Form must be between 0 and 4".to_string(), + }; + } + let roman_numeral = match to_roman_with_form(number as u32, form) { + Ok(s) => s, + Err(e) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("Could not convert to Roman numeral: {e}"), + } + } + }; + CalcResult::String(roman_numeral) + } + + pub(crate) fn fn_arabic(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + let roman_numeral = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::String(s) => s, + error @ CalcResult::Error { .. } => return error, + _ => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Argument must be a text string".to_string(), + } + } + }; + if roman_numeral.is_empty() { + return CalcResult::Number(0.0); + } + match from_roman(&roman_numeral) { + Ok(value) => CalcResult::Number(value as f64), + Err(e) => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: format!("Invalid Roman numeral: {e}"), + }, + } + } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index a6506b1..3b4bc50 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -16,6 +16,7 @@ mod information; mod logical; mod lookup_and_reference; mod macros; +mod math_util; mod mathematical; mod statistical; mod subtotal; @@ -108,6 +109,8 @@ pub enum Function { Lcm, Base, Decimal, + Roman, + Arabic, // Information ErrorType, @@ -302,7 +305,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -551,6 +554,8 @@ impl Function { Function::Delta, Function::Gestep, Function::Subtotal, + Function::Roman, + Function::Arabic, ] .into_iter() } @@ -601,6 +606,7 @@ impl Function { Function::IsoCeiling => "_xlfn.ISO.CEILING".to_string(), Function::Base => "_xlfn.BASE".to_string(), Function::Decimal => "_xlfn.DECIMAL".to_string(), + Function::Arabic => "_xlfn.ARABIC".to_string(), _ => self.to_string(), } @@ -668,6 +674,8 @@ impl Function { "LCM" => Some(Function::Lcm), "BASE" | "_XLFN.BASE" => Some(Function::Base), "DECIMAL" | "_XLFN.DECIMAL" => Some(Function::Decimal), + "ROMAN" => Some(Function::Roman), + "ARABIC" | "_XLFN.ARABIC" => Some(Function::Arabic), "PI" => Some(Function::Pi), "ABS" => Some(Function::Abs), "SQRT" => Some(Function::Sqrt), @@ -1131,6 +1139,8 @@ impl fmt::Display for Function { Function::Lcm => write!(f, "LCM"), Function::Base => write!(f, "BASE"), Function::Decimal => write!(f, "DECIMAL"), + Function::Roman => write!(f, "ROMAN"), + Function::Arabic => write!(f, "ARABIC"), } } } @@ -1406,6 +1416,8 @@ impl Model { Function::Lcm => self.fn_lcm(args, cell), Function::Base => self.fn_base(args, cell), Function::Decimal => self.fn_decimal(args, cell), + Function::Roman => self.fn_roman(args, cell), + Function::Arabic => self.fn_arabic(args, cell), } } } diff --git a/xlsx/tests/calc_tests/ARABIC_ROMAN.xlsx b/xlsx/tests/calc_tests/ARABIC_ROMAN.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a39f3bba2f68d65e711f1b338b486dc156c6b24f GIT binary patch literal 12737 zcmeHtbyyus(l^1~-QC^Yg1gJX-QC?KcyJ34oZ#*j4i+GIa0n3G-Qgp-ci+1=yYKsa z|L@LuX8KG||7vQwtGcSXOGy?C932D_1R4Ycgam|gq8(EA4G2giI0y(D2sEh9TL*hr zGkaG9H7`dq7d=K#J6qy>a8T+z5YX55|2_VLd!Qm&UZIZ}IqXJix%V8C!d{y=G}B$# z5JoQ<4BKZ)%guIVGxhpfZaEi%mZ5Y37`2#-56#zKN+t3jf#93R7*vWT@Uh>FcfM`|Ve@%IPx+$f7ls6uSA$F*LPlBzana&Q(LZc4XQDTbE&q>nar zkWZ4=f=BoWQ62C;ah(U|n1X4%cT(&tH9qQePTMR*@llIhdBN- z0;l57@iE=6s`v5&2BP#grEFAZCcAu_6P2$1TP2~p7P!B9Rgfqs5D=u-p7FG0@^Emr zF?MjU`DwK()a)Fxn310atzO>T@%2-NgIELJg93_*KP^_Q2&yx3$Ex=Xam#=D^x|iM zn&_TUT#6@S`_+U0W;y!g)>S>_8=Gcj!WHy;vKV^(ojT;JO=!PdbN9dsjrumjqKZ4{ z&Qr_}IlfQM9m)H9B44QBK=$23w*g%5HyiG2TOunCnS2qv*$dw(Ie9UV32jgap-%s%0;lW*nIKq~}ZWxQ`SIp11=8|aB# zG{HnbUH6HfH=v@(7>OdZlQgGM<@91*k(u891V+uLiuOI-!j}v|P)iGWZzlg>0#U(2 z!^Yf@zJ$_(+j?e>?j?4{cLfG)EyfJKkVCq>cuUaCDW=FMO1_S_sOsa=E|APkz?Z(}SgkHz$cC_)uU z($n-w=ih#-Q(bu{I&MTbVd!xk8$HN#ir}azvxN&Fy9`pOD^BV)$=U)V*dfTZhX<*) zZ{DwRWJ zM{<&~%SPnSg8XO$<@$Zmg562gA)Rt>o~@t7g#Ml=4d*_>Q(1@zVT4E3`;*o)xaw8r z_&`M%CjbiEcP)v)rxzP3kaSGSTou*WhyS|lh+?4!FYyQcRULsg+Vf$OCXHiTHP190 z1RH^rhqLK7TbDM0>`Ej~w|nNK`~%<*zl*Cicn9M9L?X-V6+%l-cb*Id#D-3(k{geO zj{|3>*}#aU!MVA!ajW|RkM&2Ej|!TO_3n22mv+wQaR1JX%t*u3oxf^E-K&=1f)f-V>yU)ea z!!ZH(*Wpn*SgBW2BRyPwzH3Zxo7$^;qmkr(7%M4ZP(Z^|HZfkpu4blXK_LTq8x{ux zVJXSyXXiARG%qtRP_?-dhZuven?|Y=kbipj3{S?94p&@IoB`l zCREDxjPffdK3dX29%JAB@zQBB#}hI>*@1+8D;G+hAL?O<8haiEtsFETbnKC zLfcU~wcaG^VEH+@7AEPvPOj+TCsap5T3twxqKS}U zY7a^cGVP^JZtd=)BwD@c3kpfrivcIKVA4fWdNU+72!baUIVM^CMRVBAEee9povwrkQB~cy{GsDa@Khnpwk*Qfh`pnS{r$hy66uyIo`&q@y z^8#cnHt1TSQ0-*XvrD43ia8R$i6Z4x1#_-mCxma6h2z)iH`_NzvSfCr7+vhVYvlmuv=z-yPqJTz7rq>gd6NR-Y5B{S|r7Ct@mCZO*d!*K;Ek- z4J1j$+) zcS|?b`*Tc+M9&&W2f5di?XIBG3Kt^(R(syvu*;4zWDpZNB z38U{ih~AG!S53VfSu2m2|(HtQq)sF9*yM<-`jSlrU0W6k-GW z(jTw)4t4_cJMQoAM%trE7uCIWU#^Zf?mxq0w2P)_l%jM`k;y8Q2bXg)Djt0MHu{p( ziaU9Vb}0X{ZnBZiS_vk7W42?=3x%e)%KdeUFY;*XL&&Wq(#!T3(=@Qe#KF^f#K)P> zGf_7ZQHI}VDib#1=oon=-bF+Yx~FcbY;vaFxna}YBgW9Y%&Ffpyg(YL6^HHMq3z1n zr1%tI0-Sp|$|~BF9s=U1DE1xoGn?hYB!>%YLnj|Nu`aWuv#uRGH-aKmb&yo?D}aeg zG=De2SkeoswJ+<_tCbPdXkAIcK8OR#=+@E-YBZ{25#AD=NLS2Yly95NF6qVAt|n?_ z&}op3H<`EFOdudd-`VII3<0gS5{}&G6d+YKmxj@{AtJ$#e&nsV&&i8mHTl(dh`vwg z#IIXBzNh&y)jlFSk3ZEyXce%?rR>8IJ-aAuAA~3~Y7pPR!)2-9y@FCUB+^gHp$EOY zMHVR&Qc55B#cdpLa4k} zki)3OfE{Kjs{toLvKTcRtQ^^7-e`!PZwEvaDM&c8-r5TlBvGSMwL}xWVl`8I+ehxv zAw5G7$qM78CP-a7M?OPF3Km;{iGouD?Is111j(!ol60`@x)5^d)N9>KD5g^fsDYZ) zsK7iFY2YY_@R_)LH&0}Uw2$aT7{ph~V+uVg(n<36+a+7YN20m(B?h;*l|=nzT*-?E zv1l9-&>{!a=+Ve6A)?aIe-5RB8&9aaTW?e^shVz`@hLQhW(BsKXg>!XWSWu_*){BZ zC?bGvoaPj!h&KbYKHFDW7{pBS2 zKYDwP7vU{$hhe`ZHYuYv3vPHMokmPJ$wnqdfl*`&O1l{=ygcfVs*N+eJlN2@Db9T< zlgVf{yWB-jmIQ@ZvK8q)fAx(2baswa*AcbzyjWJWIAW}ci!^Z>q_Z}0+JU`0PNcj4 zEAPTTd1prc$veE*Pu{_loygqbafb+dG;%C0N$?I$k>yQ8I^pYFW|*h8Gd&V{1-0I- zezJOkWtz<5z{$##CNej*bw>5ef$5uKribf0{}}OsF+ZNO1J_QB$ljCVK2mV?)S4aD zuV;OBm;3;smfL`mZn6Y_oINXBjL4OL*_8agVskwm#=0#sK(c$SC;GVA{QMvamrp(TsiYzQ^CvDX-!jdSpC>8()~oug6hb&joDqg{30a9 z)&O$@?ScjG?NwU_xFi1ELq@nG0cs&9`FNf${G28q24fpla8BCyvixmrtSZ$=!;cEJ zDgY$hxEJ;ml_M}8r}A9PMAQT?tJnnWDphRNAt*~L)uYr+q80%~S{#&Rziu*LZ-7q1 zT$3UWTa4;9Urupf!eYF4dYK6%YgR*yMC}LU9=K^)IZdPojs*1bfo*RrP40{Z*vdSE zQS#n%VvXQq4{aw3Ga~$`6J{j3U&t7u>@+yFmNP?h8!l(bke|V*9&qJUp6kFxTNGOv z>I9RhldMhQaZN;UC!F9>^g$YuSDRLhiwYjoQaSy0KAo8#eD2E2k_u4O?#ZvUjt$T@ zo+QVY@0+B@AB)KG;E9Qo>@Dv#Qqw!PcEncK?7+pBnB}O3j8JwX^FZc}$&##$QJ~zM z|Js*7&fRoVW6AM3)7}N0+;$);G&bd2Rp>UiCXR}7k1AF=EOQaa<{p+0XBkNBxQ zW8eB9eHquEwu<`{KBo1}(ZgYj^_vVu;y_xUqD}|I-tQZer_C&m|=DE>H<~@?$-0E1AK~9u(i4;;_L=g`>Wui#UgF&3)!sUrhM<9o5eIo|mUP zVGfv3A87red?zr$bl`92&D z&dHCBLqT2-HG8(c`6){W04#-ZK#?wrEoAMwjJAFM0e7^v)^~L{L>*^(nsVI|H_m+B zQt-0t0*u~94pu*&$mr+mHY2oByEd+FSyF$#&p*Lz53jYH^2O;Q|7ujSL$)&g-K5w2 zus{5g&%75a9@-RpuCiAhGm&l4>FKGk{^1^yie^SNFF~&2j2~%-BN5o(c?Q;XxxUPO zXl6y2B8Hu78>U~`u6BLnu8VlH@Wj(eLoNsHy{e@|{B`~1y^F;kWs>A$O2>FjGmbNb zH9pqxKA0`Smb7pKQ#idY^y#6&R@CWX!L4xk9t@GBjdno_HV4(s>aAc4>86XrAxGh& zF_r%6OW20CZB)Vg54lDrIGpY@0~+0*>=2kaSI9&V;<4LNP09^sIiaw#jx+Kklw$Ov zYiFyim%w7=ZngkQ@eEit^Kjw&W-Pbr#QM9X`2(#=EXKZrOI z)I*92sXGgbnM?*vD@J7#eh|O9-;19vXe`winsoBM#GK_iPdDDfXTt+z0Ov>=Lims% zpX)3{Pu@%#jbs>lk1BsE(1)}<_I&;*{e;~!k(jM1vQTOdDSpwN-`}&KwH|f>$%vL&rx-2XB^E4zRx;&%UWH$T_ z3PeP8n%;hFb(+%Tq2>7NmuD>5A8t`T&&E04-(r9LdIh~6u1CLu^;A|BDUUt!+#6c^ zfiaaQdrP-Yi;_iqv@oiAo1pXMHLiVm`4!33(MZWDUdhwTjE~Z33`xt=)vB{juxYAL ze~}vxtd%{7>&Y*cft9|aptx&g)|aq0ma#^Ow5TXdNX<0>-P~PT*iGBPGQ+YcEKZ&O zxn}aOfwailh}_WY!Xygm&uz6|fwZfonVlKauj{WE`VcS{kJpURjkWv2oZenBX?fXL zgTFhC=i||N`i0TCkX+_1lLa?%*$M_ISeVN`g>YcQZjrEq0JsP!xp0X7{UCPDWO0BP z(eQiomVD!Cp}>ZDjGFZI$JNdF69K_4lAU8JO_(c2W~|U6y>54^Q-SQS^Vx+XRWGKj zRSkZMQpsOY%xI49ZRj5rkDso&JSKx;oPq@s6#L)o-`6HDh&0lgPs9KTQrFRQ7_b!+ zLUp<-zVftzcfwSLi%e8dMizFZ<%7=%0_ca}eb9=gx|@{+^BuEfKH{1u=hDp1Cr-@Y zdC8Li0r1o}jL1j#6XV%>3a2TT0#Tu%PA4YY zie1_Ed|;j`id&*F*yc~mIVquQ1g=dgC za(|~B>pH@_A!@*bez*!{9uIVglTyUXKGq(cos(;zvNYffl+w6F`s@rClO*T<1eG_- zB%=RxIX!|U)a842lV;$1cHZx?ilRL*Ra*B0QMrwN;XNs>!H z`K-G8sqY}*@gi-ePmNCQY+rG$`}yoaGvN8$V9obvVs+CJ5UwZ{9OeXf%=x5eqV}r1v?Q=#&6Kzf$m(`tLx$)p{Bz55j76EhK3e)2|!0V-Ytr%-c4g(tFB{31DDEuTW6_Hu^J zJ+9WoH5bKr;VONU8_kxab3AfR5@?kOlK}`)gCf2?yy0oT-N;_=_V!f}iBj%2bW zmxB`{_(7EpFz<-hK&4y1x{(IX%K=BqIf8w#$-#A3E;85ia_%)i0BnmA2F zDrKwCOf>CgOx8-i!}vimvX<3(=qnV93@epI;(*y<$p*eCx>t!@+xNlI9Fl#)$H1x1 zV}F^Ntz#~#V5)@gUy`Fhlod6W*GC@G7cQLSv7p=Qe|#V&a~yt-`KR2GI5L zc?MRK6baz(>FaIv6a10iYVZK7db|S=1|9R zi(ob$*mDEtDknZTr^g_|#_t|>!1I$RJ&Sa3$DT_C#;G{QUt0;pzHf9d^I4CZ<7j@o z;dFuD-m1^~Dx1*Dl^O(m{??R1Z0)85J_?RX5G2_O`z0XSWC&rB8 zRo*lmF);I-Q)Avj7zWdoT)7&0XJ(5=~mEuvOp*;c|)wW3wY|B+{X?v z2NUp=PVRhB9GJyBq22LSQaoX!WRL_wpKs4OrmpgdD3P!bJ!0Uudq1JTVq}&y(KQ8; z6S;239sl*b>wyVkS>A7jM%Q{$wRnCsJXoUqiN&`vaj0=wc-px*ugn9+g+Ek!$A>N1 z?Fr%#ug@$|iL7H@^`{I*%cQMc_)tXm6fygz8qS@N}G&MAtr#}IUxsC9Vn z?QKG)^aRbBcdKAZWE($s-PAp7Hv+V)w21in^R0?78g1L^_Xc&gWGkZ7XN=99Z?3YJ z7RdRa)pl6Vo^1l&0VXrVWF?XX}Iu23$K9_Pqb}^;WS~mYWOte0S=v&*^HV$7sqsJR4;zkc{j##prPf!&}KPO zK4#VQY1VE0i;?*ZFM^G1?`#!@QC2n?^@lN|s^QKAiz-untG<-xjlzQfLU!*-Vq17x zk#?3(Vpr6q4S1d2MRS%jHU$z4*w=kK!iodZl}25@*nL*WdOK@EyVoBah!e+x=04!a zev%8=RGM9#TN9r7{In(}DXva^69cH^jV~)fN%Zcjh=@=`k?w zcOn{6pznvwA$cTI&xx&;Z=Sc?%$K5p9^P3xYZITpda?Y(?3@(hq7!(>UhVS&@$U>j zaieH!*6V(YC?p67!XJj;#nsE!%;ndzxth+;Wpn@Bh8M%T54e^?iO{8)#ZoCv)+&5f z)rjf9Or{}s);g;fzd>Qte1)Z%uJ4%JYi%+KGNSOVfA(nw6tk5 zG_^Fgm{OvWhj*`m=}}4_*Us&z3M6f}ZJWYJ+CyPGG}VG!*q=ZpIf1z=vXi4)EEBo! zdN~bg$Eq@=zI29fD=$5SRPhL@p#+p2h0+UP{Sm=gi9$-HL|LtFb)(5?F<;U;=K0vp zhPIXa^2NA%_Y)tsEMn`m%*GR9M3|&*BwF-*J&1UYXe&P3r$9K|A3JZs@X=j0kfs-& z`n*kcNurV_!KQddryDsjT+g|m^;1fMXAvWDyB6-J_aM7$j_}p^6zLSW0LtrcdAhK{ zp-W6f`S0vs00xH594dU;Qa4Iw*xLhC>(rveA@iG8Sn)y<2eoVP(Tn$}M97j2Oj5ba zWCoY;LicCr6H{Q|fRh$S-KQXCLyaRIh>z8Q7X{xac78yxD%WNnhx!OUjh<>h18KXG z&W88d)@(V@K>L35gzddI+ql^V0BE4Am}03!1+RNUB8)my58;LRc0}OHViKBoUwPv=QC4R z^--tG?a`;pS1093<$H2`pg&0>#c@*9Z^aZv6@Lmo`;ip3nFv!A*T}13SrJCsd&MMj ztJEfwaB)5!0QT>sSn>lEZPcr0i~0H#<#p%K)WJl_*}>6;$;83g?9ZIA|85w*s&|Y^ zul*7;d?(ht5Oy11-G@r*7$C&r#}W+e33vi`RT@rp8`)ss%-C9d6FwD=H}>}s!nIAX zyV2Pz$vN{A?0hG8hA-kItPS5AYL^;s8(T-J#W1C{D*+Zx4A2d<$A32GLlg2C_Ko5PTR|6*Y2#?GrS{*iBP(o~k;@>dLxb5T&ieWe~$#a|!xQ z%Z!>X+b7~6amkzJKNeAsh||OaK2P*3;z?|aE#NHrtXkpT%;>x&wYZXGudg|I5bwqx z{C+2U_wYQ}*J5pdn1GRIsexm^mT^@X36hdG>)vgK+v`{?JI&-Lw$%mPsMOnYd!B zZb}8j+Zxop(RW3N(Y|n~-)PfYv~&wx+x~DX%u5=bBn3p98PtU#z6jW;vOr|y*lLWU zZAF7n<(SDUZimOeoD@I=65TfCU1Uffh`=7r4YrlhBAD_big%v-d~PHp&S^WwIUwk% zc`MTnC#YQ96d5u3mMNze30mUmn1wH*D#SE4vw#mbzwe!_%O&s@IPK8P2nHA6OiB2) zx^YCksJiia@m!B>f9Kuxzn$fLv-&h4Uh`g|@qY@*uOQvT$kyborID5WPa$z#SW?K6 zL=RuTkm-aP*@-o2mgj;M%e3R*8!MfLtLZ_IG9-=pbbWlF0Ykku5)cgROI&~9x2b%y z8gTpYY=1Ske&+h%*rDaH)}dF8I9MKWu(UZCeMQ9PdvoFK=DkQ%edB>N*w=rb+h_H( ze!k?eYH)N?bvX8X-NM`b`OLMp%gg)z^1$4st&L~-MA3mzkRWcS`tk16vpaw{;QrRh z<^Ixh-XPZ=F{0$5eeC#X$s_CG0%0eotwATYTMUXl|iFN zcXp5WZp1Fp*(CqhiN-=F7h1uLQu|q~hkGy*s|0&}1D*_!V~Ecm6vTL#5Y+X5oIjuc zf1Drf<}t#Xj5M(q!}W|T@!Z4yRI?a^Dkypp4=B<_*CKK_cL&-`x z1Cw@)PO)%UeKZ7gI04)$9S1{&7$=b!A*omtmi#R$@H-7XP=udcc!Mr~r%OQ5RX`7* zjYTWqE~;JXS~?1sf|+9Vy79tl{L)}CX)!}s4VOY#t&K@bKzkutByI*MWiO54&V92% zf@n;#osv||RTC8p`1Jyv=3g%;G}6?R3Nn=nX1BeCrAdI7P6+oA2}73(CztB8%m1Uv zI-&{*L?Q{yl%q>o!Y?*kPSpMYEho|z3bT@n;2vxI$v9WtA7qNLT0beF*NLiFdf$}8 zij&ZhhZWED|5-Jk`>V#*hMqnwKLI0Aw9iiNBeiK*;l$;>4n~xyh7_#4!-)FNDj;67 zOb@}>TYPfkZivVTZYdxq95RwqLgWetG2sR=N*1j!KTw49S|!jkGDOVwBgh#t)Vq0} zpP!6cYXl1X3Csom1-yp-1=K_N14!}{n139c_Z=f!bY_wWy(3sPl6Nm8SknYkg8u5j*7iL#AT7El~U9sCKLiUzDkZw0fM89@HGg!zBWqyH%XP5Qi& z?B5CeJ-PQEz@KHx>n!z8slLAhe@}S)3)=P?Z2Xz3^N;k$-@$*+1^WvM0!&iK>o$k{T=>y&*GoqbJYI?|I^R-9sRq>`3wI@`-|_t86YKDh}V$?0fBw} N`M$~{6VuPT{|9eZ4GaJP literal 0 HcmV?d00001