first commit
[step2_drupal.git] / date / date_api_sql.inc
blob521cb872bef1dd1b81615750532611ce8d1612e5
1 <?php
2 // $Id: date_api_sql.inc,v 1.9.2.3.2.35 2009/02/26 18:48:00 karens Exp $
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   }
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   }  
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   }
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_name();
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   }
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   }
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           //'\W' => // TODO, what should this be?
348           );
349         $format = strtr($format, $replace);
350         return "TO_CHAR($field, '$format')";
351     }
352   }
354   /**
355    * Helper function to create cross-database SQL date extraction.
356    *
357    * @param $extract_type
358    *   The type of value to extract from the date, like 'MONTH'.
359    * @param $field
360    *   The real table and field name, like 'tablename.fieldname'.
361    * @return
362    *   An appropriate SQL string for the db type and field type.
363    */
364   function sql_extract($extract_type, $field) {
365     // Note there is no space after FROM to avoid db_rewrite problems
366     // see http://drupal.org/node/79904.
367     switch (strtoupper($extract_type)) {
368     case('DATE'):
369       return $field;
370     case('YEAR'):
371       return "EXTRACT(YEAR FROM($field))";
372     case('MONTH'):
373       return "EXTRACT(MONTH FROM($field))";
374     case('DAY'):
375       return "EXTRACT(DAY FROM($field))";
376     case('HOUR'):
377       return "EXTRACT(HOUR FROM($field))";
378     case('MINUTE'):
379       return "EXTRACT(MINUTE FROM($field))";
380     case('SECOND'):
381       return "EXTRACT(SECOND FROM($field))";
382     case('WEEK'):  // ISO week number for date
383       switch ($this->db_type) {
384         case('mysql'):
385         case('mysqli'):
386           // WEEK using arg 3 in mysql should return the same value as postgres EXTRACT
387           return "WEEK($field, 3)";
388         case('pgsql'):
389           return "EXTRACT(WEEK FROM($field))";
390       }
391     case('DOW'):
392       switch ($this->db_type) {
393         case('mysql'):
394         case('mysqli'):
395           // mysql returns 1 for Sunday through 7 for Saturday
396           // php date functions and postgres use 0 for Sunday and 6 for Saturday
397           return "INTEGER(DAYOFWEEK($field) - 1)";
398         case('pgsql'):
399           return "EXTRACT(DOW FROM($field))";
400       }
401     case('DOY'):
402       switch ($this->db_type) {
403         case('mysql'):
404         case('mysqli'):
405           return "DAYOFYEAR($field)";
406         case('pgsql'):
407           return "EXTRACT(DOY FROM($field))";
408       }
409     }
410   }
411   
412   /**
413    * Create a where clause to compare a complete date field to a complete date value.
414    *
415    * @param string $type
416    *   The type of value we're comparing to, could be another field
417    *   or a date value.
418    * @param string $field
419    *   The db table and field name, like "$table.$field".
420    * @param string $operator
421    *   The db comparison operator to use, like '='.
422    * @param int $value
423    *   The value to compare the extracted date part to, could be a
424    *   field name or a date string or NOW().
425    * @return 
426    *   SQL for the where clause for this operation.
427    */
428   function sql_where_date($type, $field, $operator, $value, $adjustment = 0) {
429     $type = strtoupper($type);
430     if (strtoupper($value) == 'NOW') {
431       $value = $this->sql_field('NOW', $adjustment);
432     }
433     elseif ($type == 'FIELD') {
434       $value = $this->sql_field($value, $adjustment);
435     }    
436     elseif ($type == 'DATE') {
437       $date = date_make_date($value, date_default_timezone_name(), DATE_DATETIME);
438       if (!empty($adjustment)) {
439         date_modify($date, $adjustment .' seconds');
440       }
441       // When comparing a field to a date we can avoid doing timezone 
442       // conversion by altering the comparison date to the db timezone.
443       // This won't work if the timezone is a field instead of a value.
444       if (empty($this->db_timezone_field) && empty($this->local_timezone_field) && $this->db_timezone_field != $this->local_timezone_field) {
445         date_timezone_set($date, timezone_open($this->db_timezone));
446         $this->local_timezone = $this->db_timezone;
447       }
448       $value = "'". date_format_date($date, 'custom', DATE_FORMAT_DATETIME) ."'";
449     }    
450     if ($this->local_timezone != $this->db_timezone) {
451       $field = $this->sql_field($field);
452     }
453     else {
454       $field = $this->sql_field($field, 0);
455     }
456     return "$field $operator $value";
457   }
458   
459   /**
460    * Create a where clause to compare an extracted part of a field to an integer value.
461    *
462    * @param string $part
463    *   The part to extract, YEAR, MONTH, DAY, etc.
464    * @param string $field
465    *   The db table and field name, like "$table.$field".
466    * @param string $operator
467    *   The db comparison operator to use, like '='.
468    * @param int $value
469    *   The integer value to compare the extracted date part to.
470    * @return 
471    *   SQL for the where clause for this operation.
472    */
473   function sql_where_extract($part, $field, $operator, $value) {
474     if ($this->local_timezone != $this->db_timezone) {
475       $field = $this->sql_field($field);
476     }
477     else {
478       $field = $this->sql_field($field, 0);
479     }
480     return $this->sql_extract($part, $field) ." $operator $value";
481   }
482   
483   /**
484    * Create a where clause to compare a formated field to a formated value.
485    *
486    * @param string $format
487    *   The format to use on the date and the value when comparing them.
488    * @param string $field
489    *   The db table and field name, like "$table.$field".
490    * @param string $operator
491    *   The db comparison operator to use, like '='.
492    * @param string $value
493    *   The value to compare the extracted date part to, could be a
494    *   field name or a date string or NOW().
495    * @return 
496    *   SQL for the where clause for this operation.
497    */
498   function sql_where_format($format, $field, $operator, $value) {
499     if ($this->local_timezone != $this->db_timezone) {
500       $field = $this->sql_field($field);
501     }
502     else {
503       $field = $this->sql_field($field, 0);
504     }
505     return $this->sql_format($format, $field) ." $operator '$value'";
506   }
508   /**
509    * An array of all date parts,
510    * optionally limited to an array of allowed parts.
511    */
512   function date_parts($limit = NULL) {
513     $parts =  array(
514       'year' => date_t('Year', 'datetime'), 'month' => date_t('Month', 'datetime'), 'day' => date_t('Day', 'datetime'),
515       'hour' => date_t('Hour', 'datetime'), 'minute' => date_t('Minute', 'datetime'), 'second' => date_t('Second', 'datetime'),
516       );
517     if (!empty($limit)) {
518       $last = FALSE;
519       foreach ($parts as $key => $part) {
520         if ($last) {
521           unset($parts[$key]);
522         }
523         if ($key == $limit) {
524           $last = TRUE;
525         }
526       }
527     }
528     return $parts;
529   }
531   /**
532    * Part information.
533    *
534    * @param $op
535    *   'min', 'max', 'format', 'sep', 'empty_now', 'empty_min', 'empty_max'. 
536    *   Returns all info if empty.
537    * @param $part
538    *   'year', 'month', 'day', 'hour', 'minute', or 'second.
539    *   returns info for all parts if empty.
540    */
541   function part_info($op = NULL, $part = NULL) {
542     $info = array();
543     $info['min'] = array(
544       'year' => 100, 'month' => 1, 'day' => 1,
545       'hour' => 0, 'minute' => 0, 'second' => 0);
546     $info['max'] = array(
547       'year' => 4000, 'month' => 12, 'day' => 31,
548       'hour' => 23, 'minute' => 59, 'second' => 59);
549     $info['format'] = array(
550       'year' => 'Y', 'month' => 'm', 'day' => 'd',
551       'hour' => 'H', 'minute' => 'i', 'second' => 's');
552     $info['sep'] = array(
553       'year' => '', 'month' => '-', 'day' => '-',
554       'hour' => ' ', 'minute' => ':', 'second' => ':');
555     $info['empty_now'] = array(
556       'year' => date('Y'), 'month' => date('m'), 'day' => min('28', date('d')),
557       'hour' => date('H'), 'minute' => date('i'), 'second' => date('s'));  
558     $info['empty_min'] = array(
559       'year' => '1000', 'month' => '01', 'day' => '01',
560       'hour' => '00', 'minute' => '00', 'second' => '00');  
561     $info['empty_max'] = array(
562       'year' => '9999', 'month' => '12', 'day' => '31',
563       'hour' => '23', 'minute' => '59', 'second' => '59');  
564     if (!empty($op)) {
565       if (!empty($part)) {
566         return $info[$op][$part];
567       }
568       else {
569         return $info[$op];
570       }
571     }
572     return $info;
573   }
575   /**
576    * Create a complete datetime value out of an 
577    * incomplete array of selected values.
578    * 
579    * For example, array('year' => 2008, 'month' => 05) will fill
580    * in the day, hour, minute and second with the earliest possible
581    * values if type = 'min', the latest possible values if type = 'max',
582    * and the current values if type = 'now'.
583    */
584   function complete_date($selected, $type = 'now') {
585     if (empty($selected)) {
586       return '';
587     }
588     // Special case for weeks.
589     if (array_key_exists('week', $selected)) {
590       $dates = date_week_range($selected['week'], $selected['year']);
591       switch ($type) {
592         case 'empty_now':
593         case 'empty_min':
594         case 'min':
595           return date_format($dates[0], 'Y-m-d H:i:s');
596         case 'empty_max':
597         case 'max':
598           return date_format($dates[1], 'Y-m-d H:i:s');
599         default:
600           return;
601       }
602     }
603     
604     $compare = array_merge($this->part_info('empty_'. $type), $selected);
605     // If this is a max date, make sure the last day of 
606     // the month is the right one for this date.
607     if ($type == 'max') {
608       $compare['day'] = date_days_in_month($compare['year'], $compare['month']);
609     }
610     $value = '';
611     $separators = $this->part_info('sep');
612     foreach ($this->date_parts() as $key => $name) {
613       $value .= $separators[$key] . (!empty($selected[$key]) ? $selected[$key] : $compare[$key]);
614     }
615     return $value;
616   }
617   /**
618    * Convert a format string into help text,
619    * i.e. 'Y-m-d' becomes 'YYYY-MM-DD'.
620    *
621    * @param unknown_type $format
622    * @return unknown
623    */
624   function format_help($format) {
625     $replace = array(
626       'Y' => 'YYYY', 'm' => 'MM', 'd' => 'DD',
627       'H' => 'HH', 'i' => 'MM', 's' => 'SS', '\T' => 'T');
628     return strtr($format, $replace);
629   }
631   /**
632    *  A function to test the validity of various date parts
633    */
634   function part_is_valid($value, $type) {
635     if ( !preg_match('/^[0-9]*$/', $value) ) {
636       return false;
637     }
638     $value = intval($value);
639     if ($value <= 0) return false;
640     switch ($type) {
641       case 'year':
642         if ($value < DATE_MIN_YEAR) return false;
643         break;
644       case 'month':
645         if ($value < 0 || $value > 12) return false;
646         break;
647       case 'day':
648         if ($value < 0 || $value > 31) return false;
649         break;
650       case 'week':
651         if ($value < 0 || $value > 53) return false;
652     }
653     return true;
654   }
656   function views_formats($granularity, $type = 'sql') {
657     $formats = array('display', 'sql');
658     // Start with the site long date format and add seconds to it
659     $long = str_replace(':i', ':i:s', variable_get('date_format_long',  'l, F j, Y - H:i'));
660     switch ($granularity) {
661       case('year'):
662         $formats['display'] = 'Y';
663         $formats['sql'] = 'Y';
664         break;
665       case('month'):
666         $formats['display'] = date_limit_format($long, array('year', 'month'));
667         $formats['sql'] = 'Y-m';
668         break;
669       case('day'):
670         $formats['display'] = date_limit_format($long, array('year', 'month', 'day'));
671         $formats['sql'] = 'Y-m-d';
672         break;
673       case('hour'):
674         $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour'));
675         $formats['sql'] = 'Y-m-d\TH';
676         break;
677       case('minute'):
678         $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour', 'minute'));
679         $formats['sql'] = 'Y-m-d\TH:i';
680         break;
681       case('second'):
682         $formats['display'] = date_limit_format($long, array('year', 'month', 'day', 'hour', 'minute', 'second'));
683         $formats['sql'] = 'Y-m-d\TH:i:s';
684         break;
685       case('week'):
686         $formats['display'] = 'F j Y (W)';
687         $formats['sql'] = 'Y-\WW';
688         break;
689     }
690     return $formats[$type];
691   }
692   
693   function granularity_form($granularity) {
694     $form = array(
695       '#title' => t('Granularity'),
696       '#type' => 'radios',
697       '#default_value' => $granularity,
698       '#options' => $this->date_parts(),
699       );
700     return $form;
701   }
703   /**
704    * Parse date parts from an ISO date argument.
705    * 
706    * Based on ISO 8601 date duration and time interval standards.
707    *
708    * See http://en.wikipedia.org/wiki/ISO_8601#Week_dates for definitions of ISO weeks.
709    * See http://en.wikipedia.org/wiki/ISO_8601#Duration for definitions of ISO duration and time interval.
710    *
711    * Parses a value like 2006-01-01--2006-01-15, or 2006-W24, or @P1W.
712    * Separate from and to dates or date and period with a double hyphen (--).
713    *
714    * The 'to' portion of the argument can be eliminated if it is the same as the 'from' portion.
715    * Use @ instead of a date to substitute in the current date and time.
716    *
717    * Use periods (P1H, P1D, P1W, P1M, P1Y) to get next hour/day/week/month/year from now.
718    * Use date before P sign to get next hour/day/week/month/year from that date.
719    * Use period then date to get a period that ends on the date.
720    *
721    */
722   function arg_parts($argument) {
723     $values = array();
724     // Keep mal-formed arguments from creating errors.
725     if (empty($argument) || is_array($argument)) {
726       return array('date' => array(), 'period' => array());
727     }
728     $fromto = explode('--', $argument);
729     foreach ($fromto as $arg) {
730       $parts = array();
731       if ($arg == '@') {
732         $parts['date'] = date_array(date_now());
733       }
734       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)) {
735         $date = array();
736         if (!empty($matches[1])) $date['year'] = $matches[1];
737         if (!empty($matches[3])) {
738           if (empty($matches[2])) {
739             $date['month'] = $matches[3];
740           }
741           else {
742             $date['week'] = $matches[3];
743           }    
744         }
745         if (!empty($matches[4])) $date['day'] = $matches[4];
746         if (!empty($matches[5])) $date['hour'] = $matches[5];
747         if (!empty($matches[6])) $date['minute'] = $matches[6];
748         if (!empty($matches[7])) $date['second'] = $matches[7];
749         $parts['date'] = $date;
750       }
751       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)) {
752         $period = array();
753         if (!empty($matches[1])) $period['year'] =  str_replace('Y', '', $matches[1]);
754         if (!empty($matches[2])) $period['month'] = str_replace('M', '', $matches[2]);
755         if (!empty($matches[3])) $period['week'] = str_replace('W', '', $matches[3]);
756         if (!empty($matches[4])) $period['day'] = str_replace('D', '', $matches[4]);
757         if (!empty($matches[6])) $period['hour'] = str_replace('H', '', $matches[6]);
758         if (!empty($matches[7])) $period['minute'] = str_replace('M', '', $matches[7]);
759         if (!empty($matches[8])) $period['second'] = str_replace('S', '', $matches[8]);
760         $parts['period'] = $period;
761       }
762       $values[] = $parts;
763     }
764     return $values;
765   }
767   /**
768    * Convert strings like '+1 day' to the ISO equivalent, like 'P1D'.
769    */
770   function arg_replace($arg) {
771     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)) {
772       return str_replace('now', '@', $arg);
773     }
774     $direction = $results[1];
775     $count = $results[2];
776     $item = $results[3];
777       
778     $replace = array(
779       'now' => '@',
780       '+' => 'P',
781       '-' => 'P-',
782       'years' => 'Y',
783       'year' => 'Y',
784       'months' => 'M',
785       'month' => 'M',
786       'weeks' => 'W',
787       'week' => 'W',
788       'days' => 'D',
789       'day' => 'D',
790       'hours' => 'H',
791       'hour' => 'H',
792       'minutes' => 'M',
793       'minute' => 'M',
794       'seconds' => 'S',
795       'second' => 'S',
796       '  ' => '',
797       ' ' => '',
798       );
799     $prefix = in_array($item, array('hours', 'hour', 'minutes', 'minute', 'seconds', 'second')) ? 'T' : '';  
800     return $prefix . strtr($direction, $replace) . $count . strtr($item, $replace);  
801   }
802   
803   /**
804    * Use the parsed values from the ISO argument to determine the
805    * granularity of this period.
806    */
807   function arg_granularity($arg) {
808     $granularity = '';
809     $parts = $this->arg_parts($arg);
810     $date = !empty($parts[0]['date']) ? $parts[0]['date'] : (!empty($parts[1]['date']) ? $parts[1]['date'] : array());
811     foreach ($date as $key => $part) {
812       $granularity = $key;    
813     }
814     return $granularity;
815   }
817   /**
818    * Use the parsed values from the ISO argument to determine the
819    * min and max date for this period.
820    */
821   function arg_range($arg) {
822     // Parse the argument to get its parts
823     $parts = $this->arg_parts($arg);
824         
825     // Build a range from a period-only argument (assumes the min date is now.)
826     if (empty($parts[0]['date']) && !empty($parts[0]['period']) && (empty($parts[1]))) {
827       $min_date = date_now();
828       $max_date = drupal_clone($min_date);
829       foreach ($parts[0]['period'] as $part => $value) {
830         date_modify($max_date, "+$value $part");
831       }
832       date_modify($max_date, '-1 second');
833       return array($min_date, $max_date);
834     }
835     // Build a range from a period to period argument
836     if (empty($parts[0]['date']) && !empty($parts[0]['period']) && !empty($parts[1]['period'])) {
837       $min_date = date_now();
838       $max_date = drupal_clone($min_date);
839       foreach ($parts[0]['period'] as $part => $value) {
840         date_modify($min_date, "+$value $part");
841       }
842       date_modify($min_date, '-1 second');
843       foreach ($parts[1]['period'] as $part => $value) {
844         date_modify($max_date, "+$value $part");
845       }
846       date_modify($max_date, '-1 second');
847       return array($min_date, $max_date);
848     }
849     if (!empty($parts[0]['date'])) {
850       $value = date_fuzzy_datetime($this->complete_date($parts[0]['date'], 'min'));
851       $min_date = date_make_date($value, date_default_timezone_name(), DATE_ISO);
852       // Build a range from a single date-only argument.
853       if (empty($parts[1]) || (empty($parts[1]['date']) && empty($parts[1]['period']))) {
854         $value = date_fuzzy_datetime($this->complete_date($parts[0]['date'], 'max'));
855         $max_date = date_make_date($value, date_default_timezone_name(), DATE_ISO);
856         return array($min_date, $max_date);
857       }
858       // Build a range from start date + period.
859       elseif (!empty($parts[1]['period'])) {
860         foreach ($parts[1]['period'] as $part => $value) {
861           $max_date = drupal_clone($min_date);
862           date_modify($max_date, "+$value $part");
863         }
864         date_modify($max_date, '-1 second');
865         return array($min_date, $max_date);
866       }
867     }
868     // Build a range from start date and end date.
869     if (!empty($parts[1]['date'])) {
870       $value = date_fuzzy_datetime($this->complete_date($parts[1]['date'], 'max'));
871       $max_date = date_make_date($value, date_default_timezone_name(), DATE_ISO);
872       if (isset($min_date)) {
873         return array($min_date, $max_date);
874       }
875     }
876     // Build a range from period + end date.
877     if (!empty($parts[0]['period'])) {
878       $min_date = date_now();
879       foreach ($parts[0]['period'] as $part => $value) {
880         date_modify($min_date, "$value $part");
881       }
882       return array($min_date, $max_date);
883     }
884      // Intercept invalid info and fall back to the current date.
885     $now = date_now();
886     return array($now, $now);