Skip to content

Commit accb321

Browse files
committed
Additional Support for Date/Time Styles
Excel supports the following notations for cell styles: - `[$-F800]` and `[$-x-sysdate]` will format the date according to what appears to be the user's system long date format. - `[$-F400]` and `[$-x-systime]`, will format the time according to what appears to be the user's system long time format. - Builtin style 14 will format date according to what appears to be the user's system short date format. - Builtin style 22 will format date and time according to the user's preference. It appears that the date portion is formatted according to the user's system short date format, but the time portion is formatted according to a format which is neither system short time format nor system long time format, so I'm not sure how this preference is set. For F800, sysdate, F400, and systime, any other characters in the style are ignored, except that, if you have more than one of this type of block in the style, Excel will treat it as corrupt (error message on open and style changed to General). Support is added for the new codes. In addition, note that the value displayed in the cell may differ in different environments. To give the PhpSpreadsheet programmer an opportunity to emulate what the intended audience will most often see, properties `shortDateFormat` (default value is builtin 14), `longDateFormat` (default value is `dddd, mmmm d, yyyy`), `dateTimeFormat` (defaults to builtin 22), and `timeFormat` (default is `FORMAT_DATE_TIME2`), with corresponding setters and getters, are added to Style/NumberFormat. Note that, if these properties are set to some other value in PhpSpreadsheet, it will not affect the values in the cell or the style - it is merely a convenience for the programmer. It will, however, affect column width if autosize is specified for the column. If the programmer does not alter any of the new properties, the output should be unchanged from before for builtins 14 and 22. The new styles are also recognized by the `TEXT` function. In this case, the cell's calculated value may differ from user to user. Note that this is a small subset of adding locale information to styles. No attempt is made to support any of the other possibilities - locale data will continue to be passed through to the spreadsheet, but PhpSpreadsheet will discard it before attempting to generate the formatted value of a cell.
1 parent d620497 commit accb321

File tree

8 files changed

+224
-10
lines changed

8 files changed

+224
-10
lines changed

src/PhpSpreadsheet/Calculation/TextData/Format.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ public static function TEXTFORMAT(mixed $value, mixed $format): array|string
124124

125125
$value = Helpers::extractString($value);
126126
$format = Helpers::extractString($format);
127+
$format = (string) NumberFormat::convertSystemFormats($format);
127128

128129
if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) {
129130
// @phpstan-ignore-next-line

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public function getFormattedValue(): string
182182
{
183183
return (string) NumberFormat::toFormattedString(
184184
$this->getCalculatedValue(),
185-
(string) $this->getStyle()->getNumberFormat()->getFormatCode()
185+
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
186186
);
187187
}
188188

src/PhpSpreadsheet/Shared/Date.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ public static function isDateTimeFormatCode(string $excelFormatCode, bool $dateW
411411
}
412412

413413
// Switch on formatcode
414+
$excelFormatCode = (string) NumberFormat::convertSystemFormats($excelFormatCode);
414415
if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) {
415416
return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY);
416417
}

src/PhpSpreadsheet/Style/NumberFormat.php

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ class NumberFormat extends Supervisor
2626
const FORMAT_DATE_DMMINUS = 'd-m';
2727
const FORMAT_DATE_MYMINUS = 'm-yy';
2828
const FORMAT_DATE_XLSX14 = 'mm-dd-yy';
29+
const FORMAT_DATE_XLSX14_ACTUAL = 'm/d/yyyy';
2930
const FORMAT_DATE_XLSX15 = 'd-mmm-yy';
3031
const FORMAT_DATE_XLSX16 = 'd-mmm';
3132
const FORMAT_DATE_XLSX17 = 'mmm-yy';
3233
const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
34+
const FORMAT_DATE_XLSX22_ACTUAL = 'm/d/yyyy h:mm';
3335
const FORMAT_DATE_DATETIME = 'd/m/yy h:mm';
3436
const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
3537
const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
@@ -40,6 +42,7 @@ class NumberFormat extends Supervisor
4042
const FORMAT_DATE_TIME7 = 'i:s.S';
4143
const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
4244
const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@';
45+
const FORMAT_DATE_LONG_DATE = 'dddd, mmmm d, yyyy';
4346

4447
const DATE_TIME_OR_DATETIME_ARRAY = [
4548
self::FORMAT_DATE_YYYYMMDD,
@@ -49,10 +52,12 @@ class NumberFormat extends Supervisor
4952
self::FORMAT_DATE_DMMINUS,
5053
self::FORMAT_DATE_MYMINUS,
5154
self::FORMAT_DATE_XLSX14,
55+
self::FORMAT_DATE_XLSX14_ACTUAL,
5256
self::FORMAT_DATE_XLSX15,
5357
self::FORMAT_DATE_XLSX16,
5458
self::FORMAT_DATE_XLSX17,
5559
self::FORMAT_DATE_XLSX22,
60+
self::FORMAT_DATE_XLSX22_ACTUAL,
5661
self::FORMAT_DATE_DATETIME,
5762
self::FORMAT_DATE_TIME1,
5863
self::FORMAT_DATE_TIME2,
@@ -63,6 +68,7 @@ class NumberFormat extends Supervisor
6368
self::FORMAT_DATE_TIME7,
6469
self::FORMAT_DATE_TIME8,
6570
self::FORMAT_DATE_YYYYMMDDSLASH,
71+
self::FORMAT_DATE_LONG_DATE,
6672
];
6773
const TIME_OR_DATETIME_ARRAY = [
6874
self::FORMAT_DATE_XLSX22,
@@ -84,6 +90,21 @@ class NumberFormat extends Supervisor
8490
const FORMAT_ACCOUNTING_USD = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
8591
const FORMAT_ACCOUNTING_EUR = '_("€"* #,##0.00_);_("€"* \(#,##0.00\);_("€"* "-"??_);_(@_)';
8692

93+
const SHORT_DATE_INDEX = 14;
94+
const DATE_TIME_INDEX = 22;
95+
const FORMAT_SYSDATE_X = '[$-x-sysdate]';
96+
const FORMAT_SYSDATE_F800 = '[$-F800]';
97+
const FORMAT_SYSTIME_X = '[$-x-systime]';
98+
const FORMAT_SYSTIME_F400 = '[$-F400]';
99+
100+
protected static string $shortDateFormat = self::FORMAT_DATE_XLSX14_ACTUAL;
101+
102+
protected static string $longDateFormat = self::FORMAT_DATE_LONG_DATE;
103+
104+
protected static string $dateTimeFormat = self::FORMAT_DATE_XLSX22_ACTUAL;
105+
106+
protected static string $timeFormat = self::FORMAT_DATE_TIME2;
107+
87108
/**
88109
* Excel built-in number formats.
89110
*/
@@ -178,16 +199,40 @@ public function applyFromArray(array $styleArray): static
178199
/**
179200
* Get Format Code.
180201
*/
181-
public function getFormatCode(): ?string
202+
public function getFormatCode(bool $extended = false): ?string
182203
{
183204
if ($this->isSupervisor) {
184-
return $this->getSharedComponent()->getFormatCode();
205+
return $this->getSharedComponent()->getFormatCode($extended);
185206
}
186-
if (is_int($this->builtInFormatCode)) {
187-
return self::builtInFormatCode($this->builtInFormatCode);
207+
$builtin = $this->getBuiltInFormatCode();
208+
if (is_int($builtin)) {
209+
if ($extended) {
210+
if ($builtin === self::SHORT_DATE_INDEX) {
211+
return self::$shortDateFormat;
212+
}
213+
if ($builtin === self::DATE_TIME_INDEX) {
214+
return self::$dateTimeFormat;
215+
}
216+
}
217+
218+
return self::builtInFormatCode($builtin);
188219
}
189220

190-
return $this->formatCode;
221+
return $extended ? self::convertSystemFormats($this->formatCode) : $this->formatCode;
222+
}
223+
224+
public static function convertSystemFormats(?string $formatCode): ?string
225+
{
226+
if (is_string($formatCode)) {
227+
if (stripos($formatCode, self::FORMAT_SYSDATE_F800) !== false || stripos($formatCode, self::FORMAT_SYSDATE_X) !== false) {
228+
return self::$longDateFormat;
229+
}
230+
if (stripos($formatCode, self::FORMAT_SYSTIME_F400) !== false || stripos($formatCode, self::FORMAT_SYSTIME_X) !== false) {
231+
return self::$timeFormat;
232+
}
233+
}
234+
235+
return $formatCode;
191236
}
192237

193238
/**
@@ -290,15 +335,15 @@ private static function fillBuiltInFormatCodes(): void
290335
self::$builtInFormats[11] = '0.00E+00';
291336
self::$builtInFormats[12] = '# ?/?';
292337
self::$builtInFormats[13] = '# ??/??';
293-
self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
294-
self::$builtInFormats[15] = 'd-mmm-yy';
338+
self::$builtInFormats[14] = self::FORMAT_DATE_XLSX14_ACTUAL; // Despite ECMA 'mm-dd-yy';
339+
self::$builtInFormats[15] = self::FORMAT_DATE_XLSX15;
295340
self::$builtInFormats[16] = 'd-mmm';
296341
self::$builtInFormats[17] = 'mmm-yy';
297342
self::$builtInFormats[18] = 'h:mm AM/PM';
298343
self::$builtInFormats[19] = 'h:mm:ss AM/PM';
299344
self::$builtInFormats[20] = 'h:mm';
300345
self::$builtInFormats[21] = 'h:mm:ss';
301-
self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
346+
self::$builtInFormats[22] = self::FORMAT_DATE_XLSX22_ACTUAL; // Despite ECMA 'm/d/yy h:mm';
302347

303348
self::$builtInFormats[37] = '#,##0_);(#,##0)'; // Despite ECMA '#,##0 ;(#,##0)';
304349
self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; // Despite ECMA '#,##0 ;[Red](#,##0)';
@@ -427,4 +472,44 @@ protected function exportArray1(): array
427472

428473
return $exportedArray;
429474
}
475+
476+
public static function getShortDateFormat(): string
477+
{
478+
return self::$shortDateFormat;
479+
}
480+
481+
public static function setShortDateFormat(string $shortDateFormat): void
482+
{
483+
self::$shortDateFormat = $shortDateFormat;
484+
}
485+
486+
public static function getLongDateFormat(): string
487+
{
488+
return self::$longDateFormat;
489+
}
490+
491+
public static function setLongDateFormat(string $longDateFormat): void
492+
{
493+
self::$longDateFormat = $longDateFormat;
494+
}
495+
496+
public static function getDateTimeFormat(): string
497+
{
498+
return self::$dateTimeFormat;
499+
}
500+
501+
public static function setDateTimeFormat(string $dateTimeFormat): void
502+
{
503+
self::$dateTimeFormat = $dateTimeFormat;
504+
}
505+
506+
public static function getTimeFormat(): string
507+
{
508+
return self::$timeFormat;
509+
}
510+
511+
public static function setTimeFormat(string $timeFormat): void
512+
{
513+
self::$timeFormat = $timeFormat;
514+
}
430515
}

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ public function calculateColumnWidths(): static
746746
$cellValue = NumberFormat::toFormattedString(
747747
$cell->getCalculatedValue(),
748748
(string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
749-
->getNumberFormat()->getFormatCode()
749+
->getNumberFormat()->getFormatCode(true)
750750
);
751751

752752
if ($cellValue !== null && $cellValue !== '') {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Style;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class NumberFormatSystemDateTimeTest extends TestCase
12+
{
13+
private string $shortDateFormat;
14+
15+
private string $longDateFormat;
16+
17+
private string $dateTimeFormat;
18+
19+
private string $timeFormat;
20+
21+
protected function setUp(): void
22+
{
23+
$this->shortDateFormat = NumberFormat::getShortDateFormat();
24+
$this->longDateFormat = NumberFormat::getLongDateFormat();
25+
$this->dateTimeFormat = NumberFormat::getDateTimeFormat();
26+
$this->timeFormat = NumberFormat::getTimeFormat();
27+
}
28+
29+
protected function tearDown(): void
30+
{
31+
NumberFormat::setShortDateFormat($this->shortDateFormat);
32+
NumberFormat::setLongDateFormat($this->longDateFormat);
33+
NumberFormat::setDateTimeFormat($this->dateTimeFormat);
34+
NumberFormat::setTimeFormat($this->timeFormat);
35+
}
36+
37+
public function testOverrides(): void
38+
{
39+
$spreadsheet = new Spreadsheet();
40+
$sheet = $spreadsheet->getActiveSheet();
41+
$formula = '=DATEVALUE("2024-02-29")+TIMEVALUE("8:12:15 AM")';
42+
$sheet->getCell('A1')->setValue($formula);
43+
$sheet->getCell('A2')->setValue($formula);
44+
$sheet->getStyle('A2')->getNumberFormat()
45+
->setBuiltinFormatCode(14);
46+
$sheet->getCell('A3')->setValue($formula);
47+
$sheet->getStyle('A3')->getNumberFormat()
48+
->setBuiltinFormatCode(15);
49+
$sheet->getCell('A4')->setValue($formula);
50+
$sheet->getStyle('A4')->getNumberFormat()
51+
->setBuiltinFormatCode(22);
52+
$sheet->getCell('A5')->setValue($formula);
53+
$sheet->getStyle('A5')->getNumberFormat()
54+
->setFormatCode('[$-F800]');
55+
$sheet->getCell('A6')->setValue($formula);
56+
$sheet->getStyle('A6')->getNumberFormat()
57+
->setFormatCode('[$-F400]');
58+
$sheet->getCell('A7')->setValue($formula);
59+
$sheet->getStyle('A7')->getNumberFormat()
60+
->setFormatCode('[$-x-sysdate]');
61+
$sheet->getCell('A8')->setValue($formula);
62+
$sheet->getStyle('A8')->getNumberFormat()
63+
->setFormatCode('[$-x-systime]');
64+
$sheet->getCell('A9')->setValue($formula);
65+
$sheet->getStyle('A9')->getNumberFormat()
66+
->setFormatCode('hello' . NumberFormat::FORMAT_SYSDATE_F800 . 'goodbye');
67+
NumberFormat::setShortDateFormat('yyyy/mm/dd');
68+
NumberFormat::setDateTimeFormat('yyyy/mm/dd hh:mm AM/PM');
69+
NumberFormat::setLongDateFormat('dddd d mmm yyyy');
70+
NumberFormat::setTimeFormat('h:mm');
71+
self::assertSame('2024/02/29', $sheet->getCell('A2')->getformattedValue());
72+
self::assertSame('2024/02/29 08:12 AM', $sheet->getCell('A4')->getformattedValue());
73+
self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A5')->getformattedValue());
74+
self::assertSame('8:12', $sheet->getCell('A6')->getformattedValue());
75+
self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A7')->getformattedValue());
76+
self::assertSame('8:12', $sheet->getCell('A8')->getformattedValue());
77+
self::assertSame('Thursday 29 Feb 2024', $sheet->getCell('A9')->getformattedValue());
78+
$spreadsheet->disconnectWorksheets();
79+
}
80+
81+
public function testDefaults(): void
82+
{
83+
$spreadsheet = new Spreadsheet();
84+
$sheet = $spreadsheet->getActiveSheet();
85+
$formula = '=DATEVALUE("2024-02-29")+TIMEVALUE("8:12:15 AM")';
86+
$sheet->getCell('A1')->setValue($formula);
87+
$sheet->getCell('A2')->setValue($formula);
88+
$sheet->getStyle('A2')->getNumberFormat()
89+
->setBuiltinFormatCode(14);
90+
$sheet->getCell('A3')->setValue($formula);
91+
$sheet->getStyle('A3')->getNumberFormat()
92+
->setBuiltinFormatCode(15);
93+
$sheet->getCell('A4')->setValue($formula);
94+
$sheet->getStyle('A4')->getNumberFormat()
95+
->setBuiltinFormatCode(22);
96+
$sheet->getCell('A5')->setValue($formula);
97+
$sheet->getStyle('A5')->getNumberFormat()
98+
->setFormatCode('[$-F800]');
99+
$sheet->getCell('A6')->setValue($formula);
100+
$sheet->getStyle('A6')->getNumberFormat()
101+
->setFormatCode('[$-F400]');
102+
$sheet->getCell('A7')->setValue($formula);
103+
$sheet->getStyle('A7')->getNumberFormat()
104+
->setFormatCode('[$-x-sysdate]');
105+
$sheet->getCell('A8')->setValue($formula);
106+
$sheet->getStyle('A8')->getNumberFormat()
107+
->setFormatCode('[$-x-systime]');
108+
$sheet->getCell('A9')->setValue($formula);
109+
$sheet->getStyle('A9')->getNumberFormat()
110+
->setFormatCode('hello' . NumberFormat::FORMAT_SYSDATE_F800 . 'goodbye');
111+
self::assertSame('2/29/2024', $sheet->getCell('A2')->getformattedValue());
112+
self::assertSame('2/29/2024 8:12', $sheet->getCell('A4')->getformattedValue());
113+
self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A5')->getformattedValue());
114+
self::assertSame('8:12:15 AM', $sheet->getCell('A6')->getformattedValue());
115+
self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A7')->getformattedValue());
116+
self::assertSame('8:12:15 AM', $sheet->getCell('A8')->getformattedValue());
117+
self::assertSame('Thursday, February 29, 2024', $sheet->getCell('A9')->getformattedValue());
118+
$spreadsheet->disconnectWorksheets();
119+
}
120+
}

tests/data/Calculation/TextData/TEXT.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,5 @@
8181
'no arguments' => ['exception'],
8282
'one argument' => ['exception', 1.75],
8383
'boolean in lieu of string' => ['TRUE', true, '@'],
84+
'system long date format' => ['Sunday, January 1, 2012', '1-Jan-2012', '[$-x-sysdate]'],
8485
];

tests/data/Shared/Date/FormatCodes.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,10 @@
160160
false,
161161
'\D-00000',
162162
],
163+
[true, '[$-F800]'],
164+
[true, 'hello[$-F400]goodbye'],
165+
[false, '[$-F401]'],
166+
[true, '[$-x-sysdate]'],
167+
[true, '[$-x-systime]'],
168+
[false, '[$-x-systim]'],
163169
];

0 commit comments

Comments
 (0)