1: <?php
2: /*
3: You may not change or alter any portion of this comment or credits
4: of supporting developers from this source code or any supporting source code
5: which is considered copyrighted (c) material of the original comment or credit authors.
6:
7: This program is distributed in the hope that it will be useful,
8: but WITHOUT ANY WARRANTY; without even the implied warranty of
9: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10: */
11:
12: namespace Xoops\Core\Locale;
13:
14: use Xoops\Core\Locale\Punic\Calendar;
15: use Punic\Data;
16: use Punic\Plural;
17: use Xoops\Locale;
18:
19: /**
20: * Xoops\Core\Locale\Time - localized time handling
21: *
22: * @category Xoops\Core\Locale\Time
23: * @package Xoops
24: * @author Richard Griffith <richard@geekwright.com>
25: * @copyright 2015 XOOPS Project (http://xoops.org)/
26: * @license GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
27: * @link http://xoops.org
28: */
29: class Time
30: {
31: /**
32: * cleanTime
33: *
34: * @param number|\DateTime|string $time An Unix timestamp, DateTime instance or string accepted by strtotime.
35: *
36: * @return \DateTime
37: */
38: public static function cleanTime($time = null)
39: {
40: if (is_a($time, '\DateTime')) {
41: return $time->setTimezone(Locale::getTimeZone());
42: }
43: if ($time === null || $time === 0 || $time === '') {
44: return new \DateTime('now', Locale::getTimeZone());
45: }
46: return Calendar::toDateTime($time, Locale::getTimeZone());
47: }
48:
49: /**
50: * Describe an relative interval from $dateStart to $dateEnd (eg '2 days ago').
51: * Only the largest differing unit is described, and the next smaller unit will be used
52: * for rounding.
53: *
54: * @param \DateTime $dateEnd The terminal date
55: * @param \DateTime|null $dateStart The anchor date, defaults to now. (if it has a timezone different than
56: * $dateEnd, we'll use the one of $dateEnd)
57: * @param string $width The format name; it can be '', 'short' or 'narrow'
58: * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data
59: *
60: * @return string
61: *
62: * @throws \InvalidArgumentException
63: */
64: public static function describeRelativeInterval($dateEnd, $dateStart = null, $width = '', $locale = '')
65: {
66: if (!is_a($dateEnd, '\DateTime')) {
67: throw new \InvalidArgumentException('Not a DateTime object');
68: }
69: if (empty($dateStart) && ($dateStart !== 0) && ($dateStart !== '0')) {
70: $dateStart = new \DateTime('now');
71: } elseif (!is_a($dateStart, '\DateTime')) {
72: throw new \InvalidArgumentException('Not a DateTime object');
73: } else {
74: $dateStart = clone $dateStart;
75: }
76: $dateStart->setTimezone($dateEnd->getTimezone());
77:
78: //$utc = new \DateTimeZone('UTC');
79: //$dateEndUTC = new \DateTime($dateEnd->format('Y-m-d H:i:s'), $utc);
80: //$dateStartUTC = new \DateTime($dateStart->format('Y-m-d H:i:s'), $utc);
81: $parts = array();
82: $data = Data::get('dateFields', $locale);
83:
84: $diff = $dateStart->diff($dateEnd, false);
85: $past = (boolean) $diff->invert;
86: $value = 0;
87: $key = '';
88: if ($diff->y != 0) {
89: $key = 'year';
90: $value = $diff->y + (($diff->m > 6) ? 1 : 0);
91: } elseif ($diff->m != 0) {
92: $key = 'month';
93: $value = $diff->m + (($diff->d > 15) ? 1 : 0);
94: } elseif ($diff->d != 0) {
95: $key = 'day';
96: $value = $diff->d + (($diff->h > 12) ? 1 : 0);
97: } elseif ($diff->h != 0) {
98: $key = 'hour';
99: $value = $diff->h + (($diff->i > 30) ? 1 : 0);
100: } elseif ($diff->i != 0) {
101: $key = 'minute';
102: $value = $diff->i + (($diff->s > 30) ? 1 : 0);
103: } elseif ($diff->s != 0) {
104: $key = 'second';
105: $value = $diff->s;
106: }
107: if ($value==0) {
108: $key = 'second';
109: $relKey = 'relative-type-0';
110: $relPattern = null;
111: } elseif ($key === 'day' && $value >1 && $value <7) {
112: $dow = $dateEnd->format('N') - 1;
113: $days = array('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun');
114: $key = $days[$dow];
115: $relKey = ($past) ? "relative-type--1" : "relative-type-1";
116: $relPattern = null;
117: } else {
118: if ($value == 1 && isset($data[$key]['relative-type--1'])) {
119: $relKey = ($past) ? 'relative-type--1' : 'relative-type-1';
120: $relPattern = null;
121: } else {
122: $relKey = ($past) ? 'relativeTime-type-past' : 'relativeTime-type-future';
123: $rule = Plural::getRule($value, $locale);
124: $relPattern = 'relativeTimePattern-count-' . $rule;
125: }
126: }
127: if (!empty($width) && array_key_exists($key . '-' . $width, $data)) {
128: $key .= '-' . $width;
129: }
130: if (empty($relPattern)) {
131: $relativeString = $data[$key][$relKey];
132: } else {
133: $tempString = $data[$key][$relKey][$relPattern];
134: $tempString = str_replace('{0}', '%d', $tempString);
135: $relativeString = sprintf($tempString, $value);
136: }
137: return $relativeString;
138: }
139:
140: /**
141: * Format a date.
142: *
143: * @param number|\DateTime|string $value An Unix timestamp, a `\DateTime` instance or a string accepted
144: * by strtotime().
145: * @param string $width The format name; it can be
146: * 'full' (eg 'EEEE, MMMM d, y' - 'Wednesday, August 20, 2014'),
147: * 'long' (eg 'MMMM d, y' - 'August 20, 2014'),
148: * 'medium' (eg 'MMM d, y' - 'August 20, 2014') or
149: * 'short' (eg 'M/d/yy' - '8/20/14').
150: * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone
151: * (or the timezone associated to $value if it's already a \DateTime)
152: * @param string $locale The locale to use. If empty we'll use the default
153: *
154: * @return string Returns an empty string if $value is empty, the localized textual representation otherwise
155: */
156: public static function formatDate($value, $width = 'short', $toTimezone = '', $locale = '')
157: {
158: try {
159: $formatted = Calendar::formatDateEx($value, $width, $toTimezone, $locale);
160: } catch (\Punic\Exception $e) {
161: \Xoops::getInstance()->events()->triggerEvent('core.exception', $e);
162: $formatted = '';
163: }
164: return $formatted;
165: }
166:
167: /**
168: * Format a date.
169: *
170: * @param number|\DateTime|string $value An Unix timestamp, a `\DateTime` instance or a string accepted
171: * by strtotime().
172: * @param string $width The format name; it can be
173: * 'full' (eg 'h:mm:ss a zzzz' - '11:42:13 AM GMT+2:00'),
174: * 'long' (eg 'h:mm:ss a z' - '11:42:13 AM GMT+2:00'),
175: * 'medium' (eg 'h:mm:ss a' - '11:42:13 AM') or
176: * 'short' (eg 'h:mm a' - '11:42 AM')
177: * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone
178: * (or the timezone associated to $value if it's already a \DateTime)
179: * @param string $locale The locale to use. If empty we'll use the default
180: *
181: * @return string Returns an empty string if $value is empty, the localized textual representation otherwise
182: *
183: * @throws \Punic\Exception Throws an exception in case of problems
184: */
185: public static function formatTime($value, $width = 'short', $toTimezone = '', $locale = '')
186: {
187: try {
188: $formatted = Calendar::formatTimeEx($value, $width, $toTimezone, $locale);
189: } catch (\Punic\Exception $e) {
190: \Xoops::getInstance()->events()->triggerEvent('core.exception', $e);
191: $formatted = '';
192: }
193: return $formatted;
194: }
195:
196: /**
197: * Format a date/time.
198: *
199: * @param \DateTime $value The \DateTime instance for which you want the localized textual representation
200: * @param string $width The format name; it can be 'full', 'long', 'medium', 'short' or a combination
201: * for date+time like 'full|short' or a combination for format+date+time like
202: * 'full|full|short'
203: * You can also append an asterisk ('*') to the date part of $width. If so,
204: * special day names may be used (like 'Today', 'Yesterday', 'Tomorrow') instead
205: * of the date part.
206: * @param string $locale The locale to use. If empty we'll use the default locale
207: *
208: * @return string Returns an empty string if $value is empty, the localized textual representation otherwise
209: *
210: * @throws \Punic\Exception Throws an exception in case of problems
211: */
212: public static function formatDateTime(\DateTime $value, $width, $locale = '')
213: {
214: return Calendar::formatDatetime($value, $width, $locale);
215: }
216:
217: /**
218: * Perform any localization required for date picker used in Form\DateSelect
219: *
220: * @return void
221: */
222: public static function localizeDatePicker()
223: {
224: $delimiter = '-';
225: $locale = Locale::normalizeLocale(Locale::getCurrent(), $delimiter, false);
226: if ('zh_Hant' === Locale::getCurrent()) {
227: $locale = 'zh-TW';
228: }
229: if ($locale === 'zh') {
230: $locale = 'zh-CN';
231: }
232: list($language) = explode($delimiter, $locale);
233: $xoops = \Xoops::getInstance();
234:
235: $locales = array($locale, $language);
236: foreach ($locales as $name) {
237: $i18nScript = 'media/jquery/ui/i18n/datepicker-' . $name . '.js';
238: if (file_exists($xoops->path($i18nScript))) {
239: $xoops->theme()->addBaseScriptAssets($i18nScript);
240: return;
241: }
242: }
243: }
244:
245: /**
246: * turn a utf8 string into an array of characters
247: *
248: * @param string $input string to convert
249: *
250: * @return array
251: */
252: protected static function utf8StringToChars($input)
253: {
254: $chars = array();
255: $strLen = mb_strlen($input, 'UTF-8');
256: for ($i = 0; $i < $strLen; $i++) {
257: $chars[] = mb_substr($input, $i, 1, 'UTF-8');
258: }
259: return $chars;
260: }
261:
262: /**
263: * parse a date input according to a locale and apply it to a DateTime object
264: *
265: * @param \DateTime $datetime datetime to apply date to
266: * @param string $input localized date string
267: * @param string $locale optional locale to use, leave blank to use current
268: *
269: * @return void
270: *
271: * @throws \Punic\Exception\ValueNotInList
272: */
273: protected static function parseInputDate(\DateTime $datetime, $input, $locale = '')
274: {
275: $year = 0;
276: $month = 0;
277: $day = 0;
278:
279: $order = [];
280: $dateFormat = Calendar::getDateFormat('short', $locale);
281: $formatChars = static::utf8StringToChars($dateFormat);
282: $state = 'non';
283: $newstate = $state;
284: foreach ($formatChars as $char) {
285: switch ($char) {
286: case 'y':
287: $newstate = 'y';
288: break;
289: case 'M':
290: $newstate = 'm';
291: break;
292: case 'd':
293: $newstate = 'd';
294: break;
295: default:
296: $newstate = 'non';
297: break;
298: }
299: if ($newstate !== $state) {
300: if (in_array($newstate, ['y', 'm', 'd'])) {
301: $order[] = $newstate;
302: }
303: $state = $newstate;
304: }
305: }
306:
307: $pieces = [];
308: $pieceIndex = -1;
309: $inputChars = static::utf8StringToChars($input);
310: $state = 'non';
311: $newstate = $state;
312: foreach ($inputChars as $char) {
313: switch ($char) {
314: case '0':
315: case '1':
316: case '2':
317: case '3':
318: case '4':
319: case '5':
320: case '6':
321: case '7':
322: case '8':
323: case '9':
324: $newstate = 'digit';
325: break;
326: default:
327: $newstate = 'non';
328: break;
329: }
330: if ($newstate !== $state) {
331: if ($newstate === 'digit') {
332: $pieces[++$pieceIndex] = $char;
333: }
334: $state = $newstate;
335: } elseif ($state === 'digit') {
336: $pieces[$pieceIndex] .= $char;
337: }
338: }
339:
340: foreach ($pieces as $i => $piece) {
341: $piece = (int) ltrim($piece, '0');
342: switch ($order[$i]) {
343: case 'd':
344: $day = $piece;
345: break;
346: case 'm':
347: $month = $piece;
348: break;
349: case 'y':
350: $year = $piece;
351: break;
352: }
353: }
354: if ($year < 100) {
355: if ($year<70) {
356: $year += 2000;
357: } else {
358: $year += 1900;
359: }
360: }
361: $datetime->setDate($year, $month, $day);
362: // public DateTime DateTime::setTime ( int $hour , int $minute [, int $second = 0 ] )
363: }
364:
365: /**
366: * parse a time input according to a locale and apply it to a DateTime object
367: *
368: * @param \DateTime $datetime datetime to apply time to
369: * @param string $input localized time string
370: * @param string $locale optional locale to use, leave blank to use current
371: *
372: * @return void
373: *
374: * @throws \Punic\Exception\BadArgumentType
375: * @throws \Punic\Exception\ValueNotInList
376: */
377: protected static function parseInputTime(\DateTime $datetime, $input, $locale = '')
378: {
379: $timeFormat = Calendar::getTimeFormat('short', $locale);
380: $am = Calendar::getDayperiodName('am', 'wide', $locale);
381: $pm = Calendar::getDayperiodName('pm', 'wide', $locale);
382: $clock12 = Calendar::has12HoursClock($locale);
383:
384: $hour = 0;
385: $minute = 0;
386: $second = 0;
387:
388: $order = [];
389: $formatChars = static::utf8StringToChars($timeFormat);
390: $state = 'non';
391: $newstate = $state;
392: foreach ($formatChars as $char) {
393: switch ($char) {
394: case 'h':
395: case 'H':
396: $newstate = 'h';
397: break;
398: case 'm':
399: $newstate = 'm';
400: break;
401: case 'a':
402: default:
403: $newstate = 'non';
404: break;
405: }
406: if ($newstate !== $state) {
407: if (in_array($newstate, ['h', 'm'])) {
408: $order[] = $newstate;
409: }
410: $state = $newstate;
411: }
412: }
413:
414: $pieces = [];
415: $pieceIndex = -1;
416: $inputChars = static::utf8StringToChars($input);
417: $state = 'non';
418: $newstate = $state;
419: foreach ($inputChars as $char) {
420: switch ($char) {
421: case '0':
422: case '1':
423: case '2':
424: case '3':
425: case '4':
426: case '5':
427: case '6':
428: case '7':
429: case '8':
430: case '9':
431: $newstate = 'digit';
432: break;
433: default:
434: $newstate = 'non';
435: break;
436: }
437: if ($newstate !== $state) {
438: if ($newstate === 'digit') {
439: $pieces[++$pieceIndex] = $char;
440: }
441: $state = $newstate;
442: } elseif ($state === 'digit') {
443: $pieces[$pieceIndex] .= $char;
444: }
445: }
446:
447: foreach ($pieces as $i => $piece) {
448: $piece = (int) ltrim($piece, '0');
449: switch ($order[$i]) {
450: case 'h':
451: $hour = $piece;
452: break;
453: case 'm':
454: $minute = $piece;
455: break;
456: }
457: }
458: if ($clock12) {
459: if ($hour == 12 && false !== mb_strpos($input, $am)) {
460: $hour = 0;
461: }
462: if (false !== mb_strpos($input, $pm)) {
463: $hour += 12;
464: }
465: }
466: $datetime->setTime($hour, $minute, $second);
467: }
468:
469: /**
470: * Convert a XOOPS DateSelect or DateTime form input into a DateTime object
471: *
472: * @param string|string[] $input date string, or array of date and time strings
473: * @param string $locale optional locale to use, leave blank to use current
474: *
475: * @return \DateTime
476: */
477: public static function inputToDateTime($input, $locale = '')
478: {
479: $dateTime = static::cleanTime();
480: $dateTime->setTime(0, 0, 0);
481:
482: if (is_array($input)) {
483: static::parseInputDate($dateTime, $input['date'], $locale);
484: static::parseInputTime($dateTime, $input['time'], $locale);
485: } else { // single string should be just a date
486: static::parseInputDate($dateTime, $input, $locale);
487: }
488: return $dateTime;
489: }
490: }
491: