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.
4.
5.
-6.
+6.
7.
8.
9.
@@ -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