Skip to content

Commit

Permalink
Permit Date/Time Entered on Spreadsheet to be Calculated as Float (#3121
Browse files Browse the repository at this point in the history
)

* 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.
  • Loading branch information
oleibman authored Oct 19, 2022
1 parent 6842ccb commit e4e99b8
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 101 deletions.
13 changes: 13 additions & 0 deletions docs/topics/calculation-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 0 additions & 36 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion samples/Autofilter/10_Autofilter_selection_1.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
$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');
Expand Down
2 changes: 1 addition & 1 deletion samples/Autofilter/10_Autofilter_selection_2.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
$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');
Expand Down
2 changes: 1 addition & 1 deletion samples/Autofilter/10_Autofilter_selection_display.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
$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');
Expand Down
2 changes: 1 addition & 1 deletion samples/Basic/02_Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
$spreadsheet->getActiveSheet()
->getStyle('C9')
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD);

$spreadsheet->getActiveSheet()
->setCellValue('A10', 'Date/Time')
Expand Down
114 changes: 96 additions & 18 deletions src/PhpSpreadsheet/Cell/Cell.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Throwable;

class Cell
{
Expand Down Expand Up @@ -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;

Expand All @@ -65,6 +64,8 @@ class Cell

/**
* Attributes of the formula.
*
* @var mixed
*/
private $formulaAttributes;

Expand All @@ -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;
}

Expand Down Expand Up @@ -122,7 +126,12 @@ public function __construct($value, ?string $dataType, Worksheet $worksheet)
*/
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();
}

/**
Expand All @@ -132,7 +141,12 @@ public function getColumn()
*/
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();
}

/**
Expand All @@ -142,9 +156,10 @@ public function getRow()
*/
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) {
Expand All @@ -171,8 +186,7 @@ public function getFormattedValue(): string
{
return (string) NumberFormat::toFormattedString(
$this->getCalculatedValue(),
$this->getStyle()
->getNumberFormat()->getFormatCode()
(string) $this->getStyle()->getNumberFormat()->getFormatCode()
);
}

Expand Down Expand Up @@ -251,8 +265,6 @@ public function setValueExplicit($value, $dataType)
break;
default:
throw new Exception('Invalid datatype: ' . $dataType);

break;
}

// set the datatype
Expand All @@ -261,6 +273,56 @@ public function setValueExplicit($value, $dataType)
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.
*
Expand All @@ -277,6 +339,7 @@ public function getCalculatedValue(bool $resetLog = true)
$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
Expand Down Expand Up @@ -306,7 +369,7 @@ public function getCalculatedValue(bool $resetLog = true)
return $this->value->getPlainText();
}

return $this->value;
return $this->convertDateTimeInt($this->value);
}

/**
Expand Down Expand Up @@ -458,7 +521,7 @@ public function setHyperlink(?Hyperlink $hyperlink = null): self
/**
* Get cell collection.
*
* @return Cells
* @return ?Cells
*/
public function getParent()
{
Expand All @@ -470,9 +533,10 @@ public function getParent()
*/
public function getWorksheet(): Worksheet
{
try {
$worksheet = $this->parent->getParent();
} catch (Throwable $e) {
$parent = $this->parent;
if ($parent !== null) {
$worksheet = $parent->getParent();
} else {
$worksheet = null;
}

Expand All @@ -483,6 +547,18 @@ public function getWorksheet(): Worksheet
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.
*/
Expand Down Expand Up @@ -666,6 +742,8 @@ public function setFormulaAttributes($attributes): self

/**
* Get the formula attributes.
*
* @return mixed
*/
public function getFormulaAttributes()
{
Expand Down
Loading

0 comments on commit e4e99b8

Please sign in to comment.