Added support for cell formats when writing Excel files

This commit is contained in:
Alexander Rakushin 2019-07-10 08:18:28 +03:00
parent a420e3fffa
commit 8a1a088677
4 changed files with 230 additions and 2 deletions

View File

@ -77,6 +77,11 @@ class Style
/** @var bool */ /** @var bool */
protected $hasSetBackgroundColor = false; protected $hasSetBackgroundColor = false;
/** @var string Format */
protected $format = null;
/** @var bool */
protected $hasSetFormat = false;
/** /**
* @return int|null * @return int|null
@ -325,6 +330,35 @@ class Style
return $this->hasSetBackgroundColor; return $this->hasSetBackgroundColor;
} }
/**
* Sets format
* @param string $format
* @return Style
*/
public function setFormat($format)
{
$this->hasSetFormat = true;
$this->format = $format;
return $this;
}
/**
* @return string
*/
public function getFormat()
{
return $this->format;
}
/**
*
* @return bool Whether format should be applied
*/
public function shouldApplyFormat()
{
return $this->hasSetFormat;
}
/** /**
* Serializes the style for future comparison with other styles. * Serializes the style for future comparison with other styles.
* The ID is excluded from the comparison, as we only care about * The ID is excluded from the comparison, as we only care about

View File

@ -145,6 +145,18 @@ class StyleBuilder
$this->style->setBackgroundColor($color); $this->style->setBackgroundColor($color);
return $this; return $this;
} }
/**
* Sets a format
*
* @api
* @param string $format Format
* @return StyleBuilder
*/
public function setFormat($format)
{
$this->style->setFormat($format);
return $this;
}
/** /**
* Returns the configured style. The style is cached and can be reused. * Returns the configured style. The style is cached and can be reused.

View File

@ -14,6 +14,62 @@ use Box\Spout\Writer\Style\Style;
*/ */
class StyleHelper extends AbstractStyleHelper class StyleHelper extends AbstractStyleHelper
{ {
/**
* @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
* @var array Mapping between built-in format and the associated numFmtId
*/
protected static $builtinNumFormatToIdMapping = [
'General' => 0,
'0' => 1,
'0.00' => 2,
'#,##0' => 3,
'#,##0.00' => 4,
'$#,##0,\-$#,##0' => 5,
'$#,##0,[Red]\-$#,##0' => 6,
'$#,##0.00,\-$#,##0.00' => 7,
'$#,##0.00,[Red]\-$#,##0.00' => 8,
'0%' => 9,
'0.00%' => 10,
'0.00E+00' => 11,
'# ?/?' => 12,
'# ??/??' => 13,
'mm-dd-yy' => 14,
'd-mmm-yy' => 15,
'd-mmm' => 16,
'mmm-yy' => 17,
'h:mm AM/PM' => 18,
'h:mm:ss AM/PM' => 19,
'h:mm' => 20,
'h:mm:ss' => 21,
'm/d/yy h:mm' => 22,
'#,##0 ,(#,##0)' => 37,
'#,##0 ,[Red](#,##0)' => 38,
'#,##0.00,(#,##0.00)' => 39,
'#,##0.00,[Red](#,##0.00)' => 40,
'_("$"* #,##0.00_),_("$"* \(#,##0.00\),_("$"* "-"??_),_(@_)' => 44,
'mm:ss' => 45,
'[h]:mm:ss' => 46,
'mm:ss.0' => 47,
'##0.0E+0' => 48,
'@' => 49,
'[$-404]e/m/d' => 27,
'm/d/yy' => 30,
't0' => 59,
't0.00' => 60,
't#,##0' => 61,
't#,##0.00' => 62,
't0%' => 67,
't0.00%' => 68,
't# ?/?' => 69,
't# ??/??' => 70,
];
/** /**
* @var array * @var array
*/ */
@ -24,6 +80,16 @@ class StyleHelper extends AbstractStyleHelper
*/ */
protected $styleIdToFillMappingTable = []; protected $styleIdToFillMappingTable = [];
/**
* @var array
*/
protected $registeredFormats = [];
/**
* @var array [STYLE_ID] => [FORMAT_ID] maps a style to a format declaration
*/
protected $styleIdToFormatsMappingTable = [];
/** /**
* Excel preserves two default fills with index 0 and 1 * Excel preserves two default fills with index 0 and 1
* Since Excel is the dominant vendor - we play along here * Since Excel is the dominant vendor - we play along here
@ -32,6 +98,15 @@ class StyleHelper extends AbstractStyleHelper
*/ */
protected $fillIndex = 2; protected $fillIndex = 2;
/**
* If the numFmtId is lower than 0xA4 (164 in decimal)
* then it's a built-in number format.
* Since Excel is the dominant vendor - we play along here
*
* @var int The fill index counter for custom fills.
*/
protected $formatIndex = 164;
/** /**
* @var array * @var array
*/ */
@ -52,10 +127,48 @@ class StyleHelper extends AbstractStyleHelper
{ {
$registeredStyle = parent::registerStyle($style); $registeredStyle = parent::registerStyle($style);
$this->registerFill($registeredStyle); $this->registerFill($registeredStyle);
$this->registerFormat($registeredStyle);
$this->registerBorder($registeredStyle); $this->registerBorder($registeredStyle);
return $registeredStyle; return $registeredStyle;
} }
/**
* Register a format definition
*
* @param \Box\Spout\Writer\Style\Style $style
*/
protected function registerFormat($style)
{
$styleId = $style->getId();
$format = $style->getFormat();
if ($format) {
$isFormatRegistered = isset($this->registeredFormats[$format]);
// We need to track the already registered format definitions
if ($isFormatRegistered) {
$registeredStyleId = $this->registeredFormats[$format];
$registeredFormatId = $this->styleIdToFormatsMappingTable[$registeredStyleId];
$this->styleIdToFormatsMappingTable[$styleId] = $registeredFormatId;
} else {
$this->registeredFormats[$format] = $styleId;
if (isset(self::$builtinNumFormatToIdMapping[$format])) {
$id = self::$builtinNumFormatToIdMapping[$format];
} else {
$id = $this->formatIndex++;
}
$this->styleIdToFormatsMappingTable[$styleId] = $id;
}
} else {
// The formatId maps a style to a format declaration
// When there is no format definition - we default to 0 ( General )
$this->styleIdToFormatsMappingTable[$styleId] = 0;
}
}
/** /**
* Register a fill definition * Register a fill definition
* *
@ -134,8 +247,9 @@ class StyleHelper extends AbstractStyleHelper
{ {
$hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0); $hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0);
$hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0); $hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0);
$hasStyleCustomFormats = (isset($this->styleIdToFormatsMappingTable[$styleId]) && $this->styleIdToFormatsMappingTable[$styleId] !== 0);
return ($hasStyleCustomFill || $hasStyleCustomBorders); return ($hasStyleCustomFill || $hasStyleCustomBorders || $hasStyleCustomFormats);
} }
@ -151,6 +265,7 @@ class StyleHelper extends AbstractStyleHelper
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
EOD; EOD;
$content .= $this->getNumFmtsSectionContent();
$content .= $this->getFontsSectionContent(); $content .= $this->getFontsSectionContent();
$content .= $this->getFillsSectionContent(); $content .= $this->getFillsSectionContent();
$content .= $this->getBordersSectionContent(); $content .= $this->getBordersSectionContent();
@ -165,6 +280,34 @@ EOD;
return $content; return $content;
} }
/**
* Returns the content of the "<fonts>" section.
*
* @return string
*/
protected function getNumFmtsSectionContent()
{
$tags = [];
foreach ($this->registeredFormats as $styleId) {
$numFmtId = $this->styleIdToFormatsMappingTable[$styleId];
//Built-in formats do not need to be declared, skip them
if ($numFmtId < 164) {
continue;
}
/** @var Style $style */
$style = $this->styleIdToStyleMappingTable[$styleId];
$format = $style->getFormat();
$tags[] = '<numFmt numFmtId="' . $numFmtId . '" formatCode="' . $format . '"/>';
}
$content = '<numFmts count="' . count($tags) . '">';
$content .= implode('', $tags);
$content .= '</numFmts>';
return $content;
}
/** /**
* Returns the content of the "<fonts>" section. * Returns the content of the "<fonts>" section.
* *
@ -305,8 +448,9 @@ EOD;
$styleId = $style->getId(); $styleId = $style->getId();
$fillId = $this->styleIdToFillMappingTable[$styleId]; $fillId = $this->styleIdToFillMappingTable[$styleId];
$borderId = $this->styleIdToBorderMappingTable[$styleId]; $borderId = $this->styleIdToBorderMappingTable[$styleId];
$numFmtId = $this->styleIdToFormatsMappingTable[$styleId];
$content .= '<xf numFmtId="0" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"'; $content .= '<xf numFmtId="' . $numFmtId . '" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"';
if ($style->shouldApplyFont()) { if ($style->shouldApplyFont()) {
$content .= ' applyFont="1"'; $content .= ' applyFont="1"';

View File

@ -241,6 +241,44 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('1', $cellDomElements[0]->getAttribute('s')); $this->assertEquals('1', $cellDomElements[0]->getAttribute('s'));
$this->assertEquals('1', $cellDomElements[1]->getAttribute('s')); $this->assertEquals('1', $cellDomElements[1]->getAttribute('s'));
} }
/**
* @return void
*/
public function testAddRowWithNumFmtStyles()
{
$fileName = 'test_add_row_with_numfmt.xlsx';
$dataRows = [
[1.123456789],
[12.1],
];
$style1 = (new StyleBuilder())
->setFontBold()
->setFormat('0.00') //Builtin format
->build();
$style2 = (new StyleBuilder())
->setFontBold()
->setFormat('0.000')
->build();
$this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style1, $style2]);
$formatsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'numFmts');
$this->assertEquals(
1,
$formatsDomElement->getAttribute('count'),
'There should be 2 formats, including the 1 default ones'
);
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
foreach ([2, 164] as $index => $expected) {
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item($index + 1);
$this->assertEquals($expected, $xfElement->getAttribute('numFmtId'));
}
}
/** /**
* @return void * @return void