Projet

Général

Profil

Paste
Télécharger (89 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api.module @ 599a39cd

1
<?php
2

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

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

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

    
36
define('DATE_REGEX_ISO', '/(\d{4})?(-(\d{2}))?(-(\d{2}))?([T\s](\d{2}))?(:(\d{2}))?(:(\d{2}))?/');
37
define('DATE_REGEX_DATETIME', '/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):?(\d{2})?/');
38
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})?)?/');
39
define('DATE_REGEX_ICAL_DATE', '/(\d{4})(\d{2})(\d{2})/');
40
define('DATE_REGEX_ICAL_DATETIME', '/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?/');
41

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

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

    
62
      if (module_exists('date_tools')) {
63
        $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')));
64
      }
65
      else {
66
        $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.');
67
      }
68

    
69
      $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>';
70

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

    
132
/**
133
 * Extend PHP DateTime class.
134
 *
135
 * Adds granularity handling, merge functionality and slightly more flexible
136
 * 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(
147
    'year',
148
    'month',
149
    'day',
150
    'hour',
151
    'minute',
152
    'second',
153
    'timezone',
154
  );
155
  private $serializedTime;
156
  private $serializedTimezone;
157

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

    
175
  /**
176
   * Re-builds the object using local variables.
177
   */
178
  public function __wakeup() {
179
    $this->__construct($this->serializedTime, new DateTimeZone($this->serializedTimezone));
180
  }
181

    
182
  /**
183
   * Returns the date object as a string.
184
   *
185
   * @return string
186
   *   The date object formatted as a string.
187
   */
188
  public function __toString() {
189
    return $this->format(DATE_FORMAT_DATETIME) . ' ' . $this->getTimeZone()->getName();
190
  }
191

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

    
209
    // Store the raw time input so it is available for validation.
210
    $this->originalTime = $time;
211

    
212
    // Allow string timezones.
213
    if (!empty($tz) && !is_object($tz)) {
214
      $tz = new DateTimeZone($tz);
215
    }
216

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

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

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

    
293
      // If the original $tz has a name, use it.
294
      if (preg_match('/[a-zA-Z]/', $tz->getName())) {
295
        $this->setTimezone($tz);
296
      }
297
      // We have no information about the timezone so must fallback to a
298
      // default.
299
      else {
300
        $this->setTimezone(new DateTimeZone("UTC"));
301
        $this->errors['timezone'] = t('No valid timezone name was provided.');
302
      }
303
    }
304
  }
305

    
306
  /**
307
   * Merges two date objects together using the current date values as defaults.
308
   *
309
   * @param DateObject $other
310
   *   Another date object to merge with.
311
   *
312
   * @return DateObject
313
   *   A merged date object.
314
   */
315
  public function merge(DateObject $other) {
316
    $other_tz = $other->getTimezone();
317
    $this_tz = $this->getTimezone();
318
    // Figure out which timezone to use for combination.
319
    $use_tz = ($this->hasGranularity('timezone') || !$other->hasGranularity('timezone')) ? $this_tz : $other_tz;
320

    
321
    $this2 = clone $this;
322
    $this2->setTimezone($use_tz);
323
    $other->setTimezone($use_tz);
324
    $val = $this2->toArray(TRUE);
325
    $otherval = $other->toArray();
326
    foreach (self::$allgranularity as $g) {
327
      if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
328
        // The other class has a property we don't; steal it.
329
        $this2->addGranularity($g);
330
        $val[$g] = $otherval[$g];
331
      }
332
    }
333
    $other->setTimezone($other_tz);
334

    
335
    $this2->setDate($val['year'], $val['month'], $val['day']);
336
    $this2->setTime($val['hour'], $val['minute'], $val['second']);
337
    return $this2;
338
  }
339

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

    
362
    if (!$this->hasTime() || !$this->hasGranularity('timezone') || $force) {
363
      // This has no time or timezone granularity, so timezone doesn't mean
364
      // much. We set the timezone using the method, which will change the
365
      // day/hour, but then we switch back.
366
      $arr = $this->toArray(TRUE);
367
      parent::setTimezone($tz);
368
      $this->setDate($arr['year'], $arr['month'], $arr['day']);
369
      $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
370
      $this->addGranularity('timezone');
371
      return;
372
    }
373
    return parent::setTimezone($tz);
374
  }
375

    
376
  /**
377
   * Returns date formatted according to given format.
378
   *
379
   * Overrides base format function, formats this date according to its
380
   * available granularity, unless $force'ed not to limit to granularity.
381
   *
382
   * @todo Add translation into this so translated names will be provided.
383
   *
384
   * @param string $format
385
   *   A date format string.
386
   * @param bool $force
387
   *   Whether or not to limit the granularity. Defaults to FALSE.
388
   *
389
   * @return string|bool
390
   *   Returns the formatted date string on success or FALSE on failure.
391
   */
392
  public function format($format, $force = FALSE) {
393
    return parent::format($force ? $format : date_limit_format($format, $this->granularity));
394
  }
395

    
396
  /**
397
   * Adds a granularity entry to the array.
398
   *
399
   * @param string $g
400
   *   A single date part.
401
   */
402
  public function addGranularity($g) {
403
    $this->granularity[] = $g;
404
    $this->granularity = array_unique($this->granularity);
405
  }
406

    
407
  /**
408
   * Removes a granularity entry from the array.
409
   *
410
   * @param string $g
411
   *   A single date part.
412
   */
413
  public function removeGranularity($g) {
414
    if (($key = array_search($g, $this->granularity)) !== FALSE) {
415
      unset($this->granularity[$key]);
416
    }
417
  }
418

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

    
455
  /**
456
   * Determines if a a date is valid for a given granularity.
457
   *
458
   * @param array|null $granularity
459
   *   An array of date parts. Defaults to NULL.
460
   * @param bool $flexible
461
   *   TRUE if the granuliarty is flexible, FALSE otherwise. Defaults to FALSE.
462
   *
463
   * @return bool
464
   *   Whether a date is valid for a given granularity.
465
   */
466
  public function validGranularity($granularity = NULL, $flexible = FALSE) {
467
    $true = $this->hasGranularity() && (!$granularity || $flexible || $this->hasGranularity($granularity));
468
    if (!$true && $granularity) {
469
      $allowed_values = array(
470
        'second',
471
        'minute',
472
        'hour',
473
        'day',
474
        'month',
475
        'year',
476
      );
477
      foreach ((array) $granularity as $part) {
478
        if (!$this->hasGranularity($part) && in_array($part, $allowed_values)) {
479
          switch ($part) {
480
            case 'second':
481
              $this->errors[$part] = t('The second is missing.');
482
              break;
483

    
484
            case 'minute':
485
              $this->errors[$part] = t('The minute is missing.');
486
              break;
487

    
488
            case 'hour':
489
              $this->errors[$part] = t('The hour is missing.');
490
              break;
491

    
492
            case 'day':
493
              $this->errors[$part] = t('The day is missing.');
494
              break;
495

    
496
            case 'month':
497
              $this->errors[$part] = t('The month is missing.');
498
              break;
499

    
500
            case 'year':
501
              $this->errors[$part] = t('The year is missing.');
502
              break;
503
          }
504
        }
505
      }
506
    }
507
    return $true;
508
  }
509

    
510
  /**
511
   * Returns whether this object has time set.
512
   *
513
   * Used primarily for timezone conversion and formatting.
514
   *
515
   * @return bool
516
   *   TRUE if the date contains time parts, FALSE otherwise.
517
   */
518
  public function hasTime() {
519
    return $this->hasGranularity('hour');
520
  }
521

    
522
  /**
523
   * Returns whether the input values included a year.
524
   *
525
   * Useful to use pseudo date objects when we only are interested in the time.
526
   *
527
   * @todo $this->completeDate does not actually exist?
528
   */
529
  public function completeDate() {
530
    return $this->completeDate;
531
  }
532

    
533
  /**
534
   * Removes unwanted date parts from a date.
535
   *
536
   * In common usage we should not unset timezone through this.
537
   *
538
   * @param array $granularity
539
   *   An array of date parts.
540
   */
541
  public function limitGranularity(array $granularity) {
542
    foreach ($this->granularity as $key => $val) {
543
      if ($val != 'timezone' && !in_array($val, $granularity)) {
544
        unset($this->granularity[$key]);
545
      }
546
    }
547
  }
548

    
549
  /**
550
   * Determines the granularity of a date based on the constructor's arguments.
551
   *
552
   * @param string $time
553
   *   A date string.
554
   * @param bool $tz
555
   *   TRUE if the date has a timezone, FALSE otherwise.
556
   */
557
  protected function setGranularityFromTime($time, $tz) {
558
    $this->granularity = array();
559
    $temp = date_parse($time);
560
    // Special case for 'now'.
561
    if ($time == 'now') {
562
      $this->granularity = array(
563
        'year',
564
        'month',
565
        'day',
566
        'hour',
567
        'minute',
568
        'second',
569
      );
570
    }
571
    else {
572
      // This PHP date_parse() method currently doesn't have resolution down to
573
      // seconds, so if there is some time, all will be set.
574
      foreach (self::$allgranularity as $g) {
575
        if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'timezone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
576
          $this->granularity[] = $g;
577
        }
578
      }
579
    }
580
    if ($tz) {
581
      $this->addGranularity('timezone');
582
    }
583
  }
584

    
585
  /**
586
   * Converts a date string into a date object.
587
   *
588
   * @param string $date
589
   *   The date string to parse.
590
   * @param object $tz
591
   *   A timezone object.
592
   * @param string $format
593
   *   The date format string.
594
   *
595
   * @return object
596
   *   Returns the date object.
597
   */
598
  protected function parse($date, $tz, $format) {
599
    $array = date_format_patterns();
600
    foreach ($array as $key => $value) {
601
      // The letter with no preceding '\'.
602
      $patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";
603
      // A single character.
604
      $repl1[] = '${1}(.)';
605
      // The.
606
      $repl2[] = '${1}(' . $value . ')';
607
    }
608
    $patterns[] = "`\\\\\\\\([" . implode(array_keys($array)) . "])`";
609
    $repl1[] = '${1}';
610
    $repl2[] = '${1}';
611

    
612
    $format_regexp = preg_quote($format);
613

    
614
    // Extract letters.
615
    $regex1 = preg_replace($patterns, $repl1, $format_regexp, 1);
616
    $regex1 = str_replace('A', '(.)', $regex1);
617
    $regex1 = str_replace('a', '(.)', $regex1);
618
    preg_match('`^' . $regex1 . '$`', stripslashes($format), $letters);
619
    array_shift($letters);
620

    
621
    $ampm_upper = date_ampm_options(FALSE, TRUE);
622
    $ampm_lower = date_ampm_options(FALSE, FALSE);
623
    $regex2 = strtr(
624
      preg_replace($patterns, $repl2, $format_regexp, 1),
625
      array(
626
        'A' => '(' . preg_quote($ampm_upper['am'], '`') . '|' . preg_quote($ampm_upper['pm'], '`') . ')',
627
        'a' => '(' . preg_quote($ampm_lower['am'], '`') . '|' . preg_quote($ampm_lower['pm'], '`') . ')',
628
      )
629
    );
630
    preg_match('`^' . $regex2 . '$`u', $date, $values);
631
    array_shift($values);
632
    // If we did not find all the values for the patterns in the format, abort.
633
    if (count($letters) != count($values)) {
634
      $this->errors['invalid'] = t('The value @date does not match the expected format.', array('@date' => $date));
635
      return FALSE;
636
    }
637
    $this->granularity = array();
638
    $final_date = array(
639
      'hour' => 0,
640
      'minute' => 0,
641
      'second' => 0,
642
      'month' => 1,
643
      'day' => 1,
644
      'year' => 0,
645
    );
646
    foreach ($letters as $i => $letter) {
647
      $value = $values[$i];
648
      switch ($letter) {
649
        case 'd':
650
        case 'j':
651
          $final_date['day'] = intval($value);
652
          $this->addGranularity('day');
653
          break;
654

    
655
        case 'n':
656
        case 'm':
657
          $final_date['month'] = intval($value);
658
          $this->addGranularity('month');
659
          break;
660

    
661
        case 'F':
662
          $array_month_long = array_flip(date_month_names());
663
          $final_date['month'] = array_key_exists($value, $array_month_long) ? $array_month_long[$value] : -1;
664
          $this->addGranularity('month');
665
          break;
666

    
667
        case 'M':
668
          $array_month = array_flip(date_month_names_abbr());
669
          $final_date['month'] = array_key_exists($value, $array_month) ? $array_month[$value] : -1;
670
          $this->addGranularity('month');
671
          break;
672

    
673
        case 'Y':
674
          $final_date['year'] = $value;
675
          $this->addGranularity('year');
676
          if (strlen($value) < 4) {
677
            $this->errors['year'] = t('The year is invalid. Please check that entry includes four digits.');
678
          }
679
          break;
680

    
681
        case 'y':
682
          $year = $value;
683
          // If no century, we add the current one ("06" => "2006").
684
          $final_date['year'] = str_pad($year, 4, substr(date("Y"), 0, 2), STR_PAD_LEFT);
685
          $this->addGranularity('year');
686
          break;
687

    
688
        case 'a':
689
          $ampm = ($value == $ampm_lower['am'] ? 'am' : 'pm');
690
          break;
691

    
692
        case 'A':
693
          $ampm = ($value == $ampm_upper['am'] ? 'am' : 'pm');
694
          break;
695

    
696
        case 'g':
697
        case 'h':
698
        case 'G':
699
        case 'H':
700
          $final_date['hour'] = intval($value);
701
          $this->addGranularity('hour');
702
          break;
703

    
704
        case 'i':
705
          $final_date['minute'] = intval($value);
706
          $this->addGranularity('minute');
707
          break;
708

    
709
        case 's':
710
          $final_date['second'] = intval($value);
711
          $this->addGranularity('second');
712
          break;
713

    
714
        case 'U':
715
          parent::__construct($value, $tz ? $tz : new DateTimeZone("UTC"));
716
          $this->addGranularity('year');
717
          $this->addGranularity('month');
718
          $this->addGranularity('day');
719
          $this->addGranularity('hour');
720
          $this->addGranularity('minute');
721
          $this->addGranularity('second');
722
          return $this;
723

    
724
      }
725
    }
726

    
727
    if (isset($ampm)) {
728
      if ($ampm == 'pm' && $final_date['hour'] < 12) {
729
        $final_date['hour'] += 12;
730
      }
731
      elseif ($ampm == 'am' && $final_date['hour'] == 12) {
732
        $final_date['hour'] -= 12;
733
      }
734
    }
735

    
736
    // Blank becomes current time, given TZ.
737
    parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
738
    if ($tz) {
739
      $this->addGranularity('timezone');
740
    }
741

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

    
746
    $this->errors += $this->arrayErrors($final_date);
747
    $granularity = drupal_map_assoc($this->granularity);
748

    
749
    // If the input value is '0000-00-00', PHP's date class will later
750
    // incorrectly convert it to something like '-0001-11-30' if we do setDate()
751
    // here. If we don't do setDate() here, it will default to the current date
752
    // and we will lose any way to tell that there was no date in the orignal
753
    // input values. So set a flag we can use later to tell that this date
754
    // object was created using only time values, and that the date values are
755
    // artifical.
756
    if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) {
757
      $this->timeOnly = TRUE;
758
    }
759
    elseif (empty($this->errors)) {
760
      // setDate() expects a valid year, month, and day.
761
      // Set some defaults for dates that don't use this to
762
      // keep PHP from interpreting it as the last day of
763
      // the previous month or last month of the previous year.
764
      if (empty($granularity['month'])) {
765
        $final_date['month'] = 1;
766
      }
767
      if (empty($granularity['day'])) {
768
        $final_date['day'] = 1;
769
      }
770
      $this->setDate($final_date['year'], $final_date['month'], $final_date['day']);
771
    }
772

    
773
    if (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) {
774
      $this->dateOnly = TRUE;
775
    }
776
    elseif (empty($this->errors)) {
777
      $this->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
778
    }
779
    return $this;
780
  }
781

    
782
  /**
783
   * Returns all standard date parts in an array.
784
   *
785
   * Will return '' for parts in which it lacks granularity.
786
   *
787
   * @param bool $force
788
   *   Whether or not to limit the granularity. Defaults to FALSE.
789
   *
790
   * @return array
791
   *   An array of formatted date part values, keyed by date parts.
792
   */
793
  public function toArray($force = FALSE) {
794
    return array(
795
      'year' => $this->format('Y', $force),
796
      'month' => $this->format('n', $force),
797
      'day' => $this->format('j', $force),
798
      'hour' => intval($this->format('H', $force)),
799
      'minute' => intval($this->format('i', $force)),
800
      'second' => intval($this->format('s', $force)),
801
      'timezone' => $this->format('e', $force),
802
    );
803
  }
804

    
805
  /**
806
   * Creates an ISO date from an array of values.
807
   *
808
   * @param array $arr
809
   *   An array of date values keyed by date part.
810
   * @param bool $full
811
   *   (optional) Whether to force a full date by filling in missing values.
812
   *   Defaults to FALSE.
813
   */
814
  public function toISO(array $arr, $full = FALSE) {
815
    // Add empty values to avoid errors. The empty values must create a valid
816
    // date or we will get date slippage, i.e. a value of 2011-00-00 will get
817
    // interpreted as November of 2010 by PHP.
818
    if ($full) {
819
      $arr += array(
820
        'year' => 0,
821
        'month' => 1,
822
        'day' => 1,
823
        'hour' => 0,
824
        'minute' => 0,
825
        'second' => 0,
826
      );
827
    }
828
    else {
829
      $arr += array(
830
        'year' => '',
831
        'month' => '',
832
        'day' => '',
833
        'hour' => '',
834
        'minute' => '',
835
        'second' => '',
836
      );
837
    }
838
    $datetime = '';
839
    if ($arr['year'] !== '') {
840
      $datetime = date_pad(intval($arr['year']), 4);
841
      if ($full || $arr['month'] !== '') {
842
        $datetime .= '-' . date_pad(intval($arr['month']));
843
        if ($full || $arr['day'] !== '') {
844
          $datetime .= '-' . date_pad(intval($arr['day']));
845
        }
846
      }
847
    }
848
    if ($arr['hour'] !== '') {
849
      $datetime .= $datetime ? 'T' : '';
850
      $datetime .= date_pad(intval($arr['hour']));
851
      if ($full || $arr['minute'] !== '') {
852
        $datetime .= ':' . date_pad(intval($arr['minute']));
853
        if ($full || $arr['second'] !== '') {
854
          $datetime .= ':' . date_pad(intval($arr['second']));
855
        }
856
      }
857
    }
858
    return $datetime;
859
  }
860

    
861
  /**
862
   * Forces an incomplete date to be valid.
863
   *
864
   * E.g., add a valid year, month, and day if only the time has been defined.
865
   *
866
   * @param array|string $date
867
   *   An array of date parts or a datetime string with values to be massaged
868
   *   into a valid date object.
869
   * @param string $format
870
   *   (optional) The format of the date. Defaults to NULL.
871
   * @param string $default
872
   *   (optional) If the fallback should use the first value of the date part,
873
   *   or the current value of the date part. Defaults to 'first'.
874
   */
875
  public function setFuzzyDate($date, $format = NULL, $default = 'first') {
876
    $timezone = $this->getTimeZone() ? $this->getTimeZone()->getName() : NULL;
877
    $comp = new DateObject($date, $timezone, $format);
878
    $arr = $comp->toArray(TRUE);
879
    foreach ($arr as $key => $value) {
880
      // Set to intval here and then test that it is still an integer.
881
      // Needed because sometimes valid integers come through as strings.
882
      $arr[$key] = $this->forceValid($key, intval($value), $default, $arr['month'], $arr['year']);
883
    }
884
    $this->setDate($arr['year'], $arr['month'], $arr['day']);
885
    $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
886
  }
887

    
888
  /**
889
   * Converts a date part into something that will produce a valid date.
890
   *
891
   * @param string $part
892
   *   The date part.
893
   * @param int $value
894
   *   The date value for this part.
895
   * @param string $default
896
   *   (optional) If the fallback should use the first value of the date part,
897
   *   or the current value of the date part. Defaults to 'first'.
898
   * @param int $month
899
   *   (optional) Used when the date part is less than 'month' to specify the
900
   *   date. Defaults to NULL.
901
   * @param int $year
902
   *   (optional) Used when the date part is less than 'year' to specify the
903
   *   date. Defaults to NULL.
904
   *
905
   * @return int
906
   *   A valid date value.
907
   */
908
  protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
909
    $now = date_now();
910
    switch ($part) {
911
      case 'year':
912
        $fallback = $now->format('Y');
913
        return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
914

    
915
      case 'month':
916
        $fallback = $default == 'first' ? 1 : $now->format('n');
917
        return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
918

    
919
      case 'day':
920
        $fallback = $default == 'first' ? 1 : $now->format('j');
921
        $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
922
        return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
923

    
924
      case 'hour':
925
        $fallback = $default == 'first' ? 0 : $now->format('G');
926
        return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
927

    
928
      case 'minute':
929
        $fallback = $default == 'first' ? 0 : $now->format('i');
930
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
931

    
932
      case 'second':
933
        $fallback = $default == 'first' ? 0 : $now->format('s');
934
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
935
    }
936
  }
937

    
938
  /**
939
   * Finds possible errors in an array of date part values.
940
   *
941
   * The forceValid() function will change an invalid value to a valid one, so
942
   * we just need to see if the value got altered.
943
   *
944
   * @param array $arr
945
   *   An array of date values, keyed by date part.
946
   *
947
   * @return array
948
   *   An array of error messages, keyed by date part.
949
   */
950
  public function arrayErrors(array $arr) {
951
    $errors = array();
952
    $now = date_now();
953
    $default_month = !empty($arr['month']) ? $arr['month'] : $now->format('n');
954
    $default_year = !empty($arr['year']) ? $arr['year'] : $now->format('Y');
955

    
956
    $this->granularity = array();
957
    foreach ($arr as $part => $value) {
958
      // Explicitly set the granularity to the values in the input array.
959
      if (is_numeric($value)) {
960
        $this->addGranularity($part);
961
      }
962
      // Avoid false errors when a numeric value is input as a string by casting
963
      // as an integer.
964
      $value = intval($value);
965
      if (!empty($value) && $this->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
966
        // Use a switch/case to make translation easier by providing a different
967
        // message for each part.
968
        switch ($part) {
969
          case 'year':
970
            $errors['year'] = t('The year is invalid.');
971
            break;
972

    
973
          case 'month':
974
            $errors['month'] = t('The month is invalid.');
975
            break;
976

    
977
          case 'day':
978
            $errors['day'] = t('The day is invalid.');
979
            break;
980

    
981
          case 'hour':
982
            $errors['hour'] = t('The hour is invalid.');
983
            break;
984

    
985
          case 'minute':
986
            $errors['minute'] = t('The minute is invalid.');
987
            break;
988

    
989
          case 'second':
990
            $errors['second'] = t('The second is invalid.');
991
            break;
992
        }
993
      }
994
    }
995
    if ($this->hasTime()) {
996
      $this->addGranularity('timezone');
997
    }
998
    return $errors;
999
  }
1000

    
1001
  /**
1002
   * Computes difference between two days using a given measure.
1003
   *
1004
   * @param object $date2_in
1005
   *   The stop date.
1006
   * @param string $measure
1007
   *   (optional) A granularity date part. Defaults to 'seconds'.
1008
   * @param bool $absolute
1009
   *   (optional) Indicate whether the absolute value of the difference should
1010
   *   be returned or if the sign should be retained. Defaults to TRUE.
1011
   */
1012
  public function difference($date2_in, $measure = 'seconds', $absolute = TRUE) {
1013
    // Create cloned objects or original dates will be impacted by the
1014
    // date_modify() operations done in this code.
1015
    $date1 = clone $this;
1016
    $date2 = clone $date2_in;
1017
    if (is_object($date1) && is_object($date2)) {
1018
      $diff = date_format($date2, 'U') - date_format($date1, 'U');
1019
      if ($diff == 0) {
1020
        return 0;
1021
      }
1022
      elseif ($diff < 0 && $absolute) {
1023
        // Make sure $date1 is the smaller date.
1024
        $temp = $date2;
1025
        $date2 = $date1;
1026
        $date1 = $temp;
1027
        $diff = date_format($date2, 'U') - date_format($date1, 'U');
1028
      }
1029
      $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
1030
      switch ($measure) {
1031
        // The easy cases first.
1032
        case 'seconds':
1033
          return $diff;
1034

    
1035
        case 'minutes':
1036
          return $diff / 60;
1037

    
1038
        case 'hours':
1039
          return $diff / 3600;
1040

    
1041
        case 'years':
1042
          return $year_diff;
1043

    
1044
        case 'months':
1045
          $format = 'n';
1046
          $item1 = date_format($date1, $format);
1047
          $item2 = date_format($date2, $format);
1048
          if ($year_diff == 0) {
1049
            return intval($item2 - $item1);
1050
          }
1051
          elseif ($year_diff < 0) {
1052
            $item_diff = 0 - $item1;
1053
            $item_diff -= intval((abs($year_diff) - 1) * 12);
1054
            return $item_diff - (12 - $item2);
1055
          }
1056
          else {
1057
            $item_diff = 12 - $item1;
1058
            $item_diff += intval(($year_diff - 1) * 12);
1059
            return $item_diff + $item2;
1060
          }
1061
          break;
1062

    
1063
        case 'days':
1064
          $format = 'z';
1065
          $item1 = date_format($date1, $format);
1066
          $item2 = date_format($date2, $format);
1067
          if ($year_diff == 0) {
1068
            return intval($item2 - $item1);
1069
          }
1070
          elseif ($year_diff < 0) {
1071
            $item_diff = 0 - $item1;
1072
            for ($i = 1; $i < abs($year_diff); $i++) {
1073
              date_modify($date1, '-1 year');
1074
              $item_diff -= date_days_in_year($date1);
1075
            }
1076
            return $item_diff - (date_days_in_year($date2) - $item2);
1077
          }
1078
          else {
1079
            $item_diff = date_days_in_year($date1) - $item1;
1080
            for ($i = 1; $i < $year_diff; $i++) {
1081
              date_modify($date1, '+1 year');
1082
              $item_diff += date_days_in_year($date1);
1083
            }
1084
            return $item_diff + $item2;
1085
          }
1086
          break;
1087

    
1088
        case 'weeks':
1089
          $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
1090
          $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
1091

    
1092
          $sign = ($year_diff < 0) ? -1 : 1;
1093

    
1094
          for ($i = 1; $i <= abs($year_diff); $i++) {
1095
            date_modify($date1, (($sign > 0) ? '+' : '-') . '1 year');
1096
            $week_diff += (date_iso_weeks_in_year($date1) * $sign);
1097
          }
1098
          return $week_diff;
1099
      }
1100
    }
1101
    return NULL;
1102
  }
1103

    
1104
}
1105

    
1106
/**
1107
 * Determines if the date element needs to be processed.
1108
 *
1109
 * Helper function to see if date element has been hidden by FAPI to see if it
1110
 * needs to be processed or just pass the value through. This is needed since
1111
 * normal date processing explands the date element into parts and then
1112
 * reconstructs it, which is not needed or desirable if the field is hidden.
1113
 *
1114
 * @param array $element
1115
 *   The date element to check.
1116
 *
1117
 * @return bool
1118
 *   TRUE if the element is effectively hidden, FALSE otherwise.
1119
 */
1120
function date_hidden_element(array $element) {
1121
  // @todo What else needs to be tested to see if dates are hidden or disabled?
1122
  if ((isset($element['#access']) && empty($element['#access']))
1123
    || !empty($element['#programmed'])
1124
    || in_array($element['#type'], array('hidden', 'value'))) {
1125
    return TRUE;
1126
  }
1127
  return FALSE;
1128
}
1129

    
1130
/**
1131
 * Helper function for getting the format string for a date type.
1132
 *
1133
 * @param string $type
1134
 *   A date type format name.
1135
 *
1136
 * @return string
1137
 *   A date type format, like 'Y-m-d H:i:s'.
1138
 */
1139
function date_type_format($type) {
1140
  switch ($type) {
1141
    case DATE_ISO:
1142
      return DATE_FORMAT_ISO;
1143

    
1144
    case DATE_UNIX:
1145
      return DATE_FORMAT_UNIX;
1146

    
1147
    case DATE_DATETIME:
1148
      return DATE_FORMAT_DATETIME;
1149

    
1150
    case DATE_ICAL:
1151
      return DATE_FORMAT_ICAL;
1152
  }
1153
}
1154

    
1155
/**
1156
 * Constructs an untranslated array of month names.
1157
 *
1158
 * Needed for CSS, translation functions, strtotime(), and other places
1159
 * that use the English versions of these words.
1160
 *
1161
 * @return array
1162
 *   An array of month names.
1163
 */
1164
function date_month_names_untranslated() {
1165
  static $month_names;
1166
  if (empty($month_names)) {
1167
    $month_names = array(
1168
      1 => 'January',
1169
      2 => 'February',
1170
      3 => 'March',
1171
      4 => 'April',
1172
      5 => 'May',
1173
      6 => 'June',
1174
      7 => 'July',
1175
      8 => 'August',
1176
      9 => 'September',
1177
      10 => 'October',
1178
      11 => 'November',
1179
      12 => 'December',
1180
    );
1181
  }
1182
  return $month_names;
1183
}
1184

    
1185
/**
1186
 * Returns a translated array of month names.
1187
 *
1188
 * @param bool $required
1189
 *   (optional) If FALSE, the returned array will include a blank value.
1190
 *   Defaults to FALSE.
1191
 *
1192
 * @return array
1193
 *   An array of month names.
1194
 */
1195
function date_month_names($required = FALSE) {
1196
  $month_names = array();
1197
  foreach (date_month_names_untranslated() as $key => $month) {
1198
    $month_names[$key] = t($month, array(), array('context' => 'Long month name'));
1199
  }
1200
  $none = array('' => '');
1201
  return !$required ? $none + $month_names : $month_names;
1202
}
1203

    
1204
/**
1205
 * Constructs a translated array of month name abbreviations.
1206
 *
1207
 * @param bool $required
1208
 *   (optional) If FALSE, the returned array will include a blank value.
1209
 *   Defaults to FALSE.
1210
 * @param int $length
1211
 *   (optional) The length of the abbreviation. Defaults to 3.
1212
 *
1213
 * @return array
1214
 *   An array of month abbreviations.
1215
 */
1216
function date_month_names_abbr($required = FALSE, $length = 3) {
1217
  $month_names = array();
1218
  foreach (date_month_names_untranslated() as $key => $month) {
1219
    if ($length == 3) {
1220
      $month_names[$key] = t(substr($month, 0, $length), array());
1221
    }
1222
    else {
1223
      $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr'));
1224
    }
1225
  }
1226
  $none = array('' => '');
1227
  return !$required ? $none + $month_names : $month_names;
1228
}
1229

    
1230
/**
1231
 * Constructs an untranslated array of week days.
1232
 *
1233
 * Needed for CSS, translation functions, strtotime(), and other places
1234
 * that use the English versions of these words.
1235
 *
1236
 * @param bool $refresh
1237
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1238
 *
1239
 * @return array
1240
 *   An array of week day names
1241
 */
1242
function date_week_days_untranslated($refresh = TRUE) {
1243
  static $weekdays;
1244
  if ($refresh || empty($weekdays)) {
1245
    $weekdays = array(
1246
      'Sunday',
1247
      'Monday',
1248
      'Tuesday',
1249
      'Wednesday',
1250
      'Thursday',
1251
      'Friday',
1252
      'Saturday',
1253
    );
1254
  }
1255
  return $weekdays;
1256
}
1257

    
1258
/**
1259
 * Returns a translated array of week names.
1260
 *
1261
 * @param bool $required
1262
 *   (optional) If FALSE, the returned array will include a blank value.
1263
 *   Defaults to FALSE.
1264
 *
1265
 * @return array
1266
 *   An array of week day names
1267
 */
1268
function date_week_days($required = FALSE, $refresh = TRUE) {
1269
  $weekdays = array();
1270
  foreach (date_week_days_untranslated() as $key => $day) {
1271
    $weekdays[$key] = t($day, array(), array('context' => ''));
1272
  }
1273
  $none = array('' => '');
1274
  return !$required ? $none + $weekdays : $weekdays;
1275
}
1276

    
1277
/**
1278
 * Constructs a translated array of week day abbreviations.
1279
 *
1280
 * @param bool $required
1281
 *   (optional) If FALSE, the returned array will include a blank value.
1282
 *   Defaults to FALSE.
1283
 * @param bool $refresh
1284
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1285
 * @param int $length
1286
 *   (optional) The length of the abbreviation. Defaults to 3.
1287
 *
1288
 * @return array
1289
 *   An array of week day abbreviations
1290
 */
1291
function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
1292
  $weekdays = array();
1293
  switch ($length) {
1294
    case 1:
1295
      $context = 'day_abbr1';
1296
      break;
1297

    
1298
    case 2:
1299
      $context = 'day_abbr2';
1300
      break;
1301

    
1302
    default:
1303
      $context = '';
1304
  }
1305
  foreach (date_week_days_untranslated() as $key => $day) {
1306
    $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context));
1307
  }
1308
  $none = array('' => '');
1309
  return !$required ? $none + $weekdays : $weekdays;
1310
}
1311

    
1312
/**
1313
 * Reorders weekdays to match the first day of the week.
1314
 *
1315
 * @param array $weekdays
1316
 *   An array of weekdays.
1317
 *
1318
 * @return array
1319
 *   An array of weekdays reordered to match the first day of the week.
1320
 */
1321
function date_week_days_ordered(array $weekdays) {
1322
  $first_day = variable_get('date_first_day', 0);
1323
  if ($first_day > 0) {
1324
    for ($i = 1; $i <= $first_day; $i++) {
1325
      $last = array_shift($weekdays);
1326
      array_push($weekdays, $last);
1327
    }
1328
  }
1329
  return $weekdays;
1330
}
1331

    
1332
/**
1333
 * Constructs an array of years.
1334
 *
1335
 * @param int $start
1336
 *   The start year in the array.
1337
 * @param int $end
1338
 *   The end year in the array.
1339
 * @param bool $required
1340
 *   (optional) If FALSE, the returned array will include a blank value.
1341
 *   Defaults to FALSE.
1342
 *
1343
 * @return array
1344
 *   An array of years in the selected range.
1345
 */
1346
function date_years($start = 0, $end = 0, $required = FALSE) {
1347
  // Ensure $min and $max are valid values.
1348
  if (empty($start)) {
1349
    $start = intval(date('Y', REQUEST_TIME) - 3);
1350
  }
1351
  if (empty($end)) {
1352
    $end = intval(date('Y', REQUEST_TIME) + 3);
1353
  }
1354
  $none = array(0 => '');
1355
  return !$required ? $none + drupal_map_assoc(range($start, $end)) : drupal_map_assoc(range($start, $end));
1356
}
1357

    
1358
/**
1359
 * Constructs an array of days in a month.
1360
 *
1361
 * @param bool $required
1362
 *   (optional) If FALSE, the returned array will include a blank value.
1363
 *   Defaults to FALSE.
1364
 * @param int $month
1365
 *   (optional) The month in which to find the number of days.
1366
 * @param int $year
1367
 *   (optional) The year in which to find the number of days.
1368
 *
1369
 * @return array
1370
 *   An array of days for the selected month.
1371
 */
1372
function date_days($required = FALSE, $month = NULL, $year = NULL) {
1373
  // If we have a month and year, find the right last day of the month.
1374
  if (!empty($month) && !empty($year)) {
1375
    $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC');
1376
    $max = $date->format('t');
1377
  }
1378
  // If there is no month and year given, default to 31.
1379
  if (empty($max)) {
1380
    $max = 31;
1381
  }
1382
  $none = array(0 => '');
1383
  return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
1384
}
1385

    
1386
/**
1387
 * Constructs an array of hours.
1388
 *
1389
 * @param string $format
1390
 *   A date format string.
1391
 * @param bool $required
1392
 *   (optional) If FALSE, the returned array will include a blank value.
1393
 *   Defaults to FALSE.
1394
 *
1395
 * @return array
1396
 *   An array of hours in the selected format.
1397
 */
1398
function date_hours($format = 'H', $required = FALSE) {
1399
  $hours = array();
1400
  if ($format == 'h' || $format == 'g') {
1401
    $min = 1;
1402
    $max = 12;
1403
  }
1404
  else {
1405
    $min = 0;
1406
    $max = 23;
1407
  }
1408
  for ($i = $min; $i <= $max; $i++) {
1409
    $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
1410
  }
1411
  $none = array('' => '');
1412
  return !$required ? $none + $hours : $hours;
1413
}
1414

    
1415
/**
1416
 * Constructs an array of minutes.
1417
 *
1418
 * @param string $format
1419
 *   A date format string.
1420
 * @param bool $required
1421
 *   (optional) If FALSE, the returned array will include a blank value.
1422
 *   Defaults to FALSE.
1423
 *
1424
 * @return array
1425
 *   An array of minutes in the selected format.
1426
 */
1427
function date_minutes($format = 'i', $required = FALSE, $increment = 1) {
1428
  $minutes = array();
1429
  // Ensure $increment has a value so we don't loop endlessly.
1430
  if (empty($increment)) {
1431
    $increment = 1;
1432
  }
1433
  for ($i = 0; $i < 60; $i += $increment) {
1434
    $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i;
1435
  }
1436
  $none = array('' => '');
1437
  return !$required ? $none + $minutes : $minutes;
1438
}
1439

    
1440
/**
1441
 * Constructs an array of seconds.
1442
 *
1443
 * @param string $format
1444
 *   A date format string.
1445
 * @param bool $required
1446
 *   (optional) If FALSE, the returned array will include a blank value.
1447
 *   Defaults to FALSE.
1448
 *
1449
 * @return array
1450
 *   An array of seconds in the selected format.
1451
 */
1452
function date_seconds($format = 's', $required = FALSE, $increment = 1) {
1453
  $seconds = array();
1454
  // Ensure $increment has a value so we don't loop endlessly.
1455
  if (empty($increment)) {
1456
    $increment = 1;
1457
  }
1458
  for ($i = 0; $i < 60; $i += $increment) {
1459
    $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
1460
  }
1461
  $none = array('' => '');
1462
  return !$required ? $none + $seconds : $seconds;
1463
}
1464

    
1465
/**
1466
 * Constructs an array of AM and PM options.
1467
 *
1468
 * @param bool $required
1469
 *   (optional) If FALSE, the returned array will include a blank value.
1470
 *   Defaults to FALSE.
1471
 *
1472
 * @return array
1473
 *   An array of AM and PM options.
1474
 */
1475
function date_ampm($required = FALSE) {
1476
  $ampm = date_ampm_options(FALSE, FALSE);
1477

    
1478
  return !$required ? array('' => '') + $ampm : $ampm;
1479
}
1480

    
1481
/**
1482
 * Constructs an array of AM and PM options without empty option.
1483
 *
1484
 * @param bool $key_upper
1485
 *   If TRUE then the array key will be uppercase.
1486
 * @param bool $label_upper
1487
 *   If TRUE then the array value will be uppercase.
1488
 *
1489
 * @return array
1490
 *   An array of AM and PM options.
1491
 */
1492
function date_ampm_options($key_upper, $label_upper) {
1493
  $am = $key_upper ? 'AM' : 'am';
1494
  $pm = $key_upper ? 'PM' : 'pm';
1495

    
1496
  if ($label_upper) {
1497
    return array(
1498
      $am => t('AM', array(), array('context' => 'ampm')),
1499
      $pm => t('PM', array(), array('context' => 'ampm')),
1500
    );
1501
  }
1502
  else {
1503
    return array(
1504
      $am => t('am', array(), array('context' => 'ampm')),
1505
      $pm => t('pm', array(), array('context' => 'ampm')),
1506
    );
1507
  }
1508
}
1509

    
1510
/**
1511
 * Constructs an array of regex replacement strings for date format elements.
1512
 *
1513
 * @param bool $strict
1514
 *   Whether or not to force 2 digits for elements that sometimes allow either
1515
 *   1 or 2 digits.
1516
 *
1517
 * @return array
1518
 *   An array of date() format letters and their regex equivalents.
1519
 */
1520
function date_format_patterns($strict = FALSE) {
1521
  return array(
1522
    'd' => '\d{' . ($strict ? '2' : '1,2') . '}',
1523
    'm' => '\d{' . ($strict ? '2' : '1,2') . '}',
1524
    'h' => '\d{' . ($strict ? '2' : '1,2') . '}',
1525
    'H' => '\d{' . ($strict ? '2' : '1,2') . '}',
1526
    'i' => '\d{' . ($strict ? '2' : '1,2') . '}',
1527
    's' => '\d{' . ($strict ? '2' : '1,2') . '}',
1528
    'j' => '\d{1,2}',
1529
    'N' => '\d',
1530
    'S' => '\w{2}',
1531
    'w' => '\d',
1532
    'z' => '\d{1,3}',
1533
    'W' => '\d{1,2}',
1534
    'n' => '\d{1,2}',
1535
    't' => '\d{2}',
1536
    'L' => '\d',
1537
    'o' => '\d{4}',
1538
    'Y' => '-?\d{1,6}',
1539
    'y' => '\d{2}',
1540
    'B' => '\d{3}',
1541
    'g' => '\d{1,2}',
1542
    'G' => '\d{1,2}',
1543
    'e' => '\w*',
1544
    'I' => '\d',
1545
    'T' => '\w*',
1546
    'U' => '\d*',
1547
    'z' => '[+-]?\d*',
1548
    'O' => '[+-]?\d{4}',
1549
    // Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like
1550
    // German umlaut. Per http://drupal.org/node/1101284, we may need as little
1551
    // as 2 and as many as 5 characters in some languages.
1552
    'D' => '\S{2,5}',
1553
    'l' => '\S*',
1554
    'M' => '\S{2,5}',
1555
    'F' => '\S*',
1556
    'P' => '[+-]?\d{2}\:\d{2}',
1557
    'O' => '[+-]\d{4}',
1558
    'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})',
1559
    'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?',
1560
  );
1561
}
1562

    
1563
/**
1564
 * Constructs an array of granularity options and their labels.
1565
 *
1566
 * @return array
1567
 *   An array of translated date parts, keyed by their machine name.
1568
 */
1569
function date_granularity_names() {
1570
  return array(
1571
    'year' => t('Year', array(), array('context' => 'datetime')),
1572
    'month' => t('Month', array(), array('context' => 'datetime')),
1573
    'day' => t('Day', array(), array('context' => 'datetime')),
1574
    'hour' => t('Hour', array(), array('context' => 'datetime')),
1575
    'minute' => t('Minute', array(), array('context' => 'datetime')),
1576
    'second' => t('Second', array(), array('context' => 'datetime')),
1577
  );
1578
}
1579

    
1580
/**
1581
 * Sorts a granularity array.
1582
 *
1583
 * @param array $granularity
1584
 *   An array of date parts.
1585
 */
1586
function date_granularity_sorted(array $granularity) {
1587
  return array_intersect(array(
1588
    'year',
1589
    'month',
1590
    'day',
1591
    'hour',
1592
    'minute',
1593
    'second',
1594
  ), $granularity);
1595
}
1596

    
1597
/**
1598
 * Constructs an array of granularity based on a given precision.
1599
 *
1600
 * @param string $precision
1601
 *   A granularity item.
1602
 *
1603
 * @return array
1604
 *   A granularity array containing the given precision and all those above it.
1605
 *   For example, passing in 'month' will return array('year', 'month').
1606
 */
1607
function date_granularity_array_from_precision($precision) {
1608
  $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second');
1609
  switch ($precision) {
1610
    case 'year':
1611
      return array_slice($granularity_array, -6, 1);
1612

    
1613
    case 'month':
1614
      return array_slice($granularity_array, -6, 2);
1615

    
1616
    case 'day':
1617
      return array_slice($granularity_array, -6, 3);
1618

    
1619
    case 'hour':
1620
      return array_slice($granularity_array, -6, 4);
1621

    
1622
    case 'minute':
1623
      return array_slice($granularity_array, -6, 5);
1624

    
1625
    default:
1626
      return $granularity_array;
1627
  }
1628
}
1629

    
1630
/**
1631
 * Give a granularity array, return the highest precision.
1632
 *
1633
 * @param array $granularity_array
1634
 *   An array of date parts.
1635
 *
1636
 * @return string
1637
 *   The most precise element in a granularity array.
1638
 */
1639
function date_granularity_precision(array $granularity_array) {
1640
  $input = date_granularity_sorted($granularity_array);
1641
  return array_pop($input);
1642
}
1643

    
1644
/**
1645
 * Constructs a valid DATETIME format string for the granularity of an item.
1646
 *
1647
 * @todo This function is no longer used as of
1648
 * http://drupalcode.org/project/date.git/commit/07efbb5.
1649
 */
1650
function date_granularity_format($granularity) {
1651
  if (is_array($granularity)) {
1652
    $granularity = date_granularity_precision($granularity);
1653
  }
1654
  $format = 'Y-m-d H:i:s';
1655
  switch ($granularity) {
1656
    case 'year':
1657
      return substr($format, 0, 1);
1658

    
1659
    case 'month':
1660
      return substr($format, 0, 3);
1661

    
1662
    case 'day':
1663
      return substr($format, 0, 5);
1664

    
1665
    case 'hour';
1666
      return substr($format, 0, 7);
1667

    
1668
    case 'minute':
1669
      return substr($format, 0, 9);
1670

    
1671
    default:
1672
      return $format;
1673
  }
1674
}
1675

    
1676
/**
1677
 * Returns a translated array of timezone names.
1678
 *
1679
 * Cache the untranslated array, make the translated array a static variable.
1680
 *
1681
 * @param bool $required
1682
 *   (optional) If FALSE, the returned array will include a blank value.
1683
 *   Defaults to FALSE.
1684
 * @param bool $refresh
1685
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1686
 *
1687
 * @return array
1688
 *   An array of timezone names.
1689
 */
1690
function date_timezone_names($required = FALSE, $refresh = FALSE) {
1691
  static $zonenames;
1692
  if (empty($zonenames) || $refresh) {
1693
    $cached = cache_get('date_timezone_identifiers_list');
1694
    $zonenames = !empty($cached) ? $cached->data : array();
1695
    if ($refresh || empty($cached) || empty($zonenames)) {
1696
      $data = timezone_identifiers_list();
1697
      asort($data);
1698
      foreach ($data as $delta => $zone) {
1699
        // Because many timezones exist in PHP only for backward compatibility
1700
        // reasons and should not be used, the list is filtered by a regular
1701
        // expression.
1702
        if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
1703
          $zonenames[$zone] = $zone;
1704
        }
1705
      }
1706

    
1707
      if (!empty($zonenames)) {
1708
        cache_set('date_timezone_identifiers_list', $zonenames);
1709
      }
1710
    }
1711
    foreach ($zonenames as $zone) {
1712
      $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone)));
1713
    }
1714
  }
1715
  $none = array('' => '');
1716
  return !$required ? $none + $zonenames : $zonenames;
1717
}
1718

    
1719
/**
1720
 * Returns an array of system-allowed timezone abbreviations.
1721
 *
1722
 * Cache an array of just the abbreviation names because the whole
1723
 * timezone_abbreviations_list() is huge, so we don't want to retrieve it more
1724
 * than necessary.
1725
 *
1726
 * @param bool $refresh
1727
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1728
 *
1729
 * @return array
1730
 *   An array of allowed timezone abbreviations.
1731
 */
1732
function date_timezone_abbr($refresh = FALSE) {
1733
  $cached = cache_get('date_timezone_abbreviations');
1734
  $data = isset($cached->data) ? $cached->data : array();
1735
  if (empty($data) || $refresh) {
1736
    $data = array_keys(timezone_abbreviations_list());
1737
    cache_set('date_timezone_abbreviations', $data);
1738
  }
1739
  return $data;
1740
}
1741

    
1742
/**
1743
 * Formats a date, using a date type or a custom date format string.
1744
 *
1745
 * Reworked from Drupal's format_date function to handle pre-1970 and
1746
 * post-2038 dates and accept a date object instead of a timestamp as input.
1747
 * Translates formatted date results, unlike PHP function date_format().
1748
 * Should only be used for display, not input, because it can't be parsed.
1749
 *
1750
 * @param object $date
1751
 *   A date object.
1752
 * @param string $type
1753
 *   (optional) The date format to use. Can be 'small', 'medium' or 'large' for
1754
 *   the preconfigured date formats. If 'custom' is specified, then $format is
1755
 *   required as well. Defaults to 'medium'.
1756
 * @param string $format
1757
 *   (optional) A PHP date format string as required by date(). A backslash
1758
 *   should be used before a character to avoid interpreting the character as
1759
 *   part of a date format. Defaults to an empty string.
1760
 * @param string $langcode
1761
 *   (optional) Language code to translate to. Defaults to NULL.
1762
 *
1763
 * @return string
1764
 *   A translated date string in the requested format.
1765
 *
1766
 * @see format_date()
1767
 */
1768
function date_format_date($date, $type = 'medium', $format = '', $langcode = NULL) {
1769
  if (empty($date)) {
1770
    return '';
1771
  }
1772
  if ($type != 'custom') {
1773
    $format = variable_get('date_format_' . $type);
1774
  }
1775
  if ($type != 'custom' && empty($format)) {
1776
    $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
1777
  }
1778
  $format = date_limit_format($format, $date->granularity);
1779
  $max = strlen($format);
1780
  $datestring = '';
1781
  for ($i = 0; $i < $max; $i++) {
1782
    $c = $format[$i];
1783
    switch ($c) {
1784
      case 'l':
1785
        $datestring .= t($date->format('l'), array(), array('context' => '', 'langcode' => $langcode));
1786
        break;
1787

    
1788
      case 'D':
1789
        $datestring .= t($date->format('D'), array(), array('context' => '', 'langcode' => $langcode));
1790
        break;
1791

    
1792
      case 'F':
1793
        $datestring .= t($date->format('F'), array(), array('context' => 'Long month name', 'langcode' => $langcode));
1794
        break;
1795

    
1796
      case 'M':
1797
        $datestring .= t($date->format('M'), array(), array('langcode' => $langcode));
1798
        break;
1799

    
1800
      case 'A':
1801
      case 'a':
1802
        $datestring .= t($date->format($c), array(), array('context' => 'ampm', 'langcode' => $langcode));
1803
        break;
1804

    
1805
      // The timezone name translations can use t().
1806
      case 'e':
1807
      case 'T':
1808
        $datestring .= t($date->format($c));
1809
        break;
1810

    
1811
      // Remaining date parts need no translation.
1812
      case 'O':
1813
        $datestring .= sprintf('%s%02d%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1814
        break;
1815

    
1816
      case 'P':
1817
        $datestring .= sprintf('%s%02d:%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1818
        break;
1819

    
1820
      case 'Z':
1821
        $datestring .= date_offset_get($date);
1822
        break;
1823

    
1824
      case '\\':
1825
        $datestring .= $format[++$i];
1826
        break;
1827

    
1828
      case 'r':
1829
        $datestring .= date_format_date($date, 'custom', 'D, d M Y H:i:s O', 'en');
1830
        break;
1831

    
1832
      default:
1833
        if (strpos('BdcgGhHiIjLmnNosStTuUwWYyz', $c) !== FALSE) {
1834
          $datestring .= $date->format($c);
1835
        }
1836
        else {
1837
          $datestring .= $c;
1838
        }
1839
    }
1840
  }
1841
  return $datestring;
1842
}
1843

    
1844
/**
1845
 * Formats a time interval with granularity, including past and future context.
1846
 *
1847
 * @param object $date
1848
 *   The current date object.
1849
 * @param int $granularity
1850
 *   (optional) Number of units to display in the string. Defaults to 2.
1851
 *
1852
 * @return string
1853
 *   A translated string representation of the interval.
1854
 *
1855
 * @see format_interval()
1856
 */
1857
function date_format_interval($date, $granularity = 2, $display_ago = TRUE) {
1858
  // If no date is sent, then return nothing.
1859
  if (empty($date)) {
1860
    return NULL;
1861
  }
1862

    
1863
  $interval = REQUEST_TIME - $date->format('U');
1864
  if ($interval > 0) {
1865
    return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) :
1866
      t('!time', array('!time' => format_interval($interval, $granularity)));
1867
  }
1868
  else {
1869
    return format_interval(abs($interval), $granularity);
1870
  }
1871
}
1872

    
1873
/**
1874
 * A date object for the current time.
1875
 *
1876
 * @param object|string|null $timezone
1877
 *   (optional) PHP DateTimeZone object, string or NULL allowed. Optionally
1878
 *   force time to a specific timezone, defaults to user timezone, if set,
1879
 *   otherwise site timezone. Defaults to NULL.
1880
 * @param bool $reset
1881
 *   (optional) Static cache reset.
1882
 *
1883
 * @return object
1884
 *   The current time as a date object.
1885
 */
1886
function date_now($timezone = NULL, $reset = FALSE) {
1887
  if ($timezone instanceof DateTimeZone) {
1888
    $static_var = __FUNCTION__ . $timezone->getName();
1889
  }
1890
  else {
1891
    $static_var = __FUNCTION__ . $timezone;
1892
  }
1893

    
1894
  if ($reset) {
1895
    drupal_static_reset($static_var);
1896
  }
1897

    
1898
  $now = &drupal_static($static_var);
1899

    
1900
  if (!isset($now)) {
1901
    $now = new DateObject('now', $timezone);
1902
  }
1903

    
1904
  // Avoid unexpected manipulation of cached $now object
1905
  // by subsequent code execution.
1906
  // @see https://drupal.org/node/2261395
1907
  $clone = clone $now;
1908
  return $clone;
1909
}
1910

    
1911
/**
1912
 * Determines if a timezone string is valid.
1913
 *
1914
 * @param string $timezone
1915
 *   A potentially invalid timezone string.
1916
 *
1917
 * @return bool
1918
 *   TRUE if the timezone is valid, FALSE otherwise.
1919
 */
1920
function date_timezone_is_valid($timezone) {
1921
  static $timezone_names;
1922
  if (empty($timezone_names)) {
1923
    $timezone_names = array_keys(date_timezone_names(TRUE));
1924
  }
1925
  return in_array($timezone, $timezone_names);
1926
}
1927

    
1928
/**
1929
 * Returns a timezone name to use as a default.
1930
 *
1931
 * @param bool $check_user
1932
 *   (optional) Whether or not to check for a user-configured timezone.
1933
 *   Defaults to TRUE.
1934
 *
1935
 * @return string
1936
 *   The default timezone for a user, if available, otherwise the site.
1937
 */
1938
function date_default_timezone($check_user = TRUE) {
1939
  global $user;
1940
  if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) {
1941
    return $user->timezone;
1942
  }
1943
  else {
1944
    $default = variable_get('date_default_timezone', '');
1945
    return empty($default) ? 'UTC' : $default;
1946
  }
1947
}
1948

    
1949
/**
1950
 * Returns a timezone object for the default timezone.
1951
 *
1952
 * @param bool $check_user
1953
 *   (optional) Whether or not to check for a user-configured timezone.
1954
 *   Defaults to TRUE.
1955
 *
1956
 * @return object
1957
 *   The default timezone for a user, if available, otherwise the site.
1958
 */
1959
function date_default_timezone_object($check_user = TRUE) {
1960
  return timezone_open(date_default_timezone($check_user));
1961
}
1962

    
1963
/**
1964
 * Identifies the number of days in a month for a date.
1965
 */
1966
function date_days_in_month($year, $month) {
1967
  // Pick a day in the middle of the month to avoid timezone shifts.
1968
  $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00';
1969
  $date = new DateObject($datetime);
1970
  if ($date->errors) {
1971
    return FALSE;
1972
  }
1973
  else {
1974
    return $date->format('t');
1975
  }
1976
}
1977

    
1978
/**
1979
 * Identifies the number of days in a year for a date.
1980
 *
1981
 * @param mixed $date
1982
 *   (optional) The current date object, or a date string. Defaults to NULL.
1983
 *
1984
 * @return int
1985
 *   The number of days in the year.
1986
 */
1987
function date_days_in_year($date = NULL) {
1988
  if (empty($date)) {
1989
    $date = date_now();
1990
  }
1991
  elseif (!is_object($date)) {
1992
    $date = new DateObject($date);
1993
  }
1994
  if (is_object($date)) {
1995
    if ($date->format('L')) {
1996
      return 366;
1997
    }
1998
    else {
1999
      return 365;
2000
    }
2001
  }
2002
  return NULL;
2003
}
2004

    
2005
/**
2006
 * Identifies the number of ISO weeks in a year for a date.
2007
 *
2008
 * December 28 is always in the last ISO week of the year.
2009
 *
2010
 * @param mixed $date
2011
 *   (optional) The current date object, or a date string. Defaults to NULL.
2012
 *
2013
 * @return int
2014
 *   The number of ISO weeks in a year.
2015
 */
2016
function date_iso_weeks_in_year($date = NULL) {
2017
  if (empty($date)) {
2018
    $date = date_now();
2019
  }
2020
  elseif (!is_object($date)) {
2021
    $date = new DateObject($date);
2022
  }
2023

    
2024
  if (is_object($date)) {
2025
    date_date_set($date, $date->format('Y'), 12, 28);
2026
    return $date->format('W');
2027
  }
2028
  return NULL;
2029
}
2030

    
2031
/**
2032
 * Returns day of week for a given date (0 = Sunday).
2033
 *
2034
 * @param mixed $date
2035
 *   (optional) A date, default is current local day. Defaults to NULL.
2036
 *
2037
 * @return int
2038
 *   The number of the day in the week.
2039
 */
2040
function date_day_of_week($date = NULL) {
2041
  if (empty($date)) {
2042
    $date = date_now();
2043
  }
2044
  elseif (!is_object($date)) {
2045
    $date = new DateObject($date);
2046
  }
2047

    
2048
  if (is_object($date)) {
2049
    return $date->format('w');
2050
  }
2051
  return NULL;
2052
}
2053

    
2054
/**
2055
 * Returns translated name of the day of week for a given date.
2056
 *
2057
 * @param mixed $date
2058
 *   (optional) A date, default is current local day. Defaults to NULL.
2059
 * @param string $abbr
2060
 *   (optional) Whether to return the abbreviated name for that day.
2061
 *   Defaults to TRUE.
2062
 *
2063
 * @return string
2064
 *   The name of the day in the week for that date.
2065
 */
2066
function date_day_of_week_name($date = NULL, $abbr = TRUE) {
2067
  if (!is_object($date)) {
2068
    $date = new DateObject($date);
2069
  }
2070
  $dow = date_day_of_week($date);
2071
  $days = $abbr ? date_week_days_abbr() : date_week_days();
2072
  return $days[$dow];
2073
}
2074

    
2075
/**
2076
 * Calculates the start and end dates for a calendar week.
2077
 *
2078
 * The dates are adjusted to use the chosen first day of week for this site.
2079
 *
2080
 * @param int $week
2081
 *   The week value.
2082
 * @param int $year
2083
 *   The year value.
2084
 *
2085
 * @return array
2086
 *   A numeric array containing the start and end dates of a week.
2087
 */
2088
function date_week_range($week, $year) {
2089
  if (variable_get('date_api_use_iso8601', FALSE)) {
2090
    return date_iso_week_range($week, $year);
2091
  }
2092
  $min_date = new DateObject($year . '-01-01 00:00:00');
2093
  $min_date->setTimezone(date_default_timezone_object());
2094

    
2095
  // Move to the right week.
2096
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
2097

    
2098
  // Move backwards to the first day of the week.
2099
  $first_day = variable_get('date_first_day', 0);
2100
  $day_wday = date_format($min_date, 'w');
2101
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
2102

    
2103
  // Move forwards to the last day of the week.
2104
  $max_date = clone $min_date;
2105
  date_modify($max_date, '+6 days +23 hours +59 minutes +59 seconds');
2106

    
2107
  if (date_format($min_date, 'Y') != $year) {
2108
    $min_date = new DateObject($year . '-01-01 00:00:00');
2109
  }
2110
  return array($min_date, $max_date);
2111
}
2112

    
2113
/**
2114
 * Calculates the start and end dates for an ISO week.
2115
 *
2116
 * @param int $week
2117
 *   The week value.
2118
 * @param int $year
2119
 *   The year value.
2120
 *
2121
 * @return array
2122
 *   A numeric array containing the start and end dates of an ISO week.
2123
 */
2124
function date_iso_week_range($week, $year) {
2125
  // Get to the last ISO week of the previous year.
2126
  $min_date = new DateObject(($year - 1) . '-12-28 00:00:00');
2127
  date_timezone_set($min_date, date_default_timezone_object());
2128

    
2129
  // Find the first day of the first ISO week in the year.
2130
  // If it's already a Monday, date_modify won't add a Monday,
2131
  // it will remain the same day. So add a Sunday first, then a Monday.
2132
  date_modify($min_date, '+1 Sunday');
2133
  date_modify($min_date, '+1 Monday');
2134

    
2135
  // Jump ahead to the desired week for the beginning of the week range.
2136
  if ($week > 1) {
2137
    date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
2138
  }
2139

    
2140
  // Move forwards to the last day of the week.
2141
  $max_date = clone $min_date;
2142
  date_modify($max_date, '+6 days +23 hours +59 minutes +59 seconds');
2143
  return array($min_date, $max_date);
2144
}
2145

    
2146
/**
2147
 * The number of calendar weeks in a year.
2148
 *
2149
 * PHP week functions return the ISO week, not the calendar week.
2150
 *
2151
 * @param int $year
2152
 *   A year value.
2153
 *
2154
 * @return int
2155
 *   Number of calendar weeks in selected year.
2156
 */
2157
function date_weeks_in_year($year) {
2158
  $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC');
2159
  date_modify($date, '-1 day');
2160
  return date_week($date->format('Y-m-d'));
2161
}
2162

    
2163
/**
2164
 * The calendar week number for a date.
2165
 *
2166
 * PHP week functions return the ISO week, not the calendar week.
2167
 *
2168
 * @param string $date
2169
 *   A date string in the format Y-m-d.
2170
 *
2171
 * @return int
2172
 *   The calendar week number.
2173
 */
2174
function date_week($date) {
2175
  $date = substr($date, 0, 10);
2176
  $parts = explode('-', $date);
2177

    
2178
  $date = new DateObject($date . ' 12:00:00', 'UTC');
2179

    
2180
  // If we are using ISO weeks, this is easy.
2181
  if (variable_get('date_api_use_iso8601', FALSE)) {
2182
    return intval($date->format('W'));
2183
  }
2184

    
2185
  $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
2186
  $week = intval($date->format('W'));
2187
  $year_week = intval(date_format($year_date, 'W'));
2188
  $date_year = intval($date->format('o'));
2189

    
2190
  // Remove the leap week if it's present.
2191
  if ($date_year > intval($parts[0])) {
2192
    $last_date = clone $date;
2193
    date_modify($last_date, '-7 days');
2194
    $week = date_format($last_date, 'W') + 1;
2195
  }
2196
  elseif ($date_year < intval($parts[0])) {
2197
    $week = 0;
2198
  }
2199

    
2200
  if ($year_week != 1) {
2201
    $week++;
2202
  }
2203

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

    
2207
  // If it's before the starting day, it's the previous week.
2208
  if (intval($date->format('N')) < $iso_first_day) {
2209
    $week--;
2210
  }
2211

    
2212
  // If the year starts before, it's an extra week at the beginning.
2213
  if (intval(date_format($year_date, 'N')) < $iso_first_day) {
2214
    $week++;
2215
  }
2216

    
2217
  return $week;
2218
}
2219

    
2220
/**
2221
 * Helper function to left pad date parts with zeros.
2222
 *
2223
 * Provided because this is needed so often with dates.
2224
 *
2225
 * @param int $value
2226
 *   The value to pad.
2227
 * @param int $size
2228
 *   (optional) Total size expected, usually 2 or 4. Defaults to 2.
2229
 *
2230
 * @return string
2231
 *   The padded value.
2232
 */
2233
function date_pad($value, $size = 2) {
2234
  return sprintf("%0" . $size . "d", $value);
2235
}
2236

    
2237
/**
2238
 * Determines if the granularity contains a time portion.
2239
 *
2240
 * @param array $granularity
2241
 *   An array of allowed date parts, all others will be removed.
2242
 *
2243
 * @return bool
2244
 *   TRUE if the granularity contains a time portion, FALSE otherwise.
2245
 */
2246
function date_has_time(array $granularity) {
2247
  if (!is_array($granularity)) {
2248
    $granularity = array();
2249
  }
2250
  $options = array('hour', 'minute', 'second');
2251
  return (bool) count(array_intersect($granularity, $options));
2252
}
2253

    
2254
/**
2255
 * Determines if the granularity contains a date portion.
2256
 *
2257
 * @param array $granularity
2258
 *   An array of allowed date parts, all others will be removed.
2259
 *
2260
 * @return bool
2261
 *   TRUE if the granularity contains a date portion, FALSE otherwise.
2262
 */
2263
function date_has_date(array $granularity) {
2264
  if (!is_array($granularity)) {
2265
    $granularity = array();
2266
  }
2267
  $options = array('year', 'month', 'day');
2268
  return (bool) count(array_intersect($granularity, $options));
2269
}
2270

    
2271
/**
2272
 * Helper function to get a format for a specific part of a date field.
2273
 *
2274
 * @param string $part
2275
 *   The date field part, either 'time' or 'date'.
2276
 * @param string $format
2277
 *   A date format string.
2278
 *
2279
 * @return string
2280
 *   The date format for the given part.
2281
 */
2282
function date_part_format($part, $format) {
2283
  switch ($part) {
2284
    case 'date':
2285
      return date_limit_format($format, array('year', 'month', 'day'));
2286

    
2287
    case 'time':
2288
      return date_limit_format($format, array('hour', 'minute', 'second'));
2289

    
2290
    default:
2291
      return date_limit_format($format, array($part));
2292
  }
2293
}
2294

    
2295
/**
2296
 * Limits a date format to include only elements from a given granularity array.
2297
 *
2298
 * Example:
2299
 *   date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
2300
 *   returns 'F j, Y'
2301
 *
2302
 * @param string $format
2303
 *   A date format string.
2304
 * @param array $granularity
2305
 *   An array of allowed date parts, all others will be removed.
2306
 *
2307
 * @return string
2308
 *   The format string with all other elements removed.
2309
 */
2310
function date_limit_format($format, array $granularity) {
2311
  // Use the advanced drupal_static() pattern to improve performance.
2312
  static $drupal_static_fast;
2313
  if (!isset($drupal_static_fast)) {
2314
    $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__);
2315
  }
2316
  $formats = &$drupal_static_fast['formats'];
2317
  $format_granularity_cid = $format . '|' . implode(',', $granularity);
2318
  if (isset($formats[$format_granularity_cid])) {
2319
    return $formats[$format_granularity_cid];
2320
  }
2321

    
2322
  // If punctuation has been escaped, remove the escaping. Done using strtr()
2323
  // because it is easier than getting the escape character extracted using
2324
  // preg_replace().
2325
  $replace = array(
2326
    '\-' => '-',
2327
    '\:' => ':',
2328
    "\'" => "'",
2329
    '\. ' => ' . ',
2330
    '\,' => ',',
2331
  );
2332
  $format = strtr($format, $replace);
2333

    
2334
  // Get the 'T' out of ISO date formats that don't have both date and time.
2335
  if (!date_has_time($granularity) || !date_has_date($granularity)) {
2336
    $format = str_replace('\T', ' ', $format);
2337
    $format = str_replace('T', ' ', $format);
2338
  }
2339

    
2340
  $regex = array();
2341
  if (!date_has_time($granularity)) {
2342
    $regex[] = '((?<!\\\\)[a|A])';
2343
  }
2344
  // Create regular expressions to remove selected values from string.
2345
  // Use (?<!\\\\) to keep escaped letters from being removed.
2346
  foreach (date_nongranularity($granularity) as $element) {
2347
    switch ($element) {
2348
      case 'year':
2349
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
2350
        break;
2351

    
2352
      case 'day':
2353
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[l|D|d|dS|j|jS|N|w|W|z]{1,2})';
2354
        break;
2355

    
2356
      case 'month':
2357
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
2358
        break;
2359

    
2360
      case 'hour':
2361
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
2362
        break;
2363

    
2364
      case 'minute':
2365
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
2366
        break;
2367

    
2368
      case 'second':
2369
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
2370
        break;
2371

    
2372
      case 'timezone':
2373
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
2374
        break;
2375

    
2376
    }
2377
  }
2378
  // Remove empty parentheses, brackets, pipes.
2379
  $regex[] = '(\(\))';
2380
  $regex[] = '(\[\])';
2381
  $regex[] = '(\|\|)';
2382

    
2383
  // Remove selected values from string.
2384
  $format = trim(preg_replace($regex, array(), $format));
2385
  // Remove orphaned punctuation at the beginning of the string.
2386
  $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
2387
  // Remove orphaned punctuation at the end of the string.
2388
  $format = preg_replace('([\-/,:\']$)', '', $format);
2389
  $format = preg_replace('(\\$)', '', $format);
2390

    
2391
  // Trim any whitespace from the result.
2392
  $format = trim($format);
2393

    
2394
  // After removing the non-desired parts of the format, test if the only things
2395
  // left are escaped, non-date, characters. If so, return nothing.
2396
  // Using S instead of w to pick up non-ASCII characters.
2397
  $test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
2398
  if (empty($test)) {
2399
    $format = '';
2400
  }
2401

    
2402
  // Store the return value in the static array for performance.
2403
  $formats[$format_granularity_cid] = $format;
2404

    
2405
  return $format;
2406
}
2407

    
2408
/**
2409
 * Converts a format to an ordered array of granularity parts.
2410
 *
2411
 * Example:
2412
 *   date_format_order('m/d/Y H:i')
2413
 *   returns
2414
 *     array(
2415
 *       0 => 'month',
2416
 *       1 => 'day',
2417
 *       2 => 'year',
2418
 *       3 => 'hour',
2419
 *       4 => 'minute',
2420
 *     );
2421
 *
2422
 * @param string $format
2423
 *   A date format string.
2424
 *
2425
 * @return array
2426
 *   An array of ordered granularity elements from the given format string.
2427
 */
2428
function date_format_order($format) {
2429
  $order = array();
2430
  if (empty($format)) {
2431
    return $order;
2432
  }
2433

    
2434
  $max = strlen($format);
2435
  for ($i = 0; $i <= $max; $i++) {
2436
    if (!isset($format[$i])) {
2437
      break;
2438
    }
2439
    switch ($format[$i]) {
2440
      case 'd':
2441
      case 'j':
2442
        $order[] = 'day';
2443
        break;
2444

    
2445
      case 'F':
2446
      case 'M':
2447
      case 'm':
2448
      case 'n':
2449
        $order[] = 'month';
2450
        break;
2451

    
2452
      case 'Y':
2453
      case 'y':
2454
        $order[] = 'year';
2455
        break;
2456

    
2457
      case 'g':
2458
      case 'G':
2459
      case 'h':
2460
      case 'H':
2461
        $order[] = 'hour';
2462
        break;
2463

    
2464
      case 'i':
2465
        $order[] = 'minute';
2466
        break;
2467

    
2468
      case 's':
2469
        $order[] = 'second';
2470
        break;
2471
    }
2472
  }
2473
  return $order;
2474
}
2475

    
2476
/**
2477
 * Strips out unwanted granularity elements.
2478
 *
2479
 * @param array $granularity
2480
 *   An array like ('year', 'month', 'day', 'hour', 'minute', 'second').
2481
 *
2482
 * @return array
2483
 *   A reduced set of granularitiy elements.
2484
 */
2485
function date_nongranularity(array $granularity) {
2486
  $options = array(
2487
    'year',
2488
    'month',
2489
    'day',
2490
    'hour',
2491
    'minute',
2492
    'second',
2493
    'timezone',
2494
  );
2495
  return array_diff($options, (array) $granularity);
2496
}
2497

    
2498
/**
2499
 * Implements hook_element_info().
2500
 */
2501
function date_api_element_info() {
2502
  module_load_include('inc', 'date_api', 'date_api_elements');
2503
  return _date_api_element_info();
2504
}
2505

    
2506
/**
2507
 * Implements hook_theme().
2508
 */
2509
function date_api_theme($existing, $type, $theme, $path) {
2510
  $base = array(
2511
    'file' => 'theme.inc',
2512
    'path' => "$path/theme",
2513
  );
2514
  return array(
2515
    'date_nav_title' => $base + array(
2516
      'variables' => array(
2517
        'granularity' => NULL,
2518
        'view' => NULL,
2519
        'link' => NULL,
2520
        'format' => NULL,
2521
      ),
2522
    ),
2523
    'date_timezone' => $base + array('render element' => 'element'),
2524
    'date_select' => $base + array('render element' => 'element'),
2525
    'date_text' => $base + array('render element' => 'element'),
2526
    'date_select_element' => $base + array('render element' => 'element'),
2527
    'date_textfield_element' => $base + array('render element' => 'element'),
2528
    'date_part_hour_prefix' => $base + array('render element' => 'element'),
2529
    'date_part_minsec_prefix' => $base + array('render element' => 'element'),
2530
    'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2531
    'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2532
    'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2533
    'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2534
    'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2535
    'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2536
    'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2537
    'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2538
    'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2539
    'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2540
    'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'),
2541
    'date_calendar_day' => $base + array('variables' => array('date' => NULL)),
2542
    'date_time_ago' => $base + array(
2543
      'variables' => array(
2544
        'start_date' => NULL,
2545
        'end_date' => NULL,
2546
        'interval' => NULL,
2547
      ),
2548
    ),
2549
  );
2550
}
2551

    
2552
/**
2553
 * Function to figure out which local timezone applies to a date and select it.
2554
 *
2555
 * @param string $handling
2556
 *   The timezone handling.
2557
 * @param string $timezone
2558
 *   (optional) A timezone string. Defaults to an empty string.
2559
 *
2560
 * @return string
2561
 *   The timezone string.
2562
 */
2563
function date_get_timezone($handling, $timezone = '') {
2564
  switch ($handling) {
2565
    case 'date':
2566
      $timezone = !empty($timezone) ? $timezone : date_default_timezone();
2567
      break;
2568

    
2569
    case 'utc':
2570
      $timezone = 'UTC';
2571
      break;
2572

    
2573
    default:
2574
      $timezone = date_default_timezone();
2575
  }
2576
  return $timezone > '' ? $timezone : date_default_timezone();
2577
}
2578

    
2579
/**
2580
 * Function to figure out which db timezone applies to a date.
2581
 *
2582
 * @param string $handling
2583
 *   The timezone handling.
2584
 * @param string $timezone
2585
 *   (optional) When $handling is 'date', date_get_timezone_db() returns this
2586
 *   value.
2587
 *
2588
 * @return string
2589
 *   The timezone string.
2590
 */
2591
function date_get_timezone_db($handling, $timezone = NULL) {
2592
  switch ($handling) {
2593
    case ('utc'):
2594
    case ('site'):
2595
    case ('user'):
2596
      // These handling modes all convert to UTC before storing in the DB.
2597
      $timezone = 'UTC';
2598
      break;
2599

    
2600
    case ('date'):
2601
      if ($timezone == NULL) {
2602
        // This shouldn't happen, since it's meaning is undefined. But we need
2603
        // to fall back to *something* that's a legal timezone.
2604
        $timezone = date_default_timezone();
2605
      }
2606
      break;
2607

    
2608
    case ('none'):
2609
    default:
2610
      $timezone = date_default_timezone();
2611
  }
2612
  return $timezone;
2613
}
2614

    
2615
/**
2616
 * Helper function for converting back and forth from '+1' to 'First'.
2617
 */
2618
function date_order_translated() {
2619
  return array(
2620
    '+1' => t('First', array(), array('context' => 'date_order')),
2621
    '+2' => t('Second', array(), array('context' => 'date_order')),
2622
    '+3' => t('Third', array(), array('context' => 'date_order')),
2623
    '+4' => t('Fourth', array(), array('context' => 'date_order')),
2624
    '+5' => t('Fifth', array(), array('context' => 'date_order')),
2625
    '-1' => t('Last', array(), array('context' => 'date_order_reverse')),
2626
    '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')),
2627
    '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')),
2628
    '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')),
2629
    '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')),
2630
  );
2631
}
2632

    
2633
/**
2634
 * Creates an array of ordered strings, using English text when possible.
2635
 */
2636
function date_order() {
2637
  return array(
2638
    '+1' => 'First',
2639
    '+2' => 'Second',
2640
    '+3' => 'Third',
2641
    '+4' => 'Fourth',
2642
    '+5' => 'Fifth',
2643
    '-1' => 'Last',
2644
    '-2' => '-2',
2645
    '-3' => '-3',
2646
    '-4' => '-4',
2647
    '-5' => '-5',
2648
  );
2649
}
2650

    
2651
/**
2652
 * Tests validity of a date range string.
2653
 *
2654
 * @param string $string
2655
 *   A min and max year string like '-3:+1'a.
2656
 *
2657
 * @return bool
2658
 *   TRUE if the date range is valid, FALSE otherwise.
2659
 */
2660
function date_range_valid($string) {
2661
  $matches = preg_match('@^([\+\-][0-9]+|[0-9]{4}):([\+\-][0-9]+|[0-9]{4})$@', $string);
2662
  return $matches < 1 ? FALSE : TRUE;
2663
}
2664

    
2665
/**
2666
 * Splits a string like -3:+3 or 2001:2010 into an array of start and end years.
2667
 *
2668
 * Center the range around the current year, if any, but expand it far
2669
 * enough so it will pick up the year value in the field in case
2670
 * the value in the field is outside the initial range.
2671
 *
2672
 * @param string $string
2673
 *   A min and max year string like '-3:+1'.
2674
 * @param object $date
2675
 *   (optional) A date object. Defaults to NULL.
2676
 *
2677
 * @return array
2678
 *   A numerically indexed array, containing a start and end year.
2679
 */
2680
function date_range_years($string, $date = NULL) {
2681
  $this_year = date_format(date_now(), 'Y');
2682
  list($start_year, $end_year) = explode(':', $string);
2683

    
2684
  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
2685
  $plus_pattern = '@[\+\-][0-9]{1,4}@';
2686
  $year_pattern = '@^[0-9]{4}@';
2687
  if (!preg_match($year_pattern, $start_year, $matches)) {
2688
    if (preg_match($plus_pattern, $start_year, $matches)) {
2689
      $start_year = $this_year + $matches[0];
2690
    }
2691
    else {
2692
      $start_year = $this_year;
2693
    }
2694
  }
2695
  if (!preg_match($year_pattern, $end_year, $matches)) {
2696
    if (preg_match($plus_pattern, $end_year, $matches)) {
2697
      $end_year = $this_year + $matches[0];
2698
    }
2699
    else {
2700
      $end_year = $this_year;
2701
    }
2702
  }
2703
  // If there is a current value, stretch the range to include it.
2704
  $value_year = is_object($date) ? $date->format('Y') : '';
2705
  if (!empty($value_year)) {
2706
    if ($start_year <= $end_year) {
2707
      $start_year = min($value_year, $start_year);
2708
      $end_year = max($value_year, $end_year);
2709
    }
2710
    else {
2711
      $start_year = max($value_year, $start_year);
2712
      $end_year = min($value_year, $end_year);
2713
    }
2714
  }
2715
  return array($start_year, $end_year);
2716
}
2717

    
2718
/**
2719
 * Converts a min and max year into a string like '-3:+1'.
2720
 *
2721
 * @param array $years
2722
 *   A numerically indexed array, containing a minimum and maximum year.
2723
 *
2724
 * @return string
2725
 *   A min and max year string like '-3:+1'.
2726
 */
2727
function date_range_string(array $years) {
2728
  $this_year = date_format(date_now(), 'Y');
2729

    
2730
  if ($years[0] < $this_year) {
2731
    $min = '-' . ($this_year - $years[0]);
2732
  }
2733
  else {
2734
    $min = '+' . ($years[0] - $this_year);
2735
  }
2736

    
2737
  if ($years[1] < $this_year) {
2738
    $max = '-' . ($this_year - $years[1]);
2739
  }
2740
  else {
2741
    $max = '+' . ($years[1] - $this_year);
2742
  }
2743

    
2744
  return $min . ':' . $max;
2745
}
2746

    
2747
/**
2748
 * Temporary helper to re-create equivalent of content_database_info().
2749
 */
2750
function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) {
2751
  return array(
2752
    'columns' => $field['storage']['details']['sql'][$revision],
2753
    'table' => _field_sql_storage_tablename($field),
2754
  );
2755
}
2756

    
2757
/**
2758
 * Implements hook_form_FORM_ID_alter() for system_regional_settings().
2759
 *
2760
 * Add a form element to configure whether or not week numbers are ISO-8601, the
2761
 * default is FALSE (US/UK/AUS norm).
2762
 */
2763
function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) {
2764
  $form['locale']['date_api_use_iso8601'] = array(
2765
    '#type' => 'checkbox',
2766
    '#title' => t('Use ISO-8601 week numbers'),
2767
    '#default_value' => variable_get('date_api_use_iso8601', FALSE),
2768
    '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'),
2769
  );
2770
  $form['#validate'][] = 'date_api_form_system_settings_validate';
2771
}
2772

    
2773
/**
2774
 * Validate that the option to use ISO weeks matches first day of week choice.
2775
 */
2776
function date_api_form_system_settings_validate(&$form, &$form_state) {
2777
  $form_values = $form_state['values'];
2778
  if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) {
2779
    form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.'));
2780
  }
2781
}
2782

    
2783
/**
2784
 * Creates an array of date format types for use as an options list.
2785
 */
2786
function date_format_type_options() {
2787
  $options = array();
2788
  $format_types = system_get_date_types();
2789
  if (!empty($format_types)) {
2790
    foreach ($format_types as $type => $type_info) {
2791
      $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')';
2792
    }
2793
  }
2794
  return $options;
2795
}
2796

    
2797
/**
2798
 * Creates an example date.
2799
 *
2800
 * This ensures a clear difference between month and day, and 12 and 24 hours.
2801
 */
2802
function date_example_date() {
2803
  $now = date_now();
2804
  if (date_format($now, 'M') == date_format($now, 'F')) {
2805
    date_modify($now, '+1 month');
2806
  }
2807
  if (date_format($now, 'm') == date_format($now, 'd')) {
2808
    date_modify($now, '+1 day');
2809
  }
2810
  if (date_format($now, 'H') == date_format($now, 'h')) {
2811
    date_modify($now, '+12 hours');
2812
  }
2813
  return $now;
2814
}
2815

    
2816
/**
2817
 * Determine if a start/end date combination qualify as 'All day'.
2818
 *
2819
 * @param string $string1
2820
 *   A string date in datetime format for the 'start' date.
2821
 * @param string $string2
2822
 *   A string date in datetime format for the 'end' date.
2823
 * @param string $granularity
2824
 *   (optional) The granularity of the date. Allowed values are:
2825
 *   - 'second' (default)
2826
 *   - 'minute'
2827
 *   - 'hour'
2828
 * @param int $increment
2829
 *   (optional) The increment of the date. Only allows positive integers.
2830
 *   Defaults to 1.
2831
 *
2832
 * @return bool
2833
 *   TRUE if the date is all day, FALSE otherwise.
2834
 */
2835
function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) {
2836
  // Both date strings must be present.
2837
  if (empty($string1) || empty($string2)) {
2838
    return FALSE;
2839
  }
2840
  // The granularity argument only allows three options.
2841
  elseif (!in_array($granularity, array('hour', 'minute', 'second'))) {
2842
    return FALSE;
2843
  }
2844
  // The increment must be an integer.
2845
  elseif (!is_int($increment) || $increment === 0) {
2846
    return FALSE;
2847
  }
2848

    
2849
  // Verify the first date argument is a valid date string.
2850
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches);
2851
  $count = count($matches);
2852
  $date1 = $count > 1 ? $matches[1] : '';
2853
  $time1 = $count > 2 ? $matches[2] : '';
2854
  $hour1 = $count > 3 ? intval($matches[3]) : 0;
2855
  $min1 = $count > 4 ? intval($matches[4]) : 0;
2856
  $sec1 = $count > 5 ? intval($matches[5]) : 0;
2857
  if (empty($date1) || empty($time1)) {
2858
    return FALSE;
2859
  }
2860

    
2861
  // Verify the second date argument is a valid date string.
2862
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches);
2863
  $count = count($matches);
2864
  $date2 = $count > 1 ? $matches[1] : '';
2865
  $time2 = $count > 2 ? $matches[2] : '';
2866
  $hour2 = $count > 3 ? intval($matches[3]) : 0;
2867
  $min2 = $count > 4 ? intval($matches[4]) : 0;
2868
  $sec2 = $count > 5 ? intval($matches[5]) : 0;
2869
  if (empty($date2) || empty($time2)) {
2870
    return FALSE;
2871
  }
2872

    
2873
  $tmp = date_seconds('s', TRUE, $increment);
2874
  $max_seconds = intval(array_pop($tmp));
2875
  $tmp = date_minutes('i', TRUE, $increment);
2876
  $max_minutes = intval(array_pop($tmp));
2877

    
2878
  // See if minutes and seconds are the maximum allowed for an increment, or the
2879
  // maximum possible (59), or 0.
2880
  switch ($granularity) {
2881
    case 'second':
2882
      $min_match = ($hour1 == 0 && $min1 == 0 && $sec1 == 0);
2883
      $max_match = ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59)))
2884
        || ($date1 != $date2 && $hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0);
2885
      break;
2886

    
2887
    case 'minute':
2888
      $min_match = ($hour1 == 0 && $min1 == 0);
2889
      $max_match = ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
2890
        || ($date1 != $date2 && $hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
2891
      break;
2892

    
2893
    case 'hour':
2894
      $min_match = ($hour1 == 0);
2895
      $max_match = ($hour2 == 23)
2896
        || ($date1 != $date2 && $hour1 == 0 && $hour2 == 0);
2897
      break;
2898

    
2899
    default:
2900
      $min_match = TRUE;
2901
      $max_match = FALSE;
2902
  }
2903

    
2904
  if ($min_match && $max_match) {
2905
    return TRUE;
2906
  }
2907

    
2908
  return FALSE;
2909
}
2910

    
2911
/**
2912
 * Helper function to round minutes and seconds to requested value.
2913
 */
2914
function date_increment_round(&$date, $increment) {
2915
  // Round minutes and seconds, if necessary.
2916
  if (is_object($date) && $increment > 1) {
2917
    $day = intval(date_format($date, 'j'));
2918
    $hour = intval(date_format($date, 'H'));
2919
    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
2920
    $minute = intval(date_format($date, 'i'));
2921
    if ($second == 60) {
2922
      $minute += 1;
2923
      $second = 0;
2924
    }
2925
    $minute = intval(round($minute / $increment) * $increment);
2926
    if ($minute == 60) {
2927
      $hour += 1;
2928
      $minute = 0;
2929
    }
2930
    date_time_set($date, $hour, $minute, $second);
2931
    if ($hour == 24) {
2932
      $day += 1;
2933
      $hour = 0;
2934
      $year = date_format($date, 'Y');
2935
      $month = date_format($date, 'n');
2936
      date_date_set($date, $year, $month, $day);
2937
    }
2938
  }
2939
  return $date;
2940
}
2941

    
2942
/**
2943
 * Determines if a date object is valid.
2944
 *
2945
 * @param object $date
2946
 *   The date object to check.
2947
 *
2948
 * @return bool
2949
 *   TRUE if the date is a valid date object, FALSE otherwise.
2950
 */
2951
function date_is_date($date) {
2952
  if (empty($date) || !is_object($date) || !empty($date->errors)) {
2953
    return FALSE;
2954
  }
2955
  return TRUE;
2956
}
2957

    
2958
/**
2959
 * Replace specific ISO values using patterns.
2960
 *
2961
 * Function will replace ISO values that have the pattern 9999-00-00T00:00:00
2962
 * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO
2963
 * dates and ensure that date objects created from this value contain a valid
2964
 * month and day.
2965
 *
2966
 * Without this fix, the ISO date '2020-00-00T00:00:00' would be created as
2967
 * November 30, 2019 (the previous day in the previous month).
2968
 *
2969
 * @param string $iso_string
2970
 *   An ISO string that needs to be made into a complete, valid date.
2971
 *
2972
 * @return mixed|string
2973
 *   replaced value, or incoming value.
2974
 *
2975
 * @todo Expand on this to work with all sorts of partial ISO dates.
2976
 */
2977
function date_make_iso_valid($iso_string) {
2978
  // If this isn't a value that uses an ISO pattern, there is nothing to do.
2979
  if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) {
2980
    return $iso_string;
2981
  }
2982
  // First see if month and day parts are '-00-00'.
2983
  if (substr($iso_string, 4, 6) == '-00-00') {
2984
    return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string);
2985
  }
2986
  // Then see if the day part is '-00'.
2987
  elseif (substr($iso_string, 7, 3) == '-00') {
2988
    return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string);
2989
  }
2990

    
2991
  // Fall through, no changes required.
2992
  return $iso_string;
2993
}