From ae80c12ef07cc5dbdc272627dd056622230a64fa Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sun, 16 May 2021 06:05:02 -0700 Subject: [PATCH 01/21] CSV Reader Enhancements This PR came about as I pondered how feasible it was to change the default escape character from backslash to null string, since the latter emulates Excel's own actions. Also, surveying issues relating to CSV, it seems that people are often in a situation where the current defaults aren't optimal for them (e.g. they are in a region where semicolon rather than comma is a better default delimiter). My case and that case can both be handled by methods after a reader is constructed. However, the issues also show that many use `IOFactory::load` rather than `new Csv()`, and the methods to affect the defaults are not available in that case. Adding a static callback that can be invoked by the constructor addresses all these problems. This can be set as part of the user application's normal initialization, and no special attention needs to be paid to CSV loads thereafter, no matter how they are invoked. This also makes it feasible to use 'guess' as inputEncoding, by providing a new setFallbackEncoding (default CP1252) method to use if none of the heuristic tests pass. There was already the ability to guess the encoding before `$reader->load()`, but not before `IOFactory::load`. Almost all typehints in Reader/Csv and Reader/Csv/Delimiter are now part of the function signature rather than in the DocBlock. The exceptions are one method in Delimiter which uses a `resource` parameter, and the `canRead` and `load` methods, which must match the signature in IOFactory. I will look into changing those later. The Csv Reader tests are moved into their own directory. All Phpstan baseline entries involving Csv Reader are eliminated. --- docs/topics/reading-and-writing-to-file.md | 35 ++++ phpstan-baseline.neon | 130 ------------ src/PhpSpreadsheet/Reader/Csv.php | 193 +++++++++--------- src/PhpSpreadsheet/Reader/Csv/Delimiter.php | 29 ++- .../Reader/Csv/CsvCallbackTest.php | 93 +++++++++ .../Reader/{ => Csv}/CsvContiguousFilter.php | 8 +- .../Reader/{ => Csv}/CsvContiguousTest.php | 21 +- .../Reader/Csv/CsvEncodingTest.php | 122 +++++++++++ .../Reader/{ => Csv}/CsvTest.php | 92 +-------- 9 files changed, 380 insertions(+), 343 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Csv/CsvCallbackTest.php rename tests/PhpSpreadsheetTests/Reader/{ => Csv}/CsvContiguousFilter.php (86%) rename tests/PhpSpreadsheetTests/Reader/{ => Csv}/CsvContiguousTest.php (82%) create mode 100644 tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php rename tests/PhpSpreadsheetTests/Reader/{ => Csv}/CsvTest.php (69%) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index a9f767aa..5340bd0a 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -477,6 +477,41 @@ $reader->setSheetIndex(0); $spreadsheet = $reader->load('sample.csv'); ``` +You can also set the reader to guess the encoding +rather than calling guessEncoding directly. In this case, +the user-settable fallback encoding is used if nothing else works. + +```php +$reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv(); +$reader->setInputEncoding(\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING); +$reader->setFallbackEncoding('ISO-8859-2'); // default CP1252 without this statement +$reader->setDelimiter(';'); +$reader->setEnclosure(''); +$reader->setSheetIndex(0); + +$spreadsheet = $reader->load('sample.csv'); +``` + +Finally, you can set a callback to be invoked when the constructor is executed, +either through `new Csv()` or `IOFactory::load`, +and have that callback set the customizable attributes to whatever +defaults are appropriate for your environment. + +```php +function constructorCallback(\PhpOffice\PhpSpreadsheet\Reader\Csv $reader): void +{ + $reader->setInputEncoding(\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING); + $reader->setFallbackEncoding('ISO-8859-2'); + $reader->setDelimiter(','); + $reader->setEnclosure('"'); + // Following represents how Excel behaves better than the default escape character + $reader->setEscapeCharacter((version_compare(PHP_VERSION, '7.4') < 0) ? "\x0" : ''); +} + +\PhpOffice\PhpSpreadsheet\Reader\Csv::setConstructorCallback('constructorCallback'); +$spreadsheet = \PhpSpreadsheet\IOFactory::load('sample.csv'); +``` + #### Read a specific worksheet CSV files can only contain one worksheet. Therefore, you can specify diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6390e0fc..25f47c4a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2730,101 +2730,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/BaseReader.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\:\\:\\$delimiter \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Parameter \\#1 \\$var of function count expects array\\|Countable, array\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\:\\:openFileOrMemory\\(\\) has parameter \\$pFilename with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Parameter \\#1 \\$value of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:convertEncoding\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Parameter \\#1 \\$fp of function fwrite expects resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Parameter \\#2 \\$newvalue of function ini_set expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Call to function is_array\\(\\) with string will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$fileHandle has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$escapeCharacter has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$enclosure has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$counts has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$numberLines has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:\\$delimiter has no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:__construct\\(\\) has parameter \\$enclosure with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:__construct\\(\\) has parameter \\$escapeCharacter with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:__construct\\(\\) has parameter \\$fileHandle with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Csv\\\\Delimiter\\:\\:getNextLine\\(\\) should return string\\|false but returns string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Csv/Delimiter.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Gnumeric\\:\\:\\$referenceHelper has no typehint specified\\.$#" count: 1 @@ -8290,41 +8195,6 @@ parameters: count: 5 path: tests/PhpSpreadsheetTests/NamedRangeTest.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\CsvContiguousFilter\\:\\:setFilterType\\(\\) has parameter \\$type with no typehint specified\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\CsvContiguousFilter\\:\\:filter1\\(\\) has no return typehint specified\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\CsvContiguousFilter\\:\\:filter1\\(\\) has parameter \\$row with no typehint specified\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\CsvContiguousFilter\\:\\:filter0\\(\\) has no return typehint specified\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Reader\\\\CsvContiguousFilter\\:\\:filter0\\(\\) has parameter \\$row with no typehint specified\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php - - - - message: "#^Cannot call method getCell\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 3 - path: tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php - - - - message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNull\\(\\) with string will always evaluate to false\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Reader/CsvTest.php - - message: "#^Cannot call method getVisible\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\RowDimension\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index dc746735..b7bc0d49 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -10,6 +10,8 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; class Csv extends BaseReader { + const DEFAULT_FALLBACK_ENCODING = 'CP1252'; + const GUESS_ENCODING = 'guess'; const UTF8_BOM = "\xEF\xBB\xBF"; const UTF8_BOM_LEN = 3; const UTF16BE_BOM = "\xfe\xff"; @@ -33,10 +35,17 @@ class Csv extends BaseReader private $inputEncoding = 'UTF-8'; /** - * Delimiter. + * Fallback encoding if 'guess' strikes out. * * @var string */ + private $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING; + + /** + * Delimiter. + * + * @var ?string + */ private $delimiter; /** @@ -67,38 +76,65 @@ class Csv extends BaseReader */ private $escapeCharacter = '\\'; + /** + * Callback for setting defaults in construction. + * + * @var ?callable + */ + private static $constructorCallback; + /** * Create a new CSV Reader instance. */ public function __construct() { parent::__construct(); + $callback = self::$constructorCallback; + if ($callback !== null) { + $callback($this); + } } /** - * Set input encoding. + * Set a callback to change the defaults. * - * @param string $pValue Input encoding, eg: 'UTF-8' - * - * @return $this + * The callback must accept the Csv Reader object as the first parameter, + * and it should return void. */ - public function setInputEncoding($pValue) + public static function setConstructorCallback(?callable $callback): void + { + self::$constructorCallback = $callback; + } + + public static function getConstructorCallback(): ?callable + { + return self::$constructorCallback; + } + + public function setInputEncoding(string $pValue): self { $this->inputEncoding = $pValue; return $this; } - /** - * Get input encoding. - * - * @return string - */ - public function getInputEncoding() + public function getInputEncoding(): string { return $this->inputEncoding; } + public function setFallbackEncoding(string $pValue): self + { + $this->fallbackEncoding = $pValue; + + return $this; + } + + public function getFallbackEncoding(): string + { + return $this->fallbackEncoding; + } + /** * Move filepointer past any BOM marker. */ @@ -161,12 +197,8 @@ class Csv extends BaseReader /** * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). - * - * @param string $pFilename - * - * @return array */ - public function listWorksheetInfo($pFilename) + public function listWorksheetInfo(string $pFilename): array { // Open file $this->openFileOrMemory($pFilename); @@ -185,9 +217,11 @@ class Csv extends BaseReader $worksheetInfo[0]['totalColumns'] = 0; // Loop through each line of the file in turn - while (($rowData = fgetcsv($fileHandle, 0, $this->delimiter, $this->enclosure, $this->escapeCharacter)) !== false) { + $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + while (is_array($rowData)) { ++$worksheetInfo[0]['totalRows']; $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); + $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); } $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); @@ -215,34 +249,35 @@ class Csv extends BaseReader return $this->loadIntoExisting($pFilename, $spreadsheet); } - private function openFileOrMemory($pFilename): void + private function openFileOrMemory(string $pFilename): void { // Open file $fhandle = $this->canRead($pFilename); if (!$fhandle) { throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); } + if ($this->inputEncoding === self::GUESS_ENCODING) { + $this->inputEncoding = self::guessEncoding($pFilename, $this->fallbackEncoding); + } $this->openFile($pFilename); if ($this->inputEncoding !== 'UTF-8') { fclose($this->fileHandle); $entireFile = file_get_contents($pFilename); $this->fileHandle = fopen('php://memory', 'r+b'); - $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); - fwrite($this->fileHandle, $data); - $this->skipBOM(); + if ($this->fileHandle !== false && $entireFile !== false) { + $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); + fwrite($this->fileHandle, $data); + $this->skipBOM(); + } } } /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. - * - * @param string $pFilename - * - * @return Spreadsheet */ - public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) + public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): Spreadsheet { - $lineEnding = ini_get('auto_detect_line_endings'); + $lineEnding = ini_get('auto_detect_line_endings') ?: '0'; ini_set('auto_detect_line_endings', '1'); // Open file @@ -265,7 +300,8 @@ class Csv extends BaseReader $outRow = 0; // Loop through each line of the file in turn - while (($rowData = fgetcsv($fileHandle, 0, $this->delimiter, $this->enclosure, $this->escapeCharacter)) !== false) { + $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); + while (is_array($rowData)) { $noOutputYet = true; $columnLetter = 'A'; foreach ($rowData as $rowDatum) { @@ -283,6 +319,7 @@ class Csv extends BaseReader } ++$columnLetter; } + $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); ++$currentRow; } @@ -295,48 +332,24 @@ class Csv extends BaseReader return $spreadsheet; } - /** - * Get delimiter. - * - * @return string - */ - public function getDelimiter() + public function getDelimiter(): ?string { return $this->delimiter; } - /** - * Set delimiter. - * - * @param string $delimiter Delimiter, eg: ',' - * - * @return $this - */ - public function setDelimiter($delimiter) + public function setDelimiter(string $delimiter): self { $this->delimiter = $delimiter; return $this; } - /** - * Get enclosure. - * - * @return string - */ - public function getEnclosure() + public function getEnclosure(): string { return $this->enclosure; } - /** - * Set enclosure. - * - * @param string $enclosure Enclosure, defaults to " - * - * @return $this - */ - public function setEnclosure($enclosure) + public function setEnclosure(string $enclosure): self { if ($enclosure == '') { $enclosure = '"'; @@ -346,78 +359,55 @@ class Csv extends BaseReader return $this; } - /** - * Get sheet index. - * - * @return int - */ - public function getSheetIndex() + public function getSheetIndex(): int { return $this->sheetIndex; } - /** - * Set sheet index. - * - * @param int $pValue Sheet index - * - * @return $this - */ - public function setSheetIndex($pValue) + public function setSheetIndex(int $pValue): self { $this->sheetIndex = $pValue; return $this; } - /** - * Set Contiguous. - * - * @param bool $contiguous - * - * @return $this - */ - public function setContiguous($contiguous) + public function setContiguous(bool $contiguous): self { $this->contiguous = (bool) $contiguous; return $this; } - /** - * Get Contiguous. - * - * @return bool - */ - public function getContiguous() + public function getContiguous(): bool { return $this->contiguous; } - /** - * Set escape backslashes. - * - * @param string $escapeCharacter - * - * @return $this - */ - public function setEscapeCharacter($escapeCharacter) + public function setEscapeCharacter(string $escapeCharacter): self { $this->escapeCharacter = $escapeCharacter; return $this; } - /** - * Get escape backslashes. - * - * @return string - */ - public function getEscapeCharacter() + public function getEscapeCharacter(): string { return $this->escapeCharacter; } + /** + * Scrutinizer believes, incorrectly, that the specific pathinfo + * call in canRead can return something other than an array. + * Phpstan knows better. + * This function satisfies both. + * + * @param mixed $extension + */ + private static function extractStringLower($extension): string + { + return is_string($extension) ? strtolower($extension) : ''; + } + /** * Can the current IReader read the file? * @@ -437,8 +427,7 @@ class Csv extends BaseReader fclose($this->fileHandle); // Trust file extension if any - $extension = pathinfo($pFilename, PATHINFO_EXTENSION); - $extension = is_array($extension) ? '' : strtolower($extension); + $extension = self::extractStringLower(pathinfo($pFilename, PATHINFO_EXTENSION)); if (in_array($extension, ['csv', 'tsv'])) { return true; } @@ -504,7 +493,7 @@ class Csv extends BaseReader return $encoding; } - public static function guessEncoding(string $filename, string $dflt = 'CP1252'): string + public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string { $encoding = self::guessEncodingBom($filename); if ($encoding === '') { diff --git a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php index eb62c9ac..fc298957 100644 --- a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php +++ b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -6,19 +6,28 @@ class Delimiter { protected const POTENTIAL_DELIMETERS = [',', ';', "\t", '|', ':', ' ', '~']; + /** @var resource */ protected $fileHandle; + /** @var string */ protected $escapeCharacter; + /** @var string */ protected $enclosure; + /** @var array */ protected $counts = []; + /** @var int */ protected $numberLines = 0; + /** @var ?string */ protected $delimiter; - public function __construct($fileHandle, $escapeCharacter, $enclosure) + /** + * @param resource $fileHandle + */ + public function __construct($fileHandle, string $escapeCharacter, string $enclosure) { $this->fileHandle = $fileHandle; $this->escapeCharacter = $escapeCharacter; @@ -52,15 +61,13 @@ class Delimiter protected function countDelimiterValues(string $line, array $delimiterKeys): void { $splitString = str_split($line, 1); - if (!is_array($splitString)) { - return; - } + if (is_array($splitString)) { + $distribution = array_count_values($splitString); + $countLine = array_intersect_key($distribution, $delimiterKeys); - $distribution = array_count_values($splitString); - $countLine = array_intersect_key($distribution, $delimiterKeys); - - foreach (self::POTENTIAL_DELIMETERS as $delimiter) { - $this->counts[$delimiter][] = $countLine[$delimiter] ?? 0; + foreach (self::POTENTIAL_DELIMETERS as $delimiter) { + $this->counts[$delimiter][] = $countLine[$delimiter] ?? 0; + } } } @@ -137,8 +144,8 @@ class Delimiter // See if we have any enclosures left in the line // if we still have an enclosure then we need to read the next line as well - } while (preg_match('/(' . $enclosure . ')/', $line) > 0); + } while (preg_match('/(' . $enclosure . ')/', $line ?? '') > 0); - return $line; + return $line ?? false; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvCallbackTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvCallbackTest.php new file mode 100644 index 00000000..c27d3b71 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvCallbackTest.php @@ -0,0 +1,93 @@ +setInputEncoding(Csv::GUESS_ENCODING); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('Å', $sheet->getCell('A1')->getValue()); + } + + public function callbackSetFallbackEncoding(Csv $reader): void + { + $reader->setFallbackEncoding('ISO-8859-2'); + $reader->setInputEncoding(Csv::GUESS_ENCODING); + $reader->setEscapeCharacter((version_compare(PHP_VERSION, '7.4') < 0) ? "\x0" : ''); + } + + public function testFallbackEncodingDefltIso2(): void + { + Csv::setConstructorCallback([$this, 'callbackSetFallbackEncoding']); + $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; + $reader = new Csv(); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); + } + + public function testIOFactory(): void + { + Csv::setConstructorCallback([$this, 'callbackSetFallbackEncoding']); + $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; + $spreadsheet = IOFactory::load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); + } + + public function testNonFallbackEncoding(): void + { + Csv::setConstructorCallback([$this, 'callbackSetFallbackEncoding']); + $filename = 'tests/data/Reader/CSV/premiere.utf16be.csv'; + $reader = new Csv(); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('première', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); + } + + public function testDefaultEscape(): void + { + self::assertNull(Csv::getConstructorCallback()); + $filename = 'tests/data/Reader/CSV/escape.csv'; + $spreadsheet = IOFactory::load($filename); + $sheet = $spreadsheet->getActiveSheet(); + // this is not how Excel views the file + self::assertEquals('a\"hello', $sheet->getCell('A1')->getValue()); + } + + public function testBetterEscape(): void + { + Csv::setConstructorCallback([$this, 'callbackSetFallbackEncoding']); + $filename = 'tests/data/Reader/CSV/escape.csv'; + $spreadsheet = IOFactory::load($filename); + $sheet = $spreadsheet->getActiveSheet(); + // this is how Excel views the file + self::assertEquals('a\"hello;hello;hello;\"', $sheet->getCell('A1')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousFilter.php similarity index 86% rename from tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php rename to tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousFilter.php index 1abe9940..346a6558 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvContiguousFilter.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousFilter.php @@ -1,6 +1,6 @@ endRow = $startRow + $chunkSize; } - public function setFilterType($type): void + public function setFilterType(int $type): void { $this->filterType = $type; } - public function filter1($row) + public function filter1(int $row): bool { // Include rows 1-10, followed by 100-110, etc. return $row % 100 <= 10; } - public function filter0($row) + public function filter0(int $row): bool { // Only read the heading row, and the rows that are configured in $this->_startRow and $this->_endRow if (($row == 1) || ($row >= $this->startRow && $row < $this->endRow)) { diff --git a/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php similarity index 82% rename from tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php rename to tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php index 82f960e4..123ff887 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php @@ -1,6 +1,6 @@ getActiveSheet()->setTitle('Country Data #' . (++$sheet)); } - $sheet = $spreadsheet->getSheetByName('Country Data #1'); - self::assertEquals('Kabul', $sheet->getCell('A2')->getValue()); - $sheet = $spreadsheet->getSheetByName('Country Data #2'); - self::assertEquals('Lesotho', $sheet->getCell('B4')->getValue()); - $sheet = $spreadsheet->getSheetByName('Country Data #3'); - self::assertEquals(-20.1, $sheet->getCell('C6')->getValue()); + self::assertSame('Kabul', self::getCellValue($spreadsheet, 'Country Data #1', 'A2')); + self::assertSame('Lesotho', self::getCellValue($spreadsheet, 'Country Data #2', 'B4')); + self::assertSame('-20.1', self::getCellValue($spreadsheet, 'Country Data #3', 'C6')); + } + + private function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string + { + $sheet = $spreadsheet->getSheetByName($sheetName); + if ($sheet === null) { + return ''; + } + + return (string) $sheet->getCell($cellAddress)->getValue(); } public function testContiguous2(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php new file mode 100644 index 00000000..448d3d1e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php @@ -0,0 +1,122 @@ +setInputEncoding($encoding); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('Å', $sheet->getCell('A1')->getValue()); + } + + /** + * @dataProvider providerEncodings + * + * @param string $filename + * @param string $encoding + */ + public function testWorkSheetInfo($filename, $encoding): void + { + $reader = new Csv(); + $reader->setInputEncoding($encoding); + $info = $reader->listWorksheetInfo($filename); + self::assertEquals('Worksheet', $info[0]['worksheetName']); + self::assertEquals('B', $info[0]['lastColumnLetter']); + self::assertEquals(1, $info[0]['lastColumnIndex']); + self::assertEquals(2, $info[0]['totalRows']); + self::assertEquals(2, $info[0]['totalColumns']); + } + + public function providerEncodings(): array + { + return [ + ['tests/data/Reader/CSV/encoding.iso88591.csv', 'ISO-8859-1'], + ['tests/data/Reader/CSV/encoding.utf8.csv', 'UTF-8'], + ['tests/data/Reader/CSV/encoding.utf8bom.csv', 'UTF-8'], + ['tests/data/Reader/CSV/encoding.utf16be.csv', 'UTF-16BE'], + ['tests/data/Reader/CSV/encoding.utf16le.csv', 'UTF-16LE'], + ['tests/data/Reader/CSV/encoding.utf32be.csv', 'UTF-32BE'], + ['tests/data/Reader/CSV/encoding.utf32le.csv', 'UTF-32LE'], + ]; + } + + /** + * @dataProvider providerGuessEncoding + */ + public function testGuessEncoding(string $filename): void + { + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename)); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('première', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); + } + + /** + * @dataProvider providerGuessEncoding + */ + public function testFallbackEncoding(string $filename): void + { + $reader = new Csv(); + $reader->setInputEncoding(Csv::GUESS_ENCODING); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('première', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); + } + + public function providerGuessEncoding(): array + { + return [ + ['tests/data/Reader/CSV/premiere.utf8.csv'], + ['tests/data/Reader/CSV/premiere.utf8bom.csv'], + ['tests/data/Reader/CSV/premiere.utf16be.csv'], + ['tests/data/Reader/CSV/premiere.utf16bebom.csv'], + ['tests/data/Reader/CSV/premiere.utf16le.csv'], + ['tests/data/Reader/CSV/premiere.utf16lebom.csv'], + ['tests/data/Reader/CSV/premiere.utf32be.csv'], + ['tests/data/Reader/CSV/premiere.utf32bebom.csv'], + ['tests/data/Reader/CSV/premiere.utf32le.csv'], + ['tests/data/Reader/CSV/premiere.utf32lebom.csv'], + ['tests/data/Reader/CSV/premiere.win1252.csv'], + ]; + } + + public function testGuessEncodingDefltIso2(): void + { + $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename, 'ISO-8859-2')); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); + } + + public function testFallbackEncodingDefltIso2(): void + { + $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; + $reader = new Csv(); + self::assertSame('CP1252', $reader->getFallbackEncoding()); + $reader->setInputEncoding(Csv::GUESS_ENCODING); + $reader->setFallbackEncoding('ISO-8859-2'); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); + self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/CsvTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php similarity index 69% rename from tests/PhpSpreadsheetTests/Reader/CsvTest.php rename to tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php index 73c281ec..b29655fb 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvTest.php @@ -1,6 +1,6 @@ getDelimiter()); + $delim1 = $reader->getDelimiter(); + self::assertNull($delim1); $spreadsheet = $reader->load($filename); @@ -132,21 +133,6 @@ class CsvTest extends TestCase self::assertSame($expected, $worksheet->toArray()); } - /** - * @dataProvider providerEncodings - * - * @param string $filename - * @param string $encoding - */ - public function testEncodings($filename, $encoding): void - { - $reader = new Csv(); - $reader->setInputEncoding($encoding); - $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getActiveSheet(); - self::assertEquals('Å', $sheet->getCell('A1')->getValue()); - } - public function testInvalidWorkSheetInfo(): void { $this->expectException(ReaderException::class); @@ -154,37 +140,6 @@ class CsvTest extends TestCase $reader->listWorksheetInfo(''); } - /** - * @dataProvider providerEncodings - * - * @param string $filename - * @param string $encoding - */ - public function testWorkSheetInfo($filename, $encoding): void - { - $reader = new Csv(); - $reader->setInputEncoding($encoding); - $info = $reader->listWorksheetInfo($filename); - self::assertEquals('Worksheet', $info[0]['worksheetName']); - self::assertEquals('B', $info[0]['lastColumnLetter']); - self::assertEquals(1, $info[0]['lastColumnIndex']); - self::assertEquals(2, $info[0]['totalRows']); - self::assertEquals(2, $info[0]['totalColumns']); - } - - public function providerEncodings(): array - { - return [ - ['tests/data/Reader/CSV/encoding.iso88591.csv', 'ISO-8859-1'], - ['tests/data/Reader/CSV/encoding.utf8.csv', 'UTF-8'], - ['tests/data/Reader/CSV/encoding.utf8bom.csv', 'UTF-8'], - ['tests/data/Reader/CSV/encoding.utf16be.csv', 'UTF-16BE'], - ['tests/data/Reader/CSV/encoding.utf16le.csv', 'UTF-16LE'], - ['tests/data/Reader/CSV/encoding.utf32be.csv', 'UTF-32BE'], - ['tests/data/Reader/CSV/encoding.utf32le.csv', 'UTF-32LE'], - ]; - } - public function testUtf16LineBreak(): void { $reader = new Csv(); @@ -296,45 +251,4 @@ EOF; [(version_compare(PHP_VERSION, '7.4') < 0) ? "\x0" : '', ','], ]; } - - /** - * @dataProvider providerGuessEncoding - */ - public function testGuessEncoding(string $filename): void - { - $reader = new Csv(); - $reader->setInputEncoding(Csv::guessEncoding($filename)); - $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getActiveSheet(); - self::assertEquals('première', $sheet->getCell('A1')->getValue()); - self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); - } - - public function providerGuessEncoding(): array - { - return [ - ['tests/data/Reader/CSV/premiere.utf8.csv'], - ['tests/data/Reader/CSV/premiere.utf8bom.csv'], - ['tests/data/Reader/CSV/premiere.utf16be.csv'], - ['tests/data/Reader/CSV/premiere.utf16bebom.csv'], - ['tests/data/Reader/CSV/premiere.utf16le.csv'], - ['tests/data/Reader/CSV/premiere.utf16lebom.csv'], - ['tests/data/Reader/CSV/premiere.utf32be.csv'], - ['tests/data/Reader/CSV/premiere.utf32bebom.csv'], - ['tests/data/Reader/CSV/premiere.utf32le.csv'], - ['tests/data/Reader/CSV/premiere.utf32lebom.csv'], - ['tests/data/Reader/CSV/premiere.win1252.csv'], - ]; - } - - public function testGuessEncodingDefltIso2(): void - { - $filename = 'tests/data/Reader/CSV/premiere.win1252.csv'; - $reader = new Csv(); - $reader->setInputEncoding(Csv::guessEncoding($filename, 'ISO-8859-2')); - $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getActiveSheet(); - self::assertEquals('premičre', $sheet->getCell('A1')->getValue()); - self::assertEquals('sixičme', $sheet->getCell('C2')->getValue()); - } } From 1d4e4af705ee72c3632ad4081a88ca5e0d07b49f Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 16 May 2021 10:04:46 -0700 Subject: [PATCH 02/21] Update Csv.php --- src/PhpSpreadsheet/Reader/Csv.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index b7bc0d49..f99ce0a3 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -35,7 +35,7 @@ class Csv extends BaseReader private $inputEncoding = 'UTF-8'; /** - * Fallback encoding if 'guess' strikes out. + * Fallback encoding if 'guess' fails to find a match. * * @var string */ From 294933c9e5132cba6630c50163567e8ed00f4ca2 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 16 May 2021 11:48:12 -0700 Subject: [PATCH 03/21] Update CsvContiguousTest.php --- tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php index 123ff887..ff095dba 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php @@ -54,7 +54,7 @@ class CsvContiguousTest extends TestCase self::assertSame('-20.1', self::getCellValue($spreadsheet, 'Country Data #3', 'C6')); } - private function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string + private static function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string { $sheet = $spreadsheet->getSheetByName($sheetName); if ($sheet === null) { From b9a9eb2316ba5c15a9af66ff1932bfc97b883d21 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 16 May 2021 15:02:11 -0700 Subject: [PATCH 04/21] Update Csv.php --- src/PhpSpreadsheet/Reader/Csv.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index f99ce0a3..b7bc0d49 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -35,7 +35,7 @@ class Csv extends BaseReader private $inputEncoding = 'UTF-8'; /** - * Fallback encoding if 'guess' fails to find a match. + * Fallback encoding if 'guess' strikes out. * * @var string */ From e400b35122b19aa04177d15aaea26bb3410da341 Mon Sep 17 00:00:00 2001 From: Athena Metis Date: Wed, 19 May 2021 12:57:14 +0100 Subject: [PATCH 05/21] Update reading-and-writing-to-file.md Added a note about formulas still being calculated where column autosizing is turned on, even if pre-calculation is set to false. This is true at least for the Xlsx writer but probably others to if they use calculateColumnWidths from Worksheet/Worksheet.php --- docs/topics/reading-and-writing-to-file.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index a9f767aa..6b51208e 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -162,6 +162,9 @@ $writer->setPreCalculateFormulas(false); $writer->save("05featuredemo.xlsx"); ``` +**Note** Formulas will still be calculated in any column set to be autosized +even if pre-calculated is set to false + #### Office 2003 compatibility pack Because of a bug in the Office2003 compatibility pack, there can be some From 4089aede0ab1ab143bcad936200c46ba1888e634 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 27 May 2021 11:35:01 +0200 Subject: [PATCH 06/21] Resolve default values when a null argument is passed for HLOOKUP(), VLOOKUP() and ADDRESS() functions --- CHANGELOG.md | 4 +- composer.lock | 352 ++++++++++-------- .../Calculation/LookupRef/Address.php | 3 +- .../Calculation/LookupRef/HLookup.php | 2 +- .../Calculation/LookupRef/Indirect.php | 9 +- .../Calculation/LookupRef/VLookup.php | 2 +- tests/data/Calculation/LookupRef/ADDRESS.php | 10 +- tests/data/Calculation/LookupRef/HLOOKUP.php | 28 ++ tests/data/Calculation/LookupRef/VLOOKUP.php | 15 + 9 files changed, 266 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7a2205..ade195d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Removed -- Nothing. +- Use of `nb` rather than `no` as the locale language code for Norsk Bokmål. ### Fixed - Fixed incorrect R1C1 to A1 subtraction formula conversion (`R[-2]C-R[2]C`) [Issue #2076](https://github.com/PHPOffice/PhpSpreadsheet/pull/2076) [PR #2086](https://github.com/PHPOffice/PhpSpreadsheet/pull/2086) @@ -109,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated -- Nothing. +- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. ### Removed diff --git a/composer.lock b/composer.lock index 4921cc8f..fa6bed6f 100644 --- a/composer.lock +++ b/composer.lock @@ -133,16 +133,16 @@ }, { "name": "markbaker/complex", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/MarkBaker/PHPComplex.git", - "reference": "9999f1432fae467bc93c53f357105b4c31bb994c" + "reference": "d18272926d58065140314c01e18ec3dd7ae854ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/9999f1432fae467bc93c53f357105b4c31bb994c", - "reference": "9999f1432fae467bc93c53f357105b4c31bb994c", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/d18272926d58065140314c01e18ec3dd7ae854ea", + "reference": "d18272926d58065140314c01e18ec3dd7ae854ea", "shasum": "" }, "require": { @@ -151,11 +151,7 @@ "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "phpcompatibility/php-compatibility": "^9.0", - "phpdocumentor/phpdocumentor": "2.*", - "phploc/phploc": "^4.0", - "phpmd/phpmd": "2.*", "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", - "sebastian/phpcpd": "^4.0", "squizlabs/php_codesniffer": "^3.4" }, "type": "library", @@ -226,22 +222,22 @@ ], "support": { "issues": "https://github.com/MarkBaker/PHPComplex/issues", - "source": "https://github.com/MarkBaker/PHPComplex/tree/PHP8" + "source": "https://github.com/MarkBaker/PHPComplex/tree/2.0.2" }, - "time": "2020-08-26T10:42:07+00:00" + "time": "2021-05-24T10:53:30+00:00" }, { "name": "markbaker/matrix", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/MarkBaker/PHPMatrix.git", - "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a" + "reference": "174395a901b5ba0925f1d790fa91bab531074b61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/361c0f545c3172ee26c3d596a0aa03f0cef65e6a", - "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/174395a901b5ba0925f1d790fa91bab531074b61", + "reference": "174395a901b5ba0925f1d790fa91bab531074b61", "shasum": "" }, "require": { @@ -300,32 +296,32 @@ ], "support": { "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/2.1.2" + "source": "https://github.com/MarkBaker/PHPMatrix/tree/2.1.3" }, - "time": "2021-01-23T16:37:31+00:00" + "time": "2021-05-25T15:42:17+00:00" }, { "name": "myclabs/php-enum", - "version": "1.7.7", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "d178027d1e679832db9f38248fcc7200647dc2b7" + "reference": "46cf3d8498b095bd33727b13fd5707263af99421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/d178027d1e679832db9f38248fcc7200647dc2b7", - "reference": "d178027d1e679832db9f38248fcc7200647dc2b7", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^3.8" + "vimeo/psalm": "^4.5.1" }, "type": "library", "autoload": { @@ -350,7 +346,7 @@ ], "support": { "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.7.7" + "source": "https://github.com/myclabs/php-enum/tree/1.8.0" }, "funding": [ { @@ -362,7 +358,7 @@ "type": "tidelift" } ], - "time": "2020-11-14T18:14:52+00:00" + "time": "2021-02-15T16:11:48+00:00" }, { "name": "psr/http-client", @@ -659,16 +655,16 @@ "packages-dev": [ { "name": "composer/semver", - "version": "3.2.4", + "version": "3.2.5", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9", + "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9", "shasum": "" }, "require": { @@ -720,7 +716,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.4" + "source": "https://github.com/composer/semver/tree/3.2.5" }, "funding": [ { @@ -736,20 +732,20 @@ "type": "tidelift" } ], - "time": "2020-11-13T08:59:24+00:00" + "time": "2021-05-24T12:41:47+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.6", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", + "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", "shasum": "" }, "require": { @@ -784,7 +780,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" }, "funding": [ { @@ -800,7 +796,7 @@ "type": "tidelift" } ], - "time": "2021-03-25T17:01:18+00:00" + "time": "2021-05-05T19:37:51+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -875,28 +871,30 @@ }, { "name": "doctrine/annotations", - "version": "1.12.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" + "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", + "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1 || ^8.0" + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "1.*", + "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^9.1.5" + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2" }, "type": "library", "autoload": { @@ -939,9 +937,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.12.1" + "source": "https://github.com/doctrine/annotations/tree/1.13.1" }, - "time": "2021-02-21T21:00:45+00:00" + "time": "2021-05-16T18:07:53+00:00" }, { "name": "doctrine/instantiator", @@ -1166,21 +1164,21 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.18.4", + "version": "v2.19.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31" + "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/06f764e3cb6d60822d8f5135205f9d32b5508a31", - "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/d5b8a9d852b292c2f8a035200fa6844b1f82300b", + "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b", "shasum": "" }, "require": { "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.2", + "composer/xdebug-handler": "^1.2 || ^2.0", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", @@ -1223,6 +1221,11 @@ "php-cs-fixer" ], "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.19-dev" + } + }, "autoload": { "psr-4": { "PhpCsFixer\\": "src/" @@ -1258,7 +1261,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.4" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.0" }, "funding": [ { @@ -1266,7 +1269,7 @@ "type": "github" } ], - "time": "2021-03-20T14:52:33+00:00" + "time": "2021-05-03T21:43:24+00:00" }, { "name": "jpgraph/jpgraph", @@ -1315,16 +1318,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.0.10", + "version": "v8.0.11", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "1333a962cd2f7ae1a127b7534b7734b58179186f" + "reference": "af17afbbfa0b6ce76defc8da5d02a73d54f94c64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/1333a962cd2f7ae1a127b7534b7734b58179186f", - "reference": "1333a962cd2f7ae1a127b7534b7734b58179186f", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/af17afbbfa0b6ce76defc8da5d02a73d54f94c64", + "reference": "af17afbbfa0b6ce76defc8da5d02a73d54f94c64", "shasum": "" }, "require": { @@ -1386,7 +1389,7 @@ "type": "custom" } ], - "time": "2021-01-08T14:59:28+00:00" + "time": "2021-05-12T14:18:06+00:00" }, { "name": "myclabs/deep-copy", @@ -2036,16 +2039,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.82", + "version": "0.12.88", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11" + "reference": "464d1a81af49409c41074aa6640ed0c4cbd9bb68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", - "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/464d1a81af49409c41074aa6640ed0c4cbd9bb68", + "reference": "464d1a81af49409c41074aa6640ed0c4cbd9bb68", "shasum": "" }, "require": { @@ -2076,7 +2079,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.82" + "source": "https://github.com/phpstan/phpstan/tree/0.12.88" }, "funding": [ { @@ -2092,25 +2095,25 @@ "type": "tidelift" } ], - "time": "2021-03-19T06:08:17+00:00" + "time": "2021-05-17T12:24:49+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "0.12.18", + "version": "0.12.19", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72" + "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72", - "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/52f7072ddc5f81492f9d2de65a24813a48c90b18", + "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.60" + "phpstan/phpstan": "^0.12.86" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -2145,9 +2148,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/0.12.18" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/0.12.19" }, - "time": "2021-03-06T11:51:27+00:00" + "time": "2021-04-30T11:10:37+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2388,29 +2391,29 @@ }, { "name": "phpunit/php-token-stream", - "version": "3.1.2", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "472b687829041c24b25f475e14c2f38a09edf1c2" + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2", - "reference": "472b687829041c24b25f475e14c2f38a09edf1c2", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.1" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2435,7 +2438,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2" + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" }, "funding": [ { @@ -2444,7 +2447,7 @@ } ], "abandoned": true, - "time": "2020-11-30T08:38:46+00:00" + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", @@ -2543,6 +2546,55 @@ ], "time": "2021-03-17T07:27:54+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.1.1", @@ -2643,16 +2695,16 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -2676,7 +2728,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -2687,9 +2739,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "sabberworm/php-css-parser", @@ -3543,16 +3595,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.8", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625", + "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625", "shasum": "" }, "require": { @@ -3595,20 +3647,20 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2020-10-23T02:01:07+00:00" + "time": "2021-04-09T00:54:41+00:00" }, { "name": "symfony/console", - "version": "v5.2.5", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79" + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79", - "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79", + "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768", + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768", "shasum": "" }, "require": { @@ -3676,7 +3728,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.5" + "source": "https://github.com/symfony/console/tree/v5.2.8" }, "funding": [ { @@ -3692,20 +3744,20 @@ "type": "tidelift" } ], - "time": "2021-03-06T13:42:15+00:00" + "time": "2021-05-11T15:45:21+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", "shasum": "" }, "require": { @@ -3714,7 +3766,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3743,7 +3795,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/master" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" }, "funding": [ { @@ -3759,7 +3811,7 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/event-dispatcher", @@ -3848,16 +3900,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11", + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11", "shasum": "" }, "require": { @@ -3870,7 +3922,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3907,7 +3959,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" }, "funding": [ { @@ -3923,20 +3975,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/filesystem", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108" + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/710d364200997a5afde34d9fe57bd52f3cc1e108", - "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/056e92acc21d977c37e6ea8e97374b2a6c8551b0", + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0", "shasum": "" }, "require": { @@ -3969,7 +4021,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.4" + "source": "https://github.com/symfony/filesystem/tree/v5.2.7" }, "funding": [ { @@ -3985,20 +4037,20 @@ "type": "tidelift" } ], - "time": "2021-02-12T10:38:38+00:00" + "time": "2021-04-01T10:42:13+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", "shasum": "" }, "require": { @@ -4030,7 +4082,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/finder/tree/v5.2.9" }, "funding": [ { @@ -4046,7 +4098,7 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-05-16T13:07:46+00:00" }, { "name": "symfony/options-resolver", @@ -4669,16 +4721,16 @@ }, { "name": "symfony/process", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/symfony/process/zipball/98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", "shasum": "" }, "require": { @@ -4711,7 +4763,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" + "source": "https://github.com/symfony/process/tree/v5.3.0-BETA1" }, "funding": [ { @@ -4727,25 +4779,25 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-04-08T10:27:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -4753,7 +4805,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -4790,7 +4842,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -4806,20 +4858,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c" + "reference": "d99310c33e833def36419c284f60e8027d359678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/d99310c33e833def36419c284f60e8027d359678", + "reference": "d99310c33e833def36419c284f60e8027d359678", "shasum": "" }, "require": { @@ -4852,7 +4904,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" + "source": "https://github.com/symfony/stopwatch/tree/v5.3.0-BETA1" }, "funding": [ { @@ -4868,20 +4920,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-03-29T15:28:41+00:00" }, { "name": "symfony/string", - "version": "v5.2.4", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e78d7d47061fa183639927ec40d607973699609" + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e78d7d47061fa183639927ec40d607973699609", - "reference": "4e78d7d47061fa183639927ec40d607973699609", + "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", "shasum": "" }, "require": { @@ -4935,7 +4987,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.4" + "source": "https://github.com/symfony/string/tree/v5.2.8" }, "funding": [ { @@ -4951,7 +5003,7 @@ "type": "tidelift" } ], - "time": "2021-02-16T10:20:28+00:00" + "time": "2021-05-10T14:56:10+00:00" }, { "name": "tecnickcom/tcpdf", diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index c217a1e4..58215f27 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -41,7 +41,8 @@ class Address { $row = Functions::flattenSingleValue($row); $column = Functions::flattenSingleValue($column); - $relativity = Functions::flattenSingleValue($relativity); + $relativity = ($relativity === null) ? 1 : Functions::flattenSingleValue($relativity); + $referenceStyle = ($referenceStyle === null) ? true : Functions::flattenSingleValue($referenceStyle); $sheetName = Functions::flattenSingleValue($sheetName); if (($row < 1) || ($column < 1)) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index 559fe7d1..96cfd7ab 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -25,7 +25,7 @@ class HLookup extends LookupBase { $lookupValue = Functions::flattenSingleValue($lookupValue); $indexNumber = Functions::flattenSingleValue($indexNumber); - $notExactMatch = Functions::flattenSingleValue($notExactMatch); + $notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); try { $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 9ba23142..d3314ff4 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -13,7 +13,8 @@ class Indirect /** * Determine whether cell address is in A1 (true) or R1C1 (false) format. * - * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, but can be provided as numeric which is cast to bool + * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, + * but can be provided as numeric which is cast to bool */ private static function a1Format($a1fmt): bool { @@ -53,7 +54,8 @@ class Indirect * =INDIRECT(cellAddress, bool) where the bool argument is optional * * @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) - * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, but can be provided as numeric which is cast to bool + * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, + * but can be provided as numeric which is cast to bool * @param Cell $pCell The current cell (containing this formula) * * @return array|string An array containing a cell or range of cells, or a string on error @@ -84,7 +86,8 @@ class Indirect /** * Extract range values. * - * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned. + * @return mixed Array of values in range if range contains more than one element. + * Otherwise, a single value is returned. */ private static function extractRequiredCells(?Worksheet $pSheet, string $cellAddress) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index f890e496..ddd5d9ee 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -25,7 +25,7 @@ class VLookup extends LookupBase { $lookupValue = Functions::flattenSingleValue($lookupValue); $indexNumber = Functions::flattenSingleValue($indexNumber); - $notExactMatch = Functions::flattenSingleValue($notExactMatch); + $notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); try { $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); diff --git a/tests/data/Calculation/LookupRef/ADDRESS.php b/tests/data/Calculation/LookupRef/ADDRESS.php index 0e2baea6..b9e170d5 100644 --- a/tests/data/Calculation/LookupRef/ADDRESS.php +++ b/tests/data/Calculation/LookupRef/ADDRESS.php @@ -44,10 +44,18 @@ return [ "'EXCEL SHEET'!R2C3", 2, 3, - 1, + null, false, 'EXCEL SHEET', ], + [ + "'EXCEL SHEET'!\$C\$2", + 2, + 3, + 1, + null, + 'EXCEL SHEET', + ], [ '#VALUE!', -2, diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 61cb7e06..e74f7cbe 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -225,6 +225,34 @@ return [ 3, true, ], + [ + 5, + 'B', + [ + [ + 'Axles', + 'Bearings', + 'Bolts', + ], + [ + 4, + 4, + 9, + ], + [ + 5, + 7, + 10, + ], + [ + 6, + 8, + 11, + ], + ], + 3, + null, + ], [ 11, 'Bolts', diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 0a059048..b35f2311 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -380,4 +380,19 @@ return [ 3, true, ], + [ + 'E', + 0.52, + [ + ['Lower', 'Upper', 'Grade'], + [0.00, 0.44, 'F'], + [0.45, 0.54, 'E'], + [0.55, 0.64, 'D'], + [0.65, 0.74, 'C'], + [0.75, 0.84, 'B'], + [0.85, 1.00, 'A'], + ], + 3, + null, + ], ]; From 004eacc49b5f06a27b0b916a985a3da2d5db25b1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 27 May 2021 11:46:39 +0200 Subject: [PATCH 07/21] Update changelog, and phpstan appeasement --- CHANGELOG.md | 1 + phpstan-baseline.neon | 5 ----- src/PhpSpreadsheet/DocumentGenerator.php | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade195d3..2ffa265b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Use of `nb` rather than `no` as the locale language code for Norsk Bokmål. ### Fixed +- Resolve default values when a null argument is passed for HLOOKUP(), VLOOKUP() and ADDRESS() functions [Issue #2120](https://github.com/PHPOffice/PhpSpreadsheet/issues/2120) - [PR #2121](https://github.com/PHPOffice/PhpSpreadsheet/pull/2121) - Fixed incorrect R1C1 to A1 subtraction formula conversion (`R[-2]C-R[2]C`) [Issue #2076](https://github.com/PHPOffice/PhpSpreadsheet/pull/2076) [PR #2086](https://github.com/PHPOffice/PhpSpreadsheet/pull/2086) - Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060) - Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b209e822..b2da4ec1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2240,11 +2240,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/DocumentGenerator.php - - - message: "#^Cannot access offset 0 on \\(int\\|string\\)\\.$#" - count: 2 - path: src/PhpSpreadsheet/DocumentGenerator.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\HashTable\\:\\:getIndexForHashCode\\(\\) should return int but returns int\\|string\\|false\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/DocumentGenerator.php b/src/PhpSpreadsheet/DocumentGenerator.php index acd25100..e67d9674 100644 --- a/src/PhpSpreadsheet/DocumentGenerator.php +++ b/src/PhpSpreadsheet/DocumentGenerator.php @@ -78,6 +78,7 @@ class DocumentGenerator $result = "# Function list by name\n"; $lastAlphabet = null; foreach ($phpSpreadsheetFunctions as $excelFunction => $functionInfo) { + /** @var string $excelFunction */ $lengths = [25, 31, 37]; if ($lastAlphabet !== $excelFunction[0]) { $lastAlphabet = $excelFunction[0]; From 290c125e2e8d5ac0e16754e2dcd85fa1ee81eeee Mon Sep 17 00:00:00 2001 From: Samuel Laulhau Date: Fri, 28 May 2021 10:03:14 +0200 Subject: [PATCH 08/21] fix type flies => files --- docs/topics/file-formats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/file-formats.md b/docs/topics/file-formats.md index 7f4e6b7e..6f8783e6 100644 --- a/docs/topics/file-formats.md +++ b/docs/topics/file-formats.md @@ -71,7 +71,7 @@ library. ### Csv Comma Separated Value (CSV) file format is a common structuring strategy -for text format files. In CSV flies, each line in the file represents a +for text format files. In CSV files, each line in the file represents a row of data and (within each line of the file) the different data fields (or columns) are separated from one another using a comma (`,`). If a data field contains a comma, then it should be enclosed (typically in From e0e5a81d69b54ff2d60b18c2c96ac01756b6e1aa Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 28 May 2021 20:15:20 +0200 Subject: [PATCH 09/21] Move documentation builder to infra so that it isn't included in non `--dev` composer downloads Unit test for locale builder Add new function stubs (as dummy) to Calculation list of functions --- bin/generate-document | 5 +- bin/generate-locales | 8 +- build/buildFunctionLists.php | 195 ------------------ .../DocumentGenerator.php | 2 +- infra/LocaleGenerator.php | 49 +++-- .../Calculation/Calculation.php | 70 +++++++ .../Calculation/locale/da/functions | 12 ++ .../Calculation/locale/de/functions | 10 + .../Calculation/locale/es/functions | 12 ++ .../Calculation/locale/fi/functions | 12 ++ .../Calculation/locale/hu/functions | 12 ++ .../Calculation/locale/it/functions | 12 ++ .../Calculation/locale/nb/functions | 12 ++ .../Calculation/locale/nl/functions | 12 ++ .../Calculation/locale/pl/functions | 12 ++ .../Calculation/locale/pt/br/functions | 2 + .../Calculation/locale/pt/functions | 12 ++ .../Calculation/locale/ru/functions | 12 ++ .../Calculation/locale/sv/functions | 9 + .../Calculation/locale/tr/functions | 12 ++ src/PhpSpreadsheet/Style/Font.php | 21 +- .../DocumentGeneratorTest.php | 2 +- .../LocaleGeneratorTest.php | 41 ++++ 23 files changed, 320 insertions(+), 226 deletions(-) delete mode 100644 build/buildFunctionLists.php rename {src/PhpSpreadsheet => infra}/DocumentGenerator.php (98%) create mode 100644 tests/PhpSpreadsheetTests/LocaleGeneratorTest.php diff --git a/bin/generate-document b/bin/generate-document index d44ec624..a8f334c9 100755 --- a/bin/generate-document +++ b/bin/generate-document @@ -2,12 +2,13 @@ getProperty('phpSpreadsheetFunctions'); + $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) + ->getProperty('phpSpreadsheetFunctions'); $phpSpreadsheetFunctionsProperty->setAccessible(true); $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); ksort($phpSpreadsheetFunctions); diff --git a/bin/generate-locales b/bin/generate-locales index 120e8680..30b9c556 100644 --- a/bin/generate-locales +++ b/bin/generate-locales @@ -7,14 +7,16 @@ use PhpOffice\PhpSpreadsheetInfra\LocaleGenerator; require_once 'vendor/autoload.php'; try { - $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class))->getProperty('phpSpreadsheetFunctions'); + $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) + ->getProperty('phpSpreadsheetFunctions'); $phpSpreadsheetFunctionsProperty->setAccessible(true); $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); $localeGenerator = new LocaleGenerator( - __DIR__ . '/../src/PhpSpreadsheet/Calculation/locale/', + realpath(__DIR__ . '/../src/PhpSpreadsheet/Calculation/locale/'), 'Translations.xlsx', - $phpSpreadsheetFunctions + $phpSpreadsheetFunctions, + true ); $localeGenerator->generateLocales(); } catch (\Exception $e) { diff --git a/build/buildFunctionLists.php b/build/buildFunctionLists.php deleted file mode 100644 index 3b824bd4..00000000 --- a/build/buildFunctionLists.php +++ /dev/null @@ -1,195 +0,0 @@ -category = $category; - $this->functionName = $functionName; - $this->excelVersion = $excelVersion; - $this->implementation = $implementation; - } -} - -class ColumnSettings -{ - public $length; - - public $title; - - public $underline; - - public function __construct(string $title, int $length) - { - $this->length = $length; - $this->title = str_pad($title, $length, ' '); - $this->underline = str_repeat('-', $length); - } -} - -class ListBuilder -{ - private $inputFileName; - - public $list = []; - - public function __construct(string $inputFileName) - { - $this->inputFileName = $inputFileName; - - $this->buildList(); - uasort( - $this->list, - function ($a, $b) { - $aSortName = str_replace('.', '', $a->functionName, $aCount) . str_repeat('.', $aCount); - $bSortName = str_replace('.', '', $b->functionName, $bCount) . str_repeat('.', $bCount); - - return $aSortName <=> $bSortName; - } - ); - } - - private function buildList(): void - { - $inputFile = new \SplFileObject($this->inputFileName); - $category = null; - - while (!$inputFile->eof()) { - $line = $inputFile->fgets(); - if (strpos($line, '#') === 0) { - if (strpos($line, '##') === 0) { - $category = trim(substr($line, 3)); - } - - continue; - } - - $lineData = explode('|', $line); - if (count($lineData) <= 1 || strpos($line, '--') === 0) { - continue; - } - - $functionData = array_map('trim', $lineData); - - if ($functionData[0] === 'Excel Function') { - continue; - } - - $function = new ExcelFunction($category, ...$functionData); - if (array_key_exists($function->functionName, $this->list)) { - echo " ERROR: Duplicate entry for function {$function->functionName} in master file", PHP_EOL; - - continue; - } - - $this->list[$function->functionName] = $function; - } - } -} - -class AlphabeticFileWriter -{ - private $outputFileName; - - private $outputFile; - - public function __construct(string $outputFileName) - { - $this->outputFileName = $outputFileName; - } - - public function generate(ExcelFunction ...$functionList): void - { - $this->outputFile = new \SplFileObject($this->outputFileName, 'w'); - - $this->excelFunctionColumnSettings = new ColumnSettings('Excel Function', max(array_map('strlen', array_column($functionList, 'functionName')))); - $this->categoryColumnSettings = new ColumnSettings('Category', max(array_map('strlen', array_column($functionList, 'category')))); - $this->excelVersionColumnSettings = new ColumnSettings('Excel Version', 13); - $this->phpSpreadsheetFunctionColumnSettings = new ColumnSettings('PhpSpreadsheet Function', 24); - - $this->header(); - $this->body(...$functionList); - } - - private function header(): void - { - $this->outputFile->fwrite('# Function list by name' . PHP_EOL); - } - - private function body(ExcelFunction ...$functionList): void - { - $initialCharacter = null; - - foreach ($functionList as $excelFunction) { - if (substr($excelFunction->functionName, 0, 1) !== $initialCharacter) { - $initialCharacter = $this->subHeader($excelFunction); - } - - $functionName = str_pad($excelFunction->functionName, $this->excelFunctionColumnSettings->length, ' '); - $category = str_pad($excelFunction->category, $this->categoryColumnSettings->length, ' '); - $excelVersion = str_pad($excelFunction->excelVersion, $this->excelVersionColumnSettings->length, ' '); - $this->outputFile->fwrite("{$functionName} | {$category} | {$excelVersion} | {$excelFunction->implementation}" . PHP_EOL); - } - } - - private function subHeader(ExcelFunction $excelFunction) - { - $initialCharacter = substr($excelFunction->functionName, 0, 1); - - $this->outputFile->fwrite(PHP_EOL . "## {$initialCharacter}" . PHP_EOL . PHP_EOL); - $this->outputFile->fwrite("{$this->excelFunctionColumnSettings->title} | {$this->categoryColumnSettings->title} | {$this->excelVersionColumnSettings->title} | {$this->phpSpreadsheetFunctionColumnSettings->title}" . PHP_EOL); - $this->outputFile->fwrite("{$this->excelFunctionColumnSettings->underline}-|-{$this->categoryColumnSettings->underline}-|-{$this->excelVersionColumnSettings->underline}-|-{$this->phpSpreadsheetFunctionColumnSettings->underline}" . PHP_EOL); - - return $initialCharacter; - } -} - -$folder = __DIR__ . '/../docs/references/'; -$inputFileName = 'function-list-by-category.md'; -$outputFileName = 'function-list-by-name.md'; - -echo "Building list of functions from master file {$inputFileName}", PHP_EOL; -$listBuilder = new ListBuilder($folder . $inputFileName); - -echo "Building new documentation list of alphabetic functions in {$outputFileName}", PHP_EOL; -$alphabeticFileWriter = new AlphabeticFileWriter($folder . $outputFileName); -$alphabeticFileWriter->generate(...array_values($listBuilder->list)); - -echo 'Identifying discrepancies between the master file and the Calculation Engine codebase', PHP_EOL; -$definedFunctions = (new Calculation())->getFunctions(); - -foreach ($listBuilder->list as $excelFunction) { - if (!array_key_exists($excelFunction->functionName, $definedFunctions)) { - echo " ERROR: Function {$excelFunction->functionName}() of category {$excelFunction->category} is not defined in the Calculation Engine", PHP_EOL; - } elseif (array_key_exists($excelFunction->functionName, $definedFunctions) && $excelFunction->implementation === '**Not yet Implemented**') { - if ($definedFunctions[$excelFunction->functionName]['functionCall'] !== [Functions::class, 'DUMMY']) { - echo " ERROR: Function {$excelFunction->functionName}() of category {$excelFunction->category} is flagged as not yet implemented in the documentation", PHP_EOL; - echo ' but does have an implementation in the code', PHP_EOL; - } - } -} - -foreach ($definedFunctions as $definedFunction => $definedFunctionDetail) { - if (!array_key_exists($definedFunction, $listBuilder->list)) { - echo " ERROR: Function {$definedFunction}() of category {$definedFunctionDetail['category']} is defined in the Calculation Engine, but not in the master file", PHP_EOL; - } -} diff --git a/src/PhpSpreadsheet/DocumentGenerator.php b/infra/DocumentGenerator.php similarity index 98% rename from src/PhpSpreadsheet/DocumentGenerator.php rename to infra/DocumentGenerator.php index e67d9674..e2c3c86c 100644 --- a/src/PhpSpreadsheet/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -1,6 +1,6 @@ translationBaseFolder = $translationBaseFolder; $this->translationSpreadsheetName = $translationSpreadsheetName; $this->phpSpreadsheetFunctions = $phpSpreadsheetFunctions; + $this->verbose = $verbose; } public function generateLocales(): void @@ -110,7 +114,7 @@ class LocaleGenerator } else { $errorCodeTranslation = "{$errorCode}" . PHP_EOL; fwrite($configFile, $errorCodeTranslation); - echo "No {$language} translation available for error code {$errorCode}", PHP_EOL; + $this->log("No {$language} translation available for error code {$errorCode}"); } } @@ -125,7 +129,7 @@ class LocaleGenerator $functionTranslation = "ArgumentSeparator = {$localeValue}" . PHP_EOL; fwrite($configFile, $functionTranslation); } else { - echo 'No Argument Separator defined', PHP_EOL; + $this->log('No Argument Separator defined'); } } @@ -142,12 +146,12 @@ class LocaleGenerator if ($this->isFunctionCategoryEntry($translationCell)) { $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})"); } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions)) { - echo "Function {$functionName} is not defined in PhpSpreadsheet", PHP_EOL; + $this->log("Function {$functionName} is not defined in PhpSpreadsheet"); } elseif (!empty($translationValue)) { $functionTranslation = "{$functionName} = {$translationValue}" . PHP_EOL; fwrite($functionFile, $functionTranslation); } else { - echo "No {$language} translation available for function {$functionName}", PHP_EOL; + $this->log("No {$language} translation available for function {$functionName}"); } } @@ -156,11 +160,11 @@ class LocaleGenerator protected function openConfigFile(string $locale, string $language, string $localeLanguage) { - echo "Building locale {$locale} ($language) configuration", PHP_EOL; + $this->log("Building locale {$locale} ($language) configuration"); $localeFolder = $this->getLocaleFolder($locale); $configFileName = realpath($localeFolder . DIRECTORY_SEPARATOR . 'config'); - echo "Writing locale configuration to {$configFileName}", PHP_EOL; + $this->log("Writing locale configuration to {$configFileName}"); $configFile = fopen($configFileName, 'wb'); $this->writeFileHeader($configFile, $localeLanguage, $language, 'locale settings'); @@ -170,11 +174,11 @@ class LocaleGenerator protected function openFunctionNameFile(string $locale, string $language, string $localeLanguage) { - echo "Building locale {$locale} ($language) function names", PHP_EOL; + $this->log("Building locale {$locale} ($language) function names"); $localeFolder = $this->getLocaleFolder($locale); $functionFileName = realpath($localeFolder . DIRECTORY_SEPARATOR . 'functions'); - echo "Writing local function names to {$functionFileName}", PHP_EOL; + $this->log("Writing local function names to {$functionFileName}"); $functionFile = fopen($functionFileName, 'wb'); $this->writeFileHeader($functionFile, $localeLanguage, $language, 'function name translations'); @@ -231,7 +235,7 @@ class LocaleGenerator protected function mapLanguageColumns(Worksheet $translationWorksheet): array { $sheetName = $translationWorksheet->getTitle(); - echo "Mapping Languages for {$sheetName}:", PHP_EOL; + $this->log("Mapping Languages for {$sheetName}:"); $baseColumn = self::ENGLISH_REFERENCE_COLUMN; $languagesList = $translationWorksheet->getColumnIterator(++$baseColumn); @@ -245,7 +249,7 @@ class LocaleGenerator /** @var Cell $cell */ if ($this->localeCanBeSupported($translationWorksheet, $cell)) { $languageNameMap[$cell->getColumn()] = $cell->getValue(); - echo $cell->getColumn(), ' -> ', $cell->getValue(), PHP_EOL; + $this->log($cell->getColumn() . ' -> ' . $cell->getValue()); } } } @@ -270,7 +274,7 @@ class LocaleGenerator protected function mapErrorCodeRows(): void { - echo 'Mapping Error Codes:', PHP_EOL; + $this->log('Mapping Error Codes:'); $errorList = $this->localeTranslations->getRowIterator(self::ERROR_CODES_FIRST_ROW); foreach ($errorList as $errorRow) { @@ -280,7 +284,7 @@ class LocaleGenerator foreach ($cells as $cell) { /** @var Cell $cell */ if ($cell->getValue() != '') { - echo $cell->getRow(), ' -> ', $cell->getValue(), PHP_EOL; + $this->log($cell->getRow() . ' -> ' . $cell->getValue()); $this->errorCodeMap[$cell->getValue()] = $cell->getRow(); } } @@ -289,7 +293,7 @@ class LocaleGenerator protected function mapFunctionNameRows(): void { - echo 'Mapping Functions:', PHP_EOL; + $this->log('Mapping Functions:'); $functionList = $this->functionNameTranslations->getRowIterator(self::FUNCTION_NAME_LIST_FIRST_ROW); foreach ($functionList as $functionRow) { @@ -300,7 +304,7 @@ class LocaleGenerator /** @var Cell $cell */ if ($this->isFunctionCategoryEntry($cell)) { if (!empty($cell->getValue())) { - echo 'CATEGORY: ', $cell->getValue(), PHP_EOL; + $this->log('CATEGORY: ' . $cell->getValue()); $this->functionNameMap[$cell->getValue()] = $cell->getRow(); } @@ -308,10 +312,10 @@ class LocaleGenerator } if ($cell->getValue() != '') { if (is_bool($cell->getValue())) { - echo $cell->getRow(), ' -> ', ($cell->getValue() ? 'TRUE' : 'FALSE'), PHP_EOL; + $this->log($cell->getRow() . ' -> ' . ($cell->getValue() ? 'TRUE' : 'FALSE')); $this->functionNameMap[($cell->getValue() ? 'TRUE' : 'FALSE')] = $cell->getRow(); } else { - echo $cell->getRow(), ' -> ', $cell->getValue(), PHP_EOL; + $this->log($cell->getRow() . ' -> ' . $cell->getValue()); $this->functionNameMap[$cell->getValue()] = $cell->getRow(); } } @@ -328,4 +332,15 @@ class LocaleGenerator return false; } + + private function log(string $message): void + { + if ($this->verbose === false) { + return; + } + + echo $message, PHP_EOL; + + return; + } } diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index f01a786b..a2ad47bd 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -299,6 +299,11 @@ class Calculation 'functionCall' => [Functions::class, 'DUMMY'], 'argumentCount' => '1', ], + 'ARRAYTOTEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'ASC' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, 'functionCall' => [Functions::class, 'DUMMY'], @@ -766,6 +771,11 @@ class Calculation 'functionCall' => [DateTimeExcel\Difference::class, 'interval'], 'argumentCount' => '2,3', ], + 'DATESTRING' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'DATEVALUE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, 'functionCall' => [DateTimeExcel\DateValue::class, 'fromString'], @@ -1527,6 +1537,11 @@ class Calculation 'functionCall' => [Functions::class, 'isText'], 'argumentCount' => '1', ], + 'ISTHAIDIGIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'JIS' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, 'functionCall' => [Functions::class, 'DUMMY'], @@ -1842,6 +1857,11 @@ class Calculation 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'], 'argumentCount' => '2+', ], + 'NUMBERSTRING' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'NUMBERVALUE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, 'functionCall' => [TextData\Format::class, 'NUMBERVALUE'], @@ -2124,6 +2144,16 @@ class Calculation 'functionCall' => [MathTrig\Round::class, 'round'], 'argumentCount' => '2', ], + 'ROUNDBAHTDOWN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'ROUNDBAHTUP' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'ROUNDDOWN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, 'functionCall' => [MathTrig\Round::class, 'down'], @@ -2427,6 +2457,41 @@ class Calculation 'functionCall' => [TextData\Concatenate::class, 'TEXTJOIN'], 'argumentCount' => '3+', ], + 'THAIDAYOFWEEK' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIDIGIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIMONTHOFYEAR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAINUMSOUND' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAINUMSTRING' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAISTRINGLENGTH' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIYEAR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'TIME' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, 'functionCall' => [DateTimeExcel\Time::class, 'fromHMS'], @@ -2532,6 +2597,11 @@ class Calculation 'functionCall' => [TextData\Format::class, 'VALUE'], 'argumentCount' => '1', ], + 'VALUETOTEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, 'functionCall' => [Statistical\Variances::class, 'VAR'], diff --git a/src/PhpSpreadsheet/Calculation/locale/da/functions b/src/PhpSpreadsheet/Calculation/locale/da/functions index bf4a7e95..6260760b 100644 --- a/src/PhpSpreadsheet/Calculation/locale/da/functions +++ b/src/PhpSpreadsheet/Calculation/locale/da/functions @@ -39,6 +39,7 @@ DVARP = DVARIANSP ## DATE = DATO DATEDIF = DATO.FORSKEL +DATESTRING = DATOSTRENG DATEVALUE = DATOVÆRDI DAY = DAG DAYS = DAGE @@ -53,6 +54,9 @@ NETWORKDAYS = ANTAL.ARBEJDSDAGE NETWORKDAYS.INTL = ANTAL.ARBEJDSDAGE.INTL NOW = NU SECOND = SEKUND +THAIDAYOFWEEK = THAILANDSKUGEDAG +THAIMONTHOFYEAR = THAILANDSKMÅNED +THAIYEAR = THAILANDSKÅR TIME = TID TIMEVALUE = TIDSVÆRDI TODAY = IDAG @@ -301,6 +305,8 @@ RAND = SLUMP RANDBETWEEN = SLUMPMELLEM ROMAN = ROMERTAL ROUND = AFRUND +ROUNDBAHTDOWN = RUNDBAHTNED +ROUNDBAHTUP = RUNDBAHTOP ROUNDDOWN = RUND.NED ROUNDUP = RUND.OP SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = KR EXACT = EKSAKT FIND = FIND FIXED = FAST +ISTHAIDIGIT = ERTHAILANDSKCIFFER LEFT = VENSTRE LEN = LÆNGDE LOWER = SMÅ.BOGSTAVER MID = MIDT +NUMBERSTRING = TALSTRENG NUMBERVALUE = TALVÆRDI PHONETIC = FONETISK PROPER = STORT.FORBOGSTAV @@ -465,6 +473,10 @@ SUBSTITUTE = UDSKIFT T = T TEXT = TEKST TEXTJOIN = TEKST.KOMBINER +THAIDIGIT = THAILANDSKCIFFER +THAINUMSOUND = THAILANDSKNUMLYD +THAINUMSTRING = THAILANDSKNUMSTRENG +THAISTRINGLENGTH = THAILANDSKSTRENGLÆNGDE TRIM = FJERN.OVERFLØDIGE.BLANKE UNICHAR = UNICHAR UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/de/functions b/src/PhpSpreadsheet/Calculation/locale/de/functions index a2687599..331232f7 100644 --- a/src/PhpSpreadsheet/Calculation/locale/de/functions +++ b/src/PhpSpreadsheet/Calculation/locale/de/functions @@ -52,6 +52,9 @@ NETWORKDAYS = NETTOARBEITSTAGE NETWORKDAYS.INTL = NETTOARBEITSTAGE.INTL NOW = JETZT SECOND = SEKUNDE +THAIDAYOFWEEK = THAIWOCHENTAG +THAIMONTHOFYEAR = THAIMONATDESJAHRES +THAIYEAR = THAIJAHR TIME = ZEIT TIMEVALUE = ZEITWERT TODAY = HEUTE @@ -300,6 +303,8 @@ RAND = ZUFALLSZAHL RANDBETWEEN = ZUFALLSBEREICH ROMAN = RÖMISCH ROUND = RUNDEN +ROUNDBAHTDOWN = RUNDBAHTNED +ROUNDBAHTUP = BAHTAUFRUNDEN ROUNDDOWN = ABRUNDEN ROUNDUP = AUFRUNDEN SEC = SEC @@ -449,6 +454,7 @@ DOLLAR = DM EXACT = IDENTISCH FIND = FINDEN FIXED = FEST +ISTHAIDIGIT = ISTTHAIZAHLENWORT LEFT = LINKS LEN = LÄNGE LOWER = KLEIN @@ -463,6 +469,10 @@ SUBSTITUTE = WECHSELN T = T TEXT = TEXT TEXTJOIN = TEXTVERKETTEN +THAIDIGIT = THAIZAHLENWORT +THAINUMSOUND = THAIZAHLSOUND +THAINUMSTRING = THAILANDSKNUMSTRENG +THAISTRINGLENGTH = THAIZEICHENFOLGENLÄNGE TRIM = GLÄTTEN UNICHAR = UNIZEICHEN UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/es/functions b/src/PhpSpreadsheet/Calculation/locale/es/functions index f433a103..1f9f2891 100644 --- a/src/PhpSpreadsheet/Calculation/locale/es/functions +++ b/src/PhpSpreadsheet/Calculation/locale/es/functions @@ -39,6 +39,7 @@ DVARP = BDVARP ## DATE = FECHA DATEDIF = SIFECHA +DATESTRING = CADENA.FECHA DATEVALUE = FECHANUMERO DAY = DIA DAYS = DIAS @@ -53,6 +54,9 @@ NETWORKDAYS = DIAS.LAB NETWORKDAYS.INTL = DIAS.LAB.INTL NOW = AHORA SECOND = SEGUNDO +THAIDAYOFWEEK = DIASEMTAI +THAIMONTHOFYEAR = MESAÑOTAI +THAIYEAR = AÑOTAI TIME = NSHORA TIMEVALUE = HORANUMERO TODAY = HOY @@ -301,6 +305,8 @@ RAND = ALEATORIO RANDBETWEEN = ALEATORIO.ENTRE ROMAN = NUMERO.ROMANO ROUND = REDONDEAR +ROUNDBAHTDOWN = REDONDEAR.BAHT.MAS +ROUNDBAHTUP = REDONDEAR.BAHT.MENOS ROUNDDOWN = REDONDEAR.MENOS ROUNDUP = REDONDEAR.MAS SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = MONEDA EXACT = IGUAL FIND = ENCONTRAR FIXED = DECIMAL +ISTHAIDIGIT = ESDIGITOTAI LEFT = IZQUIERDA LEN = LARGO LOWER = MINUSC MID = EXTRAE +NUMBERSTRING = CADENA.NUMERO NUMBERVALUE = VALOR.NUMERO PHONETIC = FONETICO PROPER = NOMPROPIO @@ -465,6 +473,10 @@ SUBSTITUTE = SUSTITUIR T = T TEXT = TEXTO TEXTJOIN = UNIRCADENAS +THAIDIGIT = DIGITOTAI +THAINUMSOUND = SONNUMTAI +THAINUMSTRING = CADENANUMTAI +THAISTRINGLENGTH = LONGCADENATAI TRIM = ESPACIOS UNICHAR = UNICAR UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/fi/functions b/src/PhpSpreadsheet/Calculation/locale/fi/functions index 71abd670..33068d93 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fi/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fi/functions @@ -39,6 +39,7 @@ DVARP = TVARIANSSIP ## DATE = PÄIVÄYS DATEDIF = PVMERO +DATESTRING = PVMMERKKIJONO DATEVALUE = PÄIVÄYSARVO DAY = PÄIVÄ DAYS = PÄIVÄT @@ -53,6 +54,9 @@ NETWORKDAYS = TYÖPÄIVÄT NETWORKDAYS.INTL = TYÖPÄIVÄT.KANSVÄL NOW = NYT SECOND = SEKUNNIT +THAIDAYOFWEEK = THAI.VIIKONPÄIVÄ +THAIMONTHOFYEAR = THAI.KUUKAUSI +THAIYEAR = THAI.VUOSI TIME = AIKA TIMEVALUE = AIKA_ARVO TODAY = TÄMÄ.PÄIVÄ @@ -301,6 +305,8 @@ RAND = SATUNNAISLUKU RANDBETWEEN = SATUNNAISLUKU.VÄLILTÄ ROMAN = ROMAN ROUND = PYÖRISTÄ +ROUNDBAHTDOWN = PYÖRISTÄ.BAHT.ALAS +ROUNDBAHTUP = PYÖRISTÄ.BAHT.YLÖS ROUNDDOWN = PYÖRISTÄ.DES.ALAS ROUNDUP = PYÖRISTÄ.DES.YLÖS SEC = SEK @@ -450,10 +456,12 @@ DOLLAR = VALUUTTA EXACT = VERTAA FIND = ETSI FIXED = KIINTEÄ +ISTHAIDIGIT = ON.THAI.NUMERO LEFT = VASEN LEN = PITUUS LOWER = PIENET MID = POIMI.TEKSTI +NUMBERSTRING = NROMERKKIJONO NUMBERVALUE = NROARVO PHONETIC = FONEETTINEN PROPER = ERISNIMI @@ -465,6 +473,10 @@ SUBSTITUTE = VAIHDA T = T TEXT = TEKSTI TEXTJOIN = TEKSTI.YHDISTÄ +THAIDIGIT = THAI.NUMERO +THAINUMSOUND = THAI.LUKU.ÄÄNI +THAINUMSTRING = THAI.LUKU.MERKKIJONO +THAISTRINGLENGTH = THAI.MERKKIJONON.PITUUS TRIM = POISTA.VÄLIT UNICHAR = UNICODEMERKKI UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/hu/functions b/src/PhpSpreadsheet/Calculation/locale/hu/functions index 6ac22bb3..46b30127 100644 --- a/src/PhpSpreadsheet/Calculation/locale/hu/functions +++ b/src/PhpSpreadsheet/Calculation/locale/hu/functions @@ -39,6 +39,7 @@ DVARP = AB.VAR2 ## DATE = DÁTUM DATEDIF = DÁTUMTÓLIG +DATESTRING = DÁTUMSZÖVEG DATEVALUE = DÁTUMÉRTÉK DAY = NAP DAYS = NAPOK @@ -53,6 +54,9 @@ NETWORKDAYS = ÖSSZ.MUNKANAP NETWORKDAYS.INTL = ÖSSZ.MUNKANAP.INTL NOW = MOST SECOND = MPERC +THAIDAYOFWEEK = THAIHÉTNAPJA +THAIMONTHOFYEAR = THAIHÓNAP +THAIYEAR = THAIÉV TIME = IDŐ TIMEVALUE = IDŐÉRTÉK TODAY = MA @@ -301,6 +305,8 @@ RAND = VÉL RANDBETWEEN = VÉLETLEN.KÖZÖTT ROMAN = RÓMAI ROUND = KEREKÍTÉS +ROUNDBAHTDOWN = BAHTKEREK.LE +ROUNDBAHTUP = BAHTKEREK.FEL ROUNDDOWN = KEREK.LE ROUNDUP = KEREK.FEL SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = FORINT EXACT = AZONOS FIND = SZÖVEG.TALÁL FIXED = FIX +ISTHAIDIGIT = ON.THAI.NUMERO LEFT = BAL LEN = HOSSZ LOWER = KISBETŰ MID = KÖZÉP +NUMBERSTRING = SZÁM.BETŰVEL NUMBERVALUE = SZÁMÉRTÉK PHONETIC = FONETIKUS PROPER = TNÉV @@ -465,6 +473,10 @@ SUBSTITUTE = HELYETTE T = T TEXT = SZÖVEG TEXTJOIN = SZÖVEGÖSSZEFŰZÉS +THAIDIGIT = THAISZÁM +THAINUMSOUND = THAISZÁMHANG +THAINUMSTRING = THAISZÁMKAR +THAISTRINGLENGTH = THAIKARHOSSZ TRIM = KIMETSZ UNICHAR = UNIKARAKTER UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/it/functions b/src/PhpSpreadsheet/Calculation/locale/it/functions index 0778eb20..c14ed85f 100644 --- a/src/PhpSpreadsheet/Calculation/locale/it/functions +++ b/src/PhpSpreadsheet/Calculation/locale/it/functions @@ -39,6 +39,7 @@ DVARP = DB.VAR.POP ## DATE = DATA DATEDIF = DATA.DIFF +DATESTRING = DATA.STRINGA DATEVALUE = DATA.VALORE DAY = GIORNO DAYS = GIORNI @@ -53,6 +54,9 @@ NETWORKDAYS = GIORNI.LAVORATIVI.TOT NETWORKDAYS.INTL = GIORNI.LAVORATIVI.TOT.INTL NOW = ADESSO SECOND = SECONDO +THAIDAYOFWEEK = THAIGIORNODELLASETTIMANA +THAIMONTHOFYEAR = THAIMESEDELLANNO +THAIYEAR = THAIANNO TIME = ORARIO TIMEVALUE = ORARIO.VALORE TODAY = OGGI @@ -301,6 +305,8 @@ RAND = CASUALE RANDBETWEEN = CASUALE.TRA ROMAN = ROMANO ROUND = ARROTONDA +ROUNDBAHTDOWN = ARROTBAHTGIU +ROUNDBAHTUP = ARROTBAHTSU ROUNDDOWN = ARROTONDA.PER.DIF ROUNDUP = ARROTONDA.PER.ECC SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = VALUTA EXACT = IDENTICO FIND = TROVA FIXED = FISSO +ISTHAIDIGIT = ÈTHAICIFRA LEFT = SINISTRA LEN = LUNGHEZZA LOWER = MINUSC MID = STRINGA.ESTRAI +NUMBERSTRING = NUMERO.STRINGA NUMBERVALUE = NUMERO.VALORE PHONETIC = FURIGANA PROPER = MAIUSC.INIZ @@ -465,6 +473,10 @@ SUBSTITUTE = SOSTITUISCI T = T TEXT = TESTO TEXTJOIN = TESTO.UNISCI +THAIDIGIT = THAICIFRA +THAINUMSOUND = THAINUMSUONO +THAINUMSTRING = THAISZÁMKAR +THAISTRINGLENGTH = THAILUNGSTRINGA TRIM = ANNULLA.SPAZI UNICHAR = CARATT.UNI UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/nb/functions b/src/PhpSpreadsheet/Calculation/locale/nb/functions index 6e09550d..b0a0f949 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nb/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nb/functions @@ -39,6 +39,7 @@ DVARP = DVARIANSP ## DATE = DATO DATEDIF = DATODIFF +DATESTRING = DATOSTRENG DATEVALUE = DATOVERDI DAY = DAG DAYS = DAGER @@ -53,6 +54,9 @@ NETWORKDAYS = NETT.ARBEIDSDAGER NETWORKDAYS.INTL = NETT.ARBEIDSDAGER.INTL NOW = NÅ SECOND = SEKUND +THAIDAYOFWEEK = THAIUKEDAG +THAIMONTHOFYEAR = THAIMÅNED +THAIYEAR = THAIÅR TIME = TID TIMEVALUE = TIDSVERDI TODAY = IDAG @@ -301,6 +305,8 @@ RAND = TILFELDIG RANDBETWEEN = TILFELDIGMELLOM ROMAN = ROMERTALL ROUND = AVRUND +ROUNDBAHTDOWN = RUNDAVBAHTNEDOVER +ROUNDBAHTUP = RUNDAVBAHTOPPOVER ROUNDDOWN = AVRUND.NED ROUNDUP = AVRUND.OPP SEC = SEC @@ -451,10 +457,12 @@ DOLLAR = VALUTA EXACT = EKSAKT FIND = FINN FIXED = FASTSATT +ISTHAIDIGIT = ERTHAISIFFER LEFT = VENSTRE LEN = LENGDE LOWER = SMÅ MID = DELTEKST +NUMBERSTRING = TALLSTRENG NUMBERVALUE = TALLVERDI PHONETIC = FURIGANA PROPER = STOR.FORBOKSTAV @@ -466,6 +474,10 @@ SUBSTITUTE = BYTT.UT T = T TEXT = TEKST TEXTJOIN = TEKST.KOMBINER +THAIDIGIT = THAISIFFER +THAINUMSOUND = THAINUMLYD +THAINUMSTRING = THAINUMSTRENG +THAISTRINGLENGTH = THAISTRENGLENGDE TRIM = TRIMME UNICHAR = UNICODETEGN UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/nl/functions b/src/PhpSpreadsheet/Calculation/locale/nl/functions index a0cc0f82..0e4f1597 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nl/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nl/functions @@ -38,6 +38,7 @@ DVARP = DBVARP ## Datum- en tijdfuncties (Date & Time Functions) ## DATE = DATUM +DATESTRING = DATUMNOTATIE DATEVALUE = DATUMWAARDE DAY = DAG DAYS = DAGEN @@ -52,6 +53,9 @@ NETWORKDAYS = NETTO.WERKDAGEN NETWORKDAYS.INTL = NETWERKDAGEN.INTL NOW = NU SECOND = SECONDE +THAIDAYOFWEEK = THAIS.WEEKDAG +THAIMONTHOFYEAR = THAIS.MAAND.VAN.JAAR +THAIYEAR = THAIS.JAAR TIME = TIJD TIMEVALUE = TIJDWAARDE TODAY = VANDAAG @@ -300,6 +304,8 @@ RAND = ASELECT RANDBETWEEN = ASELECTTUSSEN ROMAN = ROMEINS ROUND = AFRONDEN +ROUNDBAHTDOWN = BAHT.AFR.NAAR.BENEDEN +ROUNDBAHTUP = BAHT.AFR.NAAR.BOVEN ROUNDDOWN = AFRONDEN.NAAR.BENEDEN ROUNDUP = AFRONDEN.NAAR.BOVEN SEC = SEC @@ -449,10 +455,12 @@ DOLLAR = EURO EXACT = GELIJK FIND = VIND.ALLES FIXED = VAST +ISTHAIDIGIT = IS.THAIS.CIJFER LEFT = LINKS LEN = LENGTE LOWER = KLEINE.LETTERS MID = DEEL +NUMBERSTRING = GETALNOTATIE NUMBERVALUE = NUMERIEKE.WAARDE PHONETIC = FONETISCH PROPER = BEGINLETTERS @@ -464,6 +472,10 @@ SUBSTITUTE = SUBSTITUEREN T = T TEXT = TEKST TEXTJOIN = TEKST.COMBINEREN +THAIDIGIT = THAIS.CIJFER +THAINUMSOUND = THAIS.GETAL.GELUID +THAINUMSTRING = THAIS.GETAL.REEKS +THAISTRINGLENGTH = THAIS.REEKS.LENGTE TRIM = SPATIES.WISSEN UNICHAR = UNITEKEN UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/pl/functions b/src/PhpSpreadsheet/Calculation/locale/pl/functions index fadc6d57..d1b43b2e 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pl/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pl/functions @@ -39,6 +39,7 @@ DVARP = BD.WARIANCJA.POPUL ## DATE = DATA DATEDIF = DATA.RÓŻNICA +DATESTRING = DATA.CIĄG.ZNAK DATEVALUE = DATA.WARTOŚĆ DAY = DZIEŃ DAYS = DNI @@ -53,6 +54,9 @@ NETWORKDAYS = DNI.ROBOCZE NETWORKDAYS.INTL = DNI.ROBOCZE.NIESTAND NOW = TERAZ SECOND = SEKUNDA +THAIDAYOFWEEK = TAJ.DZIEŃ.TYGODNIA +THAIMONTHOFYEAR = TAJ.MIESIĄC.ROKU +THAIYEAR = TAJ.ROK TIME = CZAS TIMEVALUE = CZAS.WARTOŚĆ TODAY = DZIŚ @@ -301,6 +305,8 @@ RAND = LOS RANDBETWEEN = LOS.ZAKR ROMAN = RZYMSKIE ROUND = ZAOKR +ROUNDBAHTDOWN = ZAOKR.DÓŁ.BAT +ROUNDBAHTUP = ZAOKR.GÓRA.BAT ROUNDDOWN = ZAOKR.DÓŁ ROUNDUP = ZAOKR.GÓRA SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = KWOTA EXACT = PORÓWNAJ FIND = ZNAJDŹ FIXED = ZAOKR.DO.TEKST +ISTHAIDIGIT = CZY.CYFRA.TAJ LEFT = LEWY LEN = DŁ LOWER = LITERY.MAŁE MID = FRAGMENT.TEKSTU +NUMBERSTRING = LICZBA.CIĄG.ZNAK NUMBERVALUE = WARTOŚĆ.LICZBOWA PROPER = Z.WIELKIEJ.LITERY REPLACE = ZASTĄP @@ -464,6 +472,10 @@ SUBSTITUTE = PODSTAW T = T TEXT = TEKST TEXTJOIN = POŁĄCZ.TEKSTY +THAIDIGIT = TAJ.CYFRA +THAINUMSOUND = TAJ.DŹWIĘK.NUM +THAINUMSTRING = TAJ.CIĄG.NUM +THAISTRINGLENGTH = TAJ.DŁUGOŚĆ.CIĄGU TRIM = USUŃ.ZBĘDNE.ODSTĘPY UNICHAR = ZNAK.UNICODE UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions index 7980583b..feba30d9 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions @@ -39,6 +39,7 @@ DVARP = BDVARP ## DATE = DATA DATEDIF = DATADIF +DATESTRING = DATA.SÉRIE DATEVALUE = DATA.VALOR DAY = DIA DAYS = DIAS @@ -454,6 +455,7 @@ LEFT = ESQUERDA LEN = NÚM.CARACT LOWER = MINÚSCULA MID = EXT.TEXTO +NUMBERSTRING = SEQÜÊNCIA.NÚMERO NUMBERVALUE = VALORNUMÉRICO PHONETIC = FONÉTICA PROPER = PRI.MAIÚSCULA diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/functions b/src/PhpSpreadsheet/Calculation/locale/pt/functions index a9958e9f..8a94d826 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/functions @@ -39,6 +39,7 @@ DVARP = BDVARP ## DATE = DATA DATEDIF = DATADIF +DATESTRING = DATA.CADEIA DATEVALUE = DATA.VALOR DAY = DIA DAYS = DIAS @@ -53,6 +54,9 @@ NETWORKDAYS = DIATRABALHOTOTAL NETWORKDAYS.INTL = DIATRABALHOTOTAL.INTL NOW = AGORA SECOND = SEGUNDO +THAIDAYOFWEEK = DIA.DA.SEMANA.TAILANDÊS +THAIMONTHOFYEAR = MÊS.DO.ANO.TAILANDÊS +THAIYEAR = ANO.TAILANDÊS TIME = TEMPO TIMEVALUE = VALOR.TEMPO TODAY = HOJE @@ -301,6 +305,8 @@ RAND = ALEATÓRIO RANDBETWEEN = ALEATÓRIOENTRE ROMAN = ROMANO ROUND = ARRED +ROUNDBAHTDOWN = ARREDOND.BAHT.BAIXO +ROUNDBAHTUP = ARREDOND.BAHT.CIMA ROUNDDOWN = ARRED.PARA.BAIXO ROUNDUP = ARRED.PARA.CIMA SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = MOEDA EXACT = EXATO FIND = LOCALIZAR FIXED = FIXA +ISTHAIDIGIT = É.DÍGITO.TAILANDÊS LEFT = ESQUERDA LEN = NÚM.CARAT LOWER = MINÚSCULAS MID = SEG.TEXTO +NUMBERSTRING = NÚMERO.CADEIA NUMBERVALUE = VALOR.NÚMERO PHONETIC = FONÉTICA PROPER = INICIAL.MAIÚSCULA @@ -465,6 +473,10 @@ SUBSTITUTE = SUBST T = T TEXT = TEXTO TEXTJOIN = UNIRTEXTO +THAIDIGIT = DÍGITO.TAILANDÊS +THAINUMSOUND = SOM.NÚM.TAILANDÊS +THAINUMSTRING = CADEIA.NÚM.TAILANDÊS +THAISTRINGLENGTH = COMP.CADEIA.TAILANDÊS TRIM = COMPACTAR UNICHAR = UNICARÁT UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/ru/functions b/src/PhpSpreadsheet/Calculation/locale/ru/functions index 6655075e..7f9ce783 100644 --- a/src/PhpSpreadsheet/Calculation/locale/ru/functions +++ b/src/PhpSpreadsheet/Calculation/locale/ru/functions @@ -39,6 +39,7 @@ DVARP = БДДИСПП ## DATE = ДАТА DATEDIF = РАЗНДАТ +DATESTRING = СТРОКАДАННЫХ DATEVALUE = ДАТАЗНАЧ DAY = ДЕНЬ DAYS = ДНИ @@ -53,6 +54,9 @@ NETWORKDAYS = ЧИСТРАБДНИ NETWORKDAYS.INTL = ЧИСТРАБДНИ.МЕЖД NOW = ТДАТА SECOND = СЕКУНДЫ +THAIDAYOFWEEK = ТАЙДЕНЬНЕД +THAIMONTHOFYEAR = ТАЙМЕСЯЦ +THAIYEAR = ТАЙГОД TIME = ВРЕМЯ TIMEVALUE = ВРЕМЗНАЧ TODAY = СЕГОДНЯ @@ -301,6 +305,8 @@ RAND = СЛЧИС RANDBETWEEN = СЛУЧМЕЖДУ ROMAN = РИМСКОЕ ROUND = ОКРУГЛ +ROUNDBAHTDOWN = ОКРУГЛБАТВНИЗ +ROUNDBAHTUP = ОКРУГЛБАТВВЕРХ ROUNDDOWN = ОКРУГЛВНИЗ ROUNDUP = ОКРУГЛВВЕРХ SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = РУБЛЬ EXACT = СОВПАД FIND = НАЙТИ FIXED = ФИКСИРОВАННЫЙ +ISTHAIDIGIT = TAYRAKAMIYSA LEFT = ЛЕВСИМВ LEN = ДЛСТР LOWER = СТРОЧН MID = ПСТР +NUMBERSTRING = СТРОКАЧИСЕЛ NUMBERVALUE = ЧЗНАЧ PROPER = ПРОПНАЧ REPLACE = ЗАМЕНИТЬ @@ -464,6 +472,10 @@ SUBSTITUTE = ПОДСТАВИТЬ T = Т TEXT = ТЕКСТ TEXTJOIN = ОБЪЕДИНИТЬ +THAIDIGIT = ТАЙЦИФРА +THAINUMSOUND = ТАЙЧИСЛОВЗВУК +THAINUMSTRING = ТАЙЧИСЛОВСТРОКУ +THAISTRINGLENGTH = ТАЙДЛИНАСТРОКИ TRIM = СЖПРОБЕЛЫ UNICHAR = ЮНИСИМВ UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/sv/functions b/src/PhpSpreadsheet/Calculation/locale/sv/functions index 1606f50c..2531b4c1 100644 --- a/src/PhpSpreadsheet/Calculation/locale/sv/functions +++ b/src/PhpSpreadsheet/Calculation/locale/sv/functions @@ -52,6 +52,9 @@ NETWORKDAYS = NETTOARBETSDAGAR NETWORKDAYS.INTL = NETTOARBETSDAGAR.INT NOW = NU SECOND = SEKUND +THAIDAYOFWEEK = THAIVECKODAG +THAIMONTHOFYEAR = THAIMÅNAD +THAIYEAR = THAIÅR TIME = KLOCKSLAG TIMEVALUE = TIDVÄRDE TODAY = IDAG @@ -300,6 +303,8 @@ RAND = SLUMP RANDBETWEEN = SLUMP.MELLAN ROMAN = ROMERSK ROUND = AVRUNDA +ROUNDBAHTDOWN = AVRUNDABAHTNEDÅT +ROUNDBAHTUP = AVRUNDABAHTUPPÅT ROUNDDOWN = AVRUNDA.NEDÅT ROUNDUP = AVRUNDA.UPPÅT SEC = SEK @@ -463,6 +468,10 @@ SUBSTITUTE = BYT.UT T = T TEXT = TEXT TEXTJOIN = TEXTJOIN +THAIDIGIT = THAISIFFRA +THAINUMSOUND = THAITALLJUD +THAINUMSTRING = THAITALSTRÄNG +THAISTRINGLENGTH = THAISTRÄNGLÄNGD TRIM = RENSA UNICHAR = UNITECKENKOD UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Calculation/locale/tr/functions b/src/PhpSpreadsheet/Calculation/locale/tr/functions index ea0e00bf..f872274f 100644 --- a/src/PhpSpreadsheet/Calculation/locale/tr/functions +++ b/src/PhpSpreadsheet/Calculation/locale/tr/functions @@ -39,6 +39,7 @@ DVARP = VSEÇVARS ## DATE = TARİH DATEDIF = ETARİHLİ +DATESTRING = TARİHDİZİ DATEVALUE = TARİHSAYISI DAY = GÜN DAYS = GÜNSAY @@ -53,6 +54,9 @@ NETWORKDAYS = TAMİŞGÜNÜ NETWORKDAYS.INTL = TAMİŞGÜNÜ.ULUSL NOW = ŞİMDİ SECOND = SANİYE +THAIDAYOFWEEK = TAYHAFTANINGÜNÜ +THAIMONTHOFYEAR = TAYYILINAYI +THAIYEAR = TAYYILI TIME = ZAMAN TIMEVALUE = ZAMANSAYISI TODAY = BUGÜN @@ -301,6 +305,8 @@ RAND = S_SAYI_ÜRET RANDBETWEEN = RASTGELEARADA ROMAN = ROMEN ROUND = YUVARLA +ROUNDBAHTDOWN = BAHTAŞAĞIYUVARLA +ROUNDBAHTUP = BAHTYUKARIYUVARLA ROUNDDOWN = AŞAĞIYUVARLA ROUNDUP = YUKARIYUVARLA SEC = SEC @@ -450,10 +456,12 @@ DOLLAR = LİRA EXACT = ÖZDEŞ FIND = BUL FIXED = SAYIDÜZENLE +ISTHAIDIGIT = TAYRAKAMIYSA LEFT = SOLDAN LEN = UZUNLUK LOWER = KÜÇÜKHARF MID = PARÇAAL +NUMBERSTRING = SAYIDİZİ NUMBERVALUE = SAYIDEĞERİ PHONETIC = SES PROPER = YAZIM.DÜZENİ @@ -465,6 +473,10 @@ SUBSTITUTE = YERİNEKOY T = M TEXT = METNEÇEVİR TEXTJOIN = METİNBİRLEŞTİR +THAIDIGIT = TAYRAKAM +THAINUMSOUND = TAYSAYISES +THAINUMSTRING = TAYSAYIDİZE +THAISTRINGLENGTH = TAYDİZEUZUNLUĞU TRIM = KIRP UNICHAR = UNICODEKARAKTERİ UNICODE = UNICODE diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 473fe1dc..69979b8a 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -19,7 +19,7 @@ class Font extends Supervisor protected $name = 'Calibri'; /** - * Font Size. + * Font Size in points. * * @var null|float */ @@ -249,20 +249,27 @@ class Font extends Supervisor /** * Set Size. * - * @param float $pValue + * @param float $fontSizeInPoints * * @return $this */ - public function setSize($pValue) + public function setSize($fontSizeInPoints) { - if ($pValue == '') { - $pValue = 10; + if (is_string($fontSizeInPoints) || is_int($fontSizeInPoints)) { + $fontSizeInPoints = (float) $fontSizeInPoints; // $pValue = 0 if given string is not numeric } + + // Size must be a positive floating point number + // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 + if (!is_float($fontSizeInPoints) || !($fontSizeInPoints > 0)) { + $fontSizeInPoints = 10.0; + } + if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['size' => $pValue]); + $styleArray = $this->getStyleArray(['size' => $fontSizeInPoints]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); } else { - $this->size = $pValue; + $this->size = $fontSizeInPoints; } return $this; diff --git a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php index 8d6b15bf..bb553577 100644 --- a/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/DocumentGeneratorTest.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Calculation\Category as Cat; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Logical; -use PhpOffice\PhpSpreadsheet\DocumentGenerator; +use PhpOffice\PhpSpreadsheetInfra\DocumentGenerator; use PHPUnit\Framework\TestCase; use UnexpectedValueException; diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php new file mode 100644 index 00000000..dbec9d4a --- /dev/null +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -0,0 +1,41 @@ +getProperty('phpSpreadsheetFunctions'); + $phpSpreadsheetFunctionsProperty->setAccessible(true); + $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); + + $localeGenerator = new LocaleGenerator( + realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + 'Translations.xlsx', + $phpSpreadsheetFunctions + ); + $localeGenerator->generateLocales(); + + $testLocales = [ + 'fr', + 'nl', + 'pt', + 'pt_br', + 'ru', + ]; + + foreach ($testLocales as $locale) { + $locale = str_replace('_', '/', $locale); + $path = realpath(__DIR__ . "/../../src/PhpSpreadsheet/Calculation/locale/{$locale}"); + self::assertFileExists("{$path}/config"); + self::assertFileExists("{$path}/functions"); + } + } +} From 5e531b4511de79403763bc3498c0d7e777af4f32 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 28 May 2021 22:11:43 +0200 Subject: [PATCH 10/21] Fix phpcs, phpstan and scrutinizer issues --- infra/LocaleGenerator.php | 1 - phpstan-baseline.neon | 5 ----- src/PhpSpreadsheet/Style/Font.php | 21 +++++++------------ .../LocaleGeneratorTest.php | 2 +- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index b9862e9a..48f96ec8 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -341,6 +341,5 @@ class LocaleGenerator echo $message, PHP_EOL; - return; } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b2da4ec1..4eed9863 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2235,11 +2235,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Document/Properties.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\DocumentGenerator\\:\\:getPhpSpreadsheetFunctionText\\(\\) has parameter \\$functionCall with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/DocumentGenerator.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\HashTable\\:\\:getIndexForHashCode\\(\\) should return int but returns int\\|string\\|false\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 69979b8a..473fe1dc 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -19,7 +19,7 @@ class Font extends Supervisor protected $name = 'Calibri'; /** - * Font Size in points. + * Font Size. * * @var null|float */ @@ -249,27 +249,20 @@ class Font extends Supervisor /** * Set Size. * - * @param float $fontSizeInPoints + * @param float $pValue * * @return $this */ - public function setSize($fontSizeInPoints) + public function setSize($pValue) { - if (is_string($fontSizeInPoints) || is_int($fontSizeInPoints)) { - $fontSizeInPoints = (float) $fontSizeInPoints; // $pValue = 0 if given string is not numeric + if ($pValue == '') { + $pValue = 10; } - - // Size must be a positive floating point number - // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 - if (!is_float($fontSizeInPoints) || !($fontSizeInPoints > 0)) { - $fontSizeInPoints = 10.0; - } - if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['size' => $fontSizeInPoints]); + $styleArray = $this->getStyleArray(['size' => $pValue]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); } else { - $this->size = $fontSizeInPoints; + $this->size = $pValue; } return $this; diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index dbec9d4a..e7429a25 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -17,7 +17,7 @@ class LocaleGeneratorTest extends TestCase $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); $localeGenerator = new LocaleGenerator( - realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + (string) realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), 'Translations.xlsx', $phpSpreadsheetFunctions ); From f5c2cf9df9ea16404a2746c4c8edb5680bbe372a Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 28 May 2021 22:21:33 +0200 Subject: [PATCH 11/21] Fix more phpcs issues --- infra/LocaleGenerator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index 48f96ec8..c83042af 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -340,6 +340,5 @@ class LocaleGenerator } echo $message, PHP_EOL; - } } From 0b0f02206fb07fecf336f5d94c28e0a5ef4f3178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matja=C5=BE=20Drolc?= Date: Sat, 15 May 2021 13:41:41 +0200 Subject: [PATCH 12/21] fix: Set font size to 10 when given 0 This change restored behavior from PHP7 in PHP8. In PHP7 calling setSize(0) resulted in font size being set to 10. The fix addresses change to equal comparisons in PHP8. Extra comparison is added to keep result from PHP7 in PHP8 for the setSize(0) case. --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Style/Font.php | 13 ++++++-- tests/PhpSpreadsheetTests/Style/FontTest.php | 32 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffa265b..31101a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fixed issue with Xlsx@listWorksheetInfo not returning any data - Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) - Fix for [Issue #1916](https://github.com/PHPOffice/PhpSpreadsheet/issues/1916) - Invalid signature check for XML files +- Fix change in `Font::setSize()` behavior for PHP8. [PR #2100](https://github.com/PHPOffice/PhpSpreadsheet/pull/2100) ## 1.17.1 - 2021-03-01 diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 473fe1dc..461c3c13 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -249,15 +249,22 @@ class Font extends Supervisor /** * Set Size. * - * @param float $pValue + * @param mixed $pValue A float representing the value of a positive measurement in points (1/72 of an inch) * * @return $this */ public function setSize($pValue) { - if ($pValue == '') { - $pValue = 10; + if (is_string($pValue) || is_int($pValue)) { + $pValue = (float) $pValue; // $pValue = 0 if given string is not numeric } + + // Size must be a positive floating point number + // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 + if (!is_float($pValue) || !($pValue > 0)) { + $pValue = 10.0; + } + if ($this->isSupervisor) { $styleArray = $this->getStyleArray(['size' => $pValue]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index c014a4b6..a6843c10 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -39,4 +39,36 @@ class FontTest extends TestCase self::assertTrue($font->getSuperscript()); self::assertFalse($font->getSubscript(), 'False remains unchanged'); } + + public function testSize(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $cell = $sheet->getCell('A1'); + $cell->setValue('Cell A1'); + $font = $cell->getStyle()->getFont(); + + self::assertEquals(11, $font->getSize(), 'The default is 11'); + + $font->setSize(12); + self::assertEquals(12, $font->getSize(), 'Accepted new font size'); + + $invalidFontSizeValues = [ + '', + false, + true, + 'non_numeric_string', + '-1.0', + -1.0, + 0, + [], + (object) [], + null, + ]; + foreach ($invalidFontSizeValues as $invalidFontSizeValue) { + $font->setSize(12); + $font->setSize($invalidFontSizeValue); + self::assertEquals(10, $font->getSize(), 'Set to 10 after trying to set an invalid value.'); + } + } } From 7e4331e3ab175d162efa3a2058b2175baf1c1500 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 29 May 2021 03:02:36 -0700 Subject: [PATCH 13/21] Error in COUPNCD (#2119) See issue #2116. Code for handling end of month (method couponFirstPeriodDate) needed a fix. Fixed it, confirmed it covered the reported issue with no regression problems. Then added some extra similar tests to all the callers of couponFirstPeriodDate, and ... One new test, in COUPDAYSNC, does not agree with Excel. It also does not agree with LibreOffice. It does, however, agree with Gnumeric, and with my (hardly guaranteed) hand calculation of what the result should be. So, I'm going with it (and have added an appropriate comment to the test data). I'm glad to discuss the matter with anyone more familiar than I with how this is supposed to work - those 360-day years are killers. --- phpstan-baseline.neon | 40 ------------------- .../Calculation/Financial/Coupons.php | 32 +++++++++------ .../data/Calculation/Financial/COUPDAYBS.php | 14 +++++++ tests/data/Calculation/Financial/COUPDAYS.php | 14 +++++++ .../data/Calculation/Financial/COUPDAYSNC.php | 17 ++++++++ tests/data/Calculation/Financial/COUPNCD.php | 28 +++++++++++++ tests/data/Calculation/Financial/COUPPCD.php | 14 +++++++ 7 files changed, 106 insertions(+), 53 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4eed9863..7fb5a0f6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -635,46 +635,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php - - - message: "#^Binary operation \"\\*\" between float\\|string and int results in an error\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Binary operation \"\\*\" between float\\|string and int\\|string results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$maturity with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$next with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$settlement with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:validateCouponPeriod\\(\\) has parameter \\$maturity with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:validateCouponPeriod\\(\\) has parameter \\$settlement with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Depreciation\\:\\:validateCost\\(\\) has parameter \\$cost with no typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index 5620c327..3fd6c1d2 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; +use DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; @@ -73,7 +74,7 @@ class Coupons return abs((float) DateTimeExcel\Days::between($prev, $settlement)); } - return DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear; + return (float) DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear; } /** @@ -208,7 +209,7 @@ class Coupons } } - return DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear; + return (float) DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear; } /** @@ -322,7 +323,7 @@ class Coupons FinancialConstants::BASIS_DAYS_PER_YEAR_NASD ); - return (int) ceil($yearsBetweenSettlementAndMaturity * $frequency); + return (int) ceil((float) $yearsBetweenSettlementAndMaturity * $frequency); } /** @@ -379,28 +380,33 @@ class Coupons return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); } - private static function couponFirstPeriodDate($settlement, $maturity, int $frequency, $next) + private static function monthsDiff(DateTime $result, int $months, string $plusOrMinus, int $day, bool $lastDayFlag): void + { + $result->setDate((int) $result->format('Y'), (int) $result->format('m'), 1); + $result->modify("$plusOrMinus $months months"); + $daysInMonth = (int) $result->format('t'); + $result->setDate((int) $result->format('Y'), (int) $result->format('m'), $lastDayFlag ? $daysInMonth : min($day, $daysInMonth)); + } + + private static function couponFirstPeriodDate(float $settlement, float $maturity, int $frequency, bool $next): float { $months = 12 / $frequency; $result = Date::excelToDateTimeObject($maturity); - $maturityEoM = Helpers::isLastDayOfMonth($result); + $day = (int) $result->format('d'); + $lastDayFlag = Helpers::isLastDayOfMonth($result); while ($settlement < Date::PHPToExcel($result)) { - $result->modify('-' . $months . ' months'); + self::monthsDiff($result, $months, '-', $day, $lastDayFlag); } if ($next === true) { - $result->modify('+' . $months . ' months'); + self::monthsDiff($result, $months, '+', $day, $lastDayFlag); } - if ($maturityEoM === true) { - $result->modify('-1 day'); - } - - return Date::PHPToExcel($result); + return (float) Date::PHPToExcel($result); } - private static function validateCouponPeriod($settlement, $maturity): void + private static function validateCouponPeriod(float $settlement, float $maturity): void { if ($settlement >= $maturity) { throw new Exception(Functions::NAN()); diff --git a/tests/data/Calculation/Financial/COUPDAYBS.php b/tests/data/Calculation/Financial/COUPDAYBS.php index 260783fa..f41d3620 100644 --- a/tests/data/Calculation/Financial/COUPDAYBS.php +++ b/tests/data/Calculation/Financial/COUPDAYBS.php @@ -205,4 +205,18 @@ return [ 4, 4, ], + [ + 5, + '05-Apr-2019', + '30-Sep-2021', + 2, + 0, + ], + [ + 5, + '05-Oct-2019', + '31-Mar-2022', + 2, + 0, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPDAYS.php b/tests/data/Calculation/Financial/COUPDAYS.php index c72d26c1..caba56e5 100644 --- a/tests/data/Calculation/Financial/COUPDAYS.php +++ b/tests/data/Calculation/Financial/COUPDAYS.php @@ -219,4 +219,18 @@ return [ 4, 4, ], + [ + 180, + '05-Apr-2019', + '30-Sep-2021', + 2, + 0, + ], + [ + 180, + '05-Oct-2019', + '31-Mar-2022', + 2, + 0, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPDAYSNC.php b/tests/data/Calculation/Financial/COUPDAYSNC.php index ca611475..e8500263 100644 --- a/tests/data/Calculation/Financial/COUPDAYSNC.php +++ b/tests/data/Calculation/Financial/COUPDAYSNC.php @@ -219,4 +219,21 @@ return [ 4, 4, ], + [ + 175, + '05-Apr-2019', + '30-Sep-2021', + 2, + 0, + ], + // Excel and LibreOffice return 175 for the following calculation. + // Gnumeric returns 176. + // My hand calculation, hardly guaranteed, agrees with Gnumeric. + [ + 176, + '05-Oct-2019', + '31-Mar-2022', + 2, + 0, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPNCD.php b/tests/data/Calculation/Financial/COUPNCD.php index 8dee4c0a..d690525c 100644 --- a/tests/data/Calculation/Financial/COUPNCD.php +++ b/tests/data/Calculation/Financial/COUPNCD.php @@ -219,4 +219,32 @@ return [ 4, 4, ], + [ + 44651, + '30-Sep-2021', + '31-Mar-2022', + 2, + 0, + ], + [ + 44834, + '31-Mar-2022', + '30-Sep-2022', + 2, + 0, + ], + [ + 43738, + '05-Apr-2019', + '30-Sep-2021', + 2, + 0, + ], + [ + 43921, + '05-Oct-2019', + '31-Mar-2022', + 2, + 0, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPPCD.php b/tests/data/Calculation/Financial/COUPPCD.php index e906f147..88b93af5 100644 --- a/tests/data/Calculation/Financial/COUPPCD.php +++ b/tests/data/Calculation/Financial/COUPPCD.php @@ -219,4 +219,18 @@ return [ 4, 4, ], + [ + 43555, + '05-Apr-2019', + '30-Sep-2021', + 2, + 0, + ], + [ + 43738, + '05-Oct-2019', + '31-Mar-2022', + 2, + 0, + ], ]; From 68dd2c39da25e10be9ab8f50773c0d24dc9233df Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Thu, 27 May 2021 07:14:03 -0700 Subject: [PATCH 14/21] Tests for PreCalc PR #2110 added some documentation for an unexpected observation when formula pre-calculation was set to false. I had suggested adding a unit test to demonstrate the observation, but I couldn't find any existing tests for PreCalc. This PR rectifies that omission. --- .../Writer/PreCalcTest.php | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Writer/PreCalcTest.php diff --git a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php new file mode 100644 index 00000000..9e6a0d8a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php @@ -0,0 +1,168 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function providerPreCalc(): array + { + return [ + [true, 'Xlsx'], + [false, 'Xlsx'], + [null, 'Xlsx'], + [true, 'Xls'], + [false, 'Xls'], + [null, 'Xls'], + [true, 'Ods'], + [false, 'Ods'], + [null, 'Ods'], + [true, 'Html'], + [false, 'Html'], + [null, 'Html'], + [true, 'Csv'], + [false, 'Csv'], + [null, 'Csv'], + ]; + } + + /** + * @dataProvider providerPreCalc + */ + public function testPreCalc(?bool $preCalc, string $type): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Column not set to autoSize'); + $sheet->getCell('B1')->setValue('Column set to autoSize'); + $sheet->getCell('A2')->setValue('=1+2'); + $sheet->getCell('A3')->setValue('=5+6'); + $sheet->getCell('B2')->setValue('=3+A3'); + $columnDimension = $sheet->getColumnDimension('B'); + if ($columnDimension === null) { + self::fail('Unable to getColumnDimension'); + } else { + $columnDimension->setAutoSize(true); + } + + $writer = IOFactory::createWriter($spreadsheet, $type); + if ($preCalc !== null) { + $writer->setPreCalculateFormulas($preCalc); + } + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $title = $sheet->getTitle(); + $calculation = Calculation::getInstance($spreadsheet); + $cellValue = 0; + // A2 has no cached calculation value if preCalc is false + if ($preCalc === false) { + self::assertFalse($calculation->getValueFromCache("$title!A2", $cellValue)); + } else { + self::assertTrue($calculation->getValueFromCache("$title!A2", $cellValue)); + self::assertSame(3, $cellValue); + } + if ($type === 'Xlsx' || $type === 'Xls' || $type === 'Html' || $preCalc !== false) { + // These 3 types support auto-sizing. + // A3 has cached calculation value because it is used in B2 calculation + self::assertTrue($calculation->getValueFromCache("$title!A3", $cellValue)); + self::assertSame(11, $cellValue); + // B2 has cached calculation value because its column is auto-sized + self::assertTrue($calculation->getValueFromCache("$title!B2", $cellValue)); + self::assertSame(14, $cellValue); + } else { + self::assertFalse($calculation->getValueFromCache("$title!A3", $cellValue)); + self::assertFalse($calculation->getValueFromCache("$title!B2", $cellValue)); + } + if ($type === 'Xlsx') { + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#xl/worksheets/sheet1.xml'; + $data = file_get_contents($file); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($data === false) { + self::fail('Unable to read worksheet file'); + } else { + if ($preCalc === false) { + self::assertStringContainsString('3+A30', $data); + } else { + self::assertStringContainsString('3+A314', $data); + } + } + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#xl/workbook.xml'; + $data = file_get_contents($file); + // confirm whether workbook is set to recalculate + if ($data === false) { + self::fail('Unable to read workbook file'); + } elseif ($preCalc === false) { + self::assertStringContainsString('', $data); + } else { + self::assertStringContainsString('', $data); + } + } elseif ($type === 'Ods') { + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#content.xml'; + $data = file_get_contents($file); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($data === false) { + self::fail('Unable to read Ods file'); + } else { + if ($preCalc === false) { + self::assertStringContainsString('table:formula="of:=3+[.A3]" office:value-type="string" office:value="=3+A3"', $data); + } else { + self::assertStringContainsString(' table:formula="of:=3+[.A3]" office:value-type="float" office:value="14"', $data); + } + } + } elseif ($type === 'Html') { + $data = file_get_contents($this->outfile); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($data === false) { + self::fail('Unable to read Html file'); + } else { + if ($preCalc === false) { + self::assertStringContainsString('>=1+2', $data); + self::assertStringContainsString('>=3+A3', $data); + self::assertStringContainsString('>=5+6', $data); + } else { + self::assertStringContainsString('>3', $data); + self::assertStringContainsString('>14', $data); + self::assertStringContainsString('>11', $data); + } + } + } elseif ($type === 'Csv') { + $data = file_get_contents($this->outfile); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($data === false) { + self::fail('Unable to read Csv file'); + } else { + if ($preCalc === false) { + self::assertStringContainsString('"=1+2"', $data); + self::assertStringContainsString('"=3+A3"', $data); + self::assertStringContainsString('"=5+6"', $data); + } else { + self::assertStringContainsString('"3"', $data); + self::assertStringContainsString('"14"', $data); + self::assertStringContainsString('"11"', $data); + } + } + } + } +} From 3540a275b9552f538fe60ff356056e0badb3bb8d Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Thu, 27 May 2021 08:26:48 -0700 Subject: [PATCH 15/21] Scrutinizer and Phpstan Didn't realize Scrutinizer enforces complexity limits in tests. --- .../Writer/PreCalcTest.php | 240 ++++++++++-------- 1 file changed, 140 insertions(+), 100 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php index 9e6a0d8a..2db372c4 100644 --- a/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php +++ b/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class PreCalcTest extends AbstractFunctional @@ -42,6 +43,136 @@ class PreCalcTest extends AbstractFunctional ]; } + private static function autoSize(?ColumnDimension $columnDimension): void + { + if ($columnDimension === null) { + self::fail('Unable to getColumnDimension'); + } else { + $columnDimension->setAutoSize(true); + } + } + + private static function verifyA2(Calculation $calculation, string $title, ?bool $preCalc): void + { + $cellValue = 0; + // A2 has no cached calculation value if preCalc is false + if ($preCalc === false) { + self::assertFalse($calculation->getValueFromCache("$title!A2", $cellValue)); + } else { + self::assertTrue($calculation->getValueFromCache("$title!A2", $cellValue)); + self::assertSame(3, $cellValue); + } + } + + private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html']; + + private static function verifyA3B2(Calculation $calculation, string $title, ?bool $preCalc, string $type): void + { + $cellValue = 0; + if (in_array($type, self::AUTOSIZE_TYPES) || $preCalc !== false) { + // These 3 types support auto-sizing. + // A3 has cached calculation value because it is used in B2 calculation + self::assertTrue($calculation->getValueFromCache("$title!A3", $cellValue)); + self::assertSame(11, $cellValue); + // B2 has cached calculation value because its column is auto-sized + self::assertTrue($calculation->getValueFromCache("$title!B2", $cellValue)); + self::assertSame(14, $cellValue); + } else { + self::assertFalse($calculation->getValueFromCache("$title!A3", $cellValue)); + self::assertFalse($calculation->getValueFromCache("$title!B2", $cellValue)); + } + } + + private static function readFile(string $file): string + { + $dataOut = ''; + $data = file_get_contents($file); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($data === false) { + self::fail("Unable to read $file"); + } else { + $dataOut = $data; + } + + return $dataOut; + } + + private function verifyXlsx(?bool $preCalc, string $type): void + { + if ($type === 'Xlsx') { + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#xl/worksheets/sheet1.xml'; + $data = self::readFile($file); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($preCalc === false) { + self::assertStringContainsString('3+A30', $data); + } else { + self::assertStringContainsString('3+A314', $data); + } + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#xl/workbook.xml'; + $data = self::readFile($file); + // confirm whether workbook is set to recalculate + if ($preCalc === false) { + self::assertStringContainsString('', $data); + } else { + self::assertStringContainsString('', $data); + } + } + } + + private function verifyOds(?bool $preCalc, string $type): void + { + if ($type === 'Ods') { + $file = 'zip://'; + $file .= $this->outfile; + $file .= '#content.xml'; + $data = self::readFile($file); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($preCalc === false) { + self::assertStringContainsString('table:formula="of:=3+[.A3]" office:value-type="string" office:value="=3+A3"', $data); + } else { + self::assertStringContainsString(' table:formula="of:=3+[.A3]" office:value-type="float" office:value="14"', $data); + } + } + } + + private function verifyHtml(?bool $preCalc, string $type): void + { + if ($type === 'Html') { + $data = self::readFile($this->outfile); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($preCalc === false) { + self::assertStringContainsString('>=1+2', $data); + self::assertStringContainsString('>=3+A3', $data); + self::assertStringContainsString('>=5+6', $data); + } else { + self::assertStringContainsString('>3', $data); + self::assertStringContainsString('>14', $data); + self::assertStringContainsString('>11', $data); + } + } + } + + private function verifyCsv(?bool $preCalc, string $type): void + { + if ($type === 'Csv') { + $data = self::readFile($this->outfile); + // confirm that file contains B2 pre-calculated or not as appropriate + if ($preCalc === false) { + self::assertStringContainsString('"=1+2"', $data); + self::assertStringContainsString('"=3+A3"', $data); + self::assertStringContainsString('"=5+6"', $data); + } else { + self::assertStringContainsString('"3"', $data); + self::assertStringContainsString('"14"', $data); + self::assertStringContainsString('"11"', $data); + } + } + } + /** * @dataProvider providerPreCalc */ @@ -55,11 +186,7 @@ class PreCalcTest extends AbstractFunctional $sheet->getCell('A3')->setValue('=5+6'); $sheet->getCell('B2')->setValue('=3+A3'); $columnDimension = $sheet->getColumnDimension('B'); - if ($columnDimension === null) { - self::fail('Unable to getColumnDimension'); - } else { - $columnDimension->setAutoSize(true); - } + self::autoSize($columnDimension); $writer = IOFactory::createWriter($spreadsheet, $type); if ($preCalc !== null) { @@ -69,100 +196,13 @@ class PreCalcTest extends AbstractFunctional $writer->save($this->outfile); $title = $sheet->getTitle(); $calculation = Calculation::getInstance($spreadsheet); - $cellValue = 0; - // A2 has no cached calculation value if preCalc is false - if ($preCalc === false) { - self::assertFalse($calculation->getValueFromCache("$title!A2", $cellValue)); - } else { - self::assertTrue($calculation->getValueFromCache("$title!A2", $cellValue)); - self::assertSame(3, $cellValue); - } - if ($type === 'Xlsx' || $type === 'Xls' || $type === 'Html' || $preCalc !== false) { - // These 3 types support auto-sizing. - // A3 has cached calculation value because it is used in B2 calculation - self::assertTrue($calculation->getValueFromCache("$title!A3", $cellValue)); - self::assertSame(11, $cellValue); - // B2 has cached calculation value because its column is auto-sized - self::assertTrue($calculation->getValueFromCache("$title!B2", $cellValue)); - self::assertSame(14, $cellValue); - } else { - self::assertFalse($calculation->getValueFromCache("$title!A3", $cellValue)); - self::assertFalse($calculation->getValueFromCache("$title!B2", $cellValue)); - } - if ($type === 'Xlsx') { - $file = 'zip://'; - $file .= $this->outfile; - $file .= '#xl/worksheets/sheet1.xml'; - $data = file_get_contents($file); - // confirm that file contains B2 pre-calculated or not as appropriate - if ($data === false) { - self::fail('Unable to read worksheet file'); - } else { - if ($preCalc === false) { - self::assertStringContainsString('3+A30', $data); - } else { - self::assertStringContainsString('3+A314', $data); - } - } - $file = 'zip://'; - $file .= $this->outfile; - $file .= '#xl/workbook.xml'; - $data = file_get_contents($file); - // confirm whether workbook is set to recalculate - if ($data === false) { - self::fail('Unable to read workbook file'); - } elseif ($preCalc === false) { - self::assertStringContainsString('', $data); - } else { - self::assertStringContainsString('', $data); - } - } elseif ($type === 'Ods') { - $file = 'zip://'; - $file .= $this->outfile; - $file .= '#content.xml'; - $data = file_get_contents($file); - // confirm that file contains B2 pre-calculated or not as appropriate - if ($data === false) { - self::fail('Unable to read Ods file'); - } else { - if ($preCalc === false) { - self::assertStringContainsString('table:formula="of:=3+[.A3]" office:value-type="string" office:value="=3+A3"', $data); - } else { - self::assertStringContainsString(' table:formula="of:=3+[.A3]" office:value-type="float" office:value="14"', $data); - } - } - } elseif ($type === 'Html') { - $data = file_get_contents($this->outfile); - // confirm that file contains B2 pre-calculated or not as appropriate - if ($data === false) { - self::fail('Unable to read Html file'); - } else { - if ($preCalc === false) { - self::assertStringContainsString('>=1+2', $data); - self::assertStringContainsString('>=3+A3', $data); - self::assertStringContainsString('>=5+6', $data); - } else { - self::assertStringContainsString('>3', $data); - self::assertStringContainsString('>14', $data); - self::assertStringContainsString('>11', $data); - } - } - } elseif ($type === 'Csv') { - $data = file_get_contents($this->outfile); - // confirm that file contains B2 pre-calculated or not as appropriate - if ($data === false) { - self::fail('Unable to read Csv file'); - } else { - if ($preCalc === false) { - self::assertStringContainsString('"=1+2"', $data); - self::assertStringContainsString('"=3+A3"', $data); - self::assertStringContainsString('"=5+6"', $data); - } else { - self::assertStringContainsString('"3"', $data); - self::assertStringContainsString('"14"', $data); - self::assertStringContainsString('"11"', $data); - } - } - } + // verify values in Calculation cache + self::verifyA2($calculation, $title, $preCalc); + self::verifyA3B2($calculation, $title, $preCalc, $type); + // verify values in output file + $this->verifyXlsx($preCalc, $type); + $this->verifyOds($preCalc, $type); + $this->verifyHtml($preCalc, $type); + $this->verifyCsv($preCalc, $type); } } From 70a518981c1cac4963e534d582e063af7f3491a0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 28 May 2021 14:19:24 +0200 Subject: [PATCH 16/21] Additional unit tests for HLOOKUP() and VLOOKUP() and Examples for VLOOKUP() --- samples/Calculations/LookupRef/VLOOKUP.php | 35 +++ tests/data/Calculation/LookupRef/HLOOKUP.php | 280 +++--------------- tests/data/Calculation/LookupRef/VLOOKUP.php | 285 ++----------------- 3 files changed, 91 insertions(+), 509 deletions(-) create mode 100644 samples/Calculations/LookupRef/VLOOKUP.php diff --git a/samples/Calculations/LookupRef/VLOOKUP.php b/samples/Calculations/LookupRef/VLOOKUP.php new file mode 100644 index 00000000..3e7eaa71 --- /dev/null +++ b/samples/Calculations/LookupRef/VLOOKUP.php @@ -0,0 +1,35 @@ +log('Searches for a value in the top row of a table or an array of values, + and then returns a value in the same column from a row you specify + in the table or array.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$data = [ + ['ID', 'First Name', 'Last Name', 'Salary'], + [72, 'Emily', 'Smith', 64901, null, 'ID', 53, 66, 56], + [66, 'James', 'Anderson', 70855, null, 'Salary'], + [14, 'Mia', 'Clark', 188657], + [30, 'John', 'Lewis', 97566], + [53, 'Jessica', 'Walker', 58339], + [56, 'Mark', 'Reed', 125180], + [79, 'Richard', 'Lopez', 91632], +]; + +$worksheet->fromArray($data, null, 'B2'); + +$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); +$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); +$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); + +for ($column = 'H'; $column !== 'K'; ++$column) { + $cell = $worksheet->getCell("{$column}4"); + $helper->log("{$column}4: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index e74f7cbe..bc52e707 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -1,304 +1,94 @@ Date: Sat, 29 May 2021 13:13:21 +0200 Subject: [PATCH 17/21] Typehinting to keep phpstan happy --- tests/data/Calculation/LookupRef/HLOOKUP.php | 4 ++-- tests/data/Calculation/LookupRef/VLOOKUP.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index bc52e707..26601721 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -1,6 +1,6 @@ Date: Sat, 29 May 2021 13:43:40 +0200 Subject: [PATCH 18/21] Update change log --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Style/Font.php | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31101a33..83e25edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Use of `nb` rather than `no` as the locale language code for Norsk Bokmål. ### Fixed +- Fixed error in COUPNCD() calculation for end of month [Issue #2116](https://github.com/PHPOffice/PhpSpreadsheet/issues/2116) - [PR #2119](https://github.com/PHPOffice/PhpSpreadsheet/pull/2119) - Resolve default values when a null argument is passed for HLOOKUP(), VLOOKUP() and ADDRESS() functions [Issue #2120](https://github.com/PHPOffice/PhpSpreadsheet/issues/2120) - [PR #2121](https://github.com/PHPOffice/PhpSpreadsheet/pull/2121) - Fixed incorrect R1C1 to A1 subtraction formula conversion (`R[-2]C-R[2]C`) [Issue #2076](https://github.com/PHPOffice/PhpSpreadsheet/pull/2076) [PR #2086](https://github.com/PHPOffice/PhpSpreadsheet/pull/2086) - Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060) diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 461c3c13..5f73c1d3 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -213,20 +213,20 @@ class Font extends Supervisor /** * Set Name. * - * @param string $pValue + * @param string $fontname * * @return $this */ - public function setName($pValue) + public function setName($fontname) { - if ($pValue == '') { - $pValue = 'Calibri'; + if ($fontname == '') { + $fontname = 'Calibri'; } if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['name' => $pValue]); + $styleArray = $this->getStyleArray(['name' => $fontname]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); } else { - $this->name = $pValue; + $this->name = $fontname; } return $this; @@ -249,27 +249,27 @@ class Font extends Supervisor /** * Set Size. * - * @param mixed $pValue A float representing the value of a positive measurement in points (1/72 of an inch) + * @param mixed $sizeInPoints A float representing the value of a positive measurement in points (1/72 of an inch) * * @return $this */ - public function setSize($pValue) + public function setSize($sizeInPoints) { - if (is_string($pValue) || is_int($pValue)) { - $pValue = (float) $pValue; // $pValue = 0 if given string is not numeric + if (is_string($sizeInPoints) || is_int($sizeInPoints)) { + $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric } // Size must be a positive floating point number // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 - if (!is_float($pValue) || !($pValue > 0)) { - $pValue = 10.0; + if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { + $sizeInPoints = 10.0; } if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['size' => $pValue]); + $styleArray = $this->getStyleArray(['size' => $sizeInPoints]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); } else { - $this->size = $pValue; + $this->size = $sizeInPoints; } return $this; From 05d3b9393cbae1a4079d9c23b71bf2ac47339c61 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 29 May 2021 05:13:28 -0700 Subject: [PATCH 19/21] Document Security - Coverage, Testing, and Bug-fixing (#2128) Having a parallel project to complete cover Document Properties, I turned my attention to to Document Security. As happens, this particular change grew a bit over time. Coverage and Testing Changes: - Since the Security object has no members which are themselves objects, there is no need for a deep clone. The untested __clone method is removed. - Almost all of the coverage for the Security Object came about through samples 11 and 41, not through formal tests with assertions. Formal tests have been added. - All methods now use type-hinting via the function signature rather than doc block. - Coverage is now 100%. Bug: - Xlsx Reader was not evaluating the Lock values correctly. This revelation came as a result of the new tests ... - Which showed that Xlsx Reader was testing SimpleXmlElement as a boolean rather than the stringified version of that ... - Which didn't matter all that much because Xlsx Writer was writing the values as 'true' or 'false' rather than '1' or '0', and (bool) 'false' is true. - Xlsx Reader clearly needed a change. I was trying to avoid that while awaiting the namespacing change. At least this is restricted to a very small self-contained piece of the code. - It is less clear whether Xlsx Writer should be changed. It is true that Excel itself uses 1/0 when writing; however it is equally true that it recognizes true/false as well as 1/0 when reading. For now, I have left Xlsx Writer alone to limit the change to what is absolutely needed. Other Changes: - I was at a complete loss as to what "lock revisions" was supposed to do, and it took a while to find anything on the web that explained it. Thank you, openpyxl, for coming through. I have documented it for PhpSpreadsheet now. Miscellaneous Note: - There remains no support for Document Security in Xls Reader or Writer (nor in any of the other readers/writers except Xlsx). - No Phpstan baseline changes, possibly for the first time in any of my PRs since Phpstan was introduced. Co-authored-by: Mark Baker --- docs/topics/recipes.md | 3 + src/PhpSpreadsheet/Document/Security.php | 129 ++++-------------- src/PhpSpreadsheet/Reader/Xlsx.php | 26 ++-- .../Document/SecurityTest.php | 75 ++++++++++ 4 files changed, 123 insertions(+), 110 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Document/SecurityTest.php diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index ff1c9f42..8854e55e 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -988,6 +988,9 @@ $security->setLockStructure(true); $security->setWorkbookPassword("PhpSpreadsheet"); ``` +Note that there are additional methods setLockRevision and setRevisionsPassword +which apply only to change tracking and history for shared workbooks. + ### Worksheet An example on setting worksheet security: diff --git a/src/PhpSpreadsheet/Document/Security.php b/src/PhpSpreadsheet/Document/Security.php index cef3db8c..bdcc2393 100644 --- a/src/PhpSpreadsheet/Document/Security.php +++ b/src/PhpSpreadsheet/Document/Security.php @@ -50,156 +50,87 @@ class Security /** * Is some sort of document security enabled? - * - * @return bool */ - public function isSecurityEnabled() + public function isSecurityEnabled(): bool { return $this->lockRevision || $this->lockStructure || $this->lockWindows; } - /** - * Get LockRevision. - * - * @return bool - */ - public function getLockRevision() + public function getLockRevision(): bool { return $this->lockRevision; } - /** - * Set LockRevision. - * - * @param bool $pValue - * - * @return $this - */ - public function setLockRevision($pValue) + public function setLockRevision(?bool $pValue): self { - $this->lockRevision = $pValue; + if ($pValue !== null) { + $this->lockRevision = $pValue; + } return $this; } - /** - * Get LockStructure. - * - * @return bool - */ - public function getLockStructure() + public function getLockStructure(): bool { return $this->lockStructure; } - /** - * Set LockStructure. - * - * @param bool $pValue - * - * @return $this - */ - public function setLockStructure($pValue) + public function setLockStructure(?bool $pValue): self { - $this->lockStructure = $pValue; + if ($pValue !== null) { + $this->lockStructure = $pValue; + } return $this; } - /** - * Get LockWindows. - * - * @return bool - */ - public function getLockWindows() + public function getLockWindows(): bool { return $this->lockWindows; } - /** - * Set LockWindows. - * - * @param bool $pValue - * - * @return $this - */ - public function setLockWindows($pValue) + public function setLockWindows(?bool $pValue): self { - $this->lockWindows = $pValue; + if ($pValue !== null) { + $this->lockWindows = $pValue; + } return $this; } - /** - * Get RevisionsPassword (hashed). - * - * @return string - */ - public function getRevisionsPassword() + public function getRevisionsPassword(): string { return $this->revisionsPassword; } - /** - * Set RevisionsPassword. - * - * @param string $pValue - * @param bool $pAlreadyHashed If the password has already been hashed, set this to true - * - * @return $this - */ - public function setRevisionsPassword($pValue, $pAlreadyHashed = false) + public function setRevisionsPassword(?string $pValue, bool $pAlreadyHashed = false): self { - if (!$pAlreadyHashed) { - $pValue = PasswordHasher::hashPassword($pValue); + if ($pValue !== null) { + if (!$pAlreadyHashed) { + $pValue = PasswordHasher::hashPassword($pValue); + } + $this->revisionsPassword = $pValue; } - $this->revisionsPassword = $pValue; return $this; } - /** - * Get WorkbookPassword (hashed). - * - * @return string - */ - public function getWorkbookPassword() + public function getWorkbookPassword(): string { return $this->workbookPassword; } - /** - * Set WorkbookPassword. - * - * @param string $pValue - * @param bool $pAlreadyHashed If the password has already been hashed, set this to true - * - * @return $this - */ - public function setWorkbookPassword($pValue, $pAlreadyHashed = false) + public function setWorkbookPassword(?string $pValue, bool $pAlreadyHashed = false): self { - if (!$pAlreadyHashed) { - $pValue = PasswordHasher::hashPassword($pValue); + if ($pValue !== null) { + if (!$pAlreadyHashed) { + $pValue = PasswordHasher::hashPassword($pValue); + } + $this->workbookPassword = $pValue; } - $this->workbookPassword = $pValue; 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; - } - } - } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index f07ac008..82e4e82d 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1808,17 +1808,9 @@ class Xlsx extends BaseReader return; } - if ($xmlWorkbook->workbookProtection['lockRevision']) { - $excel->getSecurity()->setLockRevision((bool) $xmlWorkbook->workbookProtection['lockRevision']); - } - - if ($xmlWorkbook->workbookProtection['lockStructure']) { - $excel->getSecurity()->setLockStructure((bool) $xmlWorkbook->workbookProtection['lockStructure']); - } - - if ($xmlWorkbook->workbookProtection['lockWindows']) { - $excel->getSecurity()->setLockWindows((bool) $xmlWorkbook->workbookProtection['lockWindows']); - } + $excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')); + $excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')); + $excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')); if ($xmlWorkbook->workbookProtection['revisionsPassword']) { $excel->getSecurity()->setRevisionsPassword( @@ -1835,6 +1827,18 @@ class Xlsx extends BaseReader } } + private static function getLockValue(SimpleXmlElement $protection, string $key): ?bool + { + $returnValue = null; + $protectKey = $protection[$key]; + if (!empty($protectKey)) { + $protectKey = (string) $protectKey; + $returnValue = $protectKey !== 'false' && (bool) $protectKey; + } + + return $returnValue; + } + private function readFormControlProperties(Spreadsheet $excel, ZipArchive $zip, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void { if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { diff --git a/tests/PhpSpreadsheetTests/Document/SecurityTest.php b/tests/PhpSpreadsheetTests/Document/SecurityTest.php new file mode 100644 index 00000000..c2c66577 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Document/SecurityTest.php @@ -0,0 +1,75 @@ +getActiveSheet()->getCell('A1')->setValue('Hello'); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $hashedRevisionsPassword = $security->getRevisionsPassword(); + self::assertNotEquals($revisionsPassword, $hashedRevisionsPassword); + $security->setLockWindows(true); + $security->setLockStructure(true); + $workbookPassword = 'wbpasswd'; + $security->setWorkbookPassword($workbookPassword); + $hashedWorkbookPassword = $security->getWorkbookPassword(); + self::assertNotEquals($workbookPassword, $hashedWorkbookPassword); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedSecurity = $reloadedSpreadsheet->getSecurity(); + self::assertTrue($reloadedSecurity->getLockRevision()); + self::assertTrue($reloadedSecurity->getLockWindows()); + self::assertTrue($reloadedSecurity->getLockStructure()); + self::assertSame($hashedWorkbookPassword, $reloadedSecurity->getWorkbookPassword()); + self::assertSame($hashedRevisionsPassword, $reloadedSecurity->getRevisionsPassword()); + + $reloadedSecurity->setRevisionsPassword($hashedWorkbookPassword, true); + self::assertSame($hashedWorkbookPassword, $reloadedSecurity->getRevisionsPassword()); + $reloadedSecurity->setWorkbookPassword($hashedRevisionsPassword, true); + self::assertSame($hashedRevisionsPassword, $reloadedSecurity->getWorkbookPassword()); + } + + public function providerLocks(): array + { + return [ + [false, false, false], + [false, false, true], + [false, true, false], + [false, true, true], + [true, false, false], + [true, false, true], + [true, true, false], + [true, true, true], + ]; + } + + /** + * @dataProvider providerLocks + */ + public function testLocks(bool $revision, bool $windows, bool $structure): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->getActiveSheet()->getCell('A1')->setValue('Hello'); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision($revision); + $security->setLockWindows($windows); + $security->setLockStructure($structure); + $enabled = $security->isSecurityEnabled(); + self::assertSame($enabled, $revision || $windows || $structure); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $reloadedSecurity = $reloadedSpreadsheet->getSecurity(); + self::assertSame($revision, $reloadedSecurity->getLockRevision()); + self::assertSame($windows, $reloadedSecurity->getLockWindows()); + self::assertSame($structure, $reloadedSecurity->getLockStructure()); + } +} From 781b2470f693e273738c848db6338506c6d376b1 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 30 May 2021 12:27:56 +0200 Subject: [PATCH 20/21] Documentation updates (#2131) * Update notes in documentation for memory sizing on 32-bit and 64-bit PHP versions * Additional notes on the fact that PHPSpreadsheet does not change cell addresses when loading a spreadsheet using a Read Filter --- docs/topics/memory_saving.md | 2 +- docs/topics/reading-and-writing-to-file.md | 5 +++++ docs/topics/reading-files.md | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/topics/memory_saving.md b/docs/topics/memory_saving.md index 157bb704..e52a83e4 100644 --- a/docs/topics/memory_saving.md +++ b/docs/topics/memory_saving.md @@ -1,6 +1,6 @@ # Memory saving -PhpSpreadsheet uses an average of about 1k per cell in your worksheets, so +PhpSpreadsheet uses an average of about 1k per cell (1.6k on 64-bit PHP) in your worksheets, so large workbooks can quickly use up available memory. Cell caching provides a mechanism that allows PhpSpreadsheet to maintain the cell objects in a smaller size of memory, or off-memory (eg: on disk, in APCu, diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 9e4376e9..e5c2afd9 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -139,6 +139,11 @@ $reader->setReadFilter( new MyReadFilter() ); $spreadsheet = $reader->load("06largescale.xlsx"); ``` +Read Filtering does not renumber cell rows and columns. If you filter to read only rows 100-200, cells that you read will still be numbered A100-A200, not A1-A101. Cells A1-A99 will not be loaded, but if you then try to call `getCell()` for a cell outside your loaded range, then PHPSpreadsheet will create a new cell with a null value. + +Methods such as `toArray()` assume that all cells in a spreadsheet has been loaded from A1, so will return null values for rows and columns that fall outside your filter range: it is recommended that you keep track of the range that your filter has requested, and use `rangeToArray()` instead. + + ### \PhpOffice\PhpSpreadsheet\Writer\Xlsx #### Writing a spreadsheet diff --git a/docs/topics/reading-files.md b/docs/topics/reading-files.md index 1451f2ab..e1a1a179 100644 --- a/docs/topics/reading-files.md +++ b/docs/topics/reading-files.md @@ -324,6 +324,10 @@ to read and process a large workbook in "chunks": an example of this usage might be when transferring data from an Excel worksheet to a database. +Read Filtering does not renumber cell rows and columns. If you filter to read only rows 100-200, cells that you read will still be numbered A100-A200, not A1-A101. Cells A1-A99 will not be loaded, but if you then try to call `getCell()` for a cell outside your loaded range, then PHPSpreadsheet will create a new cell with a null value. + +Methods such as `toArray()` assume that all cells in a spreadsheet has been loaded from A1, so will return null values for rows and columns that fall outside your filter range: it is recommended that you keep track of the range that your filter has requested, and use `rangeToArray()` instead. + ```php $inputFileType = 'Xls'; $inputFileName = './sampleData/example2.xls'; From b533f43f75076d6816e4793c93ab684d59a41e4b Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sat, 29 May 2021 23:20:23 -0700 Subject: [PATCH 21/21] Improve Coverage for HashTable, Fix Clone Add unit tests to cover all of HashTable. I was hoping to do this without source changes, but this class does require a deep clone, and, as the new unit tests revealed, the existing code did not fill the bill - it cloned objects, but not arrays which contained objects, and all the object variables in this class are arrays which can contain objects. --- src/PhpSpreadsheet/HashTable.php | 11 ++- tests/PhpSpreadsheetTests/HashTableTest.php | 92 +++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/HashTableTest.php diff --git a/src/PhpSpreadsheet/HashTable.php b/src/PhpSpreadsheet/HashTable.php index 5d4444e7..8cf8281e 100644 --- a/src/PhpSpreadsheet/HashTable.php +++ b/src/PhpSpreadsheet/HashTable.php @@ -170,8 +170,15 @@ class HashTable { $vars = get_object_vars($this); foreach ($vars as $key => $value) { - if (is_object($value)) { - $this->$key = clone $value; + // each member of this class is an array + if (is_array($value)) { + $array1 = $value; + foreach ($array1 as $key1 => $value1) { + if (is_object($value1)) { + $array1[$key1] = clone $value1; + } + } + $this->$key = $array1; } } } diff --git a/tests/PhpSpreadsheetTests/HashTableTest.php b/tests/PhpSpreadsheetTests/HashTableTest.php new file mode 100644 index 00000000..09c2fc92 --- /dev/null +++ b/tests/PhpSpreadsheetTests/HashTableTest.php @@ -0,0 +1,92 @@ +setAuthor('Author1'); + $comment2 = new Comment(); + $comment2->setAuthor('Author2'); + + return [$comment1, $comment2]; + } + + /** + * @param mixed $comment + */ + public static function getAuthor($comment): string + { + return ($comment instanceof Comment) ? $comment->getAuthor() : ''; + } + + public function testAddRemoveClear(): void + { + $array1 = self::createArray(); + $hash1 = new HashTable($array1); + self::assertSame(2, $hash1->count()); + $comment3 = new Comment(); + $comment3->setAuthor('Author3'); + $hash1->add($comment3); + $comment4 = new Comment(); + $comment4->setAuthor('Author4'); + $hash1->add($comment4); + $comment5 = new Comment(); + $comment5->setAuthor('Author5'); + // don't add comment5 + self::assertSame(4, $hash1->count()); + self::assertNull($hash1->getByIndex(10)); + $comment = $hash1->getByIndex(2); + self::assertSame('Author3', self::getAuthor($comment)); + $hash1->remove($comment3); + self::assertSame(3, $hash1->count()); + $comment = $hash1->getByIndex(2); + self::assertSame('Author4', self::getAuthor($comment)); + $hash1->remove($comment5); + self::assertSame(3, $hash1->count(), 'Remove non-hash member'); + $comment = $hash1->getByIndex(2); + self::assertSame('Author4', self::getAuthor($comment)); + self::assertNull($hash1->getByHashCode('xyz')); + $hash1->clear(); + self::AssertSame(0, $hash1->count()); + } + + public function testToArray(): void + { + $array1 = self::createArray(); + $count1 = count($array1); + $hash1 = new HashTable($array1); + $array2 = $hash1->toArray(); + self::assertCount($count1, $array2); + $idx = 0; + foreach ($array2 as $key => $value) { + self::assertEquals($array1[$idx], $value, "Item $idx"); + self::assertSame($idx, $hash1->getIndexForHashCode($key)); + ++$idx; + } + } + + public function testClone(): void + { + $array1 = self::createArray(); + $hash1 = new HashTable($array1); + $hash2 = new HashTable(); + self::assertSame(0, $hash2->count()); + $hash2->addFromSource(); + self::assertSame(0, $hash2->count()); + $hash2->addFromSource($array1); + self::assertSame(2, $hash2->count()); + self::assertEquals($hash1, $hash2, 'Add in constructor same as addFromSource'); + $hash3 = clone $hash1; + self::assertEquals($hash1, $hash3, 'Clone equal to original'); + self::assertSame($hash1->getByIndex(0), $hash2->getByIndex(0)); + self::assertEquals($hash1->getByIndex(0), $hash3->getByIndex(0)); + self::assertNotSame($hash1->getByIndex(0), $hash3->getByIndex(0)); + } +}