diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php
index f5cdc8108..46d8f69f5 100644
--- a/src/PhpSpreadsheet/Reader/Xml.php
+++ b/src/PhpSpreadsheet/Reader/Xml.php
@@ -39,6 +39,9 @@ class Xml extends BaseReader
*/
protected array $styles = [];
+ /** @var string[] */
+ private array $numberFormatMappings = Style\NumberFormat::FORMAT_MAPPINGS;
+
/**
* Create a new Excel2003XML Reader instance.
*/
@@ -301,7 +304,7 @@ class Xml extends BaseReader
(new Properties($spreadsheet))->readProperties($xml, $namespaces);
- $this->styles = (new Style())->parseStyles($xml, $namespaces);
+ $this->styles = (new Style())->parseStyles($xml, $namespaces, $this->numberFormatMappings);
if (isset($this->styles['Default']) && is_array($this->styles['Default'])) {
$spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
}
@@ -507,8 +510,13 @@ class Xml extends BaseReader
break;
case 'DateTime':
$type = DataType::TYPE_NUMERIC;
- $dateTime = new DateTime($cellValue, new DateTimeZone('UTC'));
- $cellValue = Date::PHPToExcel($dateTime);
+
+ try {
+ $dateTime = new DateTime($cellValue, new DateTimeZone('UTC'));
+ $cellValue = Date::PHPToExcel($dateTime);
+ } catch (Throwable) {
+ $type = DataType::TYPE_STRING;
+ }
break;
case 'Error':
@@ -755,4 +763,11 @@ class Xml extends BaseReader
? new SimpleXMLElement('')
: ($simple->attributes($node) ?? new SimpleXMLElement(''));
}
+
+ public function setNumberFormatMapping(string $key, string $value): self
+ {
+ $this->numberFormatMappings[$key] = $value;
+
+ return $this;
+ }
}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style.php b/src/PhpSpreadsheet/Reader/Xml/Style.php
index 699d71828..f4ab459b8 100644
--- a/src/PhpSpreadsheet/Reader/Xml/Style.php
+++ b/src/PhpSpreadsheet/Reader/Xml/Style.php
@@ -16,10 +16,11 @@ class Style
/**
* @param string[] $namespaces
+ * @param string[] $numberFormatMappings
*
* @return mixed[]
*/
- public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
+ public function parseStyles(SimpleXMLElement $xml, array $namespaces, array $numberFormatMappings = Style\NumberFormat::FORMAT_MAPPINGS): array
{
$children = $xml->children('urn:schemas-microsoft-com:office:spreadsheet');
$stylesXml = $children->Styles[0];
@@ -70,7 +71,7 @@ class Style
break;
case 'NumberFormat':
if ($styleAttributes) {
- $numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes);
+ $numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes, $numberFormatMappings);
}
break;
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php b/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php
index 1ca825a54..d852e66c0 100644
--- a/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php
@@ -2,12 +2,40 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat as Format;
use SimpleXMLElement;
class NumberFormat extends StyleBase
{
- /** @return mixed[] */
- public function parseStyle(SimpleXMLElement $styleAttributes): array
+ /** @internal */
+ public const FORMAT_MAPPINGS = [
+ 'Euro Currency' => '€#,##0.00;[Red](€#,##0.00)',
+ 'Fixed' => Format::FORMAT_NUMBER_00,
+ 'General' => '',
+ 'General Number' => '',
+ 'General Date' => Format::FORMAT_DATE_DATETIME_BETTER,
+ 'Long Date' => Format::FORMAT_DATE_LONG_DATE,
+ 'Long Time' => Format::FORMAT_DATE_TIME2,
+ 'Medium Date' => Format::FORMAT_DATE_XLSX15_YYYY,
+ 'Medium Time' => Format::FORMAT_DATE_TIME1,
+ 'Percent' => Format::FORMAT_PERCENTAGE_00,
+ 'Scientific' => '0.00E+00',
+ 'Short Date' => Format::FORMAT_DATE_YYYYMMDD,
+ 'Short Time' => Format::FORMAT_DATE_TIME1,
+ 'Standard' => Format::FORMAT_NUMBER_COMMA_SEPARATED1,
+ // N.B. - following formats apply to numbers, not booleans
+ 'True/False' => '"True";"True";"False"',
+ 'Yes/No' => '"Yes";"Yes";"No"',
+ 'On/Off' => '"On";"On";"Off"',
+ ];
+
+ /**
+ * @param string[] $numberFormatMappings expected to be FORMAT_MAPPINGS, but individual entries can be overridden
+ *
+ * @return mixed[]
+ */
+ public function parseStyle(SimpleXMLElement $styleAttributes, array $numberFormatMappings = self::FORMAT_MAPPINGS): array
{
$style = [];
@@ -15,16 +43,16 @@ class NumberFormat extends StyleBase
$toFormats = ['-', ' '];
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
- $styleAttributeValue = str_replace($fromFormats, $toFormats, (string) $styleAttributeValue);
-
- switch ($styleAttributeValue) {
- case 'Short Date':
- $styleAttributeValue = 'dd/mm/yyyy';
-
- break;
+ $styleAttributeValue = (string) $styleAttributeValue;
+ $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
+ if (array_key_exists($styleAttributeValue, $numberFormatMappings)) {
+ $styleAttributeValue = $numberFormatMappings[$styleAttributeValue];
+ } elseif ($styleAttributeValue === 'Currency') {
+ $currencyCode = StringHelper::getCurrencyCode();
+ $styleAttributeValue = "{$currencyCode}#,##0.00;[Red]({$currencyCode}#,##0.00)";
}
- if ($styleAttributeValue > '') {
+ if ($styleAttributeValue !== '') {
$style['numberFormat']['formatCode'] = $styleAttributeValue;
}
}
diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/BadDateTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/BadDateTest.php
new file mode 100644
index 000000000..d0ad37372
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Xml/BadDateTest.php
@@ -0,0 +1,87 @@
+load($filename);
+ $sheet = $spreadsheet->getSheetByNameOrThrow('Sample Data');
+ self::assertSame('1960-12-19', $sheet->getCell('A10')->getFormattedValue());
+ self::assertSame('1.50', $sheet->getCell('A11')->getFormattedValue());
+ self::assertSame('2:30 AM', $sheet->getCell('A13')->getFormattedValue());
+ self::assertSame('1960-12-19 01:30', $sheet->getCell('A15')->getFormattedValue());
+ $sheet = $spreadsheet->getSheetByNameOrThrow('Report Data');
+ self::assertSame('11-Nov-2011', $sheet->getCell('D27')->getFormattedValue());
+ self::assertSame('11:00 AM', $sheet->getCell('E27')->getFormattedValue());
+ self::assertSame('12-Dec-2012', $sheet->getCell('D28')->getFormattedValue());
+ self::assertSame('1899-12-31T288:00:00.000', $sheet->getCell('E28')->getValue(), 'invalid date/time-stamp treated as plain string');
+ $spreadsheet->disconnectWorksheets();
+ }
+
+ /** @return string[][] */
+ public static function providerXmlSpreadsheet(): array
+ {
+ return [
+ 'spreadsheet with bad datetimestamp and formulas' => ['tests/data/Reader/Xml/Excel2003XMLTest.xml'],
+ 'spreadsheet with formulas corrected' => ['tests/data/Reader/Xml/Excel2003XMLTest.corrected.xml'],
+ ];
+ }
+
+ public function testFormats(): void
+ {
+ $reader = new Xml();
+ $reader
+ ->setNumberFormatMapping('Currency', '¥#,##0;[Red](¥#,##0)')
+ ->setNumberFormatMapping('Yes/No', '"Oui";"Oui";"Non"')
+ ->setNumberFormatMapping('On/Off', '"Marche";"Marche";"Arrêt"');
+ $filename = 'tests/data/Reader/Xml/formats.xml';
+ $spreadsheet = $reader->load($filename);
+ $sheet = $spreadsheet->getActiveSheet();
+ $expected = [
+ ['True/False', 'True', 'False'],
+ ['Yes/No', 'Oui', 'Non'],
+ ['On/Off', 'Marche', 'Arrêt'],
+ ['Long Time', '6:00:00 PM', null],
+ ['Percent', '97.00%', null],
+ ['Currency', '(¥1,235)', null],
+ ['Standard', '1,234.57', null],
+ ['Fixed', '1234.58', null],
+ ['Euro Currency', '(€1,234.59)', null],
+ ['Scientific', '-1.23E+3', null],
+ ];
+ self::assertSame($expected, $sheet->toArray());
+ $spreadsheet->disconnectWorksheets();
+ }
+
+ public function testUtf16(): void
+ {
+ $reader = new Xml();
+ $filename = 'tests/data/Reader/Xml/formats.utf16lebom.xml';
+ $spreadsheet = $reader->load($filename);
+ $sheet = $spreadsheet->getActiveSheet();
+ $expected = [
+ ['True/False', 'True', 'False'],
+ ['Yes/No', 'Yes', 'No'],
+ ['On/Off', 'On', 'Off'],
+ ['Long Time', '6:00:00 PM', null],
+ ['Percent', '97.00%', null],
+ ['Currency', '($1,234.56)', null],
+ ['Standard', '1,234.57', null],
+ ['Fixed', '1234.58', null],
+ ['Euro Currency', '(€1,234.59)', null],
+ ['Scientific', '-1.23E+3', null],
+ ];
+ self::assertSame($expected, $sheet->toArray());
+ $spreadsheet->disconnectWorksheets();
+ }
+}
diff --git a/tests/data/Reader/Xml/Excel2003XMLTest.corrected.xml b/tests/data/Reader/Xml/Excel2003XMLTest.corrected.xml
new file mode 100644
index 000000000..f674775b5
--- /dev/null
+++ b/tests/data/Reader/Xml/Excel2003XMLTest.corrected.xml
@@ -0,0 +1,1315 @@
+
+
+
+
+Untitled Spreadsheet
+Unknown Creator
+Owen Leibman
+2026-09-13T08:18:02Z
+2026-09-13T08:18:02Z
+16.00
+
+
+
+
+
+6510
+19200
+32767
+32767
+False
+False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+|
+Test String 1
+
+
+
Test for a simple colour-formatted string
+
+
+ |
+
+1
+ |
+
+5
+ |
+
+A
+ |
+
+E
+ |
+
+6
+ |
+
+AE
+ |
+
+PhpSpreadsheet
+ |
+
+
+|
+Test - String 2
+ |
+
+2
+ |
+
+6
+ |
+
+B
+ |
+
+F
+ |
+
+8
+ |
+
+BF
+ |
+
+Dot
+ |
+
+
+|
+Test #3
+ |
+
+3
+ |
+
+7
+ |
+
+C
+ |
+
+G
+ |
+
+10
+ |
+
+CG
+ |
+
+Red
+ |
+
+Red
+ |
+
+Dash
+ |
+
+
+|
+Test with (") in string
+ |
+
+4
+ |
+
+8
+ |
+
+D
+ |
+
+H
+ |
+
+12
+ |
+
+DH
+ |
+
+Orange
+ |
+
+Orange
+ |
+
+Dash/Dot/Dot
+ |
+
+
+|
+10
+ |
+
+26
+ |
+
+36
+ |
+
+Yellow
+ |
+
+Yellow
+ |
+
+Dash/Dot
+ |
+
+
+|
+Test #3
+ |
+
+1.23
+ |
+
+1
+ |
+
+1
+ |
+
+Green
+ |
+
+Green
+ |
+
+Thin Line
+ |
+
+
+|
+Test #3
+ |
+
+2.34
+ |
+
+0
+ |
+
+0
+ |
+
+#NAME?
+ |
+
+Blue
+ |
+
+Blue
+ |
+
+Thick Dash/Dot/Dot
+ |
+
+
+|
+Test #3
+ |
+
+3.45
+ |
+
+Purple
+ |
+
+Purple
+ |
+
+Variant Thick Dash/Dot/Dot
+ |
+
+
+|
+Pink
+ |
+
+Pink
+ |
+
+Thick Dash/Dot
+ |
+
+
+|
+1960-12-19T00:00:00.000
+ |
+
+TOP
+ |
+
+0
+ |
+
+Brown
+ |
+
+Brown
+ |
+
+Thick Dash
+ |
+
+
+|
+1.5
+ |
+
+#DIV/0!
+ |
+
+Thick Line
+ |
+
+
+|
+BOTTOM
+ |
+
+Extra Thick Line
+ |
+
+
+|
+1900-01-01T02:30:00.000
+ |
+
+Мойва сушеная
+
+
+
Tests for UTF-8 content
+
+
+ |
+
+Double Line
+ |
+
+
+|
+LEFT
+ |
+
+Ärendetext
+ |
+
+
+|
+1960-12-19T01:30:00.000
+ |
+
+Højde
+ |
+
+
+|
+RIGHT
+ |
+
+
+|
+BOX
+ |
+
+Test Column 1
+ |
+ |
+ |
+
+
+ |
+
+Test Column 2
+ |
+ |
+
+Patterned
+ |
+
+Patterned 2
+ |
+
+
+ |
+ |
+
+Test Column 3
+ |
+
+
+|
+Underline None
+ |
+
+Rotate 90
+ |
+
+Rotate 45
+ |
+
+Rotate -90
+ |
+
+Rotate -45
+ |
+ |
+
+
+|
+Underline 1
+ |
+
+Subscript
+ |
+ |
+
+
+|
+Underline 2
+ |
+
+Superscript
+ |
+ |
+
+
+|
+Underline 3
+ |
+ |
+
+
+|
+Underline 4
+ |
+ |
+
+
+|
+
+I don't know if Gnumeric supports
+
+
Rich Text
+
+
in the same way as
+
+
Excel
+
+
, And this row should be autofit height with text wrap
+
+ |
+
+
+|
+PhpSpreadsheet
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+9
+
+600
+600
+
+
+False
+False
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+|
+Heading 1
+ |
+
+Heading 2
+ |
+
+Third Heading
+ |
+
+Date Heading
+ |
+
+Time Heading
+ |
+
+Adjusted Date
+ |
+
+Adjusted Number
+ |
+ |
+
+
+ |
+
+Adjusted Number
+ |
+
+Third Heading
+ |
+
+
+|
+1
+ |
+
+1.1100000000000001
+ |
+
+1.1100000000000001
+ |
+
+
+|
+2
+ |
+
+4.4400000000000004
+ |
+
+2.2200000000000002
+ |
+
+
+|
+3
+ |
+
+9.99
+ |
+
+3.33
+ |
+
+
+|
+4
+ |
+
+17.760000000000002
+ |
+
+4.4400000000000004
+ |
+
+
+|
+5
+ |
+
+27.75
+ |
+
+5.55
+ |
+
+
+|
+6
+ |
+
+39.96
+ |
+
+6.66
+ |
+
+
+|
+7
+ |
+
+54.39
+ |
+
+7.77
+ |
+
+
+|
+8
+ |
+
+71.040000000000006
+ |
+
+8.8800000000000008
+ |
+
+
+|
+9
+ |
+
+89.91
+ |
+
+9.99
+ |
+
+
+|
+10
+ |
+
+111
+ |
+
+11.1
+ |
+
+
+|
+11
+ |
+
+134.31
+ |
+
+12.21
+ |
+
+
+|
+12
+ |
+
+159.84
+ |
+
+13.32
+ |
+
+
+|
+13
+ |
+
+1.1100000000000001
+ |
+
+-1.1100000000000001
+ |
+
+
+|
+ABC
+ |
+
+1
+ |
+
+1.1100000000000001
+ |
+
+2001-01-01T00:00:00.000
+ |
+
+1900-01-01T01:00:00.000
+ |
+
+#VALUE!
+ |
+
+#VALUE!
+ |
+
+A
+ |
+
+Adjusted Number
+ |
+
+
+|
+BCD
+ |
+
+2
+ |
+
+2.2200000000000002
+ |
+
+2002-02-02T00:00:00.000
+ |
+
+1900-01-01T02:00:00.000
+ |
+
+1899-12-29T21:21:36.000
+ |
+
+1.2321000000000002
+ |
+
+B
+ |
+
+1.11
+ |
+
+
+|
+CDE
+ |
+
+3
+ |
+
+3.33
+ |
+
+2003-03-03T00:00:00.000
+ |
+
+1900-01-01T03:00:00.000
+ |
+
+1899-12-26T13:26:24.000
+ |
+
+9.8568000000000016
+ |
+
+C
+ |
+
+4.44
+ |
+
+
+|
+DEF
+ |
+
+4
+ |
+
+4.4400000000000004
+ |
+
+2004-04-03T23:00:00.000
+ |
+
+1900-01-01T04:00:00.000
+ |
+
+1899-12-21T00:14:24.000
+ |
+
+33.2667
+ |
+
+D
+ |
+
+9.99
+ |
+
+
+|
+EFG
+ |
+
+5
+ |
+
+5.55
+ |
+
+2005-05-04T23:00:00.000
+ |
+
+1900-01-01T05:00:00.000
+ |
+
+1899-12-13T05:45:36.000
+ |
+
+78.854400000000012
+ |
+
+E
+ |
+
+17.76
+ |
+
+
+|
+FGH
+ |
+
+6
+ |
+
+6.66
+ |
+
+2006-06-05T23:00:00.000
+ |
+
+1900-01-01T06:00:00.000
+ |
+
+1899-12-03T06:00:00.000
+ |
+
+154.01249999999999
+ |
+
+F
+ |
+
+27.75
+ |
+
+
+|
+GHI
+ |
+
+7
+ |
+
+7.77
+ |
+
+2007-07-06T23:00:00.000
+ |
+
+1900-01-01T07:00:00.000
+ |
+
+1899-11-21T00:57:36.000
+ |
+
+266.1336
+ |
+
+G
+ |
+
+39.96
+ |
+
+
+|
+HIJ
+ |
+
+8
+ |
+
+8.8800000000000008
+ |
+
+2008-08-07T23:00:00.000
+ |
+
+1900-01-01T08:00:00.000
+ |
+
+1899-11-06T14:38:24.000
+ |
+
+422.6103
+ |
+
+H
+ |
+
+54.39
+ |
+
+
+|
+IJK
+ |
+
+9
+ |
+
+9.99
+ |
+
+2009-09-08T23:00:00.000
+ |
+
+1900-01-01T09:00:00.000
+ |
+
+1899-10-20T23:02:24.000
+ |
+
+630.8352000000001
+ |
+
+I
+ |
+
+71.04
+ |
+
+
+|
+JKL
+ |
+
+10
+ |
+
+11.1
+ |
+
+2010-10-09T23:00:00.000
+ |
+
+1900-01-01T10:00:00.000
+ |
+
+1899-10-02T02:09:36.000
+ |
+
+898.20089999999993
+ |
+
+J
+ |
+
+89.91
+ |
+
+
+|
+KLM
+ |
+
+11
+ |
+
+12.21
+ |
+
+2011-11-11T00:00:00.000
+ |
+
+1900-01-01T11:00:00.000
+ |
+
+1899-09-11T00:00:00.000
+ |
+
+1232.0999999999999
+ |
+
+K
+ |
+
+111
+ |
+
+
+|
+LMN
+ |
+
+12
+ |
+
+13.32
+ |
+
+2012-12-12T00:00:00.000
+ |
+
+1899-12-31T288:00:00.000
+ |
+
+1899-08-18T16:33:36.000
+ |
+
+1639.9251000000002
+ |
+
+L
+ |
+
+134.31
+ |
+
+
+|
+ZYX
+ |
+
+-1
+ |
+
+-1.1100000000000001
+ |
+
+1999-12-01T00:00:00.000
+ |
+
+1900-01-01T23:00:00.000
+ |
+
+1899-07-24T03:50:24.000
+ |
+
+2129.0688
+ |
+
+M
+ |
+
+159.84
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+9
+
+600
+600
+
+False
+False
+
+
+
diff --git a/samples/templates/Excel2003XMLTest.xml b/tests/data/Reader/Xml/Excel2003XMLTest.xml
similarity index 100%
rename from samples/templates/Excel2003XMLTest.xml
rename to tests/data/Reader/Xml/Excel2003XMLTest.xml
diff --git a/tests/data/Reader/Xml/formats.utf16lebom.xml b/tests/data/Reader/Xml/formats.utf16lebom.xml
new file mode 100644
index 000000000..9e01964c4
Binary files /dev/null and b/tests/data/Reader/Xml/formats.utf16lebom.xml differ
diff --git a/tests/data/Reader/Xml/formats.xml b/tests/data/Reader/Xml/formats.xml
new file mode 100644
index 000000000..ca7098345
--- /dev/null
+++ b/tests/data/Reader/Xml/formats.xml
@@ -0,0 +1,141 @@
+
+
+
+
+ Owen Leibman
+ Owen Leibman
+ 2026-09-12T08:55:46Z
+ 16.00
+
+
+
+
+
+ 6510
+ 19200
+ 32767
+ 32767
+ False
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | True/False |
+ 1 |
+ 0 |
+
+
+ | Yes/No |
+ 1 |
+ 0 |
+
+
+ | On/Off |
+ 1 |
+ 0 |
+
+
+ | Long Time |
+ 1899-12-31T18:00:00.000 |
+
+
+ | Percent |
+ 0.97 |
+
+
+ | Currency |
+ -1234.56 |
+
+
+ | Standard |
+ 1234.57 |
+
+
+ | Fixed |
+ 1234.58 |
+
+
+ | Euro Currency |
+ -1234.5899999999999 |
+
+
+ | Scientific |
+ -1234.5899999999999 |
+
+
+
+
+
+
+
+
+
+
+
+ 600
+ 600
+
+
+
+
+ 3
+ 10
+
+
+ False
+ False
+
+
+