See Release Notes
Long Term Support Release
Differences Between: [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 3 namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat; 4 5 use PhpOffice\PhpSpreadsheet\Shared\StringHelper; 6 use PhpOffice\PhpSpreadsheet\Style\NumberFormat; 7 8 class NumberFormatter 9 { 10 private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/'; 11 12 private static function mergeComplexNumberFormatMasks(array $numbers, array $masks): array 13 { 14 $decimalCount = strlen($numbers[1]); 15 $postDecimalMasks = []; 16 17 do { 18 $tempMask = array_pop($masks); 19 if ($tempMask !== null) { 20 $postDecimalMasks[] = $tempMask; 21 $decimalCount -= strlen($tempMask); 22 } 23 } while ($tempMask !== null && $decimalCount > 0); 24 25 return [ 26 implode('.', $masks), 27 implode('.', array_reverse($postDecimalMasks)), 28 ]; 29 } 30 31 /** 32 * @param mixed $number 33 */ 34 private static function processComplexNumberFormatMask($number, string $mask): string 35 { 36 /** @var string */ 37 $result = $number; 38 $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); 39 40 if ($maskingBlockCount > 1) { 41 $maskingBlocks = array_reverse($maskingBlocks[0]); 42 43 $offset = 0; 44 foreach ($maskingBlocks as $block) { 45 $size = strlen($block[0]); 46 $divisor = 10 ** $size; 47 $offset = $block[1]; 48 49 /** @var float */ 50 $numberFloat = $number; 51 $blockValue = sprintf("%0{$size}d", fmod($numberFloat, $divisor)); 52 $number = floor($numberFloat / $divisor); 53 $mask = substr_replace($mask, $blockValue, $offset, $size); 54 } 55 /** @var string */ 56 $numberString = $number; 57 if ($number > 0) { 58 $mask = substr_replace($mask, $numberString, $offset, 0); 59 } 60 $result = $mask; 61 } 62 63 return self::makeString($result); 64 } 65 66 /** 67 * @param mixed $number 68 */ 69 private static function complexNumberFormatMask($number, string $mask, bool $splitOnPoint = true): string 70 { 71 $sign = ($number < 0.0) ? '-' : ''; 72 /** @var float */ 73 $numberFloat = $number; 74 $number = (string) abs($numberFloat); 75 76 if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) { 77 $numbers = explode('.', $number); 78 $masks = explode('.', $mask); 79 if (count($masks) > 2) { 80 $masks = self::mergeComplexNumberFormatMasks($numbers, $masks); 81 } 82 $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false); 83 $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false)); 84 85 return "{$sign}{$integerPart}.{$decimalPart}"; 86 } 87 88 $result = self::processComplexNumberFormatMask($number, $mask); 89 90 return "{$sign}{$result}"; 91 } 92 93 /** 94 * @param mixed $value 95 */ 96 private static function formatStraightNumericValue($value, string $format, array $matches, bool $useThousands): string 97 { 98 /** @var float */ 99 $valueFloat = $value; 100 $left = $matches[1]; 101 $dec = $matches[2]; 102 $right = $matches[3]; 103 104 // minimun width of formatted number (including dot) 105 $minWidth = strlen($left) + strlen($dec) + strlen($right); 106 if ($useThousands) { 107 $value = number_format( 108 $valueFloat, 109 strlen($right), 110 StringHelper::getDecimalSeparator(), 111 StringHelper::getThousandsSeparator() 112 ); 113 114 return self::pregReplace(self::NUMBER_REGEX, $value, $format); 115 } 116 117 if (preg_match('/[0#]E[+-]0/i', $format)) { 118 // Scientific format 119 return sprintf('%5.2E', $valueFloat); 120 } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { 121 if ($value == (int) $valueFloat && substr_count($format, '.') === 1) { 122 $value *= 10 ** strlen(explode('.', $format)[1]); 123 } 124 125 return self::complexNumberFormatMask($value, $format); 126 } 127 128 $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; 129 /** @var float */ 130 $valueFloat = $value; 131 $value = sprintf($sprintf_pattern, round($valueFloat, strlen($right))); 132 133 return self::pregReplace(self::NUMBER_REGEX, $value, $format); 134 } 135 136 /** 137 * @param mixed $value 138 */ 139 public static function format($value, string $format): string 140 { 141 // The "_" in this string has already been stripped out, 142 // so this test is never true. Furthermore, testing 143 // on Excel shows this format uses Euro symbol, not "EUR". 144 //if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) { 145 // return 'EUR ' . sprintf('%1.2f', $value); 146 //} 147 148 // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols 149 $format = self::makeString(str_replace(['"', '*'], '', $format)); 150 151 // Find out if we need thousands separator 152 // This is indicated by a comma enclosed by a digit placeholder: 153 // #,# or 0,0 154 $useThousands = (bool) preg_match('/(#,#|0,0)/', $format); 155 if ($useThousands) { 156 $format = self::pregReplace('/0,0/', '00', $format); 157 $format = self::pregReplace('/#,#/', '##', $format); 158 } 159 160 // Scale thousands, millions,... 161 // This is indicated by a number of commas after a digit placeholder: 162 // #, or 0.0,, 163 $scale = 1; // same as no scale 164 $matches = []; 165 if (preg_match('/(#|0)(,+)/', $format, $matches)) { 166 $scale = 1000 ** strlen($matches[2]); 167 168 // strip the commas 169 $format = self::pregReplace('/0,+/', '0', $format); 170 $format = self::pregReplace('/#,+/', '#', $format); 171 } 172 if (preg_match('/#?.*\?\/\?/', $format, $m)) { 173 $value = FractionFormatter::format($value, $format); 174 } else { 175 // Handle the number itself 176 177 // scale number 178 $value = $value / $scale; 179 // Strip # 180 $format = self::pregReplace('/\\#/', '0', $format); 181 // Remove locale code [$-###] 182 $format = self::pregReplace('/\[\$\-.*\]/', '', $format); 183 184 $n = '/\\[[^\\]]+\\]/'; 185 $m = self::pregReplace($n, '', $format); 186 if (preg_match(self::NUMBER_REGEX, $m, $matches)) { 187 // There are placeholders for digits, so inject digits from the value into the mask 188 $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands); 189 } elseif ($format !== NumberFormat::FORMAT_GENERAL) { 190 // Yes, I know that this is basically just a hack; 191 // if there's no placeholders for digits, just return the format mask "as is" 192 $value = self::makeString(str_replace('?', '', $format)); 193 } 194 } 195 196 if (preg_match('/\[\$(.*)\]/u', $format, $m)) { 197 // Currency or Accounting 198 $currencyCode = $m[1]; 199 [$currencyCode] = explode('-', $currencyCode); 200 if ($currencyCode == '') { 201 $currencyCode = StringHelper::getCurrencyCode(); 202 } 203 $value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value); 204 } 205 206 return (string) $value; 207 } 208 209 /** 210 * @param array|string $value 211 */ 212 private static function makeString($value): string 213 { 214 return is_array($value) ? '' : "$value"; 215 } 216 217 private static function pregReplace(string $pattern, string $replacement, string $subject): string 218 { 219 return self::makeString(preg_replace($pattern, $replacement, $subject) ?? ''); 220 } 221 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body