Skip to content

Commit 2964a97

Browse files
committed
Ods Writer Support Some Number Formats
Fix PHPOffice#4798. Partially address PHPOffice#3961. Ods Reader supports very little related to styling. Ods Writer supports many styling details, but has not heretofore supported Number Formatting. 3961 addresses both issues; I created 4798 specifically for the Writer side. Writing number formats to Excel is pretty simple - you just supply a string and that is used directly in the Xml. Ods is much more complicated - it requires Xml nodes that give a complete description of the styling. For that reason, it is difficult and painstaking to convert from the string that Excel (and PhpSpreadsheet) uses to what Ods requires. This PR provides code to support almost all the styles defined as constants in `Style/NumberFormat`. It also allows the user to add code to handle otherwise unhandled styles. New Sample55_DefinedStyles demonstrates the use of all the constant styles, plus the addition of a couple of custom styles. I may be amenable to adding some unsupported styles to the built-in list, but the custom style option will always be around in case I am being slow or unreasonable. This PR does nothing to address Ods Reader handling of styles. If and when we get around to that (no ETA and I am very far from figuring out what needs to be done), I am sure that settings like bold font will be much easier to handle than number formats, so number formats are very likely to be handled last.
1 parent 12095e5 commit 2964a97

File tree

6 files changed

+831
-5
lines changed

6 files changed

+831
-5
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
require __DIR__ . '/../Header.php';
4+
/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */
5+
6+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
7+
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
8+
use PhpOffice\PhpSpreadsheet\Writer\BaseWriter;
9+
use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style;
10+
11+
$spreadsheet = new Spreadsheet();
12+
$sheet = $spreadsheet->getActiveSheet();
13+
$array = [
14+
['NUMBER', 1, NumberFormat::FORMAT_NUMBER],
15+
['NUMBER_0', 1, NumberFormat::FORMAT_NUMBER_0],
16+
['NUMBER_00', 1, NumberFormat::FORMAT_NUMBER_00],
17+
['NUMBER_COMMA_SEPARATED1', 1234, NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1],
18+
['NUMBER_COMMA_SEPARATED2', 1234, NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED2],
19+
['PERCENTAGE', 0.98, NumberFormat::FORMAT_PERCENTAGE],
20+
['PERCENTAGE_0', 0.985, NumberFormat::FORMAT_PERCENTAGE_0],
21+
['PERCENTAGE_00', 0.9856, NumberFormat::FORMAT_PERCENTAGE_00],
22+
['DATE_YYYYMMDD', 46000, NumberFormat::FORMAT_DATE_YYYYMMDD],
23+
['DATE_DDMMYYYY', 46000, NumberFormat::FORMAT_DATE_DDMMYYYY],
24+
['DATE_DMYSLASH', 46000, NumberFormat::FORMAT_DATE_DMYSLASH],
25+
['DATE_DMYMINUS', 46000, NumberFormat::FORMAT_DATE_DMYMINUS],
26+
['DATE_DMMINUS', 46000, NumberFormat::FORMAT_DATE_DMMINUS],
27+
['DATE_MYMINUS', 46000, NumberFormat::FORMAT_DATE_MYMINUS],
28+
['DATE_XLSX14', 46000, NumberFormat::FORMAT_DATE_XLSX14],
29+
['DATE_XLSX14_ACTUAL', 46000, NumberFormat::FORMAT_DATE_XLSX14_ACTUAL],
30+
['DATE_XLSX15', 46000, NumberFormat::FORMAT_DATE_XLSX15],
31+
['DATE_XLSX16', 46000, NumberFormat::FORMAT_DATE_XLSX16],
32+
['DATE_XLSX17', 46000, NumberFormat::FORMAT_DATE_XLSX17],
33+
['DATE_XLSX22', 46000, NumberFormat::FORMAT_DATE_XLSX22],
34+
['DATE_XLSX22_ACTUAL', 46000, NumberFormat::FORMAT_DATE_XLSX22_ACTUAL],
35+
['DATE_DATETIME', 46000.25, NumberFormat::FORMAT_DATE_DATETIME],
36+
['DATE_DATETIME_BETTER', 46000.25, NumberFormat::FORMAT_DATE_DATETIME_BETTER],
37+
['DATE_TIME1', 46000.25, NumberFormat::FORMAT_DATE_TIME1],
38+
['DATE_TIME2', 46000.25, NumberFormat::FORMAT_DATE_TIME2],
39+
['DATE_TIME3', 46000.25, NumberFormat::FORMAT_DATE_TIME3],
40+
['DATE_TIME4', 46000.25, NumberFormat::FORMAT_DATE_TIME4],
41+
['DATE_TIME5', 46000.25, NumberFormat::FORMAT_DATE_TIME5],
42+
['DATE_TIME6', 46000.25, NumberFormat::FORMAT_DATE_TIME6],
43+
['DATE_TIME7', 46000.25, NumberFormat::FORMAT_DATE_TIME7],
44+
['DATE_TIME8', 46000.25, NumberFormat::FORMAT_DATE_TIME8],
45+
['DATE_YYYYMMDDSLASH', 46000, NumberFormat::FORMAT_DATE_YYYYMMDDSLASH],
46+
['DATE_LONG_DATE', 46000, NumberFormat::FORMAT_DATE_LONG_DATE],
47+
['CURRENCY_USD_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_USD_INTEGER],
48+
['CURRENCY_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD],
49+
['CURRENCY_EUR_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR_INTEGER],
50+
['CURRENCY_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR],
51+
['ACCOUNTING_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD],
52+
['ACCOUNTING_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR],
53+
['CUSTOM1', 1234.56, '0.000'],
54+
['CUSTOM2', 1234.56, '"$"#,##0.00_);[Red]\("$"#,##0.00\)'],
55+
];
56+
$row = 0;
57+
$helper->log('Populate spreadsheet');
58+
foreach ($array as $cells) {
59+
++$row;
60+
$sheet->getCell("A$row")->setValue($cells[0]);
61+
$sheet->getCell("B$row")->setValue($cells[1]);
62+
if (!str_starts_with($cells[0], 'DATE')) {
63+
$sheet->getCell("C$row")->setValue(-$cells[1]);
64+
}
65+
$sheet->getStyle("B$row:C$row")
66+
->getNumberFormat()
67+
->setFormatCode($cells[2]);
68+
}
69+
$sheet->getColumnDimension('A')->setAutoSize(true);
70+
$sheet->getColumnDimension('B')->setAutoSize(true);
71+
$sheet->getColumnDimension('C')->setAutoSize(true);
72+
$sheet->setSelectedCells('A1');
73+
74+
function threeDecimalPlaces(Style $obj, string $name): void
75+
{
76+
$writer = $obj->getWriter();
77+
$writer->startElement('number:number-style');
78+
$writer->writeAttribute('style:name', $name);
79+
$writer->startElement('number:number');
80+
$writer->writeAttribute('number:decimal-places', '3');
81+
$writer->writeAttribute('number:min-decimal-places', '3');
82+
$writer->writeAttribute('number:min-integer-digits', '1');
83+
$writer->endElement(); // number:number
84+
$writer->endElement(); // number:number-style
85+
}
86+
87+
function redBrackets(Style $obj, string $name): void
88+
{
89+
$writer = $obj->getWriter();
90+
$writer->startElement('number:currency-style');
91+
$writer->writeAttribute('style:name', $name . 'P0');
92+
$writer->writeElement('number:currency-symbol', '$');
93+
$writer->startElement('number:number');
94+
$writer->writeAttribute('number:decimal-places', '2');
95+
$writer->writeAttribute('number:min-decimal-places', '2');
96+
$writer->writeAttribute('number:min-integer-digits', '1');
97+
$writer->writeAttribute('number:grouping', 'true');
98+
$writer->endElement(); // number:number
99+
$writer->writeElement('number:text', ' ');
100+
$writer->endElement(); // number:currency-style
101+
102+
$writer->startElement('number:currency-style');
103+
$writer->writeAttribute('style:name', $name);
104+
$writer->startElement('style:text-properties');
105+
$writer->writeAttribute('fo:color', '#FF0000');
106+
$writer->endElement(); // style:text-properties
107+
$writer->writeElement('number:text', '(');
108+
$writer->startElement('number:number');
109+
$writer->writeAttribute('number:decimal-places', '2');
110+
$writer->writeAttribute('number:min-decimal-places', '2');
111+
$writer->writeAttribute('number:min-integer-digits', '1');
112+
$writer->writeAttribute('number:grouping', 'true');
113+
$writer->endElement(); // number:number
114+
$writer->writeElement('number:text', ')');
115+
$writer->startElement('style:map');
116+
$writer->writeAttribute('style:condition', 'value()>=0');
117+
$writer->writeAttribute('style:apply-style-name', $name . 'P0');
118+
$writer->endElement(); // style:map
119+
$writer->endElement(); // number:currency-style
120+
}
121+
122+
function writeAdditional(BaseWriter $writer): void
123+
{
124+
if (method_exists($writer, 'useAdditionalNumberFormats')) {
125+
$array = [
126+
'0.000' => threeDecimalPlaces(...),
127+
'"$"#,##0.00_);[Red]\("$"#,##0.00\)' => redBrackets(...),
128+
];
129+
$writer->useAdditionalNumberFormats($array);
130+
}
131+
}
132+
133+
$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Ods'], writerCallback: writeAdditional(...));
134+
$spreadsheet->disconnectWorksheets();

src/PhpSpreadsheet/Style/NumberFormat.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class NumberFormat extends Supervisor
2323

2424
const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd';
2525
const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy';
26-
const FORMAT_DATE_DMYSLASH = 'd/m/yy';
26+
const FORMAT_DATE_DMYSLASH = 'd"/"m"/"yy';
2727
const FORMAT_DATE_DMYMINUS = 'd-m-yy';
2828
const FORMAT_DATE_DMMINUS = 'd-m';
2929
const FORMAT_DATE_MYMINUS = 'm-yy';
@@ -35,6 +35,7 @@ class NumberFormat extends Supervisor
3535
const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
3636
const FORMAT_DATE_XLSX22_ACTUAL = 'm/d/yyyy h:mm';
3737
const FORMAT_DATE_DATETIME = 'd/m/yy h:mm';
38+
const FORMAT_DATE_DATETIME_BETTER = 'yyyy-mm-dd hh:mm';
3839
const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
3940
const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
4041
const FORMAT_DATE_TIME3 = 'h:mm';
@@ -43,7 +44,7 @@ class NumberFormat extends Supervisor
4344
const FORMAT_DATE_TIME6 = 'h:mm:ss';
4445
const FORMAT_DATE_TIME7 = 'i:s.S';
4546
const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
46-
const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@';
47+
const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy"/"mm"/"dd;@';
4748
const FORMAT_DATE_LONG_DATE = 'dddd, mmmm d, yyyy';
4849

4950
const DATE_TIME_OR_DATETIME_ARRAY = [
@@ -61,6 +62,7 @@ class NumberFormat extends Supervisor
6162
self::FORMAT_DATE_XLSX22,
6263
self::FORMAT_DATE_XLSX22_ACTUAL,
6364
self::FORMAT_DATE_DATETIME,
65+
self::FORMAT_DATE_DATETIME_BETTER,
6466
self::FORMAT_DATE_TIME1,
6567
self::FORMAT_DATE_TIME2,
6668
self::FORMAT_DATE_TIME3,
@@ -75,6 +77,7 @@ class NumberFormat extends Supervisor
7577
const TIME_OR_DATETIME_ARRAY = [
7678
self::FORMAT_DATE_XLSX22,
7779
self::FORMAT_DATE_DATETIME,
80+
self::FORMAT_DATE_DATETIME_BETTER,
7881
self::FORMAT_DATE_TIME1,
7982
self::FORMAT_DATE_TIME2,
8083
self::FORMAT_DATE_TIME3,

src/PhpSpreadsheet/Writer/Ods.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public function __construct(Spreadsheet $spreadsheet)
5151
$this->writerPartThumbnails = new Thumbnails($this);
5252
}
5353

54+
/** @param array<string, callable> $additionalNumberFormats */
55+
public function useAdditionalNumberFormats(array $additionalNumberFormats): void
56+
{
57+
$this->writerPartContent->additionalNumberFormats = $additionalNumberFormats;
58+
}
59+
5460
public function getWriterPartContent(): Content
5561
{
5662
return $this->writerPartContent;

0 commit comments

Comments
 (0)