Projet

Général

Profil

Paste
Télécharger (36,2 ko) Statistiques
| Branche: | Révision:

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

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

    
27
    case 'pgsql':
28
      return implode(" || ", $array);
29
  }
30
}
31

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

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

    
66
    default:
67
      return "LPAD($str, $size, '$pad')";
68
  }
69
}
70

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

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

    
100
  /**
101
   * See if the db has timezone name support.
102
   */
103
  public function db_tz_support($reset = FALSE) {
104
    $has_support = variable_get('date_db_tz_support', -1);
105
    if ($has_support == -1 || $reset) {
106
      $has_support = FALSE;
107
      switch ($this->db_type) {
108
        case 'mysql':
109
          $test = db_query("SELECT CONVERT_TZ('2008-02-15 12:00:00', 'UTC', 'US/Central')")->fetchField();
110
          if ($test == '2008-02-15 06:00:00') {
111
            $has_support = TRUE;
112
          }
113
          break;
114

    
115
        case 'pgsql':
116
          $test = db_query("SELECT '2008-02-15 12:00:00 UTC' AT TIME ZONE 'US/Central'")->fetchField();
117
          if ($test == '2008-02-15 06:00:00') {
118
            $has_support = TRUE;
119
          }
120
          break;
121
      }
122
      variable_set('date_db_tz_support', $has_support);
123
    }
124
    return $has_support;
125
  }
126

    
127
  /**
128
   * Set the database timzone offset.
129
   *
130
   * Setting the db timezone to UTC is done to ensure consistency in date
131
   * handling whether or not the database can do proper timezone conversion.
132
   *
133
   * Views filters that not exposed are cached and won't set the timezone
134
   * so views date filters should add 'cacheable' => 'no' to their
135
   * definitions to ensure that the database timezone gets set properly
136
   * when the query is executed.
137
   *
138
   * @param string $offset
139
   *   An offset value to set the database timezone to. This will only
140
   *   set a fixed offset, not a timezone, so any value other than
141
   *   '+00:00' should be used with caution.
142
   */
143
  public function set_db_timezone($offset = '+00:00') {
144
    static $already_set = FALSE;
145
    $type = Database::getConnection()->databaseType();
146
    if (!$already_set) {
147
      switch ($type) {
148
        case 'mysql':
149
          db_query("SET @@session.time_zone = '$offset'");
150
          break;
151

    
152
        case 'pgsql':
153
          db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
154
          break;
155

    
156
        case 'sqlsrv':
157
          // Issue #1201342, This is the wrong way to set the timezone, this
158
          // still needs to be fixed. In the meantime, commenting this out makes
159
          // SQLSRV functional.
160
          // db_query('TimeZone.setDefault(TimeZone.getTimeZone("GMT"))');
161
          break;
162
      }
163
      $already_set = TRUE;
164
    }
165
  }
166

    
167
  /**
168
   * Return timezone offset for the date being processed.
169
   */
170
  public function get_offset($comp_date = NULL) {
171
    if (!empty($this->db_timezone) && !empty($this->local_timezone)) {
172
      if ($this->db_timezone != $this->local_timezone) {
173
        if (empty($comp_date)) {
174
          $comp_date = date_now($this->db_timezone);
175
        }
176
        $comp_date->setTimezone(timezone_open($this->local_timezone));
177
        return date_offset_get($comp_date);
178
      }
179
    }
180
    return 0;
181
  }
182

    
183
  /**
184
   * Helper function to create cross-database SQL dates.
185
   *
186
   * @param string $field
187
   *   The real table and field name, like 'tablename.fieldname' .
188
   * @param string $offset
189
   *   The name of a field that holds the timezone offset or an
190
   *   offset value. If NULL, the normal Drupal timezone handling
191
   *   will be used, if $offset = 0 no adjustment will be made.
192
   *
193
   * @return string
194
   *   An appropriate SQL string for the db type and field type.
195
   */
196
  function sql_field($field, $offset = NULL, $comp_date = NULL) {
197
    if (strtoupper($field) == 'NOW') {
198
      // NOW() will be in UTC since that is what we set the db timezone to.
199
      $this->local_timezone = 'UTC';
200
      return $this->sql_offset('NOW()', $offset);
201
    }
202
    switch ($this->db_type) {
203
      case 'mysql':
204
        switch ($this->date_type) {
205
          case DATE_UNIX:
206
            $field = "FROM_UNIXTIME($field)";
207
            break;
208

    
209
          case DATE_ISO:
210
            $field = "STR_TO_DATE($field, '%Y-%m-%dT%T')";
211
            break;
212

    
213
          case DATE_DATETIME:
214
            break;
215
        }
216
        break;
217

    
218
      case 'pgsql':
219
        switch ($this->date_type) {
220
          case DATE_UNIX:
221
            $field = "$field::ABSTIME";
222
            break;
223

    
224
          case DATE_ISO:
225
            $field = "TO_DATE($field, 'FMYYYY-FMMM-FMDDTFMHH24:FMMI:FMSS')";
226
            break;
227

    
228
          case DATE_DATETIME:
229
            break;
230
        }
231
        break;
232

    
233
      case 'sqlite':
234
        switch ($this->date_type) {
235
          case DATE_UNIX:
236
            $field = "datetime($field, 'unixepoch')";
237
            break;
238

    
239
          case DATE_ISO:
240
          case DATE_DATETIME:
241
            $field = "datetime($field)";
242
            break;
243
        }
244
        break;
245

    
246
      case 'sqlsrv':
247
        switch ($this->date_type) {
248
          case DATE_UNIX:
249
            $field = "DATEADD(s, $field, '19700101 00:00:00:000')";
250
            break;
251

    
252
          case DATE_ISO:
253
          case DATE_DATETIME:
254
            $field = "CAST($field as smalldatetime)";
255
            break;
256
        }
257
        break;
258

    
259
      break;
260
    }
261
    // Adjust the resulting value to the right timezone/offset.
262
    return $this->sql_tz($field, $offset, $comp_date);
263
  }
264

    
265
  /**
266
   * Adjust a field value by an offset in seconds.
267
   */
268
  function sql_offset($field, $offset = NULL) {
269
    if (!empty($offset)) {
270
      switch ($this->db_type) {
271
        case 'mysql':
272
          return "ADDTIME($field, SEC_TO_TIME($offset))";
273

    
274
        case 'pgsql':
275
          return "($field + INTERVAL '$offset SECONDS')";
276

    
277
        case 'sqlite':
278
          return "datetime($field, '$offset seconds')";
279

    
280
        case 'sqlsrv':
281
          return "DATEADD(second, $offset, $field)";
282
      }
283
    }
284
    return $field;
285
  }
286

    
287
  /**
288
   * Adjusts a field value by time interval.
289
   *
290
   * @param string $field
291
   *   The field to be adjusted.
292
   * @param string $direction
293
   *   Either ADD or SUB.
294
   * @param int $count
295
   *   The number of values to adjust.
296
   * @param string $granularity
297
   *   The granularity of the adjustment, should be singular,
298
   *   like SECOND, MINUTE, DAY, HOUR.
299
   */
300
  function sql_date_math($field, $direction, $count, $granularity) {
301
    $granularity = strtoupper($granularity);
302
    switch ($this->db_type) {
303
      case 'mysql':
304
        switch ($direction) {
305
          case 'ADD':
306
            return "DATE_ADD($field, INTERVAL $count $granularity)";
307

    
308
          case 'SUB':
309
            return "DATE_SUB($field, INTERVAL $count $granularity)";
310
        }
311

    
312
      case 'pgsql':
313
        $granularity .= 'S';
314
        switch ($direction) {
315
          case 'ADD':
316
            return "($field + INTERVAL '$count $granularity')";
317

    
318
          case 'SUB':
319
            return "($field - INTERVAL '$count $granularity')";
320
        }
321
      case 'sqlite':
322
        $granularity .= 'S';
323
        switch ($direction) {
324
          case 'ADD':
325
            return "datetime($field, '+$count $granularity')";
326

    
327
          case 'SUB':
328
            return "datetime($field, '-$count $granularity')";
329
        }
330
    }
331
    return $field;
332
  }
333

    
334
  /**
335
   * Select a date value from the database, adjusting the value
336
   * for the timezone.
337
   *
338
   * Check whether database timezone conversion is supported in
339
   * this system and use it if possible, otherwise use an
340
   * offset.
341
   *
342
   * @param string $field
343
   *   The field to be adjusted.
344
   * @param bool $offset
345
   *   Set a fixed offset or offset field to use for the date.
346
   *   If set, no timezone conversion will be done and the
347
   *   offset will be used.
348
   */
349
  function sql_tz($field, $offset = NULL, $comp_date = NULL) {
350
    // If the timezones are values they need to be quoted, but
351
    // if they are field names they do not.
352
    $db_zone   = !empty($this->db_timezone_field) ? $this->db_timezone_field : "'{$this->db_timezone}'";
353
    $localzone = !empty($this->local_timezone_field) ? $this->local_timezone_field : "'{$this->local_timezone}'";
354
    // If a fixed offset is required, use it.
355
    if ($offset !== NULL) {
356
      return $this->sql_offset($field, $offset);
357
    }
358
    // If the db and local timezones are the same, make no adjustment.
359
    elseif ($db_zone == $localzone) {
360
      return $this->sql_offset($field, 0);
361
    }
362
    // If the db has no timezone support, adjust by the offset,
363
    // could be either a field name or a value.
364
    elseif (!$this->db_tz_support() || empty($localzone)) {
365
      if (!empty($this->offset_field)) {
366
        return $this->sql_offset($field, $this->offset_field);
367
      }
368
      else {
369
        return $this->sql_offset($field, $this->get_offset($comp_date));
370
      }
371
    }
372
    // Otherwise make a database timezone adjustment to the field.
373
    else {
374
      switch ($this->db_type) {
375
        case 'mysql':
376
          return "CONVERT_TZ($field, $db_zone, $localzone)";
377

    
378
        case 'pgsql':
379
          // WITH TIME ZONE assumes the date is using the system
380
          // timezone, which should have been set to UTC.
381
          return "$field::timestamp with time zone AT TIME ZONE $localzone";
382
      }
383
    }
384
  }
385

    
386
  /**
387
   * Helper function to create cross-database SQL date formatting.
388
   *
389
   * @param string $format
390
   *   A format string for the result, like 'Y-m-d H:i:s' .
391
   * @param string $field
392
   *   The real table and field name, like 'tablename.fieldname' .
393
   *
394
   * @return string
395
   *   An appropriate SQL string for the db type and field type.
396
   */
397
  function sql_format($format, $field) {
398
    switch ($this->db_type) {
399
      case 'mysql':
400
        $replace = array(
401
          'Y' => '%Y',
402
          'y' => '%y',
403
          'M' => '%b',
404
          'm' => '%m',
405
          'n' => '%c',
406
          'F' => '%M',
407
          'D' => '%a',
408
          'd' => '%d',
409
          'l' => '%W',
410
          'j' => '%e',
411
          'W' => '%v',
412
          'H' => '%H',
413
          'h' => '%h',
414
          'i' => '%i',
415
          's' => '%s',
416
          'A' => '%p',
417
          '\WW' => 'W%U',
418
        );
419
        $format = strtr($format, $replace);
420
        return "DATE_FORMAT($field, '$format')";
421

    
422
      case 'pgsql':
423
        $replace = array(
424
          'Y' => 'YYYY',
425
          'y' => 'YY',
426
          'M' => 'Mon',
427
          'm' => 'MM',
428
          // No format for Numeric representation of a month, without leading
429
          // zeros.
430
          'n' => 'MM',
431
          'F' => 'Month',
432
          'D' => 'Dy',
433
          'd' => 'DD',
434
          'l' => 'Day',
435
          // No format for Day of the month without leading zeros.
436
          'j' => 'DD',
437
          'W' => 'WW',
438
          'H' => 'HH24',
439
          'h' => 'HH12',
440
          'i' => 'MI',
441
          's' => 'SS',
442
          'A' => 'AM',
443
          '\T' => '"T"',
444
          // '\W' => // TODO, what should this be?
445
        );
446
        $format = strtr($format, $replace);
447
        return "TO_CHAR($field, '$format')";
448

    
449
      case 'sqlite':
450
        $replace = array(
451
          // 4 digit year number.
452
          'Y' => '%Y',
453
          // No format for 2 digit year number.
454
          'y' => '%Y',
455
          // No format for 3 letter month name.
456
          'M' => '%m',
457
          // Month number with leading zeros.
458
          'm' => '%m',
459
          // No format for month number without leading zeros.
460
          'n' => '%m',
461
          // No format for full month name.
462
          'F' => '%m',
463
          // No format for 3 letter day name.
464
          'D' => '%d',
465
          // Day of month number with leading zeros.
466
          'd' => '%d',
467
          // No format for full day name.
468
          'l' => '%d',
469
          // No format for day of month number without leading zeros.
470
          'j' => '%d',
471
          // ISO week number.
472
          'W' => '%W',
473
          // 24 hour hour with leading zeros.
474
          'H' => '%H',
475
          // No format for 12 hour hour with leading zeros.
476
          'h' => '%H',
477
          // Minutes with leading zeros.
478
          'i' => '%M',
479
          // Seconds with leading zeros.
480
          's' => '%S',
481
          // No format for AM/PM.
482
          'A' => '',
483
          // Week number.
484
          '\WW' => '',
485
        );
486
        $format = strtr($format, $replace);
487
        return "strftime('$format', $field)";
488

    
489
      case 'sqlsrv':
490
        $replace = array(
491
          // 4 digit year number.
492
          'Y' => "' + CAST(DATEPART(year, $field) AS nvarchar) + '",
493
          // 2 digit year number.
494
          'y' => "' + RIGHT(DATEPART(year, $field), 2) + '",
495
          // 3 letter month name.
496
          'M' => "' + LEFT(DATENAME(month, $field), 3) + '",
497
          // Month number with leading zeros.
498
          'm' => "' + RIGHT('0' + CAST(DATEPART(month, $field) AS nvarchar), 2) + '",
499
          // Month number without leading zeros.
500
          'n' => "' + CAST(DATEPART(month, $field) AS nvarchar) + '",
501
          // Full month name.
502
          'F' => "' + DATENAME(month, $field) + '",
503
          // 3 letter day name.
504
          'D' => "' + LEFT(DATENAME(day, $field), 3) + '",
505
          // Day of month number with leading zeros.
506
          'd' => "' + RIGHT('0' + CAST(DATEPART(day, $field) AS nvarchar), 2) + '",
507
          // Full day name.
508
          'l' => "' + DATENAME(day, $field) + '",
509
          // Day of month number without leading zeros.
510
          'j' => "' + CAST(DATEPART(day, $field) AS nvarchar) + '",
511
          // ISO week number.
512
          'W' => "' + CAST(DATEPART(iso_week, $field) AS nvarchar) + '",
513
          // 24 hour with leading zeros.
514
          'H' => "' + RIGHT('0' + CAST(DATEPART(hour, $field) AS nvarchar), 2) + '",
515
          // 12 hour with leading zeros.
516
          // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
517
          // 100 in MSSQL).
518
          // Hour position is fixed, so we use SUBSTRING to extract it.
519
          'h' => "' + RIGHT('0' + LTRIM(SUBSTRING(CONVERT(nvarchar, $field, 100), 13, 2)), 2) + '",
520
          // Minutes with leading zeros.
521
          'i' => "' + RIGHT('0' + CAST(DATEPART(minute, $field) AS nvarchar), 2) + '",
522
          // Seconds with leading zeros.
523
          's' => "' + RIGHT('0' + CAST(DATEPART(second, $field) AS nvarchar), 2) + '",
524
          // AM/PM.
525
          // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
526
          // 100 in MSSQL).
527
          'A' => "' + RIGHT(CONVERT(nvarchar, $field, 100), 2) + '",
528
          // Week number.
529
          '\WW' => "' + CAST(DATEPART(week, $field) AS nvarchar) + '",
530
          '\T' => 'T',
531
          // MS SQL uses single quote as escape symbol.
532
          '\'' => '\'\'',
533
        );
534
        $format = strtr($format, $replace);
535
        $format = "'$format'";
536
        return $format;
537
    }
538
  }
539

    
540
  /**
541
   * Helper function to create cross-database SQL date extraction.
542
   *
543
   * @param string $extract_type
544
   *   The type of value to extract from the date, like 'MONTH'.
545
   * @param string $field
546
   *   The real table and field name, like 'tablename.fieldname'.
547
   *
548
   * @return string
549
   *   An appropriate SQL string for the db type and field type.
550
   */
551
  function sql_extract($extract_type, $field) {
552
    // Note there is no space after FROM to avoid db_rewrite problems
553
    // see http://drupal.org/node/79904.
554
    switch (strtoupper($extract_type)) {
555
      case 'DATE':
556
        return $field;
557

    
558
      case 'YEAR':
559
        return "EXTRACT(YEAR FROM($field))";
560

    
561
      case 'MONTH':
562
        return "EXTRACT(MONTH FROM($field))";
563

    
564
      case 'DAY':
565
        return "EXTRACT(DAY FROM($field))";
566

    
567
      case 'HOUR':
568
        return "EXTRACT(HOUR FROM($field))";
569

    
570
      case 'MINUTE':
571
        return "EXTRACT(MINUTE FROM($field))";
572

    
573
      case 'SECOND':
574
        return "EXTRACT(SECOND FROM($field))";
575

    
576
      // ISO week number for date.
577
      case 'WEEK':
578
        switch ($this->db_type) {
579
          case 'mysql':
580
            // WEEK using arg 3 in MySQl should return the same value as
581
            // Postgres EXTRACT.
582
            return "WEEK($field, 3)";
583

    
584
          case 'pgsql':
585
            return "EXTRACT(WEEK FROM($field))";
586
        }
587
      case 'DOW':
588
        switch ($this->db_type) {
589
          case 'mysql':
590
            // MySQL returns 1 for Sunday through 7 for Saturday, PHP date
591
            // functions and Postgres use 0 for Sunday and 6 for Saturday.
592
            return "INTEGER(DAYOFWEEK($field) - 1)";
593

    
594
          case 'pgsql':
595
            return "EXTRACT(DOW FROM($field))";
596
        }
597
      case 'DOY':
598
        switch ($this->db_type) {
599
          case 'mysql':
600
            return "DAYOFYEAR($field)";
601

    
602
          case 'pgsql':
603
            return "EXTRACT(DOY FROM($field))";
604
        }
605
    }
606
  }
607

    
608
  /**
609
   * Creates a where clause to compare a complete date field to a date value.
610
   *
611
   * @param string $type
612
   *   The type of value we're comparing to, could be another field
613
   *   or a date value.
614
   * @param string $field
615
   *   The db table and field name, like "$table.$field".
616
   * @param string $operator
617
   *   The db comparison operator to use, like '='.
618
   * @param int $value
619
   *   The value to compare the extracted date part to, could be a field name or
620
   *   a date string or NOW().
621
   *
622
   * @return string
623
   *   SQL for the where clause for this operation.
624
   */
625
  function sql_where_date($type, $field, $operator, $value, $adjustment = NULL) {
626
    $type = strtoupper($type);
627
    if (strtoupper($value) == 'NOW') {
628
      $value = $this->sql_field('NOW', $adjustment);
629
    }
630
    elseif ($type == 'FIELD') {
631
      $value = $this->sql_field($value, $adjustment);
632
    }
633
    elseif ($type == 'DATE') {
634
      $date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
635
      if (!empty($adjustment)) {
636
        date_modify($date, $adjustment . ' seconds');
637
      }
638
      // When comparing a field to a date we can avoid doing timezone
639
      // conversion by altering the comparison date to the db timezone.
640
      // This won't work if the timezone is a field instead of a value.
641
      if (empty($this->db_timezone_field) && empty($this->local_timezone_field) && $this->db_timezone_field != $this->local_timezone_field) {
642
        $date->setTimezone(timezone_open($this->db_timezone));
643
        $this->local_timezone = $this->db_timezone;
644
      }
645
      $value = "'" . $date->format(DATE_FORMAT_DATETIME, TRUE) . "'";
646
    }
647
    if ($this->local_timezone != $this->db_timezone) {
648
      $field = $this->sql_field($field);
649
    }
650
    else {
651
      $field = $this->sql_field($field, 0);
652
    }
653
    return "$field $operator $value";
654
  }
655

    
656
  /**
657
   * Creates a where clause comparing an extracted date part to an integer.
658
   *
659
   * @param string $part
660
   *   The part to extract, YEAR, MONTH, DAY, etc.
661
   * @param string $field
662
   *   The db table and field name, like "$table.$field".
663
   * @param string $operator
664
   *   The db comparison operator to use, like '=' .
665
   * @param int $value
666
   *   The integer value to compare the extracted date part to.
667
   *
668
   * @return string
669
   *   SQL for the where clause for this operation.
670
   */
671
  function sql_where_extract($part, $field, $operator, $value, $adjustment = NULL) {
672
    if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
673
      $field = $this->sql_field($field);
674
    }
675
    else {
676
      $field = $this->sql_field($field, $adjustment);
677
    }
678
    return $this->sql_extract($part, $field) . " $operator $value";
679
  }
680

    
681
  /**
682
   * Create a where clause to compare a formated field to a formated value.
683
   *
684
   * @param string $format
685
   *   The format to use on the date and the value when comparing them.
686
   * @param string $field
687
   *   The db table and field name, like "$table.$field".
688
   * @param string $operator
689
   *   The db comparison operator to use, like '=' .
690
   * @param string $value
691
   *   The value to compare the extracted date part to, could be a
692
   *   field name or a date string or NOW().
693
   *
694
   * @return string
695
   *   SQL for the where clause for this operation.
696
   */
697
  function sql_where_format($format, $field, $operator, $value, $adjustment = NULL) {
698
    if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
699
      $field = $this->sql_field($field);
700
    }
701
    else {
702
      $field = $this->sql_field($field, $adjustment);
703
    }
704
    return $this->sql_format($format, $field) . " $operator '$value'";
705
  }
706

    
707
  /**
708
   * An array of all date parts,
709
   * optionally limited to an array of allowed parts.
710
   */
711
  function date_parts($limit = NULL) {
712
    $parts = array(
713
      'year' => t('Year', array(), array('context' => 'datetime')),
714
      'month' => t('Month', array(), array('context' => 'datetime')),
715
      'day' => t('Day', array(), array('context' => 'datetime')),
716
      'hour' => t('Hour', array(), array('context' => 'datetime')),
717
      'minute' => t('Minute', array(), array('context' => 'datetime')),
718
      'second' => t('Second', array(), array('context' => 'datetime')),
719
    );
720
    if (!empty($limit)) {
721
      $last = FALSE;
722
      foreach ($parts as $key => $part) {
723
        if ($last) {
724
          unset($parts[$key]);
725
        }
726
        if ($key == $limit) {
727
          $last = TRUE;
728
        }
729
      }
730
    }
731
    return $parts;
732
  }
733

    
734
  /**
735
   * Part information.
736
   *
737
   * @param string $op
738
   *   'min', 'max', 'format', 'sep', 'empty_now', 'empty_min', 'empty_max' .
739
   *   Returns all info if empty.
740
   * @param string $part
741
   *   'year', 'month', 'day', 'hour', 'minute', or 'second.
742
   *   returns info for all parts if empty.
743
   */
744
  function part_info($op = NULL, $part = NULL) {
745
    $info = array();
746
    $info['min'] = array(
747
      'year' => 100,
748
      'month' => 1,
749
      'day' => 1,
750
      'hour' => 0,
751
      'minute' => 0,
752
      'second' => 0,
753
    );
754
    $info['max'] = array(
755
      'year' => 4000,
756
      'month' => 12,
757
      'day' => 31,
758
      'hour' => 23,
759
      'minute' => 59,
760
      'second' => 59,
761
    );
762
    $info['format'] = array(
763
      'year' => 'Y',
764
      'month' => 'm',
765
      'day' => 'd',
766
      'hour' => 'H',
767
      'minute' => 'i',
768
      'second' => 's',
769
    );
770
    $info['sep'] = array(
771
      'year' => '',
772
      'month' => '-',
773
      'day' => '-',
774
      'hour' => ' ',
775
      'minute' => ':',
776
      'second' => ':',
777
    );
778
    $info['empty_now'] = array(
779
      'year' => date('Y'),
780
      'month' => date('m'),
781
      'day' => min('28', date('d')),
782
      'hour' => date('H'),
783
      'minute' => date('i'),
784
      'second' => date('s'),
785
    );
786
    $info['empty_min'] = array(
787
      'year' => '1000',
788
      'month' => '01',
789
      'day' => '01',
790
      'hour' => '00',
791
      'minute' => '00',
792
      'second' => '00',
793
    );
794
    $info['empty_max'] = array(
795
      'year' => '9999',
796
      'month' => '12',
797
      'day' => '31',
798
      'hour' => '23',
799
      'minute' => '59',
800
      'second' => '59',
801
    );
802
    if (!empty($op)) {
803
      if (!empty($part)) {
804
        return $info[$op][$part];
805
      }
806
      else {
807
        return $info[$op];
808
      }
809
    }
810
    return $info;
811
  }
812

    
813
  /**
814
   * Create a complete date/time value out of an incomplete array of values.
815
   *
816
   * For example, array('year' => 2008, 'month' => 05) will fill
817
   * in the day, hour, minute and second with the earliest possible
818
   * values if type = 'min', the latest possible values if type = 'max',
819
   * and the current values if type = 'now' .
820
   */
821
  function complete_date($selected, $type = 'now') {
822
    if (empty($selected)) {
823
      return '';
824
    }
825
    // Special case for weeks.
826
    if (array_key_exists('week', $selected)) {
827
      $dates = date_week_range($selected['week'], $selected['year']);
828
      switch ($type) {
829
        case 'empty_now':
830
        case 'empty_min':
831
        case 'min':
832
          return date_format($dates[0], 'Y-m-d H:i:s');
833

    
834
        case 'empty_max':
835
        case 'max':
836
          return date_format($dates[1], 'Y-m-d H:i:s');
837

    
838
        default:
839
          return;
840
      }
841
    }
842

    
843
    $compare = array_merge($this->part_info('empty_' . $type), $selected);
844
    // If this is a max date, make sure the last day of
845
    // the month is the right one for this date.
846
    if ($type == 'max') {
847
      $compare['day'] = date_days_in_month($compare['year'], $compare['month']);
848
    }
849
    $value = '';
850
    $separators = $this->part_info('sep');
851
    foreach ($this->date_parts() as $key => $name) {
852
      $value .= $separators[$key] . (!empty($selected[$key]) ? $selected[$key] : $compare[$key]);
853
    }
854
    return $value;
855
  }
856

    
857
  /**
858
   * Converts a format string into help text, i.e. 'Y-m-d' becomes 'YYYY-MM-DD'.
859
   *
860
   * @param string $format
861
   *   A date format string.
862
   *
863
   * @return string
864
   *   The conveted help text.
865
   */
866
  function format_help($format) {
867
    $replace = array(
868
      'Y' => 'YYYY',
869
      'm' => 'MM',
870
      'd' => 'DD',
871
      'H' => 'HH',
872
      'i' => 'MM',
873
      's' => 'SS',
874
      '\T' => 'T',
875
    );
876
    return strtr($format, $replace);
877
  }
878

    
879
  /**
880
   * A function to test the validity of various date parts.
881
   */
882
  function part_is_valid($value, $type) {
883
    if (!preg_match('/^[0-9]*$/', $value)) {
884
      return FALSE;
885
    }
886
    $value = intval($value);
887
    if ($value <= 0) {
888
      return FALSE;
889
    }
890
    switch ($type) {
891
      case 'year':
892
        if ($value < DATE_MIN_YEAR) {
893
          return FALSE;
894
        }
895
        break;
896

    
897
      case 'month':
898
        if ($value < 0 || $value > 12) {
899
          return FALSE;
900
        }
901
        break;
902

    
903
      case 'day':
904
        if ($value < 0 || $value > 31) {
905
          return FALSE;
906
        }
907
        break;
908

    
909
      case 'week':
910
        if ($value < 0 || $value > 53) {
911
          return FALSE;
912
        }
913
        break;
914
    }
915
    return TRUE;
916
  }
917

    
918
  /**
919
   * @todo.
920
   */
921
  function views_formats($granularity, $type = 'sql') {
922
    if (empty($granularity)) {
923
      return DATE_FORMAT_ISO;
924
    }
925
    $formats = array('display', 'sql');
926
    // Start with the site long date format and add seconds to it.
927
    $short = str_replace(':i', ':i:s', variable_get('date_format_short', 'l, F j, Y - H:i'));
928
    switch ($granularity) {
929
      case 'year':
930
        $formats['display'] = 'Y';
931
        $formats['sql'] = 'Y';
932
        break;
933

    
934
      case 'month':
935
        $formats['display'] = date_limit_format($short, array('year', 'month'));
936
        $formats['sql'] = 'Y-m';
937
        break;
938

    
939
      case 'day':
940
        $args = array('year', 'month', 'day');
941
        $formats['display'] = date_limit_format($short, $args);
942
        $formats['sql'] = 'Y-m-d';
943
        break;
944

    
945
      case 'hour':
946
        $args = array('year', 'month', 'day', 'hour');
947
        $formats['display'] = date_limit_format($short, $args);
948
        $formats['sql'] = 'Y-m-d\TH';
949
        break;
950

    
951
      case 'minute':
952
        $args = array('year', 'month', 'day', 'hour', 'minute');
953
        $formats['display'] = date_limit_format($short, $args);
954
        $formats['sql'] = 'Y-m-d\TH:i';
955
        break;
956

    
957
      case 'second':
958
        $args = array('year', 'month', 'day', 'hour', 'minute', 'second');
959
        $formats['display'] = date_limit_format($short, $args);
960
        $formats['sql'] = 'Y-m-d\TH:i:s';
961
        break;
962

    
963
      case 'week':
964
        $formats['display'] = 'F j Y (W)';
965
        $formats['sql'] = 'Y-\WW';
966
        break;
967
    }
968
    return $formats[$type];
969
  }
970

    
971
  /**
972
   * @todo.
973
   */
974
  function granularity_form($granularity) {
975
    $form = array(
976
      '#title' => t('Granularity'),
977
      '#type' => 'radios',
978
      '#default_value' => $granularity,
979
      '#options' => $this->date_parts(),
980
    );
981
    return $form;
982
  }
983

    
984
  /**
985
   * Parse date parts from an ISO date argument.
986
   *
987
   * Based on ISO 8601 date duration and time interval standards.
988
   *
989
   * Parses a value like 2006-01-01--2006-01-15, or 2006-W24, or @P1W.
990
   * Separate start and end dates or date and period with a double hyphen (--).
991
   *
992
   * The 'end' portion of the argument can be eliminated if it is the same as
993
   * the 'start' portion. Use @ instead of a date to substitute in the current
994
   * date and time.
995
   *
996
   * Use periods (P1H, P1D, P1W, P1M, P1Y) to get next hour/day/week/month/year
997
   * from now. Use date before P sign to get next hour/day/week/month/year from
998
   * that date. Use period then date to get a period that ends on the date.
999
   *
1000
   * @see http://en.wikipedia.org/wiki/ISO_8601#Week_dates
1001
   * @see http://en.wikipedia.org/wiki/ISO_8601#Duration
1002
   */
1003
  function arg_parts($argument) {
1004
    $values = array();
1005
    // Keep mal-formed arguments from creating errors.
1006
    if (empty($argument) || is_array($argument)) {
1007
      return array('date' => array(), 'period' => array());
1008
    }
1009
    $fromto = explode('--', $argument);
1010
    foreach ($fromto as $arg) {
1011
      $parts = array();
1012
      if ($arg == '@') {
1013
        $date = date_now();
1014
        $parts['date'] = $date->toArray();
1015
      }
1016
      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)) {
1017
        $date = array();
1018
        if (!empty($matches[1])) {
1019
          $date['year'] = $matches[1];
1020
        }
1021
        if (!empty($matches[3])) {
1022
          if (empty($matches[2])) {
1023
            $date['month'] = $matches[3];
1024
          }
1025
          else {
1026
            $date['week'] = $matches[3];
1027
          }
1028
        }
1029
        if (!empty($matches[4])) {
1030
          $date['day'] = $matches[4];
1031
        }
1032
        if (!empty($matches[5])) {
1033
          $date['hour'] = $matches[5];
1034
        }
1035
        if (!empty($matches[6])) {
1036
          $date['minute'] = $matches[6];
1037
        }
1038
        if (!empty($matches[7])) {
1039
          $date['second'] = $matches[7];
1040
        }
1041
        $parts['date'] = $date;
1042
      }
1043
      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)) {
1044
        $period = array();
1045
        if (!empty($matches[1])) {
1046
          $period['year'] = str_replace('Y', '', $matches[1]);
1047
        }
1048
        if (!empty($matches[2])) {
1049
          $period['month'] = str_replace('M', '', $matches[2]);
1050
        }
1051
        if (!empty($matches[3])) {
1052
          $period['week'] = str_replace('W', '', $matches[3]);
1053
        }
1054
        if (!empty($matches[4])) {
1055
          $period['day'] = str_replace('D', '', $matches[4]);
1056
        }
1057
        if (!empty($matches[6])) {
1058
          $period['hour'] = str_replace('H', '', $matches[6]);
1059
        }
1060
        if (!empty($matches[7])) {
1061
          $period['minute'] = str_replace('M', '', $matches[7]);
1062
        }
1063
        if (!empty($matches[8])) {
1064
          $period['second'] = str_replace('S', '', $matches[8]);
1065
        }
1066
        $parts['period'] = $period;
1067
      }
1068
      $values[] = $parts;
1069
    }
1070
    return $values;
1071
  }
1072

    
1073
  /**
1074
   * Convert strings like '+1 day' to the ISO equivalent, like 'P1D' .
1075
   */
1076
  function arg_replace($arg) {
1077
    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)) {
1078
      return str_replace('now', '@', $arg);
1079
    }
1080
    $direction = $results[1];
1081
    $count = $results[2];
1082
    $item = $results[3];
1083
    $replace = array(
1084
      'now' => '@',
1085
      '+' => 'P',
1086
      '-' => 'P-',
1087
      'years' => 'Y',
1088
      'year' => 'Y',
1089
      'months' => 'M',
1090
      'month' => 'M',
1091
      'weeks' => 'W',
1092
      'week' => 'W',
1093
      'days' => 'D',
1094
      'day' => 'D',
1095
      'hours' => 'H',
1096
      'hour' => 'H',
1097
      'minutes' => 'M',
1098
      'minute' => 'M',
1099
      'seconds' => 'S',
1100
      'second' => 'S',
1101
      '  ' => '',
1102
      ' ' => '',
1103
    );
1104
    $args = array('hours', 'hour', 'minutes', 'minute', 'seconds', 'second');
1105
    if (in_array($item, $args)) {
1106
      $prefix = 'T';
1107
    }
1108
    else {
1109
      $prefix = '';
1110
    }
1111
    $return = $prefix;
1112
    $return .= strtr($direction, $replace);
1113
    $return .= $count;
1114
    $return .= strtr($item, $replace);
1115

    
1116
    return $return;
1117
  }
1118

    
1119
  /**
1120
   * Granularity arguments handler.
1121
   *
1122
   * Use the parsed values from the ISO argument
1123
   * to determine the granularity of this period.
1124
   */
1125
  function arg_granularity($arg) {
1126
    $granularity = '';
1127
    $parts = $this->arg_parts($arg);
1128
    $date = !empty($parts[0]['date']) ? $parts[0]['date'] : (!empty($parts[1]['date']) ? $parts[1]['date'] : array());
1129
    foreach ($date as $key => $part) {
1130
      $granularity = $key;
1131
    }
1132
    return $granularity;
1133
  }
1134

    
1135
  /**
1136
   * Use the parsed values from the ISO argument to determine the
1137
   * min and max date for this period.
1138
   */
1139
  function arg_range($arg) {
1140
    // Parse the argument to get its parts.
1141
    $parts = $this->arg_parts($arg);
1142

    
1143
    // Build a range from a period-only argument (assumes the min date is now.)
1144
    if (empty($parts[0]['date']) && !empty($parts[0]['period']) && (empty($parts[1]))) {
1145
      $min_date = date_now();
1146
      $max_date = clone($min_date);
1147
      foreach ($parts[0]['period'] as $part => $value) {
1148
        date_modify($max_date, "+$value $part");
1149
      }
1150
      date_modify($max_date, '-1 second');
1151
      return array($min_date, $max_date);
1152
    }
1153
    // Build a range from a period to period argument.
1154
    if (empty($parts[0]['date']) && !empty($parts[0]['period']) && !empty($parts[1]['period'])) {
1155
      $min_date = date_now();
1156
      $max_date = clone($min_date);
1157
      foreach ($parts[0]['period'] as $part => $value) {
1158
        date_modify($min_date, "+$value $part");
1159
      }
1160
      date_modify($min_date, '-1 second');
1161
      foreach ($parts[1]['period'] as $part => $value) {
1162
        date_modify($max_date, "+$value $part");
1163
      }
1164
      date_modify($max_date, '-1 second');
1165
      return array($min_date, $max_date);
1166
    }
1167
    if (!empty($parts[0]['date'])) {
1168
      $value = $this->complete_date($parts[0]['date'], 'min');
1169
      $min_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1170
      // Build a range from a single date-only argument.
1171
      if (empty($parts[1]) || (empty($parts[1]['date']) && empty($parts[1]['period']))) {
1172
        $value = $this->complete_date($parts[0]['date'], 'max');
1173
        $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1174
        return array($min_date, $max_date);
1175
      }
1176
      // Build a range from start date + period.
1177
      elseif (!empty($parts[1]['period'])) {
1178
        foreach ($parts[1]['period'] as $part => $value) {
1179
          $max_date = clone($min_date);
1180
          date_modify($max_date, "+$value $part");
1181
        }
1182
        date_modify($max_date, '-1 second');
1183
        return array($min_date, $max_date);
1184
      }
1185
    }
1186
    // Build a range from start date and end date.
1187
    if (!empty($parts[1]['date'])) {
1188
      $value = $this->complete_date($parts[1]['date'], 'max');
1189
      $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
1190
      if (isset($min_date)) {
1191
        return array($min_date, $max_date);
1192
      }
1193
    }
1194
    // Build a range from period + end date.
1195
    if (!empty($parts[0]['period'])) {
1196
      $min_date = date_now();
1197
      foreach ($parts[0]['period'] as $part => $value) {
1198
        date_modify($min_date, "$value $part");
1199
      }
1200
      return array($min_date, $max_date);
1201
    }
1202
    // Intercept invalid info and fall back to the current date.
1203
    $now = date_now();
1204
    return array($now, $now);
1205
  }
1206
}
1207
// @codingStandardsIgnoreEnd