From e4e99b8a73045395784ecba4ec712d4b75c4df69 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:58:38 -0700 Subject: [PATCH] Permit Date/Time Entered on Spreadsheet to be Calculated as Float (#3121) * Permit Date/Time Entered on Spreadsheet to be Calculated as Float Fix #1416. I do not entirely understand the use case for this old issue, but resolving it seems straightforward. Issue complains that user-entered date/time fields may be interpreted as either float or int when PhpSpreadsheet reads them. Issue suggests getCalculatedValue treat all date/time fields as float; that seems like a breaking change. However, adding an option to permit it seems okay. That option might be implemented as either a property of Calculation, or a static property of Cell. Since the changed logic is found in Cell (and Shared/Date), I opted for the latter. In Cell, the property `$parent` is incorrectly described in doc block as `Cells`, and should be `?Cells`. This change eliminates some Phpstan and Scrutinizer problems, and should allow the elimination of some try/catch blocks - I have not done an exhaustive search for those. Calls to `isDateTime` could have affected activeSheet and selectedCells; they no longer can. Optional parameters are added to it and the functions it calls to accommodate the new functionality; the defaults for the new parameters will, of course, return the same result as the earlier versions of the functions would have returned. * Scrutinizer - Self-inflicted Tests used constant which I deprecated. --- docs/topics/calculation-engine.md | 13 ++ phpstan-baseline.neon | 36 --- .../Autofilter/10_Autofilter_selection_1.php | 2 +- .../Autofilter/10_Autofilter_selection_2.php | 2 +- .../10_Autofilter_selection_display.php | 2 +- samples/Basic/02_Types.php | 2 +- src/PhpSpreadsheet/Cell/Cell.php | 114 ++++++++-- src/PhpSpreadsheet/Shared/Date.php | 75 +++---- src/PhpSpreadsheet/Style/NumberFormat.php | 37 +++ .../Cell/CellDetachTest.php | 78 +++++++ tests/PhpSpreadsheetTests/Cell/CellTest.php | 9 +- .../PhpSpreadsheetTests/Shared/Date2Test.php | 212 ++++++++++++++++++ 12 files changed, 481 insertions(+), 101 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Cell/CellDetachTest.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Date2Test.php diff --git a/docs/topics/calculation-engine.md b/docs/topics/calculation-engine.md index 7dc838f7..94863b67 100644 --- a/docs/topics/calculation-engine.md +++ b/docs/topics/calculation-engine.md @@ -321,6 +321,19 @@ and false is failure (e.g. an invalid DateTimeZone value was passed.) These functions support a timezone as an optional second parameter. This applies a specific timezone to that function call without affecting the default PhpSpreadsheet Timezone. +### Calculating Value of Date/Time Read From Spreadsheet + +Nothing special needs to be done to interpret Date/Time values entered directly into a spreadsheet. They will have been stored as numbers with an appropriate number format set for the cell. However, depending on their value, they may have been stored as either integer or float values. If that is a problem, you can force `getCalculatedValue` to return float rather than int depending on the number format used for the cell. + +```php +// All fields with Date, Time, or DateTime styles returned as float. +\PhpOffice\PhpSpreadsheet\Cell\Cell::setCalculateDateTimeType(\PhpOffice\PhpSpreadsheet\Cell\Cell::CALCULATE_DATE_TIME_FLOAT); +// All fields with Time or DateTime styles returned as float. +\PhpOffice\PhpSpreadsheet\Cell\Cell::setCalculateDateTimeType(\PhpOffice\PhpSpreadsheet\Cell\Cell::CALCULATE_TIME_FLOAT); +// Default - fields with Date, Time, or DateTime styles returned as they had been stored. +\PhpOffice\PhpSpreadsheet\Cell\Cell::setCalculateDateTimeType(\PhpOffice\PhpSpreadsheet\Cell\Cell::CALCULATE_DATE_TIME_ASIS); +``` + ## Function Reference ### Database Functions diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fde17372..930744d7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -684,26 +684,6 @@ parameters: message: "#^Variable \\$value on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/TextData/Text.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:getFormulaAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - - message: "#^Parameter \\#2 \\$format of static method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\:\\:toFormattedString\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:\\$formulaAttributes has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:\\$parent \\(PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Cells\\) in isset\\(\\) is not nullable\\.$#" - count: 6 - path: src/PhpSpreadsheet/Cell/Cell.php - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - message: "#^Call to an undefined method object\\:\\:getHashCode\\(\\)\\.$#" count: 1 @@ -1078,22 +1058,6 @@ parameters: message: "#^Strict comparison using \\=\\=\\= between int and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Settings.php - - - message: "#^Parameter \\#1 \\$excelFormatCode of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:isDateTimeFormatCode\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Date.php - - - message: "#^Parameter \\#1 \\$string of function substr expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Shared/Date.php - - - message: "#^Parameter \\#1 \\$unixTimestamp of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:timestampToExcel\\(\\) expects int, float\\|int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Date.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:\\$possibleDateFormatCharacters has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Date.php - message: "#^Cannot access offset 1 on array\\|false\\.$#" count: 1 diff --git a/samples/Autofilter/10_Autofilter_selection_1.php b/samples/Autofilter/10_Autofilter_selection_1.php index 511417f0..764bb221 100644 --- a/samples/Autofilter/10_Autofilter_selection_1.php +++ b/samples/Autofilter/10_Autofilter_selection_1.php @@ -95,7 +95,7 @@ $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getAlignment()->setWrapText(true); $spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(12.5); $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(10.5); -$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); +$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); $spreadsheet->getActiveSheet()->getStyle('E2:F' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_CURRENCY_USD_SIMPLE); $spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(14); $spreadsheet->getActiveSheet()->freezePane('A2'); diff --git a/samples/Autofilter/10_Autofilter_selection_2.php b/samples/Autofilter/10_Autofilter_selection_2.php index 9e0680fc..3d0b0249 100644 --- a/samples/Autofilter/10_Autofilter_selection_2.php +++ b/samples/Autofilter/10_Autofilter_selection_2.php @@ -95,7 +95,7 @@ $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getAlignment()->setWrapText(true); $spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(12.5); $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(10.5); -$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); +$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); $spreadsheet->getActiveSheet()->getStyle('E2:F' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_CURRENCY_USD_SIMPLE); $spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(14); $spreadsheet->getActiveSheet()->freezePane('A2'); diff --git a/samples/Autofilter/10_Autofilter_selection_display.php b/samples/Autofilter/10_Autofilter_selection_display.php index b07266b9..9905b539 100644 --- a/samples/Autofilter/10_Autofilter_selection_display.php +++ b/samples/Autofilter/10_Autofilter_selection_display.php @@ -95,7 +95,7 @@ $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A1:F1')->getAlignment()->setWrapText(true); $spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth(12.5); $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(10.5); -$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); +$spreadsheet->getActiveSheet()->getStyle('D2:D' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); $spreadsheet->getActiveSheet()->getStyle('E2:F' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_CURRENCY_USD_SIMPLE); $spreadsheet->getActiveSheet()->getColumnDimension('F')->setWidth(14); $spreadsheet->getActiveSheet()->freezePane('A2'); diff --git a/samples/Basic/02_Types.php b/samples/Basic/02_Types.php index 965071b8..4eaaa8b1 100644 --- a/samples/Basic/02_Types.php +++ b/samples/Basic/02_Types.php @@ -81,7 +81,7 @@ $spreadsheet->getActiveSheet() $spreadsheet->getActiveSheet() ->getStyle('C9') ->getNumberFormat() - ->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2); + ->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD); $spreadsheet->getActiveSheet() ->setCellValue('A10', 'Date/Time') diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index d4d793d7..47959f68 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -12,7 +12,6 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use Throwable; class Cell { @@ -52,7 +51,7 @@ class Cell /** * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet). * - * @var Cells + * @var ?Cells */ private $parent; @@ -65,6 +64,8 @@ class Cell /** * Attributes of the formula. + * + * @var mixed */ private $formulaAttributes; @@ -75,14 +76,17 @@ class Cell */ public function updateInCollection(): self { - $this->parent->update($this); + $parent = $this->parent; + if ($parent === null) { + throw new Exception('Cannot update when cell is not bound to a worksheet'); + } + $parent->update($this); return $this; } public function detach(): void { - // @phpstan-ignore-next-line $this->parent = null; } @@ -122,7 +126,12 @@ class Cell */ public function getColumn() { - return $this->parent->getCurrentColumn(); + $parent = $this->parent; + if ($parent === null) { + throw new Exception('Cannot get column when cell is not bound to a worksheet'); + } + + return $parent->getCurrentColumn(); } /** @@ -132,7 +141,12 @@ class Cell */ public function getRow() { - return $this->parent->getCurrentRow(); + $parent = $this->parent; + if ($parent === null) { + throw new Exception('Cannot get row when cell is not bound to a worksheet'); + } + + return $parent->getCurrentRow(); } /** @@ -142,9 +156,10 @@ class Cell */ public function getCoordinate() { - try { - $coordinate = $this->parent->getCurrentCoordinate(); - } catch (Throwable $e) { + $parent = $this->parent; + if ($parent !== null) { + $coordinate = $parent->getCurrentCoordinate(); + } else { $coordinate = null; } if ($coordinate === null) { @@ -171,8 +186,7 @@ class Cell { return (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), - $this->getStyle() - ->getNumberFormat()->getFormatCode() + (string) $this->getStyle()->getNumberFormat()->getFormatCode() ); } @@ -251,8 +265,6 @@ class Cell break; default: throw new Exception('Invalid datatype: ' . $dataType); - - break; } // set the datatype @@ -261,6 +273,56 @@ class Cell return $this->updateInCollection(); } + public const CALCULATE_DATE_TIME_ASIS = 0; + public const CALCULATE_DATE_TIME_FLOAT = 1; + public const CALCULATE_TIME_FLOAT = 2; + + /** @var int */ + private static $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS; + + public static function getCalculateDateTimeType(): int + { + return self::$calculateDateTimeType; + } + + public static function setCalculateDateTimeType(int $calculateDateTimeType): void + { + switch ($calculateDateTimeType) { + case self::CALCULATE_DATE_TIME_ASIS: + case self::CALCULATE_DATE_TIME_FLOAT: + case self::CALCULATE_TIME_FLOAT: + self::$calculateDateTimeType = $calculateDateTimeType; + + break; + default: + throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception("Invalid value $calculateDateTimeType for calculated date time type"); + } + } + + /** + * Convert date, time, or datetime from int to float if desired. + * + * @param mixed $result + * + * @return mixed + */ + private function convertDateTimeInt($result) + { + if (is_int($result)) { + if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) { + if (SharedDate::isDateTime($this, $result, false)) { + $result = (float) $result; + } + } elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) { + if (SharedDate::isDateTime($this, $result, true)) { + $result = (float) $result; + } + } + } + + return $result; + } + /** * Get calculated cell value. * @@ -277,6 +339,7 @@ class Cell $result = Calculation::getInstance( $this->getWorksheet()->getParent() )->calculateCellValue($this, $resetLog); + $result = $this->convertDateTimeInt($result); $this->getWorksheet()->setSelectedCells($selected); $this->getWorksheet()->getParent()->setActiveSheetIndex($index); // We don't yet handle array returns @@ -306,7 +369,7 @@ class Cell return $this->value->getPlainText(); } - return $this->value; + return $this->convertDateTimeInt($this->value); } /** @@ -458,7 +521,7 @@ class Cell /** * Get cell collection. * - * @return Cells + * @return ?Cells */ public function getParent() { @@ -470,9 +533,10 @@ class Cell */ public function getWorksheet(): Worksheet { - try { - $worksheet = $this->parent->getParent(); - } catch (Throwable $e) { + $parent = $this->parent; + if ($parent !== null) { + $worksheet = $parent->getParent(); + } else { $worksheet = null; } @@ -483,6 +547,18 @@ class Cell return $worksheet; } + public function getWorksheetOrNull(): ?Worksheet + { + $parent = $this->parent; + if ($parent !== null) { + $worksheet = $parent->getParent(); + } else { + $worksheet = null; + } + + return $worksheet; + } + /** * Is this cell in a merge range. */ @@ -666,6 +742,8 @@ class Cell /** * Get the formula attributes. + * + * @return mixed */ public function getFormulaAttributes() { diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 87278073..9b08807f 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -301,7 +301,7 @@ class Date * The use of Unix timestamps, and therefore this function, is discouraged. * They are not Y2038-safe on a 32-bit system, and have no timezone info. * - * @param int $unixTimestamp Unix Timestamp + * @param float|int|string $unixTimestamp Unix Timestamp * * @return false|float MS Excel serialized date/time value */ @@ -352,8 +352,8 @@ class Date } // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) - $century = (int) substr($year, 0, 2); - $decade = (int) substr($year, 2, 2); + $century = (int) substr((string) $year, 0, 2); + $decade = (int) substr((string) $year, 2, 2); $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear; $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; @@ -364,16 +364,30 @@ class Date /** * Is a given cell a date/time? * + * @param mixed $value + * * @return bool */ - public static function isDateTime(Cell $cell) + public static function isDateTime(Cell $cell, $value = null, bool $dateWithoutTimeOkay = true) { - return is_numeric($cell->getCalculatedValue()) && - self::isDateTimeFormat( - $cell->getWorksheet()->getStyle( - $cell->getCoordinate() - )->getNumberFormat() - ); + $result = false; + $worksheet = $cell->getWorksheetOrNull(); + $spreadsheet = ($worksheet === null) ? null : $worksheet->getParent(); + if ($worksheet !== null && $spreadsheet !== null) { + $index = $spreadsheet->getActiveSheetIndex(); + $selected = $worksheet->getSelectedCells(); + $result = is_numeric($value ?? $cell->getCalculatedValue()) && + self::isDateTimeFormat( + $worksheet->getStyle( + $cell->getCoordinate() + )->getNumberFormat(), + $dateWithoutTimeOkay + ); + $worksheet->setSelectedCells($selected); + $spreadsheet->setActiveSheetIndex($index); + } + + return $result; } /** @@ -381,12 +395,13 @@ class Date * * @return bool */ - public static function isDateTimeFormat(NumberFormat $excelFormatCode) + public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true) { - return self::isDateTimeFormatCode($excelFormatCode->getFormatCode()); + return self::isDateTimeFormatCode((string) $excelFormatCode->getFormatCode(), $dateWithoutTimeOkay); } - private static $possibleDateFormatCharacters = 'eymdHs'; + private const POSSIBLE_DATETIME_FORMAT_CHARACTERS = 'eymdHs'; + private const POSSIBLE_TIME_FORMAT_CHARACTERS = 'Hs'; // note - no 'm' due to ambiguity /** * Is a given number format code a date/time? @@ -395,7 +410,7 @@ class Date * * @return bool */ - public static function isDateTimeFormatCode($excelFormatCode) + public static function isDateTimeFormatCode($excelFormatCode, bool $dateWithoutTimeOkay = true) { if (strtolower($excelFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) { // "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check) @@ -407,31 +422,8 @@ class Date } // Switch on formatcode - switch ($excelFormatCode) { - // Explicitly defined date formats - case NumberFormat::FORMAT_DATE_YYYYMMDD: - case NumberFormat::FORMAT_DATE_YYYYMMDD2: - case NumberFormat::FORMAT_DATE_DDMMYYYY: - case NumberFormat::FORMAT_DATE_DMYSLASH: - case NumberFormat::FORMAT_DATE_DMYMINUS: - case NumberFormat::FORMAT_DATE_DMMINUS: - case NumberFormat::FORMAT_DATE_MYMINUS: - case NumberFormat::FORMAT_DATE_DATETIME: - case NumberFormat::FORMAT_DATE_TIME1: - case NumberFormat::FORMAT_DATE_TIME2: - case NumberFormat::FORMAT_DATE_TIME3: - case NumberFormat::FORMAT_DATE_TIME4: - case NumberFormat::FORMAT_DATE_TIME5: - case NumberFormat::FORMAT_DATE_TIME6: - case NumberFormat::FORMAT_DATE_TIME7: - case NumberFormat::FORMAT_DATE_TIME8: - case NumberFormat::FORMAT_DATE_YYYYMMDDSLASH: - case NumberFormat::FORMAT_DATE_XLSX14: - case NumberFormat::FORMAT_DATE_XLSX15: - case NumberFormat::FORMAT_DATE_XLSX16: - case NumberFormat::FORMAT_DATE_XLSX17: - case NumberFormat::FORMAT_DATE_XLSX22: - return true; + if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) { + return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY); } // Typically number, currency or accounting (or occasionally fraction) formats @@ -443,8 +435,9 @@ class Date if (\strpos($excelFormatCode, '-00000') !== false) { return false; } + $possibleFormatCharacters = $dateWithoutTimeOkay ? self::POSSIBLE_DATETIME_FORMAT_CHARACTERS : self::POSSIBLE_TIME_FORMAT_CHARACTERS; // Try checking for any of the date formatting characters that don't appear within square braces - if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $excelFormatCode)) { + if (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $excelFormatCode)) { // We might also have a format mask containing quoted strings... // we don't want to test for any of our characters within the quoted blocks if (strpos($excelFormatCode, '"') !== false) { @@ -453,7 +446,7 @@ class Date // Only test in alternate array entries (the non-quoted blocks) if ( ($segMatcher = !$segMatcher) && - (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $subVal)) + (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $subVal)) ) { return true; } diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 6f552cb3..c3069345 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -19,6 +19,7 @@ class NumberFormat extends Supervisor const FORMAT_PERCENTAGE_0 = '0.0%'; const FORMAT_PERCENTAGE_00 = '0.00%'; + /** @deprecated 1.26 use FORMAT_DATE_YYYYMMDD instead */ const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'; const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd'; const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy'; @@ -42,6 +43,42 @@ class NumberFormat extends Supervisor const FORMAT_DATE_TIME8 = 'h:mm:ss;@'; const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@'; + const DATE_TIME_OR_DATETIME_ARRAY = [ + self::FORMAT_DATE_YYYYMMDD, + self::FORMAT_DATE_DDMMYYYY, + self::FORMAT_DATE_DMYSLASH, + self::FORMAT_DATE_DMYMINUS, + self::FORMAT_DATE_DMMINUS, + self::FORMAT_DATE_MYMINUS, + self::FORMAT_DATE_XLSX14, + self::FORMAT_DATE_XLSX15, + self::FORMAT_DATE_XLSX16, + self::FORMAT_DATE_XLSX17, + self::FORMAT_DATE_XLSX22, + self::FORMAT_DATE_DATETIME, + self::FORMAT_DATE_TIME1, + self::FORMAT_DATE_TIME2, + self::FORMAT_DATE_TIME3, + self::FORMAT_DATE_TIME4, + self::FORMAT_DATE_TIME5, + self::FORMAT_DATE_TIME6, + self::FORMAT_DATE_TIME7, + self::FORMAT_DATE_TIME8, + self::FORMAT_DATE_YYYYMMDDSLASH, + ]; + const TIME_OR_DATETIME_ARRAY = [ + self::FORMAT_DATE_XLSX22, + self::FORMAT_DATE_DATETIME, + self::FORMAT_DATE_TIME1, + self::FORMAT_DATE_TIME2, + self::FORMAT_DATE_TIME3, + self::FORMAT_DATE_TIME4, + self::FORMAT_DATE_TIME5, + self::FORMAT_DATE_TIME6, + self::FORMAT_DATE_TIME7, + self::FORMAT_DATE_TIME8, + ]; + const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'; const FORMAT_CURRENCY_USD = '$#,##0_-'; const FORMAT_CURRENCY_EUR_SIMPLE = '#,##0.00_-"€"'; diff --git a/tests/PhpSpreadsheetTests/Cell/CellDetachTest.php b/tests/PhpSpreadsheetTests/Cell/CellDetachTest.php new file mode 100644 index 00000000..38f04cf9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/CellDetachTest.php @@ -0,0 +1,78 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + /** + * @dataProvider providerMethodName + */ + public function testDetach(string $method): void + { + $this->expectException(SpreadsheetException::class); + $this->expectExceptionMessage('is not bound to a worksheet'); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->detach(); + if (method_exists(Cell::class, $method)) { + $sheet->getCell('A1')->$method(); + } else { + self::fail("Cell method $method does not exist"); + } + } + + public function providerMethodName(): array + { + return [ + ['updateInCollection'], + ['getColumn'], + ['getRow'], + ['hasDataValidation'], + ['getDataValidation'], + ['hasHyperlink'], + ['getHyperlink'], + ]; + } + + /** + * @dataProvider providerMethodNameSet + */ + public function testDetachSet(string $method): void + { + $this->expectException(SpreadsheetException::class); + $this->expectExceptionMessage('is not bound to a worksheet'); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->detach(); + if (method_exists(Cell::class, $method)) { + $sheet->getCell('A1')->$method(null); + } else { + self::fail("Cell method $method does not exist"); + } + } + + public function providerMethodNameSet(): array + { + return [ + ['setDataValidation'], + ['setHyperlink'], + ['setValue'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/CellTest.php b/tests/PhpSpreadsheetTests/Cell/CellTest.php index 2ffe0e29..0ec8154f 100644 --- a/tests/PhpSpreadsheetTests/Cell/CellTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CellTest.php @@ -118,8 +118,13 @@ class CellTest extends TestCase self::assertSame('A1', $cell->getCoordinate()); $this->expectException(Exception::class); $this->expectExceptionMessage('Coordinate no longer exists'); - $cell->getParent()->delete('A1'); - $cell->getCoordinate(); + $parent = $cell->getParent(); + if ($parent === null) { + self::fail('Unexpected null parent'); + } else { + $parent->delete('A1'); + $cell->getCoordinate(); + } } public function testAppliedStyleWithRange(): void diff --git a/tests/PhpSpreadsheetTests/Shared/Date2Test.php b/tests/PhpSpreadsheetTests/Shared/Date2Test.php new file mode 100644 index 00000000..72f13e33 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Date2Test.php @@ -0,0 +1,212 @@ +calculateDateTimeType = Cell::getCalculateDateTimeType(); + } + + protected function tearDown(): void + { + Cell::setCalculateDateTimeType($this->calculateDateTimeType); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + public function testInvalidType(): void + { + $this->expectException(CalculationException::class); + $this->expectExceptionMessage('for calculated date time type'); + Cell::setCalculateDateTimeType(-1); + } + + /** + * @dataProvider providerTimeOnly + * + * @param float|int $expectedResult + * @param float|int $value + * @param string $format + */ + public function testTimeOnly($expectedResult, $value, ?string $format = null): void + { + Cell::setCalculateDateTimeType(Cell::CALCULATE_TIME_FLOAT); + $this->spreadsheet = new Spreadsheet(); + self::assertSame(0, $this->spreadsheet->getActiveSheetIndex()); + $sheet = $this->spreadsheet->getActiveSheet(); + $newSheet = $this->spreadsheet->createSheet(); + $newSheet->getCell('B7')->setValue('Here'); + $sheet->getCell('A1')->setValue($value); + if ($format !== null) { + $sheet->getStyle('A1')->getNumberFormat()->setFormatCode($format); + } + $sheet->setSelectedCells('B7'); + $this->spreadsheet->setActiveSheetIndex(1); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame('B7', $sheet->getSelectedCells()); + self::assertSame(1, $this->spreadsheet->getActiveSheetIndex()); + } + + public function providerTimeOnly(): array + { + $integerValue = 44046; + $integerValueAsFloat = (float) $integerValue; + $integerValueAsDateFormula = '=DATEVALUE("2020-08-03")'; + $floatValue = 44015.25; + $floatValueAsDateFormula = '=DATEVALUE("2020-07-03")+TIMEVALUE("06:00")'; + + return [ + 'default format integer' => [$integerValue, $integerValue], + 'default format float' => [$floatValue, $floatValue], + 'date format integer' => [$integerValue, $integerValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'date format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'datetime format integer' => [$integerValueAsFloat, $integerValue, 'yyyy-mm-dd h:mm'], + 'datetime format float' => [$floatValue, $floatValue, 'yyyy-mm-dd h:mm'], + 'time format integer' => [$integerValueAsFloat, $integerValue, NumberFormat::FORMAT_DATE_TIME1], + 'time format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer fltfmt' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula float' => [$floatValue, $floatValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer intfmt but formula returns float' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_YYYYMMDD], + ]; + } + + /** + * @dataProvider providerDateAndTime + * + * @param float|int $expectedResult + * @param float|int $value + * @param string $format + */ + public function testDateAndTime($expectedResult, $value, ?string $format = null): void + { + Cell::setCalculateDateTimeType(Cell::CALCULATE_DATE_TIME_FLOAT); + $this->spreadsheet = new Spreadsheet(); + self::assertSame(0, $this->spreadsheet->getActiveSheetIndex()); + $sheet = $this->spreadsheet->getActiveSheet(); + $newSheet = $this->spreadsheet->createSheet(); + $newSheet->getCell('B7')->setValue('Here'); + $sheet->getCell('A1')->setValue($value); + if ($format !== null) { + $sheet->getStyle('A1')->getNumberFormat()->setFormatCode($format); + } + $sheet->setSelectedCells('B7'); + $this->spreadsheet->setActiveSheetIndex(1); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame('B7', $sheet->getSelectedCells()); + self::assertSame(1, $this->spreadsheet->getActiveSheetIndex()); + } + + public function providerDateAndTime(): array + { + $integerValue = 44046; + $integerValueAsFloat = (float) $integerValue; + $integerValueAsDateFormula = '=DATEVALUE("2020-08-03")'; + $floatValue = 44015.25; + $floatValueAsDateFormula = '=DATEVALUE("2020-07-03")+TIMEVALUE("06:00")'; + + return [ + 'default format integer' => [$integerValue, $integerValue], + 'default format float' => [$floatValue, $floatValue], + 'date format integer' => [$integerValueAsFloat, $integerValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'date format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'datetime format integer' => [$integerValueAsFloat, $integerValue, 'yyyy-mm-dd h:mm'], + 'datetime format float' => [$floatValue, $floatValue, 'yyyy-mm-dd h:mm'], + 'time format integer' => [$integerValueAsFloat, $integerValue, NumberFormat::FORMAT_DATE_TIME1], + 'time format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer fltfmt' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula float' => [$floatValue, $floatValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer intfmt but formula returns float' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_YYYYMMDD], + ]; + } + + /** + * @dataProvider providerAsis + * + * @param float|int $expectedResult + * @param float|int $value + * @param string $format + */ + public function testDefault($expectedResult, $value, ?string $format = null): void + { + //Cell::setCalculateDateTimeType(Cell::CALCULATE_DATE_TIME_ASIS); + $this->spreadsheet = new Spreadsheet(); + self::assertSame(0, $this->spreadsheet->getActiveSheetIndex()); + $sheet = $this->spreadsheet->getActiveSheet(); + $newSheet = $this->spreadsheet->createSheet(); + $newSheet->getCell('B7')->setValue('Here'); + $sheet->getCell('A1')->setValue($value); + if ($format !== null) { + $sheet->getStyle('A1')->getNumberFormat()->setFormatCode($format); + } + $sheet->setSelectedCells('B7'); + $this->spreadsheet->setActiveSheetIndex(1); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame('B7', $sheet->getSelectedCells()); + self::assertSame(1, $this->spreadsheet->getActiveSheetIndex()); + } + + /** + * @dataProvider providerAsis + * + * @param float|int $expectedResult + * @param float|int $value + * @param string $format + */ + public function testAsis($expectedResult, $value, ?string $format = null): void + { + Cell::setCalculateDateTimeType(Cell::CALCULATE_DATE_TIME_ASIS); + $this->spreadsheet = new Spreadsheet(); + self::assertSame(0, $this->spreadsheet->getActiveSheetIndex()); + $sheet = $this->spreadsheet->getActiveSheet(); + $newSheet = $this->spreadsheet->createSheet(); + $newSheet->getCell('B7')->setValue('Here'); + $sheet->getCell('A1')->setValue($value); + if ($format !== null) { + $sheet->getStyle('A1')->getNumberFormat()->setFormatCode($format); + } + $sheet->setSelectedCells('B7'); + $this->spreadsheet->setActiveSheetIndex(1); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame('B7', $sheet->getSelectedCells()); + self::assertSame(1, $this->spreadsheet->getActiveSheetIndex()); + } + + public function providerAsis(): array + { + $integerValue = 44046; + $integerValueAsFloat = (float) $integerValue; + $integerValueAsDateFormula = '=DATEVALUE("2020-08-03")'; + $floatValue = 44015.25; + $floatValueAsDateFormula = '=DATEVALUE("2020-07-03")+TIMEVALUE("06:00")'; + + return [ + 'default format integer' => [$integerValue, $integerValue], + 'default format float' => [$floatValue, $floatValue], + 'date format integer' => [$integerValue, $integerValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'date format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_YYYYMMDD], + 'datetime format integer' => [$integerValue, $integerValue, 'yyyy-mm-dd h:mm'], + 'datetime format float' => [$floatValue, $floatValue, 'yyyy-mm-dd h:mm'], + 'time format integer' => [$integerValue, $integerValue, NumberFormat::FORMAT_DATE_TIME1], + 'time format float' => [$floatValue, $floatValue, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer fltfmt' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula float' => [$floatValue, $floatValueAsDateFormula, NumberFormat::FORMAT_DATE_TIME1], + 'date formula integer intfmt but formula returns float' => [$integerValueAsFloat, $integerValueAsDateFormula, NumberFormat::FORMAT_DATE_YYYYMMDD], + ]; + } +}