Projet

Général

Profil

Paste
Télécharger (35,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api_sql.inc @ db9ffd17

1
<?php
2

    
3
/**
4
 * @file
5
 * SQL helper for Date API.
6
 *
7
 * @TODO
8
 * Add experimental support for sqlite: http://www.sqlite.org/lang_datefunc.html
9
 * and Oracle (http://psoug.org/reference/date_func.html and
10
 * http://psoug.org/reference/datatypes.html) date/time functions.
11
 */
12

    
13
/**
14
 * A helper function to do cross-database concatation of date parts.
15
 *
16
 * @param array $array
17
 *   An array of values to be concatonated in sql.
18
 *
19
 * @return string
20
 *   Correct sql string for database type.
21
 */
22
function date_sql_concat($array) {
23
  switch (Database::getConnection()->databaseType()) {
24
    case 'mysql':
25
      return "CONCAT(" . implode(",", $array) . ")";
26
    case 'pgsql':
27
      return implode(" || ", $array);
28
  }
29
}
30

    
31
/**
32
 * Helper function to do cross-database NULL replacements
33
 *
34
 * @param array $array
35
 *   An array of values to test for NULL values.
36
 *
37
 * @return string
38
 *   SQL statement to return the first non-NULL value in the list.
39
 */
40
function date_sql_coalesce($array) {
41
  switch (Database::getConnection()->databaseType()) {
42
    case 'mysql':
43
    case 'pgsql':
44
      return "COALESCE(" . implode(',', $array) . ")";
45
  }
46
}
47

    
48
/**
49
 * A helper function to do cross-database padding of date parts.
50
 *
51
 * @param string $str
52
 *   A string to apply padding to
53
 * @param int $size
54
 *   The size the final string should be
55
 * @param string $pad
56
 *   The value to pad the string with
57
 * @param string $side
58
 *   The side of the string to pad
59
 */
60
function date_sql_pad($str, $size = 2, $pad = '0', $side = 'l') {
61
  switch ($side) {
62
    case 'r':
63
      return "RPAD($str, $size, '$pad')";
64
    default:
65
      return "LPAD($str, $size, '$pad')";
66
  }
67
}
68

    
69
/**
70
 * A class to manipulate date SQL.
71
 */
72
class date_sql_handler {
73
  var $db_type = NULL;
74
  var $date_type = DATE_DATETIME;
75
  // A string timezone name.
76
  var $db_timezone = 'UTC';
77
  // A string timezone name.
78
  var $local_timezone = NULL;
79
  // Use if the db timezone is stored in a field.
80
  var $db_timezone_field = NULL;
81
  // Use if the local timezone is stored in a field.
82
  var $local_timezone_field = NULL;
83
  // Use if the offset is stored in a field.
84
  var $offset_field = NULL;
85

    
86
  /**
87
   * The object constuctor.
88
   */
89
  function __construct($date_type = DATE_DATETIME, $local_timezone = NULL, $offset = '+00:00') {
90
    $this->db_type = Database::getConnection()->databaseType();
91
    $this->date_type = $date_type;
92
    $this->db_timezone = 'UTC';
93
    $this->local_timezone = isset($local_timezone) ? $local_timezone : date_default_timezone();
94
    $this->set_db_timezone($offset);
95
  }
96

    
97
  /**
98
   * See if the db has timezone name support.
99
   */
100
  function db_tz_support($reset = FALSE) {
101
    $has_support = variable_get('date_db_tz_support', -1);
102
    if ($has_support == -1 || $reset) {
103
      $has_support = FALSE;
104
      switch ($this->db_type) {
105
        case 'mysql':
106
          $test = db_query("SELECT CONVERT_TZ('2008-02-15 12:00:00', 'UTC', 'US/Central')")->fetchField();
107
          if ($test == '2008-02-15 06:00:00') {
108
            $has_support = TRUE;
109
          }
110
          break;
111
        case 'pgsql':
112
          $test = db_query("SELECT '2008-02-15 12:00:00 UTC' AT TIME ZONE 'US/Central'")->fetchField();
113
          if ($test == '2008-02-15 06:00:00') {
114
            $has_support = TRUE;
115
          }
116
          break;
117
      }
118
      variable_set('date_db_tz_support', $has_support);
119
    }
120
    return $has_support;
121
  }
122

    
123
  /**
124
   * Set the database timzone offset.
125
   *
126
   * Setting the db timezone to UTC is done to ensure consistency in date
127
   * handling whether or not the database can do proper timezone conversion.
128
   *
129
   * Views filters that not exposed are cached and won't set the timezone
130
   * so views date filters should add 'cacheable' => 'no' to their
131
   * definitions to ensure that the database timezone gets set properly
132
   * when the query is executed.
133
   *
134
   * @param string $offset
135
   *   An offset value to set the database timezone to. This will only
136
   *   set a fixed offset, not a timezone, so any value other than
137
   *   '+00:00' should be used with caution.
138
   */
139
  function set_db_timezone($offset = '+00:00') {
140
    static $already_set = FALSE;
141
    $type = Database::getConnection()->databaseType();
142
    if (!$already_set) {
143
      switch ($type) {
144
        case 'mysql':
145
          db_query("SET @@session.time_zone = '$offset'");
146
          break;
147
        case 'pgsql':
148
          db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
149
          break;
150
        case 'sqlsrv':
151
          // Issue #1201342, This is the wrong way to set the timezone, this
152
          // still needs to be fixed. In the meantime, commenting this out makes
153
          // SQLSRV functional.
154
          // db_query('TimeZone.setDefault(TimeZone.getTimeZone("GMT"))');
155
          break;
156
      }
157
      $already_set = TRUE;
158
    }
159
  }
160

    
161
  /**
162
   * Return timezone offset for the date being processed.
163
   */
164
  function get_offset($comp_date = NULL) {
165
    if (!empty($this->db_timezone) && !empty($this->local_timezone)) {
166
      if ($this->db_timezone != $this->local_timezone) {
167
        if (empty($comp_date)) {
168
          $comp_date = date_now($this->db_timezone);
169
        }
170
        $comp_date->setTimezone(timezone_open($this->local_timezone));
171
        return date_offset_get($comp_date);
172
      }
173
    }
174
    return 0;
175
  }
176

    
177
  /**
178
   * Helper function to create cross-database SQL dates.
179
   *
180
   * @param string $field
181
   *   The real table and field name, like 'tablename.fieldname' .
182
   * @param string $offset
183
   *   The name of a field that holds the timezone offset or an
184
   *   offset value. If NULL, the normal Drupal timezone handling
185
   *   will be used, if $offset = 0 no adjustment will be made.
186
   *
187
   * @return string
188
   *   An appropriate SQL string for the db type and field type.
189
   */
190
  function sql_field($field, $offset = NULL, $comp_date = NULL) {
191
    if (strtoupper($field) == 'NOW') {
192
      // NOW() will be in UTC since that is what we set the db timezone to.
193
      $this->local_timezone = 'UTC';
194
      return $this->sql_offset('NOW()', $offset);
195
    }
196
    switch ($this->db_type) {
197
      case 'mysql':
198
        switch ($this->date_type) {
199
          case DATE_UNIX:
200
            $field = "FROM_UNIXTIME($field)";
201
            break;
202
          case DATE_ISO:
203
            $field = "STR_TO_DATE($field, '%Y-%m-%dT%T')";
204
            break;
205
          case DATE_DATETIME:
206
            break;
207
        }
208
        break;
209
      case 'pgsql':
210
        switch ($this->date_type) {
211
          case DATE_UNIX:
212
            $field = "$field::ABSTIME";
213
            break;
214
          case DATE_ISO:
215
            $field = "TO_DATE($field, 'FMYYYY-FMMM-FMDDTFMHH24:FMMI:FMSS')";
216
            break;
217
          case DATE_DATETIME:
218
            break;
219
        }
220
        break;
221
      case 'sqlite':
222
        switch ($this->date_type) {
223
          case DATE_UNIX:
224
            $field = "datetime($field, 'unixepoch')";
225
            break;
226
          case DATE_ISO:
227
          case DATE_DATETIME:
228
            $field = "datetime($field)";
229
            break;
230
        }
231
        break;
232
      case 'sqlsrv':
233
        switch ($this->date_type) {
234
          case DATE_UNIX:
235
            $field = "DATEADD(s, $field, '19700101 00:00:00:000')";
236
            break;
237
          case DATE_ISO:
238
          case DATE_DATETIME:
239
            $field = "CAST($field as smalldatetime)";
240
            break;
241
        }
242
        break;
243
      break;
244
    }
245
    // Adjust the resulting value to the right timezone/offset.
246
    return $this->sql_tz($field, $offset, $comp_date);
247
  }
248

    
249
  /**
250
   * Adjust a field value by an offset in seconds.
251
   */
252
  function sql_offset($field, $offset = NULL) {
253
    if (!empty($offset)) {
254
      switch ($this->db_type) {
255
        case 'mysql':
256
          return "ADDTIME($field, SEC_TO_TIME($offset))";
257
        case 'pgsql':
258
          return "($field + INTERVAL '$offset SECONDS')";;
259
        case 'sqlite':
260
          return "datetime($field, '$offset seconds')";
261
        case 'sqlsrv':
262
          return "DATEADD(second, $offset, $field)";
263
      }
264
    }
265
    return $field;
266
  }
267

    
268
  /**
269
   * Adjusts a field value by time interval.
270
   *
271
   * @param string $field
272
   *   The field to be adjusted.
273
   * @param string $direction
274
   *   Either ADD or SUB.
275
   * @param int $count
276
   *   The number of values to adjust.
277
   * @param string $granularity
278
   *   The granularity of the adjustment, should be singular,
279
   *   like SECOND, MINUTE, DAY, HOUR.
280
   */
281
  function sql_date_math($field, $direction, $count, $granularity) {
282
    $granularity = strtoupper($granularity);
283
    switch ($this->db_type) {
284
      case 'mysql':
285
        switch ($direction) {
286
          case 'ADD':
287
            return "DATE_ADD($field, INTERVAL $count $granularity)";
288
          case 'SUB':
289
            return "DATE_SUB($field, INTERVAL $count $granularity)";
290
        }
291

    
292
      case 'pgsql':
293
        $granularity .= 'S';
294
        switch ($direction) {
295
          case 'ADD':
296
            return "($field + INTERVAL '$count $granularity')";
297
          case 'SUB':
298
            return "($field - INTERVAL '$count $granularity')";
299
        }
300
      case 'sqlite':
301
        $granularity .= 'S';
302
        switch ($direction) {
303
          case 'ADD':
304
            return "datetime($field, '+$count $granularity')";
305
          case 'SUB':
306
            return "datetime($field, '-$count $granularity')";
307
        }
308
    }
309
    return $field;
310
  }
311

    
312
  /**
313
   * Select a date value from the database, adjusting the value
314
   * for the timezone.
315
   *
316
   * Check whether database timezone conversion is supported in
317
   * this system and use it if possible, otherwise use an
318
   * offset.
319
   *
320
   * @param string $field
321
   *   The field to be adjusted.
322
   * @param bool $offset
323
   *   Set a fixed offset or offset field to use for the date.
324
   *   If set, no timezone conversion will be done and the
325
   *   offset will be used.
326
   */
327
  function sql_tz($field, $offset = NULL, $comp_date = NULL) {
328
    // If the timezones are values they need to be quoted, but
329
    // if they are field names they do not.
330
    $db_zone   = !empty($this->db_timezone_field) ? $this->db_timezone_field : "'{$this->db_timezone}'";
331
    $localzone = !empty($this->local_timezone_field) ? $this->local_timezone_field : "'{$this->local_timezone}'";
332
    // If a fixed offset is required, use it.
333
    if ($offset !== NULL) {
334
      return $this->sql_offset($field, $offset);
335
    }
336
    // If the db and local timezones are the same, make no adjustment.
337
    elseif ($db_zone == $localzone) {
338
      return $this->sql_offset($field, 0);
339
    }
340
    // If the db has no timezone support, adjust by the offset,
341
    // could be either a field name or a value.
342
    elseif (!$this->db_tz_support() || empty($localzone)) {
343
      if (!empty($this->offset_field)) {
344
        return $this->sql_offset($field, $this->offset_field);
345
      }
346
      else {
347
        return $this->sql_offset($field, $this->get_offset($comp_date));
348
      }
349
    }
350
    // Otherwise make a database timezone adjustment to the field.
351
    else {
352
      switch ($this->db_type) {
353
        case 'mysql':
354
          return "CONVERT_TZ($field, $db_zone, $localzone)";
355
        case 'pgsql':
356
          // WITH TIME ZONE assumes the date is using the system
357
          // timezone, which should have been set to UTC.
358
          return "$field::timestamp with time zone AT TIME ZONE $localzone";
359
      }
360
    }
361
  }
362

    
363
  /**
364
   * Helper function to create cross-database SQL date formatting.
365
   *
366
   * @param string $format
367
   *   A format string for the result, like 'Y-m-d H:i:s' .
368
   * @param string $field
369
   *   The real table and field name, like 'tablename.fieldname' .
370
   *
371
   * @return string
372
   *   An appropriate SQL string for the db type and field type.
373
   */
374
  function sql_format($format, $field) {
375
    switch ($this->db_type) {
376
      case 'mysql':
377
        $replace = array(
378
          'Y' => '%Y',
379
          'y' => '%y',
380
          'M' => '%b',
381
          'm' => '%m',
382
          'n' => '%c',
383
          'F' => '%M',
384
          'D' => '%a',
385
          'd' => '%d',
386
          'l' => '%W',
387
          'j' => '%e',
388
          'W' => '%v',
389
          'H' => '%H',
390
          'h' => '%h',
391
          'i' => '%i',
392
          's' => '%s',
393
          'A' => '%p',
394
          '\WW' => 'W%U',
395
        );
396
        $format = strtr($format, $replace);
397
        return "DATE_FORMAT($field, '$format')";
398
      case 'pgsql':
399
        $replace = array(
400
          'Y' => 'YYYY',
401
          'y' => 'YY',
402
          'M' => 'Mon',
403
          'm' => 'MM',
404
          // No format for Numeric representation of a month, without leading
405
          // zeros.
406
          'n' => 'MM',
407
          'F' => 'Month',
408
          'D' => 'Dy',
409
          'd' => 'DD',
410
          'l' => 'Day',
411
          // No format for Day of the month without leading zeros.
412
          'j' => 'DD',
413
          'W' => 'WW',
414
          'H' => 'HH24',
415
          'h' => 'HH12',
416
          'i' => 'MI',
417
          's' => 'SS',
418
          'A' => 'AM',
419
          '\T' => '"T"',
420
          // '\W' => // TODO, what should this be?
421
        );
422
        $format = strtr($format, $replace);
423
        return "TO_CHAR($field, '$format')";
424
      case 'sqlite':
425
        $replace = array(
426
          // 4 digit year number.
427
          'Y' => '%Y',
428
          // No format for 2 digit year number.
429
          'y' => '%Y',
430
          // No format for 3 letter month name.
431
          'M' => '%m',
432
          // Month number with leading zeros.
433
          'm' => '%m',
434
          // No format for month number without leading zeros.
435
          'n' => '%m',
436
          // No format for full month name.
437
          'F' => '%m',
438
          // No format for 3 letter day name.
439
          'D' => '%d',
440
          // Day of month number with leading zeros.
441
          'd' => '%d',
442
          // No format for full day name.
443
          'l' => '%d',
444
          // No format for day of month number without leading zeros.
445
          'j' => '%d',
446
          // ISO week number.
447
          'W' => '%W',
448
          // 24 hour hour with leading zeros.
449
          'H' => '%H',
450
          // No format for 12 hour hour with leading zeros.
451
          'h' => '%H',
452
          // Minutes with leading zeros.
453
          'i' => '%M',
454
          // Seconds with leading zeros.
455
          's' => '%S',
456
          // No format for AM/PM.
457
          'A' => '',
458
          // Week number.
459
          '\WW' => '',
460
        );
461
        $format = strtr($format, $replace);
462
        return "strftime('$format', $field)";
463
      case 'sqlsrv':
464
        $replace = array(
465
          // 4 digit year number.
466
          'Y' => "' + CAST(DATEPART(year, $field) AS nvarchar) + '",
467
          // 2 digit year number.
468
          'y' => "' + RIGHT(DATEPART(year, $field), 2) + '",
469
          // 3 letter month name.
470
          'M' => "' + LEFT(DATENAME(month, $field), 3) + '",
471
          // Month number with leading zeros.
472
          'm' => "' + RIGHT('0' + CAST(DATEPART(month, $field) AS nvarchar), 2) + '",
473
          // Month number without leading zeros.
474
          'n' => "' + CAST(DATEPART(month, $field) AS nvarchar) + '",
475
          // Full month name.
476
          'F' => "' + DATENAME(month, $field) + '",
477
          // 3 letter day name.
478
          'D' => "' + LEFT(DATENAME(day, $field), 3) + '",
479
          // Day of month number with leading zeros.
480
          'd' => "' + RIGHT('0' + CAST(DATEPART(day, $field) AS nvarchar), 2) + '",
481
          // Full day name.
482
          'l' => "' + DATENAME(day, $field) + '",
483
          // Day of month number without leading zeros.
484
          'j' => "' + CAST(DATEPART(day, $field) AS nvarchar) + '",
485
          // ISO week number.
486
          'W' => "' + CAST(DATEPART(iso_week, $field) AS nvarchar) + '",
487
          // 24 hour with leading zeros.
488
          'H' => "' + RIGHT('0' + CAST(DATEPART(hour, $field) AS nvarchar), 2) + '",
489
          // 12 hour with leading zeros.
490
          // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
491
          // 100 in MSSQL).
492
          // Hour position is fixed, so we use SUBSTRING to extract it.
493
          'h' => "' + RIGHT('0' + LTRIM(SUBSTRING(CONVERT(nvarchar, $field, 100), 13, 2)), 2) + '",
494
          // Minutes with leading zeros.
495
          'i' => "' + RIGHT('0' + CAST(DATEPART(minute, $field) AS nvarchar), 2) + '",
496
          // Seconds with leading zeros.
497
          's' => "' + RIGHT('0' + CAST(DATEPART(second, $field) AS nvarchar), 2) + '",
498
          // AM/PM.
499
          // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
500
          // 100 in MSSQL).
501
          'A' => "' + RIGHT(CONVERT(nvarchar, $field, 100), 2) + '",
502
          // Week number.
503
          '\WW' => "' + CAST(DATEPART(week, $field) AS nvarchar) + '",
504
          '\T' => 'T',
505
          // MS SQL uses single quote as escape symbol.
506
          '\'' => '\'\'',
507
        );
508
        $format = strtr($format, $replace);
509
        $format = "'$format'";
510
        return $format;
511
    }
512
  }
513

    
514
  /**
515
   * Helper function to create cross-database SQL date extraction.
516
   *
517
   * @param string $extract_type
518
   *   The type of value to extract from the date, like 'MONTH'.
519
   * @param string $field
520
   *   The real table and field name, like 'tablename.fieldname'.
521
   *
522
   * @return string
523
   *   An appropriate SQL string for the db type and field type.
524
   */
525
  function sql_extract($extract_type, $field) {
526
    // Note there is no space after FROM to avoid db_rewrite problems
527
    // see http://drupal.org/node/79904.
528
    switch (strtoupper($extract_type)) {
529
      case 'DATE':
530
        return $field;
531
      case 'YEAR':
532
        return "EXTRACT(YEAR FROM($field))";
533
      case 'MONTH':
534
        return "EXTRACT(MONTH FROM($field))";
535
      case 'DAY':
536
        return "EXTRACT(DAY FROM($field))";
537
      case 'HOUR':
538
        return "EXTRACT(HOUR FROM($field))";
539
      case 'MINUTE':
540
        return "EXTRACT(MINUTE FROM($field))";
541
      case 'SECOND':
542
        return "EXTRACT(SECOND FROM($field))";
543
      // ISO week number for date.
544
      case 'WEEK':
545
        switch ($this->db_type) {
546
          case 'mysql':
547
            // WEEK using arg 3 in MySQl should return the same value as
548
            // Postgres EXTRACT.
549
            return "WEEK($field, 3)";
550
          case 'pgsql':
551
            return "EXTRACT(WEEK FROM($field))";
552
        }
553
      case 'DOW':
554
        switch ($this->db_type) {
555
          case 'mysql':
556
            // MySQL returns 1 for Sunday through 7 for Saturday, PHP date
557
            // functions and Postgres use 0 for Sunday and 6 for Saturday.
558
            return "INTEGER(DAYOFWEEK($field) - 1)";
559
          case 'pgsql':
560
            return "EXTRACT(DOW FROM($field))";
561
        }
562
      case 'DOY':
563
        switch ($this->db_type) {
564
          case 'mysql':
565
            return "DAYOFYEAR($field)";
566
          case 'pgsql':
567
            return "EXTRACT(DOY FROM($field))";
568
        }
569
    }
570
  }
571

    
572
  /**
573
   * Creates a where clause to compare a complete date field to a date value.
574
   *
575
   * @param string $type
576
   *   The type of value we're comparing to, could be another field
577
   *   or a date value.
578
   * @param string $field
579
   *   The db table and field name, like "$table.$field".
580
   * @param string $operator
581
   *   The db comparison operator to use, like '='.
582
   * @param int $value
583
   *   The value to compare the extracted date part to, could be a field name or
584
   *   a date string or NOW().
585
   *
586
   * @return string
587
   *   SQL for the where clause for this operation.
588
   */
589
  function sql_where_date($type, $field, $operator, $value, $adjustment = NULL) {
590
    $type = strtoupper($type);
591
    if (strtoupper($value) == 'NOW') {
592
      $value = $this->sql_field('NOW', $adjustment);
593
    }
594
    elseif ($type == 'FIELD') {
595
      $value = $this->sql_field($value, $adjustment);
596
    }
597
    elseif ($type == 'DATE') {
598
      $date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
599
      if (!empty($adjustment)) {
600
        date_modify($date, $adjustment . ' seconds');
601
      }
602
      // When comparing a field to a date we can avoid doing timezone
603
      // conversion by altering the comparison date to the db timezone.
604
      // This won't work if the timezone is a field instead of a value.
605
      if (empty($this->db_timezone_field) && empty($this->local_timezone_field) && $this->db_timezone_field != $this->local_timezone_field) {
606
        $date->setTimezone(timezone_open($this->db_timezone));
607
        $this->local_timezone = $this->db_timezone;
608
      }
609
      $value = "'" . $date->format(DATE_FORMAT_DATETIME, TRUE) . "'";
610
    }
611
    if ($this->local_timezone != $this->db_timezone) {
612
      $field = $this->sql_field($field);
613
    }
614
    else {
615
      $field = $this->sql_field($field, 0);
616
    }
617
    return "$field $operator $value";
618
  }
619

    
620
  /**
621
   * Creates a where clause comparing an extracted date part to an integer.
622
   *
623
   * @param string $part
624
   *   The part to extract, YEAR, MONTH, DAY, etc.
625
   * @param string $field
626
   *   The db table and field name, like "$table.$field".
627
   * @param string $operator
628
   *   The db comparison operator to use, like '=' .
629
   * @param int $value
630
   *   The integer value to compare the extracted date part to.
631
   *
632
   * @return string
633
   *   SQL for the where clause for this operation.
634
   */
635
  function sql_where_extract($part, $field, $operator, $value, $adjustment = NULL) {
636
    if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
637
      $field = $this->sql_field($field);
638
    }
639
    else {
640
      $field = $this->sql_field($field, $adjustment);
641
    }
642
    return $this->sql_extract($part, $field) . " $operator $value";
643
  }
644

    
645
  /**
646
   * Create a where clause to compare a formated field to a formated value.
647
   *
648
   * @param string $format
649
   *   The format to use on the date and the value when comparing them.
650
   * @param string $field
651
   *   The db table and field name, like "$table.$field".
652
   * @param string $operator
653
   *   The db comparison operator to use, like '=' .
654
   * @param string $value
655
   *   The value to compare the extracted date part to, could be a
656
   *   field name or a date string or NOW().
657
   *
658
   * @return string
659
   *   SQL for the where clause for this operation.
660
   */
661
  function sql_where_format($format, $field, $operator, $value, $adjustment = NULL) {
662
    if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
663
      $field = $this->sql_field($field);
664
    }
665
    else {
666
      $field = $this->sql_field($field, $adjustment);
667
    }
668
    return $this->sql_format($format, $field) . " $operator '$value'";
669
  }
670

    
671
  /**
672
   * An array of all date parts,
673
   * optionally limited to an array of allowed parts.
674
   */
675
  function date_parts($limit = NULL) {
676
    $parts = array(
677
      'year' => t('Year', array(), array('context' => 'datetime')),
678
      'month' => t('Month', array(), array('context' => 'datetime')),
679
      'day' => t('Day', array(), array('context' => 'datetime')),
680
      'hour' => t('Hour', array(), array('context' => 'datetime')),
681
      'minute' => t('Minute', array(), array('context' => 'datetime')),
682
      'second' => t('Second', array(), array('context' => 'datetime')),
683
    );
684
    if (!empty($limit)) {
685
      $last = FALSE;
686
      foreach ($parts as $key => $part) {
687
        if ($last) {
688
          unset($parts[$key]);
689
        }
690
        if ($key == $limit) {
691
          $last = TRUE;
692
        }
693
      }
694
    }
695
    return $parts;
696
  }
697

    
698
  /**
699
   * Part information.
700
   *
701
   * @param string $op
702
   *   'min', 'max', 'format', 'sep', 'empty_now', 'empty_min', 'empty_max' .
703
   *   Returns all info if empty.
704
   * @param string $part
705
   *   'year', 'month', 'day', 'hour', 'minute', or 'second.
706
   *   returns info for all parts if empty.
707
   */
708
  function part_info($op = NULL, $part = NULL) {
709
    $info = array();
710
    $info['min'] = array(
711
      'year' => 100,
712
      'month' => 1,
713
      'day' => 1,
714
      'hour' => 0,
715
      'minute' => 0,
716
      'second' => 0,
717
    );
718
    $info['max'] = array(
719
      'year' => 4000,
720
      'month' => 12,
721
      'day' => 31,
722
      'hour' => 23,
723
      'minute' => 59,
724
      'second' => 59,
725
    );
726
    $info['format'] = array(
727
      'year' => 'Y',
728
      'month' => 'm',
729
      'day' => 'd',
730
      'hour' => 'H',
731
      'minute' => 'i',
732
      'second' => 's',
733
    );
734
    $info['sep'] = array(
735
      'year' => '',
736
      'month' => '-',
737
      'day' => '-',
738
      'hour' => ' ',
739
      'minute' => ':',
740
      'second' => ':',
741
    );
742
    $info['empty_now'] = array(
743
      'year' => date('Y'),
744
      'month' => date('m'),
745
      'day' => min('28', date('d')),
746
      'hour' => date('H'),
747
      'minute' => date('i'),
748
      'second' => date('s'),
749
    );
750
    $info['empty_min'] = array(
751
      'year' => '1000',
752
      'month' => '01',
753
      'day' => '01',
754
      'hour' => '00',
755
      'minute' => '00',
756
      'second' => '00',
757
    );
758
    $info['empty_max'] = array(
759
      'year' => '9999',
760
      'month' => '12',
761
      'day' => '31',
762
      'hour' => '23',
763
      'minute' => '59',
764
      'second' => '59',
765
    );
766
    if (!empty($op)) {
767
      if (!empty($part)) {
768
        return $info[$op][$part];
769
      }
770
      else {
771
        return $info[$op];
772
      }
773
    }
774
    return $info;
775
  }
776

    
777
  /**
778
   * Create a complete datetime value out of an
779
   * incomplete array of selected values.
780
   *
781
   * For example, array('year' => 2008, 'month' => 05) will fill
782
   * in the day, hour, minute and second with the earliest possible
783
   * values if type = 'min', the latest possible values if type = 'max',
784
   * and the current values if type = 'now' .
785
   */
786
  function complete_date($selected, $type = 'now') {
787
    if (empty($selected)) {
788
      return '';
789
    }
790
    // Special case for weeks.
791
    if (array_key_exists('week', $selected)) {
792
      $dates = date_week_range($selected['week'], $selected['year']);
793
      switch ($type) {
794
        case 'empty_now':
795
        case 'empty_min':
796
        case 'min':
797
          return date_format($dates[0], 'Y-m-d H:i:s');
798
        case 'empty_max':
799
        case 'max':
800
          return date_format($dates[1], 'Y-m-d H:i:s');
801
        default:
802
          return;
803
      }
804
    }
805

    
806
    $compare = array_merge($this->part_info('empty_' . $type), $selected);
807
    // If this is a max date, make sure the last day of
808
    // the month is the right one for this date.
809
    if ($type == 'max') {
810
      $compare['day'] = date_days_in_month($compare['year'], $compare['month']);
811
    }
812
    $value = '';
813
    $separators = $this->part_info('sep');
814
    foreach ($this->date_parts() as $key => $name) {
815
      $value .= $separators[$key] . (!empty($selected[$key]) ? $selected[$key] : $compare[$key]);
816
    }
817
    return $value;
818
  }
819

    
820
  /**
821
   * Converts a format string into help text, i.e. 'Y-m-d' becomes 'YYYY-MM-DD'.
822
   *
823
   * @param string $format
824
   *   A date format string.
825
   *
826
   * @return string
827
   *   The conveted help text.
828
   */
829
  function format_help($format) {
830
    $replace = array(
831
      'Y' => 'YYYY',
832
      'm' => 'MM',
833
      'd' => 'DD',
834
      'H' => 'HH',
835
      'i' => 'MM',
836
      's' => 'SS',
837
      '\T' => 'T',
838
    );
839
    return strtr($format, $replace);
840
  }
841

    
842
  /**
843
   *  A function to test the validity of various date parts
844
   */
845
  function part_is_valid($value, $type) {
846
    if (!preg_match('/^[0-9]*$/', $value)) {
847
      return FALSE;
848
    }
849
    $value = intval($value);
850
    if ($value <= 0) {
851
      return FALSE;
852
    }
853
    switch ($type) {
854
      case 'year':
855
        if ($value < DATE_MIN_YEAR) {
856
          return FALSE;
857
        }
858
        break;
859
      case 'month':
860
        if ($value < 0 || $value > 12) {
861
          return FALSE;
862
        }
863
        break;
864
      case 'day':
865
        if ($value < 0 || $value > 31) {
866
          return FALSE;
867
        }
868
        break;
869
      case 'week':
870
        if ($value < 0 || $value > 53) {
871
          return FALSE;
872
        }
873
        break;
874
    }
875
    return TRUE;
876
  }
877

    
878
  /**
879
   * @todo.
880
   */
881
  function views_formats($granularity, $type = 'sql') {
882
    if (empty($granularity)) {
883
      return DATE_FORMAT_ISO;
884
    }
885
    $formats = array('display', 'sql');
886
    // Start with the site long date format and add seconds to it.
887
    $short = str_replace(':i', ':i:s', variable_get('date_format_short', 'l, F j, Y - H:i'));
888
    switch ($granularity) {
889
      case 'year':
890
        $formats['display'] = 'Y';
891
        $formats['sql'] = 'Y';
892
        break;
893
      case 'month':
894
        $formats['display'] = date_limit_format($short, array('year', 'month'));
895
        $formats['sql'] = 'Y-m';
896
        break;
897
      case 'day':
898
        $formats['display'] = date_limit_format($short, array('year', 'month', 'day'));
899
        $formats['sql'] = 'Y-m-d';
900
        break;
901
      case 'hour':
902
        $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour'));
903
        $formats['sql'] = 'Y-m-d\TH';
904
        break;
905
      case 'minute':
906
        $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour', 'minute'));
907
        $formats['sql'] = 'Y-m-d\TH:i';
908
        break;
909
      case 'second':
910
        $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour', 'minute', 'second'));
911
        $formats['sql'] = 'Y-m-d\TH:i:s';
912
        break;
913
      case 'week':
914
        $formats['display'] = 'F j Y (W)';
915
        $formats['sql'] = 'Y-\WW';
916
        break;
917
    }
918
    return $formats[$type];
919
  }
920

    
921
  /**
922
   * @todo.
923
   */
924
  function granularity_form($granularity) {
925
    $form = array(
926
      '#title' => t('Granularity'),
927
      '#type' => 'radios',
928
      '#default_value' => $granularity,
929
      '#options' => $this->date_parts(),
930
      );
931
    return $form;
932
  }
933

    
934
  /**
935
   * Parse date parts from an ISO date argument.
936
   *
937
   * Based on ISO 8601 date duration and time interval standards.
938
   *
939
   * Parses a value like 2006-01-01--2006-01-15, or 2006-W24, or @P1W.
940
   * Separate start and end dates or date and period with a double hyphen (--).
941
   *
942
   * The 'end' portion of the argument can be eliminated if it is the same as
943
   * the 'start' portion. Use @ instead of a date to substitute in the current
944
   * date and time.
945
   *
946
   * Use periods (P1H, P1D, P1W, P1M, P1Y) to get next hour/day/week/month/year
947
   * from now. Use date before P sign to get next hour/day/week/month/year from
948
   * that date. Use period then date to get a period that ends on the date.
949
   *
950
   * @see http://en.wikipedia.org/wiki/ISO_8601#Week_dates
951
   * @see http://en.wikipedia.org/wiki/ISO_8601#Duration
952
   */
953
  function arg_parts($argument) {
954
    $values = array();
955
    // Keep mal-formed arguments from creating errors.
956
    if (empty($argument) || is_array($argument)) {
957
      return array('date' => array(), 'period' => array());
958
    }
959
    $fromto = explode('--', $argument);
960
    foreach ($fromto as $arg) {
961
      $parts = array();
962
      if ($arg == '@') {
963
        $date = date_now();
964
        $parts['date'] = $date->toArray();
965
      }
966
      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)) {
967
        $date = array();
968
        if (!empty($matches[1])) {
969
          $date['year'] = $matches[1];
970
        }
971
        if (!empty($matches[3])) {
972
          if (empty($matches[2])) {
973
            $date['month'] = $matches[3];
974
          }
975
          else {
976
            $date['week'] = $matches[3];
977
          }
978
        }
979
        if (!empty($matches[4])) {
980
          $date['day'] = $matches[4];
981
        }
982
        if (!empty($matches[5])) {
983
          $date['hour'] = $matches[5];
984
        }
985
        if (!empty($matches[6])) {
986
          $date['minute'] = $matches[6];
987
        }
988
        if (!empty($matches[7])) {
989
          $date['second'] = $matches[7];
990
        }
991
        $parts['date'] = $date;
992
      }
993
      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)) {
994
        $period = array();
995
        if (!empty($matches[1])) {
996
          $period['year'] = str_replace('Y', '', $matches[1]);
997
        }
998
        if (!empty($matches[2])) {
999
          $period['month'] = str_replace('M', '', $matches[2]);
1000
        }
1001
        if (!empty($matches[3])) {
1002
          $period['week'] = str_replace('W', '', $matches[3]);
1003
        }
1004
        if (!empty($matches[4])) {
1005
          $period['day'] = str_replace('D', '', $matches[4]);
1006
        }
1007
        if (!empty($matches[6])) {
1008
          $period['hour'] = str_replace('H', '', $matches[6]);
1009
        }
1010
        if (!empty($matches[7])) {
1011
          $period['minute'] = str_replace('M', '', $matches[7]);
1012
        }
1013
        if (!empty($matches[8])) {
1014
          $period['second'] = str_replace('S', '', $matches[8]);
1015
        }
1016
        $parts['period'] = $period;
1017
      }
1018
      $values[] = $parts;
1019
    }
1020
    return $values;
1021
  }
1022

    
1023
  /**
1024
   * Convert strings like '+1 day' to the ISO equivalent, like 'P1D' .
1025
   */
1026
  function arg_replace($arg) {
1027
    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)) {
1028
      return str_replace('now', '@', $arg);
1029
    }
1030
    $direction = $results[1];
1031
    $count = $results[2];
1032
    $item = $results[3];
1033

    
1034
    $replace = array(
1035
      'now' => '@',
1036
      '+' => 'P',
1037
      '-' => 'P-',
1038
      'years' => 'Y',
1039
      'year' => 'Y',
1040
      'months' => 'M',
1041
      'month' => 'M',
1042
      'weeks' => 'W',
1043
      'week' => 'W',
1044
      'days' => 'D',
1045
      'day' => 'D',
1046
      'hours' => 'H',
1047
      'hour' => 'H',
1048
      'minutes' => 'M',
1049
      'minute' => 'M',
1050
      'seconds' => 'S',
1051
      'second' => 'S',
1052
      '  ' => '',
1053
      ' ' => '',
1054
      );
1055
    $prefix = in_array($item, array('hours', 'hour', 'minutes', 'minute', 'seconds', 'second')) ? 'T' : '';
1056
    return $prefix . strtr($direction, $replace) . $count . strtr($item, $replace);
1057
  }
1058

    
1059
  /**
1060
   * Use the parsed values from the ISO argument to determine the
1061
   * granularity of this period.
1062
   */
1063
  function arg_granularity($arg) {
1064
    $granularity = '';
1065
    $parts = $this->arg_parts($arg);
1066
    $date = !empty($parts[0]['date']) ? $parts[0]['date'] : (!empty($parts[1]['date']) ? $parts[1]['date'] : array());
1067
    foreach ($date as $key => $part) {
1068
      $granularity = $key;
1069
    }
1070
    return $granularity;
1071
  }
1072

    
1073
  /**
1074
   * Use the parsed values from the ISO argument to determine the
1075
   * min and max date for this period.
1076
   */
1077
  function arg_range($arg) {
1078
    // Parse the argument to get its parts.
1079
    $parts = $this->arg_parts($arg);
1080

    
1081
    // Build a range from a period-only argument (assumes the min date is now.)
1082
    if (empty($parts[0]['date']) && !empty($parts[0]['period']) && (empty($parts[1]))) {
1083
      $min_date = date_now();
1084
      $max_date = clone($min_date);
1085
      foreach ($parts[0]['period'] as $part => $value) {
1086
        date_modify($max_date, "+$value $part");
1087
      }
1088
      date_modify($max_date, '-1 second');
1089
      return array($min_date, $max_date);
1090
    }
1091
    // Build a range from a period to period argument.
1092
    if (empty($parts[0]['date']) && !empty($parts[0]['period']) && !empty($parts[1]['period'])) {
1093
      $min_date = date_now();
1094
      $max_date = clone($min_date);
1095
      foreach ($parts[0]['period'] as $part => $value) {
1096
        date_modify($min_date, "+$value $part");
1097
      }
1098
      date_modify($min_date, '-1 second');
1099
      foreach ($parts[1]['period'] as $part => $value) {
1100
        date_modify($max_date, "+$value $part");
1101
      }
1102
      date_modify($max_date, '-1 second');
1103
      return array($min_date, $max_date);
1104
    }
1105
    if (!empty($parts[0]['date'])) {
1106
      $value = $this->complete_date($parts[0]['date'], 'min');
1107
      $min_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1108
      // Build a range from a single date-only argument.
1109
      if (empty($parts[1]) || (empty($parts[1]['date']) && empty($parts[1]['period']))) {
1110
        $value = $this->complete_date($parts[0]['date'], 'max');
1111
        $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1112
        return array($min_date, $max_date);
1113
      }
1114
      // Build a range from start date + period.
1115
      elseif (!empty($parts[1]['period'])) {
1116
        foreach ($parts[1]['period'] as $part => $value) {
1117
          $max_date = clone($min_date);
1118
          date_modify($max_date, "+$value $part");
1119
        }
1120
        date_modify($max_date, '-1 second');
1121
        return array($min_date, $max_date);
1122
      }
1123
    }
1124
    // Build a range from start date and end date.
1125
    if (!empty($parts[1]['date'])) {
1126
      $value = $this->complete_date($parts[1]['date'], 'max');
1127
      $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1128
      if (isset($min_date)) {
1129
        return array($min_date, $max_date);
1130
      }
1131
    }
1132
    // Build a range from period + end date.
1133
    if (!empty($parts[0]['period'])) {
1134
      $min_date = date_now();
1135
      foreach ($parts[0]['period'] as $part => $value) {
1136
        date_modify($min_date, "$value $part");
1137
      }
1138
      return array($min_date, $max_date);
1139
    }
1140
     // Intercept invalid info and fall back to the current date.
1141
    $now = date_now();
1142
    return array($now, $now);
1143
  }
1144
}