From cad1730d38785cf36ae9a2185d977c30a112f142 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sun, 23 May 2021 15:05:49 -0700 Subject: [PATCH 1/5] Document Properties - Coverage and 32-bit-safe Timestamps While researching an issue, I noticed that coverage of Document/Properties was poor. Further, the use of int timestamps will eventually lead to problems for 32-bit PHP (see issue #1826). Coverage Changes: - Many property types with no special handling are enumerated but not tested. These are removed, but will continue to function as before. - Existing code theoretically allows property to be set to an object, but there is no means to read or write such a property, and, even if there were, I don't believe Excel supports it. Setting a property to an object will now be changed to a no-op (can throw an exception if preferred). - Since the Properties object now has no members which are themselves objects, there is no need for a deep clone. The untested __clone method is removed. - Large switch statements are replaced with associative arrays. Scrutinizer will like that. - Coverage is now 100%. Timestamp Changes: - Timestamps will be stored as int if possible, or float if not. This is, or will soon be, needed for 32-bit systems. Tests have been added for beyond-epoch dates, and run successfully with 32-bit. - LibreOffice doesn't quite get the Created/Modified properties correct. These are written to the file as a string which includes offset from UTC, but LibreOffice ignores the offset portion when displaying them. Code had been generating these in UTC, but now generates them in default timezone, which should meet user's expectations. Other Changes: - Custom properties added to ODS Writer. - Samples had not been generating any ODS files. One is now generated. - Ods uses a single 'keywords' property rather than multiple 'keyword' properties. - Breaking change - default company is changed to null string from Microsoft Corporation. - Breaking change of sorts - PropertiesTest incorrectly tested a custom date property against a string, Reader/XlsxTest correctly tested against a timestamp converted to a string. PropertiesTest was defective, and will no longer work as coded; anyone using it as a model will likewise have a problem. - PHP8.1 has been complaining for weeks about a time zone conversion test. I have now downloaded a version, and changed the code so that it will work in 8.1 as well as prior releases. (It is still likely that the existing code should work in 8.1, but I haven't yet figured out how to file a bug report.) In the course of testing, 3 additional 8.1 problems were reported (all along the lines of "can't pass null to strpos"), and are fixed with null coercion. - Two Calculation tests failed because of large results on 32-bit system. These are corrected by allowing the functions involved to return float|int rather than int. I suspect that there are other functions with this problem, and will investigate as a follow-up activity. - See issue #2090. I believe that changes between 17.1 and master will merely cause the problematic spreadsheet to fail in a different way. I believe that enclosing in quotes some variables passed to Document/Properties by Reader/Xlsx will eliminate the problem, but, in the absence of an example file, cannot say for sure. - Properties tests are now separated out from Reader/XlsxTest and Reader/OdsTest, and now test both Read and Write (via reload). Miscellaneous Notes: - There remains no support for Custom Properties in Xls Reader or Writer. - We now have default timezones for all of PHP itself, Shared/Date, and Shared/Timezone. That is least one too many. I was unable to disentangle the latter two for this change, but will look into deprecating one or the other in future. --- phpstan-baseline.neon | 145 ---------- samples/Basic/01_Simple.php | 2 +- .../Calculation/Calculation.php | 6 +- .../Calculation/MathTrig/Base.php | 4 +- .../Calculation/Statistical.php | 2 +- .../Calculation/Statistical/Permutations.php | 13 +- src/PhpSpreadsheet/Document/Properties.php | 264 ++++++++---------- src/PhpSpreadsheet/Reader/Ods.php | 7 +- src/PhpSpreadsheet/Reader/Ods/Properties.php | 7 +- src/PhpSpreadsheet/Reader/Xls.php | 22 +- src/PhpSpreadsheet/Reader/Xlsx/Properties.php | 6 +- src/PhpSpreadsheet/Shared/Date.php | 44 ++- src/PhpSpreadsheet/Shared/IntOrFloat.php | 21 ++ src/PhpSpreadsheet/Shared/OLE.php | 12 +- src/PhpSpreadsheet/Shared/OLE/PPS.php | 12 +- src/PhpSpreadsheet/Shared/TimeZone.php | 20 +- src/PhpSpreadsheet/Writer/Ods/Meta.php | 64 ++++- src/PhpSpreadsheet/Writer/Xls.php | 5 +- src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 21 +- .../Document/EpochTest.php | 94 +++++++ .../Document/PropertiesTest.php | 9 +- .../Reader/Ods/OdsPropertiesTest.php | 112 ++++++++ .../Reader/Ods/OdsTest.php | 42 --- .../Reader/Xlsx/PropertiesTest.php | 94 +++++++ tests/PhpSpreadsheetTests/Reader/XlsxTest.php | 41 --- tests/PhpSpreadsheetTests/Shared/DateTest.php | 2 +- .../Shared/TimeZoneTest.php | 38 ++- .../Writer/Xls/XlsGifBmpTest.php | 5 +- 28 files changed, 638 insertions(+), 476 deletions(-) create mode 100644 src/PhpSpreadsheet/Shared/IntOrFloat.php create mode 100644 tests/PhpSpreadsheetTests/Document/EpochTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/OdsPropertiesTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6390e0fc..ee3b762d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1270,11 +1270,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php - - - message: "#^Parameter \\#1 \\$number of function base_convert expects string, int\\<0, 9007199254740991\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig/Base.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Helpers\\:\\:validateNumericNullBool\\(\\) should return float\\|int but returns float\\|int\\|string\\.$#" count: 1 @@ -2490,36 +2485,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/DefinedName.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:\\$created \\(int\\) does not accept int\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:\\$modified \\(int\\) does not accept int\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:identifyPropertyType\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:identifyPropertyType\\(\\) has parameter \\$propertyValue with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:convertProperty\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:convertProperty\\(\\) has parameter \\$propertyValue with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Document/Properties.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\DocumentGenerator\\:\\:getPhpSpreadsheetFunctionText\\(\\) has parameter \\$functionCall with no typehint specified\\.$#" count: 1 @@ -3180,16 +3145,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Ods/Properties.php - - - message: "#^Parameter \\#1 \\$timestamp of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCreated\\(\\) expects int\\|string\\|null, int\\|false given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Ods/Properties.php - - - - message: "#^Parameter \\#1 \\$timestamp of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setModified\\(\\) expects int\\|string\\|null, int\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/Properties.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\Properties\\:\\:setMetaProperties\\(\\) has parameter \\$namespacesMeta with no typehint specified\\.$#" count: 1 @@ -3405,61 +3360,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$codePage of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\CodePage\\:\\:numberToName\\(\\) expects int, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$title of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setTitle\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$subject of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setSubject\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$creator of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCreator\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$keywords of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setKeywords\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$description of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setDescription\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$modifier of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setLastModifiedBy\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$codePage of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\CodePage\\:\\:numberToName\\(\\) expects int, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$category of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCategory\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$manager of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setManager\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$company of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCompany\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, int\\|string\\|null given\\.$#" count: 1 @@ -5250,16 +5150,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLE.php - - - message: "#^Parameter \\#7 \\$time_1st of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS constructor expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE.php - - - - message: "#^Parameter \\#8 \\$time_2nd of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS constructor expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE.php - - message: "#^Parameter \\#9 \\$data of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS constructor expects string, null given\\.$#" count: 1 @@ -5330,16 +5220,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLE/PPS/File.php - - - message: "#^Parameter \\#7 \\$time_1st of method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS\\:\\:__construct\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE/PPS/File.php - - - - message: "#^Parameter \\#8 \\$time_2nd of method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS\\:\\:__construct\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE/PPS/File.php - - message: "#^Parameter \\#1 \\$No of method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS\\:\\:__construct\\(\\) expects int, null given\\.$#" count: 1 @@ -5485,11 +5365,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/StringHelper.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/TimeZone.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Trend\\\\BestFit\\:\\:\\$goodnessOfFit has no typehint specified\\.$#" count: 1 @@ -8470,31 +8345,11 @@ parameters: count: 1 path: tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php - - - message: "#^Parameter \\#1 \\$timeZone of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:setDefaultTimezone\\(\\) expects DateTimeZone\\|string, DateTimeZone\\|null given\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Shared/DateTest.php - - message: "#^Parameter \\#1 \\$pValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:setCurrencyCode\\(\\) expects string, null given\\.$#" count: 1 path: tests/PhpSpreadsheetTests/Shared/StringHelperTest.php - - - message: "#^Parameter \\#1 \\$timeZone of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:setDefaultTimezone\\(\\) expects DateTimeZone\\|string, DateTimeZone\\|null given\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php - - - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|false\\.$#" - count: 2 - path: tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php - - - - message: "#^Parameter \\#1 \\$timezone of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:getTimeZoneAdjustment\\(\\) expects string, null given\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\SpreadsheetTest\\:\\:testGetSheetByName\\(\\) has parameter \\$index with no typehint specified\\.$#" count: 1 diff --git a/samples/Basic/01_Simple.php b/samples/Basic/01_Simple.php index 69309794..1ab182e1 100644 --- a/samples/Basic/01_Simple.php +++ b/samples/Basic/01_Simple.php @@ -61,4 +61,4 @@ $spreadsheet->getActiveSheet() ->setTitle('Simple'); // Save -$helper->write($spreadsheet, __FILE__); +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Ods']); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ec85df69..981e17e2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4562,7 +4562,7 @@ class Calculation } else { $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack); } - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) { $cellRef = null; if (isset($matches[8])) { if ($pCell === null) { @@ -4637,7 +4637,7 @@ class Calculation } // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { if ($pCellParent) { $pCell->attach($pCellParent); } @@ -4727,7 +4727,7 @@ class Calculation } } else { // if the token is a number, boolean, string or an Excel error, push it onto the stack - if (isset(self::$excelConstants[strtoupper($token)])) { + if (isset(self::$excelConstants[strtoupper($token ?? '')])) { $excelConstant = strtoupper($token); $stack->push('Constant Value', self::$excelConstants[$excelConstant]); if (isset($storeKey)) { diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Base.php b/src/PhpSpreadsheet/Calculation/MathTrig/Base.php index 6f44a122..c7d60731 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Base.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Base.php @@ -24,7 +24,7 @@ class Base public static function evaluate($number, $radix, $minLength = null) { try { - $number = (int) Helpers::validateNumericNullBool($number); + $number = (float) floor(Helpers::validateNumericNullBool($number)); $radix = (int) Helpers::validateNumericNullBool($radix); } catch (Exception $e) { return $e->getMessage(); @@ -36,7 +36,7 @@ class Base return Functions::NAN(); // Numeric range constraints } - $outcome = strtoupper((string) base_convert($number, 10, $radix)); + $outcome = strtoupper((string) base_convert("$number", 10, $radix)); if ($minLength !== null) { $outcome = str_pad($outcome, (int) $minLength, '0', STR_PAD_LEFT); // String padding } diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 0f5355b9..651ee805 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -1402,7 +1402,7 @@ class Statistical * @param int $numObjs Number of different objects * @param int $numInSet Number of objects in each permutation * - * @return int|string Number of permutations, or a string containing an error + * @return float|int|string Number of permutations, or a string containing an error */ public static function PERMUT($numObjs, $numInSet) { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index ab2e14dd..83c0458d 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat; class Permutations { @@ -20,7 +21,7 @@ class Permutations * @param mixed $numObjs Integer number of different objects * @param mixed $numInSet Integer number of objects in each permutation * - * @return int|string Number of permutations, or a string containing an error + * @return float|int|string Number of permutations, or a string containing an error */ public static function PERMUT($numObjs, $numInSet) { @@ -38,7 +39,9 @@ class Permutations return Functions::NAN(); } - return (int) round(MathTrig\Fact::evaluate($numObjs) / MathTrig\Fact::evaluate($numObjs - $numInSet)); + $result = round(MathTrig\Fact::evaluate($numObjs) / MathTrig\Fact::evaluate($numObjs - $numInSet)); + + return IntOrFloat::evaluate($result); } /** @@ -50,7 +53,7 @@ class Permutations * @param mixed $numObjs Integer number of different objects * @param mixed $numInSet Integer number of objects in each permutation * - * @return int|string Number of permutations, or a string containing an error + * @return float|int|string Number of permutations, or a string containing an error */ public static function PERMUTATIONA($numObjs, $numInSet) { @@ -68,6 +71,8 @@ class Permutations return Functions::NAN(); } - return (int) ($numObjs ** $numInSet); + $result = $numObjs ** $numInSet; + + return IntOrFloat::evaluate($result); } } diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index c099bccc..1030d98c 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -2,6 +2,9 @@ namespace PhpOffice\PhpSpreadsheet\Document; +use DateTime; +use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat; + class Properties { /** constants */ @@ -37,14 +40,14 @@ class Properties /** * Created. * - * @var int + * @var float|int */ private $created; /** * Modified. * - * @var int + * @var float|int */ private $modified; @@ -95,7 +98,7 @@ class Properties * * @var string */ - private $company = 'Microsoft Corporation'; + private $company = ''; /** * Custom Properties. @@ -111,8 +114,8 @@ class Properties { // Initialise values $this->lastModifiedBy = $this->creator; - $this->created = time(); - $this->modified = time(); + $this->created = self::intOrFloatTimestamp(null); + $this->modified = self::intOrFloatTimestamp(null); } /** @@ -156,9 +159,32 @@ class Properties } /** - * Get Created. + * @param null|float|int|string $timestamp + * + * @return float|int */ - public function getCreated(): int + private static function intOrFloatTimestamp($timestamp) + { + if ($timestamp === null) { + $timestamp = (float) (new DateTime())->format('U'); + } elseif (is_string($timestamp)) { + if (is_numeric($timestamp)) { + $timestamp = (float) $timestamp; + } else { + $timestamp = preg_replace('/[.][0-9]*$/', '', $timestamp) ?? ''; + $timestamp = (float) (new DateTime($timestamp))->format('U'); + } + } + + return IntOrFloat::evaluate($timestamp); + } + + /** + * Get Created. + * + * @return float|int + */ + public function getCreated() { return $this->created; } @@ -166,31 +192,23 @@ class Properties /** * Set Created. * - * @param null|int|string $timestamp + * @param null|float|int|string $timestamp * * @return $this */ public function setCreated($timestamp): self { - if ($timestamp === null) { - $timestamp = time(); - } elseif (is_string($timestamp)) { - if (is_numeric($timestamp)) { - $timestamp = (int) $timestamp; - } else { - $timestamp = strtotime($timestamp); - } - } - - $this->created = $timestamp; + $this->created = self::intOrFloatTimestamp($timestamp); return $this; } /** * Get Modified. + * + * @return float|int */ - public function getModified(): int + public function getModified() { return $this->modified; } @@ -198,23 +216,13 @@ class Properties /** * Set Modified. * - * @param null|int|string $timestamp + * @param null|float|int|string $timestamp * * @return $this */ public function setModified($timestamp): self { - if ($timestamp === null) { - $timestamp = time(); - } elseif (is_string($timestamp)) { - if (is_numeric($timestamp)) { - $timestamp = (int) $timestamp; - } else { - $timestamp = strtotime($timestamp); - } - } - - $this->modified = $timestamp; + $this->modified = self::intOrFloatTimestamp($timestamp); return $this; } @@ -387,6 +395,8 @@ class Properties if (isset($this->customProperties[$propertyName])) { return $this->customProperties[$propertyName]['value']; } + + return null; } /** @@ -399,15 +409,18 @@ class Properties return $this->customProperties[$propertyName]['type'] ?? null; } - private function identifyPropertyType($propertyValue) + /** + * @param mixed $propertyValue + */ + private function identifyPropertyType($propertyValue): string { - if ($propertyValue === null) { - return self::PROPERTY_TYPE_STRING; - } elseif (is_float($propertyValue)) { + if (is_float($propertyValue)) { return self::PROPERTY_TYPE_FLOAT; - } elseif (is_int($propertyValue)) { + } + if (is_int($propertyValue)) { return self::PROPERTY_TYPE_INTEGER; - } elseif (is_bool($propertyValue)) { + } + if (is_bool($propertyValue)) { return self::PROPERTY_TYPE_BOOLEAN; } @@ -433,125 +446,90 @@ class Properties $propertyType = $this->identifyPropertyType($propertyValue); } - $this->customProperties[$propertyName] = [ - 'value' => $propertyValue, - 'type' => $propertyType, - ]; + if (!is_object($propertyValue)) { + $this->customProperties[$propertyName] = [ + 'value' => self::convertProperty($propertyValue, $propertyType), + 'type' => $propertyType, + ]; + } return $this; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ - public function __clone() - { - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if (is_object($value)) { - $this->$key = clone $value; - } else { - $this->$key = $value; - } - } - } + private const PROPERTY_TYPE_ARRAY = [ + 'i' => self::PROPERTY_TYPE_INTEGER, // Integer + 'i1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Signed Integer + 'i2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Signed Integer + 'i4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Signed Integer + 'i8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Signed Integer + 'int' => self::PROPERTY_TYPE_INTEGER, // Integer + 'ui1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Unsigned Integer + 'ui2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Unsigned Integer + 'ui4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Unsigned Integer + 'ui8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Unsigned Integer + 'uint' => self::PROPERTY_TYPE_INTEGER, // Unsigned Integer + 'f' => self::PROPERTY_TYPE_FLOAT, // Real Number + 'r4' => self::PROPERTY_TYPE_FLOAT, // 4-Byte Real Number + 'r8' => self::PROPERTY_TYPE_FLOAT, // 8-Byte Real Number + 'decimal' => self::PROPERTY_TYPE_FLOAT, // Decimal + 's' => self::PROPERTY_TYPE_STRING, // String + 'empty' => self::PROPERTY_TYPE_STRING, // Empty + 'null' => self::PROPERTY_TYPE_STRING, // Null + 'lpstr' => self::PROPERTY_TYPE_STRING, // LPSTR + 'lpwstr' => self::PROPERTY_TYPE_STRING, // LPWSTR + 'bstr' => self::PROPERTY_TYPE_STRING, // Basic String + 'd' => self::PROPERTY_TYPE_DATE, // Date and Time + 'date' => self::PROPERTY_TYPE_DATE, // Date and Time + 'filetime' => self::PROPERTY_TYPE_DATE, // File Time + 'b' => self::PROPERTY_TYPE_BOOLEAN, // Boolean + 'bool' => self::PROPERTY_TYPE_BOOLEAN, // Boolean + ]; + private const SPECIAL_TYPES = [ + 'empty' => '', + 'null' => null, + ]; + + /** + * Convert property to form desired by Excel. + * + * @param mixed $propertyValue + * + * @return mixed + */ public static function convertProperty($propertyValue, string $propertyType) { + return self::SPECIAL_TYPES[$propertyType] ?? self::convertProperty2($propertyValue, $propertyType); + } + + /** + * Convert property to form desired by Excel. + * + * @param mixed $propertyValue + * + * @return mixed + */ + private static function convertProperty2($propertyValue, string $type) + { + $propertyType = self::convertPropertyType($type); switch ($propertyType) { - case 'empty': // Empty - return ''; - case 'null': // Null - return null; - case 'i1': // 1-Byte Signed Integer - case 'i2': // 2-Byte Signed Integer - case 'i4': // 4-Byte Signed Integer - case 'i8': // 8-Byte Signed Integer - case 'int': // Integer - return (int) $propertyValue; - case 'ui1': // 1-Byte Unsigned Integer - case 'ui2': // 2-Byte Unsigned Integer - case 'ui4': // 4-Byte Unsigned Integer - case 'ui8': // 8-Byte Unsigned Integer - case 'uint': // Unsigned Integer - return abs((int) $propertyValue); - case 'r4': // 4-Byte Real Number - case 'r8': // 8-Byte Real Number - case 'decimal': // Decimal + case self::PROPERTY_TYPE_INTEGER: + $intValue = (int) $propertyValue; + + return ($type[0] === 'u') ? abs($intValue) : $intValue; + case self::PROPERTY_TYPE_FLOAT: return (float) $propertyValue; - case 'lpstr': // LPSTR - case 'lpwstr': // LPWSTR - case 'bstr': // Basic String - return $propertyValue; - case 'date': // Date and Time - case 'filetime': // File Time - return strtotime($propertyValue); - case 'bool': // Boolean - return $propertyValue == 'true'; - case 'cy': // Currency - case 'error': // Error Status Code - case 'vector': // Vector - case 'array': // Array - case 'blob': // Binary Blob - case 'oblob': // Binary Blob Object - case 'stream': // Binary Stream - case 'ostream': // Binary Stream Object - case 'storage': // Binary Storage - case 'ostorage': // Binary Storage Object - case 'vstream': // Binary Versioned Stream - case 'clsid': // Class ID - case 'cf': // Clipboard Data + case self::PROPERTY_TYPE_DATE: + return self::intOrFloatTimestamp($propertyValue); + case self::PROPERTY_TYPE_BOOLEAN: + return is_bool($propertyValue) ? $propertyValue : ($propertyValue === 'true'); + default: // includes string return $propertyValue; } - - return $propertyValue; } public static function convertPropertyType(string $propertyType): string { - switch ($propertyType) { - case 'i1': // 1-Byte Signed Integer - case 'i2': // 2-Byte Signed Integer - case 'i4': // 4-Byte Signed Integer - case 'i8': // 8-Byte Signed Integer - case 'int': // Integer - case 'ui1': // 1-Byte Unsigned Integer - case 'ui2': // 2-Byte Unsigned Integer - case 'ui4': // 4-Byte Unsigned Integer - case 'ui8': // 8-Byte Unsigned Integer - case 'uint': // Unsigned Integer - return self::PROPERTY_TYPE_INTEGER; - case 'r4': // 4-Byte Real Number - case 'r8': // 8-Byte Real Number - case 'decimal': // Decimal - return self::PROPERTY_TYPE_FLOAT; - case 'empty': // Empty - case 'null': // Null - case 'lpstr': // LPSTR - case 'lpwstr': // LPWSTR - case 'bstr': // Basic String - return self::PROPERTY_TYPE_STRING; - case 'date': // Date and Time - case 'filetime': // File Time - return self::PROPERTY_TYPE_DATE; - case 'bool': // Boolean - return self::PROPERTY_TYPE_BOOLEAN; - case 'cy': // Currency - case 'error': // Error Status Code - case 'vector': // Vector - case 'array': // Array - case 'blob': // Binary Blob - case 'oblob': // Binary Blob Object - case 'stream': // Binary Stream - case 'ostream': // Binary Stream Object - case 'storage': // Binary Storage - case 'ostorage': // Binary Storage Object - case 'vstream': // Binary Versioned Stream - case 'clsid': // Class ID - case 'cf': // Clipboard Data - return self::PROPERTY_TYPE_UNKNOWN; - } - - return self::PROPERTY_TYPE_UNKNOWN; + return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN; } } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 1a4d7ca3..76e4b8e5 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Reader; use DateTime; -use DateTimeZone; use DOMAttr; use DOMDocument; use DOMElement; @@ -256,9 +255,6 @@ class Ods extends BaseReader { File::assertFile($pFilename); - $timezoneObj = new DateTimeZone('Europe/London'); - $GMT = new DateTimeZone('UTC'); - $zip = new ZipArchive(); if ($zip->open($pFilename) !== true) { throw new Exception("Could not open {$pFilename} for reading! Error opening file."); @@ -500,8 +496,7 @@ class Ods extends BaseReader $type = DataType::TYPE_NUMERIC; $value = $cellData->getAttributeNS($officeNs, 'date-value'); - $dateObj = new DateTime($value, $GMT); - $dateObj->setTimeZone($timezoneObj); + $dateObj = new DateTime($value); [$year, $month, $day, $hour, $minute, $second] = explode( ' ', $dateObj->format('Y m d H i s') diff --git a/src/PhpSpreadsheet/Reader/Ods/Properties.php b/src/PhpSpreadsheet/Reader/Ods/Properties.php index d0a45e6a..b94d1ef6 100644 --- a/src/PhpSpreadsheet/Reader/Ods/Properties.php +++ b/src/PhpSpreadsheet/Reader/Ods/Properties.php @@ -55,9 +55,7 @@ class Properties break; case 'date': - $creationDate = strtotime($propertyValue); - $docProps->setCreated($creationDate); - $docProps->setModified($creationDate); + $docProps->setModified($propertyValue); break; case 'description': @@ -86,8 +84,7 @@ class Properties break; case 'creation-date': - $creationDate = strtotime($propertyValue); - $docProps->setCreated($creationDate); + $docProps->setCreated($propertyValue); break; case 'user-defined': diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index c389105b..b1c6b13f 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -1454,34 +1454,34 @@ class Xls extends BaseReader switch ($id) { case 0x01: // Code Page - $codePage = CodePage::numberToName($value); + $codePage = CodePage::numberToName((int) $value); break; case 0x02: // Title - $this->spreadsheet->getProperties()->setTitle($value); + $this->spreadsheet->getProperties()->setTitle("$value"); break; case 0x03: // Subject - $this->spreadsheet->getProperties()->setSubject($value); + $this->spreadsheet->getProperties()->setSubject("$value"); break; case 0x04: // Author (Creator) - $this->spreadsheet->getProperties()->setCreator($value); + $this->spreadsheet->getProperties()->setCreator("$value"); break; case 0x05: // Keywords - $this->spreadsheet->getProperties()->setKeywords($value); + $this->spreadsheet->getProperties()->setKeywords("$value"); break; case 0x06: // Comments (Description) - $this->spreadsheet->getProperties()->setDescription($value); + $this->spreadsheet->getProperties()->setDescription("$value"); break; case 0x07: // Template // Not supported by PhpSpreadsheet break; case 0x08: // Last Saved By (LastModifiedBy) - $this->spreadsheet->getProperties()->setLastModifiedBy($value); + $this->spreadsheet->getProperties()->setLastModifiedBy("$value"); break; case 0x09: // Revision @@ -1606,11 +1606,11 @@ class Xls extends BaseReader switch ($id) { case 0x01: // Code Page - $codePage = CodePage::numberToName($value); + $codePage = CodePage::numberToName((int) $value); break; case 0x02: // Category - $this->spreadsheet->getProperties()->setCategory($value); + $this->spreadsheet->getProperties()->setCategory("$value"); break; case 0x03: // Presentation Target @@ -1647,11 +1647,11 @@ class Xls extends BaseReader // Not supported by PhpSpreadsheet break; case 0x0E: // Manager - $this->spreadsheet->getProperties()->setManager($value); + $this->spreadsheet->getProperties()->setManager("$value"); break; case 0x0F: // Company - $this->spreadsheet->getProperties()->setCompany($value); + $this->spreadsheet->getProperties()->setCompany("$value"); break; case 0x10: // Links up-to-date diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index b6f3c61f..ffc7ec45 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -39,8 +39,10 @@ class Properties $this->docProps->setCreator((string) self::getArrayItem($xmlCore->xpath('dc:creator'))); $this->docProps->setLastModifiedBy((string) self::getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); - $this->docProps->setCreated(strtotime(self::getArrayItem($xmlCore->xpath('dcterms:created')))); //! respect xsi:type - $this->docProps->setModified(strtotime(self::getArrayItem($xmlCore->xpath('dcterms:modified')))); //! respect xsi:type + $created = (string) self::getArrayItem($xmlCore->xpath('dcterms:created')); //! respect xsi:type + $this->docProps->setCreated($created); + $modified = (string) self::getArrayItem($xmlCore->xpath('dcterms:modified')); //! respect xsi:type + $this->docProps->setModified($modified); //! respect xsi:type $this->docProps->setTitle((string) self::getArrayItem($xmlCore->xpath('dc:title'))); $this->docProps->setDescription((string) self::getArrayItem($xmlCore->xpath('dc:description'))); $this->docProps->setSubject((string) self::getArrayItem($xmlCore->xpath('dc:subject'))); diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 898dd523..32e290df 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -97,7 +97,7 @@ class Date /** * Set the Default timezone to use for dates. * - * @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions + * @param null|DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions * * @return bool Success or failure */ @@ -115,29 +115,39 @@ class Date } /** - * Return the Default timezone being used for dates. - * - * @return DateTimeZone The timezone being used as default for Excel timestamp to PHP DateTime object + * Return the Default timezone, or UTC if default not set. */ - public static function getDefaultTimezone() + public static function getDefaultTimezone(): DateTimeZone { - if (self::$defaultTimeZone === null) { - self::$defaultTimeZone = new DateTimeZone('UTC'); - } + return self::$defaultTimeZone ?? new DateTimeZone('UTC'); + } + /** + * Return the Default timezone, or local timezone if default is not set. + */ + public static function getDefaultOrLocalTimezone(): DateTimeZone + { + return self::$defaultTimeZone ?? new DateTimeZone(date_default_timezone_get()); + } + + /** + * Return the Default timezone even if null. + */ + public static function getDefaultTimezoneOrNull(): ?DateTimeZone + { return self::$defaultTimeZone; } /** * Validate a timezone. * - * @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object + * @param null|DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object * - * @return DateTimeZone The timezone as a timezone object + * @return ?DateTimeZone The timezone as a timezone object */ private static function validateTimeZone($timeZone) { - if ($timeZone instanceof DateTimeZone) { + if ($timeZone instanceof DateTimeZone || $timeZone === null) { return $timeZone; } if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) { @@ -490,4 +500,16 @@ class Date return $day; } + + public static function dateTimeFromTimestamp(string $date): DateTime + { + return DateTime::createFromFormat('U', $date) ?: new DateTime(); + } + + public static function formattedDateTimeFromTimestamp(string $date, string $format): string + { + $dtobj = self::dateTimeFromTimestamp($date); + + return $dtobj->format($format); + } } diff --git a/src/PhpSpreadsheet/Shared/IntOrFloat.php b/src/PhpSpreadsheet/Shared/IntOrFloat.php new file mode 100644 index 00000000..060f09c8 --- /dev/null +++ b/src/PhpSpreadsheet/Shared/IntOrFloat.php @@ -0,0 +1,21 @@ +format('U'); // multiply just to make MS happy $big_date *= 10000000; @@ -537,7 +538,7 @@ class OLE * * @param string $oleTimestamp A binary string with the encoded date * - * @return int The Unix timestamp corresponding to the string + * @return float|int The Unix timestamp corresponding to the string */ public static function OLE2LocalDate($oleTimestamp) { @@ -560,9 +561,6 @@ class OLE // translate to seconds since 1970: $unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5); - $iTimestamp = (int) $unixTimestamp; - - // Overflow conditions can't happen on 64-bit system - return ($iTimestamp == $unixTimestamp) ? $iTimestamp : ($unixTimestamp >= 0.0 ? PHP_INT_MAX : PHP_INT_MIN); + return IntOrFloat::evaluate($unixTimestamp); } } diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS.php b/src/PhpSpreadsheet/Shared/OLE/PPS.php index 104b0d6a..8b9e92a8 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS.php @@ -74,14 +74,14 @@ class PPS /** * A timestamp. * - * @var int + * @var float|int */ public $Time1st; /** * A timestamp. * - * @var int + * @var float|int */ public $Time2nd; @@ -129,8 +129,8 @@ class PPS * @param int $prev The index of the previous PPS * @param int $next The index of the next PPS * @param int $dir The index of it's first child if this is a Dir or Root PPS - * @param int $time_1st A timestamp - * @param int $time_2nd A timestamp + * @param null|float|int $time_1st A timestamp + * @param null|float|int $time_2nd A timestamp * @param string $data The (usually binary) source data of the PPS * @param array $children Array containing children PPS for this PPS */ @@ -142,8 +142,8 @@ class PPS $this->PrevPps = $prev; $this->NextPps = $next; $this->DirPps = $dir; - $this->Time1st = $time_1st; - $this->Time2nd = $time_2nd; + $this->Time1st = $time_1st ?? 0; + $this->Time2nd = $time_2nd ?? 0; $this->_data = $data; $this->children = $children; if ($data != '') { diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index 43fd3653..a6f98507 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -58,24 +58,20 @@ class TimeZone * Return the Timezone offset used for date/time conversions to/from UST * This requires both the timezone and the calculated date/time to allow for local DST. * - * @param string $timezone The timezone for finding the adjustment to UST - * @param int $timestamp PHP date/time value + * @param ?string $timezone The timezone for finding the adjustment to UST + * @param float|int $timestamp PHP date/time value * * @return int Number of seconds for timezone adjustment */ public static function getTimeZoneAdjustment($timezone, $timestamp) { - if ($timezone !== null) { - if (!self::validateTimezone($timezone)) { - throw new PhpSpreadsheetException('Invalid timezone ' . $timezone); - } - } else { - $timezone = self::$timezone; + $timezone = $timezone ?? self::$timezone; + $dtobj = Date::dateTimeFromTimestamp("$timestamp"); + if (!self::validateTimezone($timezone)) { + throw new PhpSpreadsheetException("Invalid timezone $timezone"); } + $dtobj->setTimeZone(new DateTimeZone($timezone)); - $objTimezone = new DateTimeZone($timezone); - $transitions = $objTimezone->getTransitions($timestamp, $timestamp); - - return (count($transitions) > 0) ? $transitions[0]['offset'] : 0; + return $dtobj->getOffset(); } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Meta.php b/src/PhpSpreadsheet/Writer/Ods/Meta.php index cd3054c0..9504ed7c 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Meta.php +++ b/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -2,7 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Document\Properties; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; +use PhpOffice\PhpSpreadsheet\Spreadsheet; class Meta extends WriterPart { @@ -40,15 +43,24 @@ class Meta extends WriterPart $objWriter->writeElement('meta:initial-creator', $spreadsheet->getProperties()->getCreator()); $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator()); - $objWriter->writeElement('meta:creation-date', date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); - $objWriter->writeElement('dc:date', date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); + $created = $spreadsheet->getProperties()->getCreated(); + $date = Date::dateTimeFromTimestamp("$created"); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $objWriter->writeElement('meta:creation-date', $date->format(DATE_W3C)); + $created = $spreadsheet->getProperties()->getModified(); + $date = Date::dateTimeFromTimestamp("$created"); + $date->setTimeZone(Date::getDefaultOrLocalTimeZone()); + $objWriter->writeElement('dc:date', $date->format(DATE_W3C)); $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle()); $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription()); $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject()); - $keywords = explode(' ', $spreadsheet->getProperties()->getKeywords()); - foreach ($keywords as $keyword) { - $objWriter->writeElement('meta:keyword', $keyword); - } + $objWriter->writeElement('meta:keyword', $spreadsheet->getProperties()->getKeywords()); + // Don't know if this changed over time, but the keywords are all + // in a single declaration now. + //$keywords = explode(' ', $spreadsheet->getProperties()->getKeywords()); + //foreach ($keywords as $keyword) { + // $objWriter->writeElement('meta:keyword', $keyword); + //} // $objWriter->startElement('meta:user-defined'); @@ -61,10 +73,50 @@ class Meta extends WriterPart $objWriter->writeRaw($spreadsheet->getProperties()->getCategory()); $objWriter->endElement(); + self::writeDocPropsCustom($objWriter, $spreadsheet); + $objWriter->endElement(); $objWriter->endElement(); return $objWriter->getData(); } + + private function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + { + $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); + foreach ($customPropertyList as $key => $customProperty) { + $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty); + $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty); + + $objWriter->startElement('meta:user-defined'); + $objWriter->writeAttribute('meta:name', $customProperty); + + switch ($propertyType) { + case Properties::PROPERTY_TYPE_INTEGER: + case Properties::PROPERTY_TYPE_FLOAT: + $objWriter->writeAttribute('meta:value-type', 'float'); + $objWriter->writeRawData($propertyValue); + + break; + case Properties::PROPERTY_TYPE_BOOLEAN: + $objWriter->writeAttribute('meta:value-type', 'boolean'); + $objWriter->writeRawData($propertyValue ? 'true' : 'false'); + + break; + case Properties::PROPERTY_TYPE_DATE: + $objWriter->writeAttribute('meta:value-type', 'date'); + $dtobj = Date::dateTimeFromTimestamp($propertyValue); + $objWriter->writeRawData($dtobj->format(DATE_W3C)); + + break; + default: + $objWriter->writeRawData($propertyValue); + + break; + } + + $objWriter->endElement(); + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index a1b477bf..fea216cc 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -765,7 +765,10 @@ class Xls extends BaseWriter return $data; } - private function writeSummaryPropOle(int $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void + /** + * @param float|int $dataProp + */ + private function writeSummaryPropOle($dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void { if ($dataProp) { $dataSection[] = [ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 6332a34d..43ce442f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Document\Properties; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -137,13 +139,17 @@ class DocProps extends WriterPart // dcterms:created $objWriter->startElement('dcterms:created'); $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); - $objWriter->writeRawData(date(DATE_W3C, $spreadsheet->getProperties()->getCreated())); + $created = $spreadsheet->getProperties()->getCreated(); + $date = Date::dateTimeFromTimestamp("$created"); + $objWriter->writeRawData($date->format(DATE_W3C)); $objWriter->endElement(); // dcterms:modified $objWriter->startElement('dcterms:modified'); $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF'); - $objWriter->writeRawData(date(DATE_W3C, $spreadsheet->getProperties()->getModified())); + $created = $spreadsheet->getProperties()->getModified(); + $date = Date::dateTimeFromTimestamp("$created"); + $objWriter->writeRawData($date->format(DATE_W3C)); $objWriter->endElement(); // dc:title @@ -205,21 +211,22 @@ class DocProps extends WriterPart $objWriter->writeAttribute('name', $customProperty); switch ($propertyType) { - case 'i': + case Properties::PROPERTY_TYPE_INTEGER: $objWriter->writeElement('vt:i4', $propertyValue); break; - case 'f': + case Properties::PROPERTY_TYPE_FLOAT: $objWriter->writeElement('vt:r8', $propertyValue); break; - case 'b': + case Properties::PROPERTY_TYPE_BOOLEAN: $objWriter->writeElement('vt:bool', ($propertyValue) ? 'true' : 'false'); break; - case 'd': + case Properties::PROPERTY_TYPE_DATE: $objWriter->startElement('vt:filetime'); - $objWriter->writeRawData(date(DATE_W3C, $propertyValue)); + $date = Date::dateTimeFromTimestamp("$propertyValue"); + $objWriter->writeRawData($date->format(DATE_W3C)); $objWriter->endElement(); break; diff --git a/tests/PhpSpreadsheetTests/Document/EpochTest.php b/tests/PhpSpreadsheetTests/Document/EpochTest.php new file mode 100644 index 00000000..63c51a59 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Document/EpochTest.php @@ -0,0 +1,94 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $spreadsheet->getProperties()->setCreated($timestamp); + $timestamp2 = preg_replace('/1-/', '2-', $timestamp); + self::AssertNotEquals($timestamp, $timestamp2); + $spreadsheet->getProperties()->setModified($timestamp2); + $timestamp3 = preg_replace('/1-/', '3-', $timestamp); + self::AssertNotEquals($timestamp, $timestamp3); + self::AssertNotEquals($timestamp2, $timestamp3); + $spreadsheet->getProperties()->setCustomProperty('cprop', $timestamp3, 'd'); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $created = $reloadedSpreadsheet->getProperties()->getCreated(); + $dt1 = DateTime::createFromFormat('U', "$created"); + $modified = $reloadedSpreadsheet->getProperties()->getModified(); + $dt2 = DateTime::createFromFormat('U', "$modified"); + if ($dt1 === false || $dt2 === false) { + self::fail('Invalid timestamp for created or modified'); + } else { + self::assertSame($timestamp, $dt1->format('Y-m-d H:i:s') . 'Z'); + self::assertSame($timestamp2, $dt2->format('Y-m-d H:i:s') . 'Z'); + } + if ($format === 'Xlsx' || $format === 'Ods') { + // No custom property support in Xls + $cprop = $reloadedSpreadsheet->getProperties()->getCustomPropertyValue('cprop'); + if (!is_numeric($cprop)) { + self::fail('Cannot find custom property'); + } else { + $dt3 = DateTime::createFromFormat('U', "$cprop"); + if ($dt3 === false) { + self::fail('Invalid timestamp for custom property'); + } else { + self::assertSame($timestamp3, $dt3->format('Y-m-d H:i:s') . 'Z'); + } + } + } + } + + public function providerFormats2(): array + { + return [ + ['Ods'], + ['Xls'], + ['Xlsx'], + ]; + } + + /** + * @dataProvider providerFormats2 + */ + public function testConsistentTimeStamp(string $format): void + { + $pgmstart = (float) (new DateTime())->format('U'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $pgmend = (float) (new DateTime())->format('U'); + self::assertLessThanOrEqual($pgmend, $pgmstart); + $created = $reloadedSpreadsheet->getProperties()->getCreated(); + $modified = $reloadedSpreadsheet->getProperties()->getModified(); + self::assertLessThanOrEqual($pgmend, $created); + self::assertLessThanOrEqual($pgmend, $modified); + self::assertLessThanOrEqual($created, $pgmstart); + self::assertLessThanOrEqual($modified, $pgmstart); + } +} diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index 54932bb8..4583e240 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Document; use PhpOffice\PhpSpreadsheet\Document\Properties; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; class PropertiesTest extends TestCase @@ -23,7 +24,7 @@ class PropertiesTest extends TestCase self::assertSame('Unknown Creator', $this->properties->getCreator()); self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy()); self::assertSame('Untitled Spreadsheet', $this->properties->getTitle()); - self::assertSame('Microsoft Corporation', $this->properties->getCompany()); + self::assertSame('', $this->properties->getCompany()); self::assertSame($createdTime, $this->properties->getCreated()); self::assertSame($modifiedTime, $this->properties->getModified()); } @@ -159,8 +160,12 @@ class PropertiesTest extends TestCase { $this->properties->setCustomProperty($propertyName, ...$args); self::assertTrue($this->properties->isCustomPropertySet($propertyName)); - self::assertSame($expectedValue, $this->properties->getCustomPropertyValue($propertyName)); self::assertSame($expectedType, $this->properties->getCustomPropertyType($propertyName)); + $result = $this->properties->getCustomPropertyValue($propertyName); + if ($expectedType === Properties::PROPERTY_TYPE_DATE) { + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + } + self::assertSame($expectedValue, $result); } public function providerCustomProperties(): array diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsPropertiesTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsPropertiesTest.php new file mode 100644 index 00000000..577b4221 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsPropertiesTest.php @@ -0,0 +1,112 @@ +timeZone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + } + + protected function tearDown(): void + { + date_default_timezone_set($this->timeZone); + } + + public function testLoadOdsWorkbookProperties(): void + { + $customPropertySet = [ + 'Owner' => ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice'], + 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], + 'Counter' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 10.0], + 'TestDate' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], + 'HereAndNow' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], + ]; + + $filename = 'tests/data/Reader/Ods/propertyTest.ods'; + $reader = new Ods(); + $spreadsheet = $reader->load($filename); + + $properties = $spreadsheet->getProperties(); + // Core Properties +// self::assertSame('Mark Baker', $properties->getCreator()); + self::assertSame('Property Test File', $properties->getTitle()); + self::assertSame('Testing for Properties', $properties->getSubject()); + self::assertSame('TEST ODS PHPSpreadsheet', $properties->getKeywords()); + + // Extended Properties +// self::assertSame('PHPOffice', $properties->getCompany()); +// self::assertSame('The Big Boss', $properties->getManager()); + + // Custom Properties + $customProperties = $properties->getCustomProperties(); + self::assertIsArray($customProperties); + $customProperties = array_flip($customProperties); + self::assertArrayHasKey('TestDate', $customProperties); + + foreach ($customPropertySet as $propertyName => $testData) { + self::assertTrue($properties->isCustomPropertySet($propertyName)); + self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); + $result = $properties->getCustomPropertyValue($propertyName); + if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + } + self::assertSame($testData['value'], $result); + } + } + + public function testReloadOdsWorkbookProperties(): void + { + $customPropertySet = [ + 'Owner' => ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice'], + 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], + 'Counter' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 10.0], + 'TestDate' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], + 'HereAndNow' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], + ]; + + $filename = 'tests/data/Reader/Ods/propertyTest.ods'; + $reader = new Ods(); + $spreadsheetOld = $reader->load($filename); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + + $properties = $spreadsheet->getProperties(); + // Core Properties +// self::assertSame('Mark Baker', $properties->getCreator()); + self::assertSame('Property Test File', $properties->getTitle()); + self::assertSame('Testing for Properties', $properties->getSubject()); + self::assertSame('TEST ODS PHPSpreadsheet', $properties->getKeywords()); + + // Extended Properties +// self::assertSame('PHPOffice', $properties->getCompany()); +// self::assertSame('The Big Boss', $properties->getManager()); + + // Custom Properties + $customProperties = $properties->getCustomProperties(); + self::assertIsArray($customProperties); + $customProperties = array_flip($customProperties); + self::assertArrayHasKey('TestDate', $customProperties); + + foreach ($customPropertySet as $propertyName => $testData) { + self::assertTrue($properties->isCustomPropertySet($propertyName)); + self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); + $result = $properties->getCustomPropertyValue($propertyName); + if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + } + self::assertSame($testData['value'], $result); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 10910207..a7f1466c 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -290,46 +290,4 @@ class OdsTest extends TestCase self::assertTrue($style->getFont()->getBold()); self::assertTrue($style->getFont()->getItalic()); } - - public function testLoadOdsWorkbookProperties(): void - { - $customPropertySet = [ - 'Owner' => ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice'], - 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], - 'Counter' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 10.0], - 'TestDate' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], - 'HereAndNow' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-30'], - ]; - - $filename = 'tests/data/Reader/Ods/propertyTest.ods'; - $reader = new Ods(); - $spreadsheet = $reader->load($filename); - - $properties = $spreadsheet->getProperties(); - // Core Properties -// self::assertSame('Mark Baker', $properties->getCreator()); - self::assertSame('Property Test File', $properties->getTitle()); - self::assertSame('Testing for Properties', $properties->getSubject()); - self::assertSame('TEST ODS PHPSpreadsheet', $properties->getKeywords()); - - // Extended Properties -// self::assertSame('PHPOffice', $properties->getCompany()); -// self::assertSame('The Big Boss', $properties->getManager()); - - // Custom Properties - $customProperties = $properties->getCustomProperties(); - self::assertIsArray($customProperties); - $customProperties = array_flip($customProperties); - self::assertArrayHasKey('TestDate', $customProperties); - - foreach ($customPropertySet as $propertyName => $testData) { - self::assertTrue($properties->isCustomPropertySet($propertyName)); - self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); - if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { - self::assertSame($testData['value'], date('Y-m-d', $properties->getCustomPropertyValue($propertyName))); - } else { - self::assertSame($testData['value'], $properties->getCustomPropertyValue($propertyName)); - } - } - } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php new file mode 100644 index 00000000..1e2bd841 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php @@ -0,0 +1,94 @@ + ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice Suite'], + 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], + 'Counter' => ['type' => Properties::PROPERTY_TYPE_INTEGER, 'value' => 15], + 'Rate' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 1.15], + 'Refactor Date' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-10'], + ]; + + $filename = 'tests/data/Reader/XLSX/propertyTest.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $properties = $spreadsheet->getProperties(); + // Core Properties + self::assertSame('Mark Baker', $properties->getCreator()); + self::assertSame('Unit Testing', $properties->getTitle()); + self::assertSame('Property Test', $properties->getSubject()); + + // Extended Properties + self::assertSame('PHPOffice', $properties->getCompany()); + self::assertSame('The Big Boss', $properties->getManager()); + + // Custom Properties + $customProperties = $properties->getCustomProperties(); + self::assertIsArray($customProperties); + $customProperties = array_flip($customProperties); + self::assertArrayHasKey('Publisher', $customProperties); + + foreach ($customPropertySet as $propertyName => $testData) { + self::assertTrue($properties->isCustomPropertySet($propertyName)); + self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); + $result = $properties->getCustomPropertyValue($propertyName); + if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + } + self::assertSame($testData['value'], $result); + } + } + + public function testReloadXlsxWorkbookProperties(): void + { + $customPropertySet = [ + 'Publisher' => ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice Suite'], + 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], + 'Counter' => ['type' => Properties::PROPERTY_TYPE_INTEGER, 'value' => 15], + 'Rate' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 1.15], + 'Refactor Date' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-10'], + ]; + + $filename = 'tests/data/Reader/XLSX/propertyTest.xlsx'; + $reader = new Xlsx(); + $spreadsheetOld = $reader->load($filename); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Xlsx'); + + $properties = $spreadsheet->getProperties(); + // Core Properties + self::assertSame('Mark Baker', $properties->getCreator()); + self::assertSame('Unit Testing', $properties->getTitle()); + self::assertSame('Property Test', $properties->getSubject()); + + // Extended Properties + self::assertSame('PHPOffice', $properties->getCompany()); + self::assertSame('The Big Boss', $properties->getManager()); + + // Custom Properties + $customProperties = $properties->getCustomProperties(); + self::assertIsArray($customProperties); + $customProperties = array_flip($customProperties); + self::assertArrayHasKey('Publisher', $customProperties); + + foreach ($customPropertySet as $propertyName => $testData) { + self::assertTrue($properties->isCustomPropertySet($propertyName)); + self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); + $result = $properties->getCustomPropertyValue($propertyName); + if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + } + self::assertSame($testData['value'], $result); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php index 738a25b9..b5b26712 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php @@ -14,47 +14,6 @@ use PHPUnit\Framework\TestCase; class XlsxTest extends TestCase { - public function testLoadXlsxWorkbookProperties(): void - { - $customPropertySet = [ - 'Publisher' => ['type' => Properties::PROPERTY_TYPE_STRING, 'value' => 'PHPOffice Suite'], - 'Tested' => ['type' => Properties::PROPERTY_TYPE_BOOLEAN, 'value' => true], - 'Counter' => ['type' => Properties::PROPERTY_TYPE_INTEGER, 'value' => 15], - 'Rate' => ['type' => Properties::PROPERTY_TYPE_FLOAT, 'value' => 1.15], - 'Refactor Date' => ['type' => Properties::PROPERTY_TYPE_DATE, 'value' => '2019-06-10'], - ]; - - $filename = 'tests/data/Reader/XLSX/propertyTest.xlsx'; - $reader = new Xlsx(); - $spreadsheet = $reader->load($filename); - - $properties = $spreadsheet->getProperties(); - // Core Properties - self::assertSame('Mark Baker', $properties->getCreator()); - self::assertSame('Unit Testing', $properties->getTitle()); - self::assertSame('Property Test', $properties->getSubject()); - - // Extended Properties - self::assertSame('PHPOffice', $properties->getCompany()); - self::assertSame('The Big Boss', $properties->getManager()); - - // Custom Properties - $customProperties = $properties->getCustomProperties(); - self::assertIsArray($customProperties); - $customProperties = array_flip($customProperties); - self::assertArrayHasKey('Publisher', $customProperties); - - foreach ($customPropertySet as $propertyName => $testData) { - self::assertTrue($properties->isCustomPropertySet($propertyName)); - self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); - if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { - self::assertSame($testData['value'], date('Y-m-d', $properties->getCustomPropertyValue($propertyName))); - } else { - self::assertSame($testData['value'], $properties->getCustomPropertyValue($propertyName)); - } - } - } - public function testListWorksheetInfo(): void { $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 1ca4c674..e4bf6838 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -21,7 +21,7 @@ class DateTest extends TestCase protected function setUp(): void { - $this->dttimezone = Date::getDefaultTimeZone(); + $this->dttimezone = Date::getDefaultTimeZoneOrNull(); $this->excelCalendar = Date::getExcelCalendar(); } diff --git a/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php b/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php index edad6e6b..0efdc05f 100644 --- a/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php +++ b/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php @@ -23,7 +23,7 @@ class TimeZoneTest extends TestCase protected function setUp(): void { $this->tztimezone = TimeZone::getTimeZone(); - $this->dttimezone = Date::getDefaultTimeZone(); + $this->dttimezone = Date::getDefaultTimeZoneOrNull(); } protected function tearDown(): void @@ -72,24 +72,32 @@ class TimeZoneTest extends TestCase { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $dtobj = DateTime::createFromFormat('Y-m-d H:i:s', '2008-09-22 00:00:00'); - $tstmp = $dtobj->getTimestamp(); - $unsupportedTimeZone = 'XEtc/GMT+10'; - TimeZone::getTimeZoneAdjustment($unsupportedTimeZone, $tstmp); + if ($dtobj === false) { + self::fail('DateTime createFromFormat failed'); + } else { + $tstmp = $dtobj->getTimestamp(); + $unsupportedTimeZone = 'XEtc/GMT+10'; + TimeZone::getTimeZoneAdjustment($unsupportedTimeZone, $tstmp); + } } public function testTimeZoneAdjustments(): void { $dtobj = DateTime::createFromFormat('Y-m-d H:i:s', '2008-01-01 00:00:00'); - $tstmp = $dtobj->getTimestamp(); - $supportedTimeZone = 'UTC'; - $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); - self::assertEquals(0, $adj); - $supportedTimeZone = 'America/Toronto'; - $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); - self::assertEquals(-18000, $adj); - $supportedTimeZone = 'America/Chicago'; - TimeZone::setTimeZone($supportedTimeZone); - $adj = TimeZone::getTimeZoneAdjustment(null, $tstmp); - self::assertEquals(-21600, $adj); + if ($dtobj === false) { + self::fail('DateTime createFromFormat failed'); + } else { + $tstmp = $dtobj->getTimestamp(); + $supportedTimeZone = 'UTC'; + $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); + self::assertEquals(0, $adj); + $supportedTimeZone = 'America/Toronto'; + $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); + self::assertEquals(-18000, $adj); + $supportedTimeZone = 'America/Chicago'; + TimeZone::setTimeZone($supportedTimeZone); + $adj = TimeZone::getTimeZoneAdjustment(null, $tstmp); + self::assertEquals(-21600, $adj); + } } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php index 03f201be..dea47522 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls; +use DateTime; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; @@ -24,7 +25,7 @@ class XlsGifBmpTest extends AbstractFunctional public function testBmp(): void { - $pgmstart = time(); + $pgmstart = (float) (new DateTime())->format('U'); $spreadsheet = new Spreadsheet(); $filstart = $spreadsheet->getProperties()->getModified(); self::assertLessThanOrEqual($filstart, $pgmstart); @@ -49,7 +50,7 @@ class XlsGifBmpTest extends AbstractFunctional $mimeType = ($drawing instanceof MemoryDrawing) ? $drawing->getMimeType() : 'notmemorydrawing'; self::assertEquals('image/png', $mimeType); } - $pgmend = time(); + $pgmend = (float) (new DateTime())->format('U'); self::assertLessThanOrEqual($pgmend, $pgmstart); self::assertLessThanOrEqual($pgmend, $filstart); From 98e30e3bf1a4538090e3f4e7d79bfbac2051bd01 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sun, 23 May 2021 15:50:05 -0700 Subject: [PATCH 2/5] Phpstan 6 baseline deletions, 2 docblock changes --- phpstan-baseline.neon | 40 ---------------------- src/PhpSpreadsheet/Shared/OLE/PPS/Root.php | 4 +-- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b706716d..1305815f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2965,36 +2965,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$description of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setDescription\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$modifier of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setLastModifiedBy\\(\\) expects string, int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$codePage of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\CodePage\\:\\:numberToName\\(\\) expects int, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$category of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCategory\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$manager of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setManager\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$company of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCompany\\(\\) expects string, bool\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, int\\|string\\|null given\\.$#" count: 1 @@ -4600,16 +4570,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLE.php - - - message: "#^Parameter \\#1 \\$time_1st of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS\\\\Root constructor expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE.php - - - - message: "#^Parameter \\#2 \\$time_2nd of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS\\\\Root constructor expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/OLE.php - - message: "#^Parameter \\#1 \\$No of class PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\OLE\\\\PPS constructor expects int, null given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php index 2fe41055..c9b99a85 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php @@ -46,8 +46,8 @@ class Root extends PPS private $bigBlockSize; /** - * @param int $time_1st A timestamp - * @param int $time_2nd A timestamp + * @param null|float|int $time_1st A timestamp + * @param null|float|int $time_2nd A timestamp * @param File[] $raChild */ public function __construct($time_1st, $time_2nd, $raChild) From 1bce0e193d8d702785e589c0598a11e78a618406 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sun, 23 May 2021 17:24:26 -0700 Subject: [PATCH 3/5] Scrutinizer's Turn 3 minor errors that hadn't blocked the request. --- samples/Reading_workbook_data/Properties.php | 10 +++++----- src/PhpSpreadsheet/Shared/Date.php | 11 +++++++---- src/PhpSpreadsheet/Writer/Ods/Meta.php | 4 ++-- tests/PhpSpreadsheetTests/Document/PropertiesTest.php | 3 ++- .../Reader/Xlsx/PropertiesTest.php | 5 +++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/samples/Reading_workbook_data/Properties.php b/samples/Reading_workbook_data/Properties.php index 5bf25b8e..8f7be375 100644 --- a/samples/Reading_workbook_data/Properties.php +++ b/samples/Reading_workbook_data/Properties.php @@ -1,6 +1,7 @@ log('Document Creator: ' . $creator); // Read the Date when the workbook was created (as a PHP timestamp value) $creationDatestamp = $spreadsheet->getProperties()->getCreated(); -// Format the date and time using the standard PHP date() function -$creationDate = date('l, d<\s\up>S F Y', $creationDatestamp); -$creationTime = date('g:i A', $creationDatestamp); +$creationDate = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'l, d<\s\up>S F Y'); +$creationTime = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'g:i A'); $helper->log('Created On: ' . $creationDate . ' at ' . $creationTime); // Read the name of the last person to modify this workbook @@ -30,8 +30,8 @@ $helper->log('Last Modified By: ' . $modifiedBy); // Read the Date when the workbook was last modified (as a PHP timestamp value) $modifiedDatestamp = $spreadsheet->getProperties()->getModified(); // Format the date and time using the standard PHP date() function -$modifiedDate = date('l, d<\s\up>S F Y', $modifiedDatestamp); -$modifiedTime = date('g:i A', $modifiedDatestamp); +$modifiedDate = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'l, d<\s\up>S F Y'); +$modifiedTime = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'g:i A'); $helper->log('Last Modified On: ' . $modifiedDate . ' at ' . $modifiedTime); // Read the workbook title property diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 7d4319c3..7ddce046 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -501,14 +501,17 @@ class Date return $day; } - public static function dateTimeFromTimestamp(string $date): DateTime + public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime { - return DateTime::createFromFormat('U', $date) ?: new DateTime(); + $dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime(); + $dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone()); + + return $dtobj; } - public static function formattedDateTimeFromTimestamp(string $date, string $format): string + public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string { - $dtobj = self::dateTimeFromTimestamp($date); + $dtobj = self::dateTimeFromTimestamp($date, $timeZone); return $dtobj->format($format); } diff --git a/src/PhpSpreadsheet/Writer/Ods/Meta.php b/src/PhpSpreadsheet/Writer/Ods/Meta.php index 9504ed7c..16f7c8b5 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Meta.php +++ b/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -82,7 +82,7 @@ class Meta extends WriterPart return $objWriter->getData(); } - private function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + private static function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void { $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); foreach ($customPropertyList as $key => $customProperty) { @@ -106,7 +106,7 @@ class Meta extends WriterPart break; case Properties::PROPERTY_TYPE_DATE: $objWriter->writeAttribute('meta:value-type', 'date'); - $dtobj = Date::dateTimeFromTimestamp($propertyValue); + $dtobj = Date::dateTimeFromTimestamp($propertyValue ?? 0); $objWriter->writeRawData($dtobj->format(DATE_W3C)); break; diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index 4583e240..c539da09 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Document; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; @@ -163,7 +164,7 @@ class PropertiesTest extends TestCase self::assertSame($expectedType, $this->properties->getCustomPropertyType($propertyName)); $result = $this->properties->getCustomPropertyValue($propertyName); if ($expectedType === Properties::PROPERTY_TYPE_DATE) { - $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d', new DateTimeZone('UTC')); } self::assertSame($expectedValue, $result); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php index 1e2bd841..bb414da5 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PropertiesTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -44,7 +45,7 @@ class PropertiesTest extends AbstractFunctional self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); $result = $properties->getCustomPropertyValue($propertyName); if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { - $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d', new DateTimeZone('UTC')); } self::assertSame($testData['value'], $result); } @@ -86,7 +87,7 @@ class PropertiesTest extends AbstractFunctional self::assertSame($testData['type'], $properties->getCustomPropertyType($propertyName)); $result = $properties->getCustomPropertyValue($propertyName); if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) { - $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d'); + $result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d', new DateTimeZone('UTC')); } self::assertSame($testData['value'], $result); } From b19fcef51f688e7bf573bde0866b0662c0cbffc7 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Wed, 2 Jun 2021 20:46:14 -0700 Subject: [PATCH 4/5] Autofilter Part 1 Most of the remaining 32-bit-unsafe date handling that remains in PhpSpreadsheet is in AutoFilter. Cleaning this up demonstrated that there are a lot of problems with AutoFilter, and I will do it in (probably two) pieces. In this PR: - Dynamic date processing was really wrong. There were no tests nor samples to exercise this code. (If you need details, you can try running the new sample against old code.) It is completely re-written. - ThisYear/Month/Week/Quarter had been omitted. - Rules such as AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2 were almost correct, but showed some off-by-1 errors. I suspect these were timezone-related, and therefore more obvious to those of us far away from Greenwich. - All Autofilter tests are moved to a single directory. - The documentation suggested using null with the Dynamic Date setup, but Phpstan did not like that in my new tests/samples. Rather than change the doc block, I changed the documentation to suggest null string. - I created a new sample to generate sheets using all the dynamic filters. - I have added some new unit tests for each of the dynamic filters. I would love to be able to add some "time travel" tests because the dynamic nature of the filter makes most of the results change from day to day, which presents significant challenges in writing comprehensive unit tests (the same is true for code coverage). I was not able to find a good way to simulate time within PhpUnit, but the Linux 'faketime' package was extraordinarily easy and helpful in allowing me to confirm some edge cases. I had less satisfactory results with some Windows equivalents, but was still able to run some tests. - Code coverage increases from below 60% to above 80%. To be done: - Some 32-bit unsafe dates remain in filterTestInDateGroupSet. - Also in some of the existing AutoFilter samples. - Study existing unit tests for AutoFilter which use mocking to see if they can/should be replaced with 'real' tests. - Improve code coverage in AutoFilter, AutoFilter/Column, and AutoFilter/Common/Rule. --- docs/topics/autofilters.md | 4 +- phpstan-baseline.neon | 42 +- .../10_Autofilter_dynamic_dates.php | 100 +++++ src/PhpSpreadsheet/Worksheet/AutoFilter.php | 416 ++++++++++++------ .../Worksheet/AutoFilter/Column.php | 4 +- .../AutoFilter/AutoFilterMonthTest.php | 91 ++++ .../AutoFilter/AutoFilterQuarterTest.php | 75 ++++ .../{ => AutoFilter}/AutoFilterTest.php | 3 +- .../AutoFilter/AutoFilterTodayTest.php | 65 +++ .../AutoFilter/AutoFilterWeekTest.php | 76 ++++ .../AutoFilter/AutoFilterYearTest.php | 119 +++++ 11 files changed, 805 insertions(+), 190 deletions(-) create mode 100644 samples/Autofilter/10_Autofilter_dynamic_dates.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php rename tests/PhpSpreadsheetTests/Worksheet/{ => AutoFilter}/AutoFilterTest.php (99%) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php diff --git a/docs/topics/autofilters.md b/docs/topics/autofilters.md index d5a07f8b..16efb8ff 100644 --- a/docs/topics/autofilters.md +++ b/docs/topics/autofilters.md @@ -327,14 +327,14 @@ $columnFilter->setFilterType( ``` When defining the rule for a dynamic filter, we don't define a value (we -can simply set that to NULL) but we do specify the dynamic filter +can simply set that to null string) but we do specify the dynamic filter category. ```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, - NULL, + '', \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE ) ->setRuleType( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4ee0a9ef..a64f3781 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5417,7 +5417,7 @@ parameters: - message: "#^Parameter \\#1 \\$excelTimestamp of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:excelToTimestamp\\(\\) expects float\\|int, float\\|int\\|string given\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - @@ -5430,36 +5430,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - message: "#^Parameter \\#2 \\$now of function strtotime expects int, int\\|false given\\.$#" - count: 6 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#5 \\$day of function gmmktime expects int, string given\\.$#" - count: 8 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#6 \\$year of function gmmktime expects int, string given\\.$#" - count: 13 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#4 \\$mon of function gmmktime expects int, string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#5 \\$day of function gmmktime expects int, float given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#1 \\$attributes of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttributes\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\:\\:calculateTopTenValue\\(\\) has no return typehint specified\\.$#" count: 1 @@ -7445,21 +7415,11 @@ parameters: count: 1 path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/Column/RuleTest.php - - - message: "#^Parameter \\#1 \\$attributes of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttributes\\(\\) expects array\\, array\\ given\\.$#" - count: 3 - path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php - - message: "#^Parameter \\#2 \\$pValue of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttribute\\(\\) expects string, int given\\.$#" count: 1 path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php - - - message: "#^Parameter \\#1 \\$pColumn of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\:\\:setColumn\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\|string, float given\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php - - message: "#^Parameter \\#1 \\$im of function imagecolorallocate expects resource, resource\\|false given\\.$#" count: 1 diff --git a/samples/Autofilter/10_Autofilter_dynamic_dates.php b/samples/Autofilter/10_Autofilter_dynamic_dates.php new file mode 100644 index 00000000..959925c1 --- /dev/null +++ b/samples/Autofilter/10_Autofilter_dynamic_dates.php @@ -0,0 +1,100 @@ +createSheet(); + $sheet->setTitle($rule); + $sheet->getCell('A1')->setValue('Date'); + $row = 1; + $date = new DateTime(); + $year = (int) $date->format('Y'); + $month = (int) $date->format('m'); + $day = (int) $date->format('d'); + $yearMinus2 = $year - 2; + $sheet->getCell('B1')->setValue("=DATE($year, $month, $day)"); + // Each day for two weeks before today through 2 weeks after + for ($dayOffset = -14; $dayOffset < 14; ++$dayOffset) { + ++$row; + $sheet->getCell("A$row")->setValue("=B1+($dayOffset)"); + } + // First and last day of each month, starting with January 2 years before, + // through December 2 years after. + for ($monthOffset = 0; $monthOffset < 48; ++$monthOffset) { + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($yearMinus2, $monthOffset, 1)"); + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($yearMinus2, $monthOffset + 1, 0)"); + } + $sheet->getStyle("A2:A$row")->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $sheet->getStyle('B1')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $sheet->getColumnDimension('A')->setAutoSize(true); + $sheet->getColumnDimension('B')->setAutoSize(true); + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$row"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $sheet->setSelectedCell('B1'); +} + +// Create new Spreadsheet object +$helper->log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); + +// Set document properties +$helper->log('Set document properties'); +$spreadsheet->getProperties()->setCreator('Owen Leibman') + ->setLastModifiedBy('Owen Leibman') + ->setTitle('PhpSpreadsheet Test Document') + ->setSubject('PhpSpreadsheet Test Document') + ->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.') + ->setKeywords('office PhpSpreadsheet php') + ->setCategory('Test result file'); + +$ruleNames = [ + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3, +]; + +// Create the worksheets +foreach ($ruleNames as $ruleName) { + $helper->log("Add data and filter for $ruleName"); + createSheet($spreadsheet, $ruleName); +} +$spreadsheet->removeSheetByIndex(0); +$spreadsheet->setActiveSheetIndex(0); +// Save +$helper->write($spreadsheet, __FILE__); diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index b846f7b6..c75e20ba 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -2,12 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use DateTime; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; class AutoFilter { @@ -358,38 +360,38 @@ class AutoFilter if (is_numeric($rule['value'])) { // Numeric values are tested using the appropriate operator switch ($rule['operator']) { - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_EQUAL: $retVal = ($cellValue == $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: $retVal = ($cellValue != $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: + case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: $retVal = ($cellValue > $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: $retVal = ($cellValue >= $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: + case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: $retVal = ($cellValue < $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: $retVal = ($cellValue <= $rule['value']); break; } } elseif ($rule['value'] == '') { switch ($rule['operator']) { - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_EQUAL: $retVal = (($cellValue == '') || ($cellValue === null)); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: $retVal = (($cellValue != '') && ($cellValue !== null)); break; @@ -439,7 +441,8 @@ class AutoFilter } if (is_numeric($cellValue)) { - $dateValue = date('m', Date::excelToTimestamp($cellValue)); + $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC')); + $dateValue = (int) $dateObject->format('m'); if (in_array($dateValue, $monthSet)) { return true; } @@ -457,6 +460,224 @@ class AutoFilter private static $toReplace = ['.*', '.', '~', '\*', '\?']; + private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime + { + $baseDate = new DateTime(); + $baseDate->setDate($year, $month, $day); + $baseDate->setTime($hour, $minute, $second); + + return $baseDate; + } + + private const DATE_FUNCTIONS = [ + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday', + ]; + + private static function dynamicLastMonth(): array + { + $maxval = new DateTime(); + $year = (int) $maxval->format('Y'); + $month = (int) $maxval->format('m'); + $maxval->setDate($year, $month, 1); + $maxval->setTime(0, 0, 0); + $val = clone $maxval; + $val->modify('-1 month'); + + return [$val, $maxval]; + } + + private static function firstDayOfQuarter(): DateTime + { + $val = new DateTime(); + $year = (int) $val->format('Y'); + $month = (int) $val->format('m'); + $month = 3 * intdiv($month - 1, 3) + 1; + $val->setDate($year, $month, 1); + $val->setTime(0, 0, 0); + + return $val; + } + + private static function dynamicLastQuarter(): array + { + $maxval = self::firstDayOfQuarter(); + $val = clone $maxval; + $val->modify('-3 months'); + + return [$val, $maxval]; + } + + private static function dynamicLastWeek(): array + { + $val = new DateTime(); + $val->setTime(0, 0, 0); + $dayOfWeek = (int) $val->format('w'); // Sunday is 0 + $subtract = $dayOfWeek + 7; // revert to prior Sunday + $val->modify("-$subtract days"); + $maxval = clone $val; + $maxval->modify('+7 days'); + + return [$val, $maxval]; + } + + private static function dynamicLastYear(): array + { + $val = new DateTime(); + $year = (int) $val->format('Y'); + $val = self::makeDateObject($year - 1, 1, 1); + $maxval = self::makeDateObject($year, 1, 1); + + return [$val, $maxval]; + } + + private static function dynamicNextMonth(): array + { + $val = new DateTime(); + $year = (int) $val->format('Y'); + $month = (int) $val->format('m'); + $val->setDate($year, $month, 1); + $val->setTime(0, 0, 0); + $val->modify('+1 month'); + $maxval = clone $val; + $maxval->modify('+1 month'); + + return [$val, $maxval]; + } + + private static function dynamicNextQuarter(): array + { + $val = self::firstDayOfQuarter(); + $val->modify('+3 months'); + $maxval = clone $val; + $maxval->modify('+3 months'); + + return [$val, $maxval]; + } + + private static function dynamicNextWeek(): array + { + $val = new DateTime(); + $val->setTime(0, 0, 0); + $dayOfWeek = (int) $val->format('w'); // Sunday is 0 + $add = 7 - $dayOfWeek; // move to next Sunday + $val->modify("+$add days"); + $maxval = clone $val; + $maxval->modify('+7 days'); + + return [$val, $maxval]; + } + + private static function dynamicNextYear(): array + { + $val = new DateTime(); + $year = (int) $val->format('Y'); + $val = self::makeDateObject($year + 1, 1, 1); + $maxval = self::makeDateObject($year + 2, 1, 1); + + return [$val, $maxval]; + } + + private static function dynamicThisMonth(): array + { + $baseDate = new DateTime(); + $baseDate->setTime(0, 0, 0); + $year = (int) $baseDate->format('Y'); + $month = (int) $baseDate->format('m'); + $val = self::makeDateObject($year, $month, 1); + $maxval = clone $val; + $maxval->modify('+1 month'); + + return [$val, $maxval]; + } + + private static function dynamicThisQuarter(): array + { + $val = self::firstDayOfQuarter(); + $maxval = clone $val; + $maxval->modify('+3 months'); + + return [$val, $maxval]; + } + + private static function dynamicThisWeek(): array + { + $val = new DateTime(); + $val->setTime(0, 0, 0); + $dayOfWeek = (int) $val->format('w'); // Sunday is 0 + $subtract = $dayOfWeek; // revert to Sunday + $val->modify("-$subtract days"); + $maxval = clone $val; + $maxval->modify('+7 days'); + + return [$val, $maxval]; + } + + private static function dynamicThisYear(): array + { + $val = new DateTime(); + $year = (int) $val->format('Y'); + $val = self::makeDateObject($year, 1, 1); + $maxval = self::makeDateObject($year + 1, 1, 1); + + return [$val, $maxval]; + } + + private static function dynamicToday(): array + { + $val = new DateTime(); + $val->setTime(0, 0, 0); + $maxval = clone $val; + $maxval->modify('+1 day'); + + return [$val, $maxval]; + } + + private static function dynamicTomorrow(): array + { + $val = new DateTime(); + $val->setTime(0, 0, 0); + $val->modify('+1 day'); + $maxval = clone $val; + $maxval->modify('+1 day'); + + return [$val, $maxval]; + } + + private static function dynamicYearToDate(): array + { + $maxval = new DateTime(); + $maxval->setTime(0, 0, 0); + $val = self::makeDateObject((int) $maxval->format('Y'), 1, 1); + $maxval->modify('+1 day'); + + return [$val, $maxval]; + } + + private static function dynamicYesterday(): array + { + $maxval = new DateTime(); + $maxval->setTime(0, 0, 0); + $val = clone $maxval; + $val->modify('-1 day'); + + return [$val, $maxval]; + } + /** * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation. * @@ -467,118 +688,25 @@ class AutoFilter */ private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn) { - $rDateType = Functions::getReturnDateType(); - Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC); - $val = $maxVal = null; - $ruleValues = []; - $baseDate = DateTimeExcel\Current::now(); + $val = $maxVal = null; + $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found? // Calculate start/end dates for the required date range based on current date - switch ($dynamicRuleType) { - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: - $baseDate = strtotime('-7 days', $baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: - $baseDate = strtotime('-7 days', $baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: - $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: - $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: - $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: - $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: - $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: - $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - } - - switch ($dynamicRuleType) { - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: - $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); - $val = (int) Date::PHPToExcel($baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE: - $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: - $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: - $thisMonth = date('m', $baseDate); - $thisQuarter = floor(--$thisMonth / 3); - $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: - $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: - $dayOfWeek = date('w', $baseDate); - $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek; - $maxVal = $val + 7; - - break; - } - - switch ($dynamicRuleType) { - // Adjust Today dates for Yesterday and Tomorrow - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: - --$maxVal; - --$val; - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: - ++$maxVal; - ++$val; - - break; + // Val is lowest permitted value. + // Maxval is greater than highest permitted value + $val = $maxval = 0; + if (is_callable($callBack)) { + [$val, $maxval] = $callBack(); } + $val = Date::dateTimeToExcel($val); + $maxval = Date::dateTimeToExcel($maxval); // Set the filter column rule attributes ready for writing - $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxVal]); + $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]); // Set the rules for identifying rows for hide/show - $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val]; - $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal]; - Functions::setReturnDateType($rDateType); + $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val]; + $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval]; return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]]; } @@ -589,7 +717,7 @@ class AutoFilter $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false)); $dataValues = array_filter($dataValues); - if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) { + if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) { rsort($dataValues); } else { sort($dataValues); @@ -630,7 +758,7 @@ class AutoFilter if (count($ruleValues) != count($ruleDataSet)) { $blanks = true; } - if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) { + if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) { // Filter on absolute values $columnFilterTests[$columnID] = [ 'method' => 'filterTestInSimpleDataSet', @@ -646,40 +774,40 @@ class AutoFilter foreach ($ruleDataSet as $ruleValue) { $date = $time = ''; if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '') ) { - $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]); + $date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '') ) { - $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]); + $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '') ) { - $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]); + $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]); } $dateTime = $date . $time; $arguments['date'][] = $date; @@ -727,17 +855,17 @@ class AutoFilter // We should only ever have one Dynamic Filter Rule anyway $dynamicRuleType = $rule->getGrouping(); if ( - ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) || - ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE) + ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) || + ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE) ) { // Number (Average) based // Calculate the average $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')'; $average = Calculation::getInstance()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1')); // Set above/below rule based on greaterThan or LessTan - $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) - ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN - : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN; + $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) + ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN + : Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN; $ruleValues[] = [ 'operator' => $operator, 'value' => $average, @@ -788,7 +916,7 @@ class AutoFilter $ruleValue = $rule->getValue(); $ruleOperator = $rule->getOperator(); } - if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) { + if ($ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) { $ruleValue = floor($ruleValue * ($dataRowCount / 100)); } if (!is_array($ruleValue) && $ruleValue < 1) { @@ -800,9 +928,9 @@ class AutoFilter $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue); - $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) - ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL - : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL; + $operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) + ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL + : Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL; $ruleValues[] = ['operator' => $operator, 'value' => $maxVal]; $columnFilterTests[$columnID] = [ 'method' => 'filterTestInCustomDataSet', diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php index 8ec0ca3b..1309b30f 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -215,11 +215,11 @@ class Column /** * Set AutoFilter Attributes. * - * @param string[] $attributes + * @param mixed[] $attributes * * @return $this */ - public function setAttributes(array $attributes) + public function setAttributes($attributes) { $this->attributes = $attributes; diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php new file mode 100644 index 00000000..064c0dac --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php @@ -0,0 +1,91 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), 1)'); + if ($startMonth === 12) { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2) + 1, 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2) + 1, 2, 1)'); + } elseif ($startMonth === 11) { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2) + 1, 1, 1)'); + } else { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), MONTH(A2) + 2, 1)'); + } + if ($startMonth === 1) { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2) - 1, 12, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2) - 1, 10, 1)'); + } elseif ($startMonth === 2) { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), 1, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2) - 1, 12, 1)'); + } else { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), MONTH(A2) - 1, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2), MONTH(A2) - 2, 1)'); + } + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerMonth + */ + public function testMonths(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet, $startMonth); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php new file mode 100644 index 00000000..d75f1bc2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php @@ -0,0 +1,75 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), 1)'); + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 3, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), MONTH(A2) + 6, 1)'); + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), MONTH(A2) - 3, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2), MONTH(A2) - 6, 1)'); + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerQuarter + */ + public function testQuarters(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet, $startMonth); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php similarity index 99% rename from tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php rename to tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index 7f218b54..a6693d34 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -1,6 +1,6 @@ expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $invalidColumn = 123.456; + // @phpstan-ignore-next-line $this->testAutoFilterObject->setColumn($invalidColumn); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php new file mode 100644 index 00000000..3875eb8d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php @@ -0,0 +1,65 @@ +getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = $dtStart->format('d'); + $sheet->getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=NOW()'); + $sheet->getCell('A3')->setValue('=A2+1'); + $sheet->getCell('A4')->setValue('=A2-1'); + $sheet->getCell('A5')->setValue('=TODAY()'); + $sheet->getCell('A6')->setValue('=A5+1'); + $sheet->getCell('A7')->setValue('=A5-1'); + $maxRow = 7; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php new file mode 100644 index 00000000..3e8fcdb8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php @@ -0,0 +1,76 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('B2')->setValue('=WEEKDAY(A2) - 1'); // subtract to get to Sunday + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), DAY(A2) - B2)'); + $sheet->getCell('A4')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) + 8)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) + 19)'); + $sheet->getCell('A6')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) - 6)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) - 12)'); + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerWeek + */ + public function testWeek(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php new file mode 100644 index 00000000..105608ae --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php @@ -0,0 +1,119 @@ +getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $sheet->getCell('A1')->setValue('Date'); + $year = (int) $dtStart->format('Y') - 1; + $row = 1; + $iteration = 0; + while ($iteration < 3) { + for ($month = 3; $month < 13; $month += 4) { + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($year, $month, 1)"); + } + ++$year; + ++$iteration; + } + ++$row; + $sheet->getCell("A$row")->setValue('=DATE(2041, 1, 1)'); // beyond epoch + $maxRow = $row; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } + + public function testYearToDate(): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + $sheet->getCell('A1')->setValue('Date'); + $year = (int) $dtStart->format('Y'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), 12, 31)'); + $sheet->getCell('A4')->setValue('=A3 + 1'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), 1, 1)'); + $sheet->getCell('A6')->setValue('=A5 - 1'); + + $maxRow = 6; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + $expected = ($startMonth === 12 && $startDay === 31) ? [2, 3, 5] : [2, 5]; + self::assertEquals($expected, $actualVisible); + } +} From 4ab6439de16f98287af3ceeb23a3b94343cfd494 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Wed, 2 Jun 2021 21:49:12 -0700 Subject: [PATCH 5/5] Scrutinizer Fix 5 minor errors. --- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 1 - .../Worksheet/AutoFilter/AutoFilterQuarterTest.php | 5 ++--- .../Worksheet/AutoFilter/AutoFilterWeekTest.php | 1 - .../Worksheet/AutoFilter/AutoFilterYearTest.php | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index c75e20ba..6c23805d 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -689,7 +689,6 @@ class AutoFilter private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn) { $ruleValues = []; - $val = $maxVal = null; $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found? // Calculate start/end dates for the required date range based on current date // Val is lowest permitted value. diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php index d75f1bc2..73c48c4a 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php @@ -20,7 +20,7 @@ class AutoFilterQuarterTest extends TestCase ]; } - private static function setCells(Worksheet $sheet, int $startMonth): void + private static function setCells(Worksheet $sheet): void { $sheet->getCell('A1')->setValue('Date'); $sheet->getCell('A2')->setValue('=TODAY()'); @@ -45,8 +45,7 @@ class AutoFilterQuarterTest extends TestCase $sheet = $spreadsheet->getActiveSheet(); $dtStart = new DateTimeImmutable(); $startDay = (int) $dtStart->format('d'); - $startMonth = (int) $dtStart->format('m'); - self::setCells($sheet, $startMonth); + self::setCells($sheet); $maxRow = 9; $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php index 3e8fcdb8..765bf11c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php @@ -46,7 +46,6 @@ class AutoFilterWeekTest extends TestCase $sheet = $spreadsheet->getActiveSheet(); $dtStart = new DateTimeImmutable(); $startDay = (int) $dtStart->format('d'); - $startMonth = (int) $dtStart->format('m'); self::setCells($sheet); $maxRow = 9; diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php index 105608ae..8c416cc3 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php @@ -84,7 +84,6 @@ class AutoFilterYearTest extends TestCase $startDay = (int) $dtStart->format('d'); $startMonth = (int) $dtStart->format('m'); $sheet->getCell('A1')->setValue('Date'); - $year = (int) $dtStart->format('Y'); $sheet->getCell('A2')->setValue('=TODAY()'); $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), 12, 31)'); $sheet->getCell('A4')->setValue('=A3 + 1');