From 699da091760d39ddc9f0656b507b7c26b9dc4f4b Mon Sep 17 00:00:00 2001 From: GreatHumorist Date: Sat, 8 Sep 2018 01:32:17 +0800 Subject: [PATCH] Fix time format for duration was incorrect When using format `[h]:mm` it should convert to the "total hours:minutes" Closes #666 Fixes #664 Fixes #446 Fixes #342 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Style/NumberFormat.php | 178 +++++++++++----------- tests/data/Style/NumberFormatDates.php | 23 +++ 3 files changed, 117 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e859fe9c..91c60830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Improved performance when loading large spreadsheets - [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824) - Fix color from CSS when reading from HTML - [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831) - Fix infinite loop when reading invalid ODS files - [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832) +- Fix time format for duration is incorrect - [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666) ## [1.5.2] - 2018-11-25 diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index dc52a841..b6b80378 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -424,9 +424,9 @@ class NumberFormat extends Supervisor * @var array */ private static $dateFormatReplacements24 = [ - 'hh' => 'H', - 'h' => 'G', - ]; + 'hh' => 'H', + 'h' => 'G', + ]; /** * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). @@ -434,9 +434,9 @@ class NumberFormat extends Supervisor * @var array */ private static $dateFormatReplacements12 = [ - 'hh' => 'h', - 'h' => 'g', - ]; + 'hh' => 'h', + 'h' => 'g', + ]; private static function setLowercaseCallback($matches) { @@ -467,6 +467,13 @@ class NumberFormat extends Supervisor $block = strtr($block, self::$dateFormatReplacements); if (!strpos($block, 'A')) { // 24-hour time format + // when [h]:mm format, the [h] should replace to the hours of the value * 24 + if (false !== strpos($block, '[h]')) { + $hours = (int) ($value * 24); + $block = str_replace('[h]', $hours, $block); + + continue; + } $block = strtr($block, self::$dateFormatReplacements24); } else { // 12-hour time format @@ -636,104 +643,105 @@ class NumberFormat extends Supervisor // Save format with color information for later use below $formatColor = $format; - // Strip color information - $color_regex = '/^\\[[a-zA-Z]+\\]/'; - $format = preg_replace($color_regex, '', $format); - // Let's begin inspecting the format and converting the value to a formatted string // Check for date/time characters (not inside quotes) if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) { // datetime format self::formatAsDate($value, $format); - } elseif (preg_match('/%$/', $format)) { - // % number format - self::formatAsPercentage($value, $format); } else { - if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) { - $value = 'EUR ' . sprintf('%1.2f', $value); + // Strip color information + $color_regex = '/^\\[[a-zA-Z]+\\]/'; + $format = preg_replace($color_regex, '', $format); + if (preg_match('/%$/', $format)) { + // % number format + self::formatAsPercentage($value, $format); } else { - // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols - $format = str_replace(['"', '*'], '', $format); - - // Find out if we need thousands separator - // This is indicated by a comma enclosed by a digit placeholder: - // #,# or 0,0 - $useThousands = preg_match('/(#,#|0,0)/', $format); - if ($useThousands) { - $format = preg_replace('/0,0/', '00', $format); - $format = preg_replace('/#,#/', '##', $format); - } - - // Scale thousands, millions,... - // This is indicated by a number of commas after a digit placeholder: - // #, or 0.0,, - $scale = 1; // same as no scale - $matches = []; - if (preg_match('/(#|0)(,+)/', $format, $matches)) { - $scale = pow(1000, strlen($matches[2])); - - // strip the commas - $format = preg_replace('/0,+/', '0', $format); - $format = preg_replace('/#,+/', '#', $format); - } - - if (preg_match('/#?.*\?\/\?/', $format, $m)) { - if ($value != (int) $value) { - self::formatAsFraction($value, $format); - } + if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) { + $value = 'EUR ' . sprintf('%1.2f', $value); } else { - // Handle the number itself + // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols + $format = str_replace(['"', '*'], '', $format); - // scale number - $value = $value / $scale; + // Find out if we need thousands separator + // This is indicated by a comma enclosed by a digit placeholder: + // #,# or 0,0 + $useThousands = preg_match('/(#,#|0,0)/', $format); + if ($useThousands) { + $format = preg_replace('/0,0/', '00', $format); + $format = preg_replace('/#,#/', '##', $format); + } - // Strip # - $format = preg_replace('/\\#/', '0', $format); + // Scale thousands, millions,... + // This is indicated by a number of commas after a digit placeholder: + // #, or 0.0,, + $scale = 1; // same as no scale + $matches = []; + if (preg_match('/(#|0)(,+)/', $format, $matches)) { + $scale = pow(1000, strlen($matches[2])); - // Remove locale code [$-###] - $format = preg_replace('/\[\$\-.*\]/', '', $format); + // strip the commas + $format = preg_replace('/0,+/', '0', $format); + $format = preg_replace('/#,+/', '#', $format); + } - $n = '/\\[[^\\]]+\\]/'; - $m = preg_replace($n, '', $format); - $number_regex = '/(0+)(\\.?)(0*)/'; - if (preg_match($number_regex, $m, $matches)) { - $left = $matches[1]; - $dec = $matches[2]; - $right = $matches[3]; + if (preg_match('/#?.*\?\/\?/', $format, $m)) { + if ($value != (int) $value) { + self::formatAsFraction($value, $format); + } + } else { + // Handle the number itself - // minimun width of formatted number (including dot) - $minWidth = strlen($left) + strlen($dec) + strlen($right); - if ($useThousands) { - $value = number_format( - $value, - strlen($right), - StringHelper::getDecimalSeparator(), - StringHelper::getThousandsSeparator() - ); - $value = preg_replace($number_regex, $value, $format); - } else { - if (preg_match('/[0#]E[+-]0/i', $format)) { - // Scientific format - $value = sprintf('%5.2E', $value); - } elseif (preg_match('/0([^\d\.]+)0/', $format)) { - $value = self::complexNumberFormatMask($value, $format); - } else { - $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; - $value = sprintf($sprintf_pattern, $value); + // scale number + $value = $value / $scale; + + // Strip # + $format = preg_replace('/\\#/', '0', $format); + + // Remove locale code [$-###] + $format = preg_replace('/\[\$\-.*\]/', '', $format); + + $n = '/\\[[^\\]]+\\]/'; + $m = preg_replace($n, '', $format); + $number_regex = '/(0+)(\\.?)(0*)/'; + if (preg_match($number_regex, $m, $matches)) { + $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( + $value, + strlen($right), + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); $value = preg_replace($number_regex, $value, $format); + } else { + if (preg_match('/[0#]E[+-]0/i', $format)) { + // Scientific format + $value = sprintf('%5.2E', $value); + } elseif (preg_match('/0([^\d\.]+)0/', $format)) { + $value = self::complexNumberFormatMask($value, $format); + } else { + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; + $value = sprintf($sprintf_pattern, $value); + $value = preg_replace($number_regex, $value, $format); + } } } } - } - if (preg_match('/\[\$(.*)\]/u', $format, $m)) { - // Currency or Accounting - $currencyCode = $m[1]; - list($currencyCode) = explode('-', $currencyCode); - if ($currencyCode == '') { - $currencyCode = StringHelper::getCurrencyCode(); + if (preg_match('/\[\$(.*)\]/u', $format, $m)) { + // Currency or Accounting + $currencyCode = $m[1]; + list($currencyCode) = explode('-', $currencyCode); + if ($currencyCode == '') { + $currencyCode = StringHelper::getCurrencyCode(); + } + $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value); } - $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value); } } } diff --git a/tests/data/Style/NumberFormatDates.php b/tests/data/Style/NumberFormatDates.php index be8dc966..dea04de7 100644 --- a/tests/data/Style/NumberFormatDates.php +++ b/tests/data/Style/NumberFormatDates.php @@ -72,4 +72,27 @@ return [ 43332, '[$-1010409]m/d/yyyy', ], + [ + '27:15', + 1.1354166666667, + '[h]:mm', + ], + // Percentage + [ + '12%', + 0.12, + '0%', + ], + // Simple color + [ + '12345', + 12345, + '[Green]General', + ], + // Multiple colors + [ + '12345', + 12345, + '[Blue]0;[Red]0', + ], ];