diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index fae240d38..a3d568ddd 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -28,7 +28,7 @@ ✔ ✔ ● - ● 6 + ● ✔ N/A N/A @@ -513,7 +513,7 @@ ✔ ✔ ● - ● + ✖ ✔ N/A ● @@ -524,7 +524,7 @@ ✔ ✔ ✔ - ● + ● 6 ✔ N/A ✔ @@ -1025,7 +1025,7 @@ 3. Only BIFF8 files support alignment and rotation. Prior to that, comments could only be unformatted text 4. Xlsx forms and controls can be read and written but not otherwise manipulated 5. Xlsx macros can be read and written; their values can be retrieved and changed, but only in a binary form which is unlikely to be useful -6. There is very limited support for reading styles from an Ods spreadsheet. Writing styles has better support, although Number Format is incomplete. +6. There is very limited support for reading styles from an Ods spreadsheet. Writing styles has much better support. Starting with release 5.5.0, Number Format Writer supports many common styles, and users may extend support to additional styles; Reader support is improved but still imperfect. 7. In most cases, Html reader processes only inline styles; styles provided by Css classes may be ignored. 8. Code must [opt in](../topics/recipes.md#array-formulas) to array output. 9. Use with caution - allowing external images may can subject the caller to security exploits. Starting with release 4.5.0 (also earlier releases 3.9.3, 2.3.10, 2.1.11, and 1.29.12), code can allow or not external images. In those starting releases, and in earlier releases which do not offer an option, default is to allow it. In release 5+ (and earlier supported versions 1.30+, 2.1.12+, 2.4+, and 3.10+), the default is to not allow it. @@ -1293,7 +1293,7 @@ Number Format Mask ✔ ✔ - ● + ✔6 N/A ✔ ✔ diff --git a/samples/Basic4/50_xlsverticalbreak.php b/samples/Basic5/50_xlsverticalbreak.php similarity index 100% rename from samples/Basic4/50_xlsverticalbreak.php rename to samples/Basic5/50_xlsverticalbreak.php diff --git a/samples/Basic4/51_ProtectedSort.php b/samples/Basic5/51_ProtectedSort.php similarity index 100% rename from samples/Basic4/51_ProtectedSort.php rename to samples/Basic5/51_ProtectedSort.php diff --git a/samples/Basic4/52_Currency.php b/samples/Basic5/52_Currency.php similarity index 100% rename from samples/Basic4/52_Currency.php rename to samples/Basic5/52_Currency.php diff --git a/samples/Basic4/53_ImageOpacity.php b/samples/Basic5/53_ImageOpacity.php similarity index 100% rename from samples/Basic4/53_ImageOpacity.php rename to samples/Basic5/53_ImageOpacity.php diff --git a/samples/Basic4/54_ImagesInAndOut.php b/samples/Basic5/54_ImagesInAndOut.php similarity index 100% rename from samples/Basic4/54_ImagesInAndOut.php rename to samples/Basic5/54_ImagesInAndOut.php diff --git a/samples/Basic5/55_DefinedStyles.php b/samples/Basic5/55_DefinedStyles.php new file mode 100644 index 000000000..3f70cf0de --- /dev/null +++ b/samples/Basic5/55_DefinedStyles.php @@ -0,0 +1,136 @@ +getActiveSheet(); +$array = [ + ['NUMBER', 1, NumberFormat::FORMAT_NUMBER], + ['NUMBER_0', 1, NumberFormat::FORMAT_NUMBER_0], + ['NUMBER_00', 1, NumberFormat::FORMAT_NUMBER_00], + ['NUMBER_COMMA_SEPARATED1', 1234, NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1], + ['NUMBER_COMMA_SEPARATED2', 1234, NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED2], + ['PERCENTAGE', 0.98, NumberFormat::FORMAT_PERCENTAGE], + ['PERCENTAGE_0', 0.985, NumberFormat::FORMAT_PERCENTAGE_0], + ['PERCENTAGE_00', 0.9856, NumberFormat::FORMAT_PERCENTAGE_00], + ['DATE_YYYYMMDD', 46000, NumberFormat::FORMAT_DATE_YYYYMMDD], + ['DATE_DDMMYYYY', 46000, NumberFormat::FORMAT_DATE_DDMMYYYY], + ['DATE_DMYSLASH', 46000, NumberFormat::FORMAT_DATE_DMYSLASH], + ['DATE_DMYMINUS', 46000, NumberFormat::FORMAT_DATE_DMYMINUS], + ['DATE_DMMINUS', 46000, NumberFormat::FORMAT_DATE_DMMINUS], + ['DATE_MYMINUS', 46000, NumberFormat::FORMAT_DATE_MYMINUS], + ['DATE_XLSX14', 46000, NumberFormat::FORMAT_DATE_XLSX14], + ['DATE_XLSX14_ACTUAL', 46000, NumberFormat::FORMAT_DATE_XLSX14_ACTUAL], + ['DATE_XLSX15', 46000, NumberFormat::FORMAT_DATE_XLSX15], + ['DATE_XLSX15_YYYY', 46000, NumberFormat::FORMAT_DATE_XLSX15_YYYY], + ['DATE_XLSX16', 46000, NumberFormat::FORMAT_DATE_XLSX16], + ['DATE_XLSX17', 46000, NumberFormat::FORMAT_DATE_XLSX17], + ['DATE_XLSX22', 46000, NumberFormat::FORMAT_DATE_XLSX22], + ['DATE_XLSX22_ACTUAL', 46000, NumberFormat::FORMAT_DATE_XLSX22_ACTUAL], + ['DATE_DATETIME', 46000.25, NumberFormat::FORMAT_DATE_DATETIME], + ['DATE_DATETIME_BETTER', 46000.25, NumberFormat::FORMAT_DATE_DATETIME_BETTER], + ['DATE_TIME1', 46000.25, NumberFormat::FORMAT_DATE_TIME1], + ['DATE_TIME2', 46000.25, NumberFormat::FORMAT_DATE_TIME2], + ['DATE_TIME3', 46000.25, NumberFormat::FORMAT_DATE_TIME3], + ['DATE_TIME4', 46000.25, NumberFormat::FORMAT_DATE_TIME4], + ['DATE_TIME5', 46000.25, NumberFormat::FORMAT_DATE_TIME5], + ['DATE_TIME6', 46000.25, NumberFormat::FORMAT_DATE_TIME6], + ['DATE_TIME7', 46000.25, NumberFormat::FORMAT_DATE_TIME7], + ['DATE_TIME8', 46000.25, NumberFormat::FORMAT_DATE_TIME8], + [' DATE_TIME_INTERVAL_HMS', 0.0087731481481482, NumberFormat::FORMAT_DATE_TIME_INTERVAL_HMS], + ['DATE_YYYYMMDDSLASH', 46000, NumberFormat::FORMAT_DATE_YYYYMMDDSLASH], + ['DATE_LONG_DATE', 46000, NumberFormat::FORMAT_DATE_LONG_DATE], + ['CURRENCY_USD_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_USD_INTEGER], + ['CURRENCY_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD], + ['CURRENCY_EUR_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR_INTEGER], + ['CURRENCY_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR], + ['ACCOUNTING_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD], + ['ACCOUNTING_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR], + ['CUSTOM1', 1234.56, '0.000'], + ['CUSTOM2', 1234.56, '"$"#,##0.00_);[Red]\("$"#,##0.00\)'], +]; +$row = 0; +$helper->log('Populate spreadsheet'); +foreach ($array as $cells) { + ++$row; + $sheet->getCell("A$row")->setValue($cells[0]); + $sheet->getCell("B$row")->setValue($cells[1]); + if (!str_starts_with($cells[0], 'DATE')) { + $sheet->getCell("C$row")->setValue(-$cells[1]); + } + $sheet->getStyle("B$row:C$row") + ->getNumberFormat() + ->setFormatCode($cells[2]); +} +$sheet->getColumnDimension('A')->setAutoSize(true); +$sheet->getColumnDimension('B')->setAutoSize(true); +$sheet->getColumnDimension('C')->setAutoSize(true); +$sheet->setSelectedCells('A1'); + +function threeDecimalPlaces(Style $obj, string $name): void +{ + $writer = $obj->getWriter(); + $writer->startElement('number:number-style'); + $writer->writeAttribute('style:name', $name); + $writer->startElement('number:number'); + $writer->writeAttribute('number:decimal-places', '3'); + $writer->writeAttribute('number:min-decimal-places', '3'); + $writer->writeAttribute('number:min-integer-digits', '1'); + $writer->endElement(); // number:number + $writer->endElement(); // number:number-style +} + +function redBrackets(Style $obj, string $name): void +{ + $writer = $obj->getWriter(); + $writer->startElement('number:currency-style'); + $writer->writeAttribute('style:name', $name . 'P0'); + $writer->writeElement('number:currency-symbol', '$'); + $writer->startElement('number:number'); + $writer->writeAttribute('number:decimal-places', '2'); + $writer->writeAttribute('number:min-decimal-places', '2'); + $writer->writeAttribute('number:min-integer-digits', '1'); + $writer->writeAttribute('number:grouping', 'true'); + $writer->endElement(); // number:number + $writer->writeElement('number:text', ' '); + $writer->endElement(); // number:currency-style + + $writer->startElement('number:currency-style'); + $writer->writeAttribute('style:name', $name); + $writer->startElement('style:text-properties'); + $writer->writeAttribute('fo:color', '#FF0000'); + $writer->endElement(); // style:text-properties + $writer->writeElement('number:text', '('); + $writer->startElement('number:number'); + $writer->writeAttribute('number:decimal-places', '2'); + $writer->writeAttribute('number:min-decimal-places', '2'); + $writer->writeAttribute('number:min-integer-digits', '1'); + $writer->writeAttribute('number:grouping', 'true'); + $writer->endElement(); // number:number + $writer->writeElement('number:text', ')'); + $writer->startElement('style:map'); + $writer->writeAttribute('style:condition', 'value()>=0'); + $writer->writeAttribute('style:apply-style-name', $name . 'P0'); + $writer->endElement(); // style:map + $writer->endElement(); // number:currency-style +} + +function writeAdditional(BaseWriter $writer): void +{ + if (method_exists($writer, 'useAdditionalNumberFormats')) { + $array = [ + '0.000' => threeDecimalPlaces(...), + '"$"#,##0.00_);[Red]\("$"#,##0.00\)' => redBrackets(...), + ]; + $writer->useAdditionalNumberFormats($array); + } +} + +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Ods'], writerCallback: writeAdditional(...)); +$spreadsheet->disconnectWorksheets(); diff --git a/samples/Basic5/56_OdsToISO8601.php b/samples/Basic5/56_OdsToISO8601.php new file mode 100644 index 000000000..2ff43e31b --- /dev/null +++ b/samples/Basic5/56_OdsToISO8601.php @@ -0,0 +1,91 @@ +open($infile) !== true) { + throw new Exception("unable to open zip $infile"); + } + $files = ['content.xml', 'styles.xml']; + $modified = false; + foreach ($files as $file) { + $data = $zip->getFromName($file); + if ($data === false) { + throw new Exception("unable to read member $file"); + } + $newData = str_replace('', '', $data); + $newData = preg_replace( + '~' + . '()' + . '[/-]' + . '()' + . '[/-]' + . '()' + . '~', + '$3-$2' + . '-$1', + $newData + ) ?? $newData; + $newData = preg_replace( + '~' + . '()' + . '[/-]' + . '()' + . '[/-]' + . '()' + . '~', + '$3-$1' + . '-$2', + $newData + ) ?? $newData; + $newData = preg_replace( + '~' + . '(]*)' + . ' number:automatic-order="true"' + . '~', + '$1', + $newData + ) ?? $newData; + if ($data === $newData) { + $helper->log("no changes needed for $file"); + } else { + $zip->deleteName($file); + $zip->addFromString($file, $newData); + $helper->log("modified $file"); + $modified = true; + } + } + $zip->close(); + if ($modified) { + $helper->log("Modified $infile"); + } else { + $helper->log("No modifications to $infile"); + } +} + +$infileBase = '56_MixedDateFormats.ods'; +$infileBase1 = __DIR__ . '/../templates/' . $infileBase; +$infile = realpath($infileBase1); +if ($infile === false) { + throw new Exception("Unable to locate $infileBase1"); +} + +/** @var Sample $helper */ +$helper->log("Infile is $infile"); +$outDirectory = $helper->getTemporaryFolder(); +$helper->log("outDirectory is $outDirectory"); +$outfile = $outDirectory . '/56_OdsToISO8601.ods'; +$helper->log("Outfile is $outfile"); +$helper->log('Attempting copy'); +if (!copy($infile, $outfile)) { + throw new Exception('Copy failed'); +} + +$helper->log('Update date formatting xml'); +datesToIso8601($outfile, $helper); + +$helper->addDownloadLink($outfile); diff --git a/samples/Header.php b/samples/Header.php index 670776851..1516243ea 100644 --- a/samples/Header.php +++ b/samples/Header.php @@ -45,6 +45,14 @@ if ($helper->isCli()) { PHPSpreadsheet +       + +   + +       + +   + diff --git a/samples/templates/56_MixedDateFormats.ods b/samples/templates/56_MixedDateFormats.ods new file mode 100644 index 000000000..8646387df Binary files /dev/null and b/samples/templates/56_MixedDateFormats.ods differ diff --git a/src/PhpSpreadsheet/Helper/Downloader.php b/src/PhpSpreadsheet/Helper/Downloader.php index 0c39efe04..a969f3628 100644 --- a/src/PhpSpreadsheet/Helper/Downloader.php +++ b/src/PhpSpreadsheet/Helper/Downloader.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Helper; +use DateTimeImmutable; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Exception; /** @@ -90,7 +92,8 @@ class Downloader // If you're serving to IE over SSL, then the following may be needed header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified + $dt = new DateTimeImmutable(timezone: new DateTimeZone('UTC')); + header('Last-Modified: ' . $dt->format('D, d M Y H:i:s') . ' GMT'); // always modified header('Cache-Control: cache, must-revalidate'); // HTTP/1.1 header('Pragma: public'); // HTTP/1.0 } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index b1283bb0b..8cdfe41c0 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -131,16 +131,21 @@ class Sample $callStartTime = microtime(true); $writer->save($path); $this->logWrite($writer, $path, $callStartTime); - if ($this->isCli() === false) { - // @codeCoverageIgnoreStart - echo 'Download ' . basename($path) . '
'; - // @codeCoverageIgnoreEnd - } + $this->addDownloadLink($path); } $this->logEndingNotes(); } + public function addDownloadLink(string $path): void + { + if ($this->isCli() === false) { + // @codeCoverageIgnoreStart + echo 'Download ' . basename($path) . '
'; + // @codeCoverageIgnoreEnd + } + } + protected function isDirOrMkdir(string $folder): bool { return \is_dir($folder) || \mkdir($folder); diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 825b84d45..da85ecdf9 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -2,6 +2,9 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use Composer\Pcre\Preg; +use DateTime; +use DateTimeZone; use DOMAttr; use DOMDocument; use DOMElement; @@ -684,6 +687,7 @@ class Ods extends BaseReader } if (count($paragraphs) > 0) { + $dataValue = null; // Consolidate if there are multiple p records (maybe with spans as well) $dataArray = []; @@ -718,33 +722,49 @@ class Ods extends BaseReader break; case 'percentage': + if (!str_contains($allCellDataText, '.')) { + $formatting = NumberFormat::FORMAT_PERCENTAGE; + } elseif (substr($allCellDataText, -3, 1) === '.') { + $formatting = NumberFormat::FORMAT_PERCENTAGE_0; + } else { + $formatting = NumberFormat::FORMAT_PERCENTAGE_00; + } $type = DataType::TYPE_NUMERIC; $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - // percentage should always be float - //if (floor($dataValue) == $dataValue) { - // $dataValue = (int) $dataValue; - //} - $formatting = NumberFormat::FORMAT_PERCENTAGE_00; - break; case 'currency': $type = DataType::TYPE_NUMERIC; $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - if (floor($dataValue) == $dataValue) { - $dataValue = (int) $dataValue; + $currency = $cellData->getAttributeNS($officeNs, 'currency'); + if ($currency === 'USD') { + $typeValue = 'currency'; + $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER; + } elseif ($currency === 'EUR' || str_contains($allCellDataText, '€')) { + $typeValue = 'currency'; + $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_EUR : NumberFormat::FORMAT_CURRENCY_EUR_INTEGER; } - $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER; break; case 'float': $type = DataType::TYPE_NUMERIC; $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); - if (floor($dataValue) == $dataValue) { - if ($dataValue == (int) $dataValue) { - $dataValue = (int) $dataValue; + if (str_starts_with($allCellDataText, '$')) { + $formatting = str_contains($allCellDataText, '.') ? NumberFormat::FORMAT_CURRENCY_USD : NumberFormat::FORMAT_CURRENCY_USD_INTEGER; + } else { + if ($dataValue !== floor($dataValue)) { + // do nothing + } elseif (substr($allCellDataText, -2, 1) === '.') { + $formatting = NumberFormat::FORMAT_NUMBER_0; + } elseif (substr($allCellDataText, -3, 1) === '.') { + $formatting = NumberFormat::FORMAT_NUMBER_00; + } + if (floor($dataValue) == $dataValue) { + if ($dataValue == (int) $dataValue) { + $dataValue = (int) $dataValue; + } } } @@ -754,7 +774,11 @@ class Ods extends BaseReader $value = $cellData->getAttributeNS($officeNs, 'date-value'); $dataValue = Date::convertIsoDate($value); - if ($dataValue != floor($dataValue)) { + if (Preg::isMatch('/^\d\d\d\d-\d\d-\d\d$/', $allCellDataText)) { + $formatting = 'yyyy-mm-dd'; + } elseif (Preg::isMatch('/^\d\d?-[a-zA-Z]+-\d\d\d\d$/', $allCellDataText)) { + $formatting = 'd-mmm-yyyy'; + } elseif ($dataValue != floor($dataValue)) { $formatting = NumberFormat::FORMAT_DATE_XLSX15 . ' ' . NumberFormat::FORMAT_DATE_TIME4; @@ -767,13 +791,26 @@ class Ods extends BaseReader $type = DataType::TYPE_NUMERIC; $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); - - $dataValue = Date::PHPToExcel( - strtotime( - '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) - ) - ); - $formatting = NumberFormat::FORMAT_DATE_TIME4; + $minus = ''; + if (str_starts_with($timeValue, '-')) { + $minus = '-'; + $timeValue = substr($timeValue, 1); + } + $timeArray = sscanf($timeValue, 'PT%dH%dM%dS'); + if (is_array($timeArray)) { + /** @var array{int, int, int} $timeArray */ + $days = intdiv($timeArray[0], 24); + $hours = $timeArray[0] % 24; + $dt = new DateTime("1899-12-30 $hours:{$timeArray[1]}:{$timeArray[2]}", new DateTimeZone('UTC')); + $dt->modify("+$days days"); + $dataValue = Date::PHPToExcel($dt); + if ($minus === '-') { + $dataValue *= -1; + $formatting = '[hh]:mm:ss'; + } else { + $formatting = NumberFormat::FORMAT_DATE_TIME4; + } + } break; default: @@ -790,57 +827,55 @@ class Ods extends BaseReader $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); } - if ($type !== null) { // @phpstan-ignore-line - for ($i = 0; $i < $colRepeats; ++$i) { - if ($i > 0) { - StringHelper::stringIncrement($columnID); - } + for ($i = 0; $i < $colRepeats; ++$i) { + if ($i > 0) { + StringHelper::stringIncrement($columnID); + } - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { - continue; - } + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + continue; + } - if ($type !== DataType::TYPE_NULL) { - for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { - $rID = $rowID + $rowAdjust; + if ($type !== DataType::TYPE_NULL) { + for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { + $rID = $rowID + $rowAdjust; - $cell = $spreadsheet->getActiveSheet() - ->getCell($columnID . $rID); + $cell = $spreadsheet->getActiveSheet() + ->getCell($columnID . $rID); - // Set value - if ($hasCalculatedValue) { - $cell->setValueExplicit($cellDataFormula, $type); - if ($cellDataType === 'array') { - $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); - } - } elseif ($type !== '' || $dataValue !== null) { - $cell->setValueExplicit($dataValue, $type); + // Set value + if ($hasCalculatedValue) { + $cell->setValueExplicit($cellDataFormula, $type); + if ($cellDataType === 'array') { + $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); } + } elseif ($type !== '' || $dataValue !== null) { + $cell->setValueExplicit($dataValue, $type); + } - if ($hasCalculatedValue) { - $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); - } + if ($hasCalculatedValue) { + $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); + } - // Set other properties - if ($formatting !== null) { - $spreadsheet->getActiveSheet() - ->getStyle($columnID . $rID) - ->getNumberFormat() - ->setFormatCode($formatting); - } else { - $spreadsheet->getActiveSheet() - ->getStyle($columnID . $rID) - ->getNumberFormat() - ->setFormatCode(NumberFormat::FORMAT_GENERAL); - } + // Set other properties + if ($formatting !== null) { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode($formatting); + } else { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_GENERAL); + } - if ($hyperlink !== null) { - if ($hyperlink[0] === '#') { - $hyperlink = 'sheet://' . substr($hyperlink, 1); - } - $cell->getHyperlink() - ->setUrl($hyperlink); + if ($hyperlink !== null) { + if ($hyperlink[0] === '#') { + $hyperlink = 'sheet://' . substr($hyperlink, 1); } + $cell->getHyperlink() + ->setUrl($hyperlink); } } } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 28be160d8..ae0a386aa 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet; +use Composer\Pcre\Preg; use JsonSerializable; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; @@ -1060,7 +1061,7 @@ class Spreadsheet implements JsonSerializable // then look for local defined name (has priority over global defined name if both names exist) if ($worksheet !== null) { $wsTitle = StringHelper::strToUpper($worksheet->getTitle()); - $definedName = (string) preg_replace('/^.*!/', '', $definedName); + $definedName = Preg::replace('/^.*!/', '', $definedName); foreach ($this->definedNames as $dn) { $sheet = $dn->getScope() ?? $dn->getWorksheet(); $upper = StringHelper::strToUpper($dn->getName()); @@ -1863,6 +1864,37 @@ class Spreadsheet implements JsonSerializable } } + /** + * Change all 2-digit-year date styles to use 4-digit year; + * change all dd-mm-yyyy and mm-dd-yyyy styles to yyyy-mm-dd; + * dd-mmm-yyyy is unambiguous and left unchanged. + */ + public function disambiguateDateStyles(): void + { + foreach ($this->cellXfCollection as $style) { + $numberFormat = $style->getNumberFormat(); + $oldFormat = (string) $numberFormat->getFormatCode(); + $newFormat = Preg::replace('/\byy\b/i', 'yyyy', $oldFormat); + $newFormat = Preg::replace( + '~\bdd?(-|/|"-"|"/")' + . 'mm?(-|/|"-"|"/")' + . 'yyyy~', + 'yyyy-mm-dd', + $newFormat + ); + $newFormat = Preg::replace( + '~\bmm?(-|/|"-"|"/")' + . 'dd?(-|/|"-"|"/")' + . 'yyyy~', + 'yyyy-mm-dd', + $newFormat + ); + if ($newFormat !== $oldFormat) { + $numberFormat->setFormatCode($newFormat); + } + } + } + public function returnArrayAsArray(): void { $this->calculationEngine->setInstanceArrayReturnType( diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 07d0017ee..008990640 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -23,18 +23,20 @@ class NumberFormat extends Supervisor const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd'; const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy'; - const FORMAT_DATE_DMYSLASH = 'd/m/yy'; + const FORMAT_DATE_DMYSLASH = 'd"/"m"/"yy'; const FORMAT_DATE_DMYMINUS = 'd-m-yy'; const FORMAT_DATE_DMMINUS = 'd-m'; const FORMAT_DATE_MYMINUS = 'm-yy'; const FORMAT_DATE_XLSX14 = 'mm-dd-yy'; const FORMAT_DATE_XLSX14_ACTUAL = 'm/d/yyyy'; const FORMAT_DATE_XLSX15 = 'd-mmm-yy'; + const FORMAT_DATE_XLSX15_YYYY = 'd-mmm-yyyy'; const FORMAT_DATE_XLSX16 = 'd-mmm'; const FORMAT_DATE_XLSX17 = 'mmm-yy'; const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm'; const FORMAT_DATE_XLSX22_ACTUAL = 'm/d/yyyy h:mm'; const FORMAT_DATE_DATETIME = 'd/m/yy h:mm'; + const FORMAT_DATE_DATETIME_BETTER = 'yyyy-mm-dd hh:mm'; const FORMAT_DATE_TIME1 = 'h:mm AM/PM'; const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM'; const FORMAT_DATE_TIME3 = 'h:mm'; @@ -43,7 +45,8 @@ class NumberFormat extends Supervisor const FORMAT_DATE_TIME6 = 'h:mm:ss'; const FORMAT_DATE_TIME7 = 'i:s.S'; const FORMAT_DATE_TIME8 = 'h:mm:ss;@'; - const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@'; + const FORMAT_DATE_TIME_INTERVAL_HMS = '[hh]:mm:ss'; + const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy"/"mm"/"dd;@'; const FORMAT_DATE_LONG_DATE = 'dddd, mmmm d, yyyy'; const DATE_TIME_OR_DATETIME_ARRAY = [ @@ -61,6 +64,7 @@ class NumberFormat extends Supervisor self::FORMAT_DATE_XLSX22, self::FORMAT_DATE_XLSX22_ACTUAL, self::FORMAT_DATE_DATETIME, + self::FORMAT_DATE_DATETIME_BETTER, self::FORMAT_DATE_TIME1, self::FORMAT_DATE_TIME2, self::FORMAT_DATE_TIME3, @@ -69,12 +73,14 @@ class NumberFormat extends Supervisor self::FORMAT_DATE_TIME6, self::FORMAT_DATE_TIME7, self::FORMAT_DATE_TIME8, + self::FORMAT_DATE_TIME_INTERVAL_HMS, self::FORMAT_DATE_YYYYMMDDSLASH, self::FORMAT_DATE_LONG_DATE, ]; const TIME_OR_DATETIME_ARRAY = [ self::FORMAT_DATE_XLSX22, self::FORMAT_DATE_DATETIME, + self::FORMAT_DATE_DATETIME_BETTER, self::FORMAT_DATE_TIME1, self::FORMAT_DATE_TIME2, self::FORMAT_DATE_TIME3, @@ -83,6 +89,7 @@ class NumberFormat extends Supervisor self::FORMAT_DATE_TIME6, self::FORMAT_DATE_TIME7, self::FORMAT_DATE_TIME8, + self::FORMAT_DATE_TIME_INTERVAL_HMS, ]; const FORMAT_CURRENCY_USD_INTEGER = '$#,##0_-'; diff --git a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index 8b87062aa..213894d72 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use Throwable; @@ -121,17 +122,18 @@ class DateFormatter /** @param float|int $value value to be formatted */ public static function format(mixed $value, string $format): string { + if ($value < 0 && Preg::isMatch('/^\[?[hms]/i', $format)) { + return '-' . self::format(-$value, $format); + } // strip off first part containing e.g. [$-F800] or [$USD-409] // general syntax: [$-] // language info is in hexadecimal // strip off chinese part like [DBNum1][$-804] - $format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format); + $format = Preg::replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format); // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case; // but we don't want to change any quoted strings - /** @var callable $callable */ - $callable = [self::class, 'setLowercaseCallback']; - $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); + $format = Preg::replaceCallback('/(?:^|")([^"]*)(?:$|")/', self::setLowerCaseCallback(...), $format); // Only process the non-quoted blocks for date format characters @@ -159,9 +161,7 @@ class DateFormatter $format = implode('"', $blocks); // escape any quoted characters so that DateTime format() will render them correctly - /** @var callable $callback */ - $callback = [self::class, 'escapeQuotesCallback']; - $format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format); + $format = Preg::replaceCallback('/"(.*)"/U', self::escapeQuotesCallback(...), $format); try { $dateObj = Date::excelToDateTimeObject($value); @@ -171,7 +171,7 @@ class DateFormatter // If the colon preceding minute had been quoted, as happens in // Excel 2003 XML formats, m will not have been changed to i above. // Change it now. - $format = (string) \preg_replace('/\\\:m/', ':i', $format); + $format = Preg::replace('/\\\:m/', ':i', $format); $microseconds = (int) $dateObj->format('u'); if (str_contains($format, ':s.000')) { $milliseconds = (int) round($microseconds / 1000.0); @@ -207,15 +207,17 @@ class DateFormatter return $dateObj->format($format); } - /** @param string[] $matches */ + /** @param array $matches */ private static function setLowercaseCallback(array $matches): string { + /** @var string[] $matches */ return mb_strtolower($matches[0]); } - /** @param string[] $matches */ + /** @param array $matches */ private static function escapeQuotesCallback(array $matches): string { + /** @var string[] $matches */ return '\\' . implode('\\', mb_str_split($matches[1], 1, 'UTF-8')); } } diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 638c16f1e..13f7c0634 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -86,6 +86,12 @@ class Ods extends BaseWriter return $this->writerPartThumbnails; } + /** @param array $additionalNumberFormats */ + public function useAdditionalNumberFormats(array $additionalNumberFormats): void + { + $this->writerPartContent->additionalNumberFormats = $additionalNumberFormats; + } + /** * Save PhpSpreadsheet to file. * diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 5c3cf5277..0f1fb6faf 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; @@ -24,9 +25,19 @@ class Style private XMLWriter $writer; - public function __construct(XMLWriter $writer) + /** @var array */ + private array $additionalNumberFormats; + + /** @param array $additionalNumberFormats */ + public function __construct(XMLWriter $writer, array $additionalNumberFormats = []) { $this->writer = $writer; + $this->additionalNumberFormats = $additionalNumberFormats; + } + + public function getWriter(): XMLWriter + { + return $this->writer; } private function mapHorizontalAlignment(?string $horizontalAlignment): string @@ -344,12 +355,40 @@ class Style $this->writer->endElement(); // Close style:style } + private int $numFmtIndex = 199; + + /** @var array */ + private array $numFmtIndexes = []; + + private function writeNumFmt(string $numFmt): void + { + if (array_key_exists($numFmt, $this->numFmtIndexes)) { + return; + } + $method = self::NUMBER_FORMAT_METHODS[$numFmt] ?? $this->additionalNumberFormats[$numFmt] ?? null; + if ($method === null) { + return; + } + ++$this->numFmtIndex; + $name = 'N' . $this->numFmtIndex; + $this->numFmtIndexes[$numFmt] = $name; + $method($this, $name); + } + public function write(CellStyle $style): void { + $numFmt = (string) $style->getNumberFormat()->getFormatCode(); + $this->writeNumFmt($numFmt); $this->writer->startElement('style:style'); $this->writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex()); $this->writer->writeAttribute('style:family', 'table-cell'); $this->writer->writeAttribute('style:parent-style-name', 'Default'); + if (array_key_exists($numFmt, $this->numFmtIndexes)) { + $this->writer->writeAttribute( + 'style:data-style-name', + $this->numFmtIndexes[$numFmt] + ); + } // Alignment, fill colour, etc $this->writeCellProperties($style); @@ -360,4 +399,676 @@ class Style // End $this->writer->endElement(); // Close style:style } + + private const NUMBER_FORMAT_METHODS = [ + NumberFormat::FORMAT_NUMBER => [self::class, 'formatNumber'], + NumberFormat::FORMAT_NUMBER_0 => [self::class, 'formatNumber0'], + NumberFormat::FORMAT_NUMBER_00 => [self::class, 'formatNumber00'], + NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1 => [self::class, 'formatNumberCommaSeparated1'], + NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED2 => [self::class, 'formatNumberCommaSeparated2'], + NumberFormat::FORMAT_PERCENTAGE => [self::class, 'formatPercentage'], + NumberFormat::FORMAT_PERCENTAGE_0 => [self::class, 'formatPercentage0'], + NumberFormat::FORMAT_PERCENTAGE_00 => [self::class, 'formatPercentage00'], + NumberFormat::FORMAT_DATE_YYYYMMDD => [self::class, 'formatDateYyyymmdd'], + NumberFormat::FORMAT_DATE_DDMMYYYY => [self::class, 'formatDateDdmmyyyy'], + NumberFormat::FORMAT_DATE_DMYSLASH => [self::class, 'formatDateDmyslash'], + NumberFormat::FORMAT_DATE_DMYMINUS => [self::class, 'formatDateDmyminus'], + NumberFormat::FORMAT_DATE_DMMINUS => [self::class, 'formatDateDmminus'], + NumberFormat::FORMAT_DATE_MYMINUS => [self::class, 'formatDateMyminus'], + NumberFormat::FORMAT_DATE_XLSX14 => [self::class, 'formatDateXlsx14'], + NumberFormat::FORMAT_DATE_XLSX14_ACTUAL => [self::class, 'formatDateXlsx14Actual'], + NumberFormat::FORMAT_DATE_XLSX15 => [self::class, 'formatDateXlsx15'], + NumberFormat::FORMAT_DATE_XLSX15_YYYY => [self::class, 'formatDateXlsx15Yyyy'], + NumberFormat::FORMAT_DATE_XLSX16 => [self::class, 'formatDateXlsx16'], + NumberFormat::FORMAT_DATE_XLSX17 => [self::class, 'formatDateXlsx17'], + NumberFormat::FORMAT_DATE_XLSX22 => [self::class, 'formatDateXlsx22'], + NumberFormat::FORMAT_DATE_XLSX22_ACTUAL => [self::class, 'formatDateXlsx22Actual'], + NumberFormat::FORMAT_DATE_DATETIME => [self::class, 'formatDateDatetime'], + NumberFormat::FORMAT_DATE_DATETIME_BETTER => [self::class, 'formatDateDatetimeBetter'], + NumberFormat::FORMAT_DATE_TIME1 => [self::class, 'formatDateTime1'], + NumberFormat::FORMAT_DATE_TIME2 => [self::class, 'formatDateTime2'], + NumberFormat::FORMAT_DATE_TIME3 => [self::class, 'formatDateTime3'], + NumberFormat::FORMAT_DATE_TIME4 => [self::class, 'formatDateTime4'], + NumberFormat::FORMAT_DATE_TIME5 => [self::class, 'formatDateTime5'], + //NumberFormat::FORMAT_DATE_TIME6 => [self::class, 'formatDateTime6'], // FORMAT_DATE_TIME6 is identical to TIME4 + NumberFormat::FORMAT_DATE_TIME7 => [self::class, 'formatDateTime7'], // constant is probably mis-coded + NumberFormat::FORMAT_DATE_TIME8 => [self::class, 'formatDateTime8'], + NumberFormat::FORMAT_DATE_TIME_INTERVAL_HMS => [self::class, 'formatDateTimeIntervalHms'], + NumberFormat::FORMAT_DATE_YYYYMMDDSLASH => [self::class, 'formatDateYyyymmddslash'], + NumberFormat::FORMAT_DATE_LONG_DATE => [self::class, 'formatDateLongDate'], + NumberFormat::FORMAT_CURRENCY_USD_INTEGER => [self::class, 'formatCurrencyUsdInteger'], + NumberFormat::FORMAT_CURRENCY_USD => [self::class, 'formatCurrencyUsd'], + NumberFormat::FORMAT_ACCOUNTING_USD => [self::class, 'formatCurrencyUsd'], // ACCOUNTING and CURRENCY are same in Ods + NumberFormat::FORMAT_CURRENCY_EUR_INTEGER => [self::class, 'formatCurrencyEurInteger'], + NumberFormat::FORMAT_CURRENCY_EUR => [self::class, 'formatCurrencyEur'], + NumberFormat::FORMAT_ACCOUNTING_EUR => [self::class, 'formatCurrencyEur'], // ACCOUNTING and CURRENCY are same in Ods + ]; + + protected static function formatNumber(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->endElement(); // number:number-style + } + + protected static function formatNumber0(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '1'); + $obj->writer->writeAttribute('number:min-decimal-places', '1'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->endElement(); // number:number-style + } + + protected static function formatNumber00(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '2'); + $obj->writer->writeAttribute('number:min-decimal-places', '2'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->endElement(); // number:number-style + } + + protected static function formatNumberCommaSeparated1(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '2'); + $obj->writer->writeAttribute('number:min-decimal-places', '2'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->endElement(); // number:number + $obj->writer->endElement(); // number:number-style + } + + protected static function formatNumberCommaSeparated2(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '2'); + $obj->writer->writeAttribute('number:min-decimal-places', '2'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->endElement(); // number:number + $obj->writer->startElement('number:text'); + //$obj->writer->writeAttribute('loext:blank-width-char', '-'); + $obj->writer->text(' '); + $obj->writer->endElement(); // number:text + $obj->writer->endElement(); // number:number-style + } + + protected static function formatPercentage(self $obj, string $name): void + { + $obj->writer->startElement('number:percentage-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '0'); + $obj->writer->writeAttribute('number:min-decimal-places', '0'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->writeElement('number:text', '%'); + $obj->writer->endElement(); // number:percentage-style + } + + protected static function formatPercentage0(self $obj, string $name): void + { + $obj->writer->startElement('number:percentage-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '1'); + $obj->writer->writeAttribute('number:min-decimal-places', '1'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->writeElement('number:text', '%'); + $obj->writer->endElement(); // number:percentage-style + } + + protected static function formatPercentage00(self $obj, string $name): void + { + $obj->writer->startElement('number:percentage-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $obj->writer->writeAttribute('number:decimal-places', '2'); + $obj->writer->writeAttribute('number:min-decimal-places', '2'); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->endElement(); // number:number + $obj->writer->writeElement('number:text', '%'); + $obj->writer->endElement(); // number:percentage-style + } + + protected static function formatDateYyyymmdd(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDdmmyyyy(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDmyslash(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDmyminus(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDmminus(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:month'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateMyminus(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx14(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx14Actual(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx15(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:textual', 'true'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx15Yyyy(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:textual', 'true'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx16(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:textual', 'true'); + $obj->writer->endElement(); // number:month + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx17(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:textual', 'true'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->writeElement('number:year'); + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx22(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:year'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateXlsx22Actual(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDatetime(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->writeElement('number:month'); + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:year'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateDatetimeBetter(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '-'); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->writeElement('number:text', ' '); + $obj->writer->startElement('number:hours'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:hours + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->endElement(); // number:date-style + } + + protected static function formatDateTime1(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:am-pm'); + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime2(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:am-pm'); + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime3(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime4(self $obj, string $name): void + { + // TIME4 and TIME6 are identical + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime5(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime7(self $obj, string $name): void + { + // constant is probably mis-coded + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateTime8(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name . 'P0'); + $obj->writer->writeElement('number:hours'); + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->endElement(); // number:time-style + $obj->writer->startElement('number:text-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:text-content'); + $obj->writer->startElement('style:map'); + $obj->writer->writeAttribute('style:condition', 'value()>=0'); + $obj->writer->writeAttribute('style:apply-style-name', $name . 'P0'); + $obj->writer->endElement(); // number:style-map + $obj->writer->endElement(); // number:text-style + } + + protected static function formatDateTimeIntervalHms(self $obj, string $name): void + { + $obj->writer->startElement('number:time-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeAttribute( + 'number:truncate-on-overflow', + 'false' + ); + $obj->writer->startElement('number:hours'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:hours + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:minutes'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:minutes + $obj->writer->writeElement('number:text', ':'); + $obj->writer->startElement('number:seconds'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:seconds + $obj->writer->endElement(); // number:time-style + } + + protected static function formatDateYyyymmddslash(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name . 'P0'); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', '/'); + $obj->writer->startElement('number:day'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day + $obj->writer->endElement(); // number:date-style + $obj->writer->startElement('number:text-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->writeElement('number:text-content'); + $obj->writer->startElement('style:map'); + $obj->writer->writeAttribute('style:condition', 'value()>=0'); + $obj->writer->writeAttribute('style:apply-style-name', $name . 'P0'); + $obj->writer->endElement(); // number:style-map + $obj->writer->endElement(); // number:text-style + } + + protected static function formatDateLongDate(self $obj, string $name): void + { + $obj->writer->startElement('number:date-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:day-of-week'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:day-of-week + $obj->writer->writeElement('number:text', ', '); + $obj->writer->startElement('number:month'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->writeAttribute('number:textual', 'true'); + $obj->writer->endElement(); // number:month + $obj->writer->writeElement('number:text', ' '); + $obj->writer->writeElement('number:day'); + $obj->writer->writeElement('number:text', ', '); + $obj->writer->startElement('number:year'); + $obj->writer->writeAttribute('number:style', 'long'); + $obj->writer->endElement(); // number:year + $obj->writer->endElement(); // number:date-style + } + + protected static function formatCurrencyUsdInteger(self $obj, string $name): void + { + $obj->writer->startElement('number:number-style'); // not currency-style + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->WriteElement('number:text', '$'); + $obj->writer->startElement('number:number'); + $decimals = '0'; + $obj->writer->writeAttribute('number:decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->startElement('number:embedded-text'); + $obj->writer->writeAttribute('number-position', '0'); + $obj->writer->text(' '); + $obj->writer->endElement(); // number:embedded-text + $obj->writer->endElement(); // number:number + $obj->writer->endElement(); // number:number-style + } + + protected static function formatCurrencyUsd(self $obj, string $name): void + { + // Ods uses same format for Currency and Accounting + $obj->writer->startElement('number:number-style'); // NOT currency-style + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->WriteElement('number:text', '$'); + $obj->writer->startElement('number:number'); + $decimals = '2'; + $obj->writer->writeAttribute('number:decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->endElement(); // number:number + $obj->writer->writeElement('number:text', ' '); + $obj->writer->endElement(); // number:currency-style + } + + protected static function formatCurrencyEurInteger(self $obj, string $name): void + { + $obj->writer->startElement('number:currency-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $decimals = '0'; + $obj->writer->writeAttribute('number:decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->startElement('number:embedded-text'); + $obj->writer->writeAttribute('number:position', '0'); + $obj->writer->endElement(); // number:embedded-text + $obj->writer->endElement(); // number:number + $obj->writer->startElement('number:text'); + // $obj->writer->writeAttribute('loext:blank-width-char', '-'); + $obj->writer->text(' '); + $obj->writer->endElement(); // number:text + $obj->writer->startElement('number:currency-symbol'); + $obj->writer->writeAttribute('number:language', 'en'); + $obj->writer->writeAttribute('number:country', 'us'); + $obj->writer->text('€'); + $obj->writer->endElement(); // number:currency-symbol + + $obj->writer->endElement(); // number:currency-style + } + + protected static function formatCurrencyEur(self $obj, string $name): void + { + // Ods uses same format for Currency and Accounting + $obj->writer->startElement('number:currency-style'); + $obj->writer->writeAttribute('style:name', $name); + $obj->writer->startElement('number:number'); + $decimals = '2'; + $obj->writer->writeAttribute('number:decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-decimal-places', $decimals); + $obj->writer->writeAttribute('number:min-integer-digits', '1'); + $obj->writer->writeAttribute('number:grouping', 'true'); + $obj->writer->endElement(); // number:number + $obj->writer->startElement('number:text'); + // $obj->writer->writeAttribute('loext:blank-width-char', '-'); + $obj->writer->text(' '); + $obj->writer->endElement(); // number:text + $obj->writer->startElement('number:currency-symbol'); + $obj->writer->writeAttribute('number:language', 'en'); + $obj->writer->writeAttribute('number:country', 'us'); + $obj->writer->text('€'); + $obj->writer->endElement(); // number:currency-symbol + + $obj->writer->endElement(); // number:currency-style + } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 5aea6c2a4..6364cb440 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -8,8 +8,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\Ods; @@ -247,19 +249,99 @@ class Content extends WriterPart } } $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValueString())); - if (is_numeric($formulaValue)) { - $objWriter->writeAttribute('office:value-type', 'float'); - } else { - $objWriter->writeAttribute('office:value-type', 'string'); - } - $objWriter->writeAttribute('office:value', $formulaValue); - $objWriter->writeElement('text:p', $formulaValue); + if (!is_numeric($formulaValue)) { + $objWriter->writeAttribute( + 'office:value-type', + 'string' + ); + $objWriter->writeAttribute('office:value', $formulaValue); + $objWriter->writeElement('text:p', $formulaValue); - break; + break; + } + // no break case DataType::TYPE_NUMERIC: - $objWriter->writeAttribute('office:value-type', 'float'); - $objWriter->writeAttribute('office:value', $cell->getValueString()); - $objWriter->writeElement('text:p', $cell->getValueString()); + $holdWorksheet = $cell->getWorksheet(); + $holdSelected = $holdWorksheet->getSelectedCells(); + $holdSpreadsheet = $holdWorksheet->getParent(); + $holdActiveSheetIndex = $holdSpreadsheet?->getActiveSheetIndex(); + $formatted = $cell->getFormattedValue(); + $type = 'float'; + $valueType = 'value'; + $value = $cell->getCalculatedValueString(); + $numFmt = $cell->getStyle() + ->getNumberFormat() + ->getFormatCode() ?? ''; + $holdWorksheet->setSelectedCells($holdSelected); + if (isset($holdSpreadsheet, $holdActiveSheetIndex)) { + $holdSpreadsheet->setActiveSheetIndex( + $holdActiveSheetIndex + ); + } + if (Date::isDateTimeFormatCode($numFmt, true)) { + $valueCalc = $cell->getCalculatedValueString(); + if (Preg::isMatch('/[HhSs]/', $numFmt) && !Preg::isMatch('/[YyDd]/', $numFmt)) { + $minus = ''; + $type = 'time'; + $valueType = 'time-value'; + if (str_starts_with($valueCalc, '-')) { + $minus = '-'; + $absVal = fmod(abs((float) $value), 1.0); + $hms = (int) round(86400 * $absVal); + $hours = intdiv($hms, 3600); + $hms -= $hours * 3600; + $minutes = intdiv($hms, 60); + $seconds = $hms % 60; + $value = sprintf('-PT%02dH%02dM%02dS', $hours, $minutes, $seconds); + $formatted = sprintf('-%02d:%02d:%02d', $hours, $minutes, $seconds); + } else { + $hhmmss = NumberFormat::toFormattedString( + $value, + NumberFormat::FORMAT_DATE_TIME_INTERVAL_HMS + ); + $daysAndHours = 24 * (int) $valueCalc + (int) substr($hhmmss, 0, 2); + $value = "PT$daysAndHours" + . 'H' + . substr($hhmmss, 3, 2) + . 'M' + . substr($hhmmss, 6, 2) + . 'S'; + } + } else { + $type = 'date'; + $valueType = 'date-value'; + $value = NumberFormat::toFormattedString( + $value, + 'yyyy-mm-dd"T"hh:mm:ss' + ); + } + } elseif (str_ends_with($numFmt, '%')) { + $type = 'percentage'; + } + if ($numFmt === NumberFormat::FORMAT_CURRENCY_EUR || $numFmt === NumberFormat::FORMAT_CURRENCY_EUR_INTEGER) { + $objWriter->writeAttribute( + 'office:value-type', + 'currency' + ); + $objWriter->writeAttribute( + 'office:currency', + 'EUR' + ); + $objWriter->writeAttribute( + 'office:value', + $value + ); + } else { + $objWriter->writeAttribute( + 'office:value-type', + $type + ); + $objWriter->writeAttribute( + "office:$valueType", + $value + ); + } + $objWriter->writeElement('text:p', $formatted); break; case DataType::TYPE_INLINE: @@ -307,12 +389,15 @@ class Content extends WriterPart } } + /** @var array */ + public array $additionalNumberFormats = []; + /** * Write XF cell styles. */ private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void { - $styleWriter = new Style($writer); + $styleWriter = new Style($writer, $this->additionalNumberFormats); $sheetCount = $spreadsheet->getSheetCount(); for ($i = 0; $i < $sheetCount; ++$i) { diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 01b6e01a0..b64848c3e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -175,15 +175,30 @@ class OdsTest extends TestCase self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A1')->getDataType()); // Percentage (10%) self::assertEquals(0.1, $firstSheet->getCell('A1')->getValue()); + self::assertSame('10%', $firstSheet->getCell('A1')->getFormattedValue()); + self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A2')->getDataType()); // Percentage (10%) + self::assertEquals(0.1, $firstSheet->getCell('A2')->getValue()); + self::assertSame('10.00%', $firstSheet->getCell('A2')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A2')->getDataType()); // Percentage (10.00%) self::assertEquals(0.1, $firstSheet->getCell('A2')->getValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A4')->getDataType()); // Currency (€10.00) - self::assertEquals(10, $firstSheet->getCell('A4')->getValue()); + self::assertSame(10.00, $firstSheet->getCell('A4')->getValue()); + self::assertSame('10.00 €', $firstSheet->getCell('A4')->getFormattedValue()); + self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('B4')->getDataType()); // Currency ($10) + self::assertSame(10.00, $firstSheet->getCell('B4')->getValue()); + self::assertSame('$10 ', $firstSheet->getCell('B4')->getFormattedValue()); + self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('C4')->getDataType()); // Currency (€10.12) + self::assertEquals(10.12, $firstSheet->getCell('C4')->getValue()); + self::assertSame('10.12 €', $firstSheet->getCell('C4')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A5')->getDataType()); // Currency ($20) - self::assertEquals(20, $firstSheet->getCell('A5')->getValue()); + self::assertSame(20.00, $firstSheet->getCell('A5')->getValue()); + self::assertSame('$20 ', $firstSheet->getCell('A5')->getFormattedValue()); + self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('C5')->getDataType()); // Currency ($20.34) + self::assertEquals(20.34, $firstSheet->getCell('C5')->getValue()); + self::assertSame('$20.34 ', $firstSheet->getCell('C5')->getFormattedValue()); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php deleted file mode 100644 index 59b5cf3c3..000000000 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ReplaceBuiltinNumberFormatTest.php +++ /dev/null @@ -1,69 +0,0 @@ -spreadsheet !== null) { - $this->spreadsheet->disconnectWorksheets(); - $this->spreadsheet = null; - } - if ($this->reloadedSpreadsheet !== null) { - $this->reloadedSpreadsheet->disconnectWorksheets(); - $this->reloadedSpreadsheet = null; - } - } - - public function testReplaceBuiltinNumberFormat(): void - { - $spreadsheet = $this->spreadsheet = new Spreadsheet(); - $sheet = $this->spreadsheet->getActiveSheet(); - $sheet->fromArray([45486, 1023, 45487, 45488, 45489]); - $sheet->getStyle('A1')->getNumberFormat() - ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); - $sheet->getStyle('B1')->getNumberFormat() - ->setFormatCode('#,##0.00'); - $sheet->getStyle('C1')->getNumberFormat() - ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); - $sheet->getStyle('D1')->getNumberFormat() - ->setFormatCode('dd-MMM-yyyy'); - $sheet->getStyle('E1')->getNumberFormat() - ->setBuiltInFormatCode(16); - $values = $sheet->toArray(); - $expected = [[ - '7/13/2024', // builtin style 14 - '1,023.00', // #,##0.00 - '7/14/2024', // builtin style 14 - '15-Jul-2024', // dd-MMM-yyyy - '16-Jul', // builtin style 16 - ]]; - self::assertSame($expected, $values); - $this->reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); - $this->reloadedSpreadsheet->replaceBuiltinNumberFormat( - NumberFormat::SHORT_DATE_INDEX, - 'yyyy-mm-dd' - ); - $rsheet = $this->reloadedSpreadsheet->getActiveSheet(); - $newValues = $rsheet->toArray(); - $newExpected = [[ - '2024-07-13', // yyyy-mm-dd changed from builtin style 14 - '1,023.00', // unchanged #,##0.00 - '2024-07-14', // yyyy-mm-dd changed from builtin style 14 - '15-Jul-2024', // unchanged dd-MMM-yyyy - '16-Jul', // unchanged builtin style 16 - ]]; - self::assertSame($newExpected, $newValues); - } -} diff --git a/tests/PhpSpreadsheetTests/Style/ReplaceBuiltinNumberFormatTest.php b/tests/PhpSpreadsheetTests/Style/ReplaceBuiltinNumberFormatTest.php new file mode 100644 index 000000000..3c8c4a31d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/ReplaceBuiltinNumberFormatTest.php @@ -0,0 +1,101 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + public function testReplaceBuiltinNumberFormat(): void + { + $spreadsheet = $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->fromArray([45486, 1023, 45487, 45488, 45489, 45480, 45479, 45478, 45477, 45476, 45475]); + $sheet->getStyle('A1')->getNumberFormat() + ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); + $sheet->getStyle('B1')->getNumberFormat() + ->setFormatCode('#,##0.00'); + $sheet->getStyle('C1')->getNumberFormat() + ->setBuiltInFormatCode(NumberFormat::SHORT_DATE_INDEX); + $sheet->getStyle('D1')->getNumberFormat() + ->setFormatCode('dd-MMM-yyyy'); + $sheet->getStyle('E1')->getNumberFormat() + ->setBuiltInFormatCode(16); + $sheet->getStyle('F1')->getNumberFormat() + ->setFormatCode('d-MMM-yy'); + $sheet->getStyle('G1')->getNumberFormat() + ->setFormatCode('dd/m/yy'); + $sheet->getStyle('H1')->getNumberFormat() + ->setFormatCode('mm-dd-yy'); + $sheet->getStyle('I1')->getNumberFormat() + ->setFormatCode('yy-mm-dd'); + $sheet->getStyle('J1')->getNumberFormat() + ->setFormatCode('m/d/yyyy h:mm'); + $sheet->getStyle('K1')->getNumberFormat() + ->setFormatCode('d"/"m"/"yy'); + $values = $sheet->toArray(); + $expected = [[ + '7/13/2024', // builtin style 14 + '1,023.00', // #,##0.00 + '7/14/2024', // builtin style 14 + '15-Jul-2024', // dd-MMM-yyyy + '16-Jul', // builtin style 16 + '7-Jul-24', // d-MMM-yy + '06/7/24', // dd/m/yy + '07-05-24', // mm-dd-yy + '24-07-04', // yy-mm-dd + '7/3/2024 0:00', // m/d/yyyy h:mm + '2/7/24', // d"/"m"/"yy + ]]; + self::assertSame($expected, $values); + $spreadsheet->replaceBuiltinNumberFormat( + NumberFormat::SHORT_DATE_INDEX, + 'yyyy-mm-dd' + ); + $newValues = $sheet->toArray(); + $newExpected = [[ + '2024-07-13', // yyyy-mm-dd changed from builtin style 14 + '1,023.00', // unchanged #,##0.00 + '2024-07-14', // yyyy-mm-dd changed from builtin style 14 + '15-Jul-2024', // unchanged dd-MMM-yyyy + '16-Jul', // unchanged builtin style 16 + '7-Jul-24', // unchanged d-MMM-yy + '06/7/24', // unchanged dd/m/yy + '07-05-24', // unchanged mm-dd-yy + '24-07-04', // unchanged yy-mm-dd + '7/3/2024 0:00', // unchanged m/d/yyyy h:mm + '2/7/24', // unchanged d"/"m"/"yy + ]]; + self::assertSame($newExpected, $newValues); + $spreadsheet->disambiguateDateStyles(); + $newValues2 = $sheet->toArray(); + $newExpected2 = [[ + '2024-07-13', // unchanged yyyy-mm-dd + '1,023.00', // unchanged #,##0.00 + '2024-07-14', // unchanged yyyy-mm-dd + '15-Jul-2024', // unchanged dd-MMM-yyyy + '16-Jul', // unchanged builtin style 16 + '7-Jul-2024', // changed from d-MMM-yy to d-MMM-yyyy + '2024-07-06', // changed from dd/m/yy to yyyy-mm-dd + '2024-07-05', // changed from mm-dd-yy tp yyyy-mm-dd + '2024-07-04', // changed from yy-mm-dd to yyyy-mm-dd + '2024-07-03 0:00', // changed m/d/yyyy h:mm to yyyy-mm-dd h:mm + '2024-07-02', // changed d"/"m"/"yy to yyyy-mm-dd + ]]; + self::assertSame($newExpected2, $newValues2); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/DurationTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/DurationTest.php new file mode 100644 index 000000000..a9d1cc67a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/DurationTest.php @@ -0,0 +1,50 @@ +getActiveSheet(); + $sheet->getCell('E1')->setValue('=TIMEVALUE("8:15:01")'); + $sheet->getCell('E2')->setValue('=TIMEVALUE("9:17:35")'); + $sheet->getCell('E3')->setValue('=TIMEVALUE("8:02:23")'); + $sheet->getCell('F2')->setValue('=E2-E1'); + $sheet->getCell('F3')->setValue('=E3-E1'); + $sheet->getStyle('E1:F3')->getNumberFormat() + ->setFormatCode( + NumberFormat::FORMAT_DATE_TIME_INTERVAL_HMS + ); + $sheet->getCell('A1')->setValue(0.5); + $sheet->getStyle('A1')->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_PERCENTAGE_0); + $sheet->getCell('A2')->setValue(20); + $sheet->getStyle('A2')->getNumberFormat() + ->setFormatCode('$0.00'); + $sheet->getCell('A3')->setValue(20); + $sheet->getStyle('A3')->getNumberFormat() + ->setFormatCode('#.0'); + $sheet->getCell('A4')->setValue(46000); + $sheet->getStyle('A4')->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Ods'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('1:02:34', $rsheet->getCell('F2')->getFormattedValue()); + self::assertSame('-00:12:38', $rsheet->getCell('F3')->getFormattedValue()); + self::assertSame('50.0%', $rsheet->getCell('A1')->getFormattedValue()); + self::assertSame('$20.00 ', $rsheet->getCell('A2')->getFormattedValue()); + self::assertSame('20.0', $rsheet->getCell('A3')->getFormattedValue()); + self::assertSame('2025-12-09', $rsheet->getCell('A4')->getFormattedValue()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/Issue4798Test.php b/tests/PhpSpreadsheetTests/Writer/Ods/Issue4798Test.php new file mode 100644 index 000000000..4871a266c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/Issue4798Test.php @@ -0,0 +1,37 @@ +getActiveSheet(); + $sheet->setCellValue('A1', 1); + $sheet->setCellValue('A2', 1.2); + $sheet->setCellValue('A3', -1.234); + $sheet->setCellValue('A4', '=1+2'); + $sheet->getStyle('A1:A4')->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_NUMBER_00); + $sheet->getStyle('A1')->getFont()->setItalic(true); + $sheet->getStyle('A2')->getFont()->setBold(true); + $writer = new OdsWriter($spreadsheet); + $writerWorksheet = new OdsWriter\Content($writer); + $data = $writerWorksheet->write(); + $count = substr_count($data, 'style:data-style-name="N200"'); + self::assertSame(3, $count, 'once for the italic cell, once for the bold, and once for the other two'); + self::assertStringContainsString( + '', + $data + ); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index bde23e29d..5654cd762 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -13,7 +13,18 @@ - + + + / + + / + + + + : + + + @@ -92,8 +103,8 @@ 1 1 - - 42798.572060185 + + 4/3/17 13:43