nfTlWNl rn Ѻq$³*L ZR]_-H#GDn,|2 ^C#֒Hnn!V5J4ot&:5_mPkmPTW=,_NgTs 'D0fI4 ;$*J0$H$ vG&GU65Ҭ N,ԐUO#ux@t%3-٤ >iĻco9N4r:/Ч5<3INl*oZsh*i箑E3;*ˀ=~ Mݽǂ(dmbeD z:z!A\^\J6 |;\ /TEld9n<5P+YM:\sf6UZExgWiSb1eA!RҼqpAԵwɑf3(eAErWF/q5 N%zhOL&4\eT! zV<T$.(lR/@Yn &vc,.>&L>A{}9<'$sve/QVߨ݈S#+O;X%O.v6+%_g2q9KYHkJH*0Mb¯<-Z ]NKB{ݾ|a.aٳeΏVB5޶)f`k{-h2\os/QqnvlϺn A7 3:9+~Ǧ/%7V[At#q2TlMqi脭輣}0hx3%Es!vP-\#ĉb ĖoKoC6/bm69]u-6$#nG[0`Pue5#A+%DmމejI{\Mp&ߌs4GNZQ z#Wy{d1Cx"+;**SNv)-}VD 1gX?ܲګ˜O$A+ Tᤨn%]o8sU#:@S5)݇*Y5j~V?۫i+`5%]Bx`SΪՖ9+;Wl`#~0|{Ps˩ * G|p8ǤQ2UcFC0<)Dw)Z8 .1ϲlQ`tvKIz6*go-sK\"z9/MBK7)-'R !n~fٛ.goAXD{\|O|2}ECw;4QH c(RzI0/nWg ]D~Wki 3PAɮglb1_ڟcG?FDj1pm_:;c4aٳeΏV #>KhҸ-l cCDBm6S^yԐ`+ - zlaչ YuutcdW>Kfx"ןUm4)x @cQ˖+NV^uaˏoKI:<Ƅe/ ߂,ˬn2]l\t^+H3*Pà$4!f1ېGH"ttD,D0RU$}^aX)yƀDfL<*7֣(څV)aٳeΏVކxo4RL `uzpCJvL u߃wV)mJtǮ.< h|4"]!12. yM)-ݝ6i+٠}c_kJFa|o̷Z!欺BZтl )U4!f1ېGH"tt&.g?S_zt>S@8P Qn$з`˴Gudܼ쟤'G^!{7ly&i%X<1 TZa S'%vM8dm8 1T!?y?= ֈ:hy:bh˨+6QBqvuȈ<7YE0\X/I3'Y&oԍ QNCoz?O9;au(g, >v]_kb 6+H["⟘DA ѫQl2 ;ʀsh*i箑E3;*ˀ=~XbDjM] Zt07Zl*\yCI[rg zq&4`hn.H4tV<99Xާ!T &F-ә*( U(Ln(KCȹljr@ӓo7|jJgf|r߽ q^gx]5;xz)k$0{\N tG C+މՐDBS*2]w!ʱ1_)6AEpspVC~a$й Jbl_Ll£3 N`!yUKA&BORokTkմ1qPsd|ሑO-D0Jjg?\ǭ-hSGKi9Tʠ!o5 h >h6NoܗFI||D鐯K^2W3u:҅x (e permitted), optional sign, one or more digits if (preg_match('/^([+-])?([1-9])([.]([0-9]+))?[eE]([+-]?[0-9]+)$/', $s, $matches) === 1) { $exponent = (int) $matches[5]; $sign = ($matches[1] === '-') ? '-' : ''; if ($exponent >= 0) { $exponentPlus1 = $exponent + 1; $out = $matches[2] . $matches[4]; $len = strlen($out); if ($len < $exponentPlus1) { $out .= str_repeat('0', $exponentPlus1 - $len); } $out = substr($out, 0, $exponentPlus1) . ((strlen($out) === $exponentPlus1) ? '' : ('.' . substr($out, $exponentPlus1))); $s = "$sign$out"; } else { $s = $sign . '0.' . str_repeat('0', -$exponent - 1) . $matches[2] . $matches[4]; } } return $s; } /** * @param mixed $value */ private static function formatStraightNumericValue($value, string $format, array $matches, bool $useThousands): string { /** @var float */ $valueFloat = $value; $left = $matches[1]; $dec = $matches[2]; $right = $matches[3]; // minimun width of formatted number (including dot) $minWidth = strlen($left) + strlen($dec) + strlen($right); if ($useThousands) { $value = number_format( $valueFloat, strlen($right), StringHelper::getDecimalSeparator(), StringHelper::getThousandsSeparator() ); return self::pregReplace(self::NUMBER_REGEX, $value, $format); } if (preg_match('/[0#]E[+-]0/i', $format)) { // Scientific format $decimals = strlen($right); $size = $decimals + 3; return sprintf("%{$size}.{$decimals}E", $valueFloat); } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { if ($valueFloat == floor($valueFloat) && substr_count($format, '.') === 1) { $value *= 10 ** strlen(explode('.', $format)[1]); } $result = self::complexNumberFormatMask($value, $format); if (strpos($result, 'E') !== false) { // This is a hack and doesn't match Excel. // It will, at least, be an accurate representation, // even if formatted incorrectly. // This is needed for absolute values >=1E18. $result = self::f2s($valueFloat); } return $result; } $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; /** @var float */ $valueFloat = $value; $value = sprintf($sprintf_pattern, round($valueFloat, strlen($right))); return self::pregReplace(self::NUMBER_REGEX, $value, $format); } /** * @param mixed $value */ public static function format($value, string $format): string { // The "_" in this string has already been stripped out, // so this test is never true. Furthermore, testing // on Excel shows this format uses Euro symbol, not "EUR". // if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) { // return 'EUR ' . sprintf('%1.2f', $value); // } $baseFormat = $format; $useThousands = self::areThousandsRequired($format); $scale = self::scaleThousandsMillions($format); if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) { // It's a dirty hack; but replace # and 0 digit placeholders with ? $format = (string) preg_replace('/[#0]+\//', '?/', $format); $format = (string) preg_replace('/\/[#0]+/', '/?', $format); $value = FractionFormatter::format($value, $format); } else { // Handle the number itself // scale number $value = $value / $scale; $paddingPlaceholder = (strpos($format, '?') !== false); // Replace # or ? with 0 $format = self::pregReplace('/[\\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format); // Remove locale code [$-###] for an LCID $format = self::pregReplace('/\[\$\-.*\]/', '', $format); $n = '/\\[[^\\]]+\\]/'; $m = self::pregReplace($n, '', $format); // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols $format = self::makeString(str_replace(['"', '*'], '', $format)); if (preg_match(self::NUMBER_REGEX, $m, $matches)) { // There are placeholders for digits, so inject digits from the value into the mask $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands); if ($paddingPlaceholder === true) { $value = self::padValue($value, $baseFormat); } } elseif ($format !== NumberFormat::FORMAT_GENERAL) { // Yes, I know that this is basically just a hack; // if there's no placeholders for digits, just return the format mask "as is" $value = self::makeString(str_replace('?', '', $format)); } } if (preg_match('/\[\$(.*)\]/u', $format, $m)) { // Currency or Accounting $currencyCode = $m[1]; [$currencyCode] = explode('-', $currencyCode); if ($currencyCode == '') { $currencyCode = StringHelper::getCurrencyCode(); } $value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value); } if ( (strpos((string) $value, '0.') !== false) && ((strpos($baseFormat, '#.') !== false) || (strpos($baseFormat, '?.') !== false)) ) { $value = preg_replace('/(\b)0\.|([^\d])0\./', '${2}.', (string) $value); } return (string) $value; } /** * @param array|string $value */ private static function makeString($value): string { return is_array($value) ? '' : "$value"; } private static function pregReplace(string $pattern, string $replacement, string $subject): string { return self::makeString(preg_replace($pattern, $replacement, $subject) ?? ''); } public static function padValue(string $value, string $baseFormat): string { /** @phpstan-ignore-next-line */ [$preDecimal, $postDecimal] = preg_split('/\.(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu', $baseFormat . '.?'); $length = strlen($value); if (strpos($postDecimal, '?') !== false) { $value = str_pad(rtrim($value, '0. '), $length, ' ', STR_PAD_RIGHT); } if (strpos($preDecimal, '?') !== false) { $value = str_pad(ltrim($value, '0, '), $length, ' ', STR_PAD_LEFT); } return $value; } /** * Find out if we need thousands separator * This is indicated by a comma enclosed by a digit placeholders: #, 0 or ? */ public static function areThousandsRequired(string &$format): bool { $useThousands = (bool) preg_match('/([#\?0]),([#\?0])/', $format); if ($useThousands) { $format = self::pregReplace('/([#\?0]),([#\?0])/', '${1}${2}', $format); } return $useThousands; } /** * Scale thousands, millions,... * This is indicated by a number of commas after a digit placeholder: #, or 0.0,, or ?,. */ public static function scaleThousandsMillions(string &$format): int { $scale = 1; // same as no scale if (preg_match('/(#|0|\?)(,+)/', $format, $matches)) { $scale = 1000 ** strlen($matches[2]); // strip the commas $format = self::pregReplace('/([#\?0]),+/', '${1}', $format); } return $scale; } }