PageRenderTime 28ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php

http://github.com/moodle/moodle
PHP | 226 lines | 119 code | 36 blank | 71 comment | 12 complexity | ea8ffe5112758a23119fb1ab7df86336 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. namespace Box\Spout\Reader\XLSX\Helper;
  3. use Box\Spout\Reader\Wrapper\SimpleXMLElement;
  4. use Box\Spout\Reader\Wrapper\XMLReader;
  5. /**
  6. * Class StyleHelper
  7. * This class provides helper functions related to XLSX styles
  8. *
  9. * @package Box\Spout\Reader\XLSX\Helper
  10. */
  11. class StyleHelper
  12. {
  13. /** Paths of XML files relative to the XLSX file root */
  14. const STYLES_XML_FILE_PATH = 'xl/styles.xml';
  15. /** Nodes used to find relevant information in the styles XML file */
  16. const XML_NODE_NUM_FMTS = 'numFmts';
  17. const XML_NODE_NUM_FMT = 'numFmt';
  18. const XML_NODE_CELL_XFS = 'cellXfs';
  19. const XML_NODE_XF = 'xf';
  20. /** Attributes used to find relevant information in the styles XML file */
  21. const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
  22. const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
  23. const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
  24. /** By convention, default style ID is 0 */
  25. const DEFAULT_STYLE_ID = 0;
  26. /** @var string Path of the XLSX file being read */
  27. protected $filePath;
  28. /** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
  29. protected $customNumberFormats;
  30. /** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
  31. protected $stylesAttributes;
  32. /**
  33. * @param string $filePath Path of the XLSX file being read
  34. */
  35. public function __construct($filePath)
  36. {
  37. $this->filePath = $filePath;
  38. }
  39. /**
  40. * Reads the styles.xml file and extract the relevant information from the file.
  41. *
  42. * @return void
  43. */
  44. protected function extractRelevantInfo()
  45. {
  46. $this->customNumberFormats = [];
  47. $this->stylesAttributes = [];
  48. $stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH;
  49. $xmlReader = new XMLReader();
  50. if ($xmlReader->open('zip://' . $stylesXmlFilePath)) {
  51. while ($xmlReader->read()) {
  52. if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
  53. $numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml());
  54. $this->extractNumberFormats($numFmtsNode);
  55. } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
  56. $cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml());
  57. $this->extractStyleAttributes($cellXfsNode);
  58. }
  59. }
  60. $xmlReader->close();
  61. }
  62. }
  63. /**
  64. * Extracts number formats from the "numFmt" nodes.
  65. * For simplicity, the styles attributes are kept in memory. This is possible thanks
  66. * to the reuse of formats. So 1 million cells should not use 1 million formats.
  67. *
  68. * @param SimpleXMLElement $numFmtsNode The "numFmts" node
  69. * @return void
  70. */
  71. protected function extractNumberFormats($numFmtsNode)
  72. {
  73. foreach ($numFmtsNode->children() as $numFmtNode) {
  74. $numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
  75. $formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
  76. $this->customNumberFormats[$numFmtId] = $formatCode;
  77. }
  78. }
  79. /**
  80. * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
  81. * For simplicity, the styles attributes are kept in memory. This is possible thanks
  82. * to the reuse of styles. So 1 million cells should not use 1 million styles.
  83. *
  84. * @param SimpleXMLElement $cellXfsNode The "cellXfs" node
  85. * @return void
  86. */
  87. protected function extractStyleAttributes($cellXfsNode)
  88. {
  89. foreach ($cellXfsNode->children() as $xfNode) {
  90. $this->stylesAttributes[] = [
  91. self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
  92. self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
  93. ];
  94. }
  95. }
  96. /**
  97. * @return array The custom number formats
  98. */
  99. protected function getCustomNumberFormats()
  100. {
  101. if (!isset($this->customNumberFormats)) {
  102. $this->extractRelevantInfo();
  103. }
  104. return $this->customNumberFormats;
  105. }
  106. /**
  107. * @return array The styles attributes
  108. */
  109. protected function getStylesAttributes()
  110. {
  111. if (!isset($this->stylesAttributes)) {
  112. $this->extractRelevantInfo();
  113. }
  114. return $this->stylesAttributes;
  115. }
  116. /**
  117. * Returns whether the style with the given ID should consider
  118. * numeric values as timestamps and format the cell as a date.
  119. *
  120. * @param int $styleId Zero-based style ID
  121. * @return bool Whether the cell with the given cell should display a date instead of a numeric value
  122. */
  123. public function shouldFormatNumericValueAsDate($styleId)
  124. {
  125. $stylesAttributes = $this->getStylesAttributes();
  126. // Default style (0) does not format numeric values as timestamps. Only custom styles do.
  127. // Also if the style ID does not exist in the styles.xml file, format as numeric value.
  128. // Using isset here because it is way faster than array_key_exists...
  129. if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) {
  130. return false;
  131. }
  132. $styleAttributes = $stylesAttributes[$styleId];
  133. $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
  134. if (!$applyNumberFormat) {
  135. return false;
  136. }
  137. $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
  138. return $this->doesNumFmtIdIndicateDate($numFmtId);
  139. }
  140. /**
  141. * @param int $numFmtId
  142. * @return bool Whether the number format ID indicates that the number is a timestamp
  143. */
  144. protected function doesNumFmtIdIndicateDate($numFmtId)
  145. {
  146. return (
  147. $this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
  148. $this->isNumFmtIdCustomDateFormat($numFmtId)
  149. );
  150. }
  151. /**
  152. * @param int $numFmtId
  153. * @return bool Whether the number format ID indicates that the number is a timestamp
  154. */
  155. protected function isNumFmtIdBuiltInDateFormat($numFmtId)
  156. {
  157. $builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
  158. return in_array($numFmtId, $builtInDateFormatIds);
  159. }
  160. /**
  161. * @param int $numFmtId
  162. * @return bool Whether the number format ID indicates that the number is a timestamp
  163. */
  164. protected function isNumFmtIdCustomDateFormat($numFmtId)
  165. {
  166. $customNumberFormats = $this->getCustomNumberFormats();
  167. // Using isset here because it is way faster than array_key_exists...
  168. if (!isset($customNumberFormats[$numFmtId])) {
  169. return false;
  170. }
  171. $customNumberFormat = $customNumberFormats[$numFmtId];
  172. // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
  173. $pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
  174. $customNumberFormat = preg_replace($pattern, '', $customNumberFormat);
  175. // custom date formats contain specific characters to represent the date:
  176. // e - yy - m - d - h - s
  177. // and all of their variants (yyyy - mm - dd...)
  178. $dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's'];
  179. $hasFoundDateFormatCharacter = false;
  180. foreach ($dateFormatCharacters as $dateFormatCharacter) {
  181. // character not preceded by "\"
  182. $pattern = '/(?<!\\\)' . $dateFormatCharacter . '/';
  183. if (preg_match($pattern, $customNumberFormat)) {
  184. $hasFoundDateFormatCharacter = true;
  185. break;
  186. }
  187. }
  188. return $hasFoundDateFormatCharacter;
  189. }
  190. }