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\Style\NumberFormat;
4
 
5
use PhpOffice\PhpSpreadsheet\Shared\Date;
6
 
7
class DateFormatter
8
{
9
    /**
10
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
11
     */
12
    private const DATE_FORMAT_REPLACEMENTS = [
13
        // first remove escapes related to non-format characters
14
        '\\' => '',
15
        //    12-hour suffix
16
        'am/pm' => 'A',
17
        //    4-digit year
18
        'e' => 'Y',
19
        'yyyy' => 'Y',
20
        //    2-digit year
21
        'yy' => 'y',
22
        //    first letter of month - no php equivalent
23
        'mmmmm' => 'M',
24
        //    full month name
25
        'mmmm' => 'F',
26
        //    short month name
27
        'mmm' => 'M',
28
        //    mm is minutes if time, but can also be month w/leading zero
29
        //    so we try to identify times be the inclusion of a : separator in the mask
30
        //    It isn't perfect, but the best way I know how
31
        ':mm' => ':i',
32
        'mm:' => 'i:',
33
        //    full day of week name
34
        'dddd' => 'l',
35
        //    short day of week name
36
        'ddd' => 'D',
37
        //    days leading zero
38
        'dd' => 'd',
39
        //    days no leading zero
40
        'd' => 'j',
41
        //    fractional seconds - no php equivalent
42
        '.s' => '',
43
    ];
44
 
45
    /**
46
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
47
     */
48
    private const DATE_FORMAT_REPLACEMENTS24 = [
49
        'hh' => 'H',
50
        'h' => 'G',
51
        //    month leading zero
52
        'mm' => 'm',
53
        //    month no leading zero
54
        'm' => 'n',
55
        //    seconds
56
        'ss' => 's',
57
    ];
58
 
59
    /**
60
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
61
     */
62
    private const DATE_FORMAT_REPLACEMENTS12 = [
63
        'hh' => 'h',
64
        'h' => 'g',
65
        //    month leading zero
66
        'mm' => 'm',
67
        //    month no leading zero
68
        'm' => 'n',
69
        //    seconds
70
        'ss' => 's',
71
    ];
72
 
73
    private const HOURS_IN_DAY = 24;
74
    private const MINUTES_IN_DAY = 60 * self::HOURS_IN_DAY;
75
    private const SECONDS_IN_DAY = 60 * self::MINUTES_IN_DAY;
76
    private const INTERVAL_PRECISION = 10;
77
    private const INTERVAL_LEADING_ZERO = [
78
        '[hh]',
79
        '[mm]',
80
        '[ss]',
81
    ];
82
    private const INTERVAL_ROUND_PRECISION = [
83
        // hours and minutes truncate
84
        '[h]' => self::INTERVAL_PRECISION,
85
        '[hh]' => self::INTERVAL_PRECISION,
86
        '[m]' => self::INTERVAL_PRECISION,
87
        '[mm]' => self::INTERVAL_PRECISION,
88
        // seconds round
89
        '[s]' => 0,
90
        '[ss]' => 0,
91
    ];
92
    private const INTERVAL_MULTIPLIER = [
93
        '[h]' => self::HOURS_IN_DAY,
94
        '[hh]' => self::HOURS_IN_DAY,
95
        '[m]' => self::MINUTES_IN_DAY,
96
        '[mm]' => self::MINUTES_IN_DAY,
97
        '[s]' => self::SECONDS_IN_DAY,
98
        '[ss]' => self::SECONDS_IN_DAY,
99
    ];
100
 
101
    private static function tryInterval(bool &$seekingBracket, string &$block, mixed $value, string $format): void
102
    {
103
        if ($seekingBracket) {
104
            if (str_contains($block, $format)) {
105
                $hours = (string) (int) round(
106
                    self::INTERVAL_MULTIPLIER[$format] * $value,
107
                    self::INTERVAL_ROUND_PRECISION[$format]
108
                );
109
                if (strlen($hours) === 1 && in_array($format, self::INTERVAL_LEADING_ZERO, true)) {
110
                    $hours = "0$hours";
111
                }
112
                $block = str_replace($format, $hours, $block);
113
                $seekingBracket = false;
114
            }
115
        }
116
    }
117
 
118
    /** @param float|int $value value to be formatted */
119
    public static function format(mixed $value, string $format): string
120
    {
121
        // strip off first part containing e.g. [$-F800] or [$USD-409]
122
        // general syntax: [$<Currency string>-<language info>]
123
        // language info is in hexadecimal
124
        // strip off chinese part like [DBNum1][$-804]
125
        $format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format);
126
 
127
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
128
        //    but we don't want to change any quoted strings
129
        /** @var callable $callable */
130
        $callable = [self::class, 'setLowercaseCallback'];
131
        $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format);
132
 
133
        // Only process the non-quoted blocks for date format characters
134
 
135
        $blocks = explode('"', $format);
136
        foreach ($blocks as $key => &$block) {
137
            if ($key % 2 == 0) {
138
                $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS);
139
                if (!strpos($block, 'A')) {
140
                    // 24-hour time format
141
                    // when [h]:mm format, the [h] should replace to the hours of the value * 24
142
                    $seekingBracket = true;
143
                    self::tryInterval($seekingBracket, $block, $value, '[h]');
144
                    self::tryInterval($seekingBracket, $block, $value, '[hh]');
145
                    self::tryInterval($seekingBracket, $block, $value, '[mm]');
146
                    self::tryInterval($seekingBracket, $block, $value, '[m]');
147
                    self::tryInterval($seekingBracket, $block, $value, '[s]');
148
                    self::tryInterval($seekingBracket, $block, $value, '[ss]');
149
                    $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS24);
150
                } else {
151
                    // 12-hour time format
152
                    $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS12);
153
                }
154
            }
155
        }
156
        $format = implode('"', $blocks);
157
 
158
        // escape any quoted characters so that DateTime format() will render them correctly
159
        /** @var callable $callback */
160
        $callback = [self::class, 'escapeQuotesCallback'];
161
        $format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format);
162
 
163
        $dateObj = Date::excelToDateTimeObject($value);
164
        // If the colon preceding minute had been quoted, as happens in
165
        // Excel 2003 XML formats, m will not have been changed to i above.
166
        // Change it now.
167
        $format = (string) \preg_replace('/\\\:m/', ':i', $format);
168
        $microseconds = (int) $dateObj->format('u');
169
        if (str_contains($format, ':s.000')) {
170
            $milliseconds = (int) round($microseconds / 1000.0);
171
            if ($milliseconds === 1000) {
172
                $milliseconds = 0;
173
                $dateObj->modify('+1 second');
174
            }
175
            $dateObj->modify("-$microseconds microseconds");
176
            $format = str_replace(':s.000', ':s.' . sprintf('%03d', $milliseconds), $format);
177
        } elseif (str_contains($format, ':s.00')) {
178
            $centiseconds = (int) round($microseconds / 10000.0);
179
            if ($centiseconds === 100) {
180
                $centiseconds = 0;
181
                $dateObj->modify('+1 second');
182
            }
183
            $dateObj->modify("-$microseconds microseconds");
184
            $format = str_replace(':s.00', ':s.' . sprintf('%02d', $centiseconds), $format);
185
        } elseif (str_contains($format, ':s.0')) {
186
            $deciseconds = (int) round($microseconds / 100000.0);
187
            if ($deciseconds === 10) {
188
                $deciseconds = 0;
189
                $dateObj->modify('+1 second');
190
            }
191
            $dateObj->modify("-$microseconds microseconds");
192
            $format = str_replace(':s.0', ':s.' . sprintf('%1d', $deciseconds), $format);
193
        } else { // no fractional second
194
            if ($microseconds >= 500000) {
195
                $dateObj->modify('+1 second');
196
            }
197
            $dateObj->modify("-$microseconds microseconds");
198
        }
199
 
200
        return $dateObj->format($format);
201
    }
202
 
203
    private static function setLowercaseCallback(array $matches): string
204
    {
205
        return mb_strtolower($matches[0]);
206
    }
207
 
208
    private static function escapeQuotesCallback(array $matches): string
209
    {
210
        return '\\' . implode('\\', mb_str_split($matches[1], 1, 'UTF-8'));
211
    }
212
}