diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 67101ca1d..df7cb5637 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -124,6 +124,7 @@ class Format $value = Helpers::extractString($value); $format = Helpers::extractString($format); + $format = (string) NumberFormat::convertSystemFormats($format); if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) { // @phpstan-ignore-next-line diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 987e1a361..4cbdbbe92 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -182,7 +182,7 @@ class Cell implements Stringable { return (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), - (string) $this->getStyle()->getNumberFormat()->getFormatCode() + (string) $this->getStyle()->getNumberFormat()->getFormatCode(true) ); } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 55fe77c37..ed19534de 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -411,6 +411,7 @@ class Date } // Switch on formatcode + $excelFormatCode = (string) NumberFormat::convertSystemFormats($excelFormatCode); if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) { return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY); } diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 5153d84de..b344da64b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -26,10 +26,12 @@ class NumberFormat extends Supervisor 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_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_TIME1 = 'h:mm AM/PM'; const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM'; @@ -40,6 +42,7 @@ class NumberFormat extends Supervisor const FORMAT_DATE_TIME7 = 'i:s.S'; const FORMAT_DATE_TIME8 = 'h:mm:ss;@'; const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@'; + const FORMAT_DATE_LONG_DATE = 'dddd, mmmm d, yyyy'; const DATE_TIME_OR_DATETIME_ARRAY = [ self::FORMAT_DATE_YYYYMMDD, @@ -49,10 +52,12 @@ class NumberFormat extends Supervisor self::FORMAT_DATE_DMMINUS, self::FORMAT_DATE_MYMINUS, self::FORMAT_DATE_XLSX14, + self::FORMAT_DATE_XLSX14_ACTUAL, self::FORMAT_DATE_XLSX15, self::FORMAT_DATE_XLSX16, self::FORMAT_DATE_XLSX17, self::FORMAT_DATE_XLSX22, + self::FORMAT_DATE_XLSX22_ACTUAL, self::FORMAT_DATE_DATETIME, self::FORMAT_DATE_TIME1, self::FORMAT_DATE_TIME2, @@ -63,6 +68,7 @@ class NumberFormat extends Supervisor self::FORMAT_DATE_TIME7, self::FORMAT_DATE_TIME8, self::FORMAT_DATE_YYYYMMDDSLASH, + self::FORMAT_DATE_LONG_DATE, ]; const TIME_OR_DATETIME_ARRAY = [ self::FORMAT_DATE_XLSX22, @@ -84,6 +90,21 @@ class NumberFormat extends Supervisor const FORMAT_ACCOUNTING_USD = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)'; const FORMAT_ACCOUNTING_EUR = '_("€"* #,##0.00_);_("€"* \(#,##0.00\);_("€"* "-"??_);_(@_)'; + const SHORT_DATE_INDEX = 14; + const DATE_TIME_INDEX = 22; + const FORMAT_SYSDATE_X = '[$-x-sysdate]'; + const FORMAT_SYSDATE_F800 = '[$-F800]'; + const FORMAT_SYSTIME_X = '[$-x-systime]'; + const FORMAT_SYSTIME_F400 = '[$-F400]'; + + protected static string $shortDateFormat = self::FORMAT_DATE_XLSX14_ACTUAL; + + protected static string $longDateFormat = self::FORMAT_DATE_LONG_DATE; + + protected static string $dateTimeFormat = self::FORMAT_DATE_XLSX22_ACTUAL; + + protected static string $timeFormat = self::FORMAT_DATE_TIME2; + /** * Excel built-in number formats. */ @@ -178,16 +199,40 @@ class NumberFormat extends Supervisor /** * Get Format Code. */ - public function getFormatCode(): ?string + public function getFormatCode(bool $extended = false): ?string { if ($this->isSupervisor) { - return $this->getSharedComponent()->getFormatCode(); + return $this->getSharedComponent()->getFormatCode($extended); } - if (is_int($this->builtInFormatCode)) { - return self::builtInFormatCode($this->builtInFormatCode); + $builtin = $this->getBuiltInFormatCode(); + if (is_int($builtin)) { + if ($extended) { + if ($builtin === self::SHORT_DATE_INDEX) { + return self::$shortDateFormat; + } + if ($builtin === self::DATE_TIME_INDEX) { + return self::$dateTimeFormat; + } + } + + return self::builtInFormatCode($builtin); } - return $this->formatCode; + return $extended ? self::convertSystemFormats($this->formatCode) : $this->formatCode; + } + + public static function convertSystemFormats(?string $formatCode): ?string + { + if (is_string($formatCode)) { + if (stripos($formatCode, self::FORMAT_SYSDATE_F800) !== false || stripos($formatCode, self::FORMAT_SYSDATE_X) !== false) { + return self::$longDateFormat; + } + if (stripos($formatCode, self::FORMAT_SYSTIME_F400) !== false || stripos($formatCode, self::FORMAT_SYSTIME_X) !== false) { + return self::$timeFormat; + } + } + + return $formatCode; } /** @@ -290,15 +335,15 @@ class NumberFormat extends Supervisor self::$builtInFormats[11] = '0.00E+00'; self::$builtInFormats[12] = '# ?/?'; self::$builtInFormats[13] = '# ??/??'; - self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy'; - self::$builtInFormats[15] = 'd-mmm-yy'; + self::$builtInFormats[14] = self::FORMAT_DATE_XLSX14_ACTUAL; // Despite ECMA 'mm-dd-yy'; + self::$builtInFormats[15] = self::FORMAT_DATE_XLSX15; self::$builtInFormats[16] = 'd-mmm'; self::$builtInFormats[17] = 'mmm-yy'; self::$builtInFormats[18] = 'h:mm AM/PM'; self::$builtInFormats[19] = 'h:mm:ss AM/PM'; self::$builtInFormats[20] = 'h:mm'; self::$builtInFormats[21] = 'h:mm:ss'; - self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm'; + self::$builtInFormats[22] = self::FORMAT_DATE_XLSX22_ACTUAL; // Despite ECMA 'm/d/yy h:mm'; self::$builtInFormats[37] = '#,##0_);(#,##0)'; // Despite ECMA '#,##0 ;(#,##0)'; self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; // Despite ECMA '#,##0 ;[Red](#,##0)'; @@ -427,4 +472,44 @@ class NumberFormat extends Supervisor return $exportedArray; } + + public static function getShortDateFormat(): string + { + return self::$shortDateFormat; + } + + public static function setShortDateFormat(string $shortDateFormat): void + { + self::$shortDateFormat = $shortDateFormat; + } + + public static function getLongDateFormat(): string + { + return self::$longDateFormat; + } + + public static function setLongDateFormat(string $longDateFormat): void + { + self::$longDateFormat = $longDateFormat; + } + + public static function getDateTimeFormat(): string + { + return self::$dateTimeFormat; + } + + public static function setDateTimeFormat(string $dateTimeFormat): void + { + self::$dateTimeFormat = $dateTimeFormat; + } + + public static function getTimeFormat(): string + { + return self::$timeFormat; + } + + public static function setTimeFormat(string $timeFormat): void + { + self::$timeFormat = $timeFormat; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 2ba3f0818..a6507b94f 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -746,7 +746,7 @@ class Worksheet implements IComparable $cellValue = NumberFormat::toFormattedString( $cell->getCalculatedValue(), (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex()) - ->getNumberFormat()->getFormatCode() + ->getNumberFormat()->getFormatCode(true) ); if ($cellValue !== null && $cellValue !== '') { diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatSystemDateTimeTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatSystemDateTimeTest.php new file mode 100644 index 000000000..52799b33e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatSystemDateTimeTest.php @@ -0,0 +1,120 @@ +shortDateFormat = NumberFormat::getShortDateFormat(); + $this->longDateFormat = NumberFormat::getLongDateFormat(); + $this->dateTimeFormat = NumberFormat::getDateTimeFormat(); + $this->timeFormat = NumberFormat::getTimeFormat(); + } + + protected function tearDown(): void + { + NumberFormat::setShortDateFormat($this->shortDateFormat); + NumberFormat::setLongDateFormat($this->longDateFormat); + NumberFormat::setDateTimeFormat($this->dateTimeFormat); + NumberFormat::setTimeFormat($this->timeFormat); + } + + public function testOverrides(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $formula = '=DATEVALUE("2024-02-29")+TIMEVALUE("8:12:15 AM")'; + $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue($formula); + $sheet->getStyle('A2')->getNumberFormat() + ->setBuiltinFormatCode(14); + $sheet->getCell('A3')->setValue($formula); + $sheet->getStyle('A3')->getNumberFormat() + ->setBuiltinFormatCode(15); + $sheet->getCell('A4')->setValue($formula); + $sheet->getStyle('A4')->getNumberFormat() + ->setBuiltinFormatCode(22); + $sheet->getCell('A5')->setValue($formula); + $sheet->getStyle('A5')->getNumberFormat() + ->setFormatCode('[$-F800]'); + $sheet->getCell('A6')->setValue($formula); + $sheet->getStyle('A6')->getNumberFormat() + ->setFormatCode('[$-F400]'); + $sheet->getCell('A7')->setValue($formula); + $sheet->getStyle('A7')->getNumberFormat() + ->setFormatCode('[$-x-sysdate]'); + $sheet->getCell('A8')->setValue($formula); + $sheet->getStyle('A8')->getNumberFormat() + ->setFormatCode('[$-x-systime]'); + $sheet->getCell('A9')->setValue($formula); + $sheet->getStyle('A9')->getNumberFormat() + ->setFormatCode('hello' . NumberFormat::FORMAT_SYSDATE_F800 . 'goodbye'); + NumberFormat::setShortDateFormat('yyyy/mm/dd'); + NumberFormat::setDateTimeFormat('yyyy/mm/dd hh:mm AM/PM'); + NumberFormat::setLongDateFormat('dddd d mmm yyyy'); + NumberFormat::setTimeFormat('h:mm'); + self::assertSame('2024/02/29', $sheet->getCell('A2')->getformattedValue()); + self::assertSame('2024/02/29 08:12 AM', $sheet->getCell('A4')->getformattedValue()); + self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A5')->getformattedValue()); + self::assertSame('8:12', $sheet->getCell('A6')->getformattedValue()); + self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A7')->getformattedValue()); + self::assertSame('8:12', $sheet->getCell('A8')->getformattedValue()); + self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A9')->getformattedValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testDefaults(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $formula = '=DATEVALUE("2024-02-29")+TIMEVALUE("8:12:15 AM")'; + $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue($formula); + $sheet->getStyle('A2')->getNumberFormat() + ->setBuiltinFormatCode(14); + $sheet->getCell('A3')->setValue($formula); + $sheet->getStyle('A3')->getNumberFormat() + ->setBuiltinFormatCode(15); + $sheet->getCell('A4')->setValue($formula); + $sheet->getStyle('A4')->getNumberFormat() + ->setBuiltinFormatCode(22); + $sheet->getCell('A5')->setValue($formula); + $sheet->getStyle('A5')->getNumberFormat() + ->setFormatCode('[$-F800]'); + $sheet->getCell('A6')->setValue($formula); + $sheet->getStyle('A6')->getNumberFormat() + ->setFormatCode('[$-F400]'); + $sheet->getCell('A7')->setValue($formula); + $sheet->getStyle('A7')->getNumberFormat() + ->setFormatCode('[$-x-sysdate]'); + $sheet->getCell('A8')->setValue($formula); + $sheet->getStyle('A8')->getNumberFormat() + ->setFormatCode('[$-x-systime]'); + $sheet->getCell('A9')->setValue($formula); + $sheet->getStyle('A9')->getNumberFormat() + ->setFormatCode('hello' . NumberFormat::FORMAT_SYSDATE_F800 . 'goodbye'); + self::assertSame('2/29/2024', $sheet->getCell('A2')->getformattedValue()); + self::assertSame('2/29/2024 8:12', $sheet->getCell('A4')->getformattedValue()); + self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A5')->getformattedValue()); + self::assertSame('8:12:15 AM', $sheet->getCell('A6')->getformattedValue()); + self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A7')->getformattedValue()); + self::assertSame('8:12:15 AM', $sheet->getCell('A8')->getformattedValue()); + self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A9')->getformattedValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Calculation/TextData/TEXT.php b/tests/data/Calculation/TextData/TEXT.php index 19032d006..bf6474a28 100644 --- a/tests/data/Calculation/TextData/TEXT.php +++ b/tests/data/Calculation/TextData/TEXT.php @@ -81,4 +81,5 @@ return [ 'no arguments' => ['exception'], 'one argument' => ['exception', 1.75], 'boolean in lieu of string' => ['TRUE', true, '@'], + 'system long date format' => ['Sunday, January 1, 2012', '1-Jan-2012', '[$-x-sysdate]'], ]; diff --git a/tests/data/Shared/Date/FormatCodes.php b/tests/data/Shared/Date/FormatCodes.php index 8cd09172c..7e2c62668 100644 --- a/tests/data/Shared/Date/FormatCodes.php +++ b/tests/data/Shared/Date/FormatCodes.php @@ -160,4 +160,10 @@ return [ false, '\D-00000', ], + [true, '[$-F800]'], + [true, 'hello[$-F400]goodbye'], + [false, '[$-F401]'], + [true, '[$-x-sysdate]'], + [true, '[$-x-systime]'], + [false, '[$-x-systim]'], ];