Document Properties - Coverage and 32-bit-safe Timestamps (#2113)

* 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%.

<!-- end of coverage changes list -->

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.

<!-- end of timestamp changes list -->

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).

<!-- end of other changes list -->

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

6 baseline deletions, 2 docblock changes

* Scrutinizer's Turn

3 minor errors that hadn't blocked the request.
This commit is contained in:
oleibman 2021-05-30 04:55:58 -07:00 committed by GitHub
parent b533f43f75
commit e53a2b2e0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 649 additions and 488 deletions

View File

@ -2165,36 +2165,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\\\\HashTable\\:\\:getIndexForHashCode\\(\\) should return int but returns int\\|string\\|false\\.$#"
count: 1
@ -2640,16 +2610,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
@ -2860,61 +2820,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
@ -4520,16 +4425,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
@ -4560,16 +4455,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
@ -4640,16 +4525,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
@ -4795,11 +4670,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
@ -7545,31 +7415,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

View File

@ -61,4 +61,4 @@ $spreadsheet->getActiveSheet()
->setTitle('Simple');
// Save
$helper->write($spreadsheet, __FILE__);
$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Ods']);

View File

@ -1,6 +1,7 @@
<?php
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date;
require __DIR__ . '/../Header.php';
@ -18,9 +19,8 @@ $helper->log('<b>Document Creator: </b>' . $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</\s\up> F Y', $creationDatestamp);
$creationTime = date('g:i A', $creationDatestamp);
$creationDate = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'l, d<\s\up>S</\s\up> F Y');
$creationTime = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'g:i A');
$helper->log('<b>Created On: </b>' . $creationDate . ' at ' . $creationTime);
// Read the name of the last person to modify this workbook
@ -30,8 +30,8 @@ $helper->log('<b>Last Modified By: </b>' . $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</\s\up> F Y', $modifiedDatestamp);
$modifiedTime = date('g:i A', $modifiedDatestamp);
$modifiedDate = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'l, d<\s\up>S</\s\up> F Y');
$modifiedTime = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'g:i A');
$helper->log('<b>Last Modified On: </b>' . $modifiedDate . ' at ' . $modifiedTime);
// Read the workbook title property

View File

@ -4687,7 +4687,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])) {
@ -4765,7 +4765,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);
}
@ -4855,7 +4855,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)) {

View File

@ -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((string) $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
}

View File

@ -1308,7 +1308,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)
{

View File

@ -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)
{
@ -37,8 +38,9 @@ class Permutations
if ($numObjs < $numInSet) {
return Functions::NAN();
}
$result = round(MathTrig\Factorial::fact($numObjs) / MathTrig\Factorial::fact($numObjs - $numInSet));
return (int) round(MathTrig\Factorial::fact($numObjs) / MathTrig\Factorial::fact($numObjs - $numInSet));
return IntOrFloat::evaluate($result);
}
/**
@ -50,7 +52,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 +70,8 @@ class Permutations
return Functions::NAN();
}
return (int) ($numObjs ** $numInSet);
$result = $numObjs ** $numInSet;
return IntOrFloat::evaluate($result);
}
}

View File

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

View File

@ -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.");
@ -502,8 +498,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')

View File

@ -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':

View File

@ -1455,34 +1455,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
@ -1607,11 +1607,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
@ -1648,11 +1648,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

View File

@ -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')));

View File

@ -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,19 @@ class Date
return $day;
}
public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime
{
$dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime();
$dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone());
return $dtobj;
}
public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string
{
$dtobj = self::dateTimeFromTimestamp($date, $timeZone);
return $dtobj->format($format);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
class IntOrFloat
{
/**
* Help some functions with large results operate correctly on 32-bit,
* by returning result as int when possible, float otherwise.
*
* @param float|int $value
*
* @return float|int
*/
public static function evaluate($value)
{
$iValue = (int) $value;
return ($value == $iValue) ? $iValue : $value;
}
}

View File

@ -491,7 +491,7 @@ class OLE
* Utility function
* Returns a string for the OLE container with the date given.
*
* @param int $date A timestamp
* @param float|int $date A timestamp
*
* @return string The string for the OLE container
*/
@ -500,6 +500,7 @@ class OLE
if (!$date) {
return "\x00\x00\x00\x00\x00\x00\x00\x00";
}
$dateTime = Date::dateTimeFromTimestamp("$date");
// factor used for separating numbers into 4 bytes parts
$factor = 2 ** 32;
@ -507,7 +508,7 @@ class OLE
// days from 1-1-1601 until the beggining of UNIX era
$days = 134774;
// calculate seconds
$big_date = $days * 24 * 3600 + mktime((int) date('H', $date), (int) date('i', $date), (int) date('s', $date), (int) date('m', $date), (int) date('d', $date), (int) date('Y', $date));
$big_date = $days * 24 * 3600 + (float) $dateTime->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);
}
}

View File

@ -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 != '') {

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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);
//}
//<meta:document-statistic meta:table-count="XXX" meta:cell-count="XXX" meta:object-count="XXX"/>
$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 static 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 ?? 0);
$objWriter->writeRawData($dtobj->format(DATE_W3C));
break;
default:
$objWriter->writeRawData($propertyValue);
break;
}
$objWriter->endElement();
}
}
}

View File

@ -766,7 +766,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[] = [

View File

@ -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;

View File

@ -0,0 +1,94 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Document;
use DateTime;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class EpochTest extends AbstractFunctional
{
public function providerFormats(): array
{
return [
['Ods', '2021-03-17 11:30:00Z'],
['Ods', '2041-03-17 11:30:00Z'],
['Xls', '2021-03-17 11:30:00Z'],
['Xls', '2041-03-17 11:30:00Z'],
['Xlsx', '2021-03-17 11:30:00Z'],
['Xlsx', '2041-03-17 11:30:00Z'],
];
}
/**
* @dataProvider providerFormats
*/
public function testSetCreated(string $format, string $timestamp): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->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);
}
}

View File

@ -2,7 +2,9 @@
namespace PhpOffice\PhpSpreadsheetTests\Document;
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PHPUnit\Framework\TestCase;
class PropertiesTest extends TestCase
@ -23,7 +25,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 +161,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', new DateTimeZone('UTC'));
}
self::assertSame($expectedValue, $result);
}
public function providerCustomProperties(): array

View File

@ -0,0 +1,112 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class OdsPropertiesTest extends AbstractFunctional
{
/**
* @var string
*/
private $timeZone;
protected function setUp(): void
{
$this->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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class PropertiesTest extends AbstractFunctional
{
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));
$result = $properties->getCustomPropertyValue($propertyName);
if ($properties->getCustomPropertyType($propertyName) == Properties::PROPERTY_TYPE_DATE) {
$result = Date::formattedDateTimeFromTimestamp("$result", 'Y-m-d', new DateTimeZone('UTC'));
}
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', new DateTimeZone('UTC'));
}
self::assertSame($testData['value'], $result);
}
}
}

View File

@ -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';

View File

@ -21,7 +21,7 @@ class DateTest extends TestCase
protected function setUp(): void
{
$this->dttimezone = Date::getDefaultTimeZone();
$this->dttimezone = Date::getDefaultTimeZoneOrNull();
$this->excelCalendar = Date::getExcelCalendar();
}

View File

@ -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);
}
}
}

View File

@ -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);