/[drupal]/contributions/modules/date/date_api_sql.inc
ViewVC logotype

Contents of /contributions/modules/date/date_api_sql.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.16 - (show annotations) (download) (as text)
Tue Sep 8 08:52:19 2009 UTC (2 months, 2 weeks ago) by karens
Branch: MAIN
CVS Tags: HEAD
Changes since 1.15: +6 -6 lines
File MIME type: text/x-php
Getting HEAD ready for D7.
1 <?php
2 // $Id: date_api_sql.inc,v 1.15 2009/09/05 09:26:57 karens Exp $
3
4 /**
5 * A helper function to do cross-database concatation of date parts
6 *
7 * @param $array - an array of values to be concatonated in sql
8 * @return - correct sql string for database type
9 */
10 function date_sql_concat($array) {
11 global $db_type;
12 switch ($db_type) {
13 case('mysql'):
14 case('mysqli'):
15 return "CONCAT(" . implode(",", $array) . ")";
16 case('pgsql'):
17 return implode(" || ", $array);
18 }
19 }
20
21 /**
22 * Helper function to do cross-database NULL replacements
23 *
24 * @param an array of values to test for NULL values
25 * @return SQL statement to return the first non-NULL value in the list.
26 */
27 function date_sql_coalesce($array) {
28 global $db_type;
29 switch ($db_type) {
30 case('mysql'):
31 case('mysqli'):
32 case('pgsql'):
33 return "COALESCE(" . implode(',', $array) . ")";
34 }
35 }
36
37 /**
38 * A helper function to do cross-database padding of date parts
39 *
40 * @param $str - a string to apply padding to
41 * @param $size - the size the final string should be
42 * @param $pad - the value to pad the string with
43 * @param $side - the side of the string to pad
44 */
45 function date_sql_pad($str, $size = 2, $pad = '0', $side = 'l') {
46 switch ($side) {
47 case('r'):
48 return "RPAD($str, $size, '$pad')";
49 default:
50 return "LPAD($str, $size, '$pad')";
51 }
52 }
53
54 /**
55 * A class to manipulate date SQL.
56 */
57 class date_sql_handler {
58 var $db_type = 'mysql';
59 var $date_type = DATE_DATETIME;
60 var $db_timezone = 'UTC'; // A string timezone name.
61 var $local_timezone = NULL; // A string timezone name.
62 var $db_timezone_field = NULL; // Use if the db timezone is stored in a field.
63 var $local_timezone_field = NULL; // Use if the local timezone is stored in a field.
64 var $offset_field = NULL; // Use if the offset is stored in a field.
65
66 function construct($date_type = DATE_DATETIME, $local_timezone = NULL) {
67 $this->db_type = $GLOBALS['db_type'];
68 $this->date_type = $date_type;
69 $this->db_timezone = 'UTC';
70 $this->local_timezone = isset($local_timezone) ? $local_timezone : date_default_timezone();
71 if (isset($this->definition['content_field'])) {
72 $this->date_handler->date_type = $this->definition['content_field']['type'];
73 }
74 date_api_set_db_timezone();
75 }
76
77 /**
78 * See if the db has timezone name support.
79 */
80 function db_tz_support($reset = FALSE) {
81 $has_support = variable_get('date_db_tz_support', -1);
82 if ($has_support == -1 || $reset) {
83 date_api_set_db_timezone();
84 $has_support = FALSE;
85 switch ($this->db_type) {
86 case 'mysql':
87 case 'mysqli':
88 if (version_compare(db_version(), '4.1.3', '>=')) {
89 $test = db_result(db_query("SELECT CONVERT_TZ('2008-02-15 12:00:00', 'UTC', 'US/Central')"));
90 if ($test == '2008-02-15 06:00:00') {
91 $has_support = TRUE;
92 }
93 }
94 break;
95 case 'pgsql':
96 $test = db_result(db_query("'2008-02-15 12:00:00 UTC' AT TIME ZONE 'US/Central'"));
97 if ($test == '2008-02-15 06:00:00') {
98 $has_support = TRUE;
99 }
100 break;
101 }
102 variable_set('date_db_tz_support', $has_support);
103 }
104 return $has_support;
105 }
106
107 /**
108 * Set the database timzone offset.
109 *
110 * Setting the db timezone to UTC is done to ensure consistency in date
111 * handling whether or not the database can do proper timezone conversion.
112 *
113 * Views filters that not exposed are cached and won't set the timezone
114 * so views date filters should add 'cacheable' => 'no' to their
115 * definitions to ensure that the database timezone gets set properly
116 * when the query is executed.
117 *
118 * @param $offset
119 * An offset value to set the database timezone to. This will only
120 * set a fixed offset, not a timezone, so any value other than
121 * '+00:00' should be used with caution.
122 */
123 function set_db_timezone($offset = '+00:00') {
124 static $already_set = FALSE;
125 $type = $GLOBALS['db_type'];
126 if (!$already_set) {
127 if (($type == 'mysqli' || $type == 'mysql') && version_compare(db_version(), '4.1.3', '>=')) {
128 db_query("SET @@session.time_zone = '$offset'");
129 }
130 elseif ($type == 'pgsql') {
131 db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
132 }
133 $already_set = TRUE;
134 }
135 }
136
137 /**
138 * Return timezone offset for the date being processed.
139 */
140 function get_offset() {
141 if (!empty($this->db_timezone) && !empty($this->local_timezone)) {
142 if ($this->db_timezone != $this->local_timezone) {
143 $date = date_now($this->db_timezone);
144 date_timezone_set($date, timezone_open($this->local_timezone));
145 return date_offset_get($date);
146 }
147 }
148 return 0;
149 }
150
151 /**
152 * Helper function to create cross-database SQL dates.
153 *
154 * @param $field
155 * The real table and field name, like 'tablename.fieldname' .
156 * @param $offset
157 * The name of a field that holds the timezone offset or an
158 * offset value. If NULL, the normal Drupal timezone handling
159 * will be used, if $offset = 0 no adjustment will be made.
160 * @return
161 * An appropriate SQL string for the db type and field type.
162 */
163 function sql_field($field, $offset = NULL) {
164 if (strtoupper($field) == 'NOW') {
165 // NOW() will be in UTC since that is what we set the db timezone to.
166 $this->local_timezone = 'UTC';
167 return $this->sql_offset('NOW()', $offset);
168 }
169 switch ($this->db_type) {
170 case 'mysql':
171 case 'mysqli':
172 switch ($this->date_type) {
173 case DATE_UNIX:
174 $field = "FROM_UNIXTIME($field)";
175 break;
176 case DATE_ISO:
177 if (version_compare(db_version(), '4.1.1', '>=')) {
178 $field = "STR_TO_DATE($field, '%Y-%m-%%dT%T')";
179 }
180 else {
181 $field = "REPLACE($field, 'T', ' ')";
182 }
183 break;
184 case DATE_DATETIME:
185 break;
186 }
187 break;
188 case 'pgsql':
189 switch ($this->date_type) {
190 case DATE_UNIX:
191 $field = "$field::ABSTIME";
192 break;
193 case DATE_ISO:
194 $field = "TO_DATE($field, 'FMYYYY-FMMM-FMDDTFMHH:FMMI:FMSS')";
195 break;
196 case DATE_DATETIME:
197 break;
198 }
199 break;
200 }
201 // Adjust the resulting value to the right timezone/offset.
202 return $this->sql_tz($field, $offset);
203 }
204
205 /**
206 * Adjust a field value by an offset in seconds.
207 */
208 function sql_offset($field, $offset = NULL) {
209 if (!empty($offset)) {
210 switch ($this->db_type) {
211 case 'mysql':
212 case 'mysqli':
213 if (version_compare(db_version(), '4.1.1', '>=')) {
214 return "ADDTIME($field, SEC_TO_TIME($offset))";
215 }
216 else {
217 return "DATE_ADD($field, INTERVAL $offset SECOND)";
218 }
219 case 'pgsql':
220 return "($field + INTERVAL '$offset SECONDS')";;
221 }
222 }
223 return $field;
224 }
225
226 /**
227 * Adjust a field value by time interval.
228 *
229 * @param $field
230 * The field to be adjusted.
231 * @param $direction
232 * Either ADD or SUB.
233 * @param $count
234 * The number of values to adjust.
235 * @param $granularity
236 * The granularity of the adjustment, should be singular,
237 * like SECOND, MINUTE, DAY, HOUR.
238 */
239 function sql_date_math($field, $direction, $count, $granularity) {
240 $granularity = strtoupper($granularity);
241 switch ($this->db_type) {
242 case 'mysql':
243 case 'mysqli':
244 switch ($direction) {
245 case 'ADD':
246 return "DATE_ADD($field, INTERVAL $count $granularity)";
247 case 'SUB':
248 return "DATE_SUB($field, INTERVAL $count $granularity)";
249 }
250
251 case 'pgsql':
252 $granularity .= 'S';
253 switch ($direction) {
254 case 'ADD':
255 return "($field + INTERVAL '$count $granularity')";
256 case 'SUB':
257 return "($field - INTERVAL '$count $granularity')";
258 }
259 }
260 return $field;
261 }
262
263 /**
264 * Select a date value from the database, adjusting the value
265 * for the timezone.
266 *
267 * Check whether database timezone conversion is supported in
268 * this system and use it if possible, otherwise use an
269 * offset.
270 *
271 * @param $offset
272 * Set a fixed offset or offset field to use for the date.
273 * If set, no timezone conversion will be done and the
274 * offset will be used.
275 */
276 function sql_tz($field, $offset = NULL) {
277 // If the timezones are values they need to be quoted, but
278 // if they are field names they do not.
279 $db_zone = $this->db_timezone_field ? $this->db_timezone_field : "'{$this->db_timezone}'";
280 $localzone = $this->local_timezone_field ? $this->local_timezone_field : "'{$this->local_timezone}'";
281
282 // If a fixed offset is required, use it.
283 if ($offset !== NULL) {
284 return $this->sql_offset($field, $offset);
285 }
286 // If the db and local timezones are the same, make no adjustment.
287 elseif ($db_zone == $localzone) {
288 return $this->sql_offset($field, 0);
289 }
290 // If the db has no timezone support, adjust by the offset,
291 // could be either a field name or a value.
292 elseif (!$this->db_tz_support()) {
293 if (!empty($this->offset_field)) {
294 return $this->sql_offset($field, $this->offset_field);
295 }
296 else {
297 return $this->sql_offset($field, $this->get_offset());
298 }
299 }
300 // Otherwise make a database timezone adjustment to the field.
301 else {
302 switch ($this->db_type) {
303 case 'mysql':
304 case 'mysqli':
305 return "CONVERT_TZ($field, $db_zone, $localzone)";
306 case 'pgsql':
307 // WITH TIME ZONE assumes the date is using the system
308 // timezone, which should have been set to UTC.
309 return "TIMESTAMP WITH TIME ZONE $field AT TIME ZONE $localzone";
310 }
311 }
312 }
313
314 /**
315 * Helper function to create cross-database SQL date formatting.
316 *
317 * @param $format
318 * A format string for the result, like 'Y-m-d H:i:s' .
319 * @param $field
320 * The real table and field name, like 'tablename.fieldname' .
321 * @return
322 * An appropriate SQL string for the db type and field type.
323 */
324 function sql_format($format, $field) {
325 switch ($this->db_type) {
326 case 'mysql':
327 case 'mysqli':
328 $replace = array(
329 'Y' => '%Y', 'y' => '%y',
330 'm' => '%m', 'n' => '%c',
331 'd' => '%%d', 'j' => '%e',
332 'H' => '%H',
333 'i' => '%i',
334 's' => '%%s',
335 '\WW' => 'W%U',
336 );
337 $format = strtr($format, $replace);
338 return "DATE_FORMAT($field, '$format')";
339 case 'pgsql':
340 $replace = array(
341 'Y' => 'YYYY', 'y' => 'Y',
342 'm' => 'MM', 'n' => 'M',
343 'd' => 'DD', 'j' => 'D',
344 'H' => 'HH24',
345 'i' => 'MI',
346 's' => 'SS',
347 '\T' => '"T"',
348 //'\W' => // TODO, what should this be?
349 );
350 $format = strtr($format, $replace);
351 return "TO_CHAR($field, '$format')";
352 }
353 }
354
355 /**
356 * Helper function to create cross-database SQL date extraction.
357 *
358 * @param $extract_type
359 * The type of value to extract from the date, like 'MONTH' .
360 * @param $field
361 * The real table and field name, like 'tablename.fieldname' .
362 * @return
363 * An appropriate SQL string for the db type and field type.
364 */
365 function sql_extract($extract_type, $field) {
366 // Note there is no space after FROM to avoid db_rewrite problems
367 // see http://drupal.org/node/79904.
368 switch (strtoupper($extract_type)) {
369 case('DATE'):
370 return $field;
371 case('YEAR'):
372 return "EXTRACT(YEAR FROM($field))";
373 case('MONTH'):
374 return "EXTRACT(MONTH FROM($field))";
375 case('DAY'):
376 return "EXTRACT(DAY FROM($field))";
377 case('HOUR'):
378 return "EXTRACT(HOUR FROM($field))";
379 case('MINUTE'):
380 return "EXTRACT(MINUTE FROM($field))";
381 case('SECOND'):
382 return "EXTRACT(SECOND FROM($field))";
383 case('WEEK'): // ISO week number for date
384 switch ($this->db_type) {
385 case('mysql'):
386 case('mysqli'):
387 // WEEK using arg 3 in mysql should return the same value as postgres EXTRACT
388 return "WEEK($field, 3)";
389 case('pgsql'):
390 return "EXTRACT(WEEK FROM($field))";
391 }
392 case('DOW'):
393 switch ($this->db_type) {
394 case('mysql'):
395 case('mysqli'):
396 // mysql returns 1 for Sunday through 7 for Saturday
397 // php date functions and postgres use 0 for Sunday and 6 for Saturday
398 return "INTEGER(DAYOFWEEK($field) - 1)";
399 case('pgsql'):
400 return "EXTRACT(DOW FROM($field))";
401 }
402 case('DOY'):
403 switch ($this->db_type) {
404 case('mysql'):
405 case('mysqli'):
406 return "DAYOFYEAR($field)";
407 case('pgsql'):
408 return "EXTRACT(DOY FROM($field))";
409 }
410 }
411 }
412
413 /**
414 * Create a where clause to compare a complete date field to a complete date value.
415 *
416 * @param string $type
417 * The type of value we're comparing to, could be another field
418 * or a date value.
419 * @param string $field
420 * The db table and field name, like "$table.$field".
421 * @param string $operator
422 * The db comparison operator to use, like '=' .
423 * @param int $value
424 * The value to compare the extracted date part to, could be a
425 * field name or a date string or NOW().
426 * @return
427 * SQL for the where clause for this operation.
428 */
429 function sql_where_date($type, $field, $operator, $value, $adjustment = 0) {
430 $type = strtoupper($type);
431 if (strtoupper($value) == 'NOW') {
432 $value = $this->sql_field('NOW', $adjustment);
433 }
434 elseif ($type == 'FIELD') {
435 $value = $this->sql_field($value, $adjustment);
436 }
437 elseif ($type == 'DATE') {
438 $date = date_make_date($value, date_default_timezone(), DATE_DATETIME);
439 if (!empty($adjustment)) {
440 date_modify($date, $adjustment . ' seconds');
441 }
442 // When comparing a field to a date we can avoid doing timezone
443 // conversion by altering the comparison date to the db timezone.
444 // This won't work if the timezone is a field instead of a value.
445 if (empty($this->db_timezone_field) && empty($this->local_timezone_field) && $this->db_timezone_field != $this->local_timezone_field) {
446 date_timezone_set($date, timezone_open($this->db_timezone));
447 $this->local_timezone = $this->db_timezone;
448 }
449 $value = "'" . date_format_date($date, 'custom', DATE_FORMAT_DATETIME) . "'";
450 }
451 if ($this->local_timezone != $this->db_timezone) {
452 $field = $this->sql_field($field);
453 }
454 else {
455 $field = $this->sql_field($field, 0);
456 }
457 return "$field $operator $value";
458 }
459
460 /**
461 * Create a where clause to compare an extracted part of a field to an integer value.
462 *
463 * @param string $part
464 * The part to extract, YEAR, MONTH, DAY, etc.
465 * @param string $field
466 * The db table and field name, like "$table.$field".
467 * @param string $operator
468 * The db comparison operator to use, like '=' .
469 * @param int $value
470 * The integer value to compare the extracted date part to.
471 * @return
472 * SQL for the where clause for this operation.
473 */
474 function sql_where_extract($part, $field, $operator, $value) {
475 if ($this->local_timezone != $this->db_timezone) {
476 $field = $this->sql_field($field);
477 }
478 else {
479 $field = $this->sql_field($field, 0);
480 }
481 return $this->sql_extract($part, $field) ." $operator $value";
482 }
483
484 /**
485 * Create a where clause to compare a formated field to a formated value.
486 *
487 * @param string $format
488 * The format to use on the date and the value when comparing them.
489 * @param string $field
490 * The db table and field name, like "$table.$field".
491 * @param string $operator
492 * The db comparison operator to use, like '=' .
493 * @param string $value
494 * The value to compare the extracted date part to, could be a
495 * field name or a date string or NOW().
496 * @return
497 * SQL for the where clause for this operation.
498 */
499 function sql_where_format($format, $field, $operator, $value) {
500 if ($this->local_timezone != $this->db_timezone) {
501 $field = $this->sql_field($field);
502 }
503 else {
504 $field = $this->sql_field($field, 0);
505 }
506 return $this->sql_format($format, $field) ." $operator '$value'";
507 }
508
509 /**
510 * An array of all date parts,
511 * optionally limited to an array of allowed parts.
512 */
513 function date_parts($limit = NULL) {
514 $parts = array(
515 'year' => date_t('Year', 'datetime'), 'month' => date_t('Month', 'datetime'), 'day' => date_t('Day', 'datetime'),
516 'hour' => date_t('Hour', 'datetime'), 'minute' => date_t('Minute', 'datetime'), 'second' => date_t('Second', 'datetime'),
517 );
518 if (!empty($limit)) {
519 $last = FALSE;
520 foreach ($parts as $key => $part) {
521 if ($last) {
522 unset($parts[$key]);
523 }
524 if ($key == $limit) {
525 $last = TRUE;
526 }
527 }
528 }
529 return $parts;
530 }
531
532 /**
533 * Part information.
534 *
535 * @param $op
536 * 'min', 'max', 'format', 'sep', 'empty_now', 'empty_min', 'empty_max' .
537 * Returns all info if empty.
538 * @param $part
539 * 'year', 'month', 'day', 'hour', 'minute', or 'second.
540 * returns info for all parts if empty.
541 */
542 function part_info($op = NULL, $part = NULL) {
543 $info = array();
544 $info['min'] = array(
545 'year' => 100, 'month' => 1, 'day' => 1,
546 'hour' => 0, 'minute' => 0, 'second' => 0);
547 $info['max'] = array(
548 'year' => 4000, 'month' => 12, 'day' => 31,
549 'hour' => 23, 'minute' => 59, 'second' => 59);
550 $info['format'] = array(
551 'year' => 'Y', 'month' => 'm', 'day' => 'd',
552 'hour' => 'H', 'minute' => 'i', 'second' => 's');
553 $info['sep'] = array(
554 'year' => '', 'month' => '-', 'day' => '-',
555 'hour' => ' ', 'minute' => ':', 'second' => ':');
556 $info['empty_now'] = array(
557 'year' => date('Y'), 'month' => date('m'), 'day' => min('28', date('d')),
558 'hour' => date('H'), 'minute' => date('i'), 'second' => date('s'));
559 $info['empty_min'] = array(
560 'year' => '1000', 'month' => '01', 'day' => '01',
561 'hour' => '00', 'minute' => '00', 'second' => '00');
562 $info['empty_max'] = array(
563 'year' => '9999', 'month' => '12', 'day' => '31',
564 'hour' => '23', 'minute' => '59', 'second' => '59');
565 if (!empty($op)) {
566 if (!empty($part)) {
567 return $info[$op][$part];
568 }
569 else {
570 return $info[$op];
571 }
572 }
573 return $info;
574 }
575
576 /**
577 * Create a complete datetime value out of an
578 * incomplete array of selected values.
579 *
580 * For example, array('year' => 2008, 'month' => 05) will fill
581 * in the day, hour, minute and second with the earliest possible
582 * values if type = 'min', the latest possible values if type = 'max',
583 * and the current values if type = 'now' .
584 */
585 function complete_date($selected, $type = 'now') {
586 if (empty($selected)) {
587 return '';
588 }
589 // Special case for weeks.
590 if (array_key_exists('week', $selected)) {
591 $dates = date_week_range($selected['week'], $selected['year']);
592 switch ($type) {
593 case 'empty_now':
594 case 'empty_min':
595 case 'min':
596 return date_format($dates[0], 'Y-m-d H:i:s');
597 case 'empty_max':
598 case 'max':
599 return date_format($dates[1], 'Y-m-d H:i:s');
600 default:
601 return;
602 }
603 }
604
605 $compare = array_merge($this->part_info('empty_' . $type), $selected);
606 // If this is a max date, make sure the last day of
607 // the month is the right one for this date.
608 if ($type == 'max') {
609 $compare['day'] = date_days_in_month($compare['year'], $compare['month']);
610 }
611 $value = '';
612 $separators = $this->part_info('sep');
613 foreach ($this->date_parts() as $key => $name) {
614 $value .= $separators[$key] . (!empty($selected[$key]) ? $selected[$key] : $compare[$key]);
615 }
616 return $value;
617 }
618 /**
619 * Convert a format string into help text,
620 * i.e. 'Y-m-d' becomes 'YYYY-MM-DD' .
621 *
622 * @param unknown_type $format
623 * @return unknown
624 */
625 function format_help($format) {
626 $replace = array(
627 'Y' => 'YYYY', 'm' => 'MM', 'd' => 'DD',
628 'H' => 'HH', 'i' => 'MM', 's' => 'SS', '\T' => 'T');
629 return strtr($format, $replace);
630 }
631
632 /**
633 * A function to test the validity of various date parts
634 */
635 function part_is_valid($value, $type) {
636 if ( !preg_match('/^[0-9]*$/', $value) ) {
637 return FALSE;
638 }
639 $value = intval($value);
640 if ($value <= 0) return false;
641 switch ($type) {
642 case 'year':
643 if ($value < DATE_MIN_YEAR) return FALSE;
644 break;
645 case 'month':
646 if ($value < 0 || $value > 12) return FALSE;
647 break;
648 case 'day':
649 if ($value < 0 || $value > 31) return FALSE;
650 break;
651 case 'week':
652 if ($value < 0 || $value > 53) return FALSE;
653 }
654 return TRUE;
655 }
656
657 function views_formats($granularity, $type = 'sql') {
658 $formats = array('display', 'sql');
659 // Start with the site long date format and add seconds to it
660 $long = str_replace(':i', ':i:s', variable_get('date_format_long', 'l, F j, Y - H:i'));
661 switch ($granularity) {
662 case('year'):
663 $formats['display'] = 'Y';
664 $formats['sql'] = 'Y';
665 break;
666 case('month'):
667 $formats['display'] = date_limit_format($long, array('year', 'month'));
668 $formats['sql'] = 'Y-m';
669 break;
670 case('day'):
671 $formats['display'] = date_limit_format($long, array('year', 'month', 'day'));
672 $formats['sql'] = 'Y-m-d';
673 break;
674 case('hour'):
675 $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour'));
676 $formats['sql'] = 'Y-m-d\TH';
677 break;
678 case('minute'):
679 $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour', 'minute'));
680 $formats['sql'] = 'Y-m-d\TH:i';
681 break;
682 case('second'):
683 $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour', 'minute', 'second'));
684 $formats['sql'] = 'Y-m-d\TH:i:s';
685 break;
686 case('week'):
687 $formats['display'] = 'F j Y (W)';
688 $formats['sql'] = 'Y-\WW';
689 break;
690 }
691 return $formats[$type];
692 }
693
694 function granularity_form($granularity) {
695 $form = array(
696 '#title' => t('Granularity'),
697 '#type' => 'radios',
698 '#default_value' => $granularity,
699 '#options' => $this->date_parts(),
700 );
701 return $form;
702 }
703
704 /**
705 * Parse date parts from an ISO date argument.
706 *
707 * Based on ISO 8601 date duration and time interval standards.
708 *
709 * See http://en.wikipedia.org/wiki/ISO_8601#Week_dates for definitions of ISO weeks.
710 * See http://en.wikipedia.org/wiki/ISO_8601#Duration for definitions of ISO duration and time interval.
711 *
712 * Parses a value like 2006-01-01--2006-01-15, or 2006-W24, or @P1W.
713 * Separate from and to dates or date and period with a double hyphen (--).
714 *
715 * The 'to' portion of the argument can be eliminated if it is the same as the 'from' portion.
716 * Use @ instead of a date to substitute in the current date and time.
717 *
718 * Use periods (P1H, P1D, P1W, P1M, P1Y) to get next hour/day/week/month/year from now.
719 * Use date before P sign to get next hour/day/week/month/year from that date.
720 * Use period then date to get a period that ends on the date.
721 *
722 */
723 function arg_parts($argument) {
724 $values = array();
725 // Keep mal-formed arguments from creating errors.
726 if (empty($argument) || is_array($argument)) {
727 return array('date' => array(), 'period' => array());
728 }
729 $fromto = explode('--', $argument);
730 foreach ($fromto as $arg) {
731 $parts = array();
732 if ($arg == '@') {
733 $parts['date'] = date_array(date_now());
734 }
735 elseif (preg_match('/(\d{4})?-?(W)?(\d{1,2})?-?(\d{1,2})?[T\s]?(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?/', $arg, $matches)) {
736 $date = array();
737 if (!empty($matches[1])) $date['year'] = $matches[1];
738 if (!empty($matches[3])) {
739 if (empty($matches[2])) {
740 $date['month'] = $matches[3];
741 }
742 else {
743 $date['week'] = $matches[3];
744 }
745 }
746 if (!empty($matches[4])) $date['day'] = $matches[4];
747 if (!empty($matches[5])) $date['hour'] = $matches[5];
748 if (!empty($matches[6])) $date['minute'] = $matches[6];
749 if (!empty($matches[7])) $date['second'] = $matches[7];
750 $parts['date'] = $date;
751 }
752 if (preg_match('/^P(\d{1,4}[Y])?(\d{1,2}[M])?(\d{1,2}[W])?(\d{1,2}[D])?([T]{0,1})?(\d{1,2}[H])?(\d{1,2}[M])?(\d{1,2}[S])?/', $arg, $matches)) {
753 $period = array();
754 if (!empty($matches[1])) $period['year'] = str_replace('Y', '', $matches[1]);
755 if (!empty($matches[2])) $period['month'] = str_replace('M', '', $matches[2]);
756 if (!empty($matches[3])) $period['week'] = str_replace('W', '', $matches[3]);
757 if (!empty($matches[4])) $period['day'] = str_replace('D', '', $matches[4]);
758 if (!empty($matches[6])) $period['hour'] = str_replace('H', '', $matches[6]);
759 if (!empty($matches[7])) $period['minute'] = str_replace('M', '', $matches[7]);
760 if (!empty($matches[8])) $period['second'] = str_replace('S', '', $matches[8]);
761 $parts['period'] = $period;
762 }
763 $values[] = $parts;
764 }
765 return $values;
766 }
767
768 /**
769 * Convert strings like '+1 day' to the ISO equivalent, like 'P1D' .
770 */
771 function arg_replace($arg) {
772 if (!preg_match('/([+|-])\s?([0-9]{1,32})\s?([day(s)?|week(s)?|month(s)?|year(s)?|hour(s)?|minute(s)?|second(s)?]{1,10})/', $arg, $results)) {
773 return str_replace('now', '@', $arg);
774 }
775 $direction = $results[1];
776 $count = $results[2];
777 $item = $results[3];
778
779 $replace = array(
780 'now' => '@',
781 '+' => 'P',
782 '-' => 'P-',
783 'years' => 'Y',
784 'year' => 'Y',
785 'months' => 'M',
786 'month' => 'M',
787 'weeks' => 'W',
788 'week' => 'W',
789 'days' => 'D',
790 'day' => 'D',
791 'hours' => 'H',
792 'hour' => 'H',
793 'minutes' => 'M',
794 'minute' => 'M',
795 'seconds' => 'S',
796 'second' => 'S',
797 ' ' => '',
798 ' ' => '',
799 );
800 $prefix = in_array($item, array('hours', 'hour', 'minutes', 'minute', 'seconds', 'second')) ? 'T' : '';
801 return $prefix . strtr($direction, $replace) . $count . strtr($item, $replace);
802 }
803
804 /**
805 * Use the parsed values from the ISO argument to determine the
806 * granularity of this period.
807 */
808 function arg_granularity($arg) {
809 $granularity = '';
810 $parts = $this->arg_parts($arg);
811 $date = !empty($parts[0]['date']) ? $parts[0]['date'] : (!empty($parts[1]['date']) ? $parts[1]['date'] : array());
812 foreach ($date as $key => $part) {
813 $granularity = $key;
814 }
815 return $granularity;
816 }
817
818 /**
819 * Use the parsed values from the ISO argument to determine the
820 * min and max date for this period.
821 */
822 function arg_range($arg) {
823 // Parse the argument to get its parts
824 $parts = $this->arg_parts($arg);
825
826 // Build a range from a period-only argument (assumes the min date is now.)
827 if (empty($parts[0]['date']) && !empty($parts[0]['period']) && (empty($parts[1]))) {
828 $min_date = date_now();
829 $max_date = clone($min_date);
830 foreach ($parts[0]['period'] as $part => $value) {
831 date_modify($max_date, "+$value $part");
832 }
833 date_modify($max_date, '-1 second');
834 return array($min_date, $max_date);
835 }
836 // Build a range from a period to period argument
837 if (empty($parts[0]['date']) && !empty($parts[0]['period']) && !empty($parts[1]['period'])) {
838 $min_date = date_now();
839 $max_date = clone($min_date);
840 foreach ($parts[0]['period'] as $part => $value) {
841 date_modify($min_date, "+$value $part");
842 }
843 date_modify($min_date, '-1 second');
844 foreach ($parts[1]['period'] as $part => $value) {
845 date_modify($max_date, "+$value $part");
846 }
847 date_modify($max_date, '-1 second');
848 return array($min_date, $max_date);
849 }
850 if (!empty($parts[0]['date'])) {
851 $value = date_fuzzy_datetime($this->complete_date($parts[0]['date'], 'min'));
852 $min_date = date_make_date($value, date_default_timezone(), DATE_ISO);
853 // Build a range from a single date-only argument.
854 if (empty($parts[1]) || (empty($parts[1]['date']) && empty($parts[1]['period']))) {
855 $value = date_fuzzy_datetime($this->complete_date($parts[0]['date'], 'max'));
856 $max_date = date_make_date($value, date_default_timezone(), DATE_ISO);
857 return array($min_date, $max_date);
858 }
859 // Build a range from start date + period.
860 elseif (!empty($parts[1]['period'])) {
861 foreach ($parts[1]['period'] as $part => $value) {
862 $max_date = clone($min_date);
863 date_modify($max_date, "+$value $part");
864 }
865 date_modify($max_date, '-1 second');
866 return array($min_date, $max_date);
867 }
868 }
869 // Build a range from start date and end date.
870 if (!empty($parts[1]['date'])) {
871 $value = date_fuzzy_datetime($this->complete_date($parts[1]['date'], 'max'));
872 $max_date = date_make_date($value, date_default_timezone(), DATE_ISO);
873 if (isset($min_date)) {
874 return array($min_date, $max_date);
875 }
876 }
877 // Build a range from period + end date.
878 if (!empty($parts[0]['period'])) {
879 $min_date = date_now();
880 foreach ($parts[0]['period'] as $part => $value) {
881 date_modify($min_date, "$value $part");
882 }
883 return array($min_date, $max_date);
884 }
885 // Intercept invalid info and fall back to the current date.
886 $now = date_now();
887 return array($now, $now);
888 }
889 }

  ViewVC Help
Powered by ViewVC 1.1.2