Projet

Général

Profil

Paste
Télécharger (85,4 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api.module @ 74f6bef0

1
<?php
2

    
3
/**
4
 * @file
5
 * This module will make the date API available to other modules.
6
 * Designed to provide a light but flexible assortment of functions
7
 * and constants, with more functionality in additional files that
8
 * are not loaded unless other modules specifically include them.
9
 */
10

    
11
/**
12
 * Set up some constants.
13
 *
14
 * Includes standard date types, format strings, strict regex strings for ISO
15
 * and DATETIME formats (seconds are optional).
16
 *
17
 * The loose regex will find any variety of ISO date and time, with or
18
 * without time, with or without dashes and colons separating the elements,
19
 * and with either a 'T' or a space separating date and time.
20
 */
21
define('DATE_ISO', 'date');
22
define('DATE_UNIX', 'datestamp');
23
define('DATE_DATETIME', 'datetime');
24
define('DATE_ARRAY', 'array');
25
define('DATE_OBJECT', 'object');
26
define('DATE_ICAL', 'ical');
27

    
28
define('DATE_FORMAT_ISO', "Y-m-d\TH:i:s");
29
define('DATE_FORMAT_UNIX', "U");
30
define('DATE_FORMAT_DATETIME', "Y-m-d H:i:s");
31
define('DATE_FORMAT_ICAL', "Ymd\THis");
32
define('DATE_FORMAT_ICAL_DATE', "Ymd");
33
define('DATE_FORMAT_DATE', 'Y-m-d');
34

    
35
define('DATE_REGEX_ISO', '/(\d{4})?(-(\d{2}))?(-(\d{2}))?([T\s](\d{2}))?(:(\d{2}))?(:(\d{2}))?/');
36
define('DATE_REGEX_DATETIME', '/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):?(\d{2})?/');
37
define('DATE_REGEX_LOOSE', '/(\d{4})-?(\d{1,2})-?(\d{1,2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?/');
38
define('DATE_REGEX_ICAL_DATE', '/(\d{4})(\d{2})(\d{2})/');
39
define('DATE_REGEX_ICAL_DATETIME', '/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?/');
40

    
41
/**
42
 * Core DateTime extension module used for as many date operations as possible.
43
 */
44

    
45
/**
46
 * Implements hook_help().
47
 */
48
function date_help($path, $arg) {
49
  switch ($path) {
50
    case 'admin/help#date':
51
      $output = '';
52
      $messages = date_api_status();
53
      $output = '<h2>Date API Status</h2>';
54
      if (!empty($messages['success'])) {
55
        $output .= '<ul><li>' . implode('</li><li>', $messages['success']) . '</li></ul>';
56
      }
57
      if (!empty($messages['errors'])) {
58
        $output .= '<h3>Errors</h3><ul class="error"><li>' . implode('</li><li>', $messages['errors']) . '</li></ul>';
59
      }
60

    
61
      if (module_exists('date_tools')) {
62
        $output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. The !date_wizard makes it easy to create a simple date content type and with a date field. ', array('!date_wizard' => l(t('Date wizard'), 'admin/config/date/tools/date_wizard')));
63
      }
64
      else {
65
        $output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. If you enable the Date Tools module, it provides a Date Wizard that makes it easy to create a simple date content type with a date field. ');
66
      }
67

    
68
      $output .= '<h2>More Information</h2><p>' . t('Complete documentation for the Date and Date API modules is available at <a href="@link">http://drupal.org/node/92460</a>.', array('@link' => 'http://drupal.org/node/262062')) . '</p>';
69

    
70
      return $output;
71
      break;
72
  }
73
}
74

    
75
/**
76
 * Helper function to retun the status of required date variables.
77
 */
78
function date_api_status() {
79
  $t = get_t();
80

    
81
  $error_messages = array();
82
  $success_messages = array();
83

    
84
  $value = variable_get('date_default_timezone');
85
  if (isset($value)) {
86
    $success_messages[] = $t('The timezone has been set to <a href="@regional_settings">@timezone</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@timezone' => $value));
87
  }
88
  else {
89
    $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site timezone</a> to function correctly.', array('@regional_settings' => url('admin/config/regional/settings')));
90
  }
91

    
92
  $value = variable_get('date_first_day');
93
  if (isset($value)) {
94
    $days = date_week_days();
95
    $success_messages[] = $t('The first day of the week has been set to <a href="@regional_settings">@day</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@day' => $days[$value]));
96
  }
97
  else {
98
    $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site first day of week settings</a> to function correctly.', array('@regional_settings' => url('admin/config/regional/settings')));
99
  }
100

    
101
  $value = variable_get('date_format_medium');
102
  if (isset($value)) {
103
    $now = date_now();
104
    $success_messages[] = $t('The medium date format type has been set to to @value. You may find it helpful to add new format types like Date, Time, Month, or Year, with appropriate formats, at <a href="@regional_date_time">Date and time</a> settings.', array('@value' => $now->format($value), '@regional_date_time' => url('admin/config/regional/date-time')));
105
  }
106
  else {
107
    $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_date_time">system date formats</a> to function correctly.', array('@regional_date_time' => url('admin/config/regional/date-time')));
108
  }
109

    
110
  return array('errors', $error_messages, 'success' => $success_messages);
111

    
112
}
113

    
114
/**
115
 * Implements hook_menu().
116
 *
117
 * Creates a 'Date API' section on the administration page for Date
118
 * modules to use for their configuration and settings.
119
 */
120
function date_api_menu() {
121
  $items['admin/config/date'] = array(
122
    'title' => 'Date API',
123
    'description' => 'Settings for modules the use the Date API.',
124
    'position' => 'left',
125
    'weight' => -10,
126
    'page callback' => 'system_admin_menu_block_page',
127
    'access arguments' => array('administer site configuration'),
128
    'file' => 'system.admin.inc',
129
    'file path' => drupal_get_path('module', 'system'),
130
  );
131
  return $items;
132
}
133

    
134
/**
135
 * Extend PHP DateTime class with granularity handling, merge functionality and
136
 * slightly more flexible initialization parameters.
137
 *
138
 * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
139
 * class.
140
 *
141
 * @see FeedsDateTimeElement class
142
 */
143
class DateObject extends DateTime {
144
  public $granularity = array();
145
  public $errors = array();
146
  protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'timezone');
147
  private $serializedTime;
148
  private $serializedTimezone;
149

    
150
  /**
151
   * Prepares the object during serialization.
152
   *
153
   * We are extending a core class and core classes cannot be serialized.
154
   *
155
   * @return array
156
   *   Returns an array with the names of the variables that were serialized.
157
   *
158
   * @see http://bugs.php.net/41334
159
   * @see http://bugs.php.net/39821
160
   */
161
  public function __sleep() {
162
    $this->serializedTime = $this->format('c');
163
    $this->serializedTimezone = $this->getTimezone()->getName();
164
    return array('serializedTime', 'serializedTimezone');
165
  }
166

    
167
  /**
168
   * Re-builds the object using local variables.
169
   */
170
  public function __wakeup() {
171
    $this->__construct($this->serializedTime, new DateTimeZone($this->serializedTimezone));
172
  }
173

    
174
  /**
175
   * Returns the date object as a string.
176
   *
177
   * @return string
178
   *   The date object formatted as a string.
179
   */
180
  public function __toString() {
181
    return $this->format(DATE_FORMAT_DATETIME) . ' ' . $this->getTimeZone()->getName();
182
  }
183

    
184
  /**
185
   * Constructs a date object.
186
   *
187
   * @param string $time
188
   *   A date/time string or array. Defaults to 'now'.
189
   * @param object|string|null $tz
190
   *   PHP DateTimeZone object, string or NULL allowed. Defaults to NULL.
191
   * @param string $format
192
   *   PHP date() type format for parsing. Doesn't support timezones; if you
193
   *   have a timezone, send NULL and the default constructor method will
194
   *   hopefully parse it. $format is recommended in order to use negative or
195
   *   large years, which php's parser fails on.
196
   */
197
  public function __construct($time = 'now', $tz = NULL, $format = NULL) {
198
    $this->timeOnly = FALSE;
199
    $this->dateOnly = FALSE;
200

    
201
    // Store the raw time input so it is available for validation.
202
    $this->originalTime = $time;
203

    
204
    // Allow string timezones.
205
    if (!empty($tz) && !is_object($tz)) {
206
      $tz = new DateTimeZone($tz);
207
    }
208

    
209
    // Default to the site timezone when not explicitly provided.
210
    elseif (empty($tz)) {
211
      $tz = date_default_timezone_object();
212
    }
213
    // Special handling for Unix timestamps expressed in the local timezone.
214
    // Create a date object in UTC and convert it to the local timezone. Don't
215
    // try to turn things like '2010' with a format of 'Y' into a timestamp.
216
    if (is_numeric($time) && (empty($format) || $format == 'U')) {
217
      // Assume timestamp.
218
      $time = "@" . $time;
219
      $date = new DateObject($time, 'UTC');
220
      if ($tz->getName() != 'UTC') {
221
        $date->setTimezone($tz);
222
      }
223
      $time = $date->format(DATE_FORMAT_DATETIME);
224
      $format = DATE_FORMAT_DATETIME;
225
      $this->addGranularity('timezone');
226
    }
227
    elseif (is_array($time)) {
228
      // Assume we were passed an indexed array.
229
      if (empty($time['year']) && empty($time['month']) && empty($time['day'])) {
230
        $this->timeOnly = TRUE;
231
      }
232
      if (empty($time['hour']) && empty($time['minute']) && empty($time['second'])) {
233
        $this->dateOnly = TRUE;
234
      }
235
      $this->errors = $this->arrayErrors($time);
236
      // Make this into an ISO date, forcing a full ISO date even if some values
237
      // are missing.
238
      $time = $this->toISO($time, TRUE);
239
      // We checked for errors already, skip parsing the input values.
240
      $format = NULL;
241
    }
242
    else {
243
      // Make sure dates like 2010-00-00T00:00:00 get converted to
244
      // 2010-01-01T00:00:00 before creating a date object
245
      // to avoid unintended changes in the month or day.
246
      $time = date_make_iso_valid($time);
247
    }
248

    
249
    // The parse function will also set errors on the date parts.
250
    if (!empty($format)) {
251
      $arg = self::$allgranularity;
252
      $element = array_pop($arg);
253
      while (!$this->parse($time, $tz, $format) && $element != 'year') {
254
        $element = array_pop($arg);
255
        $format = date_limit_format($format, $arg);
256
      }
257
      if ($element == 'year') {
258
        return FALSE;
259
      }
260
    }
261
    elseif (is_string($time)) {
262
      // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
263
      $time = str_replace("GMT-", "-", $time);
264
      $time = str_replace("GMT+", "+", $time);
265
      // We are going to let the parent dateObject do a best effort attempt to
266
      // turn this string into a valid date. It might fail and we want to
267
      // control the error messages.
268
      try {
269
        @parent::__construct($time, $tz);
270
      }
271
      catch (Exception $e) {
272
        $this->errors['date'] = $e;
273
        return;
274
      }
275
      if (empty($this->granularity)) {
276
        $this->setGranularityFromTime($time, $tz);
277
      }
278
    }
279

    
280
    // If we haven't got a valid timezone name yet, we need to set one or
281
    // we will get undefined index errors.
282
    // This can happen if $time had an offset or no timezone.
283
    if (!$this->getTimezone() || !preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
284

    
285
      // If the original $tz has a name, use it.
286
      if (preg_match('/[a-zA-Z]/', $tz->getName())) {
287
        $this->setTimezone($tz);
288
      }
289
      // We have no information about the timezone so must fallback to a default.
290
      else {
291
        $this->setTimezone(new DateTimeZone("UTC"));
292
        $this->errors['timezone'] = t('No valid timezone name was provided.');
293
      }
294
    }
295
  }
296

    
297
  /**
298
   * Merges two date objects together using the current date values as defaults.
299
   *
300
   * @param object $other
301
   *   Another date object to merge with.
302
   *
303
   * @return object
304
   *   A merged date object.
305
   */
306
  public function merge(FeedsDateTime $other) {
307
    $other_tz = $other->getTimezone();
308
    $this_tz = $this->getTimezone();
309
    // Figure out which timezone to use for combination.
310
    $use_tz = ($this->hasGranularity('timezone') || !$other->hasGranularity('timezone')) ? $this_tz : $other_tz;
311

    
312
    $this2 = clone $this;
313
    $this2->setTimezone($use_tz);
314
    $other->setTimezone($use_tz);
315
    $val = $this2->toArray(TRUE);
316
    $otherval = $other->toArray();
317
    foreach (self::$allgranularity as $g) {
318
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
319
        // The other class has a property we don't; steal it.
320
        $this2->addGranularity($g);
321
        $val[$g] = $otherval[$g];
322
      }
323
    }
324
    $other->setTimezone($other_tz);
325

    
326
    $this2->setDate($val['year'], $val['month'], $val['day']);
327
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
328
    return $this2;
329
  }
330

    
331
  /**
332
   * Sets the time zone for the current date.
333
   *
334
   * Overrides default DateTime function. Only changes output values if
335
   * actually had time granularity. This should be used as a "converter" for
336
   * output, to switch tzs.
337
   *
338
   * In order to set a timezone for a datetime that doesn't have such
339
   * granularity, merge() it with one that does.
340
   *
341
   * @param object $tz
342
   *   A timezone object.
343
   * @param bool $force
344
   *   Whether or not to skip a date with no time. Defaults to FALSE.
345
   */
346
  public function setTimezone($tz, $force = FALSE) {
347
    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
348
    // http://bugs.php.net/bug.php?id=45038
349
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
350
      $tz = new DateTimeZone($tz->getName());
351
    }
352

    
353
    if (!$this->hasTime() || !$this->hasGranularity('timezone') || $force) {
354
      // This has no time or timezone granularity, so timezone doesn't mean
355
      // much. We set the timezone using the method, which will change the
356
      // day/hour, but then we switch back.
357
      $arr = $this->toArray(TRUE);
358
      parent::setTimezone($tz);
359
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
360
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
361
      $this->addGranularity('timezone');
362
      return;
363
    }
364
    return parent::setTimezone($tz);
365
  }
366

    
367
  /**
368
   * Returns date formatted according to given format.
369
   *
370
   * Overrides base format function, formats this date according to its
371
   * available granularity, unless $force'ed not to limit to granularity.
372
   *
373
   * @TODO Add translation into this so translated names will be provided.
374
   *
375
   * @param string $format
376
   *   A date format string.
377
   * @param bool $force
378
   *   Whether or not to limit the granularity. Defaults to FALSE.
379
   *
380
   * @return string|false
381
   *   Returns the formatted date string on success or FALSE on failure.
382
   */
383
  public function format($format, $force = FALSE) {
384
    return parent::format($force ? $format : date_limit_format($format, $this->granularity));
385
  }
386

    
387
  /**
388
   * Adds a granularity entry to the array.
389
   *
390
   * @param string $g
391
   *   A single date part.
392
   */
393
  public function addGranularity($g) {
394
    $this->granularity[] = $g;
395
    $this->granularity = array_unique($this->granularity);
396
  }
397

    
398
  /**
399
   * Removes a granularity entry from the array.
400
   *
401
   * @param string $g
402
   *   A single date part.
403
   */
404
  public function removeGranularity($g) {
405
    if ($key = array_search($g, $this->granularity)) {
406
      unset($this->granularity[$key]);
407
    }
408
  }
409

    
410
  /**
411
   * Checks granularity array for a given entry.
412
   *
413
   * @param array|null $g
414
   *   An array of date parts. Defaults to NULL.
415
   *
416
   * @returns bool
417
   *   TRUE if the date part is present in the date's granularity.
418
   */
419
  public function hasGranularity($g = NULL) {
420
    if ($g === NULL) {
421
      // Just want to know if it has something valid means no lower
422
      // granularities without higher ones.
423
      $last = TRUE;
424
      foreach (self::$allgranularity as $arg) {
425
        if ($arg == 'timezone') {
426
          continue;
427
        }
428
        if (in_array($arg, $this->granularity) && !$last) {
429
          return FALSE;
430
        }
431
        $last = in_array($arg, $this->granularity);
432
      }
433
      return in_array('year', $this->granularity);
434
    }
435
    if (is_array($g)) {
436
      foreach ($g as $gran) {
437
        if (!in_array($gran, $this->granularity)) {
438
          return FALSE;
439
        }
440
      }
441
      return TRUE;
442
    }
443
    return in_array($g, $this->granularity);
444
  }
445

    
446
  /**
447
   * Determines if a a date is valid for a given granularity.
448
   *
449
   * @param array|null $granularity
450
   *   An array of date parts. Defaults to NULL.
451
   * @param bool $flexible
452
   *   TRUE if the granuliarty is flexible, FALSE otherwise. Defaults to FALSE.
453
   *
454
   * @return bool
455
   *   Whether a date is valid for a given granularity.
456
   */
457
  public function validGranularity($granularity = NULL, $flexible = FALSE) {
458
    $true = $this->hasGranularity() && (!$granularity || $flexible || $this->hasGranularity($granularity));
459
    if (!$true && $granularity) {
460
      foreach ((array) $granularity as $part) {
461
        if (!$this->hasGranularity($part) && in_array($part, array('second', 'minute', 'hour', 'day', 'month', 'year'))) {
462
          $this->errors[$part] = t("The $part is missing.");
463
        }
464
      }
465
    }
466
    return $true;
467
  }
468

    
469
  /**
470
   * Returns whether this object has time set.
471
   *
472
   * Used primarily for timezone conversion and formatting.
473
   *
474
   * @return bool
475
   *   TRUE if the date contains time parts, FALSE otherwise.
476
   */
477
  public function hasTime() {
478
    return $this->hasGranularity('hour');
479
  }
480

    
481
  /**
482
   * Returns whether the input values included a year.
483
   *
484
   * Useful to use pseudo date objects when we only are interested in the time.
485
   *
486
   * @todo $this->completeDate does not actually exist?
487
   */
488
  public function completeDate() {
489
    return $this->completeDate;
490
  }
491

    
492
  /**
493
   * Removes unwanted date parts from a date.
494
   *
495
   * In common usage we should not unset timezone through this.
496
   *
497
   * @param array $granularity
498
   *   An array of date parts.
499
   */
500
  public function limitGranularity($granularity) {
501
    foreach ($this->granularity as $key => $val) {
502
      if ($val != 'timezone' && !in_array($val, $granularity)) {
503
        unset($this->granularity[$key]);
504
      }
505
    }
506
  }
507

    
508
  /**
509
   * Determines the granularity of a date based on the constructor's arguments.
510
   *
511
   * @param string $time
512
   *   A date string.
513
   * @param bool $tz
514
   *   TRUE if the date has a timezone, FALSE otherwise.
515
   */
516
  protected function setGranularityFromTime($time, $tz) {
517
    $this->granularity = array();
518
    $temp = date_parse($time);
519
    // Special case for 'now'.
520
    if ($time == 'now') {
521
      $this->granularity = array('year', 'month', 'day', 'hour', 'minute', 'second');
522
    }
523
    else {
524
      // This PHP date_parse() method currently doesn't have resolution down to
525
      // seconds, so if there is some time, all will be set.
526
      foreach (self::$allgranularity as $g) {
527
        if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'timezone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
528
          $this->granularity[] = $g;
529
        }
530
      }
531
    }
532
    if ($tz) {
533
      $this->addGranularity('timezone');
534
    }
535
  }
536

    
537
  /**
538
   * Converts a date string into a date object.
539
   *
540
   * @param string $date
541
   *   The date string to parse.
542
   * @param object $tz
543
   *   A timezone object.
544
   * @param string $format
545
   *   The date format string.
546
   *
547
   * @return object
548
   *   Returns the date object.
549
   */
550
  protected function parse($date, $tz, $format) {
551
    $array = date_format_patterns();
552
    foreach ($array as $key => $value) {
553
      // The letter with no preceding '\'.
554
      $patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";
555
      // A single character.
556
      $repl1[] = '${1}(.)';
557
      // The.
558
      $repl2[] = '${1}(' . $value . ')';
559
    }
560
    $patterns[] = "`\\\\\\\\([" . implode(array_keys($array)) . "])`";
561
    $repl1[] = '${1}';
562
    $repl2[] = '${1}';
563

    
564
    $format_regexp = preg_quote($format);
565

    
566
    // Extract letters.
567
    $regex1 = preg_replace($patterns, $repl1, $format_regexp, 1);
568
    $regex1 = str_replace('A', '(.)', $regex1);
569
    $regex1 = str_replace('a', '(.)', $regex1);
570
    preg_match('`^' . $regex1 . '$`', stripslashes($format), $letters);
571
    array_shift($letters);
572
    // Extract values.
573
    $regex2 = preg_replace($patterns, $repl2, $format_regexp, 1);
574
    $regex2 = str_replace('A', '(AM|PM)', $regex2);
575
    $regex2 = str_replace('a', '(am|pm)', $regex2);
576
    preg_match('`^' . $regex2 . '$`u', $date, $values);
577
    array_shift($values);
578
    // If we did not find all the values for the patterns in the format, abort.
579
    if (count($letters) != count($values)) {
580
      $this->errors['invalid'] = t('The value @date does not match the expected format.', array('@date' => $date));
581
      return FALSE;
582
    }
583
    $this->granularity = array();
584
    $final_date = array('hour' => 0, 'minute' => 0, 'second' => 0, 'month' => 1, 'day' => 1, 'year' => 0);
585
    foreach ($letters as $i => $letter) {
586
      $value = $values[$i];
587
      switch ($letter) {
588
        case 'd':
589
        case 'j':
590
          $final_date['day'] = intval($value);
591
          $this->addGranularity('day');
592
          break;
593
        case 'n':
594
        case 'm':
595
          $final_date['month'] = intval($value);
596
          $this->addGranularity('month');
597
          break;
598
        case 'F':
599
          $array_month_long = array_flip(date_month_names());
600
          $final_date['month'] = array_key_exists($value, $array_month_long) ? $array_month_long[$value] : -1;
601
          $this->addGranularity('month');
602
          break;
603
        case 'M':
604
          $array_month = array_flip(date_month_names_abbr());
605
          $final_date['month'] = array_key_exists($value, $array_month) ? $array_month[$value] : -1;
606
          $this->addGranularity('month');
607
          break;
608
        case 'Y':
609
          $final_date['year'] = $value;
610
          $this->addGranularity('year');
611
          if (strlen($value) < 4) {
612
            $this->errors['year'] = t('The year is invalid. Please check that entry includes four digits.');
613
          }
614
          break;
615
        case 'y':
616
          $year = $value;
617
          // If no century, we add the current one ("06" => "2006").
618
          $final_date['year'] = str_pad($year, 4, substr(date("Y"), 0, 2), STR_PAD_LEFT);
619
          $this->addGranularity('year');
620
          break;
621
        case 'a':
622
        case 'A':
623
          $ampm = strtolower($value);
624
          break;
625
        case 'g':
626
        case 'h':
627
        case 'G':
628
        case 'H':
629
          $final_date['hour'] = intval($value);
630
          $this->addGranularity('hour');
631
          break;
632
        case 'i':
633
          $final_date['minute'] = intval($value);
634
          $this->addGranularity('minute');
635
          break;
636
        case 's':
637
          $final_date['second'] = intval($value);
638
          $this->addGranularity('second');
639
          break;
640
        case 'U':
641
          parent::__construct($value, $tz ? $tz : new DateTimeZone("UTC"));
642
          $this->addGranularity('year');
643
          $this->addGranularity('month');
644
          $this->addGranularity('day');
645
          $this->addGranularity('hour');
646
          $this->addGranularity('minute');
647
          $this->addGranularity('second');
648
          return $this;
649
          break;
650
      }
651
    }
652
    if (isset($ampm) && $ampm == 'pm' && $final_date['hour'] < 12) {
653
      $final_date['hour'] += 12;
654
    }
655
    elseif (isset($ampm) && $ampm == 'am' && $final_date['hour'] == 12) {
656
      $final_date['hour'] -= 12;
657
    }
658

    
659
    // Blank becomes current time, given TZ.
660
    parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
661
    if ($tz) {
662
      $this->addGranularity('timezone');
663
    }
664

    
665
    // SetDate expects an integer value for the year, results can be unexpected
666
    // if we feed it something like '0100' or '0000'.
667
    $final_date['year'] = intval($final_date['year']);
668

    
669
    $this->errors += $this->arrayErrors($final_date);
670
    $granularity = drupal_map_assoc($this->granularity);
671

    
672
    // If the input value is '0000-00-00', PHP's date class will later
673
    // incorrectly convert it to something like '-0001-11-30' if we do setDate()
674
    // here. If we don't do setDate() here, it will default to the current date
675
    // and we will lose any way to tell that there was no date in the orignal
676
    // input values. So set a flag we can use later to tell that this date
677
    // object was created using only time values, and that the date values are
678
    // artifical.
679
    if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) {
680
      $this->timeOnly = TRUE;
681
    }
682
    elseif (empty($this->errors)) {
683
      // setDate() expects a valid year, month, and day.
684
      // Set some defaults for dates that don't use this to
685
      // keep PHP from interpreting it as the last day of
686
      // the previous month or last month of the previous year.
687
      if (empty($granularity['month'])) {
688
        $final_date['month'] = 1;
689
      }
690
      if (empty($granularity['day'])) {
691
        $final_date['day'] = 1;
692
      }
693
      $this->setDate($final_date['year'], $final_date['month'], $final_date['day']);
694
    }
695

    
696
    if (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) {
697
      $this->dateOnly = TRUE;
698
    }
699
    elseif (empty($this->errors)) {
700
      $this->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
701
    }
702
    return $this;
703
  }
704

    
705
  /**
706
   * Returns all standard date parts in an array.
707
   *
708
   * Will return '' for parts in which it lacks granularity.
709
   *
710
   * @param bool $force
711
   *   Whether or not to limit the granularity. Defaults to FALSE.
712
   *
713
   * @return array
714
   *   An array of formatted date part values, keyed by date parts.
715
   */
716
  public function toArray($force = FALSE) {
717
    return array(
718
      'year' => $this->format('Y', $force),
719
      'month' => $this->format('n', $force),
720
      'day' => $this->format('j', $force),
721
      'hour' => intval($this->format('H', $force)),
722
      'minute' => intval($this->format('i', $force)),
723
      'second' => intval($this->format('s', $force)),
724
      'timezone' => $this->format('e', $force),
725
    );
726
  }
727

    
728
  /**
729
   * Creates an ISO date from an array of values.
730
   *
731
   * @param array $arr
732
   *   An array of date values keyed by date part.
733
   * @param bool $full
734
   *   (optional) Whether to force a full date by filling in missing values.
735
   *   Defaults to FALSE.
736
   */
737
  public function toISO($arr, $full = FALSE) {
738
    // Add empty values to avoid errors. The empty values must create a valid
739
    // date or we will get date slippage, i.e. a value of 2011-00-00 will get
740
    // interpreted as November of 2010 by PHP.
741
    if ($full) {
742
      $arr += array('year' => 0, 'month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0);
743
    }
744
    else {
745
      $arr += array('year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => '', 'second' => '');
746
    }
747
    $datetime = '';
748
    if ($arr['year'] !== '') {
749
      $datetime = date_pad(intval($arr['year']), 4);
750
      if ($full || $arr['month'] !== '') {
751
        $datetime .= '-' . date_pad(intval($arr['month']));
752
        if ($full || $arr['day'] !== '') {
753
          $datetime .= '-' . date_pad(intval($arr['day']));
754
        }
755
      }
756
    }
757
    if ($arr['hour'] !== '') {
758
      $datetime .= $datetime ? 'T' : '';
759
      $datetime .= date_pad(intval($arr['hour']));
760
      if ($full || $arr['minute'] !== '') {
761
        $datetime .= ':' . date_pad(intval($arr['minute']));
762
        if ($full || $arr['second'] !== '') {
763
          $datetime .= ':' . date_pad(intval($arr['second']));
764
        }
765
      }
766
    }
767
    return $datetime;
768
  }
769

    
770
  /**
771
   * Forces an incomplete date to be valid.
772
   *
773
   * E.g., add a valid year, month, and day if only the time has been defined.
774
   *
775
   * @param array|string $date
776
   *   An array of date parts or a datetime string with values to be massaged
777
   *   into a valid date object.
778
   * @param string $format
779
   *   (optional) The format of the date. Defaults to NULL.
780
   * @param string $default
781
   *   (optional) If the fallback should use the first value of the date part,
782
   *   or the current value of the date part. Defaults to 'first'.
783
   */
784
  public function setFuzzyDate($date, $format = NULL, $default = 'first') {
785
    $timezone = $this->getTimeZone() ? $this->getTimeZone()->getName() : NULL;
786
    $comp = new DateObject($date, $timezone, $format);
787
    $arr = $comp->toArray(TRUE);
788
    foreach ($arr as $key => $value) {
789
      // Set to intval here and then test that it is still an integer.
790
      // Needed because sometimes valid integers come through as strings.
791
      $arr[$key] = $this->forceValid($key, intval($value), $default, $arr['month'], $arr['year']);
792
    }
793
    $this->setDate($arr['year'], $arr['month'], $arr['day']);
794
    $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
795
  }
796

    
797
  /**
798
   * Converts a date part into something that will produce a valid date.
799
   *
800
   * @param string $part
801
   *   The date part.
802
   * @param int $value
803
   *   The date value for this part.
804
   * @param string $default
805
   *   (optional) If the fallback should use the first value of the date part,
806
   *   or the current value of the date part. Defaults to 'first'.
807
   * @param int $month
808
   *   (optional) Used when the date part is less than 'month' to specify the
809
   *   date. Defaults to NULL.
810
   * @param int $year
811
   *   (optional) Used when the date part is less than 'year' to specify the
812
   *   date. Defaults to NULL.
813
   *
814
   * @return int
815
   *   A valid date value.
816
   */
817
  protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
818
    $now = date_now();
819
    switch ($part) {
820
      case 'year':
821
        $fallback = $now->format('Y');
822
        return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
823
        break;
824
      case 'month':
825
        $fallback = $default == 'first' ? 1 : $now->format('n');
826
        return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
827
        break;
828
      case 'day':
829
        $fallback = $default == 'first' ? 1 : $now->format('j');
830
        $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
831
        return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
832
        break;
833
      case 'hour':
834
        $fallback = $default == 'first' ? 0 : $now->format('G');
835
        return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
836
        break;
837
      case 'minute':
838
        $fallback = $default == 'first' ? 0 : $now->format('i');
839
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
840
        break;
841
      case 'second':
842
        $fallback = $default == 'first' ? 0 : $now->format('s');
843
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
844
        break;
845
    }
846
  }
847

    
848
  /**
849
   * Finds possible errors in an array of date part values.
850
   *
851
   * The forceValid() function will change an invalid value to a valid one, so
852
   * we just need to see if the value got altered.
853
   *
854
   * @param array $arr
855
   *   An array of date values, keyed by date part.
856
   *
857
   * @return array
858
   *   An array of error messages, keyed by date part.
859
   */
860
  public function arrayErrors($arr) {
861
    $errors = array();
862
    $now = date_now();
863
    $default_month = !empty($arr['month']) ? $arr['month'] : $now->format('n');
864
    $default_year = !empty($arr['year']) ? $arr['year'] : $now->format('Y');
865

    
866
    $this->granularity = array();
867
    foreach ($arr as $part => $value) {
868
      // Explicitly set the granularity to the values in the input array.
869
      if (is_numeric($value)) {
870
        $this->addGranularity($part);
871
      }
872
      // Avoid false errors when a numeric value is input as a string by casting
873
      // as an integer.
874
      $value = intval($value);
875
      if (!empty($value) && $this->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
876
        // Use a switch/case to make translation easier by providing a different
877
        // message for each part.
878
        switch ($part) {
879
          case 'year':
880
            $errors['year'] = t('The year is invalid.');
881
            break;
882
          case 'month':
883
            $errors['month'] = t('The month is invalid.');
884
            break;
885
          case 'day':
886
            $errors['day'] = t('The day is invalid.');
887
            break;
888
          case 'hour':
889
            $errors['hour'] = t('The hour is invalid.');
890
            break;
891
          case 'minute':
892
            $errors['minute'] = t('The minute is invalid.');
893
            break;
894
          case 'second':
895
            $errors['second'] = t('The second is invalid.');
896
            break;
897
        }
898
      }
899
    }
900
    if ($this->hasTime()) {
901
      $this->addGranularity('timezone');
902
    }
903
    return $errors;
904
  }
905

    
906
  /**
907
   * Computes difference between two days using a given measure.
908
   *
909
   * @param object $date2_in
910
   *   The stop date.
911
   * @param string $measure
912
   *   (optional) A granularity date part. Defaults to 'seconds'.
913
   * @param boolean $absolute
914
   *   (optional) Indicate whether the absolute value of the difference should
915
   *   be returned or if the sign should be retained. Defaults to TRUE.
916
   */
917
  public function difference($date2_in, $measure = 'seconds', $absolute = TRUE) {
918
    // Create cloned objects or original dates will be impacted by the
919
    // date_modify() operations done in this code.
920
    $date1 = clone($this);
921
    $date2 = clone($date2_in);
922
    if (is_object($date1) && is_object($date2)) {
923
      $diff = date_format($date2, 'U') - date_format($date1, 'U');
924
      if ($diff == 0) {
925
        return 0;
926
      }
927
      elseif ($diff < 0 && $absolute) {
928
        // Make sure $date1 is the smaller date.
929
        $temp = $date2;
930
        $date2 = $date1;
931
        $date1 = $temp;
932
        $diff = date_format($date2, 'U') - date_format($date1, 'U');
933
      }
934
      $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
935
      switch ($measure) {
936
        // The easy cases first.
937
        case 'seconds':
938
          return $diff;
939
        case 'minutes':
940
          return $diff / 60;
941
        case 'hours':
942
          return $diff / 3600;
943
        case 'years':
944
          return $year_diff;
945

    
946
        case 'months':
947
          $format = 'n';
948
          $item1 = date_format($date1, $format);
949
          $item2 = date_format($date2, $format);
950
          if ($year_diff == 0) {
951
            return intval($item2 - $item1);
952
          }
953
          elseif ($year_diff < 0) {
954
            $item_diff = 0 - $item1;
955
            $item_diff -= intval((abs($year_diff) - 1) * 12);
956
            return $item_diff - (12 - $item2);
957
          }
958
          else {
959
            $item_diff = 12 - $item1;
960
            $item_diff += intval(($year_diff - 1) * 12);
961
            return $item_diff + $item2;
962
          }
963
          break;
964

    
965
        case 'days':
966
          $format = 'z';
967
          $item1 = date_format($date1, $format);
968
          $item2 = date_format($date2, $format);
969
          if ($year_diff == 0) {
970
            return intval($item2 - $item1);
971
          }
972
          elseif ($year_diff < 0) {
973
            $item_diff = 0 - $item1;
974
            for ($i = 1; $i < abs($year_diff); $i++) {
975
              date_modify($date1, '-1 year');
976
              $item_diff -= date_days_in_year($date1);
977
            }
978
            return $item_diff - (date_days_in_year($date2) - $item2);
979
          }
980
          else {
981
            $item_diff = date_days_in_year($date1) - $item1;
982
            for ($i = 1; $i < $year_diff; $i++) {
983
              date_modify($date1, '+1 year');
984
              $item_diff += date_days_in_year($date1);
985
            }
986
            return $item_diff + $item2;
987
          }
988
          break;
989

    
990
        case 'weeks':
991
          $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
992
          $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
993

    
994
          $sign = ($year_diff < 0) ? -1 : 1;
995

    
996
          for ($i = 1; $i <= abs($year_diff); $i++) {
997
            date_modify($date1, (($sign > 0) ? '+': '-').'1 year');
998
            $week_diff += (date_iso_weeks_in_year($date1) * $sign);
999
          }
1000
          return $week_diff;
1001
      }
1002
    }
1003
    return NULL;
1004
  }
1005
}
1006

    
1007
/**
1008
 * Determines if the date element needs to be processed.
1009
 *
1010
 * Helper function to see if date element has been hidden by FAPI to see if it
1011
 * needs to be processed or just pass the value through. This is needed since
1012
 * normal date processing explands the date element into parts and then
1013
 * reconstructs it, which is not needed or desirable if the field is hidden.
1014
 *
1015
 * @param array $element
1016
 *   The date element to check.
1017
 *
1018
 * @return bool
1019
 *   TRUE if the element is effectively hidden, FALSE otherwise.
1020
 */
1021
function date_hidden_element($element) {
1022
  // @TODO What else needs to be tested to see if dates are hidden or disabled?
1023
  if ((isset($element['#access']) && empty($element['#access']))
1024
    || !empty($element['#programmed'])
1025
    || in_array($element['#type'], array('hidden', 'value'))) {
1026
    return TRUE;
1027
  }
1028
  return FALSE;
1029
}
1030

    
1031
/**
1032
 * Helper function for getting the format string for a date type.
1033
 *
1034
 * @param string $type
1035
 *   A date type format name.
1036
 *
1037
 * @return string
1038
 *   A date type format, like 'Y-m-d H:i:s'.
1039
 */
1040
function date_type_format($type) {
1041
  switch ($type) {
1042
    case DATE_ISO:
1043
      return DATE_FORMAT_ISO;
1044
    case DATE_UNIX:
1045
      return DATE_FORMAT_UNIX;
1046
    case DATE_DATETIME:
1047
      return DATE_FORMAT_DATETIME;
1048
    case DATE_ICAL:
1049
      return DATE_FORMAT_ICAL;
1050
  }
1051
}
1052

    
1053
/**
1054
 * Constructs an untranslated array of month names.
1055
 *
1056
 * Needed for CSS, translation functions, strtotime(), and other places
1057
 * that use the English versions of these words.
1058
 *
1059
 * @return array
1060
 *   An array of month names.
1061
 */
1062
function date_month_names_untranslated() {
1063
  static $month_names;
1064
  if (empty($month_names)) {
1065
    $month_names = array(
1066
      1 => 'January',
1067
      2 => 'February',
1068
      3 => 'March',
1069
      4 => 'April',
1070
      5 => 'May',
1071
      6 => 'June',
1072
      7 => 'July',
1073
      8 => 'August',
1074
      9 => 'September',
1075
      10 => 'October',
1076
      11 => 'November',
1077
      12 => 'December',
1078
    );
1079
  }
1080
  return $month_names;
1081
}
1082

    
1083
/**
1084
 * Returns a translated array of month names.
1085
 *
1086
 * @param bool $required
1087
 *   (optional) If FALSE, the returned array will include a blank value.
1088
 *   Defaults to FALSE.
1089
 *
1090
 * @return array
1091
 *   An array of month names.
1092
 */
1093
function date_month_names($required = FALSE) {
1094
  $month_names = array();
1095
  foreach (date_month_names_untranslated() as $key => $month) {
1096
    $month_names[$key] = t($month, array(), array('context' => 'Long month name'));
1097
  }
1098
  $none = array('' => '');
1099
  return !$required ? $none + $month_names : $month_names;
1100
}
1101

    
1102
/**
1103
 * Constructs a translated array of month name abbreviations
1104
 *
1105
 * @param bool $required
1106
 *   (optional) If FALSE, the returned array will include a blank value.
1107
 *   Defaults to FALSE.
1108
 * @param int $length
1109
 *   (optional) The length of the abbreviation. Defaults to 3.
1110
 *
1111
 * @return array
1112
 *   An array of month abbreviations.
1113
 */
1114
function date_month_names_abbr($required = FALSE, $length = 3) {
1115
  $month_names = array();
1116
  foreach (date_month_names_untranslated() as $key => $month) {
1117
    if ($length == 3) {
1118
      $month_names[$key] = t(substr($month, 0, $length), array());
1119
    }
1120
    else {
1121
      $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr'));
1122
    }
1123
  }
1124
  $none = array('' => '');
1125
  return !$required ? $none + $month_names : $month_names;
1126
}
1127

    
1128
/**
1129
 * Constructs an untranslated array of week days.
1130
 *
1131
 * Needed for CSS, translation functions, strtotime(), and other places
1132
 * that use the English versions of these words.
1133
 *
1134
 * @param bool $refresh
1135
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1136
 *
1137
 * @return array
1138
 *   An array of week day names
1139
 */
1140
function date_week_days_untranslated($refresh = TRUE) {
1141
  static $weekdays;
1142
  if ($refresh || empty($weekdays)) {
1143
    $weekdays = array(
1144
      'Sunday',
1145
      'Monday',
1146
      'Tuesday',
1147
      'Wednesday',
1148
      'Thursday',
1149
      'Friday',
1150
      'Saturday',
1151
    );
1152
  }
1153
  return $weekdays;
1154
}
1155

    
1156
/**
1157
 * Returns a translated array of week names.
1158
 *
1159
 * @param bool $required
1160
 *   (optional) If FALSE, the returned array will include a blank value.
1161
 *   Defaults to FALSE.
1162
 *
1163
 * @return array
1164
 *   An array of week day names
1165
 */
1166
function date_week_days($required = FALSE, $refresh = TRUE) {
1167
  $weekdays = array();
1168
  foreach (date_week_days_untranslated() as $key => $day) {
1169
    $weekdays[$key] = t($day, array(), array('context' => ''));
1170
  }
1171
  $none = array('' => '');
1172
  return !$required ? $none + $weekdays : $weekdays;
1173
}
1174

    
1175
/**
1176
 * Constructs a translated array of week day abbreviations.
1177
 *
1178
 * @param bool $required
1179
 *   (optional) If FALSE, the returned array will include a blank value.
1180
 *   Defaults to FALSE.
1181
 * @param bool $refresh
1182
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1183
 * @param int $length
1184
 *   (optional) The length of the abbreviation. Defaults to 3.
1185
 *
1186
 * @return array
1187
 *   An array of week day abbreviations
1188
 */
1189
function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
1190
  $weekdays = array();
1191
  switch ($length) {
1192
    case 1:
1193
      $context = 'day_abbr1';
1194
      break;
1195
    case 2:
1196
      $context = 'day_abbr2';
1197
      break;
1198
    default:
1199
      $context = '';
1200
      break;
1201
  }
1202
  foreach (date_week_days_untranslated() as $key => $day) {
1203
    $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context));
1204
  }
1205
  $none = array('' => '');
1206
  return !$required ? $none + $weekdays : $weekdays;
1207
}
1208

    
1209
/**
1210
 * Reorders weekdays to match the first day of the week.
1211
 *
1212
 * @param array $weekdays
1213
 *   An array of weekdays.
1214
 *
1215
 * @return array
1216
 *   An array of weekdays reordered to match the first day of the week.
1217
 */
1218
function date_week_days_ordered($weekdays) {
1219
  $first_day = variable_get('date_first_day', 0);
1220
  if ($first_day > 0) {
1221
    for ($i = 1; $i <= $first_day; $i++) {
1222
      $last = array_shift($weekdays);
1223
      array_push($weekdays, $last);
1224
    }
1225
  }
1226
  return $weekdays;
1227
}
1228

    
1229
/**
1230
 * Constructs an array of years.
1231
 *
1232
 * @param int $min
1233
 *   The minimum year in the array.
1234
 * @param int $max
1235
 *   The maximum year in the array.
1236
 * @param bool $required
1237
 *   (optional) If FALSE, the returned array will include a blank value.
1238
 *   Defaults to FALSE.
1239
 *
1240
 * @return array
1241
 *   An array of years in the selected range.
1242
 */
1243
function date_years($min = 0, $max = 0, $required = FALSE) {
1244
  // Ensure $min and $max are valid values.
1245
  if (empty($min)) {
1246
    $min = intval(date('Y', REQUEST_TIME) - 3);
1247
  }
1248
  if (empty($max)) {
1249
    $max = intval(date('Y', REQUEST_TIME) + 3);
1250
  }
1251
  $none = array(0 => '');
1252
  return !$required ? $none + drupal_map_assoc(range($min, $max)) : drupal_map_assoc(range($min, $max));
1253
}
1254

    
1255
/**
1256
 * Constructs an array of days in a month.
1257
 *
1258
 * @param bool $required
1259
 *   (optional) If FALSE, the returned array will include a blank value.
1260
 *   Defaults to FALSE.
1261
 * @param int $month
1262
 *   (optional) The month in which to find the number of days.
1263
 * @param int $year
1264
 *   (optional) The year in which to find the number of days.
1265
 *
1266
 * @return array
1267
 *   An array of days for the selected month.
1268
 */
1269
function date_days($required = FALSE, $month = NULL, $year = NULL) {
1270
  // If we have a month and year, find the right last day of the month.
1271
  if (!empty($month) && !empty($year)) {
1272
    $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC');
1273
    $max = $date->format('t');
1274
  }
1275
  // If there is no month and year given, default to 31.
1276
  if (empty($max)) {
1277
    $max = 31;
1278
  }
1279
  $none = array(0 => '');
1280
  return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
1281
}
1282

    
1283
/**
1284
 * Constructs an array of hours.
1285
 *
1286
 * @param string $format
1287
 *   A date format string.
1288
 * @param bool $required
1289
 *   (optional) If FALSE, the returned array will include a blank value.
1290
 *   Defaults to FALSE.
1291
 *
1292
 * @return array
1293
 *   An array of hours in the selected format.
1294
 */
1295
function date_hours($format = 'H', $required = FALSE) {
1296
  $hours = array();
1297
  if ($format == 'h' || $format == 'g') {
1298
    $min = 1;
1299
    $max = 12;
1300
  }
1301
  else {
1302
    $min = 0;
1303
    $max = 23;
1304
  }
1305
  for ($i = $min; $i <= $max; $i++) {
1306
    $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
1307
  }
1308
  $none = array('' => '');
1309
  return !$required ? $none + $hours : $hours;
1310
}
1311

    
1312
/**
1313
 * Constructs an array of minutes.
1314
 *
1315
 * @param string $format
1316
 *   A date format string.
1317
 * @param bool $required
1318
 *   (optional) If FALSE, the returned array will include a blank value.
1319
 *   Defaults to FALSE.
1320
 *
1321
 * @return array
1322
 *   An array of minutes in the selected format.
1323
 */
1324
function date_minutes($format = 'i', $required = FALSE, $increment = 1) {
1325
  $minutes = array();
1326
  // Ensure $increment has a value so we don't loop endlessly.
1327
  if (empty($increment)) {
1328
    $increment = 1;
1329
  }
1330
  for ($i = 0; $i < 60; $i += $increment) {
1331
    $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i;
1332
  }
1333
  $none = array('' => '');
1334
  return !$required ? $none + $minutes : $minutes;
1335
}
1336

    
1337
/**
1338
 * Constructs an array of seconds.
1339
 *
1340
 * @param string $format
1341
 *   A date format string.
1342
 * @param bool $required
1343
 *   (optional) If FALSE, the returned array will include a blank value.
1344
 *   Defaults to FALSE.
1345
 *
1346
 * @return array
1347
 *   An array of seconds in the selected format.
1348
 */
1349
function date_seconds($format = 's', $required = FALSE, $increment = 1) {
1350
  $seconds = array();
1351
  // Ensure $increment has a value so we don't loop endlessly.
1352
  if (empty($increment)) {
1353
    $increment = 1;
1354
  }
1355
  for ($i = 0; $i < 60; $i += $increment) {
1356
    $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
1357
  }
1358
  $none = array('' => '');
1359
  return !$required ? $none + $seconds : $seconds;
1360
}
1361

    
1362
/**
1363
 * Constructs an array of AM and PM options.
1364
 *
1365
 * @param bool $required
1366
 *   (optional) If FALSE, the returned array will include a blank value.
1367
 *   Defaults to FALSE.
1368
 *
1369
 * @return array
1370
 *   An array of AM and PM options.
1371
 */
1372
function date_ampm($required = FALSE) {
1373
  $none = array('' => '');
1374
  $ampm = array(
1375
    'am' => t('am', array(), array('context' => 'ampm')),
1376
    'pm' => t('pm', array(), array('context' => 'ampm')),
1377
  );
1378
  return !$required ? $none + $ampm : $ampm;
1379
}
1380

    
1381
/**
1382
 * Constructs an array of regex replacement strings for date format elements.
1383
 *
1384
 * @param bool $strict
1385
 *   Whether or not to force 2 digits for elements that sometimes allow either
1386
 *   1 or 2 digits.
1387
 *
1388
 * @return array
1389
 *   An array of date() format letters and their regex equivalents.
1390
 */
1391
function date_format_patterns($strict = FALSE) {
1392
  return array(
1393
    'd' => '\d{' . ($strict ? '2' : '1,2') . '}',
1394
    'm' => '\d{' . ($strict ? '2' : '1,2') . '}',
1395
    'h' => '\d{' . ($strict ? '2' : '1,2') . '}',
1396
    'H' => '\d{' . ($strict ? '2' : '1,2') . '}',
1397
    'i' => '\d{' . ($strict ? '2' : '1,2') . '}',
1398
    's' => '\d{' . ($strict ? '2' : '1,2') . '}',
1399
    'j' => '\d{1,2}',
1400
    'N' => '\d',
1401
    'S' => '\w{2}',
1402
    'w' => '\d',
1403
    'z' => '\d{1,3}',
1404
    'W' => '\d{1,2}',
1405
    'n' => '\d{1,2}',
1406
    't' => '\d{2}',
1407
    'L' => '\d',
1408
    'o' => '\d{4}',
1409
    'Y' => '-?\d{1,6}',
1410
    'y' => '\d{2}',
1411
    'B' => '\d{3}',
1412
    'g' => '\d{1,2}',
1413
    'G' => '\d{1,2}',
1414
    'e' => '\w*',
1415
    'I' => '\d',
1416
    'T' => '\w*',
1417
    'U' => '\d*',
1418
    'z' => '[+-]?\d*',
1419
    'O' => '[+-]?\d{4}',
1420
    // Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like
1421
    // German umlaut. Per http://drupal.org/node/1101284, we may need as little
1422
    // as 2 and as many as 5 characters in some languages.
1423
    'D' => '\S{2,5}',
1424
    'l' => '\S*',
1425
    'M' => '\S{2,5}',
1426
    'F' => '\S*',
1427
    'P' => '[+-]?\d{2}\:\d{2}',
1428
    'O' => '[+-]\d{4}',
1429
    'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})',
1430
    'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?',
1431
  );
1432
}
1433

    
1434
/**
1435
 * Constructs an array of granularity options and their labels.
1436
 *
1437
 * @return array
1438
 *   An array of translated date parts, keyed by their machine name.
1439
 */
1440
function date_granularity_names() {
1441
  return array(
1442
    'year' => t('Year', array(), array('context' => 'datetime')),
1443
    'month' => t('Month', array(), array('context' => 'datetime')),
1444
    'day' => t('Day', array(), array('context' => 'datetime')),
1445
    'hour' => t('Hour', array(), array('context' => 'datetime')),
1446
    'minute' => t('Minute', array(), array('context' => 'datetime')),
1447
    'second' => t('Second', array(), array('context' => 'datetime')),
1448
  );
1449
}
1450

    
1451
/**
1452
 * Sorts a granularity array.
1453
 *
1454
 * @param array $granularity
1455
 *   An array of date parts.
1456
 */
1457
function date_granularity_sorted($granularity) {
1458
  return array_intersect(array('year', 'month', 'day', 'hour', 'minute', 'second'), $granularity);
1459
}
1460

    
1461
/**
1462
 * Constructs an array of granularity based on a given precision.
1463
 *
1464
 * @param string $precision
1465
 *   A granularity item.
1466
 *
1467
 * @return array
1468
 *   A granularity array containing the given precision and all those above it.
1469
 *   For example, passing in 'month' will return array('year', 'month').
1470
 */
1471
function date_granularity_array_from_precision($precision) {
1472
  $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second');
1473
  switch ($precision) {
1474
    case 'year':
1475
      return array_slice($granularity_array, -6, 1);
1476
    case 'month':
1477
      return array_slice($granularity_array, -6, 2);
1478
    case 'day':
1479
      return array_slice($granularity_array, -6, 3);
1480
    case 'hour':
1481
      return array_slice($granularity_array, -6, 4);
1482
    case 'minute':
1483
      return array_slice($granularity_array, -6, 5);
1484
    default:
1485
      return $granularity_array;
1486
  }
1487
}
1488

    
1489
/**
1490
 * Give a granularity array, return the highest precision.
1491
 *
1492
 * @param array $granularity_array
1493
 *   An array of date parts.
1494
 *
1495
 * @return string
1496
 *   The most precise element in a granularity array.
1497
 */
1498
function date_granularity_precision($granularity_array) {
1499
  $input = date_granularity_sorted($granularity_array);
1500
  return array_pop($input);
1501
}
1502

    
1503
/**
1504
 * Constructs a valid DATETIME format string for the granularity of an item.
1505
 *
1506
 * @todo This function is no longer used as of
1507
 * http://drupalcode.org/project/date.git/commit/07efbb5.
1508
 */
1509
function date_granularity_format($granularity) {
1510
  if (is_array($granularity)) {
1511
    $granularity = date_granularity_precision($granularity);
1512
  }
1513
  $format = 'Y-m-d H:i:s';
1514
  switch ($granularity) {
1515
    case 'year':
1516
      return substr($format, 0, 1);
1517
    case 'month':
1518
      return substr($format, 0, 3);
1519
    case 'day':
1520
      return substr($format, 0, 5);
1521
    case 'hour';
1522
      return substr($format, 0, 7);
1523
    case 'minute':
1524
      return substr($format, 0, 9);
1525
    default:
1526
      return $format;
1527
  }
1528
}
1529

    
1530
/**
1531
 * Returns a translated array of timezone names.
1532
 *
1533
 * Cache the untranslated array, make the translated array a static variable.
1534
 *
1535
 * @param bool $required
1536
 *   (optional) If FALSE, the returned array will include a blank value.
1537
 *   Defaults to FALSE.
1538
 * @param bool $refresh
1539
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1540
 *
1541
 * @return array
1542
 *   An array of timezone names.
1543
 */
1544
function date_timezone_names($required = FALSE, $refresh = FALSE) {
1545
  static $zonenames;
1546
  if (empty($zonenames) || $refresh) {
1547
    $cached = cache_get('date_timezone_identifiers_list');
1548
    $zonenames = !empty($cached) ? $cached->data : array();
1549
    if ($refresh || empty($cached) || empty($zonenames)) {
1550
      $data = timezone_identifiers_list();
1551
      asort($data);
1552
      foreach ($data as $delta => $zone) {
1553
        // Because many timezones exist in PHP only for backward compatibility
1554
        // reasons and should not be used, the list is filtered by a regular
1555
        // expression.
1556
        if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
1557
          $zonenames[$zone] = $zone;
1558
        }
1559
      }
1560

    
1561
      if (!empty($zonenames)) {
1562
        cache_set('date_timezone_identifiers_list', $zonenames);
1563
      }
1564
    }
1565
    foreach ($zonenames as $zone) {
1566
      $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone)));
1567
    }
1568
  }
1569
  $none = array('' => '');
1570
  return !$required ? $none + $zonenames : $zonenames;
1571
}
1572

    
1573
/**
1574
 * Returns an array of system-allowed timezone abbreviations.
1575
 *
1576
 * Cache an array of just the abbreviation names because the whole
1577
 * timezone_abbreviations_list() is huge, so we don't want to retrieve it more
1578
 * than necessary.
1579
 *
1580
 * @param bool $refresh
1581
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1582
 *
1583
 * @return array
1584
 *   An array of allowed timezone abbreviations.
1585
 */
1586
function date_timezone_abbr($refresh = FALSE) {
1587
  $cached = cache_get('date_timezone_abbreviations');
1588
  $data = isset($cached->data) ? $cached->data : array();
1589
  if (empty($data) || $refresh) {
1590
    $data = array_keys(timezone_abbreviations_list());
1591
    cache_set('date_timezone_abbreviations', $data);
1592
  }
1593
  return $data;
1594
}
1595

    
1596
/**
1597
 * Formats a date, using a date type or a custom date format string.
1598
 *
1599
 * Reworked from Drupal's format_date function to handle pre-1970 and
1600
 * post-2038 dates and accept a date object instead of a timestamp as input.
1601
 * Translates formatted date results, unlike PHP function date_format().
1602
 * Should only be used for display, not input, because it can't be parsed.
1603
 *
1604
 * @param object $date
1605
 *   A date object.
1606
 * @param string $type
1607
 *   (optional) The date format to use. Can be 'small', 'medium' or 'large' for
1608
 *   the preconfigured date formats. If 'custom' is specified, then $format is
1609
 *   required as well. Defaults to 'medium'.
1610
 * @param string $format
1611
 *   (optional) A PHP date format string as required by date(). A backslash
1612
 *   should be used before a character to avoid interpreting the character as
1613
 *   part of a date format. Defaults to an empty string.
1614
 * @param string $langcode
1615
 *   (optional) Language code to translate to. Defaults to NULL.
1616
 *
1617
 * @return string
1618
 *   A translated date string in the requested format.
1619
 *
1620
 * @see format_date()
1621
 */
1622
function date_format_date($date, $type = 'medium', $format = '', $langcode = NULL) {
1623
  if (empty($date)) {
1624
    return '';
1625
  }
1626
  if ($type != 'custom') {
1627
    $format = variable_get('date_format_' . $type);
1628
  }
1629
  if ($type != 'custom' && empty($format)) {
1630
    $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
1631
  }
1632
  $format = date_limit_format($format, $date->granularity);
1633
  $max = strlen($format);
1634
  $datestring = '';
1635
  for ($i = 0; $i < $max; $i++) {
1636
    $c = $format[$i];
1637
    switch ($c) {
1638
      case 'l':
1639
        $datestring .= t($date->format('l'), array(), array('context' => '', 'langcode' => $langcode));
1640
        break;
1641
      case 'D':
1642
        $datestring .= t($date->format('D'), array(), array('context' => '', 'langcode' => $langcode));
1643
        break;
1644
      case 'F':
1645
        $datestring .= t($date->format('F'), array(), array('context' => 'Long month name', 'langcode' => $langcode));
1646
        break;
1647
      case 'M':
1648
        $datestring .= t($date->format('M'), array(), array('langcode' => $langcode));
1649
        break;
1650
      case 'A':
1651
      case 'a':
1652
        $datestring .= t($date->format($c), array(), array('context' => 'ampm', 'langcode' => $langcode));
1653
        break;
1654
      // The timezone name translations can use t().
1655
      case 'e':
1656
      case 'T':
1657
        $datestring .= t($date->format($c));
1658
        break;
1659
      // Remaining date parts need no translation.
1660
      case 'O':
1661
        $datestring .= sprintf('%s%02d%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1662
        break;
1663
      case 'P':
1664
        $datestring .= sprintf('%s%02d:%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1665
        break;
1666
      case 'Z':
1667
        $datestring .= date_offset_get($date);
1668
        break;
1669
      case '\\':
1670
        $datestring .= $format[++$i];
1671
        break;
1672
      case 'r':
1673
        $datestring .= date_format_date($date, 'custom', 'D, d M Y H:i:s O', $langcode);
1674
        break;
1675
      default:
1676
        if (strpos('BdcgGhHiIjLmnNosStTuUwWYyz', $c) !== FALSE) {
1677
          $datestring .= $date->format($c);
1678
        }
1679
        else {
1680
          $datestring .= $c;
1681
        }
1682
    }
1683
  }
1684
  return $datestring;
1685
}
1686

    
1687
/**
1688
 * Formats a time interval with granularity, including past and future context.
1689
 *
1690
 * @param object $date
1691
 *   The current date object.
1692
 * @param int $granularity
1693
 *   (optional) Number of units to display in the string. Defaults to 2.
1694
 *
1695
 * @return string
1696
 *   A translated string representation of the interval.
1697
 *
1698
 * @see format_interval()
1699
 */
1700
function date_format_interval($date, $granularity = 2, $display_ago = TRUE) {
1701
  // If no date is sent, then return nothing.
1702
  if (empty($date)) {
1703
    return NULL;
1704
  }
1705

    
1706
  $interval = REQUEST_TIME - $date->format('U');
1707
  if ($interval > 0) {
1708
    return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) :
1709
      t('!time', array('!time' => format_interval($interval, $granularity)));
1710
  }
1711
  else {
1712
    return format_interval(abs($interval), $granularity);
1713
  }
1714
}
1715

    
1716
/**
1717
 * A date object for the current time.
1718
 *
1719
 * @param object $timezone
1720
 *   (optional) Optionally force time to a specific timezone, defaults to user
1721
 *   timezone, if set, otherwise site timezone. Defaults to NULL.
1722
 *
1723
 * @param boolean $reset [optional]
1724
 *  Static cache reset
1725
 *
1726
 * @return object
1727
 *   The current time as a date object.
1728
 */
1729
function date_now($timezone = NULL, $reset = FALSE) {
1730
  $now = &drupal_static(__FUNCTION__);
1731

    
1732
  if (!isset($now) || $reset) {
1733
    $now = new DateObject('now', $timezone);
1734
  }
1735

    
1736
  return $now;
1737
}
1738
/**
1739
 * Determines if a timezone string is valid.
1740
 *
1741
 * @param string $timezone
1742
 *   A potentially invalid timezone string.
1743
 *
1744
 * @return bool
1745
 *   TRUE if the timezone is valid, FALSE otherwise.
1746
 */
1747
function date_timezone_is_valid($timezone) {
1748
  static $timezone_names;
1749
  if (empty($timezone_names)) {
1750
    $timezone_names = array_keys(date_timezone_names(TRUE));
1751
  }
1752
  return in_array($timezone, $timezone_names);
1753
}
1754

    
1755
/**
1756
 * Returns a timezone name to use as a default.
1757
 *
1758
 * @param bool $check_user
1759
 *   (optional) Whether or not to check for a user-configured timezone.
1760
 *   Defaults to TRUE.
1761
 *
1762
 * @return string
1763
 *   The default timezone for a user, if available, otherwise the site.
1764
 */
1765
function date_default_timezone($check_user = TRUE) {
1766
  global $user;
1767
  if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) {
1768
    return $user->timezone;
1769
  }
1770
  else {
1771
    $default = variable_get('date_default_timezone', '');
1772
    return empty($default) ? 'UTC' : $default;
1773
  }
1774
}
1775

    
1776
/**
1777
 * Returns a timezone object for the default timezone.
1778
 *
1779
 * @param bool $check_user
1780
 *   (optional) Whether or not to check for a user-configured timezone.
1781
 *   Defaults to TRUE.
1782
 *
1783
 * @return object
1784
 *   The default timezone for a user, if available, otherwise the site.
1785
 */
1786
function date_default_timezone_object($check_user = TRUE) {
1787
  return timezone_open(date_default_timezone($check_user));
1788
}
1789

    
1790
/**
1791
 * Identifies the number of days in a month for a date.
1792
 */
1793
function date_days_in_month($year, $month) {
1794
  // Pick a day in the middle of the month to avoid timezone shifts.
1795
  $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00';
1796
  $date = new DateObject($datetime);
1797
  return $date->format('t');
1798
}
1799

    
1800
/**
1801
 * Identifies the number of days in a year for a date.
1802
 *
1803
 * @param mixed $date
1804
 *   (optional) The current date object, or a date string. Defaults to NULL.
1805
 *
1806
 * @return integer
1807
 *   The number of days in the year.
1808
 */
1809
function date_days_in_year($date = NULL) {
1810
  if (empty($date)) {
1811
    $date = date_now();
1812
  }
1813
  elseif (!is_object($date)) {
1814
    $date = new DateObject($date);
1815
  }
1816
  if (is_object($date)) {
1817
    if ($date->format('L')) {
1818
      return 366;
1819
    }
1820
    else {
1821
      return 365;
1822
    }
1823
  }
1824
  return NULL;
1825
}
1826

    
1827
/**
1828
 * Identifies the number of ISO weeks in a year for a date.
1829
 *
1830
 * December 28 is always in the last ISO week of the year.
1831
 *
1832
 * @param mixed $date
1833
 *   (optional) The current date object, or a date string. Defaults to NULL.
1834
 *
1835
 * @return integer
1836
 *   The number of ISO weeks in a year.
1837
 */
1838
function date_iso_weeks_in_year($date = NULL) {
1839
  if (empty($date)) {
1840
    $date = date_now();
1841
  }
1842
  elseif (!is_object($date)) {
1843
    $date = new DateObject($date);
1844
  }
1845

    
1846
  if (is_object($date)) {
1847
    date_date_set($date, $date->format('Y'), 12, 28);
1848
    return $date->format('W');
1849
  }
1850
  return NULL;
1851
}
1852

    
1853
/**
1854
 * Returns day of week for a given date (0 = Sunday).
1855
 *
1856
 * @param mixed $date
1857
 *   (optional) A date, default is current local day. Defaults to NULL.
1858
 *
1859
 * @return int
1860
 *   The number of the day in the week.
1861
 */
1862
function date_day_of_week($date = NULL) {
1863
  if (empty($date)) {
1864
    $date = date_now();
1865
  }
1866
  elseif (!is_object($date)) {
1867
    $date = new DateObject($date);
1868
  }
1869

    
1870
  if (is_object($date)) {
1871
    return $date->format('w');
1872
  }
1873
  return NULL;
1874
}
1875

    
1876
/**
1877
 * Returns translated name of the day of week for a given date.
1878
 *
1879
 * @param mixed $date
1880
 *   (optional) A date, default is current local day. Defaults to NULL.
1881
 * @param string $abbr
1882
 *   (optional) Whether to return the abbreviated name for that day.
1883
 *   Defaults to TRUE.
1884
 *
1885
 * @return string
1886
 *   The name of the day in the week for that date.
1887
 */
1888
function date_day_of_week_name($date = NULL, $abbr = TRUE) {
1889
  if (!is_object($date)) {
1890
    $date = new DateObject($date);
1891
  }
1892
  $dow = date_day_of_week($date);
1893
  $days = $abbr ? date_week_days_abbr() : date_week_days();
1894
  return $days[$dow];
1895
}
1896

    
1897
/**
1898
 * Calculates the start and end dates for a calendar week.
1899
 *
1900
 * The dates are adjusted to use the chosen first day of week for this site.
1901
 *
1902
 * @param int $week
1903
 *   The week value.
1904
 * @param int $year
1905
 *   The year value.
1906
 *
1907
 * @return array
1908
 *   A numeric array containing the start and end dates of a week.
1909
 */
1910
function date_week_range($week, $year) {
1911
  if (variable_get('date_api_use_iso8601', FALSE)) {
1912
    return date_iso_week_range($week, $year);
1913
  }
1914
  $min_date = new DateObject($year . '-01-01 00:00:00');
1915
  $min_date->setTimezone(date_default_timezone_object());
1916

    
1917
  // Move to the right week.
1918
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
1919

    
1920
  // Move backwards to the first day of the week.
1921
  $first_day = variable_get('date_first_day', 0);
1922
  $day_wday = date_format($min_date, 'w');
1923
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
1924

    
1925
  // Move forwards to the last day of the week.
1926
  $max_date = clone($min_date);
1927
  date_modify($max_date, '+7 days');
1928

    
1929
  if (date_format($min_date, 'Y') != $year) {
1930
    $min_date = new DateObject($year . '-01-01 00:00:00');
1931
  }
1932
  return array($min_date, $max_date);
1933
}
1934

    
1935
/**
1936
 * Calculates the start and end dates for an ISO week.
1937
 *
1938
 * @param int $week
1939
 *   The week value.
1940
 * @param int $year
1941
 *   The year value.
1942
 *
1943
 * @return array
1944
 *   A numeric array containing the start and end dates of an ISO week.
1945
 */
1946
function date_iso_week_range($week, $year) {
1947
  // Get to the last ISO week of the previous year.
1948
  $min_date = new DateObject(($year - 1) . '-12-28 00:00:00');
1949
  date_timezone_set($min_date, date_default_timezone_object());
1950

    
1951
  // Find the first day of the first ISO week in the year.
1952
  date_modify($min_date, '+1 Monday');
1953

    
1954
  // Jump ahead to the desired week for the beginning of the week range.
1955
  if ($week > 1) {
1956
    date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
1957
  }
1958

    
1959
  // Move forwards to the last day of the week.
1960
  $max_date = clone($min_date);
1961
  date_modify($max_date, '+7 days');
1962
  return array($min_date, $max_date);
1963
}
1964

    
1965
/**
1966
 * The number of calendar weeks in a year.
1967
 *
1968
 * PHP week functions return the ISO week, not the calendar week.
1969
 *
1970
 * @param int $year
1971
 *   A year value.
1972
 *
1973
 * @return int
1974
 *   Number of calendar weeks in selected year.
1975
 */
1976
function date_weeks_in_year($year) {
1977
  $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC');
1978
  date_modify($date, '-1 day');
1979
  return date_week($date->format('Y-m-d'));
1980
}
1981

    
1982
/**
1983
 * The calendar week number for a date.
1984
 *
1985
 * PHP week functions return the ISO week, not the calendar week.
1986
 *
1987
 * @param string $date
1988
 *   A date string in the format Y-m-d.
1989
 *
1990
 * @return int
1991
 *   The calendar week number.
1992
 */
1993
function date_week($date) {
1994
  $date = substr($date, 0, 10);
1995
  $parts = explode('-', $date);
1996

    
1997
  $date = new DateObject($date . ' 12:00:00', 'UTC');
1998

    
1999
  // If we are using ISO weeks, this is easy.
2000
  if (variable_get('date_api_use_iso8601', FALSE)) {
2001
    return intval($date->format('W'));
2002
  }
2003

    
2004
  $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
2005
  $week = intval($date->format('W'));
2006
  $year_week = intval(date_format($year_date, 'W'));
2007
  $date_year = intval($date->format('o'));
2008

    
2009
  // Remove the leap week if it's present.
2010
  if ($date_year > intval($parts[0])) {
2011
    $last_date = clone($date);
2012
    date_modify($last_date, '-7 days');
2013
    $week = date_format($last_date, 'W') + 1;
2014
  }
2015
  elseif ($date_year < intval($parts[0])) {
2016
    $week = 0;
2017
  }
2018

    
2019
  if ($year_week != 1) {
2020
    $week++;
2021
  }
2022

    
2023
  // Convert to ISO-8601 day number, to match weeks calculated above.
2024
  $iso_first_day = 1 + (variable_get('date_first_day', 0) + 6) % 7;
2025

    
2026
  // If it's before the starting day, it's the previous week.
2027
  if (intval($date->format('N')) < $iso_first_day) {
2028
    $week--;
2029
  }
2030

    
2031
  // If the year starts before, it's an extra week at the beginning.
2032
  if (intval(date_format($year_date, 'N')) < $iso_first_day) {
2033
    $week++;
2034
  }
2035

    
2036
  return $week;
2037
}
2038

    
2039
/**
2040
 * Helper function to left pad date parts with zeros.
2041
 *
2042
 * Provided because this is needed so often with dates.
2043
 *
2044
 * @param int $value
2045
 *   The value to pad.
2046
 * @param int $size
2047
 *   (optional) Total size expected, usually 2 or 4. Defaults to 2.
2048
 *
2049
 * @return string
2050
 *   The padded value.
2051
 */
2052
function date_pad($value, $size = 2) {
2053
  return sprintf("%0" . $size . "d", $value);
2054
}
2055

    
2056
/**
2057
 * Determines if the granularity contains a time portion.
2058
 *
2059
 * @param array $granularity
2060
 *   An array of allowed date parts, all others will be removed.
2061
 *
2062
 * @return bool
2063
 *   TRUE if the granularity contains a time portion, FALSE otherwise.
2064
 */
2065
function date_has_time($granularity) {
2066
  if (!is_array($granularity)) {
2067
    $granularity = array();
2068
  }
2069
  return (bool) count(array_intersect($granularity, array('hour', 'minute', 'second')));
2070
}
2071

    
2072
/**
2073
 * Determines if the granularity contains a date portion.
2074
 *
2075
 * @param array $granularity
2076
 *   An array of allowed date parts, all others will be removed.
2077
 *
2078
 * @return bool
2079
 *   TRUE if the granularity contains a date portion, FALSE otherwise.
2080
 */
2081
function date_has_date($granularity) {
2082
  if (!is_array($granularity)) {
2083
    $granularity = array();
2084
  }
2085
  return (bool) count(array_intersect($granularity, array('year', 'month', 'day')));
2086
}
2087

    
2088
/**
2089
 * Helper function to get a format for a specific part of a date field.
2090
 *
2091
 * @param string $part
2092
 *   The date field part, either 'time' or 'date'.
2093
 * @param string $format
2094
 *   A date format string.
2095
 *
2096
 * @return string
2097
 *   The date format for the given part.
2098
 */
2099
function date_part_format($part, $format) {
2100
  switch ($part) {
2101
    case 'date':
2102
      return date_limit_format($format, array('year', 'month', 'day'));
2103
    case 'time':
2104
      return date_limit_format($format, array('hour', 'minute', 'second'));
2105
    default:
2106
      return date_limit_format($format, array($part));
2107
  }
2108
}
2109

    
2110
/**
2111
 * Limits a date format to include only elements from a given granularity array.
2112
 *
2113
 * Example:
2114
 *   date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
2115
 *   returns 'F j, Y'
2116
 *
2117
 * @param string $format
2118
 *   A date format string.
2119
 * @param array $granularity
2120
 *   An array of allowed date parts, all others will be removed.
2121
 *
2122
 * @return string
2123
 *   The format string with all other elements removed.
2124
 */
2125
function date_limit_format($format, $granularity) {
2126
  // Use the advanced drupal_static() pattern to improve performance.
2127
  static $drupal_static_fast;
2128
  if (!isset($drupal_static_fast)) {
2129
    $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__);
2130
  }
2131
  $formats = &$drupal_static_fast['formats'];
2132
  $format_granularity_cid = $format .'|'. implode(',', $granularity);
2133
  if (isset($formats[$format_granularity_cid])) {
2134
    return $formats[$format_granularity_cid];
2135
  }
2136

    
2137
  // If punctuation has been escaped, remove the escaping. Done using strtr()
2138
  // because it is easier than getting the escape character extracted using
2139
  // preg_replace().
2140
  $replace = array(
2141
    '\-' => '-',
2142
    '\:' => ':',
2143
    "\'" => "'",
2144
    '\. ' => ' . ',
2145
    '\,' => ',',
2146
  );
2147
  $format = strtr($format, $replace);
2148

    
2149
  // Get the 'T' out of ISO date formats that don't have both date and time.
2150
  if (!date_has_time($granularity) || !date_has_date($granularity)) {
2151
    $format = str_replace('\T', ' ', $format);
2152
    $format = str_replace('T', ' ', $format);
2153
  }
2154

    
2155
  $regex = array();
2156
  if (!date_has_time($granularity)) {
2157
    $regex[] = '((?<!\\\\)[a|A])';
2158
  }
2159
  // Create regular expressions to remove selected values from string.
2160
  // Use (?<!\\\\) to keep escaped letters from being removed.
2161
  foreach (date_nongranularity($granularity) as $element) {
2162
    switch ($element) {
2163
      case 'year':
2164
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
2165
        break;
2166
      case 'day':
2167
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[l|D|d|dS|j|jS|N|w|W|z]{1,2})';
2168
        break;
2169
      case 'month':
2170
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
2171
        break;
2172
      case 'hour':
2173
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
2174
        break;
2175
      case 'minute':
2176
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
2177
        break;
2178
      case 'second':
2179
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
2180
        break;
2181
      case 'timezone':
2182
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
2183
        break;
2184

    
2185
    }
2186
  }
2187
  // Remove empty parentheses, brackets, pipes.
2188
  $regex[] = '(\(\))';
2189
  $regex[] = '(\[\])';
2190
  $regex[] = '(\|\|)';
2191

    
2192
  // Remove selected values from string.
2193
  $format = trim(preg_replace($regex, array(), $format));
2194
  // Remove orphaned punctuation at the beginning of the string.
2195
  $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
2196
  // Remove orphaned punctuation at the end of the string.
2197
  $format = preg_replace('([\-/,:\']$)', '', $format);
2198
  $format = preg_replace('(\\$)', '', $format);
2199

    
2200
  // Trim any whitespace from the result.
2201
  $format = trim($format);
2202

    
2203
  // After removing the non-desired parts of the format, test if the only things
2204
  // left are escaped, non-date, characters. If so, return nothing.
2205
  // Using S instead of w to pick up non-ASCII characters.
2206
  $test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
2207
  if (empty($test)) {
2208
    $format = '';
2209
  }
2210

    
2211
  // Store the return value in the static array for performance.
2212
  $formats[$format_granularity_cid] = $format;
2213

    
2214
  return $format;
2215
}
2216

    
2217
/**
2218
 * Converts a format to an ordered array of granularity parts.
2219
 *
2220
 * Example:
2221
 *   date_format_order('m/d/Y H:i')
2222
 *   returns
2223
 *     array(
2224
 *       0 => 'month',
2225
 *       1 => 'day',
2226
 *       2 => 'year',
2227
 *       3 => 'hour',
2228
 *       4 => 'minute',
2229
 *     );
2230
 *
2231
 * @param string $format
2232
 *   A date format string.
2233
 *
2234
 * @return array
2235
 *   An array of ordered granularity elements from the given format string.
2236
 */
2237
function date_format_order($format) {
2238
  $order = array();
2239
  if (empty($format)) {
2240
    return $order;
2241
  }
2242

    
2243
  $max = strlen($format);
2244
  for ($i = 0; $i <= $max; $i++) {
2245
    if (!isset($format[$i])) {
2246
      break;
2247
    }
2248
    switch ($format[$i]) {
2249
      case 'd':
2250
      case 'j':
2251
        $order[] = 'day';
2252
        break;
2253
      case 'F':
2254
      case 'M':
2255
      case 'm':
2256
      case 'n':
2257
        $order[] = 'month';
2258
        break;
2259
      case 'Y':
2260
      case 'y':
2261
        $order[] = 'year';
2262
        break;
2263
      case 'g':
2264
      case 'G':
2265
      case 'h':
2266
      case 'H':
2267
        $order[] = 'hour';
2268
        break;
2269
      case 'i':
2270
        $order[] = 'minute';
2271
        break;
2272
      case 's':
2273
        $order[] = 'second';
2274
        break;
2275
    }
2276
  }
2277
  return $order;
2278
}
2279

    
2280
/**
2281
 * Strips out unwanted granularity elements.
2282
 *
2283
 * @param array $granularity
2284
 *   An array like ('year', 'month', 'day', 'hour', 'minute', 'second');
2285
 *
2286
 * @return array
2287
 *   A reduced set of granularitiy elements.
2288
 */
2289
function date_nongranularity($granularity) {
2290
  return array_diff(array('year', 'month', 'day', 'hour', 'minute', 'second', 'timezone'), (array) $granularity);
2291
}
2292

    
2293
/**
2294
 * Implements hook_element_info().
2295
 */
2296
function date_api_element_info() {
2297
  module_load_include('inc', 'date_api', 'date_api_elements');
2298
  return _date_api_element_info();
2299
}
2300

    
2301
/**
2302
 * Implements hook_theme().
2303
 */
2304
function date_api_theme($existing, $type, $theme, $path) {
2305
  $base = array(
2306
    'file' => 'theme.inc',
2307
    'path' => "$path/theme",
2308
  );
2309
  return array(
2310
    'date_nav_title' => $base + array('variables' => array('granularity' => NULL, 'view' => NULL, 'link' => NULL, 'format' => NULL)),
2311
    'date_timezone' => $base + array('render element' => 'element'),
2312
    'date_select' => $base + array('render element' => 'element'),
2313
    'date_text' => $base + array('render element' => 'element'),
2314
    'date_select_element' => $base + array('render element' => 'element'),
2315
    'date_textfield_element' => $base + array('render element' => 'element'),
2316
    'date_part_hour_prefix' => $base + array('render element' => 'element'),
2317
    'date_part_minsec_prefix' => $base + array('render element' => 'element'),
2318
    'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2319
    'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2320
    'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2321
    'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2322
    'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2323
    'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2324
    'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2325
    'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2326
    'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2327
    'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2328
    'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'),
2329
    'date_calendar_day' => $base + array('variables' => array('date' => NULL)),
2330
    'date_time_ago' => $base + array('variables' => array('start_date' => NULL, 'end_date' => NULL, 'interval' => NULL)),
2331
  );
2332
}
2333

    
2334
/**
2335
 * Function to figure out which local timezone applies to a date and select it.
2336
 *
2337
 * @param string $handling
2338
 *   The timezone handling.
2339
 * @param string $timezone
2340
 *   (optional) A timezone string. Defaults to an empty string.
2341
 *
2342
 * @return string
2343
 *   The timezone string.
2344
 */
2345
function date_get_timezone($handling, $timezone = '') {
2346
  switch ($handling) {
2347
    case 'date':
2348
      $timezone = !empty($timezone) ? $timezone : date_default_timezone();
2349
      break;
2350
    case 'utc':
2351
      $timezone = 'UTC';
2352
      break;
2353
    default:
2354
      $timezone = date_default_timezone();
2355
  }
2356
  return $timezone > '' ? $timezone : date_default_timezone();
2357
}
2358

    
2359
/**
2360
 * Function to figure out which db timezone applies to a date and select it.
2361
 *
2362
 * @param string $handling
2363
 *   The timezone handling.
2364
 * @param string $timezone
2365
 *   (optional) A timezone string. Defaults to an empty string.
2366
 *
2367
 * @return string
2368
 *   The timezone string.
2369
 */
2370
function date_get_timezone_db($handling, $timezone = '') {
2371
  switch ($handling) {
2372
    case 'none':
2373
      $timezone = date_default_timezone();
2374
      break;
2375
    default:
2376
      $timezone = 'UTC';
2377
      break;
2378
  }
2379
  return $timezone > '' ? $timezone : 'UTC';
2380
}
2381

    
2382
/**
2383
 * Helper function for converting back and forth from '+1' to 'First'.
2384
 */
2385
function date_order_translated() {
2386
  return array(
2387
    '+1' => t('First', array(), array('context' => 'date_order')),
2388
    '+2' => t('Second', array(), array('context' => 'date_order')),
2389
    '+3' => t('Third', array(), array('context' => 'date_order')),
2390
    '+4' => t('Fourth', array(), array('context' => 'date_order')),
2391
    '+5' => t('Fifth', array(), array('context' => 'date_order')),
2392
    '-1' => t('Last', array(), array('context' => 'date_order_reverse')),
2393
    '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')),
2394
    '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')),
2395
    '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')),
2396
    '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')),
2397
  );
2398
}
2399

    
2400
/**
2401
 * Creates an array of ordered strings, using English text when possible.
2402
 */
2403
function date_order() {
2404
  return array(
2405
    '+1' => 'First',
2406
    '+2' => 'Second',
2407
    '+3' => 'Third',
2408
    '+4' => 'Fourth',
2409
    '+5' => 'Fifth',
2410
    '-1' => 'Last',
2411
    '-2' => '-2',
2412
    '-3' => '-3',
2413
    '-4' => '-4',
2414
    '-5' => '-5',
2415
  );
2416
}
2417

    
2418
/**
2419
 * Tests validity of a date range string.
2420
 *
2421
 * @param string $string
2422
 *   A min and max year string like '-3:+1'a.
2423
 *
2424
 * @return bool
2425
 *   TRUE if the date range is valid, FALSE otherwise.
2426
 */
2427
function date_range_valid($string) {
2428
  $matches = preg_match('@^(\-[0-9]+|[0-9]{4}):([\+|\-][0-9]+|[0-9]{4})$@', $string);
2429
  return $matches < 1 ? FALSE : TRUE;
2430
}
2431

    
2432
/**
2433
 * Splits a string like -3:+3 or 2001:2010 into an array of min and max years.
2434
 *
2435
 * Center the range around the current year, if any, but expand it far
2436
 * enough so it will pick up the year value in the field in case
2437
 * the value in the field is outside the initial range.
2438
 *
2439
 * @param string $string
2440
 *   A min and max year string like '-3:+1'.
2441
 * @param object $date
2442
 *   (optional) A date object. Defaults to NULL.
2443
 *
2444
 * @return array
2445
 *   A numerically indexed array, containing a minimum and maximum year.
2446
 */
2447
function date_range_years($string, $date = NULL) {
2448
  $this_year = date_format(date_now(), 'Y');
2449
  list($min_year, $max_year) = explode(':', $string);
2450

    
2451
  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
2452
  $plus_pattern = '@[\+|\-][0-9]{1,4}@';
2453
  $year_pattern = '@^[0-9]{4}@';
2454
  if (!preg_match($year_pattern, $min_year, $matches)) {
2455
    if (preg_match($plus_pattern, $min_year, $matches)) {
2456
      $min_year = $this_year + $matches[0];
2457
    }
2458
    else {
2459
      $min_year = $this_year;
2460
    }
2461
  }
2462
  if (!preg_match($year_pattern, $max_year, $matches)) {
2463
    if (preg_match($plus_pattern, $max_year, $matches)) {
2464
      $max_year = $this_year + $matches[0];
2465
    }
2466
    else {
2467
      $max_year = $this_year;
2468
    }
2469
  }
2470
  // We expect the $min year to be less than the $max year.
2471
  // Some custom values for -99:+99 might not obey that.
2472
  if ($min_year > $max_year) {
2473
    $temp = $max_year;
2474
    $max_year = $min_year;
2475
    $min_year = $temp;
2476
  }
2477
  // If there is a current value, stretch the range to include it.
2478
  $value_year = is_object($date) ? $date->format('Y') : '';
2479
  if (!empty($value_year)) {
2480
    $min_year = min($value_year, $min_year);
2481
    $max_year = max($value_year, $max_year);
2482
  }
2483
  return array($min_year, $max_year);
2484
}
2485

    
2486
/**
2487
 * Converts a min and max year into a string like '-3:+1'.
2488
 *
2489
 * @param array $years
2490
 *   A numerically indexed array, containing a minimum and maximum year.
2491
 *
2492
 * @return string
2493
 *   A min and max year string like '-3:+1'.
2494
 */
2495
function date_range_string($years) {
2496
  $this_year = date_format(date_now(), 'Y');
2497

    
2498
  if ($years[0] < $this_year) {
2499
    $min = '-' . ($this_year - $years[0]);
2500
  }
2501
  else {
2502
    $min = '+' . ($years[0] - $this_year);
2503
  }
2504

    
2505
  if ($years[1] < $this_year) {
2506
    $max = '-' . ($this_year - $years[1]);
2507
  }
2508
  else {
2509
    $max = '+' . ($years[1] - $this_year);
2510
  }
2511

    
2512
  return $min . ':' . $max;
2513
}
2514

    
2515
/**
2516
 * Temporary helper to re-create equivalent of content_database_info().
2517
 */
2518
function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) {
2519
  return array(
2520
    'columns' => $field['storage']['details']['sql'][$revision],
2521
    'table' => _field_sql_storage_tablename($field),
2522
  );
2523
}
2524

    
2525
/**
2526
 * Implements hook_form_FORM_ID_alter() for system_regional_settings().
2527
 *
2528
 * Add a form element to configure whether or not week numbers are ISO-8601, the
2529
 * default is FALSE (US/UK/AUS norm).
2530
 */
2531
function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) {
2532
  $form['locale']['date_api_use_iso8601'] = array(
2533
    '#type' => 'checkbox',
2534
    '#title' => t('Use ISO-8601 week numbers'),
2535
    '#default_value' => variable_get('date_api_use_iso8601', FALSE),
2536
    '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'),
2537
  );
2538
  $form['#validate'][] = 'date_api_form_system_settings_validate';
2539
}
2540

    
2541
/**
2542
 * Validate that the option to use ISO weeks matches first day of week choice.
2543
 */
2544
function date_api_form_system_settings_validate(&$form, &$form_state) {
2545
  $form_values = $form_state['values'];
2546
  if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) {
2547
    form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.'));
2548
  }
2549
}
2550

    
2551
/**
2552
 * Creates an array of date format types for use as an options list.
2553
 */
2554
function date_format_type_options() {
2555
  $options = array();
2556
  $format_types = system_get_date_types();
2557
  if (!empty($format_types)) {
2558
    foreach ($format_types as $type => $type_info) {
2559
      $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')';
2560
    }
2561
  }
2562
  return $options;
2563
}
2564

    
2565
/**
2566
 * Creates an example date.
2567
 *
2568
 * This ensures a clear difference between month and day, and 12 and 24 hours.
2569
 */
2570
function date_example_date() {
2571
  $now = date_now();
2572
  if (date_format($now, 'M') == date_format($now, 'F')) {
2573
    date_modify($now, '+1 month');
2574
  }
2575
  if (date_format($now, 'm') == date_format($now, 'd')) {
2576
    date_modify($now, '+1 day');
2577
  }
2578
  if (date_format($now, 'H') == date_format($now, 'h')) {
2579
    date_modify($now, '+12 hours');
2580
  }
2581
  return $now;
2582
}
2583

    
2584
/**
2585
 * Determine if a start/end date combination qualify as 'All day'.
2586
 *
2587
 * @param string $string1
2588
 *   A string date in datetime format for the 'start' date.
2589
 * @param string $string2
2590
 *   A string date in datetime format for the 'end' date.
2591
 * @param string $granularity
2592
 *   (optional) The granularity of the date. Defaults to 'second'.
2593
 * @param int $increment
2594
 *   (optional) The increment of the date. Defaults to 1.
2595
 *
2596
 * @return bool
2597
 *   TRUE if the date is all day, FALSE otherwise.
2598
 */
2599
function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) {
2600
  if (empty($string1) || empty($string2)) {
2601
    return FALSE;
2602
  }
2603
  elseif (!in_array($granularity, array('hour', 'minute', 'second'))) {
2604
    return FALSE;
2605
  }
2606

    
2607
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches);
2608
  $count = count($matches);
2609
  $date1 = $count > 1 ? $matches[1] : '';
2610
  $time1 = $count > 2 ? $matches[2] : '';
2611
  $hour1 = $count > 3 ? intval($matches[3]) : 0;
2612
  $min1 = $count > 4 ? intval($matches[4]) : 0;
2613
  $sec1 = $count > 5 ? intval($matches[5]) : 0;
2614
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches);
2615
  $count = count($matches);
2616
  $date2 = $count > 1 ? $matches[1] : '';
2617
  $time2 = $count > 2 ? $matches[2] : '';
2618
  $hour2 = $count > 3 ? intval($matches[3]) : 0;
2619
  $min2 = $count > 4 ? intval($matches[4]) : 0;
2620
  $sec2 = $count > 5 ? intval($matches[5]) : 0;
2621
  if (empty($date1) || empty($date2)) {
2622
    return FALSE;
2623
  }
2624
  if (empty($time1) || empty($time2)) {
2625
    return FALSE;
2626
  }
2627

    
2628
  $tmp = date_seconds('s', TRUE, $increment);
2629
  $max_seconds = intval(array_pop($tmp));
2630
  $tmp = date_minutes('i', TRUE, $increment);
2631
  $max_minutes = intval(array_pop($tmp));
2632

    
2633
  // See if minutes and seconds are the maximum allowed for an increment or the
2634
  // maximum possible (59), or 0.
2635
  switch ($granularity) {
2636
    case 'second':
2637
      $min_match = $time1 == '00:00:00'
2638
        || ($hour1 == 0 && $min1 == 0 && $sec1 == 0);
2639
      $max_match = $time2 == '00:00:00'
2640
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59)))
2641
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0);
2642
      break;
2643
    case 'minute':
2644
      $min_match = $time1 == '00:00:00'
2645
        || ($hour1 == 0 && $min1 == 0);
2646
      $max_match = $time2 == '00:00:00'
2647
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
2648
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
2649
      break;
2650
    case 'hour':
2651
      $min_match = $time1 == '00:00:00'
2652
        || ($hour1 == 0);
2653
      $max_match = $time2 == '00:00:00'
2654
        || ($hour2 == 23)
2655
        || ($hour1 == 0 && $hour2 == 0);
2656
      break;
2657
    default:
2658
      $min_match = TRUE;
2659
      $max_match = FALSE;
2660
  }
2661

    
2662
  if ($min_match && $max_match) {
2663
    return TRUE;
2664
  }
2665

    
2666
  return FALSE;
2667
}
2668

    
2669
/**
2670
 * Helper function to round minutes and seconds to requested value.
2671
 */
2672
function date_increment_round(&$date, $increment) {
2673
  // Round minutes and seconds, if necessary.
2674
  if (is_object($date) && $increment > 1) {
2675
    $day = intval(date_format($date, 'j'));
2676
    $hour = intval(date_format($date, 'H'));
2677
    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
2678
    $minute = intval(date_format($date, 'i'));
2679
    if ($second == 60) {
2680
      $minute += 1;
2681
      $second = 0;
2682
    }
2683
    $minute = intval(round($minute / $increment) * $increment);
2684
    if ($minute == 60) {
2685
      $hour += 1;
2686
      $minute = 0;
2687
    }
2688
    date_time_set($date, $hour, $minute, $second);
2689
    if ($hour == 24) {
2690
      $day += 1;
2691
      $hour = 0;
2692
      $year = date_format($date, 'Y');
2693
      $month = date_format($date, 'n');
2694
      date_date_set($date, $year, $month, $day);
2695
    }
2696
  }
2697
  return $date;
2698
}
2699

    
2700
/**
2701
 * Determines if a date object is valid.
2702
 *
2703
 * @param object $date
2704
 *   The date object to check.
2705
 *
2706
 * @return bool
2707
 *   TRUE if the date is a valid date object, FALSE otherwise.
2708
 */
2709
function date_is_date($date) {
2710
  if (empty($date) || !is_object($date) || !empty($date->errors)) {
2711
    return FALSE;
2712
  }
2713
  return TRUE;
2714
}
2715

    
2716
/**
2717
 * This function will replace ISO values that have the pattern 9999-00-00T00:00:00
2718
 * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO
2719
 * dates and ensure that date objects created from this value contain a valid month
2720
 * and day. Without this fix, the ISO date '2020-00-00T00:00:00' would be created as
2721
 * November 30, 2019 (the previous day in the previous month).
2722
 *
2723
 * @param string $iso_string
2724
 *   An ISO string that needs to be made into a complete, valid date.
2725
 *
2726
 * @TODO Expand on this to work with all sorts of partial ISO dates.
2727
 */
2728
function date_make_iso_valid($iso_string) {
2729
  // If this isn't a value that uses an ISO pattern, there is nothing to do.
2730
  if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) {
2731
    return $iso_string;
2732
  }
2733
  // First see if month and day parts are '-00-00'.
2734
  if (substr($iso_string, 4, 6) == '-00-00') {
2735
    return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string);
2736
  }
2737
  // Then see if the day part is '-00'.
2738
  elseif (substr($iso_string, 7, 3) == '-00') {
2739
    return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string);
2740
  }
2741

    
2742
  // Fall through, no changes required.
2743
  return $iso_string;
2744
}