Completed
Pull Request — master (#155)
by
unknown
11:37 queued 15s
created

CellValueFormatter::formatExcelTimestampValue()   B

Complexity

Conditions 4
Paths 10

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.128

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 26
ccs 8
cts 10
cp 0.8
rs 8.5806
cc 4
eloc 14
nc 10
nop 1
crap 4.128
1
<?php
2
3
namespace Box\Spout\Reader\XLSX\Helper;
4
5
/**
6
 * Class CellValueFormatter
7
 * This class provides helper functions to format cell values
8
 *
9
 * @package Box\Spout\Reader\XLSX\Helper
10
 */
11
class CellValueFormatter
12
{
13
    /** Definition of all possible cell types */
14
    const CELL_TYPE_INLINE_STRING = 'inlineStr';
15
    const CELL_TYPE_STR = 'str';
16
    const CELL_TYPE_SHARED_STRING = 's';
17
    const CELL_TYPE_BOOLEAN = 'b';
18
    const CELL_TYPE_NUMERIC = 'n';
19
    const CELL_TYPE_DATE = 'd';
20
    const CELL_TYPE_ERROR = 'e';
21
22
    /** Definition of XML nodes names used to parse data */
23
    const XML_NODE_VALUE = 'v';
24
    const XML_NODE_INLINE_STRING_VALUE = 't';
25
26
    /** Definition of XML attributes used to parse data */
27
    const XML_ATTRIBUTE_TYPE = 't';
28
    const XML_ATTRIBUTE_STYLE_ID = 's';
29
30
    /** Constants used for date formatting */
31
    const NUM_SECONDS_IN_ONE_DAY = 86400;
32
33
    /**
34
     * February 29th, 1900 is NOT a leap year but Excel thinks it is...
35
     * @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
36
     */
37
    const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
38
39
    /** @var SharedStringsHelper Helper to work with shared strings */
40
    protected $sharedStringsHelper;
41
42
    /** @var StyleHelper Helper to work with styles */
43
    protected $styleHelper;
44
45
    /** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
46
    protected $escaper;
47
48
    /**
49
     * @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
50
     * @param StyleHelper $styleHelper Helper to work with styles
51
     */
52
    public function __construct($sharedStringsHelper, $styleHelper)
53 60
    {
54
        $this->sharedStringsHelper = $sharedStringsHelper;
55 60
        $this->styleHelper = $styleHelper;
56 60
57
        /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
58
        $this->escaper = new \Box\Spout\Common\Escaper\XLSX();
59 60
    }
60 60
61
    /**
62
     * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
63
     *
64
     * @param \DOMNode $node
65
     * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
66
     */
67
    public function extractAndFormatNodeValue($node)
68 51
    {
69
        // Default cell type is "n"
70
        $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
71 51
        $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
72 51
        $vNodeValue = $this->getVNodeValue($node);
73 51
74
        if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
75 51
            return $vNodeValue;
76 3
        }
77
78
        switch ($cellType) {
79
            case self::CELL_TYPE_INLINE_STRING:
80 51
                return $this->formatInlineStringCellValue($node);
81 12
            case self::CELL_TYPE_SHARED_STRING:
82 42
                return $this->formatSharedStringCellValue($vNodeValue);
83 33
            case self::CELL_TYPE_STR:
84 15
                return $this->formatStrCellValue($vNodeValue);
85 3
            case self::CELL_TYPE_BOOLEAN:
86 15
                return $this->formatBooleanCellValue($vNodeValue);
87 3
            case self::CELL_TYPE_NUMERIC:
88 15
                return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
89 15
            case self::CELL_TYPE_DATE:
90 3
                return $this->formatDateCellValue($vNodeValue);
91 3
            default:
92 3
                return null;
93 3
        }
94 3
    }
95
96
    /**
97
     * Returns the cell's string value from a node's nested value node
98
     *
99
     * @param \DOMNode $node
100
     * @return string The value associated with the cell
101
     */
102
    protected function getVNodeValue($node)
103 51
    {
104
        // for cell types having a "v" tag containing the value.
105
        // if not, the returned value should be empty string.
106
        $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
107 51
        return ($vNode !== null) ? $vNode->nodeValue : '';
108 51
    }
109
110
    /**
111
     * Returns the cell String value where string is inline.
112
     *
113
     * @param \DOMNode $node
114
     * @return string The value associated with the cell (null when the cell has an error)
115
     */
116
    protected function formatInlineStringCellValue($node)
117 12
    {
118
        // inline strings are formatted this way:
119
        // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
120
        $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
121 12
        $escapedCellValue = trim($tNode->nodeValue);
122 12
        $cellValue = $this->escaper->unescape($escapedCellValue);
123 12
        return $cellValue;
124 12
    }
125
126
    /**
127
     * Returns the cell String value from shared-strings file using nodeValue index.
128
     *
129
     * @param string $nodeValue
130
     * @return string The value associated with the cell (null when the cell has an error)
131
     */
132
    protected function formatSharedStringCellValue($nodeValue)
133 33
    {
134
        // shared strings are formatted this way:
135
        // <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
136
        $sharedStringIndex = intval($nodeValue);
137 33
        $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
138 33
        $cellValue = $this->escaper->unescape($escapedCellValue);
139 33
        return $cellValue;
140 33
    }
141
142
    /**
143
     * Returns the cell String value, where string is stored in value node.
144
     *
145
     * @param string $nodeValue
146
     * @return string The value associated with the cell (null when the cell has an error)
147
     */
148
    protected function formatStrCellValue($nodeValue)
149 3
    {
150
        $escapedCellValue = trim($nodeValue);
151 3
        $cellValue = $this->escaper->unescape($escapedCellValue);
152 3
        return $cellValue;
153 3
    }
154
155
    /**
156
     * Returns the cell Numeric value from string of nodeValue.
157
     * The value can also represent a timestamp and a DateTime will be returned.
158
     *
159
     * @param string $nodeValue
160
     * @param int $cellStyleId 0 being the default style
161
     * @return int|float|\DateTime|null The value associated with the cell
0 ignored issues
show
Documentation introduced by
Should the return type not be null|\DateTime|false|integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
162
     */
163
    protected function formatNumericCellValue($nodeValue, $cellStyleId)
164 15
    {
165
        // Numeric values can represent numbers as well as timestamps.
166
        // We need to look at the style of the cell to determine whether it is one or the other.
167
        $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
168 15
169
        if ($shouldFormatAsDate) {
170 15
            return $this->formatExcelTimestampValue(floatval($nodeValue));
171 6
        } else {
172
            return is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
173 9
        }
174
    }
175
176
    /**
177
     * Returns a cell's PHP Date value, associated to the given timestamp.
178
     * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
179
     *
180
     * @param float $nodeValue
181
     * @return \DateTime|null The value associated with the cell or NULL if invalid date value
0 ignored issues
show
Documentation introduced by
Should the return type not be null|\DateTime|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
182
     */
183
    protected function formatExcelTimestampValue($nodeValue)
184 6
    {
185
        if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
186 6
            --$nodeValue;
187
        }
188
189 6
        if ($nodeValue < 1) {
190 3
            return null;
191 3
        }
192
193 6
        // Do not use any unix timestamps for calculation to prevent
194
        // issues with numbers exceeding 2^31.
195
196 6
        $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
197 6
        $secondsRemainder = round($secondsRemainder);
198
199
        try {
200
            $cellValue = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
201
            $cellValue->modify(intval($nodeValue) . 'days');
202
            $cellValue->modify(intval($secondsRemainder) . 'seconds');
203
204
            return $cellValue;
205
        } catch (\Exception $e) {
206
            return null;
207
        }
208
    }
209 3
210
    /**
211
     * Returns the cell Boolean value from a specific node's Value.
212 3
     *
213 3
     * @param string $nodeValue
214
     * @return bool The value associated with the cell
215
     */
216
    protected function formatBooleanCellValue($nodeValue)
217
    {
218
        // !! is similar to boolval()
219
        $cellValue = !!$nodeValue;
220
        return $cellValue;
221
    }
222 3
223
    /**
224
     * Returns a cell's PHP Date value, associated to the given stored nodeValue.
225
     *
226 3
     * @param string $nodeValue
227 3
     * @return \DateTime|null The value associated with the cell or NULL if invalid date value
228 3
     */
229 3
    protected function formatDateCellValue($nodeValue)
230
    {
231
        // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
232
        try {
233
            $cellValue = new \DateTime($nodeValue);
234
            return $cellValue;
235
        } catch (\Exception $e) {
236
            return null;
237
        }
238
    }
239
}
240