Projet

Général

Profil

Paste
Télécharger (86,6 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api.module @ db9ffd17

1
<?php
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
112
}
113

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
488
  /**
489
   * Returns whether this object has time set.
490
   *
491
   * Used primarily for timezone conversion and formatting.
492
   *
493
   * @return bool
494
   *   TRUE if the date contains time parts, FALSE otherwise.
495
   */
496
  public function hasTime() {
497
    return $this->hasGranularity('hour');
498
  }
499

    
500
  /**
501
   * Returns whether the input values included a year.
502
   *
503
   * Useful to use pseudo date objects when we only are interested in the time.
504
   *
505
   * @todo $this->completeDate does not actually exist?
506
   */
507
  public function completeDate() {
508
    return $this->completeDate;
509
  }
510

    
511
  /**
512
   * Removes unwanted date parts from a date.
513
   *
514
   * In common usage we should not unset timezone through this.
515
   *
516
   * @param array $granularity
517
   *   An array of date parts.
518
   */
519
  public function limitGranularity($granularity) {
520
    foreach ($this->granularity as $key => $val) {
521
      if ($val != 'timezone' && !in_array($val, $granularity)) {
522
        unset($this->granularity[$key]);
523
      }
524
    }
525
  }
526

    
527
  /**
528
   * Determines the granularity of a date based on the constructor's arguments.
529
   *
530
   * @param string $time
531
   *   A date string.
532
   * @param bool $tz
533
   *   TRUE if the date has a timezone, FALSE otherwise.
534
   */
535
  protected function setGranularityFromTime($time, $tz) {
536
    $this->granularity = array();
537
    $temp = date_parse($time);
538
    // Special case for 'now'.
539
    if ($time == 'now') {
540
      $this->granularity = array('year', 'month', 'day', 'hour', 'minute', 'second');
541
    }
542
    else {
543
      // This PHP date_parse() method currently doesn't have resolution down to
544
      // seconds, so if there is some time, all will be set.
545
      foreach (self::$allgranularity as $g) {
546
        if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'timezone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
547
          $this->granularity[] = $g;
548
        }
549
      }
550
    }
551
    if ($tz) {
552
      $this->addGranularity('timezone');
553
    }
554
  }
555

    
556
  /**
557
   * Converts a date string into a date object.
558
   *
559
   * @param string $date
560
   *   The date string to parse.
561
   * @param object $tz
562
   *   A timezone object.
563
   * @param string $format
564
   *   The date format string.
565
   *
566
   * @return object
567
   *   Returns the date object.
568
   */
569
  protected function parse($date, $tz, $format) {
570
    $array = date_format_patterns();
571
    foreach ($array as $key => $value) {
572
      // The letter with no preceding '\'.
573
      $patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";
574
      // A single character.
575
      $repl1[] = '${1}(.)';
576
      // The.
577
      $repl2[] = '${1}(' . $value . ')';
578
    }
579
    $patterns[] = "`\\\\\\\\([" . implode(array_keys($array)) . "])`";
580
    $repl1[] = '${1}';
581
    $repl2[] = '${1}';
582

    
583
    $format_regexp = preg_quote($format);
584

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

    
678
    // Blank becomes current time, given TZ.
679
    parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
680
    if ($tz) {
681
      $this->addGranularity('timezone');
682
    }
683

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

    
688
    $this->errors += $this->arrayErrors($final_date);
689
    $granularity = drupal_map_assoc($this->granularity);
690

    
691
    // If the input value is '0000-00-00', PHP's date class will later
692
    // incorrectly convert it to something like '-0001-11-30' if we do setDate()
693
    // here. If we don't do setDate() here, it will default to the current date
694
    // and we will lose any way to tell that there was no date in the orignal
695
    // input values. So set a flag we can use later to tell that this date
696
    // object was created using only time values, and that the date values are
697
    // artifical.
698
    if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) {
699
      $this->timeOnly = TRUE;
700
    }
701
    elseif (empty($this->errors)) {
702
      // setDate() expects a valid year, month, and day.
703
      // Set some defaults for dates that don't use this to
704
      // keep PHP from interpreting it as the last day of
705
      // the previous month or last month of the previous year.
706
      if (empty($granularity['month'])) {
707
        $final_date['month'] = 1;
708
      }
709
      if (empty($granularity['day'])) {
710
        $final_date['day'] = 1;
711
      }
712
      $this->setDate($final_date['year'], $final_date['month'], $final_date['day']);
713
    }
714

    
715
    if (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) {
716
      $this->dateOnly = TRUE;
717
    }
718
    elseif (empty($this->errors)) {
719
      $this->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
720
    }
721
    return $this;
722
  }
723

    
724
  /**
725
   * Returns all standard date parts in an array.
726
   *
727
   * Will return '' for parts in which it lacks granularity.
728
   *
729
   * @param bool $force
730
   *   Whether or not to limit the granularity. Defaults to FALSE.
731
   *
732
   * @return array
733
   *   An array of formatted date part values, keyed by date parts.
734
   */
735
  public function toArray($force = FALSE) {
736
    return array(
737
      'year' => $this->format('Y', $force),
738
      'month' => $this->format('n', $force),
739
      'day' => $this->format('j', $force),
740
      'hour' => intval($this->format('H', $force)),
741
      'minute' => intval($this->format('i', $force)),
742
      'second' => intval($this->format('s', $force)),
743
      'timezone' => $this->format('e', $force),
744
    );
745
  }
746

    
747
  /**
748
   * Creates an ISO date from an array of values.
749
   *
750
   * @param array $arr
751
   *   An array of date values keyed by date part.
752
   * @param bool $full
753
   *   (optional) Whether to force a full date by filling in missing values.
754
   *   Defaults to FALSE.
755
   */
756
  public function toISO($arr, $full = FALSE) {
757
    // Add empty values to avoid errors. The empty values must create a valid
758
    // date or we will get date slippage, i.e. a value of 2011-00-00 will get
759
    // interpreted as November of 2010 by PHP.
760
    if ($full) {
761
      $arr += array('year' => 0, 'month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0);
762
    }
763
    else {
764
      $arr += array('year' => '', 'month' => '', 'day' => '', 'hour' => '', 'minute' => '', 'second' => '');
765
    }
766
    $datetime = '';
767
    if ($arr['year'] !== '') {
768
      $datetime = date_pad(intval($arr['year']), 4);
769
      if ($full || $arr['month'] !== '') {
770
        $datetime .= '-' . date_pad(intval($arr['month']));
771
        if ($full || $arr['day'] !== '') {
772
          $datetime .= '-' . date_pad(intval($arr['day']));
773
        }
774
      }
775
    }
776
    if ($arr['hour'] !== '') {
777
      $datetime .= $datetime ? 'T' : '';
778
      $datetime .= date_pad(intval($arr['hour']));
779
      if ($full || $arr['minute'] !== '') {
780
        $datetime .= ':' . date_pad(intval($arr['minute']));
781
        if ($full || $arr['second'] !== '') {
782
          $datetime .= ':' . date_pad(intval($arr['second']));
783
        }
784
      }
785
    }
786
    return $datetime;
787
  }
788

    
789
  /**
790
   * Forces an incomplete date to be valid.
791
   *
792
   * E.g., add a valid year, month, and day if only the time has been defined.
793
   *
794
   * @param array|string $date
795
   *   An array of date parts or a datetime string with values to be massaged
796
   *   into a valid date object.
797
   * @param string $format
798
   *   (optional) The format of the date. Defaults to NULL.
799
   * @param string $default
800
   *   (optional) If the fallback should use the first value of the date part,
801
   *   or the current value of the date part. Defaults to 'first'.
802
   */
803
  public function setFuzzyDate($date, $format = NULL, $default = 'first') {
804
    $timezone = $this->getTimeZone() ? $this->getTimeZone()->getName() : NULL;
805
    $comp = new DateObject($date, $timezone, $format);
806
    $arr = $comp->toArray(TRUE);
807
    foreach ($arr as $key => $value) {
808
      // Set to intval here and then test that it is still an integer.
809
      // Needed because sometimes valid integers come through as strings.
810
      $arr[$key] = $this->forceValid($key, intval($value), $default, $arr['month'], $arr['year']);
811
    }
812
    $this->setDate($arr['year'], $arr['month'], $arr['day']);
813
    $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
814
  }
815

    
816
  /**
817
   * Converts a date part into something that will produce a valid date.
818
   *
819
   * @param string $part
820
   *   The date part.
821
   * @param int $value
822
   *   The date value for this part.
823
   * @param string $default
824
   *   (optional) If the fallback should use the first value of the date part,
825
   *   or the current value of the date part. Defaults to 'first'.
826
   * @param int $month
827
   *   (optional) Used when the date part is less than 'month' to specify the
828
   *   date. Defaults to NULL.
829
   * @param int $year
830
   *   (optional) Used when the date part is less than 'year' to specify the
831
   *   date. Defaults to NULL.
832
   *
833
   * @return int
834
   *   A valid date value.
835
   */
836
  protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
837
    $now = date_now();
838
    switch ($part) {
839
      case 'year':
840
        $fallback = $now->format('Y');
841
        return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
842
        break;
843
      case 'month':
844
        $fallback = $default == 'first' ? 1 : $now->format('n');
845
        return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
846
        break;
847
      case 'day':
848
        $fallback = $default == 'first' ? 1 : $now->format('j');
849
        $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
850
        return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
851
        break;
852
      case 'hour':
853
        $fallback = $default == 'first' ? 0 : $now->format('G');
854
        return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
855
        break;
856
      case 'minute':
857
        $fallback = $default == 'first' ? 0 : $now->format('i');
858
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
859
        break;
860
      case 'second':
861
        $fallback = $default == 'first' ? 0 : $now->format('s');
862
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
863
        break;
864
    }
865
  }
866

    
867
  /**
868
   * Finds possible errors in an array of date part values.
869
   *
870
   * The forceValid() function will change an invalid value to a valid one, so
871
   * we just need to see if the value got altered.
872
   *
873
   * @param array $arr
874
   *   An array of date values, keyed by date part.
875
   *
876
   * @return array
877
   *   An array of error messages, keyed by date part.
878
   */
879
  public function arrayErrors($arr) {
880
    $errors = array();
881
    $now = date_now();
882
    $default_month = !empty($arr['month']) ? $arr['month'] : $now->format('n');
883
    $default_year = !empty($arr['year']) ? $arr['year'] : $now->format('Y');
884

    
885
    $this->granularity = array();
886
    foreach ($arr as $part => $value) {
887
      // Explicitly set the granularity to the values in the input array.
888
      if (is_numeric($value)) {
889
        $this->addGranularity($part);
890
      }
891
      // Avoid false errors when a numeric value is input as a string by casting
892
      // as an integer.
893
      $value = intval($value);
894
      if (!empty($value) && $this->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
895
        // Use a switch/case to make translation easier by providing a different
896
        // message for each part.
897
        switch ($part) {
898
          case 'year':
899
            $errors['year'] = t('The year is invalid.');
900
            break;
901
          case 'month':
902
            $errors['month'] = t('The month is invalid.');
903
            break;
904
          case 'day':
905
            $errors['day'] = t('The day is invalid.');
906
            break;
907
          case 'hour':
908
            $errors['hour'] = t('The hour is invalid.');
909
            break;
910
          case 'minute':
911
            $errors['minute'] = t('The minute is invalid.');
912
            break;
913
          case 'second':
914
            $errors['second'] = t('The second is invalid.');
915
            break;
916
        }
917
      }
918
    }
919
    if ($this->hasTime()) {
920
      $this->addGranularity('timezone');
921
    }
922
    return $errors;
923
  }
924

    
925
  /**
926
   * Computes difference between two days using a given measure.
927
   *
928
   * @param object $date2_in
929
   *   The stop date.
930
   * @param string $measure
931
   *   (optional) A granularity date part. Defaults to 'seconds'.
932
   * @param boolean $absolute
933
   *   (optional) Indicate whether the absolute value of the difference should
934
   *   be returned or if the sign should be retained. Defaults to TRUE.
935
   */
936
  public function difference($date2_in, $measure = 'seconds', $absolute = TRUE) {
937
    // Create cloned objects or original dates will be impacted by the
938
    // date_modify() operations done in this code.
939
    $date1 = clone($this);
940
    $date2 = clone($date2_in);
941
    if (is_object($date1) && is_object($date2)) {
942
      $diff = date_format($date2, 'U') - date_format($date1, 'U');
943
      if ($diff == 0) {
944
        return 0;
945
      }
946
      elseif ($diff < 0 && $absolute) {
947
        // Make sure $date1 is the smaller date.
948
        $temp = $date2;
949
        $date2 = $date1;
950
        $date1 = $temp;
951
        $diff = date_format($date2, 'U') - date_format($date1, 'U');
952
      }
953
      $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
954
      switch ($measure) {
955
        // The easy cases first.
956
        case 'seconds':
957
          return $diff;
958
        case 'minutes':
959
          return $diff / 60;
960
        case 'hours':
961
          return $diff / 3600;
962
        case 'years':
963
          return $year_diff;
964

    
965
        case 'months':
966
          $format = 'n';
967
          $item1 = date_format($date1, $format);
968
          $item2 = date_format($date2, $format);
969
          if ($year_diff == 0) {
970
            return intval($item2 - $item1);
971
          }
972
          elseif ($year_diff < 0) {
973
            $item_diff = 0 - $item1;
974
            $item_diff -= intval((abs($year_diff) - 1) * 12);
975
            return $item_diff - (12 - $item2);
976
          }
977
          else {
978
            $item_diff = 12 - $item1;
979
            $item_diff += intval(($year_diff - 1) * 12);
980
            return $item_diff + $item2;
981
          }
982
          break;
983

    
984
        case 'days':
985
          $format = 'z';
986
          $item1 = date_format($date1, $format);
987
          $item2 = date_format($date2, $format);
988
          if ($year_diff == 0) {
989
            return intval($item2 - $item1);
990
          }
991
          elseif ($year_diff < 0) {
992
            $item_diff = 0 - $item1;
993
            for ($i = 1; $i < abs($year_diff); $i++) {
994
              date_modify($date1, '-1 year');
995
              $item_diff -= date_days_in_year($date1);
996
            }
997
            return $item_diff - (date_days_in_year($date2) - $item2);
998
          }
999
          else {
1000
            $item_diff = date_days_in_year($date1) - $item1;
1001
            for ($i = 1; $i < $year_diff; $i++) {
1002
              date_modify($date1, '+1 year');
1003
              $item_diff += date_days_in_year($date1);
1004
            }
1005
            return $item_diff + $item2;
1006
          }
1007
          break;
1008

    
1009
        case 'weeks':
1010
          $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
1011
          $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
1012

    
1013
          $sign = ($year_diff < 0) ? -1 : 1;
1014

    
1015
          for ($i = 1; $i <= abs($year_diff); $i++) {
1016
            date_modify($date1, (($sign > 0) ? '+': '-').'1 year');
1017
            $week_diff += (date_iso_weeks_in_year($date1) * $sign);
1018
          }
1019
          return $week_diff;
1020
      }
1021
    }
1022
    return NULL;
1023
  }
1024
}
1025

    
1026
/**
1027
 * Determines if the date element needs to be processed.
1028
 *
1029
 * Helper function to see if date element has been hidden by FAPI to see if it
1030
 * needs to be processed or just pass the value through. This is needed since
1031
 * normal date processing explands the date element into parts and then
1032
 * reconstructs it, which is not needed or desirable if the field is hidden.
1033
 *
1034
 * @param array $element
1035
 *   The date element to check.
1036
 *
1037
 * @return bool
1038
 *   TRUE if the element is effectively hidden, FALSE otherwise.
1039
 */
1040
function date_hidden_element($element) {
1041
  // @TODO What else needs to be tested to see if dates are hidden or disabled?
1042
  if ((isset($element['#access']) && empty($element['#access']))
1043
    || !empty($element['#programmed'])
1044
    || in_array($element['#type'], array('hidden', 'value'))) {
1045
    return TRUE;
1046
  }
1047
  return FALSE;
1048
}
1049

    
1050
/**
1051
 * Helper function for getting the format string for a date type.
1052
 *
1053
 * @param string $type
1054
 *   A date type format name.
1055
 *
1056
 * @return string
1057
 *   A date type format, like 'Y-m-d H:i:s'.
1058
 */
1059
function date_type_format($type) {
1060
  switch ($type) {
1061
    case DATE_ISO:
1062
      return DATE_FORMAT_ISO;
1063
    case DATE_UNIX:
1064
      return DATE_FORMAT_UNIX;
1065
    case DATE_DATETIME:
1066
      return DATE_FORMAT_DATETIME;
1067
    case DATE_ICAL:
1068
      return DATE_FORMAT_ICAL;
1069
  }
1070
}
1071

    
1072
/**
1073
 * Constructs an untranslated array of month names.
1074
 *
1075
 * Needed for CSS, translation functions, strtotime(), and other places
1076
 * that use the English versions of these words.
1077
 *
1078
 * @return array
1079
 *   An array of month names.
1080
 */
1081
function date_month_names_untranslated() {
1082
  static $month_names;
1083
  if (empty($month_names)) {
1084
    $month_names = array(
1085
      1 => 'January',
1086
      2 => 'February',
1087
      3 => 'March',
1088
      4 => 'April',
1089
      5 => 'May',
1090
      6 => 'June',
1091
      7 => 'July',
1092
      8 => 'August',
1093
      9 => 'September',
1094
      10 => 'October',
1095
      11 => 'November',
1096
      12 => 'December',
1097
    );
1098
  }
1099
  return $month_names;
1100
}
1101

    
1102
/**
1103
 * Returns a translated array of month names.
1104
 *
1105
 * @param bool $required
1106
 *   (optional) If FALSE, the returned array will include a blank value.
1107
 *   Defaults to FALSE.
1108
 *
1109
 * @return array
1110
 *   An array of month names.
1111
 */
1112
function date_month_names($required = FALSE) {
1113
  $month_names = array();
1114
  foreach (date_month_names_untranslated() as $key => $month) {
1115
    $month_names[$key] = t($month, array(), array('context' => 'Long month name'));
1116
  }
1117
  $none = array('' => '');
1118
  return !$required ? $none + $month_names : $month_names;
1119
}
1120

    
1121
/**
1122
 * Constructs a translated array of month name abbreviations
1123
 *
1124
 * @param bool $required
1125
 *   (optional) If FALSE, the returned array will include a blank value.
1126
 *   Defaults to FALSE.
1127
 * @param int $length
1128
 *   (optional) The length of the abbreviation. Defaults to 3.
1129
 *
1130
 * @return array
1131
 *   An array of month abbreviations.
1132
 */
1133
function date_month_names_abbr($required = FALSE, $length = 3) {
1134
  $month_names = array();
1135
  foreach (date_month_names_untranslated() as $key => $month) {
1136
    if ($length == 3) {
1137
      $month_names[$key] = t(substr($month, 0, $length), array());
1138
    }
1139
    else {
1140
      $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr'));
1141
    }
1142
  }
1143
  $none = array('' => '');
1144
  return !$required ? $none + $month_names : $month_names;
1145
}
1146

    
1147
/**
1148
 * Constructs an untranslated array of week days.
1149
 *
1150
 * Needed for CSS, translation functions, strtotime(), and other places
1151
 * that use the English versions of these words.
1152
 *
1153
 * @param bool $refresh
1154
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1155
 *
1156
 * @return array
1157
 *   An array of week day names
1158
 */
1159
function date_week_days_untranslated($refresh = TRUE) {
1160
  static $weekdays;
1161
  if ($refresh || empty($weekdays)) {
1162
    $weekdays = array(
1163
      'Sunday',
1164
      'Monday',
1165
      'Tuesday',
1166
      'Wednesday',
1167
      'Thursday',
1168
      'Friday',
1169
      'Saturday',
1170
    );
1171
  }
1172
  return $weekdays;
1173
}
1174

    
1175
/**
1176
 * Returns a translated array of week names.
1177
 *
1178
 * @param bool $required
1179
 *   (optional) If FALSE, the returned array will include a blank value.
1180
 *   Defaults to FALSE.
1181
 *
1182
 * @return array
1183
 *   An array of week day names
1184
 */
1185
function date_week_days($required = FALSE, $refresh = TRUE) {
1186
  $weekdays = array();
1187
  foreach (date_week_days_untranslated() as $key => $day) {
1188
    $weekdays[$key] = t($day, array(), array('context' => ''));
1189
  }
1190
  $none = array('' => '');
1191
  return !$required ? $none + $weekdays : $weekdays;
1192
}
1193

    
1194
/**
1195
 * Constructs a translated array of week day abbreviations.
1196
 *
1197
 * @param bool $required
1198
 *   (optional) If FALSE, the returned array will include a blank value.
1199
 *   Defaults to FALSE.
1200
 * @param bool $refresh
1201
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1202
 * @param int $length
1203
 *   (optional) The length of the abbreviation. Defaults to 3.
1204
 *
1205
 * @return array
1206
 *   An array of week day abbreviations
1207
 */
1208
function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
1209
  $weekdays = array();
1210
  switch ($length) {
1211
    case 1:
1212
      $context = 'day_abbr1';
1213
      break;
1214
    case 2:
1215
      $context = 'day_abbr2';
1216
      break;
1217
    default:
1218
      $context = '';
1219
      break;
1220
  }
1221
  foreach (date_week_days_untranslated() as $key => $day) {
1222
    $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context));
1223
  }
1224
  $none = array('' => '');
1225
  return !$required ? $none + $weekdays : $weekdays;
1226
}
1227

    
1228
/**
1229
 * Reorders weekdays to match the first day of the week.
1230
 *
1231
 * @param array $weekdays
1232
 *   An array of weekdays.
1233
 *
1234
 * @return array
1235
 *   An array of weekdays reordered to match the first day of the week.
1236
 */
1237
function date_week_days_ordered($weekdays) {
1238
  $first_day = variable_get('date_first_day', 0);
1239
  if ($first_day > 0) {
1240
    for ($i = 1; $i <= $first_day; $i++) {
1241
      $last = array_shift($weekdays);
1242
      array_push($weekdays, $last);
1243
    }
1244
  }
1245
  return $weekdays;
1246
}
1247

    
1248
/**
1249
 * Constructs an array of years.
1250
 *
1251
 * @param int $min
1252
 *   The minimum year in the array.
1253
 * @param int $max
1254
 *   The maximum year in the array.
1255
 * @param bool $required
1256
 *   (optional) If FALSE, the returned array will include a blank value.
1257
 *   Defaults to FALSE.
1258
 *
1259
 * @return array
1260
 *   An array of years in the selected range.
1261
 */
1262
function date_years($min = 0, $max = 0, $required = FALSE) {
1263
  // Ensure $min and $max are valid values.
1264
  if (empty($min)) {
1265
    $min = intval(date('Y', REQUEST_TIME) - 3);
1266
  }
1267
  if (empty($max)) {
1268
    $max = intval(date('Y', REQUEST_TIME) + 3);
1269
  }
1270
  $none = array(0 => '');
1271
  return !$required ? $none + drupal_map_assoc(range($min, $max)) : drupal_map_assoc(range($min, $max));
1272
}
1273

    
1274
/**
1275
 * Constructs an array of days in a month.
1276
 *
1277
 * @param bool $required
1278
 *   (optional) If FALSE, the returned array will include a blank value.
1279
 *   Defaults to FALSE.
1280
 * @param int $month
1281
 *   (optional) The month in which to find the number of days.
1282
 * @param int $year
1283
 *   (optional) The year in which to find the number of days.
1284
 *
1285
 * @return array
1286
 *   An array of days for the selected month.
1287
 */
1288
function date_days($required = FALSE, $month = NULL, $year = NULL) {
1289
  // If we have a month and year, find the right last day of the month.
1290
  if (!empty($month) && !empty($year)) {
1291
    $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC');
1292
    $max = $date->format('t');
1293
  }
1294
  // If there is no month and year given, default to 31.
1295
  if (empty($max)) {
1296
    $max = 31;
1297
  }
1298
  $none = array(0 => '');
1299
  return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
1300
}
1301

    
1302
/**
1303
 * Constructs an array of hours.
1304
 *
1305
 * @param string $format
1306
 *   A date format string.
1307
 * @param bool $required
1308
 *   (optional) If FALSE, the returned array will include a blank value.
1309
 *   Defaults to FALSE.
1310
 *
1311
 * @return array
1312
 *   An array of hours in the selected format.
1313
 */
1314
function date_hours($format = 'H', $required = FALSE) {
1315
  $hours = array();
1316
  if ($format == 'h' || $format == 'g') {
1317
    $min = 1;
1318
    $max = 12;
1319
  }
1320
  else {
1321
    $min = 0;
1322
    $max = 23;
1323
  }
1324
  for ($i = $min; $i <= $max; $i++) {
1325
    $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
1326
  }
1327
  $none = array('' => '');
1328
  return !$required ? $none + $hours : $hours;
1329
}
1330

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

    
1356
/**
1357
 * Constructs an array of seconds.
1358
 *
1359
 * @param string $format
1360
 *   A date format string.
1361
 * @param bool $required
1362
 *   (optional) If FALSE, the returned array will include a blank value.
1363
 *   Defaults to FALSE.
1364
 *
1365
 * @return array
1366
 *   An array of seconds in the selected format.
1367
 */
1368
function date_seconds($format = 's', $required = FALSE, $increment = 1) {
1369
  $seconds = array();
1370
  // Ensure $increment has a value so we don't loop endlessly.
1371
  if (empty($increment)) {
1372
    $increment = 1;
1373
  }
1374
  for ($i = 0; $i < 60; $i += $increment) {
1375
    $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
1376
  }
1377
  $none = array('' => '');
1378
  return !$required ? $none + $seconds : $seconds;
1379
}
1380

    
1381
/**
1382
 * Constructs an array of AM and PM options.
1383
 *
1384
 * @param bool $required
1385
 *   (optional) If FALSE, the returned array will include a blank value.
1386
 *   Defaults to FALSE.
1387
 *
1388
 * @return array
1389
 *   An array of AM and PM options.
1390
 */
1391
function date_ampm($required = FALSE) {
1392
  $none = array('' => '');
1393
  $ampm = array(
1394
    'am' => t('am', array(), array('context' => 'ampm')),
1395
    'pm' => t('pm', array(), array('context' => 'ampm')),
1396
  );
1397
  return !$required ? $none + $ampm : $ampm;
1398
}
1399

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

    
1453
/**
1454
 * Constructs an array of granularity options and their labels.
1455
 *
1456
 * @return array
1457
 *   An array of translated date parts, keyed by their machine name.
1458
 */
1459
function date_granularity_names() {
1460
  return array(
1461
    'year' => t('Year', array(), array('context' => 'datetime')),
1462
    'month' => t('Month', array(), array('context' => 'datetime')),
1463
    'day' => t('Day', array(), array('context' => 'datetime')),
1464
    'hour' => t('Hour', array(), array('context' => 'datetime')),
1465
    'minute' => t('Minute', array(), array('context' => 'datetime')),
1466
    'second' => t('Second', array(), array('context' => 'datetime')),
1467
  );
1468
}
1469

    
1470
/**
1471
 * Sorts a granularity array.
1472
 *
1473
 * @param array $granularity
1474
 *   An array of date parts.
1475
 */
1476
function date_granularity_sorted($granularity) {
1477
  return array_intersect(array('year', 'month', 'day', 'hour', 'minute', 'second'), $granularity);
1478
}
1479

    
1480
/**
1481
 * Constructs an array of granularity based on a given precision.
1482
 *
1483
 * @param string $precision
1484
 *   A granularity item.
1485
 *
1486
 * @return array
1487
 *   A granularity array containing the given precision and all those above it.
1488
 *   For example, passing in 'month' will return array('year', 'month').
1489
 */
1490
function date_granularity_array_from_precision($precision) {
1491
  $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second');
1492
  switch ($precision) {
1493
    case 'year':
1494
      return array_slice($granularity_array, -6, 1);
1495
    case 'month':
1496
      return array_slice($granularity_array, -6, 2);
1497
    case 'day':
1498
      return array_slice($granularity_array, -6, 3);
1499
    case 'hour':
1500
      return array_slice($granularity_array, -6, 4);
1501
    case 'minute':
1502
      return array_slice($granularity_array, -6, 5);
1503
    default:
1504
      return $granularity_array;
1505
  }
1506
}
1507

    
1508
/**
1509
 * Give a granularity array, return the highest precision.
1510
 *
1511
 * @param array $granularity_array
1512
 *   An array of date parts.
1513
 *
1514
 * @return string
1515
 *   The most precise element in a granularity array.
1516
 */
1517
function date_granularity_precision($granularity_array) {
1518
  $input = date_granularity_sorted($granularity_array);
1519
  return array_pop($input);
1520
}
1521

    
1522
/**
1523
 * Constructs a valid DATETIME format string for the granularity of an item.
1524
 *
1525
 * @todo This function is no longer used as of
1526
 * http://drupalcode.org/project/date.git/commit/07efbb5.
1527
 */
1528
function date_granularity_format($granularity) {
1529
  if (is_array($granularity)) {
1530
    $granularity = date_granularity_precision($granularity);
1531
  }
1532
  $format = 'Y-m-d H:i:s';
1533
  switch ($granularity) {
1534
    case 'year':
1535
      return substr($format, 0, 1);
1536
    case 'month':
1537
      return substr($format, 0, 3);
1538
    case 'day':
1539
      return substr($format, 0, 5);
1540
    case 'hour';
1541
      return substr($format, 0, 7);
1542
    case 'minute':
1543
      return substr($format, 0, 9);
1544
    default:
1545
      return $format;
1546
  }
1547
}
1548

    
1549
/**
1550
 * Returns a translated array of timezone names.
1551
 *
1552
 * Cache the untranslated array, make the translated array a static variable.
1553
 *
1554
 * @param bool $required
1555
 *   (optional) If FALSE, the returned array will include a blank value.
1556
 *   Defaults to FALSE.
1557
 * @param bool $refresh
1558
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1559
 *
1560
 * @return array
1561
 *   An array of timezone names.
1562
 */
1563
function date_timezone_names($required = FALSE, $refresh = FALSE) {
1564
  static $zonenames;
1565
  if (empty($zonenames) || $refresh) {
1566
    $cached = cache_get('date_timezone_identifiers_list');
1567
    $zonenames = !empty($cached) ? $cached->data : array();
1568
    if ($refresh || empty($cached) || empty($zonenames)) {
1569
      $data = timezone_identifiers_list();
1570
      asort($data);
1571
      foreach ($data as $delta => $zone) {
1572
        // Because many timezones exist in PHP only for backward compatibility
1573
        // reasons and should not be used, the list is filtered by a regular
1574
        // expression.
1575
        if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
1576
          $zonenames[$zone] = $zone;
1577
        }
1578
      }
1579

    
1580
      if (!empty($zonenames)) {
1581
        cache_set('date_timezone_identifiers_list', $zonenames);
1582
      }
1583
    }
1584
    foreach ($zonenames as $zone) {
1585
      $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone)));
1586
    }
1587
  }
1588
  $none = array('' => '');
1589
  return !$required ? $none + $zonenames : $zonenames;
1590
}
1591

    
1592
/**
1593
 * Returns an array of system-allowed timezone abbreviations.
1594
 *
1595
 * Cache an array of just the abbreviation names because the whole
1596
 * timezone_abbreviations_list() is huge, so we don't want to retrieve it more
1597
 * than necessary.
1598
 *
1599
 * @param bool $refresh
1600
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1601
 *
1602
 * @return array
1603
 *   An array of allowed timezone abbreviations.
1604
 */
1605
function date_timezone_abbr($refresh = FALSE) {
1606
  $cached = cache_get('date_timezone_abbreviations');
1607
  $data = isset($cached->data) ? $cached->data : array();
1608
  if (empty($data) || $refresh) {
1609
    $data = array_keys(timezone_abbreviations_list());
1610
    cache_set('date_timezone_abbreviations', $data);
1611
  }
1612
  return $data;
1613
}
1614

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

    
1706
/**
1707
 * Formats a time interval with granularity, including past and future context.
1708
 *
1709
 * @param object $date
1710
 *   The current date object.
1711
 * @param int $granularity
1712
 *   (optional) Number of units to display in the string. Defaults to 2.
1713
 *
1714
 * @return string
1715
 *   A translated string representation of the interval.
1716
 *
1717
 * @see format_interval()
1718
 */
1719
function date_format_interval($date, $granularity = 2, $display_ago = TRUE) {
1720
  // If no date is sent, then return nothing.
1721
  if (empty($date)) {
1722
    return NULL;
1723
  }
1724

    
1725
  $interval = REQUEST_TIME - $date->format('U');
1726
  if ($interval > 0) {
1727
    return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) :
1728
      t('!time', array('!time' => format_interval($interval, $granularity)));
1729
  }
1730
  else {
1731
    return format_interval(abs($interval), $granularity);
1732
  }
1733
}
1734

    
1735
/**
1736
 * A date object for the current time.
1737
 *
1738
 * @param object $timezone
1739
 *   (optional) Optionally force time to a specific timezone, defaults to user
1740
 *   timezone, if set, otherwise site timezone. Defaults to NULL.
1741
 *
1742
 * @param boolean $reset [optional]
1743
 *  Static cache reset
1744
 *
1745
 * @return object
1746
 *   The current time as a date object.
1747
 */
1748
function date_now($timezone = NULL, $reset = FALSE) {
1749
  if ($reset) {
1750
    drupal_static_reset(__FUNCTION__ . $timezone);
1751
  }
1752

    
1753
  $now = &drupal_static(__FUNCTION__ . $timezone);
1754

    
1755
  if (!isset($now)) {
1756
    $now = new DateObject('now', $timezone);
1757
  }
1758

    
1759
  // Avoid unexpected manipulation of cached $now object
1760
  // by subsequent code execution
1761
  // @see https://drupal.org/node/2261395
1762
  $clone = clone $now;
1763
  return $clone;
1764
}
1765

    
1766
/**
1767
 * Determines if a timezone string is valid.
1768
 *
1769
 * @param string $timezone
1770
 *   A potentially invalid timezone string.
1771
 *
1772
 * @return bool
1773
 *   TRUE if the timezone is valid, FALSE otherwise.
1774
 */
1775
function date_timezone_is_valid($timezone) {
1776
  static $timezone_names;
1777
  if (empty($timezone_names)) {
1778
    $timezone_names = array_keys(date_timezone_names(TRUE));
1779
  }
1780
  return in_array($timezone, $timezone_names);
1781
}
1782

    
1783
/**
1784
 * Returns a timezone name to use as a default.
1785
 *
1786
 * @param bool $check_user
1787
 *   (optional) Whether or not to check for a user-configured timezone.
1788
 *   Defaults to TRUE.
1789
 *
1790
 * @return string
1791
 *   The default timezone for a user, if available, otherwise the site.
1792
 */
1793
function date_default_timezone($check_user = TRUE) {
1794
  global $user;
1795
  if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) {
1796
    return $user->timezone;
1797
  }
1798
  else {
1799
    $default = variable_get('date_default_timezone', '');
1800
    return empty($default) ? 'UTC' : $default;
1801
  }
1802
}
1803

    
1804
/**
1805
 * Returns a timezone object for the default timezone.
1806
 *
1807
 * @param bool $check_user
1808
 *   (optional) Whether or not to check for a user-configured timezone.
1809
 *   Defaults to TRUE.
1810
 *
1811
 * @return object
1812
 *   The default timezone for a user, if available, otherwise the site.
1813
 */
1814
function date_default_timezone_object($check_user = TRUE) {
1815
  return timezone_open(date_default_timezone($check_user));
1816
}
1817

    
1818
/**
1819
 * Identifies the number of days in a month for a date.
1820
 */
1821
function date_days_in_month($year, $month) {
1822
  // Pick a day in the middle of the month to avoid timezone shifts.
1823
  $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00';
1824
  $date = new DateObject($datetime);
1825
  return $date->format('t');
1826
}
1827

    
1828
/**
1829
 * Identifies the number of days in a year for a date.
1830
 *
1831
 * @param mixed $date
1832
 *   (optional) The current date object, or a date string. Defaults to NULL.
1833
 *
1834
 * @return integer
1835
 *   The number of days in the year.
1836
 */
1837
function date_days_in_year($date = NULL) {
1838
  if (empty($date)) {
1839
    $date = date_now();
1840
  }
1841
  elseif (!is_object($date)) {
1842
    $date = new DateObject($date);
1843
  }
1844
  if (is_object($date)) {
1845
    if ($date->format('L')) {
1846
      return 366;
1847
    }
1848
    else {
1849
      return 365;
1850
    }
1851
  }
1852
  return NULL;
1853
}
1854

    
1855
/**
1856
 * Identifies the number of ISO weeks in a year for a date.
1857
 *
1858
 * December 28 is always in the last ISO week of the year.
1859
 *
1860
 * @param mixed $date
1861
 *   (optional) The current date object, or a date string. Defaults to NULL.
1862
 *
1863
 * @return integer
1864
 *   The number of ISO weeks in a year.
1865
 */
1866
function date_iso_weeks_in_year($date = NULL) {
1867
  if (empty($date)) {
1868
    $date = date_now();
1869
  }
1870
  elseif (!is_object($date)) {
1871
    $date = new DateObject($date);
1872
  }
1873

    
1874
  if (is_object($date)) {
1875
    date_date_set($date, $date->format('Y'), 12, 28);
1876
    return $date->format('W');
1877
  }
1878
  return NULL;
1879
}
1880

    
1881
/**
1882
 * Returns day of week for a given date (0 = Sunday).
1883
 *
1884
 * @param mixed $date
1885
 *   (optional) A date, default is current local day. Defaults to NULL.
1886
 *
1887
 * @return int
1888
 *   The number of the day in the week.
1889
 */
1890
function date_day_of_week($date = NULL) {
1891
  if (empty($date)) {
1892
    $date = date_now();
1893
  }
1894
  elseif (!is_object($date)) {
1895
    $date = new DateObject($date);
1896
  }
1897

    
1898
  if (is_object($date)) {
1899
    return $date->format('w');
1900
  }
1901
  return NULL;
1902
}
1903

    
1904
/**
1905
 * Returns translated name of the day of week for a given date.
1906
 *
1907
 * @param mixed $date
1908
 *   (optional) A date, default is current local day. Defaults to NULL.
1909
 * @param string $abbr
1910
 *   (optional) Whether to return the abbreviated name for that day.
1911
 *   Defaults to TRUE.
1912
 *
1913
 * @return string
1914
 *   The name of the day in the week for that date.
1915
 */
1916
function date_day_of_week_name($date = NULL, $abbr = TRUE) {
1917
  if (!is_object($date)) {
1918
    $date = new DateObject($date);
1919
  }
1920
  $dow = date_day_of_week($date);
1921
  $days = $abbr ? date_week_days_abbr() : date_week_days();
1922
  return $days[$dow];
1923
}
1924

    
1925
/**
1926
 * Calculates the start and end dates for a calendar week.
1927
 *
1928
 * The dates are adjusted to use the chosen first day of week for this site.
1929
 *
1930
 * @param int $week
1931
 *   The week value.
1932
 * @param int $year
1933
 *   The year value.
1934
 *
1935
 * @return array
1936
 *   A numeric array containing the start and end dates of a week.
1937
 */
1938
function date_week_range($week, $year) {
1939
  if (variable_get('date_api_use_iso8601', FALSE)) {
1940
    return date_iso_week_range($week, $year);
1941
  }
1942
  $min_date = new DateObject($year . '-01-01 00:00:00');
1943
  $min_date->setTimezone(date_default_timezone_object());
1944

    
1945
  // Move to the right week.
1946
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
1947

    
1948
  // Move backwards to the first day of the week.
1949
  $first_day = variable_get('date_first_day', 0);
1950
  $day_wday = date_format($min_date, 'w');
1951
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
1952

    
1953
  // Move forwards to the last day of the week.
1954
  $max_date = clone($min_date);
1955
  date_modify($max_date, '+7 days');
1956

    
1957
  if (date_format($min_date, 'Y') != $year) {
1958
    $min_date = new DateObject($year . '-01-01 00:00:00');
1959
  }
1960
  return array($min_date, $max_date);
1961
}
1962

    
1963
/**
1964
 * Calculates the start and end dates for an ISO week.
1965
 *
1966
 * @param int $week
1967
 *   The week value.
1968
 * @param int $year
1969
 *   The year value.
1970
 *
1971
 * @return array
1972
 *   A numeric array containing the start and end dates of an ISO week.
1973
 */
1974
function date_iso_week_range($week, $year) {
1975
  // Get to the last ISO week of the previous year.
1976
  $min_date = new DateObject(($year - 1) . '-12-28 00:00:00');
1977
  date_timezone_set($min_date, date_default_timezone_object());
1978

    
1979
  // Find the first day of the first ISO week in the year.
1980
  date_modify($min_date, '+1 Monday');
1981

    
1982
  // Jump ahead to the desired week for the beginning of the week range.
1983
  if ($week > 1) {
1984
    date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
1985
  }
1986

    
1987
  // Move forwards to the last day of the week.
1988
  $max_date = clone($min_date);
1989
  date_modify($max_date, '+7 days');
1990
  return array($min_date, $max_date);
1991
}
1992

    
1993
/**
1994
 * The number of calendar weeks in a year.
1995
 *
1996
 * PHP week functions return the ISO week, not the calendar week.
1997
 *
1998
 * @param int $year
1999
 *   A year value.
2000
 *
2001
 * @return int
2002
 *   Number of calendar weeks in selected year.
2003
 */
2004
function date_weeks_in_year($year) {
2005
  $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC');
2006
  date_modify($date, '-1 day');
2007
  return date_week($date->format('Y-m-d'));
2008
}
2009

    
2010
/**
2011
 * The calendar week number for a date.
2012
 *
2013
 * PHP week functions return the ISO week, not the calendar week.
2014
 *
2015
 * @param string $date
2016
 *   A date string in the format Y-m-d.
2017
 *
2018
 * @return int
2019
 *   The calendar week number.
2020
 */
2021
function date_week($date) {
2022
  $date = substr($date, 0, 10);
2023
  $parts = explode('-', $date);
2024

    
2025
  $date = new DateObject($date . ' 12:00:00', 'UTC');
2026

    
2027
  // If we are using ISO weeks, this is easy.
2028
  if (variable_get('date_api_use_iso8601', FALSE)) {
2029
    return intval($date->format('W'));
2030
  }
2031

    
2032
  $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
2033
  $week = intval($date->format('W'));
2034
  $year_week = intval(date_format($year_date, 'W'));
2035
  $date_year = intval($date->format('o'));
2036

    
2037
  // Remove the leap week if it's present.
2038
  if ($date_year > intval($parts[0])) {
2039
    $last_date = clone($date);
2040
    date_modify($last_date, '-7 days');
2041
    $week = date_format($last_date, 'W') + 1;
2042
  }
2043
  elseif ($date_year < intval($parts[0])) {
2044
    $week = 0;
2045
  }
2046

    
2047
  if ($year_week != 1) {
2048
    $week++;
2049
  }
2050

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

    
2054
  // If it's before the starting day, it's the previous week.
2055
  if (intval($date->format('N')) < $iso_first_day) {
2056
    $week--;
2057
  }
2058

    
2059
  // If the year starts before, it's an extra week at the beginning.
2060
  if (intval(date_format($year_date, 'N')) < $iso_first_day) {
2061
    $week++;
2062
  }
2063

    
2064
  return $week;
2065
}
2066

    
2067
/**
2068
 * Helper function to left pad date parts with zeros.
2069
 *
2070
 * Provided because this is needed so often with dates.
2071
 *
2072
 * @param int $value
2073
 *   The value to pad.
2074
 * @param int $size
2075
 *   (optional) Total size expected, usually 2 or 4. Defaults to 2.
2076
 *
2077
 * @return string
2078
 *   The padded value.
2079
 */
2080
function date_pad($value, $size = 2) {
2081
  return sprintf("%0" . $size . "d", $value);
2082
}
2083

    
2084
/**
2085
 * Determines if the granularity contains a time portion.
2086
 *
2087
 * @param array $granularity
2088
 *   An array of allowed date parts, all others will be removed.
2089
 *
2090
 * @return bool
2091
 *   TRUE if the granularity contains a time portion, FALSE otherwise.
2092
 */
2093
function date_has_time($granularity) {
2094
  if (!is_array($granularity)) {
2095
    $granularity = array();
2096
  }
2097
  return (bool) count(array_intersect($granularity, array('hour', 'minute', 'second')));
2098
}
2099

    
2100
/**
2101
 * Determines if the granularity contains a date portion.
2102
 *
2103
 * @param array $granularity
2104
 *   An array of allowed date parts, all others will be removed.
2105
 *
2106
 * @return bool
2107
 *   TRUE if the granularity contains a date portion, FALSE otherwise.
2108
 */
2109
function date_has_date($granularity) {
2110
  if (!is_array($granularity)) {
2111
    $granularity = array();
2112
  }
2113
  return (bool) count(array_intersect($granularity, array('year', 'month', 'day')));
2114
}
2115

    
2116
/**
2117
 * Helper function to get a format for a specific part of a date field.
2118
 *
2119
 * @param string $part
2120
 *   The date field part, either 'time' or 'date'.
2121
 * @param string $format
2122
 *   A date format string.
2123
 *
2124
 * @return string
2125
 *   The date format for the given part.
2126
 */
2127
function date_part_format($part, $format) {
2128
  switch ($part) {
2129
    case 'date':
2130
      return date_limit_format($format, array('year', 'month', 'day'));
2131
    case 'time':
2132
      return date_limit_format($format, array('hour', 'minute', 'second'));
2133
    default:
2134
      return date_limit_format($format, array($part));
2135
  }
2136
}
2137

    
2138
/**
2139
 * Limits a date format to include only elements from a given granularity array.
2140
 *
2141
 * Example:
2142
 *   date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
2143
 *   returns 'F j, Y'
2144
 *
2145
 * @param string $format
2146
 *   A date format string.
2147
 * @param array $granularity
2148
 *   An array of allowed date parts, all others will be removed.
2149
 *
2150
 * @return string
2151
 *   The format string with all other elements removed.
2152
 */
2153
function date_limit_format($format, $granularity) {
2154
  // Use the advanced drupal_static() pattern to improve performance.
2155
  static $drupal_static_fast;
2156
  if (!isset($drupal_static_fast)) {
2157
    $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__);
2158
  }
2159
  $formats = &$drupal_static_fast['formats'];
2160
  $format_granularity_cid = $format .'|'. implode(',', $granularity);
2161
  if (isset($formats[$format_granularity_cid])) {
2162
    return $formats[$format_granularity_cid];
2163
  }
2164

    
2165
  // If punctuation has been escaped, remove the escaping. Done using strtr()
2166
  // because it is easier than getting the escape character extracted using
2167
  // preg_replace().
2168
  $replace = array(
2169
    '\-' => '-',
2170
    '\:' => ':',
2171
    "\'" => "'",
2172
    '\. ' => ' . ',
2173
    '\,' => ',',
2174
  );
2175
  $format = strtr($format, $replace);
2176

    
2177
  // Get the 'T' out of ISO date formats that don't have both date and time.
2178
  if (!date_has_time($granularity) || !date_has_date($granularity)) {
2179
    $format = str_replace('\T', ' ', $format);
2180
    $format = str_replace('T', ' ', $format);
2181
  }
2182

    
2183
  $regex = array();
2184
  if (!date_has_time($granularity)) {
2185
    $regex[] = '((?<!\\\\)[a|A])';
2186
  }
2187
  // Create regular expressions to remove selected values from string.
2188
  // Use (?<!\\\\) to keep escaped letters from being removed.
2189
  foreach (date_nongranularity($granularity) as $element) {
2190
    switch ($element) {
2191
      case 'year':
2192
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
2193
        break;
2194
      case 'day':
2195
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[l|D|d|dS|j|jS|N|w|W|z]{1,2})';
2196
        break;
2197
      case 'month':
2198
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
2199
        break;
2200
      case 'hour':
2201
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
2202
        break;
2203
      case 'minute':
2204
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
2205
        break;
2206
      case 'second':
2207
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
2208
        break;
2209
      case 'timezone':
2210
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
2211
        break;
2212

    
2213
    }
2214
  }
2215
  // Remove empty parentheses, brackets, pipes.
2216
  $regex[] = '(\(\))';
2217
  $regex[] = '(\[\])';
2218
  $regex[] = '(\|\|)';
2219

    
2220
  // Remove selected values from string.
2221
  $format = trim(preg_replace($regex, array(), $format));
2222
  // Remove orphaned punctuation at the beginning of the string.
2223
  $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
2224
  // Remove orphaned punctuation at the end of the string.
2225
  $format = preg_replace('([\-/,:\']$)', '', $format);
2226
  $format = preg_replace('(\\$)', '', $format);
2227

    
2228
  // Trim any whitespace from the result.
2229
  $format = trim($format);
2230

    
2231
  // After removing the non-desired parts of the format, test if the only things
2232
  // left are escaped, non-date, characters. If so, return nothing.
2233
  // Using S instead of w to pick up non-ASCII characters.
2234
  $test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
2235
  if (empty($test)) {
2236
    $format = '';
2237
  }
2238

    
2239
  // Store the return value in the static array for performance.
2240
  $formats[$format_granularity_cid] = $format;
2241

    
2242
  return $format;
2243
}
2244

    
2245
/**
2246
 * Converts a format to an ordered array of granularity parts.
2247
 *
2248
 * Example:
2249
 *   date_format_order('m/d/Y H:i')
2250
 *   returns
2251
 *     array(
2252
 *       0 => 'month',
2253
 *       1 => 'day',
2254
 *       2 => 'year',
2255
 *       3 => 'hour',
2256
 *       4 => 'minute',
2257
 *     );
2258
 *
2259
 * @param string $format
2260
 *   A date format string.
2261
 *
2262
 * @return array
2263
 *   An array of ordered granularity elements from the given format string.
2264
 */
2265
function date_format_order($format) {
2266
  $order = array();
2267
  if (empty($format)) {
2268
    return $order;
2269
  }
2270

    
2271
  $max = strlen($format);
2272
  for ($i = 0; $i <= $max; $i++) {
2273
    if (!isset($format[$i])) {
2274
      break;
2275
    }
2276
    switch ($format[$i]) {
2277
      case 'd':
2278
      case 'j':
2279
        $order[] = 'day';
2280
        break;
2281
      case 'F':
2282
      case 'M':
2283
      case 'm':
2284
      case 'n':
2285
        $order[] = 'month';
2286
        break;
2287
      case 'Y':
2288
      case 'y':
2289
        $order[] = 'year';
2290
        break;
2291
      case 'g':
2292
      case 'G':
2293
      case 'h':
2294
      case 'H':
2295
        $order[] = 'hour';
2296
        break;
2297
      case 'i':
2298
        $order[] = 'minute';
2299
        break;
2300
      case 's':
2301
        $order[] = 'second';
2302
        break;
2303
    }
2304
  }
2305
  return $order;
2306
}
2307

    
2308
/**
2309
 * Strips out unwanted granularity elements.
2310
 *
2311
 * @param array $granularity
2312
 *   An array like ('year', 'month', 'day', 'hour', 'minute', 'second');
2313
 *
2314
 * @return array
2315
 *   A reduced set of granularitiy elements.
2316
 */
2317
function date_nongranularity($granularity) {
2318
  return array_diff(array('year', 'month', 'day', 'hour', 'minute', 'second', 'timezone'), (array) $granularity);
2319
}
2320

    
2321
/**
2322
 * Implements hook_element_info().
2323
 */
2324
function date_api_element_info() {
2325
  module_load_include('inc', 'date_api', 'date_api_elements');
2326
  return _date_api_element_info();
2327
}
2328

    
2329
/**
2330
 * Implements hook_theme().
2331
 */
2332
function date_api_theme($existing, $type, $theme, $path) {
2333
  $base = array(
2334
    'file' => 'theme.inc',
2335
    'path' => "$path/theme",
2336
  );
2337
  return array(
2338
    'date_nav_title' => $base + array('variables' => array('granularity' => NULL, 'view' => NULL, 'link' => NULL, 'format' => NULL)),
2339
    'date_timezone' => $base + array('render element' => 'element'),
2340
    'date_select' => $base + array('render element' => 'element'),
2341
    'date_text' => $base + array('render element' => 'element'),
2342
    'date_select_element' => $base + array('render element' => 'element'),
2343
    'date_textfield_element' => $base + array('render element' => 'element'),
2344
    'date_part_hour_prefix' => $base + array('render element' => 'element'),
2345
    'date_part_minsec_prefix' => $base + array('render element' => 'element'),
2346
    'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2347
    'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2348
    'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2349
    'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2350
    'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2351
    'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2352
    'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2353
    'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2354
    'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2355
    'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2356
    'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'),
2357
    'date_calendar_day' => $base + array('variables' => array('date' => NULL)),
2358
    'date_time_ago' => $base + array('variables' => array('start_date' => NULL, 'end_date' => NULL, 'interval' => NULL)),
2359
  );
2360
}
2361

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

    
2387
/**
2388
 * Function to figure out which db timezone applies to a date.
2389
 *
2390
 * @param string $handling
2391
 *   The timezone handling.
2392
 * @param string $timezone
2393
 *   (optional) When $handling is 'date', date_get_timezone_db() returns this
2394
 *   value.
2395
 *
2396
 * @return string
2397
 *   The timezone string.
2398
 */
2399
function date_get_timezone_db($handling, $timezone = NULL) {
2400
  switch ($handling) {
2401
    case ('utc'):
2402
    case ('site'):
2403
    case ('user'):
2404
      // These handling modes all convert to UTC before storing in the DB.
2405
      $timezone = 'UTC';
2406
      break;
2407
    case ('date'):
2408
      if ($timezone == NULL) {
2409
        // This shouldn't happen, since it's meaning is undefined. But we need
2410
        // to fall back to *something* that's a legal timezone.
2411
        $timezone = date_default_timezone();
2412
      }
2413
      break;
2414
    case ('none'):
2415
    default:
2416
      $timezone = date_default_timezone();
2417
      break;
2418
  }
2419
  return $timezone;
2420
}
2421

    
2422
/**
2423
 * Helper function for converting back and forth from '+1' to 'First'.
2424
 */
2425
function date_order_translated() {
2426
  return array(
2427
    '+1' => t('First', array(), array('context' => 'date_order')),
2428
    '+2' => t('Second', array(), array('context' => 'date_order')),
2429
    '+3' => t('Third', array(), array('context' => 'date_order')),
2430
    '+4' => t('Fourth', array(), array('context' => 'date_order')),
2431
    '+5' => t('Fifth', array(), array('context' => 'date_order')),
2432
    '-1' => t('Last', array(), array('context' => 'date_order_reverse')),
2433
    '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')),
2434
    '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')),
2435
    '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')),
2436
    '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')),
2437
  );
2438
}
2439

    
2440
/**
2441
 * Creates an array of ordered strings, using English text when possible.
2442
 */
2443
function date_order() {
2444
  return array(
2445
    '+1' => 'First',
2446
    '+2' => 'Second',
2447
    '+3' => 'Third',
2448
    '+4' => 'Fourth',
2449
    '+5' => 'Fifth',
2450
    '-1' => 'Last',
2451
    '-2' => '-2',
2452
    '-3' => '-3',
2453
    '-4' => '-4',
2454
    '-5' => '-5',
2455
  );
2456
}
2457

    
2458
/**
2459
 * Tests validity of a date range string.
2460
 *
2461
 * @param string $string
2462
 *   A min and max year string like '-3:+1'a.
2463
 *
2464
 * @return bool
2465
 *   TRUE if the date range is valid, FALSE otherwise.
2466
 */
2467
function date_range_valid($string) {
2468
  $matches = preg_match('@^(\-[0-9]+|[0-9]{4}):([\+|\-][0-9]+|[0-9]{4})$@', $string);
2469
  return $matches < 1 ? FALSE : TRUE;
2470
}
2471

    
2472
/**
2473
 * Splits a string like -3:+3 or 2001:2010 into an array of min and max years.
2474
 *
2475
 * Center the range around the current year, if any, but expand it far
2476
 * enough so it will pick up the year value in the field in case
2477
 * the value in the field is outside the initial range.
2478
 *
2479
 * @param string $string
2480
 *   A min and max year string like '-3:+1'.
2481
 * @param object $date
2482
 *   (optional) A date object. Defaults to NULL.
2483
 *
2484
 * @return array
2485
 *   A numerically indexed array, containing a minimum and maximum year.
2486
 */
2487
function date_range_years($string, $date = NULL) {
2488
  $this_year = date_format(date_now(), 'Y');
2489
  list($min_year, $max_year) = explode(':', $string);
2490

    
2491
  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
2492
  $plus_pattern = '@[\+|\-][0-9]{1,4}@';
2493
  $year_pattern = '@^[0-9]{4}@';
2494
  if (!preg_match($year_pattern, $min_year, $matches)) {
2495
    if (preg_match($plus_pattern, $min_year, $matches)) {
2496
      $min_year = $this_year + $matches[0];
2497
    }
2498
    else {
2499
      $min_year = $this_year;
2500
    }
2501
  }
2502
  if (!preg_match($year_pattern, $max_year, $matches)) {
2503
    if (preg_match($plus_pattern, $max_year, $matches)) {
2504
      $max_year = $this_year + $matches[0];
2505
    }
2506
    else {
2507
      $max_year = $this_year;
2508
    }
2509
  }
2510
  // We expect the $min year to be less than the $max year.
2511
  // Some custom values for -99:+99 might not obey that.
2512
  if ($min_year > $max_year) {
2513
    $temp = $max_year;
2514
    $max_year = $min_year;
2515
    $min_year = $temp;
2516
  }
2517
  // If there is a current value, stretch the range to include it.
2518
  $value_year = is_object($date) ? $date->format('Y') : '';
2519
  if (!empty($value_year)) {
2520
    $min_year = min($value_year, $min_year);
2521
    $max_year = max($value_year, $max_year);
2522
  }
2523
  return array($min_year, $max_year);
2524
}
2525

    
2526
/**
2527
 * Converts a min and max year into a string like '-3:+1'.
2528
 *
2529
 * @param array $years
2530
 *   A numerically indexed array, containing a minimum and maximum year.
2531
 *
2532
 * @return string
2533
 *   A min and max year string like '-3:+1'.
2534
 */
2535
function date_range_string($years) {
2536
  $this_year = date_format(date_now(), 'Y');
2537

    
2538
  if ($years[0] < $this_year) {
2539
    $min = '-' . ($this_year - $years[0]);
2540
  }
2541
  else {
2542
    $min = '+' . ($years[0] - $this_year);
2543
  }
2544

    
2545
  if ($years[1] < $this_year) {
2546
    $max = '-' . ($this_year - $years[1]);
2547
  }
2548
  else {
2549
    $max = '+' . ($years[1] - $this_year);
2550
  }
2551

    
2552
  return $min . ':' . $max;
2553
}
2554

    
2555
/**
2556
 * Temporary helper to re-create equivalent of content_database_info().
2557
 */
2558
function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) {
2559
  return array(
2560
    'columns' => $field['storage']['details']['sql'][$revision],
2561
    'table' => _field_sql_storage_tablename($field),
2562
  );
2563
}
2564

    
2565
/**
2566
 * Implements hook_form_FORM_ID_alter() for system_regional_settings().
2567
 *
2568
 * Add a form element to configure whether or not week numbers are ISO-8601, the
2569
 * default is FALSE (US/UK/AUS norm).
2570
 */
2571
function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) {
2572
  $form['locale']['date_api_use_iso8601'] = array(
2573
    '#type' => 'checkbox',
2574
    '#title' => t('Use ISO-8601 week numbers'),
2575
    '#default_value' => variable_get('date_api_use_iso8601', FALSE),
2576
    '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'),
2577
  );
2578
  $form['#validate'][] = 'date_api_form_system_settings_validate';
2579
}
2580

    
2581
/**
2582
 * Validate that the option to use ISO weeks matches first day of week choice.
2583
 */
2584
function date_api_form_system_settings_validate(&$form, &$form_state) {
2585
  $form_values = $form_state['values'];
2586
  if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) {
2587
    form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.'));
2588
  }
2589
}
2590

    
2591
/**
2592
 * Creates an array of date format types for use as an options list.
2593
 */
2594
function date_format_type_options() {
2595
  $options = array();
2596
  $format_types = system_get_date_types();
2597
  if (!empty($format_types)) {
2598
    foreach ($format_types as $type => $type_info) {
2599
      $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')';
2600
    }
2601
  }
2602
  return $options;
2603
}
2604

    
2605
/**
2606
 * Creates an example date.
2607
 *
2608
 * This ensures a clear difference between month and day, and 12 and 24 hours.
2609
 */
2610
function date_example_date() {
2611
  $now = date_now();
2612
  if (date_format($now, 'M') == date_format($now, 'F')) {
2613
    date_modify($now, '+1 month');
2614
  }
2615
  if (date_format($now, 'm') == date_format($now, 'd')) {
2616
    date_modify($now, '+1 day');
2617
  }
2618
  if (date_format($now, 'H') == date_format($now, 'h')) {
2619
    date_modify($now, '+12 hours');
2620
  }
2621
  return $now;
2622
}
2623

    
2624
/**
2625
 * Determine if a start/end date combination qualify as 'All day'.
2626
 *
2627
 * @param string $string1
2628
 *   A string date in datetime format for the 'start' date.
2629
 * @param string $string2
2630
 *   A string date in datetime format for the 'end' date.
2631
 * @param string $granularity
2632
 *   (optional) The granularity of the date. Defaults to 'second'.
2633
 * @param int $increment
2634
 *   (optional) The increment of the date. Defaults to 1.
2635
 *
2636
 * @return bool
2637
 *   TRUE if the date is all day, FALSE otherwise.
2638
 */
2639
function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) {
2640
  if (empty($string1) || empty($string2)) {
2641
    return FALSE;
2642
  }
2643
  elseif (!in_array($granularity, array('hour', 'minute', 'second'))) {
2644
    return FALSE;
2645
  }
2646

    
2647
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches);
2648
  $count = count($matches);
2649
  $date1 = $count > 1 ? $matches[1] : '';
2650
  $time1 = $count > 2 ? $matches[2] : '';
2651
  $hour1 = $count > 3 ? intval($matches[3]) : 0;
2652
  $min1 = $count > 4 ? intval($matches[4]) : 0;
2653
  $sec1 = $count > 5 ? intval($matches[5]) : 0;
2654
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches);
2655
  $count = count($matches);
2656
  $date2 = $count > 1 ? $matches[1] : '';
2657
  $time2 = $count > 2 ? $matches[2] : '';
2658
  $hour2 = $count > 3 ? intval($matches[3]) : 0;
2659
  $min2 = $count > 4 ? intval($matches[4]) : 0;
2660
  $sec2 = $count > 5 ? intval($matches[5]) : 0;
2661
  if (empty($date1) || empty($date2)) {
2662
    return FALSE;
2663
  }
2664
  if (empty($time1) || empty($time2)) {
2665
    return FALSE;
2666
  }
2667

    
2668
  $tmp = date_seconds('s', TRUE, $increment);
2669
  $max_seconds = intval(array_pop($tmp));
2670
  $tmp = date_minutes('i', TRUE, $increment);
2671
  $max_minutes = intval(array_pop($tmp));
2672

    
2673
  // See if minutes and seconds are the maximum allowed for an increment or the
2674
  // maximum possible (59), or 0.
2675
  switch ($granularity) {
2676
    case 'second':
2677
      $min_match = $time1 == '00:00:00'
2678
        || ($hour1 == 0 && $min1 == 0 && $sec1 == 0);
2679
      $max_match = $time2 == '00:00:00'
2680
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59)))
2681
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0);
2682
      break;
2683
    case 'minute':
2684
      $min_match = $time1 == '00:00:00'
2685
        || ($hour1 == 0 && $min1 == 0);
2686
      $max_match = $time2 == '00:00:00'
2687
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
2688
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
2689
      break;
2690
    case 'hour':
2691
      $min_match = $time1 == '00:00:00'
2692
        || ($hour1 == 0);
2693
      $max_match = $time2 == '00:00:00'
2694
        || ($hour2 == 23)
2695
        || ($hour1 == 0 && $hour2 == 0);
2696
      break;
2697
    default:
2698
      $min_match = TRUE;
2699
      $max_match = FALSE;
2700
  }
2701

    
2702
  if ($min_match && $max_match) {
2703
    return TRUE;
2704
  }
2705

    
2706
  return FALSE;
2707
}
2708

    
2709
/**
2710
 * Helper function to round minutes and seconds to requested value.
2711
 */
2712
function date_increment_round(&$date, $increment) {
2713
  // Round minutes and seconds, if necessary.
2714
  if (is_object($date) && $increment > 1) {
2715
    $day = intval(date_format($date, 'j'));
2716
    $hour = intval(date_format($date, 'H'));
2717
    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
2718
    $minute = intval(date_format($date, 'i'));
2719
    if ($second == 60) {
2720
      $minute += 1;
2721
      $second = 0;
2722
    }
2723
    $minute = intval(round($minute / $increment) * $increment);
2724
    if ($minute == 60) {
2725
      $hour += 1;
2726
      $minute = 0;
2727
    }
2728
    date_time_set($date, $hour, $minute, $second);
2729
    if ($hour == 24) {
2730
      $day += 1;
2731
      $hour = 0;
2732
      $year = date_format($date, 'Y');
2733
      $month = date_format($date, 'n');
2734
      date_date_set($date, $year, $month, $day);
2735
    }
2736
  }
2737
  return $date;
2738
}
2739

    
2740
/**
2741
 * Determines if a date object is valid.
2742
 *
2743
 * @param object $date
2744
 *   The date object to check.
2745
 *
2746
 * @return bool
2747
 *   TRUE if the date is a valid date object, FALSE otherwise.
2748
 */
2749
function date_is_date($date) {
2750
  if (empty($date) || !is_object($date) || !empty($date->errors)) {
2751
    return FALSE;
2752
  }
2753
  return TRUE;
2754
}
2755

    
2756
/**
2757
 * This function will replace ISO values that have the pattern 9999-00-00T00:00:00
2758
 * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO
2759
 * dates and ensure that date objects created from this value contain a valid month
2760
 * and day. Without this fix, the ISO date '2020-00-00T00:00:00' would be created as
2761
 * November 30, 2019 (the previous day in the previous month).
2762
 *
2763
 * @param string $iso_string
2764
 *   An ISO string that needs to be made into a complete, valid date.
2765
 *
2766
 * @TODO Expand on this to work with all sorts of partial ISO dates.
2767
 */
2768
function date_make_iso_valid($iso_string) {
2769
  // If this isn't a value that uses an ISO pattern, there is nothing to do.
2770
  if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) {
2771
    return $iso_string;
2772
  }
2773
  // First see if month and day parts are '-00-00'.
2774
  if (substr($iso_string, 4, 6) == '-00-00') {
2775
    return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string);
2776
  }
2777
  // Then see if the day part is '-00'.
2778
  elseif (substr($iso_string, 7, 3) == '-00') {
2779
    return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string);
2780
  }
2781

    
2782
  // Fall through, no changes required.
2783
  return $iso_string;
2784
}