Failed Conditions
Push — develop ( 0ef1b5...a1e8c8 )
by Mark
34:44
created

Date   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 479
Duplicated Lines 0 %

Test Coverage

Coverage 86.03%

Importance

Changes 0
Metric Value
eloc 170
dl 0
loc 479
ccs 117
cts 136
cp 0.8603
rs 2.08
c 0
b 0
f 0
wmc 79

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isDateTimeFormat() 0 3 1
A excelToTimestamp() 0 4 1
A getExcelCalendar() 0 3 1
A formattedPHPToExcel() 0 33 5
A stringToExcel() 0 24 6
A dayStringToNumber() 0 8 2
A dateTimeToExcel() 0 9 1
A validateTimeZone() 0 9 4
A setDefaultTimezone() 0 9 2
A isDateTime() 0 6 1
A getDefaultTimezone() 0 7 2
A monthStringToNumber() 0 11 4
A timestampToExcel() 0 7 2
A setExcelCalendar() 0 10 3
B excelToDateTimeObject() 0 35 7
A PHPToExcel() 0 11 5
D isDateTimeFormatCode() 0 65 32

How to fix   Complexity   

Complex Class

Complex classes like Date often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Date, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Shared;
4
5
use DateTimeInterface;
6
use DateTimeZone;
7
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
8
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
9
use PhpOffice\PhpSpreadsheet\Cell\Cell;
10
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
11
12
class Date
13
{
14
    /** constants */
15
    const CALENDAR_WINDOWS_1900 = 1900; //    Base date of 1st Jan 1900 = 1.0
16
    const CALENDAR_MAC_1904 = 1904; //    Base date of 2nd Jan 1904 = 1.0
17
18
    /**
19
     * Names of the months of the year, indexed by shortname
20
     * Planned usage for locale settings.
21
     *
22
     * @var string[]
23
     */
24
    public static $monthNames = [
25
        'Jan' => 'January',
26
        'Feb' => 'February',
27
        'Mar' => 'March',
28
        'Apr' => 'April',
29
        'May' => 'May',
30
        'Jun' => 'June',
31
        'Jul' => 'July',
32
        'Aug' => 'August',
33
        'Sep' => 'September',
34
        'Oct' => 'October',
35
        'Nov' => 'November',
36
        'Dec' => 'December',
37
    ];
38
39
    /**
40
     * @var string[]
41
     */
42
    public static $numberSuffixes = [
43
        'st',
44
        'nd',
45
        'rd',
46
        'th',
47
    ];
48
49
    /**
50
     * Base calendar year to use for calculations
51
     * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904).
52
     *
53
     * @var int
54
     */
55
    protected static $excelCalendar = self::CALENDAR_WINDOWS_1900;
56
57
    /**
58
     * Default timezone to use for DateTime objects.
59
     *
60
     * @var null|\DateTimeZone
61
     */
62
    protected static $defaultTimeZone;
63
64
    /**
65
     * Set the Excel calendar (Windows 1900 or Mac 1904).
66
     *
67
     * @param int $baseDate Excel base date (1900 or 1904)
68
     *
69
     * @return bool Success or failure
70
     */
71 151
    public static function setExcelCalendar($baseDate)
72
    {
73 151
        if (($baseDate == self::CALENDAR_WINDOWS_1900) ||
74 151
            ($baseDate == self::CALENDAR_MAC_1904)) {
75 150
            self::$excelCalendar = $baseDate;
76
77 150
            return true;
78
        }
79
80 1
        return false;
81
    }
82
83
    /**
84
     * Return the Excel calendar (Windows 1900 or Mac 1904).
85
     *
86
     * @return int Excel base date (1900 or 1904)
87
     */
88 188
    public static function getExcelCalendar()
89
    {
90 188
        return self::$excelCalendar;
91
    }
92
93
    /**
94
     * Set the Default timezone to use for dates.
95
     *
96
     * @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
97
     *
98
     * @throws \Exception
99
     *
100
     * @return bool Success or failure
101
     * @return bool Success or failure
102
     */
103
    public static function setDefaultTimezone($timeZone)
104
    {
105
        if ($timeZone = self::validateTimeZone($timeZone)) {
106
            self::$defaultTimeZone = $timeZone;
107
108
            return true;
109
        }
110
111
        return false;
112
    }
113
114
    /**
115
     * Return the Default timezone being used for dates.
116
     *
117
     * @return DateTimeZone The timezone being used as default for Excel timestamp to PHP DateTime object
118
     */
119 444
    public static function getDefaultTimezone()
120
    {
121 444
        if (self::$defaultTimeZone === null) {
122 19
            self::$defaultTimeZone = new DateTimeZone('UTC');
123
        }
124
125 444
        return self::$defaultTimeZone;
126
    }
127
128
    /**
129
     * Validate a timezone.
130
     *
131
     * @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
132
     *
133
     * @throws \Exception
134
     *
135
     * @return DateTimeZone The timezone as a timezone object
136
     * @return DateTimeZone The timezone as a timezone object
137
     */
138 22
    protected static function validateTimeZone($timeZone)
139
    {
140 22
        if (is_object($timeZone) && $timeZone instanceof DateTimeZone) {
141
            return $timeZone;
142 22
        } elseif (is_string($timeZone)) {
143 22
            return new DateTimeZone($timeZone);
144
        }
145
146
        throw new \Exception('Invalid timezone');
147
    }
148
149
    /**
150
     * Convert a MS serialized datetime value from Excel to a PHP Date/Time object.
151
     *
152
     * @param float|int $excelTimestamp MS Excel serialized date/time value
153
     * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
154
     *                                                                        if you don't want to treat it as a UTC value
155
     *                                                                    Use the default (UST) unless you absolutely need a conversion
156
     *
157
     * @throws \Exception
158
     *
159
     * @return \DateTime PHP date/time object
160
     */
161 466
    public static function excelToDateTimeObject($excelTimestamp, $timeZone = null)
162
    {
163 466
        $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
164 466
        if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
165 463
            if ($excelTimestamp < 1.0) {
166
                // Unix timestamp base date
167 49
                $baseDate = new \DateTime('1970-01-01', $timeZone);
168
            } else {
169
                // MS Excel calendar base dates
170 419
                if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
171
                    // Allow adjustment for 1900 Leap Year in MS Excel
172 413
                    $baseDate = ($excelTimestamp < 60) ? new \DateTime('1899-12-31', $timeZone) : new \DateTime('1899-12-30', $timeZone);
173
                } else {
174 463
                    $baseDate = new \DateTime('1904-01-01', $timeZone);
175
                }
176
            }
177
        } else {
178 9
            $baseDate = new \DateTime('1899-12-30', $timeZone);
179
        }
180
181 466
        $days = floor($excelTimestamp);
182 466
        $partDay = $excelTimestamp - $days;
183 466
        $hours = floor($partDay * 24);
184 466
        $partDay = $partDay * 24 - $hours;
185 466
        $minutes = floor($partDay * 60);
186 466
        $partDay = $partDay * 60 - $minutes;
187 466
        $seconds = round($partDay * 60);
188
189 466
        if ($days >= 0) {
190 465
            $days = '+' . $days;
191
        }
192 466
        $interval = $days . ' days';
193
194 466
        return $baseDate->modify($interval)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $baseDate->modify...rs, $minutes, $seconds) could also return false which is incompatible with the documented return type DateTime. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
195 466
            ->setTime($hours, $minutes, $seconds);
0 ignored issues
show
Bug introduced by
$hours of type double is incompatible with the type integer expected by parameter $hour of DateTime::setTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
            ->setTime(/** @scrutinizer ignore-type */ $hours, $minutes, $seconds);
Loading history...
Bug introduced by
$minutes of type double is incompatible with the type integer expected by parameter $minute of DateTime::setTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
            ->setTime($hours, /** @scrutinizer ignore-type */ $minutes, $seconds);
Loading history...
Bug introduced by
$seconds of type double is incompatible with the type integer expected by parameter $second of DateTime::setTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
            ->setTime($hours, $minutes, /** @scrutinizer ignore-type */ $seconds);
Loading history...
196
    }
197
198
    /**
199
     * Convert a MS serialized datetime value from Excel to a unix timestamp.
200
     *
201
     * @param float|int $excelTimestamp MS Excel serialized date/time value
202
     * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
203
     *                                                                        if you don't want to treat it as a UTC value
204
     *                                                                    Use the default (UST) unless you absolutely need a conversion
205
     *
206
     * @throws \Exception
207
     *
208
     * @return int Unix timetamp for this date/time
209
     */
210 84
    public static function excelToTimestamp($excelTimestamp, $timeZone = null)
211
    {
212 84
        return (int) self::excelToDateTimeObject($excelTimestamp, $timeZone)
213 84
            ->format('U');
214
    }
215
216
    /**
217
     * Convert a date from PHP to an MS Excel serialized date/time value.
218
     *
219
     * @param mixed $dateValue Unix Timestamp or PHP DateTime object or a string
220
     *
221
     * @return bool|float Excel date/time value
222
     *                                  or boolean FALSE on failure
223
     */
224 64
    public static function PHPToExcel($dateValue)
225
    {
226 64
        if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
227 42
            return self::dateTimeToExcel($dateValue);
228 22
        } elseif (is_numeric($dateValue)) {
229 22
            return self::timestampToExcel($dateValue);
230 7
        } elseif (is_string($dateValue)) {
231
            return self::stringToExcel($dateValue);
232
        }
233
234 7
        return false;
235
    }
236
237
    /**
238
     * Convert a PHP DateTime object to an MS Excel serialized date/time value.
239
     *
240
     * @param DateTimeInterface $dateValue PHP DateTime object
241
     *
242
     * @return float MS Excel serialized date/time value
243
     */
244 96
    public static function dateTimeToExcel(DateTimeInterface $dateValue)
245
    {
246 96
        return self::formattedPHPToExcel(
247 96
            $dateValue->format('Y'),
0 ignored issues
show
Bug introduced by
$dateValue->format('Y') of type string is incompatible with the type integer expected by parameter $year of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
            /** @scrutinizer ignore-type */ $dateValue->format('Y'),
Loading history...
248 96
            $dateValue->format('m'),
0 ignored issues
show
Bug introduced by
$dateValue->format('m') of type string is incompatible with the type integer expected by parameter $month of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

248
            /** @scrutinizer ignore-type */ $dateValue->format('m'),
Loading history...
249 96
            $dateValue->format('d'),
0 ignored issues
show
Bug introduced by
$dateValue->format('d') of type string is incompatible with the type integer expected by parameter $day of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
            /** @scrutinizer ignore-type */ $dateValue->format('d'),
Loading history...
250 96
            $dateValue->format('H'),
0 ignored issues
show
Bug introduced by
$dateValue->format('H') of type string is incompatible with the type integer expected by parameter $hours of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
            /** @scrutinizer ignore-type */ $dateValue->format('H'),
Loading history...
251 96
            $dateValue->format('i'),
0 ignored issues
show
Bug introduced by
$dateValue->format('i') of type string is incompatible with the type integer expected by parameter $minutes of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

251
            /** @scrutinizer ignore-type */ $dateValue->format('i'),
Loading history...
252 96
            $dateValue->format('s')
0 ignored issues
show
Bug introduced by
$dateValue->format('s') of type string is incompatible with the type integer expected by parameter $seconds of PhpOffice\PhpSpreadsheet...::formattedPHPToExcel(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
            /** @scrutinizer ignore-type */ $dateValue->format('s')
Loading history...
253
        );
254
    }
255
256
    /**
257
     * Convert a Unix timestamp to an MS Excel serialized date/time value.
258
     *
259
     * @param int $dateValue Unix Timestamp
260
     *
261
     * @return float MS Excel serialized date/time value
262
     */
263 39
    public static function timestampToExcel($dateValue)
264
    {
265 39
        if (!is_numeric($dateValue)) {
0 ignored issues
show
introduced by
The condition is_numeric($dateValue) is always true.
Loading history...
266
            return false;
267
        }
268
269 39
        return self::dateTimeToExcel(new \DateTime('@' . $dateValue));
270
    }
271
272
    /**
273
     * formattedPHPToExcel.
274
     *
275
     * @param int $year
276
     * @param int $month
277
     * @param int $day
278
     * @param int $hours
279
     * @param int $minutes
280
     * @param int $seconds
281
     *
282
     * @return float Excel date/time value
283
     */
284 598
    public static function formattedPHPToExcel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0)
285
    {
286 598
        if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
287
            //
288
            //    Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
289
            //    This affects every date following 28th February 1900
290
            //
291 591
            $excel1900isLeapYear = true;
292 591
            if (($year == 1900) && ($month <= 2)) {
293 55
                $excel1900isLeapYear = false;
294
            }
295 591
            $myexcelBaseDate = 2415020;
296
        } else {
297 7
            $myexcelBaseDate = 2416481;
298 7
            $excel1900isLeapYear = false;
299
        }
300
301
        //    Julian base date Adjustment
302 598
        if ($month > 2) {
303 367
            $month -= 3;
304
        } else {
305 386
            $month += 9;
306 386
            --$year;
307
        }
308
309
        //    Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
310 598
        $century = substr($year, 0, 2);
311 598
        $decade = substr($year, 2, 2);
312 598
        $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear;
313
314 598
        $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
315
316 598
        return (float) $excelDate + $excelTime;
317
    }
318
319
    /**
320
     * Is a given cell a date/time?
321
     *
322
     * @param Cell $pCell
323
     *
324
     * @return bool
325
     */
326
    public static function isDateTime(Cell $pCell)
327
    {
328
        return self::isDateTimeFormat(
329
            $pCell->getWorksheet()->getStyle(
330
                $pCell->getCoordinate()
331
            )->getNumberFormat()
332
        );
333
    }
334
335
    /**
336
     * Is a given number format a date/time?
337
     *
338
     * @param NumberFormat $pFormat
339
     *
340
     * @return bool
341
     */
342
    public static function isDateTimeFormat(NumberFormat $pFormat)
343
    {
344
        return self::isDateTimeFormatCode($pFormat->getFormatCode());
345
    }
346
347
    private static $possibleDateFormatCharacters = 'eymdHs';
348
349
    /**
350
     * Is a given number format code a date/time?
351
     *
352
     * @param string $pFormatCode
353
     *
354
     * @return bool
355
     */
356 38
    public static function isDateTimeFormatCode($pFormatCode)
357
    {
358 38
        if (strtolower($pFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
359
            //    "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check)
360 1
            return false;
361
        }
362 37
        if (preg_match('/[0#]E[+-]0/i', $pFormatCode)) {
363
            //    Scientific format
364
            return false;
365
        }
366
367
        // Switch on formatcode
368 37
        switch ($pFormatCode) {
369
            //    Explicitly defined date formats
370
            case NumberFormat::FORMAT_DATE_YYYYMMDD:
371
            case NumberFormat::FORMAT_DATE_YYYYMMDD2:
372
            case NumberFormat::FORMAT_DATE_DDMMYYYY:
373
            case NumberFormat::FORMAT_DATE_DMYSLASH:
374
            case NumberFormat::FORMAT_DATE_DMYMINUS:
375
            case NumberFormat::FORMAT_DATE_DMMINUS:
376
            case NumberFormat::FORMAT_DATE_MYMINUS:
377
            case NumberFormat::FORMAT_DATE_DATETIME:
378
            case NumberFormat::FORMAT_DATE_TIME1:
379
            case NumberFormat::FORMAT_DATE_TIME2:
380
            case NumberFormat::FORMAT_DATE_TIME3:
381
            case NumberFormat::FORMAT_DATE_TIME4:
382
            case NumberFormat::FORMAT_DATE_TIME5:
383
            case NumberFormat::FORMAT_DATE_TIME6:
384
            case NumberFormat::FORMAT_DATE_TIME7:
385
            case NumberFormat::FORMAT_DATE_TIME8:
386
            case NumberFormat::FORMAT_DATE_YYYYMMDDSLASH:
387
            case NumberFormat::FORMAT_DATE_XLSX14:
388
            case NumberFormat::FORMAT_DATE_XLSX15:
389
            case NumberFormat::FORMAT_DATE_XLSX16:
390
            case NumberFormat::FORMAT_DATE_XLSX17:
391
            case NumberFormat::FORMAT_DATE_XLSX22:
392 20
                return true;
393
        }
394
395
        //    Typically number, currency or accounting (or occasionally fraction) formats
396 17
        if ((substr($pFormatCode, 0, 1) == '_') || (substr($pFormatCode, 0, 2) == '0 ')) {
397 1
            return false;
398
        }
399
        // Try checking for any of the date formatting characters that don't appear within square braces
400 16
        if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $pFormatCode)) {
401
            //    We might also have a format mask containing quoted strings...
402
            //        we don't want to test for any of our characters within the quoted blocks
403 5
            if (strpos($pFormatCode, '"') !== false) {
404 1
                $segMatcher = false;
405 1
                foreach (explode('"', $pFormatCode) as $subVal) {
406
                    //    Only test in alternate array entries (the non-quoted blocks)
407 1
                    if (($segMatcher = !$segMatcher) &&
0 ignored issues
show
introduced by
The condition $segMatcher is always false.
Loading history...
408 1
                        (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $subVal))) {
409 1
                        return true;
410
                    }
411
                }
412
413 1
                return false;
414
            }
415
416 4
            return true;
417
        }
418
419
        // No date...
420 11
        return false;
421
    }
422
423
    /**
424
     * Convert a date/time string to Excel time.
425
     *
426
     * @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10'
427
     *
428
     * @return false|float Excel date/time serial value
429
     */
430 2
    public static function stringToExcel($dateValue)
431
    {
432 2
        if (strlen($dateValue) < 2) {
433 1
            return false;
434
        }
435 2
        if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2})?)?$/iu', $dateValue)) {
436 2
            return false;
437
        }
438
439 2
        $dateValueNew = DateTime::DATEVALUE($dateValue);
440
441 2
        if ($dateValueNew === Functions::VALUE()) {
442
            return false;
443
        }
444
445 2
        if (strpos($dateValue, ':') !== false) {
446 1
            $timeValue = DateTime::TIMEVALUE($dateValue);
447 1
            if ($timeValue === Functions::VALUE()) {
448
                return false;
449
            }
450 1
            $dateValueNew += $timeValue;
451
        }
452
453 2
        return $dateValueNew;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $dateValueNew also could return the type string|DateTime which is incompatible with the documented return type false|double.
Loading history...
454
    }
455
456
    /**
457
     * Converts a month name (either a long or a short name) to a month number.
458
     *
459
     * @param string $month Month name or abbreviation
460
     *
461
     * @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name
462
     */
463 3
    public static function monthStringToNumber($month)
464
    {
465 3
        $monthIndex = 1;
466 3
        foreach (self::$monthNames as $shortMonthName => $longMonthName) {
467 3
            if (($month === $longMonthName) || ($month === $shortMonthName)) {
468 2
                return $monthIndex;
469
            }
470 3
            ++$monthIndex;
471
        }
472
473 1
        return $month;
474
    }
475
476
    /**
477
     * Strips an ordinal from a numeric value.
478
     *
479
     * @param string $day Day number with an ordinal
480
     *
481
     * @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric
482
     */
483 3
    public static function dayStringToNumber($day)
484
    {
485 3
        $strippedDayValue = (str_replace(self::$numberSuffixes, '', $day));
486 3
        if (is_numeric($strippedDayValue)) {
487 2
            return (int) $strippedDayValue;
488
        }
489
490 1
        return $day;
491
    }
492
}
493