Skip to content

Commit fb55d1b

Browse files
committed
Ods Writer Support Some Number Formats
Redo of PR PHPOffice#4799 due to failed attempt to resolve merge conflict. 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 not fully support Ods Reader handling of styles. However, based on the new Writer output, it will often be able to guess the true type of numeric items and assign an appropriate style for that type. So, for example, if it can identify the field as a date, it will assign a date style. It will not always match the style in the sheet being read, but it is a big advance from just formatting the data as a generic number.
1 parent 8f5c11b commit fb55d1b

25 files changed

+1439
-187
lines changed

docs/references/features-cross-reference.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<td style="text-align: center; color: green;">✔</td>
2929
<td style="text-align: center; color: green;">✔</td>
3030
<td style="text-align: center; color: orange;">●</td>
31-
<td style="text-align: center; color: orange;">● <a href="#footnote6"><sup>6</sup></a></td>
31+
<td style="text-align: center; color: orange;">●</td>
3232
<td style="text-align: center; color: green;">✔</td>
3333
<td style="text-align: center;">N/A</td>
3434
<td style="text-align: center;">N/A</td>
@@ -513,7 +513,7 @@
513513
<td style="text-align: center; color: green;">✔</td>
514514
<td style="text-align: center; color: green;">✔</td>
515515
<td style="text-align: center; color: orange;">●</td>
516-
<td style="text-align: center; color: orange;"></td>
516+
<td style="text-align: center; color: red;"></td>
517517
<td style="text-align: center; color: green;">✔</td>
518518
<td style="text-align: center;">N/A</td>
519519
<td style="text-align: center; color: orange;">●</td>
@@ -524,7 +524,7 @@
524524
<td style="text-align: center; color: green;">✔</td>
525525
<td style="text-align: center; color: green;">✔</td>
526526
<td style="text-align: center; color: green;">✔</td>
527-
<td style="text-align: center; color: orange;">●</td>
527+
<td style="text-align: center; color: orange;">● <a href="#footnote6"><sup>6</sup></a></td>
528528
<td style="text-align: center; color: green;">✔</td>
529529
<td style="text-align: center;">N/A</td>
530530
<td style="text-align: center; color: green;">✔</td>
@@ -1025,7 +1025,7 @@
10251025
3. <span id="footnote3">Only BIFF8 files support alignment and rotation. Prior to that, comments could only be unformatted text</span>
10261026
4. <span id="footnote4">Xlsx forms and controls can be read and written but not otherwise manipulated</span>
10271027
5. <span id="footnote5">Xlsx macros can be read and written; their values can be retrieved and changed, but only in a binary form which is unlikely to be useful</span>
1028-
6. <span id="footnote6">There is very limited support for reading styles from an Ods spreadsheet. Writing styles has better support, although Number Format is incomplete.</span>
1028+
6. <span id="footnote6">There is very limited support for reading styles from an Ods spreadsheet. Writing styles has much better support. Starting with release 5.5.0, Number Format Writer supports many common styles, and users may extend support to additional styles; Reader support is improved but still imperfect.</span>
10291029
7. <span id="footnote7">In most cases, Html reader processes only inline styles; styles provided by Css classes may be ignored.</span>
10301030
8. <span id="footnote8">Code must [opt in](../topics/recipes.md#array-formulas) to array output.</span>
10311031
9. <span id="footnote9">Use with caution - allowing external images may can subject the caller to security exploits. Starting with release 4.5.0 (also earlier releases 3.9.3, 2.3.10, 2.1.11, and 1.29.12), code can allow or not external images. In those starting releases, and in earlier releases which do not offer an option, default is to allow it. In release 5+ (and earlier supported versions 1.30+, 2.1.12+, 2.4+, and 3.10+), the default is to not allow it.</span>
@@ -1293,7 +1293,7 @@
12931293
<td style="padding-left: 1em;">Number Format Mask</td>
12941294
<td style="text-align: center; color: green;">✔</td>
12951295
<td style="text-align: center; color: green;">✔</td>
1296-
<td style="text-align: center; color: orange;"></td>
1296+
<td style="text-align: center; color: green;">✔<a href="#footnote6"><sup>6</sup></a></td>
12971297
<td style="text-align: center;">N/A</td>
12981298
<td style="text-align: center; color: green;">✔</td>
12991299
<td style="text-align: center; color: green;">✔</td>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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_XLSX15_YYYY', 46000, NumberFormat::FORMAT_DATE_XLSX15_YYYY],
32+
['DATE_XLSX16', 46000, NumberFormat::FORMAT_DATE_XLSX16],
33+
['DATE_XLSX17', 46000, NumberFormat::FORMAT_DATE_XLSX17],
34+
['DATE_XLSX22', 46000, NumberFormat::FORMAT_DATE_XLSX22],
35+
['DATE_XLSX22_ACTUAL', 46000, NumberFormat::FORMAT_DATE_XLSX22_ACTUAL],
36+
['DATE_DATETIME', 46000.25, NumberFormat::FORMAT_DATE_DATETIME],
37+
['DATE_DATETIME_BETTER', 46000.25, NumberFormat::FORMAT_DATE_DATETIME_BETTER],
38+
['DATE_TIME1', 46000.25, NumberFormat::FORMAT_DATE_TIME1],
39+
['DATE_TIME2', 46000.25, NumberFormat::FORMAT_DATE_TIME2],
40+
['DATE_TIME3', 46000.25, NumberFormat::FORMAT_DATE_TIME3],
41+
['DATE_TIME4', 46000.25, NumberFormat::FORMAT_DATE_TIME4],
42+
['DATE_TIME5', 46000.25, NumberFormat::FORMAT_DATE_TIME5],
43+
['DATE_TIME6', 46000.25, NumberFormat::FORMAT_DATE_TIME6],
44+
['DATE_TIME7', 46000.25, NumberFormat::FORMAT_DATE_TIME7],
45+
['DATE_TIME8', 46000.25, NumberFormat::FORMAT_DATE_TIME8],
46+
[' DATE_TIME_INTERVAL_HMS', 0.0087731481481482, NumberFormat::FORMAT_DATE_TIME_INTERVAL_HMS],
47+
['DATE_YYYYMMDDSLASH', 46000, NumberFormat::FORMAT_DATE_YYYYMMDDSLASH],
48+
['DATE_LONG_DATE', 46000, NumberFormat::FORMAT_DATE_LONG_DATE],
49+
['CURRENCY_USD_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_USD_INTEGER],
50+
['CURRENCY_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD],
51+
['CURRENCY_EUR_INTEGER', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR_INTEGER],
52+
['CURRENCY_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR],
53+
['ACCOUNTING_USD', 1234.56, NumberFormat::FORMAT_CURRENCY_USD],
54+
['ACCOUNTING_EUR', 1234.56, NumberFormat::FORMAT_CURRENCY_EUR],
55+
['CUSTOM1', 1234.56, '0.000'],
56+
['CUSTOM2', 1234.56, '"$"#,##0.00_);[Red]\("$"#,##0.00\)'],
57+
];
58+
$row = 0;
59+
$helper->log('Populate spreadsheet');
60+
foreach ($array as $cells) {
61+
++$row;
62+
$sheet->getCell("A$row")->setValue($cells[0]);
63+
$sheet->getCell("B$row")->setValue($cells[1]);
64+
if (!str_starts_with($cells[0], 'DATE')) {
65+
$sheet->getCell("C$row")->setValue(-$cells[1]);
66+
}
67+
$sheet->getStyle("B$row:C$row")
68+
->getNumberFormat()
69+
->setFormatCode($cells[2]);
70+
}
71+
$sheet->getColumnDimension('A')->setAutoSize(true);
72+
$sheet->getColumnDimension('B')->setAutoSize(true);
73+
$sheet->getColumnDimension('C')->setAutoSize(true);
74+
$sheet->setSelectedCells('A1');
75+
76+
function threeDecimalPlaces(Style $obj, string $name): void
77+
{
78+
$writer = $obj->getWriter();
79+
$writer->startElement('number:number-style');
80+
$writer->writeAttribute('style:name', $name);
81+
$writer->startElement('number:number');
82+
$writer->writeAttribute('number:decimal-places', '3');
83+
$writer->writeAttribute('number:min-decimal-places', '3');
84+
$writer->writeAttribute('number:min-integer-digits', '1');
85+
$writer->endElement(); // number:number
86+
$writer->endElement(); // number:number-style
87+
}
88+
89+
function redBrackets(Style $obj, string $name): void
90+
{
91+
$writer = $obj->getWriter();
92+
$writer->startElement('number:currency-style');
93+
$writer->writeAttribute('style:name', $name . 'P0');
94+
$writer->writeElement('number:currency-symbol', '$');
95+
$writer->startElement('number:number');
96+
$writer->writeAttribute('number:decimal-places', '2');
97+
$writer->writeAttribute('number:min-decimal-places', '2');
98+
$writer->writeAttribute('number:min-integer-digits', '1');
99+
$writer->writeAttribute('number:grouping', 'true');
100+
$writer->endElement(); // number:number
101+
$writer->writeElement('number:text', ' ');
102+
$writer->endElement(); // number:currency-style
103+
104+
$writer->startElement('number:currency-style');
105+
$writer->writeAttribute('style:name', $name);
106+
$writer->startElement('style:text-properties');
107+
$writer->writeAttribute('fo:color', '#FF0000');
108+
$writer->endElement(); // style:text-properties
109+
$writer->writeElement('number:text', '(');
110+
$writer->startElement('number:number');
111+
$writer->writeAttribute('number:decimal-places', '2');
112+
$writer->writeAttribute('number:min-decimal-places', '2');
113+
$writer->writeAttribute('number:min-integer-digits', '1');
114+
$writer->writeAttribute('number:grouping', 'true');
115+
$writer->endElement(); // number:number
116+
$writer->writeElement('number:text', ')');
117+
$writer->startElement('style:map');
118+
$writer->writeAttribute('style:condition', 'value()>=0');
119+
$writer->writeAttribute('style:apply-style-name', $name . 'P0');
120+
$writer->endElement(); // style:map
121+
$writer->endElement(); // number:currency-style
122+
}
123+
124+
function writeAdditional(BaseWriter $writer): void
125+
{
126+
if (method_exists($writer, 'useAdditionalNumberFormats')) {
127+
$array = [
128+
'0.000' => threeDecimalPlaces(...),
129+
'"$"#,##0.00_);[Red]\("$"#,##0.00\)' => redBrackets(...),
130+
];
131+
$writer->useAdditionalNumberFormats($array);
132+
}
133+
}
134+
135+
$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Ods'], writerCallback: writeAdditional(...));
136+
$spreadsheet->disconnectWorksheets();

samples/Basic5/56_OdsToISO8601.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
require __DIR__ . '/../Header.php';
4+
5+
use PhpOffice\PhpSpreadsheet\Helper\Sample;
6+
7+
function datesToIso8601(string $infile, Sample $helper): void
8+
{
9+
$zip = new ZipArchive();
10+
if ($zip->open($infile) !== true) {
11+
throw new Exception("unable to open zip $infile");
12+
}
13+
$files = ['content.xml', 'styles.xml'];
14+
$modified = false;
15+
foreach ($files as $file) {
16+
$data = $zip->getFromName($file);
17+
if ($data === false) {
18+
throw new Exception("unable to read member $file");
19+
}
20+
$newData = str_replace('<number:year/>', '<number:year number:style="long"/>', $data);
21+
$newData = preg_replace(
22+
'~'
23+
. '(<number:day(?: number:style="long")?/>)'
24+
. '<number:text>[/-]</number:text>'
25+
. '(<number:month(?: number:style="long")?/>)'
26+
. '<number:text>[/-]</number:text>'
27+
. '(<number:year number:style="long"/>)'
28+
. '~',
29+
'$3<number:text>-</number:text>$2'
30+
. '<number:text>-</number:text>$1',
31+
$newData
32+
) ?? $newData;
33+
$newData = preg_replace(
34+
'~'
35+
. '(<number:month(?: number:style="long")?/>)'
36+
. '<number:text>[/-]</number:text>'
37+
. '(<number:day(?: number:style="long")?/>)'
38+
. '<number:text>[/-]</number:text>'
39+
. '(<number:year number:style="long"/>)'
40+
. '~',
41+
'$3<number:text>-</number:text>$1'
42+
. '<number:text>-</number:text>$2',
43+
$newData
44+
) ?? $newData;
45+
$newData = preg_replace(
46+
'~'
47+
. '(<number:date-style [^>]*)'
48+
. ' number:automatic-order="true"'
49+
. '~',
50+
'$1',
51+
$newData
52+
) ?? $newData;
53+
if ($data === $newData) {
54+
$helper->log("no changes needed for $file");
55+
} else {
56+
$zip->deleteName($file);
57+
$zip->addFromString($file, $newData);
58+
$helper->log("modified $file");
59+
$modified = true;
60+
}
61+
}
62+
$zip->close();
63+
if ($modified) {
64+
$helper->log("Modified $infile");
65+
} else {
66+
$helper->log("No modifications to $infile");
67+
}
68+
}
69+
70+
$infileBase = '56_MixedDateFormats.ods';
71+
$infileBase1 = __DIR__ . '/../templates/' . $infileBase;
72+
$infile = realpath($infileBase1);
73+
if ($infile === false) {
74+
throw new Exception("Unable to locate $infileBase1");
75+
}
76+
77+
/** @var Sample $helper */
78+
$helper->log("Infile is $infile");
79+
$outDirectory = $helper->getTemporaryFolder();
80+
$helper->log("outDirectory is $outDirectory");
81+
$outfile = $outDirectory . '/56_OdsToISO8601.ods';
82+
$helper->log("Outfile is $outfile");
83+
$helper->log('Attempting copy');
84+
if (!copy($infile, $outfile)) {
85+
throw new Exception('Copy failed');
86+
}
87+
88+
$helper->log('Update date formatting xml');
89+
datesToIso8601($outfile, $helper);
90+
91+
$helper->addDownloadLink($outfile);

samples/Header.php

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
<span class="icon-bar"></span>
4646
</button>
4747
<a class="navbar-brand" href="/">PHPSpreadsheet</a>
48+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://github.com/PHPOffice/PHPSpreadsheet">
49+
<i class="fa fa-github fa-lg" title="GitHub"></i>
50+
&nbsp;
51+
</a>
52+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://phpspreadsheet.readthedocs.io">
53+
<i class="fa fa-book fa-lg" title="Docs"></i>
54+
&nbsp;
55+
</a>
4856
</div>
4957
<div class="navbar-collapse collapse">
5058
<ul class="nav navbar-nav"><?php
@@ -72,20 +80,6 @@
7280
<?php
7381
} ?>
7482
</ul>
75-
<ul class="nav navbar-nav navbar-right">
76-
<li>
77-
<a href="https://github.com/PHPOffice/PHPSpreadsheet">
78-
<i class="fa fa-github fa-lg" title="GitHub"></i>
79-
&nbsp;
80-
</a>
81-
</li>
82-
<li>
83-
<a href="https://phpspreadsheet.readthedocs.io">
84-
<i class="fa fa-book fa-lg" title="Docs"></i>
85-
&nbsp;
86-
</a>
87-
</li>
88-
</ul>
8983
</div>
9084
</div>
9185
</div>
18.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)