From d7ef6810a4fb5c455215641709c601de732fd9f3 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 1 Aug 2015 00:39:10 +0100 Subject: [PATCH] Improved masking for number format handling, particularly for datetime masks --- src/PhpSpreadsheet/Style/NumberFormat.php | 57 +++++++++++++------ .../src/Style/NumberFormatDateTest.php | 35 ++++++++++++ .../rawTestData/Style/NumberFormatDates.data | 6 ++ 3 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 unitTests/Classes/src/Style/NumberFormatDateTest.php create mode 100644 unitTests/rawTestData/Style/NumberFormatDates.data diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 78cd07be..0ccf7f68 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -425,26 +425,43 @@ class NumberFormat extends Supervisor implements \PHPExcel\IComparable 'h' => 'g' ); + private static function setLowercaseCallback($matches) { + return mb_strtolower($matches[0]); + } + + private static function escapeQuotesCallback($matches) { + return '\\' . implode('\\', str_split($matches[1])); + } + private static function formatAsDate(&$value, &$format) { - // dvc: convert Excel formats to PHP date formats - // strip off first part containing e.g. [$-F800] or [$USD-409] // general syntax: [$-] // language info is in hexadecimal $format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format); - // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case - $format = strtolower($format); + // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case; + // but we don't want to change any quoted strings + $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format); - $format = strtr($format, self::$dateFormatReplacements); - if (!strpos($format, 'A')) { - // 24-hour time format - $format = strtr($format, self::$dateFormatReplacements24); - } else { - // 12-hour time format - $format = strtr($format, self::$dateFormatReplacements12); + // Only process the non-quoted blocks for date format characters + $blocks = explode('"', $format); + foreach($blocks as $key => &$block) { + if ($key % 2 == 0) { + $block = strtr($block, self::$dateFormatReplacements); + if (!strpos($block, 'A')) { + // 24-hour time format + $block = strtr($block, self::$dateFormatReplacements24); + } else { + // 12-hour time format + $block = strtr($block, self::$dateFormatReplacements12); + } + } } + $format = implode('"', $blocks); + + // escape any quoted characters so that DateTime format() will render them correctly + $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format); $dateObj = \PHPExcel\Shared\Date::ExcelToPHPObject($value); $value = $dateObj->format($format); @@ -553,10 +570,12 @@ class NumberFormat extends Supervisor implements \PHPExcel\IComparable return $value; } - // Get the sections, there can be up to four sections - $sections = explode(';', $format); + // Convert any escaped characters to quoted strings, e.g. (\T to "T") + $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format); + // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) + $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); - // Fetch the relevant section depending on whether number is positive, negative, or zero? + // Extract the relevant section depending on whether number is positive, negative, or zero? // Text not supported yet. // Here is how the sections apply to various values in Excel: // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] @@ -597,9 +616,13 @@ class NumberFormat extends Supervisor implements \PHPExcel\IComparable $format = preg_replace($color_regex, '', $format); // Let's begin inspecting the format and converting the value to a formatted string - if (preg_match('/^(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $format)) { // datetime format + + // 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 + } elseif (preg_match('/%$/', $format)) { + // % number format self::formatAsPercentage($value, $format); } else { if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) { @@ -687,7 +710,7 @@ class NumberFormat extends Supervisor implements \PHPExcel\IComparable } } if (preg_match('/\[\$(.*)\]/u', $format, $m)) { - // Currency or Accounting + // Currency or Accounting $currencyFormat = $m[0]; $currencyCode = $m[1]; list($currencyCode) = explode('-', $currencyCode); diff --git a/unitTests/Classes/src/Style/NumberFormatDateTest.php b/unitTests/Classes/src/Style/NumberFormatDateTest.php new file mode 100644 index 00000000..8ba828f7 --- /dev/null +++ b/unitTests/Classes/src/Style/NumberFormatDateTest.php @@ -0,0 +1,35 @@ +assertEquals($expectedResult, $result); + } + + public function providerNumberFormat() + { + return new testDataFileIterator('rawTestData/Style/NumberFormatDates.data'); + } +} diff --git a/unitTests/rawTestData/Style/NumberFormatDates.data b/unitTests/rawTestData/Style/NumberFormatDates.data new file mode 100644 index 00000000..7c7754cd --- /dev/null +++ b/unitTests/rawTestData/Style/NumberFormatDates.data @@ -0,0 +1,6 @@ +22269.0625, 'dd-mm-yyyy hh:mm:ss', "19-12-1960 01:30:00" +22269.0625, 'MM/DD/YYYY HH:MM:SS', "12/19/1960 01:30:00" // Oasis uses upper-case +22269.0625, 'yyyy-mm-dd\Thh:mm:ss', "1960-12-19T01:30:00" // Date with plaintext escaped with a \ +22269.0625, 'yyyy-mm-dd"T"hh:mm:ss \Z', "1960-12-19T01:30:00 Z" // Date with plaintext in quotes +22269.0625, '"y-m-d" yyyy-mm-dd "h:m:s" hh:mm:ss', "y-m-d 1960-12-19 h:m:s 01:30:00" // Date with quoted formatting characters +22269.0625, '"y-m-d "yyyy-mm-dd" h:m:s "hh:mm:ss', "y-m-d 1960-12-19 h:m:s 01:30:00" // Date with quoted formatting characters