mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-20 15:16:36 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 !== '') {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Style;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NumberFormatSystemDateTimeTest extends TestCase
|
||||
{
|
||||
private string $shortDateFormat;
|
||||
|
||||
private string $longDateFormat;
|
||||
|
||||
private string $dateTimeFormat;
|
||||
|
||||
private string $timeFormat;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
@@ -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]'],
|
||||
];
|
||||
|
||||
@@ -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]'],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user