Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Shared;
4
 
5
use DateTime;
6
use DateTimeInterface;
7
use DateTimeZone;
8
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
9
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
10
use PhpOffice\PhpSpreadsheet\Cell\Cell;
11
use PhpOffice\PhpSpreadsheet\Exception;
12
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
13
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
14
 
15
class Date
16
{
17
    /** constants */
18
    const CALENDAR_WINDOWS_1900 = 1900; //    Base date of 1st Jan 1900 = 1.0
19
    const CALENDAR_MAC_1904 = 1904; //    Base date of 2nd Jan 1904 = 1.0
20
 
21
    /**
22
     * Names of the months of the year, indexed by shortname
23
     * Planned usage for locale settings.
24
     *
25
     * @var string[]
26
     */
27
    public static array $monthNames = [
28
        'Jan' => 'January',
29
        'Feb' => 'February',
30
        'Mar' => 'March',
31
        'Apr' => 'April',
32
        'May' => 'May',
33
        'Jun' => 'June',
34
        'Jul' => 'July',
35
        'Aug' => 'August',
36
        'Sep' => 'September',
37
        'Oct' => 'October',
38
        'Nov' => 'November',
39
        'Dec' => 'December',
40
    ];
41
 
42
    /**
43
     * @var string[]
44
     */
45
    public static array $numberSuffixes = [
46
        'st',
47
        'nd',
48
        'rd',
49
        'th',
50
    ];
51
 
52
    /**
53
     * Base calendar year to use for calculations
54
     * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904).
55
     */
56
    protected static int $excelCalendar = self::CALENDAR_WINDOWS_1900;
57
 
58
    /**
59
     * Default timezone to use for DateTime objects.
60
     */
61
    protected static ?DateTimeZone $defaultTimeZone = null;
62
 
63
    /**
64
     * Set the Excel calendar (Windows 1900 or Mac 1904).
65
     *
66
     * @param ?int $baseYear Excel base date (1900 or 1904)
67
     *
68
     * @return bool Success or failure
69
     */
70
    public static function setExcelCalendar(?int $baseYear): bool
71
    {
72
        if (
73
            ($baseYear === self::CALENDAR_WINDOWS_1900)
74
            || ($baseYear === self::CALENDAR_MAC_1904)
75
        ) {
76
            self::$excelCalendar = $baseYear;
77
 
78
            return true;
79
        }
80
 
81
        return false;
82
    }
83
 
84
    /**
85
     * Return the Excel calendar (Windows 1900 or Mac 1904).
86
     *
87
     * @return int Excel base date (1900 or 1904)
88
     */
89
    public static function getExcelCalendar(): int
90
    {
91
        return self::$excelCalendar;
92
    }
93
 
94
    /**
95
     * Set the Default timezone to use for dates.
96
     *
97
     * @param null|DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
98
     *
99
     * @return bool Success or failure
100
     */
101
    public static function setDefaultTimezone($timeZone): bool
102
    {
103
        try {
104
            $timeZone = self::validateTimeZone($timeZone);
105
            self::$defaultTimeZone = $timeZone;
106
            $retval = true;
107
        } catch (PhpSpreadsheetException) {
108
            $retval = false;
109
        }
110
 
111
        return $retval;
112
    }
113
 
114
    /**
115
     * Return the Default timezone, or UTC if default not set.
116
     */
117
    public static function getDefaultTimezone(): DateTimeZone
118
    {
119
        return self::$defaultTimeZone ?? new DateTimeZone('UTC');
120
    }
121
 
122
    /**
123
     * Return the Default timezone, or local timezone if default is not set.
124
     */
125
    public static function getDefaultOrLocalTimezone(): DateTimeZone
126
    {
127
        return self::$defaultTimeZone ?? new DateTimeZone(date_default_timezone_get());
128
    }
129
 
130
    /**
131
     * Return the Default timezone even if null.
132
     */
133
    public static function getDefaultTimezoneOrNull(): ?DateTimeZone
134
    {
135
        return self::$defaultTimeZone;
136
    }
137
 
138
    /**
139
     * Validate a timezone.
140
     *
141
     * @param null|DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
142
     *
143
     * @return ?DateTimeZone The timezone as a timezone object
144
     */
145
    private static function validateTimeZone($timeZone): ?DateTimeZone
146
    {
147
        if ($timeZone instanceof DateTimeZone || $timeZone === null) {
148
            return $timeZone;
149
        }
150
        if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) {
151
            return new DateTimeZone($timeZone);
152
        }
153
 
154
        throw new PhpSpreadsheetException('Invalid timezone');
155
    }
156
 
157
    /**
158
     * @param mixed $value Converts a date/time in ISO-8601 standard format date string to an Excel
159
     *                         serialized timestamp.
160
     *                     See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format.
161
     */
162
    public static function convertIsoDate(mixed $value): float|int
163
    {
164
        if (!is_string($value)) {
165
            throw new Exception('Non-string value supplied for Iso Date conversion');
166
        }
167
 
168
        $date = new DateTime($value);
169
        $dateErrors = DateTime::getLastErrors();
170
 
171
        if (is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) {
172
            throw new Exception("Invalid string $value supplied for datatype Date");
173
        }
174
 
175
        $newValue = self::PHPToExcel($date);
176
        if ($newValue === false) {
177
            throw new Exception("Invalid string $value supplied for datatype Date");
178
        }
179
 
180
        if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) {
181
            $newValue = fmod($newValue, 1.0);
182
        }
183
 
184
        return $newValue;
185
    }
186
 
187
    /**
188
     * Convert a MS serialized datetime value from Excel to a PHP Date/Time object.
189
     *
190
     * @param float|int $excelTimestamp MS Excel serialized date/time value
191
     * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
192
     *                                           if you don't want to treat it as a UTC value
193
     *                                           Use the default (UTC) unless you absolutely need a conversion
194
     *
195
     * @return DateTime PHP date/time object
196
     */
197
    public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime
198
    {
199
        $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
200
        if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
201
            if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) {
202
                // Unix timestamp base date
203
                $baseDate = new DateTime('1970-01-01', $timeZone);
204
            } else {
205
                // MS Excel calendar base dates
206
                if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
207
                    // Allow adjustment for 1900 Leap Year in MS Excel
208
                    $baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone);
209
                } else {
210
                    $baseDate = new DateTime('1904-01-01', $timeZone);
211
                }
212
            }
213
        } else {
214
            $baseDate = new DateTime('1899-12-30', $timeZone);
215
        }
216
 
217
        $days = floor($excelTimestamp);
218
        $partDay = $excelTimestamp - $days;
219
        $hms = 86400 * $partDay;
220
        $microseconds = (int) round(fmod($hms, 1) * 1000000);
221
        $hms = (int) floor($hms);
222
        $hours = intdiv($hms, 3600);
223
        $hms -= $hours * 3600;
224
        $minutes = intdiv($hms, 60);
225
        $seconds = $hms % 60;
226
 
227
        if ($days >= 0) {
228
            $days = '+' . $days;
229
        }
230
        $interval = $days . ' days';
231
 
232
        return $baseDate->modify($interval)
233
            ->setTime($hours, $minutes, $seconds, $microseconds);
234
    }
235
 
236
    /**
237
     * Convert a MS serialized datetime value from Excel to a unix timestamp.
238
     * The use of Unix timestamps, and therefore this function, is discouraged.
239
     * They are not Y2038-safe on a 32-bit system, and have no timezone info.
240
     *
241
     * @param float|int $excelTimestamp MS Excel serialized date/time value
242
     * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
243
     *                                               if you don't want to treat it as a UTC value
244
     *                                               Use the default (UTC) unless you absolutely need a conversion
245
     *
246
     * @return int Unix timetamp for this date/time
247
     */
248
    public static function excelToTimestamp($excelTimestamp, $timeZone = null): int
249
    {
250
        $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone);
251
        self::roundMicroseconds($dto);
252
 
253
        return (int) $dto->format('U');
254
    }
255
 
256
    /**
257
     * Convert a date from PHP to an MS Excel serialized date/time value.
258
     *
259
     * @param mixed $dateValue PHP DateTime object or a string - Unix timestamp is also permitted, but discouraged;
260
     *    not Y2038-safe on a 32-bit system, and no timezone info
261
     *
262
     * @return false|float Excel date/time value
263
     *                                  or boolean FALSE on failure
264
     */
265
    public static function PHPToExcel(mixed $dateValue)
266
    {
267
        if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
268
            return self::dateTimeToExcel($dateValue);
269
        } elseif (is_numeric($dateValue)) {
270
            return self::timestampToExcel($dateValue);
271
        } elseif (is_string($dateValue)) {
272
            return self::stringToExcel($dateValue);
273
        }
274
 
275
        return false;
276
    }
277
 
278
    /**
279
     * Convert a PHP DateTime object to an MS Excel serialized date/time value.
280
     *
281
     * @param DateTimeInterface $dateValue PHP DateTime object
282
     *
283
     * @return float MS Excel serialized date/time value
284
     */
285
    public static function dateTimeToExcel(DateTimeInterface $dateValue): float
286
    {
287
        $seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u'));
288
 
289
        return self::formattedPHPToExcel(
290
            (int) $dateValue->format('Y'),
291
            (int) $dateValue->format('m'),
292
            (int) $dateValue->format('d'),
293
            (int) $dateValue->format('H'),
294
            (int) $dateValue->format('i'),
295
            $seconds
296
        );
297
    }
298
 
299
    /**
300
     * Convert a Unix timestamp to an MS Excel serialized date/time value.
301
     * The use of Unix timestamps, and therefore this function, is discouraged.
302
     * They are not Y2038-safe on a 32-bit system, and have no timezone info.
303
     *
304
     * @param float|int|string $unixTimestamp Unix Timestamp
305
     *
306
     * @return false|float MS Excel serialized date/time value
307
     */
308
    public static function timestampToExcel($unixTimestamp): bool|float
309
    {
310
        if (!is_numeric($unixTimestamp)) {
311
            return false;
312
        }
313
 
314
        return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
315
    }
316
 
317
    /**
318
     * formattedPHPToExcel.
319
     *
320
     * @return float Excel date/time value
321
     */
322
    public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float
323
    {
324
        if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
325
            //
326
            //    Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
327
            //    This affects every date following 28th February 1900
328
            //
329
            $excel1900isLeapYear = true;
330
            if (($year == 1900) && ($month <= 2)) {
331
                $excel1900isLeapYear = false;
332
            }
333
            $myexcelBaseDate = 2415020;
334
        } else {
335
            $myexcelBaseDate = 2416481;
336
            $excel1900isLeapYear = false;
337
        }
338
 
339
        //    Julian base date Adjustment
340
        if ($month > 2) {
341
            $month -= 3;
342
        } else {
343
            $month += 9;
344
            --$year;
345
        }
346
 
347
        //    Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
348
        $century = (int) substr((string) $year, 0, 2);
349
        $decade = (int) substr((string) $year, 2, 2);
350
        $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear;
351
 
352
        $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
353
 
354
        return (float) $excelDate + $excelTime;
355
    }
356
 
357
    /**
358
     * Is a given cell a date/time?
359
     */
360
    public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWithoutTimeOkay = true): bool
361
    {
362
        $result = false;
363
        $worksheet = $cell->getWorksheetOrNull();
364
        $spreadsheet = ($worksheet === null) ? null : $worksheet->getParent();
365
        if ($worksheet !== null && $spreadsheet !== null) {
366
            $index = $spreadsheet->getActiveSheetIndex();
367
            $selected = $worksheet->getSelectedCells();
368
 
369
            try {
370
                $result = is_numeric($value ?? $cell->getCalculatedValue())
371
                    && self::isDateTimeFormat(
372
                        $worksheet->getStyle(
373
                            $cell->getCoordinate()
374
                        )->getNumberFormat(),
375
                        $dateWithoutTimeOkay
376
                    );
377
            } catch (Exception) {
378
                // Result is already false, so no need to actually do anything here
379
            }
380
            $worksheet->setSelectedCells($selected);
381
            $spreadsheet->setActiveSheetIndex($index);
382
        }
383
 
384
        return $result;
385
    }
386
 
387
    /**
388
     * Is a given NumberFormat code a date/time format code?
389
     */
390
    public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true): bool
391
    {
392
        return self::isDateTimeFormatCode((string) $excelFormatCode->getFormatCode(), $dateWithoutTimeOkay);
393
    }
394
 
395
    private const POSSIBLE_DATETIME_FORMAT_CHARACTERS = 'eymdHs';
396
    private const POSSIBLE_TIME_FORMAT_CHARACTERS = 'Hs'; // note - no 'm' due to ambiguity
397
 
398
    /**
399
     * Is a given number format code a date/time?
400
     */
401
    public static function isDateTimeFormatCode(string $excelFormatCode, bool $dateWithoutTimeOkay = true): bool
402
    {
403
        if (strtolower($excelFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
404
            //    "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check)
405
            return false;
406
        }
407
        if (preg_match('/[0#]E[+-]0/i', $excelFormatCode)) {
408
            //    Scientific format
409
            return false;
410
        }
411
 
412
        // Switch on formatcode
413
        $excelFormatCode = (string) NumberFormat::convertSystemFormats($excelFormatCode);
414
        if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) {
415
            return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY);
416
        }
417
 
418
        //    Typically number, currency or accounting (or occasionally fraction) formats
419
        if ((str_starts_with($excelFormatCode, '_')) || (str_starts_with($excelFormatCode, '0 '))) {
420
            return false;
421
        }
422
        // Some "special formats" provided in German Excel versions were detected as date time value,
423
        // so filter them out here - "\C\H\-00000" (Switzerland) and "\D-00000" (Germany).
424
        if (str_contains($excelFormatCode, '-00000')) {
425
            return false;
426
        }
427
        $possibleFormatCharacters = $dateWithoutTimeOkay ? self::POSSIBLE_DATETIME_FORMAT_CHARACTERS : self::POSSIBLE_TIME_FORMAT_CHARACTERS;
428
        // Try checking for any of the date formatting characters that don't appear within square braces
429
        if (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $excelFormatCode)) {
430
            //    We might also have a format mask containing quoted strings...
431
            //        we don't want to test for any of our characters within the quoted blocks
432
            if (str_contains($excelFormatCode, '"')) {
433
                $segMatcher = false;
434
                foreach (explode('"', $excelFormatCode) as $subVal) {
435
                    //    Only test in alternate array entries (the non-quoted blocks)
436
                    $segMatcher = $segMatcher === false;
437
                    if (
438
                        $segMatcher
439
                        && (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $subVal))
440
                    ) {
441
                        return true;
442
                    }
443
                }
444
 
445
                return false;
446
            }
447
 
448
            return true;
449
        }
450
 
451
        // No date...
452
        return false;
453
    }
454
 
455
    /**
456
     * Convert a date/time string to Excel time.
457
     *
458
     * @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10'
459
     *
460
     * @return false|float Excel date/time serial value
461
     */
462
    public static function stringToExcel(string $dateValue): bool|float
463
    {
464
        if (strlen($dateValue) < 2) {
465
            return false;
466
        }
467
        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)) {
468
            return false;
469
        }
470
 
471
        $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
472
 
473
        if (!is_float($dateValueNew)) {
474
            return false;
475
        }
476
 
477
        if (str_contains($dateValue, ':')) {
478
            $timeValue = DateTimeExcel\TimeValue::fromString($dateValue);
479
            if (!is_float($timeValue)) {
480
                return false;
481
            }
482
            $dateValueNew += $timeValue;
483
        }
484
 
485
        return $dateValueNew;
486
    }
487
 
488
    /**
489
     * Converts a month name (either a long or a short name) to a month number.
490
     *
491
     * @param string $monthName Month name or abbreviation
492
     *
493
     * @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name
494
     */
495
    public static function monthStringToNumber(string $monthName)
496
    {
497
        $monthIndex = 1;
498
        foreach (self::$monthNames as $shortMonthName => $longMonthName) {
499
            if (($monthName === $longMonthName) || ($monthName === $shortMonthName)) {
500
                return $monthIndex;
501
            }
502
            ++$monthIndex;
503
        }
504
 
505
        return $monthName;
506
    }
507
 
508
    /**
509
     * Strips an ordinal from a numeric value.
510
     *
511
     * @param string $day Day number with an ordinal
512
     *
513
     * @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric
514
     */
515
    public static function dayStringToNumber(string $day)
516
    {
517
        $strippedDayValue = (str_replace(self::$numberSuffixes, '', $day));
518
        if (is_numeric($strippedDayValue)) {
519
            return (int) $strippedDayValue;
520
        }
521
 
522
        return $day;
523
    }
524
 
525
    public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime
526
    {
527
        $dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime();
528
        $dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone());
529
 
530
        return $dtobj;
531
    }
532
 
533
    public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string
534
    {
535
        $dtobj = self::dateTimeFromTimestamp($date, $timeZone);
536
 
537
        return $dtobj->format($format);
538
    }
539
 
540
    /**
541
     * Round the given DateTime object to seconds.
542
     */
543
    public static function roundMicroseconds(DateTime $dti): void
544
    {
545
        $microseconds = (int) $dti->format('u');
546
        $rounded = (int) round($microseconds, -6);
547
        $modify = $rounded - $microseconds;
548
        if ($modify !== 0) {
549
            $dti->modify(($modify > 0 ? '+' : '') . $modify . ' microseconds');
550
        }
551
    }
552
}