1
|
|
|
<?php |
2
|
|
|
namespace Agavi\Date; |
3
|
|
|
|
4
|
|
|
// +---------------------------------------------------------------------------+ |
5
|
|
|
// | This file is part of the Agavi package. | |
6
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
7
|
|
|
// | | |
8
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
9
|
|
|
// | file that was distributed with this source code. You can also view the | |
10
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
11
|
|
|
// | vi: set noexpandtab: | |
12
|
|
|
// | Local Variables: | |
13
|
|
|
// | indent-tabs-mode: t | |
14
|
|
|
// | End: | |
15
|
|
|
// +---------------------------------------------------------------------------+ |
16
|
|
|
use Agavi\Core\Context; |
17
|
|
|
use Agavi\Exception\AgaviException; |
18
|
|
|
use Agavi\Translation\Locale; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* This class lets your format date and time according to a given format |
22
|
|
|
* definition. |
23
|
|
|
* |
24
|
|
|
* @package agavi |
25
|
|
|
* @subpackage util |
26
|
|
|
* |
27
|
|
|
* @author Dominik del Bondio <[email protected]> |
28
|
|
|
* @copyright Authors |
29
|
|
|
* @copyright The Agavi Project |
30
|
|
|
* |
31
|
|
|
* @since 0.11.0 |
32
|
|
|
* |
33
|
|
|
* @version $Id$ |
34
|
|
|
*/ |
35
|
|
|
class DateFormat |
36
|
|
|
{ |
37
|
|
|
/** |
38
|
|
|
* @var string The format string given by the user |
39
|
|
|
*/ |
40
|
|
|
protected $originalFormatString = null; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var string The format string which will be given to sprintf |
44
|
|
|
*/ |
45
|
|
|
protected $formatString = ''; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var Context An Context instance. |
49
|
|
|
*/ |
50
|
|
|
protected $context = null; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Constructs a new date formatter. |
54
|
|
|
* |
55
|
|
|
* @param string $format Format to be used for formatting. |
56
|
|
|
* |
57
|
|
|
* @author Dominik del Bondio <[email protected]> |
58
|
|
|
* @since 0.11.0 |
59
|
|
|
*/ |
60
|
|
|
public function __construct($format = null) |
61
|
|
|
{ |
62
|
|
|
if ($format) { |
|
|
|
|
63
|
|
|
$this->setFormat($format); |
64
|
|
|
} |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Initialize this Format. |
69
|
|
|
* |
70
|
|
|
* @param Context $context The current application context. |
71
|
|
|
* @param array $parameters An associative array of initialization parameters |
72
|
|
|
* |
73
|
|
|
* @author Dominik del Bondio <[email protected]> |
74
|
|
|
* @since 1.0.0 |
75
|
|
|
*/ |
76
|
|
|
public function initialize(Context $context, array $parameters = array()) |
77
|
|
|
{ |
78
|
|
|
$this->context = $context; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Retrieve the current application context. |
83
|
|
|
* |
84
|
|
|
* @return Context The current Context instance. |
85
|
|
|
* |
86
|
|
|
* @author Dominik del Bondio <[email protected]> |
87
|
|
|
* @since 1.0.0 |
88
|
|
|
*/ |
89
|
|
|
final public function getContext() |
90
|
|
|
{ |
91
|
|
|
return $this->context; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
|
95
|
|
|
const T_TEXT = -1; |
96
|
|
|
|
97
|
|
|
const T_ERA = 0; |
98
|
|
|
const T_YEAR = 1; |
99
|
|
|
const T_MONTH = 2; |
100
|
|
|
const T_DATE = 3; |
101
|
|
|
const T_HOUR_1_24 = 4; |
102
|
|
|
const T_HOUR_0_23 = 5; |
103
|
|
|
const T_MINUTE = 6; |
104
|
|
|
const T_SECOND = 7; |
105
|
|
|
const T_FRACTIONAL_SECOND = 8; |
106
|
|
|
const T_DAY_OF_WEEK = 9; |
107
|
|
|
const T_DAY_OF_YEAR = 10; |
108
|
|
|
const T_DAY_OF_WEEK_IN_MONTH = 11; |
109
|
|
|
const T_WEEK_OF_YEAR = 12; |
110
|
|
|
const T_WEEK_OF_MONTH = 13; |
111
|
|
|
const T_AM_PM = 14; |
112
|
|
|
const T_HOUR_1_12 = 15; |
113
|
|
|
const T_HOUR_0_11 = 16; |
114
|
|
|
const T_TIMEZONE = 17; |
115
|
|
|
const T_ISO_YEAR = 18; |
116
|
|
|
const T_LOCAL_DAY_OF_WEEK = 19; |
117
|
|
|
const T_EXTENDED_YEAR = 20; |
118
|
|
|
const T_MODIFIED_JD = 21; |
119
|
|
|
const T_MS_IN_DAY = 22; |
120
|
|
|
const T_TIMEZONE_RFC = 23; |
121
|
|
|
const T_TIMEZONE_GENERIC = 24; |
122
|
|
|
const T_SA_LOCAL_DAY_OF_WEEK = 25; |
123
|
|
|
const T_SA_MONTH = 26; |
124
|
|
|
const T_QUARTER = 27; |
125
|
|
|
const T_SA_QUARTER = 28; |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @var array The default mapping of format characters to their |
129
|
|
|
* meanings. |
130
|
|
|
*/ |
131
|
|
|
protected static $defaultMap = array( |
132
|
|
|
'G' => self::T_ERA, |
133
|
|
|
'y' => self::T_YEAR, |
134
|
|
|
'M' => self::T_MONTH, |
135
|
|
|
'd' => self::T_DATE, |
136
|
|
|
'k' => self::T_HOUR_1_24, |
137
|
|
|
'H' => self::T_HOUR_0_23, |
138
|
|
|
'm' => self::T_MINUTE, |
139
|
|
|
's' => self::T_SECOND, |
140
|
|
|
'S' => self::T_FRACTIONAL_SECOND, |
141
|
|
|
'E' => self::T_DAY_OF_WEEK, |
142
|
|
|
'D' => self::T_DAY_OF_YEAR, |
143
|
|
|
'F' => self::T_DAY_OF_WEEK_IN_MONTH, |
144
|
|
|
'w' => self::T_WEEK_OF_YEAR, |
145
|
|
|
'W' => self::T_WEEK_OF_MONTH, |
146
|
|
|
'a' => self::T_AM_PM, |
147
|
|
|
'h' => self::T_HOUR_1_12, |
148
|
|
|
'K' => self::T_HOUR_0_11, |
149
|
|
|
'z' => self::T_TIMEZONE, |
150
|
|
|
'Y' => self::T_ISO_YEAR, |
151
|
|
|
'e' => self::T_LOCAL_DAY_OF_WEEK, |
152
|
|
|
'u' => self::T_EXTENDED_YEAR, |
153
|
|
|
'g' => self::T_MODIFIED_JD, |
154
|
|
|
'A' => self::T_MS_IN_DAY, |
155
|
|
|
'Z' => self::T_TIMEZONE_RFC, |
156
|
|
|
'v' => self::T_TIMEZONE_GENERIC, |
157
|
|
|
'c' => self::T_SA_LOCAL_DAY_OF_WEEK, |
158
|
|
|
'L' => self::T_SA_MONTH, |
159
|
|
|
'Q' => self::T_QUARTER, |
160
|
|
|
'q' => self::T_SA_QUARTER, |
161
|
|
|
); |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @var array The list of tokens in the format. |
165
|
|
|
*/ |
166
|
|
|
protected $tokenList; |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Returns the format which is currently used to format dates. |
170
|
|
|
* |
171
|
|
|
* @return string The current format. |
172
|
|
|
* |
173
|
|
|
* @author Dominik del Bondio <[email protected]> |
174
|
|
|
* @since 0.11.0 |
175
|
|
|
*/ |
176
|
|
|
public function getFormat() |
177
|
|
|
{ |
178
|
|
|
return $this->originalFormatString; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Sets the format which should be used. |
183
|
|
|
* |
184
|
|
|
* @param string $format Format to be used for formatting. |
185
|
|
|
* |
186
|
|
|
* @author Dominik del Bondio <[email protected]> |
187
|
|
|
* @since 0.11.0 |
188
|
|
|
*/ |
189
|
|
|
public function setFormat($format) |
190
|
|
|
{ |
191
|
|
|
$this->internalParseFormat($format, self::$defaultMap); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Sets the format which should be used. This will use the the format |
196
|
|
|
* characters specified in the locale instead the default ones. |
197
|
|
|
* |
198
|
|
|
* NOTE: this function is not implemented yet! |
199
|
|
|
* |
200
|
|
|
* @param string $format Format to be used for formatting. |
201
|
|
|
* @param Locale $locale The locale. |
202
|
|
|
* |
203
|
|
|
* @author Dominik del Bondio <[email protected]> |
204
|
|
|
* @since 0.11.0 |
205
|
|
|
*/ |
206
|
|
|
public function setLocalizedFormat($format, Locale $locale) |
|
|
|
|
207
|
|
|
{ |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Formats a given date. |
212
|
|
|
* |
213
|
|
|
* @param mixed $data The date. This can either be an array containing all the |
214
|
|
|
* needed info with the AgaviDateDefinitions constants as |
215
|
|
|
* keys or an unix timestamp (doesn't work yet!) or a |
216
|
|
|
* Calendar instance. |
217
|
|
|
* @param string $calendarType The calendar type this date should be formatted in |
218
|
|
|
* (this will usually be gregorian) |
219
|
|
|
* @param Locale|string $locale The locale to format the date in. |
220
|
|
|
* |
221
|
|
|
* @return string The formatted date. |
222
|
|
|
* |
223
|
|
|
* @author Dominik del Bondio <[email protected]> |
224
|
|
|
* @since 0.11.0 |
225
|
|
|
*/ |
226
|
|
|
public function format($data, $calendarType = Calendar::GREGORIAN, $locale = null) |
227
|
|
|
{ |
228
|
|
View Code Duplication |
if ($locale instanceof Locale) { |
|
|
|
|
229
|
|
|
$tm = $locale->getContext()->getTranslationManager(); |
230
|
|
|
} elseif ($this->context) { |
231
|
|
|
$tm = $this->context->getTranslationManager(); |
232
|
|
|
if ($locale) { |
|
|
|
|
233
|
|
|
$locale = $tm->getLocale($locale); |
234
|
|
|
} else { |
235
|
|
|
$locale = $tm->getCurrentLocale(); |
236
|
|
|
} |
237
|
|
|
} else { |
238
|
|
|
throw new \InvalidArgumentException('This AgaviDateFormat has not been initialize()d. To be able to pass a string as locale you need to call initialize() or create this DateFormat using TranslationManager::createDateFormat()'); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$tzid = null; |
|
|
|
|
242
|
|
|
if ($locale->getLocaleTimeZone()) { |
243
|
|
|
// use the timezone from the current/provided locale if available |
244
|
|
|
$tzid = $tm->resolveTimeZoneId($locale->getLocaleTimeZone()); |
245
|
|
|
} else { |
246
|
|
|
// otherwise use the default timezone |
247
|
|
|
$tzid = $tm->getDefaultTimeZone()->getResolvedId(); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$cal = null; |
251
|
|
|
// remember if cloning the calendar is required on changes |
252
|
|
|
// (this is only true if the calendar was passed to this method and not |
253
|
|
|
// implicitly created) |
254
|
|
|
$calNeedsClone = false; |
255
|
|
|
if ($data instanceof Calendar) { |
256
|
|
|
$cal = $data; |
257
|
|
|
$calNeedsClone = true; |
258
|
|
|
} elseif ($data instanceof \DateTime) { |
259
|
|
|
$cal = $this->context->getTranslationManager()->createCalendar($data); |
260
|
|
|
} elseif (is_int($data)) { |
261
|
|
|
$cal = $this->context->getTranslationManager()->createCalendar($locale); |
262
|
|
|
$cal->setUnixTimestamp($data); |
263
|
|
|
} elseif (is_array($data)) { |
264
|
|
|
$cal = $tm->createCalendar(); |
265
|
|
|
$cal->fromArray($data); |
266
|
|
|
} else { |
267
|
|
|
// $data is most likely a string a this point (or something which can be |
268
|
|
|
// implicitly converted to a string, so there is no explicit is_string check) |
269
|
|
|
try { |
270
|
|
|
// maybe it is a date/time string we can parse... |
271
|
|
|
$cal = $tm->createCalendar(new \DateTime($data)); |
272
|
|
|
} catch (\Exception $e) { |
273
|
|
|
// err... no, it isn't. try to use the message as a calendar name |
274
|
|
|
$cal = $tm->createCalendar($data); |
275
|
|
|
} |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
if ($tzid != $cal->getTimeZone()->getResolvedId()) { |
279
|
|
|
// the requested timezone for use in the output differs from the timezone |
280
|
|
|
// set in the calendar: make the calendar reside in the new timezone |
281
|
|
|
// so it calculates the correct date values for that timezone |
282
|
|
|
|
283
|
|
|
if ($calNeedsClone) { |
284
|
|
|
$cal = clone $cal; |
285
|
|
|
} |
286
|
|
|
$cal->setTimeZone($tm->createTimeZone($tzid)); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
$data = $cal->toArray(); |
290
|
|
|
|
291
|
|
|
$out = ''; |
292
|
|
|
|
293
|
|
|
foreach ($this->tokenList as $token) { |
294
|
|
|
$count = $token[1]; |
295
|
|
|
switch ($token[0]) { |
296
|
|
|
case self::T_TEXT: |
297
|
|
|
$out .= $token[1]; |
298
|
|
|
break; |
299
|
|
|
|
300
|
|
View Code Duplication |
case self::T_ERA: |
|
|
|
|
301
|
|
|
$era = $data[DateDefinitions::ERA]; |
302
|
|
|
if ($count == 4) { |
303
|
|
|
$out .= $locale->getCalendarEraWide($calendarType, $era); |
304
|
|
|
} elseif ($count == 5) { |
305
|
|
|
$out .= $locale->getCalendarEraNarrow($calendarType, $era); |
306
|
|
|
} else { |
307
|
|
|
$out .= $locale->getCalendarEraAbbreviated($calendarType, $era); |
308
|
|
|
} |
309
|
|
|
break; |
310
|
|
|
|
311
|
|
View Code Duplication |
case self::T_YEAR: |
|
|
|
|
312
|
|
|
$year = $data[DateDefinitions::YEAR]; |
313
|
|
|
if ($count == 2) { |
314
|
|
|
// strip year to 2 chars |
315
|
|
|
$year = $year % 100; |
316
|
|
|
} |
317
|
|
|
$out .= str_pad($year, $count, '0', STR_PAD_LEFT); |
318
|
|
|
break; |
319
|
|
|
|
320
|
|
|
case self::T_MONTH: |
321
|
|
View Code Duplication |
case self::T_SA_MONTH: |
|
|
|
|
322
|
|
|
$month = $data[DateDefinitions::MONTH] + 1; |
323
|
|
|
if ($count == 3) { |
324
|
|
|
$out .= $locale->getCalendarMonthAbbreviated($calendarType, $month); |
325
|
|
|
} elseif ($count == 4) { |
326
|
|
|
$out .= $locale->getCalendarMonthWide($calendarType, $month); |
327
|
|
|
} elseif ($count == 5) { |
328
|
|
|
$out .= $locale->getCalendarMonthNarrow($calendarType, $month); |
329
|
|
|
} else { |
330
|
|
|
$out .= str_pad($month, $count, '0', STR_PAD_LEFT); |
331
|
|
|
} |
332
|
|
|
break; |
333
|
|
|
|
334
|
|
View Code Duplication |
case self::T_DATE: |
|
|
|
|
335
|
|
|
$out .= str_pad($data[DateDefinitions::DATE], $count, '0', STR_PAD_LEFT); |
336
|
|
|
break; |
337
|
|
|
|
338
|
|
View Code Duplication |
case self::T_HOUR_1_24: |
|
|
|
|
339
|
|
|
$hour = $data[DateDefinitions::HOUR_OF_DAY]; |
340
|
|
|
$out .= str_pad($hour == 0 ? 24 : $hour, $count, '0', STR_PAD_LEFT); |
341
|
|
|
break; |
342
|
|
|
|
343
|
|
View Code Duplication |
case self::T_HOUR_0_23: |
|
|
|
|
344
|
|
|
$out .= str_pad($data[DateDefinitions::HOUR_OF_DAY], $count, '0', STR_PAD_LEFT); |
345
|
|
|
break; |
346
|
|
|
|
347
|
|
View Code Duplication |
case self::T_MINUTE: |
|
|
|
|
348
|
|
|
$out .= str_pad($data[DateDefinitions::MINUTE], $count, '0', STR_PAD_LEFT); |
349
|
|
|
break; |
350
|
|
|
|
351
|
|
View Code Duplication |
case self::T_SECOND: |
|
|
|
|
352
|
|
|
$out .= str_pad($data[DateDefinitions::SECOND], $count, '0', STR_PAD_LEFT); |
353
|
|
|
break; |
354
|
|
|
|
355
|
|
View Code Duplication |
case self::T_FRACTIONAL_SECOND: |
|
|
|
|
356
|
|
|
$value = str_pad($data[DateDefinitions::MILLISECOND], 3, '0', STR_PAD_LEFT); |
357
|
|
|
$value = substr($value, 0, $count); |
358
|
|
|
$out .= $value; |
359
|
|
|
break; |
360
|
|
|
|
361
|
|
View Code Duplication |
case self::T_DAY_OF_WEEK: |
|
|
|
|
362
|
|
|
$dow = $data[DateDefinitions::DAY_OF_WEEK]; |
363
|
|
|
if ($count == 4) { |
364
|
|
|
$out .= $locale->getCalendarDayWide($calendarType, $dow); |
365
|
|
|
} elseif ($count == 5) { |
366
|
|
|
$out .= $locale->getCalendarDayNarrow($calendarType, $dow); |
367
|
|
|
} else { |
368
|
|
|
$out .= $locale->getCalendarDayAbbreviated($calendarType, $dow); |
369
|
|
|
} |
370
|
|
|
break; |
371
|
|
|
|
372
|
|
View Code Duplication |
case self::T_DAY_OF_YEAR: |
|
|
|
|
373
|
|
|
$out .= str_pad($data[DateDefinitions::DAY_OF_YEAR], $count, '0', STR_PAD_LEFT); |
374
|
|
|
break; |
375
|
|
|
|
376
|
|
View Code Duplication |
case self::T_DAY_OF_WEEK_IN_MONTH: |
|
|
|
|
377
|
|
|
$out .= str_pad($data[DateDefinitions::DAY_OF_WEEK_IN_MONTH], $count, '0', STR_PAD_LEFT); |
378
|
|
|
break; |
379
|
|
|
|
380
|
|
View Code Duplication |
case self::T_WEEK_OF_YEAR: |
|
|
|
|
381
|
|
|
$out .= str_pad($data[DateDefinitions::WEEK_OF_YEAR], $count, '0', STR_PAD_LEFT); |
382
|
|
|
break; |
383
|
|
|
|
384
|
|
View Code Duplication |
case self::T_WEEK_OF_MONTH: |
|
|
|
|
385
|
|
|
$out .= str_pad($data[DateDefinitions::WEEK_OF_MONTH], $count, '0', STR_PAD_LEFT); |
386
|
|
|
break; |
387
|
|
|
|
388
|
|
|
case self::T_AM_PM: |
389
|
|
|
$isPm = $data[DateDefinitions::AM_PM]; |
390
|
|
|
if ($isPm) { |
391
|
|
|
$out .= $locale->getCalendarPm($calendarType); |
392
|
|
|
} else { |
393
|
|
|
$out .= $locale->getCalendarAm($calendarType); |
394
|
|
|
} |
395
|
|
|
break; |
396
|
|
|
|
397
|
|
View Code Duplication |
case self::T_HOUR_1_12: |
|
|
|
|
398
|
|
|
$hour = $data[DateDefinitions::HOUR]; |
399
|
|
|
$out .= str_pad($hour == 0 ? 12 : $hour, $count, '0', STR_PAD_LEFT); |
400
|
|
|
break; |
401
|
|
|
|
402
|
|
View Code Duplication |
case self::T_HOUR_0_11: |
|
|
|
|
403
|
|
|
$out .= str_pad($data[DateDefinitions::HOUR], $count, '0', STR_PAD_LEFT); |
404
|
|
|
break; |
405
|
|
|
|
406
|
|
|
case self::T_TIMEZONE: |
407
|
|
|
case self::T_TIMEZONE_GENERIC: |
408
|
|
|
if (!$tzid) { |
409
|
|
|
$out .= $this->getGmtZoneString($data); |
410
|
|
|
} else { |
411
|
|
|
$displayString = ''; |
|
|
|
|
412
|
|
|
|
413
|
|
|
if ($token[0] == self::T_TIMEZONE_GENERIC) { |
414
|
|
|
if ($count < 4) { |
415
|
|
|
$displayString = $locale->getTimeZoneShortGenericName($tzid); |
416
|
|
|
} else { |
417
|
|
|
$displayString = $locale->getTimeZoneLongGenericName($tzid); |
418
|
|
|
} |
419
|
|
|
// if we don't have the generic data available |
420
|
|
|
if (!$displayString && strlen($tzid) > 2 && strpos($tzid, '/') !== false && strncmp($tzid, 'Etc', 3) != 0) { |
421
|
|
|
$hasMultiple = false; |
422
|
|
|
$territory = $tm->getTimeZoneTerritory($tzid, $hasMultiple); |
|
|
|
|
423
|
|
|
// TODO: there are lots of rules in the ldml spec which could be covered here too |
424
|
|
|
$displayString = $tzid; |
425
|
|
|
} |
426
|
|
|
} else { |
427
|
|
|
if ($data[DateDefinitions::DST_OFFSET] != 0) { |
428
|
|
|
if ($count < 4) { |
429
|
|
|
$displayString = $locale->getTimeZoneShortDaylightName($tzid); |
430
|
|
|
} else { |
431
|
|
|
$displayString = $locale->getTimeZoneLongDaylightName($tzid); |
432
|
|
|
} |
433
|
|
|
} else { |
434
|
|
|
if ($count < 4) { |
435
|
|
|
$displayString = $locale->getTimeZoneShortStandardName($tzid); |
436
|
|
|
} else { |
437
|
|
|
$displayString = $locale->getTimeZoneLongStandardName($tzid); |
438
|
|
|
} |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
if (!$displayString) { |
443
|
|
|
$out .= $this->getGmtZoneString($data); |
444
|
|
|
} else { |
445
|
|
|
$out .= $displayString; |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
break; |
449
|
|
|
|
450
|
|
View Code Duplication |
case self::T_ISO_YEAR: |
|
|
|
|
451
|
|
|
$out .= str_pad($data[DateDefinitions::YEAR_WOY], $count, '0', STR_PAD_LEFT); |
452
|
|
|
break; |
453
|
|
|
|
454
|
|
|
case self::T_LOCAL_DAY_OF_WEEK: |
455
|
|
View Code Duplication |
case self::T_SA_LOCAL_DAY_OF_WEEK: |
|
|
|
|
456
|
|
|
$dow = $data[DateDefinitions::DOW_LOCAL]; |
457
|
|
|
if ($count == 4) { |
458
|
|
|
$out .= $locale->getCalendarDayWide($calendarType, $dow); |
459
|
|
|
} elseif ($count == 5) { |
460
|
|
|
$out .= $locale->getCalendarDayNarrow($calendarType, $dow); |
461
|
|
|
} elseif ($count == 3) { |
462
|
|
|
$out .= $locale->getCalendarDayAbbreviated($calendarType, $dow); |
463
|
|
|
} else { |
464
|
|
|
$out .= str_pad($dow, $count, '0', STR_PAD_LEFT); |
465
|
|
|
} |
466
|
|
|
break; |
467
|
|
|
|
468
|
|
View Code Duplication |
case self::T_EXTENDED_YEAR: |
|
|
|
|
469
|
|
|
$out .= str_pad($data[DateDefinitions::EXTENDED_YEAR], $count, '0', STR_PAD_LEFT); |
470
|
|
|
break; |
471
|
|
|
|
472
|
|
View Code Duplication |
case self::T_MODIFIED_JD: |
|
|
|
|
473
|
|
|
$out .= str_pad($data[DateDefinitions::JULIAN_DAY], $count, '0', STR_PAD_LEFT); |
474
|
|
|
break; |
475
|
|
|
|
476
|
|
View Code Duplication |
case self::T_MS_IN_DAY: |
|
|
|
|
477
|
|
|
$out .= str_pad($data[DateDefinitions::MILLISECONDS_IN_DAY], $count, '0', STR_PAD_LEFT); |
478
|
|
|
break; |
479
|
|
|
|
480
|
|
|
case self::T_TIMEZONE_RFC: |
481
|
|
|
if ($count > 3) { |
482
|
|
|
$out .= $this->getGmtZoneString($data); |
483
|
|
|
} else { |
484
|
|
|
$sign = '+'; |
485
|
|
|
|
486
|
|
|
$value = ($data[DateDefinitions::ZONE_OFFSET] + $data[DateDefinitions::DST_OFFSET]) / DateDefinitions::MILLIS_PER_MINUTE; |
487
|
|
|
if ($value < 0) { |
488
|
|
|
$value = -$value; |
489
|
|
|
$sign = '-'; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
$value = ($value / 3) * 5 + ($value % 60); // minutes => KKmm |
493
|
|
|
$out .= $sign . str_pad($value, 4, '0', STR_PAD_LEFT); |
494
|
|
|
} |
495
|
|
|
break; |
496
|
|
|
|
497
|
|
|
case self::T_QUARTER: |
498
|
|
|
case self::T_SA_QUARTER: |
499
|
|
|
$quarter = (int)($data[DateDefinitions::MONTH] / 3); |
500
|
|
|
if ($count == 3) { |
501
|
|
|
$out .= $locale->getCalendarQuarterAbbreviated($calendarType, $quarter); |
502
|
|
|
} elseif ($count == 4) { |
503
|
|
|
$out .= $locale->getCalendarQuarterWide($calendarType, $quarter); |
504
|
|
|
} else { |
505
|
|
|
$out .= str_pad($quarter, $count, '0', STR_PAD_LEFT); |
506
|
|
|
} |
507
|
|
|
break; |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
return $out; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* Returns the GMT-+hhmm string for a the offset given in data. |
516
|
|
|
* |
517
|
|
|
* @param array $data Date information. |
518
|
|
|
* |
519
|
|
|
* @return string The timezone string. |
520
|
|
|
* |
521
|
|
|
* @author Dominik del Bondio <[email protected]> |
522
|
|
|
* @since 0.11.0 |
523
|
|
|
*/ |
524
|
|
|
protected function getGmtZoneString(array $data) |
525
|
|
|
{ |
526
|
|
|
$value = $data[DateDefinitions::ZONE_OFFSET] + $data[DateDefinitions::DST_OFFSET]; |
527
|
|
|
|
528
|
|
|
if ($value < 0) { |
529
|
|
|
$str = 'GMT-'; |
530
|
|
|
$value = -$value; // suppress the '-' sign for text display. |
531
|
|
|
} else { |
532
|
|
|
$str = 'GMT+'; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
$str .= str_pad((int) ($value / DateDefinitions::MILLIS_PER_HOUR), 2, '0', STR_PAD_LEFT) |
536
|
|
|
. ':' |
537
|
|
|
. str_pad((int) (($value % DateDefinitions::MILLIS_PER_HOUR) / DateDefinitions::MILLIS_PER_MINUTE), 2, '0', STR_PAD_LEFT); |
538
|
|
|
return $str; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Parses the format with the given character to token map. |
543
|
|
|
* |
544
|
|
|
* @param string $format The format to parse. |
545
|
|
|
* @param array $charToTokenMap The character to token map. |
546
|
|
|
* |
547
|
|
|
* @author Dominik del Bondio <[email protected]> |
548
|
|
|
* @since 0.11.0 |
549
|
|
|
*/ |
550
|
|
|
protected function internalParseFormat($format, array $charToTokenMap) |
551
|
|
|
{ |
552
|
|
|
if ($this->originalFormatString == $format) { |
553
|
|
|
return; |
554
|
|
|
} |
555
|
|
|
$this->originalFormatString = $format; |
556
|
|
|
|
557
|
|
|
$this->tokenList = array(); |
558
|
|
|
$tokenIdx = 0; |
559
|
|
|
|
560
|
|
|
$escapeStr = ''; |
561
|
|
|
|
562
|
|
|
$inEscape = false; |
563
|
|
|
$fLen = strlen($format); |
564
|
|
|
for ($i = 0; $i < $fLen; ++$i) { |
565
|
|
|
$c = $format[$i]; |
566
|
|
|
$cNext = ($i + 1 < $fLen) ? $format[$i+1] : ''; |
567
|
|
|
|
568
|
|
|
if ($inEscape) { |
569
|
|
|
if ($c == '\'') { |
570
|
|
View Code Duplication |
if ('\'' == $cNext) { |
|
|
|
|
571
|
|
|
$escapeStr .= '\''; |
572
|
|
|
++$i; |
573
|
|
|
} else { |
574
|
|
|
$inEscape = false; |
575
|
|
|
$this->tokenList[$tokenIdx] = array(self::T_TEXT, $escapeStr); |
576
|
|
|
++$tokenIdx; |
577
|
|
|
$escapeStr = ''; |
578
|
|
|
} |
579
|
|
|
} else { |
580
|
|
|
$escapeStr .= $c; |
581
|
|
|
} |
582
|
|
|
} else { |
583
|
|
|
if ($c == '\'') { |
584
|
|
View Code Duplication |
if ($cNext == '\'') { |
|
|
|
|
585
|
|
|
$this->tokenList[$tokenIdx] = array(self::T_TEXT, $c); |
586
|
|
|
++$tokenIdx; |
587
|
|
|
++$i; |
588
|
|
|
} else { |
589
|
|
|
$escapeStr = ''; |
590
|
|
|
$inEscape = true; |
591
|
|
|
} |
592
|
|
|
} else { |
593
|
|
|
if (preg_match('#[a-z]#i', $c)) { |
594
|
|
|
if (isset($charToTokenMap[$c])) { |
595
|
|
|
$tok = $charToTokenMap[$c]; |
596
|
|
|
if ($tokenIdx > 0 && $this->tokenList[$tokenIdx - 1][0] == $tok) { |
597
|
|
|
// increase the tokencount if the last token was the same as this |
598
|
|
|
++$this->tokenList[$tokenIdx - 1][1]; |
599
|
|
|
} else { |
600
|
|
|
$this->tokenList[$tokenIdx] = array($tok, 1); |
601
|
|
|
++$tokenIdx; |
602
|
|
|
} |
603
|
|
|
} else { |
604
|
|
|
throw new \Agavi\Exception\AgaviException('Unknown pattern char "' . $c . '" (#'. ord($c) . ') in format string "' . $format . '" at index ' . $i); |
605
|
|
|
} |
606
|
|
|
} else { |
607
|
|
|
$this->tokenList[$tokenIdx] = array(self::T_TEXT, $c); |
608
|
|
|
++$tokenIdx; |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
} |
613
|
|
|
if ($escapeStr) { |
614
|
|
|
$this->tokenList[$tokenIdx] = array(self::T_TEXT, $escapeStr); |
615
|
|
|
++$tokenIdx; |
616
|
|
|
} |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
/** |
620
|
|
|
* Parses a string into an calendar object. Note that this doesn't fallback to |
621
|
|
|
* the English locale. |
622
|
|
|
* |
623
|
|
|
* @param string $dateString The string containing the date. |
624
|
|
|
* @param mixed $locale The locale name or instance which should be used for |
625
|
|
|
* parsing local day, month, etc names. |
626
|
|
|
* @param bool $strict Whether the parsing should be strict. (not allowing |
627
|
|
|
* numbers to exceed the defined length, not allowing missing |
628
|
|
|
* additional parts). The returned calendar object will be |
629
|
|
|
* non-lenient. |
630
|
|
|
* |
631
|
|
|
* @return Calendar The calendar object. |
632
|
|
|
* |
633
|
|
|
* @author Dominik del Bondio <[email protected]> |
634
|
|
|
* @since 0.11.0 |
635
|
|
|
*/ |
636
|
|
|
public function parse($dateString, $locale = null, $strict = false) |
637
|
|
|
{ |
638
|
|
View Code Duplication |
if ($locale instanceof Locale) { |
|
|
|
|
639
|
|
|
$tm = $locale->getContext()->getTranslationManager(); |
640
|
|
|
} elseif ($this->context) { |
641
|
|
|
$tm = $this->context->getTranslationManager(); |
642
|
|
|
if ($locale) { |
643
|
|
|
$locale = $tm->getLocale($locale); |
644
|
|
|
} else { |
645
|
|
|
$locale = $tm->getCurrentLocale(); |
646
|
|
|
} |
647
|
|
|
} else { |
648
|
|
|
throw new \InvalidArgumentException('This DateFormat has not been initialize()d. To be able to pass a string as locale you need to call initialize() or create this DateFormat using TranslationManager::createDateFormat()'); |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
$cal = $tm->createCalendar($locale); |
652
|
|
|
// we need to extract the era from the current date and set that |
653
|
|
|
// era after reinitialising the calendar because if this is not |
654
|
|
|
// done all dates would be BC instead of AD |
655
|
|
|
$era = $cal->get(DateDefinitions::ERA); |
656
|
|
|
$cal->clear(); |
657
|
|
|
$cal->set(DateDefinitions::ERA, $era); |
658
|
|
|
|
659
|
|
|
if ($strict) { |
660
|
|
|
$cal->setLenient(false); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
// TODO: let user chose calendar type when more calendars exist |
664
|
|
|
$calendarType = Calendar::GREGORIAN; |
665
|
|
|
$datePos = 0; |
666
|
|
|
|
667
|
|
|
$unprocessedTokens = array(); |
|
|
|
|
668
|
|
|
|
669
|
|
|
$tlCount = count($this->tokenList); |
670
|
|
|
for ($i = 0; $i < $tlCount; ++$i) { |
671
|
|
View Code Duplication |
if ($datePos >= strlen($dateString)) { |
|
|
|
|
672
|
|
|
if ($strict) { |
673
|
|
|
throw new AgaviException('Input string "' . $dateString . '" is to short'); |
674
|
|
|
} |
675
|
|
|
break; |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
$token = $this->tokenList[$i]; |
679
|
|
|
$type = $this->getTokenType($token); |
680
|
|
|
// this and the next token are numbers |
681
|
|
|
if ($type == 'number' && $i + 1 < $tlCount && $this->getTokenType($this->tokenList[$i + 1]) == 'number') { |
682
|
|
|
$unprocessedTokens = array($token); |
683
|
|
|
|
684
|
|
|
// store all abutting numerical tokens for later processing |
685
|
|
|
do { |
686
|
|
|
++$i; |
687
|
|
|
$unprocessedTokens[] = $this->tokenList[$i]; |
688
|
|
|
} while ($i + 1 < $tlCount && $this->getTokenType($this->tokenList[$i + 1]) == 'number'); |
689
|
|
|
|
690
|
|
|
// retrieve the amount of number characters at our current parse position |
691
|
|
|
$numberLen = strspn($dateString, '0123456789', $datePos); |
692
|
|
|
|
693
|
|
|
// calculate the length the numbers should have from the tokens |
694
|
|
|
$tokenReqLen = 0; |
695
|
|
|
foreach ($unprocessedTokens as $tk) { |
696
|
|
|
$tokenReqLen += $tk[1]; |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
// we mimic the ICU behaviour here which only decrements the count of |
700
|
|
|
// the first token (from ICU: Take the pattern "HHmmss" as an example. |
701
|
|
|
// We will try to parse 2/2/2 characters of the input text, then if that |
702
|
|
|
// fails, 1/2/2. We only adjust the width of the leftmost field; the |
703
|
|
|
// others remain fixed.) |
704
|
|
|
// I'm not sure why they are doing it that way since i think that |
705
|
|
|
// decrementing the next tokens when there still aren't enough numbers |
706
|
|
|
// wouldn't do any harm (ok, maybe the algorithm is a little harder to |
707
|
|
|
// implement ;) - Dominik |
708
|
|
|
|
709
|
|
|
$diff = $tokenReqLen - $numberLen; |
710
|
|
|
if ($diff > 0) { |
711
|
|
|
// use a reference here so we can simply store the new token length into $tLen |
712
|
|
|
$tLen =& $unprocessedTokens[0][1]; |
713
|
|
|
if ($diff >= $tLen - 1) { |
714
|
|
|
$tLen -= $diff; |
715
|
|
|
} else { |
716
|
|
|
throw new AgaviException('Not enough digits given in "' . $dateString . '" at pos ' . $datePos . ' (expected ' . $tokenReqLen . ' digits)'); |
717
|
|
|
} |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
foreach ($unprocessedTokens as $token) { |
721
|
|
|
$dateField = $this->getDateFieldFromTokenType($token[0]); |
722
|
|
|
if ($dateField === null) { |
723
|
|
|
throw new AgaviException('Token type ' . $token[0] . ' claims to be numerical but has no date field'); |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
$number = (int) substr($dateString, $datePos, $token[1]); |
727
|
|
|
|
728
|
|
|
$datePos += $token[1]; |
729
|
|
|
if ($dateField == DateDefinitions::HOUR_OF_DAY && $token[0] == DateFormat::T_HOUR_1_24 && $number == 24) { |
730
|
|
|
$number = 0; |
731
|
|
View Code Duplication |
} elseif ($dateField == DateDefinitions::HOUR && $token[0] == DateFormat::T_HOUR_1_12 && $number == 12) { |
|
|
|
|
732
|
|
|
$number = 0; |
733
|
|
|
} elseif ($dateField == DateDefinitions::MONTH && $number > 0) { |
734
|
|
|
$number -= 1; |
735
|
|
|
} |
736
|
|
|
|
737
|
|
View Code Duplication |
if (self::T_QUARTER == $token[0] || self::T_SA_QUARTER == $token[0]) { |
|
|
|
|
738
|
|
|
// only set the quarter if the date hasn't been set on the calendar object |
739
|
|
|
if (!$cal->_isSet(DateDefinitions::MONTH)) { |
740
|
|
|
$cal->set($dateField, ($number - 1) * 3); |
741
|
|
|
} |
742
|
|
|
} else { |
743
|
|
|
$cal->set($dateField, $number); |
744
|
|
|
} |
745
|
|
|
} |
746
|
|
|
} elseif ($type == 'number') { |
747
|
|
|
$numberLen = strspn($dateString, '0123456789', $datePos); |
748
|
|
|
$dateField = $this->getDateFieldFromTokenType($token[0]); |
749
|
|
|
if ($dateField === null) { |
750
|
|
|
throw new AgaviException('Token type ' . $token[0] . ' claims to be numerical but has no date field'); |
751
|
|
|
} |
752
|
|
|
if ($strict && $numberLen > 2 && $numberLen > $token[1]) { |
753
|
|
|
$numberLen = $token[1]; |
754
|
|
|
} |
755
|
|
|
if ($dateField == DateDefinitions::MILLISECOND && $numberLen > 3) { |
756
|
|
|
// we only store in millisecond precision so only use the first 3 digits of the fractional second |
757
|
|
|
$number = (int) substr($dateString, $datePos, 3); |
758
|
|
|
} else { |
759
|
|
|
$number = (int) substr($dateString, $datePos, $numberLen); |
760
|
|
|
} |
761
|
|
|
|
762
|
|
|
$datePos += $numberLen; |
763
|
|
|
// since the month is 0-indexed in the calendar and 1-indexed in the |
764
|
|
|
// cldr we need to subtract 1 from the month |
765
|
|
|
if ($dateField == DateDefinitions::MONTH && $number > 0) { |
766
|
|
|
$number -= 1; |
767
|
|
View Code Duplication |
} elseif ($dateField == DateDefinitions::HOUR_OF_DAY && $token[0] == DateFormat::T_HOUR_1_24 && $number == 24) { |
|
|
|
|
768
|
|
|
$number = 0; |
769
|
|
|
} elseif ($dateField == DateDefinitions::HOUR && $token[0] == DateFormat::T_HOUR_1_12 && $number == 12) { |
770
|
|
|
$number = 0; |
771
|
|
|
} elseif ($token[0] == self::T_YEAR && $token[1] == 2 && $numberLen <= 2) { |
772
|
|
|
if ($number >= 40) { |
773
|
|
|
$number += 1900; |
774
|
|
|
} else { |
775
|
|
|
$number += 2000; |
776
|
|
|
} |
777
|
|
|
} |
778
|
|
|
|
779
|
|
View Code Duplication |
if (self::T_QUARTER == $token[0] || self::T_SA_QUARTER == $token[0]) { |
|
|
|
|
780
|
|
|
// only set the quarter if the date hasn't been set on the calendar object |
781
|
|
|
if (!$cal->_isSet(DateDefinitions::MONTH)) { |
782
|
|
|
$cal->set($dateField, ($number - 1) * 3); |
783
|
|
|
} |
784
|
|
|
} else { |
785
|
|
|
$cal->set($dateField, $number); |
786
|
|
|
} |
787
|
|
|
} else { // $type == 'text' |
788
|
|
|
$count = $token[1]; |
789
|
|
|
switch ($token[0]) { |
790
|
|
|
case self::T_TEXT: |
791
|
|
|
if (substr_compare($dateString, $token[1], $datePos, strlen($token[1])) == 0) { |
792
|
|
|
$datePos += strlen($token[1]); |
793
|
|
|
} elseif ($i + 1 == $tlCount && !$strict) { |
|
|
|
|
794
|
|
|
// when the last text token didn't match we don't do anything in non strict mode |
795
|
|
|
} else { |
796
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected: "' . $token[1] . '", got: "' . substr($dateString, $datePos, strlen($token[1])) . '")'); |
797
|
|
|
} |
798
|
|
|
break; |
799
|
|
|
|
800
|
|
|
case self::T_ERA: |
801
|
|
|
case self::T_DAY_OF_WEEK: |
802
|
|
|
case self::T_LOCAL_DAY_OF_WEEK: |
803
|
|
|
case self::T_SA_LOCAL_DAY_OF_WEEK: |
804
|
|
|
$funcPrefix = ''; |
805
|
|
|
|
806
|
|
|
switch ($token[0]) { |
807
|
|
|
case self::T_ERA: |
808
|
|
|
$funcPrefix = 'getCalendarEras'; |
809
|
|
|
$dataField = DateDefinitions::ERA; |
810
|
|
|
break; |
811
|
|
|
case self::T_DAY_OF_WEEK: |
812
|
|
|
$funcPrefix = 'getCalendarDays'; |
813
|
|
|
$dataField = DateDefinitions::DAY_OF_WEEK; |
814
|
|
|
break; |
815
|
|
|
|
816
|
|
|
case self::T_LOCAL_DAY_OF_WEEK: |
817
|
|
|
case self::T_SA_LOCAL_DAY_OF_WEEK: |
818
|
|
|
$funcPrefix = 'getCalendarDays'; |
819
|
|
|
$dataField = DateDefinitions::DOW_LOCAL; |
820
|
|
|
break; |
821
|
|
|
} |
822
|
|
|
|
823
|
|
|
if ($count == 4) { |
824
|
|
|
$items = $locale->{$funcPrefix . 'Wide'}($calendarType); |
825
|
|
|
} elseif ($count == 5) { |
826
|
|
|
$items = $locale->{$funcPrefix . 'Narrow'}($calendarType); |
827
|
|
|
} else { |
828
|
|
|
$items = $locale->{$funcPrefix . 'Abbreviated'}($calendarType); |
829
|
|
|
} |
830
|
|
|
$item = null; |
831
|
|
|
if ($this->matchStringWithFallbacks($dateString, $items, $datePos, $item)) { |
832
|
|
|
$cal->set($dataField, $item); |
|
|
|
|
833
|
|
|
} else { |
834
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected one of: ' . implode(', ', $items) . ')'); |
835
|
|
|
} |
836
|
|
|
break; |
837
|
|
|
|
838
|
|
|
case self::T_AM_PM: |
839
|
|
|
$items = array( |
840
|
|
|
0 => $locale->getCalendarAm($calendarType), |
841
|
|
|
1 => $locale->getCalendarPm($calendarType), |
842
|
|
|
); |
843
|
|
|
|
844
|
|
|
$item = null; |
845
|
|
|
if ($this->matchStringWithFallbacks($dateString, $items, $datePos, $item)) { |
846
|
|
|
$cal->set(DateDefinitions::AM_PM, $item); |
847
|
|
|
} else { |
848
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected one of: ' . implode(', ', $items) . ')'); |
849
|
|
|
} |
850
|
|
|
break; |
851
|
|
|
|
852
|
|
|
case self::T_TIMEZONE: |
853
|
|
|
case self::T_TIMEZONE_GENERIC: |
854
|
|
|
case self::T_TIMEZONE_RFC: |
855
|
|
|
$remainder = substr($dateString, $datePos); |
856
|
|
|
if (preg_match('#^(GMT)?(\+|-)?(\d{1,2}:\d{1,2}|\d{1,2}\d{1,2})#i', $remainder, $match)) { |
857
|
|
|
$datePos += strlen($match[0]); |
858
|
|
|
if (strtolower($match[1]) != 'gmt') { |
859
|
|
|
$remainder = 'GMT' . $match[0]; |
860
|
|
|
} else { |
861
|
|
|
$remainder = $match[0]; |
862
|
|
|
} |
863
|
|
|
$tz = $tm->createTimeZone($remainder); |
864
|
|
|
} else { |
865
|
|
|
if ($i + 1 != $tlCount && preg_match('#^([a-z/]+)#i', $remainder, $match)) { |
866
|
|
|
$remainder = $match[0]; |
867
|
|
|
} |
868
|
|
|
if (!($tz = $tm->createTimeZone($remainder))) { |
869
|
|
|
// try to match a localized timezone string |
870
|
|
|
$z = 0; |
871
|
|
|
$localizedTzMap = array(); |
872
|
|
|
$idToTzMap = array(); |
873
|
|
|
$tzNames = $locale->getTimeZoneNames(); |
874
|
|
|
foreach ($tzNames as $tzId => $tz) { |
875
|
|
|
foreach ($tz as $type => $names) { |
876
|
|
|
if (is_array($names)) { |
877
|
|
|
foreach ($names as $name) { |
878
|
|
|
$localizedTzMap[$z] = $name; |
879
|
|
|
$idToTzMap[$z] = $tzId; |
880
|
|
|
++$z; |
881
|
|
|
} |
882
|
|
|
} |
883
|
|
|
} |
884
|
|
|
} |
885
|
|
|
|
886
|
|
|
$id = 0; |
887
|
|
|
if ($this->matchStringWithFallbacks($dateString, $localizedTzMap, $datePos, $id)) { |
888
|
|
|
$tz = $tm->createTimeZone($idToTzMap[$id]); |
889
|
|
|
} else { |
890
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected one of: ' . implode(', ', $localizedTzMap) . ')'); |
891
|
|
|
} |
892
|
|
|
} else { |
893
|
|
|
$datePos += strlen($remainder); |
894
|
|
|
} |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
$cal->setTimeZone($tz); |
898
|
|
|
break; |
899
|
|
|
|
900
|
|
|
case self::T_MONTH: |
901
|
|
View Code Duplication |
case self::T_SA_MONTH: |
|
|
|
|
902
|
|
|
if ($count == 3) { |
903
|
|
|
$months = $locale->getCalendarMonthsAbbreviated($calendarType); |
904
|
|
|
} elseif ($count == 4) { |
905
|
|
|
$months = $locale->getCalendarMonthsWide($calendarType); |
906
|
|
|
} elseif ($count == 5) { |
907
|
|
|
$months = $locale->getCalendarMonthsNarrow($calendarType); |
908
|
|
|
} |
909
|
|
|
$month = null; |
910
|
|
|
if ($this->matchStringWithFallbacks($dateString, $months, $datePos, $month)) { |
|
|
|
|
911
|
|
|
$cal->set(DateDefinitions::MONTH, $month - 1); |
912
|
|
|
} else { |
913
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected one of: ' . implode(', ', $months) . ')'); |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
break; |
917
|
|
|
|
918
|
|
|
case self::T_QUARTER: |
919
|
|
View Code Duplication |
case self::T_SA_QUARTER: |
|
|
|
|
920
|
|
|
if ($count == 3) { |
921
|
|
|
$quarters = $locale->getCalendarQuartersAbbreviated($calendarType); |
922
|
|
|
} elseif ($count == 4) { |
923
|
|
|
$quarters = $locale->getCalendarQuartersWide($calendarType); |
924
|
|
|
} elseif ($count == 5) { |
925
|
|
|
$quarters = $locale->getCalendarQuartersNarrow($calendarType); |
926
|
|
|
} |
927
|
|
|
$quarter = null; |
928
|
|
|
if ($this->matchStringWithFallbacks($dateString, $quarters, $datePos, $quarter)) { |
|
|
|
|
929
|
|
|
if (!$cal->_isSet(DateDefinitions::MONTH)) { |
930
|
|
|
$cal->set(DateDefinitions::MONTH, ($quarter - 1) * 3); |
931
|
|
|
} |
932
|
|
|
} else { |
933
|
|
|
throw new AgaviException('Unknown character in "' . $dateString . '" at pos ' . $datePos . ' (expected one of: ' . implode(', ', $quarters) . ')'); |
934
|
|
|
} |
935
|
|
|
break; |
936
|
|
|
} |
937
|
|
|
} |
938
|
|
|
} |
939
|
|
|
|
940
|
|
|
// make sure the calendar has it's time calculated, |
941
|
|
|
// so there aren't any strange effects when setting a new timezone |
942
|
|
|
// or a new date |
943
|
|
|
$cal->getTime(); |
944
|
|
View Code Duplication |
if ($strict) { |
|
|
|
|
945
|
|
|
// calculate the time to get errors for invalid dates |
946
|
|
|
if ($datePos < strlen($dateString)) { |
947
|
|
|
throw new AgaviException('Input string "' . $dateString . '" has characters after the date'); |
948
|
|
|
} |
949
|
|
|
} |
950
|
|
|
|
951
|
|
|
return $cal; |
952
|
|
|
} |
953
|
|
|
|
954
|
|
|
/** |
955
|
|
|
* Returns the date field which is associated with the given token. |
956
|
|
|
* |
957
|
|
|
* @param int $type The type of the token. |
958
|
|
|
* |
959
|
|
|
* @return int The date field in the calendar for this token type. |
960
|
|
|
* |
961
|
|
|
* @author Dominik del Bondio <[email protected]> |
962
|
|
|
* @since 0.11.0 |
963
|
|
|
*/ |
964
|
|
|
protected function getDateFieldFromTokenType($type) |
965
|
|
|
{ |
966
|
|
|
static $typeMap = array( |
967
|
|
|
self::T_ERA => DateDefinitions::ERA, |
968
|
|
|
self::T_YEAR => DateDefinitions::YEAR, |
969
|
|
|
self::T_MONTH => DateDefinitions::MONTH, |
970
|
|
|
self::T_SA_MONTH => DateDefinitions::MONTH, |
971
|
|
|
self::T_DATE => DateDefinitions::DATE, |
972
|
|
|
self::T_HOUR_1_24 => DateDefinitions::HOUR_OF_DAY, |
973
|
|
|
self::T_HOUR_0_23 => DateDefinitions::HOUR_OF_DAY, |
974
|
|
|
self::T_MINUTE => DateDefinitions::MINUTE, |
975
|
|
|
self::T_SECOND => DateDefinitions::SECOND, |
976
|
|
|
self::T_FRACTIONAL_SECOND => DateDefinitions::MILLISECOND, |
977
|
|
|
self::T_DAY_OF_WEEK => DateDefinitions::DAY_OF_WEEK, |
978
|
|
|
self::T_DAY_OF_YEAR => DateDefinitions::DAY_OF_YEAR, |
979
|
|
|
self::T_DAY_OF_WEEK_IN_MONTH => DateDefinitions::DAY_OF_WEEK_IN_MONTH, |
980
|
|
|
self::T_WEEK_OF_YEAR => DateDefinitions::WEEK_OF_YEAR, |
981
|
|
|
self::T_WEEK_OF_MONTH => DateDefinitions::WEEK_OF_MONTH, |
982
|
|
|
self::T_AM_PM => DateDefinitions::AM_PM, |
983
|
|
|
self::T_HOUR_1_12 => DateDefinitions::HOUR, |
984
|
|
|
self::T_HOUR_0_11 => DateDefinitions::HOUR, |
985
|
|
|
self::T_ISO_YEAR => DateDefinitions::YEAR_WOY, |
986
|
|
|
self::T_LOCAL_DAY_OF_WEEK => DateDefinitions::DOW_LOCAL, |
987
|
|
|
self::T_SA_LOCAL_DAY_OF_WEEK => DateDefinitions::DOW_LOCAL, |
988
|
|
|
self::T_EXTENDED_YEAR => DateDefinitions::EXTENDED_YEAR, |
989
|
|
|
self::T_MODIFIED_JD => DateDefinitions::JULIAN_DAY, |
990
|
|
|
self::T_MS_IN_DAY => DateDefinitions::MILLISECONDS_IN_DAY, |
991
|
|
|
self::T_QUARTER => DateDefinitions::MONTH, |
992
|
|
|
self::T_SA_QUARTER => DateDefinitions::MONTH, |
993
|
|
|
); |
994
|
|
|
|
995
|
|
|
if (isset($typeMap[$type])) { |
996
|
|
|
return $typeMap[$type]; |
997
|
|
|
} else { |
998
|
|
|
return null; |
999
|
|
|
} |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
/** |
1003
|
|
|
* Returns whether a string matches any of the given possibilities at the |
1004
|
|
|
* current offset. |
1005
|
|
|
* |
1006
|
|
|
* @param string $string The string to be matched in. |
1007
|
|
|
* @param array $possibilities The possibilities which can match. |
1008
|
|
|
* @param int $offset The offset to match at in the input string. |
1009
|
|
|
* @param mixed $matchedKey The key of the possibilities entry that matched. |
1010
|
|
|
* |
1011
|
|
|
* @return bool Whether any possibility could be matched. |
1012
|
|
|
* |
1013
|
|
|
* @author Dominik del Bondio <[email protected]> |
1014
|
|
|
* @since 0.11.0 |
1015
|
|
|
*/ |
1016
|
|
|
protected function matchStringWithFallbacks($string, array $possibilities, &$offset, &$matchedKey) |
1017
|
|
|
{ |
1018
|
|
|
$strlen = strlen($string); |
1019
|
|
|
// TODO: change this to match to longest match and not the first one. |
1020
|
|
|
foreach ($possibilities as $key => $possibility) { |
1021
|
|
|
$possLen = strlen($possibility); |
1022
|
|
|
// avoid warning when $string is not long enough for the possibility |
1023
|
|
|
if ($strlen >= ($offset + $possLen) && substr_compare($string, $possibility, $offset, $possLen) == 0) { |
1024
|
|
|
$offset += $possLen; |
1025
|
|
|
$matchedKey = $key; |
1026
|
|
|
return true; |
1027
|
|
|
} |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
return false; |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
/** |
1034
|
|
|
* Returns the type of a token |
1035
|
|
|
* |
1036
|
|
|
* @param array $token The token. |
1037
|
|
|
* |
1038
|
|
|
* @return string either 'text' or 'number' |
1039
|
|
|
* |
1040
|
|
|
* @author Dominik del Bondio <[email protected]> |
1041
|
|
|
* @since 0.11.0 |
1042
|
|
|
*/ |
1043
|
|
|
protected function getTokenType($token) |
1044
|
|
|
{ |
1045
|
|
|
$type = 'text'; |
1046
|
|
|
$tok = $token[0]; |
1047
|
|
|
$cnt = $token[1]; |
1048
|
|
|
|
1049
|
|
|
switch ($tok) { |
1050
|
|
|
case self::T_YEAR: |
1051
|
|
|
case self::T_DATE: |
1052
|
|
|
case self::T_HOUR_1_24: |
1053
|
|
|
case self::T_HOUR_0_23: |
1054
|
|
|
case self::T_HOUR_1_12: |
1055
|
|
|
case self::T_HOUR_0_11: |
1056
|
|
|
case self::T_MINUTE: |
1057
|
|
|
case self::T_SECOND: |
1058
|
|
|
case self::T_DAY_OF_YEAR: |
1059
|
|
|
case self::T_DAY_OF_WEEK_IN_MONTH: |
1060
|
|
|
case self::T_WEEK_OF_YEAR: |
1061
|
|
|
case self::T_WEEK_OF_MONTH: |
1062
|
|
|
case self::T_ISO_YEAR: |
1063
|
|
|
case self::T_EXTENDED_YEAR: |
1064
|
|
|
case self::T_MODIFIED_JD: |
1065
|
|
|
case self::T_MS_IN_DAY: |
1066
|
|
|
case self::T_FRACTIONAL_SECOND: |
1067
|
|
|
// number |
1068
|
|
|
$type = 'number'; |
1069
|
|
|
break; |
1070
|
|
|
|
1071
|
|
|
case self::T_MONTH: |
1072
|
|
|
case self::T_SA_MONTH: |
1073
|
|
|
case self::T_LOCAL_DAY_OF_WEEK: |
1074
|
|
|
case self::T_SA_LOCAL_DAY_OF_WEEK: |
1075
|
|
|
case self::T_QUARTER: |
1076
|
|
|
case self::T_SA_QUARTER: |
1077
|
|
|
// string > 2 |
1078
|
|
|
if ($cnt > 2) { |
1079
|
|
|
$type = 'text'; |
1080
|
|
|
} else { |
1081
|
|
|
$type = 'number'; |
1082
|
|
|
} |
1083
|
|
|
break; |
1084
|
|
|
|
1085
|
|
|
case self::T_TEXT: |
1086
|
|
|
case self::T_ERA: |
1087
|
|
|
case self::T_DAY_OF_WEEK: |
1088
|
|
|
case self::T_AM_PM: |
1089
|
|
|
case self::T_TIMEZONE: |
1090
|
|
|
case self::T_TIMEZONE_GENERIC: |
1091
|
|
|
case self::T_TIMEZONE_RFC: |
1092
|
|
|
$type = 'text'; |
1093
|
|
|
// string |
1094
|
|
|
break; |
1095
|
|
|
} |
1096
|
|
|
|
1097
|
|
|
return $type; |
1098
|
|
|
} |
1099
|
|
|
} |
1100
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: