diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index b34e036..ab9ec8e 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -71,6 +71,12 @@ class Style /** @var bool */ private $hasSetBackgroundColor = false; + /** @var string Format */ + private $format = null; + + /** @var bool */ + private $hasSetFormat = false; + /** * @return int|null */ @@ -383,4 +389,34 @@ class Style { 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; + } } diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index a35a280..ffe4814 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -147,6 +147,18 @@ class StyleBuilder 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. diff --git a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php index d3446d2..1628783 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php @@ -88,6 +88,9 @@ class StyleMerger if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) { $styleToUpdate->setBorder($baseStyle->getBorder()); } + if (!$style->getFormat() && $baseStyle->shouldApplyFormat()) { + $styleToUpdate->setFormat($baseStyle->getFormat()); + } if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) { $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); } diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php index b98307a..b7491a9 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php @@ -33,7 +33,10 @@ class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager $associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId); $hasStyleCustomBorders = ($associatedBorderId !== null && $associatedBorderId !== 0); - return ($hasStyleCustomFill || $hasStyleCustomBorders); + $associatedFormatId = $this->styleRegistry->getFormatIdForStyleId($styleId); + $hasStyleCustomFormats = ($associatedFormatId !== null && $associatedFormatId !== 0); + + return ($hasStyleCustomFill || $hasStyleCustomBorders || $hasStyleCustomFormats); } /** @@ -48,6 +51,7 @@ class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager EOD; + $content .= $this->getFormatsSectionContent(); $content .= $this->getFontsSectionContent(); $content .= $this->getFillsSectionContent(); $content .= $this->getBordersSectionContent(); @@ -62,6 +66,35 @@ EOD; return $content; } + /** + * Returns the content of the "" section. + * + * @return string + */ + protected function getFormatsSectionContent() + { + $tags = []; + $registeredFormats = $this->styleRegistry->getRegisteredFormats(); + foreach ($registeredFormats as $styleId) { + $numFmtId = $this->styleRegistry->getFormatIdForStyleId($styleId); + + //Built-in formats do not need to be declared, skip them + if ($numFmtId < 164) { + continue; + } + + /** @var Style $style */ + $style = $this->styleRegistry->getStyleFromStyleId($styleId); + $format = $style->getFormat(); + $tags[] = ''; + } + $content = ''; + $content .= implode('', $tags); + $content .= ''; + + return $content; + } + /** * Returns the content of the "" section. * @@ -206,8 +239,9 @@ EOD; $styleId = $style->getId(); $fillId = $this->getFillIdForStyleId($styleId); $borderId = $this->getBorderIdForStyleId($styleId); + $numFmtId = $this->getFormatIdForStyleId($styleId); - $content .= 'shouldApplyFont()) { $content .= ' applyFont="1"'; @@ -261,6 +295,22 @@ EOD; return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?: 0); } + /** + * Returns the format ID associated to the given style ID. + * For the default style use general format. + * + * @param int $styleId + * @return int + */ + private function getFormatIdForStyleId($styleId) + { + // For the default style (ID = 0), we don't want to override the format. + // Otherwise all cells of the spreadsheet will have a format. + $isDefaultStyle = ($styleId === 0); + + return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?: 0); + } + /** * Returns the content of the "" section. * diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php b/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php index abd89f1..ae47cd7 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleRegistry.php @@ -10,6 +10,81 @@ use Box\Spout\Common\Entity\Style\Style; */ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry { + /** + * @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 + */ + protected $registeredFormats = []; + + /** + * @var array [STYLE_ID] => [FORMAT_ID] maps a style to a format declaration + */ + protected $styleIdToFormatsMappingTable = []; + + /** + * 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 */ @@ -48,11 +123,58 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry { $registeredStyle = parent::registerStyle($style); $this->registerFill($registeredStyle); + $this->registerFormat($registeredStyle);; $this->registerBorder($registeredStyle); return $registeredStyle; } + /** + * Register a format definition + * + * @param Style $style + */ + protected function registerFormat(Style $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; + } + } + + /** + * @param int $styleId + * @return int|null Format ID associated to the given style ID + */ + public function getFormatIdForStyleId($styleId) + { + return (isset($this->styleIdToFormatsMappingTable[$styleId])) ? + $this->styleIdToFormatsMappingTable[$styleId] : + null; + } + /** * Register a fill definition * @@ -136,6 +258,7 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry null; } + /** * @return array */ @@ -151,4 +274,14 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry { return $this->registeredBorders; } + + + /** + * @return array + */ + public function getRegisteredFormats() + { + return $this->registeredFormats; + } + } diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index 02e6d11..a724e2b 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -210,6 +210,47 @@ class WriterWithStyleTest extends TestCase $this->assertEquals('1', $cellDomElements[1]->getAttribute('s')); } + /** + * @return void + */ + public function testAddRowWithNumFmtStyles() + { + $fileName = 'test_add_row_with_numfmt.xlsx'; + $style = (new StyleBuilder()) + ->setFontBold() + ->setFormat('0.00')//Builtin format + ->build(); + $style2 = (new StyleBuilder()) + ->setFontBold() + ->setFormat('0.000') + ->build(); + + + $dataRows = [ + $this->createStyledRowFromValues([1.123456789], $style), + $this->createStyledRowFromValues([12.1], $style2), + ]; + + $this->writeToXLSXFile($dataRows, $fileName); + + + $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 */