Projet

Général

Profil

Paste
Télécharger (87,5 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api.module @ 1f683914

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

    
72
  }
73
}
74

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

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

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

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

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

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

    
112
}
113

    
114
/**
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(
147
    'year',
148
    'month',
149
    'day',
150
    'hour',
151
    'minute',
152
    'second',
153
    'timezone'
154
  );
155
  private $serializedTime;
156
  private $serializedTimezone;
157

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
610
    $format_regexp = preg_quote($format);
611

    
612
    // Extract letters.
613
    $regex1 = preg_replace($patterns, $repl1, $format_regexp, 1);
614
    $regex1 = str_replace('A', '(.)', $regex1);
615
    $regex1 = str_replace('a', '(.)', $regex1);
616
    preg_match('`^' . $regex1 . '$`', stripslashes($format), $letters);
617
    array_shift($letters);
618
    // Extract values.
619
    $regex2 = preg_replace($patterns, $repl2, $format_regexp, 1);
620
    $regex2 = str_replace('A', '(AM|PM)', $regex2);
621
    $regex2 = str_replace('a', '(am|pm)', $regex2);
622
    preg_match('`^' . $regex2 . '$`u', $date, $values);
623
    array_shift($values);
624
    // If we did not find all the values for the patterns in the format, abort.
625
    if (count($letters) != count($values)) {
626
      $this->errors['invalid'] = t('The value @date does not match the expected format.', array('@date' => $date));
627
      return FALSE;
628
    }
629
    $this->granularity = array();
630
    $final_date = array(
631
      'hour' => 0,
632
      'minute' => 0,
633
      'second' => 0,
634
      'month' => 1,
635
      'day' => 1,
636
      'year' => 0,
637
    );
638
    foreach ($letters as $i => $letter) {
639
      $value = $values[$i];
640
      switch ($letter) {
641
        case 'd':
642
        case 'j':
643
          $final_date['day'] = intval($value);
644
          $this->addGranularity('day');
645
          break;
646

    
647
        case 'n':
648
        case 'm':
649
          $final_date['month'] = intval($value);
650
          $this->addGranularity('month');
651
          break;
652

    
653
        case 'F':
654
          $array_month_long = array_flip(date_month_names());
655
          $final_date['month'] = array_key_exists($value, $array_month_long) ? $array_month_long[$value] : -1;
656
          $this->addGranularity('month');
657
          break;
658

    
659
        case 'M':
660
          $array_month = array_flip(date_month_names_abbr());
661
          $final_date['month'] = array_key_exists($value, $array_month) ? $array_month[$value] : -1;
662
          $this->addGranularity('month');
663
          break;
664

    
665
        case 'Y':
666
          $final_date['year'] = $value;
667
          $this->addGranularity('year');
668
          if (strlen($value) < 4) {
669
            $this->errors['year'] = t('The year is invalid. Please check that entry includes four digits.');
670
          }
671
          break;
672

    
673
        case 'y':
674
          $year = $value;
675
          // If no century, we add the current one ("06" => "2006").
676
          $final_date['year'] = str_pad($year, 4, substr(date("Y"), 0, 2), STR_PAD_LEFT);
677
          $this->addGranularity('year');
678
          break;
679

    
680
        case 'a':
681
        case 'A':
682
          $ampm = strtolower($value);
683
          break;
684

    
685
        case 'g':
686
        case 'h':
687
        case 'G':
688
        case 'H':
689
          $final_date['hour'] = intval($value);
690
          $this->addGranularity('hour');
691
          break;
692

    
693
        case 'i':
694
          $final_date['minute'] = intval($value);
695
          $this->addGranularity('minute');
696
          break;
697

    
698
        case 's':
699
          $final_date['second'] = intval($value);
700
          $this->addGranularity('second');
701
          break;
702

    
703
        case 'U':
704
          parent::__construct($value, $tz ? $tz : new DateTimeZone("UTC"));
705
          $this->addGranularity('year');
706
          $this->addGranularity('month');
707
          $this->addGranularity('day');
708
          $this->addGranularity('hour');
709
          $this->addGranularity('minute');
710
          $this->addGranularity('second');
711
          return $this;
712

    
713
      }
714
    }
715
    if (isset($ampm) && $ampm == 'pm' && $final_date['hour'] < 12) {
716
      $final_date['hour'] += 12;
717
    }
718
    elseif (isset($ampm) && $ampm == 'am' && $final_date['hour'] == 12) {
719
      $final_date['hour'] -= 12;
720
    }
721

    
722
    // Blank becomes current time, given TZ.
723
    parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
724
    if ($tz) {
725
      $this->addGranularity('timezone');
726
    }
727

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

    
732
    $this->errors += $this->arrayErrors($final_date);
733
    $granularity = drupal_map_assoc($this->granularity);
734

    
735
    // If the input value is '0000-00-00', PHP's date class will later
736
    // incorrectly convert it to something like '-0001-11-30' if we do setDate()
737
    // here. If we don't do setDate() here, it will default to the current date
738
    // and we will lose any way to tell that there was no date in the orignal
739
    // input values. So set a flag we can use later to tell that this date
740
    // object was created using only time values, and that the date values are
741
    // artifical.
742
    if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) {
743
      $this->timeOnly = TRUE;
744
    }
745
    elseif (empty($this->errors)) {
746
      // setDate() expects a valid year, month, and day.
747
      // Set some defaults for dates that don't use this to
748
      // keep PHP from interpreting it as the last day of
749
      // the previous month or last month of the previous year.
750
      if (empty($granularity['month'])) {
751
        $final_date['month'] = 1;
752
      }
753
      if (empty($granularity['day'])) {
754
        $final_date['day'] = 1;
755
      }
756
      $this->setDate($final_date['year'], $final_date['month'], $final_date['day']);
757
    }
758

    
759
    if (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) {
760
      $this->dateOnly = TRUE;
761
    }
762
    elseif (empty($this->errors)) {
763
      $this->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
764
    }
765
    return $this;
766
  }
767

    
768
  /**
769
   * Returns all standard date parts in an array.
770
   *
771
   * Will return '' for parts in which it lacks granularity.
772
   *
773
   * @param bool $force
774
   *   Whether or not to limit the granularity. Defaults to FALSE.
775
   *
776
   * @return array
777
   *   An array of formatted date part values, keyed by date parts.
778
   */
779
  public function toArray($force = FALSE) {
780
    return array(
781
      'year' => $this->format('Y', $force),
782
      'month' => $this->format('n', $force),
783
      'day' => $this->format('j', $force),
784
      'hour' => intval($this->format('H', $force)),
785
      'minute' => intval($this->format('i', $force)),
786
      'second' => intval($this->format('s', $force)),
787
      'timezone' => $this->format('e', $force),
788
    );
789
  }
790

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

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

    
874
  /**
875
   * Converts a date part into something that will produce a valid date.
876
   *
877
   * @param string $part
878
   *   The date part.
879
   * @param int $value
880
   *   The date value for this part.
881
   * @param string $default
882
   *   (optional) If the fallback should use the first value of the date part,
883
   *   or the current value of the date part. Defaults to 'first'.
884
   * @param int $month
885
   *   (optional) Used when the date part is less than 'month' to specify the
886
   *   date. Defaults to NULL.
887
   * @param int $year
888
   *   (optional) Used when the date part is less than 'year' to specify the
889
   *   date. Defaults to NULL.
890
   *
891
   * @return int
892
   *   A valid date value.
893
   */
894
  protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
895
    $now = date_now();
896
    switch ($part) {
897
      case 'year':
898
        $fallback = $now->format('Y');
899
        return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
900

    
901
      case 'month':
902
        $fallback = $default == 'first' ? 1 : $now->format('n');
903
        return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
904

    
905
      case 'day':
906
        $fallback = $default == 'first' ? 1 : $now->format('j');
907
        $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
908
        return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
909

    
910
      case 'hour':
911
        $fallback = $default == 'first' ? 0 : $now->format('G');
912
        return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
913

    
914
      case 'minute':
915
        $fallback = $default == 'first' ? 0 : $now->format('i');
916
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
917

    
918
      case 'second':
919
        $fallback = $default == 'first' ? 0 : $now->format('s');
920
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
921
    }
922
  }
923

    
924
  /**
925
   * Finds possible errors in an array of date part values.
926
   *
927
   * The forceValid() function will change an invalid value to a valid one, so
928
   * we just need to see if the value got altered.
929
   *
930
   * @param array $arr
931
   *   An array of date values, keyed by date part.
932
   *
933
   * @return array
934
   *   An array of error messages, keyed by date part.
935
   */
936
  public function arrayErrors($arr) {
937
    $errors = array();
938
    $now = date_now();
939
    $default_month = !empty($arr['month']) ? $arr['month'] : $now->format('n');
940
    $default_year = !empty($arr['year']) ? $arr['year'] : $now->format('Y');
941

    
942
    $this->granularity = array();
943
    foreach ($arr as $part => $value) {
944
      // Explicitly set the granularity to the values in the input array.
945
      if (is_numeric($value)) {
946
        $this->addGranularity($part);
947
      }
948
      // Avoid false errors when a numeric value is input as a string by casting
949
      // as an integer.
950
      $value = intval($value);
951
      if (!empty($value) && $this->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
952
        // Use a switch/case to make translation easier by providing a different
953
        // message for each part.
954
        switch ($part) {
955
          case 'year':
956
            $errors['year'] = t('The year is invalid.');
957
            break;
958

    
959
          case 'month':
960
            $errors['month'] = t('The month is invalid.');
961
            break;
962

    
963
          case 'day':
964
            $errors['day'] = t('The day is invalid.');
965
            break;
966

    
967
          case 'hour':
968
            $errors['hour'] = t('The hour is invalid.');
969
            break;
970

    
971
          case 'minute':
972
            $errors['minute'] = t('The minute is invalid.');
973
            break;
974

    
975
          case 'second':
976
            $errors['second'] = t('The second is invalid.');
977
            break;
978
        }
979
      }
980
    }
981
    if ($this->hasTime()) {
982
      $this->addGranularity('timezone');
983
    }
984
    return $errors;
985
  }
986

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

    
1021
        case 'minutes':
1022
          return $diff / 60;
1023

    
1024
        case 'hours':
1025
          return $diff / 3600;
1026

    
1027
        case 'years':
1028
          return $year_diff;
1029

    
1030
        case 'months':
1031
          $format = 'n';
1032
          $item1 = date_format($date1, $format);
1033
          $item2 = date_format($date2, $format);
1034
          if ($year_diff == 0) {
1035
            return intval($item2 - $item1);
1036
          }
1037
          elseif ($year_diff < 0) {
1038
            $item_diff = 0 - $item1;
1039
            $item_diff -= intval((abs($year_diff) - 1) * 12);
1040
            return $item_diff - (12 - $item2);
1041
          }
1042
          else {
1043
            $item_diff = 12 - $item1;
1044
            $item_diff += intval(($year_diff - 1) * 12);
1045
            return $item_diff + $item2;
1046
          }
1047
          break;
1048

    
1049
        case 'days':
1050
          $format = 'z';
1051
          $item1 = date_format($date1, $format);
1052
          $item2 = date_format($date2, $format);
1053
          if ($year_diff == 0) {
1054
            return intval($item2 - $item1);
1055
          }
1056
          elseif ($year_diff < 0) {
1057
            $item_diff = 0 - $item1;
1058
            for ($i = 1; $i < abs($year_diff); $i++) {
1059
              date_modify($date1, '-1 year');
1060
              $item_diff -= date_days_in_year($date1);
1061
            }
1062
            return $item_diff - (date_days_in_year($date2) - $item2);
1063
          }
1064
          else {
1065
            $item_diff = date_days_in_year($date1) - $item1;
1066
            for ($i = 1; $i < $year_diff; $i++) {
1067
              date_modify($date1, '+1 year');
1068
              $item_diff += date_days_in_year($date1);
1069
            }
1070
            return $item_diff + $item2;
1071
          }
1072
          break;
1073

    
1074
        case 'weeks':
1075
          $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
1076
          $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
1077

    
1078
          $sign = ($year_diff < 0) ? -1 : 1;
1079

    
1080
          for ($i = 1; $i <= abs($year_diff); $i++) {
1081
            date_modify($date1, (($sign > 0) ? '+' : '-') . '1 year');
1082
            $week_diff += (date_iso_weeks_in_year($date1) * $sign);
1083
          }
1084
          return $week_diff;
1085
      }
1086
    }
1087
    return NULL;
1088
  }
1089
}
1090

    
1091
/**
1092
 * Determines if the date element needs to be processed.
1093
 *
1094
 * Helper function to see if date element has been hidden by FAPI to see if it
1095
 * needs to be processed or just pass the value through. This is needed since
1096
 * normal date processing explands the date element into parts and then
1097
 * reconstructs it, which is not needed or desirable if the field is hidden.
1098
 *
1099
 * @param array $element
1100
 *   The date element to check.
1101
 *
1102
 * @return bool
1103
 *   TRUE if the element is effectively hidden, FALSE otherwise.
1104
 */
1105
function date_hidden_element($element) {
1106
  // @TODO What else needs to be tested to see if dates are hidden or disabled?
1107
  if ((isset($element['#access']) && empty($element['#access']))
1108
    || !empty($element['#programmed'])
1109
    || in_array($element['#type'], array('hidden', 'value'))) {
1110
    return TRUE;
1111
  }
1112
  return FALSE;
1113
}
1114

    
1115
/**
1116
 * Helper function for getting the format string for a date type.
1117
 *
1118
 * @param string $type
1119
 *   A date type format name.
1120
 *
1121
 * @return string
1122
 *   A date type format, like 'Y-m-d H:i:s'.
1123
 */
1124
function date_type_format($type) {
1125
  switch ($type) {
1126
    case DATE_ISO:
1127
      return DATE_FORMAT_ISO;
1128

    
1129
    case DATE_UNIX:
1130
      return DATE_FORMAT_UNIX;
1131

    
1132
    case DATE_DATETIME:
1133
      return DATE_FORMAT_DATETIME;
1134

    
1135
    case DATE_ICAL:
1136
      return DATE_FORMAT_ICAL;
1137
  }
1138
}
1139

    
1140
/**
1141
 * Constructs an untranslated array of month names.
1142
 *
1143
 * Needed for CSS, translation functions, strtotime(), and other places
1144
 * that use the English versions of these words.
1145
 *
1146
 * @return array
1147
 *   An array of month names.
1148
 */
1149
function date_month_names_untranslated() {
1150
  static $month_names;
1151
  if (empty($month_names)) {
1152
    $month_names = array(
1153
      1 => 'January',
1154
      2 => 'February',
1155
      3 => 'March',
1156
      4 => 'April',
1157
      5 => 'May',
1158
      6 => 'June',
1159
      7 => 'July',
1160
      8 => 'August',
1161
      9 => 'September',
1162
      10 => 'October',
1163
      11 => 'November',
1164
      12 => 'December',
1165
    );
1166
  }
1167
  return $month_names;
1168
}
1169

    
1170
/**
1171
 * Returns a translated array of month names.
1172
 *
1173
 * @param bool $required
1174
 *   (optional) If FALSE, the returned array will include a blank value.
1175
 *   Defaults to FALSE.
1176
 *
1177
 * @return array
1178
 *   An array of month names.
1179
 */
1180
function date_month_names($required = FALSE) {
1181
  $month_names = array();
1182
  foreach (date_month_names_untranslated() as $key => $month) {
1183
    $month_names[$key] = t($month, array(), array('context' => 'Long month name'));
1184
  }
1185
  $none = array('' => '');
1186
  return !$required ? $none + $month_names : $month_names;
1187
}
1188

    
1189
/**
1190
 * Constructs a translated array of month name abbreviations.
1191
 *
1192
 * @param bool $required
1193
 *   (optional) If FALSE, the returned array will include a blank value.
1194
 *   Defaults to FALSE.
1195
 * @param int $length
1196
 *   (optional) The length of the abbreviation. Defaults to 3.
1197
 *
1198
 * @return array
1199
 *   An array of month abbreviations.
1200
 */
1201
function date_month_names_abbr($required = FALSE, $length = 3) {
1202
  $month_names = array();
1203
  foreach (date_month_names_untranslated() as $key => $month) {
1204
    if ($length == 3) {
1205
      $month_names[$key] = t(substr($month, 0, $length), array());
1206
    }
1207
    else {
1208
      $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr'));
1209
    }
1210
  }
1211
  $none = array('' => '');
1212
  return !$required ? $none + $month_names : $month_names;
1213
}
1214

    
1215
/**
1216
 * Constructs an untranslated array of week days.
1217
 *
1218
 * Needed for CSS, translation functions, strtotime(), and other places
1219
 * that use the English versions of these words.
1220
 *
1221
 * @param bool $refresh
1222
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1223
 *
1224
 * @return array
1225
 *   An array of week day names
1226
 */
1227
function date_week_days_untranslated($refresh = TRUE) {
1228
  static $weekdays;
1229
  if ($refresh || empty($weekdays)) {
1230
    $weekdays = array(
1231
      'Sunday',
1232
      'Monday',
1233
      'Tuesday',
1234
      'Wednesday',
1235
      'Thursday',
1236
      'Friday',
1237
      'Saturday',
1238
    );
1239
  }
1240
  return $weekdays;
1241
}
1242

    
1243
/**
1244
 * Returns a translated array of week names.
1245
 *
1246
 * @param bool $required
1247
 *   (optional) If FALSE, the returned array will include a blank value.
1248
 *   Defaults to FALSE.
1249
 *
1250
 * @return array
1251
 *   An array of week day names
1252
 */
1253
function date_week_days($required = FALSE, $refresh = TRUE) {
1254
  $weekdays = array();
1255
  foreach (date_week_days_untranslated() as $key => $day) {
1256
    $weekdays[$key] = t($day, array(), array('context' => ''));
1257
  }
1258
  $none = array('' => '');
1259
  return !$required ? $none + $weekdays : $weekdays;
1260
}
1261

    
1262
/**
1263
 * Constructs a translated array of week day abbreviations.
1264
 *
1265
 * @param bool $required
1266
 *   (optional) If FALSE, the returned array will include a blank value.
1267
 *   Defaults to FALSE.
1268
 * @param bool $refresh
1269
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1270
 * @param int $length
1271
 *   (optional) The length of the abbreviation. Defaults to 3.
1272
 *
1273
 * @return array
1274
 *   An array of week day abbreviations
1275
 */
1276
function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
1277
  $weekdays = array();
1278
  switch ($length) {
1279
    case 1:
1280
      $context = 'day_abbr1';
1281
      break;
1282

    
1283
    case 2:
1284
      $context = 'day_abbr2';
1285
      break;
1286

    
1287
    default:
1288
      $context = '';
1289
      break;
1290
  }
1291
  foreach (date_week_days_untranslated() as $key => $day) {
1292
    $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context));
1293
  }
1294
  $none = array('' => '');
1295
  return !$required ? $none + $weekdays : $weekdays;
1296
}
1297

    
1298
/**
1299
 * Reorders weekdays to match the first day of the week.
1300
 *
1301
 * @param array $weekdays
1302
 *   An array of weekdays.
1303
 *
1304
 * @return array
1305
 *   An array of weekdays reordered to match the first day of the week.
1306
 */
1307
function date_week_days_ordered($weekdays) {
1308
  $first_day = variable_get('date_first_day', 0);
1309
  if ($first_day > 0) {
1310
    for ($i = 1; $i <= $first_day; $i++) {
1311
      $last = array_shift($weekdays);
1312
      array_push($weekdays, $last);
1313
    }
1314
  }
1315
  return $weekdays;
1316
}
1317

    
1318
/**
1319
 * Constructs an array of years.
1320
 *
1321
 * @param int $start
1322
 *   The start year in the array.
1323
 * @param int $end
1324
 *   The end year in the array.
1325
 * @param bool $required
1326
 *   (optional) If FALSE, the returned array will include a blank value.
1327
 *   Defaults to FALSE.
1328
 *
1329
 * @return array
1330
 *   An array of years in the selected range.
1331
 */
1332
function date_years($start = 0, $end = 0, $required = FALSE) {
1333
  // Ensure $min and $max are valid values.
1334
  if (empty($start)) {
1335
    $start = intval(date('Y', REQUEST_TIME) - 3);
1336
  }
1337
  if (empty($end)) {
1338
    $end = intval(date('Y', REQUEST_TIME) + 3);
1339
  }
1340
  $none = array(0 => '');
1341
  return !$required ? $none + drupal_map_assoc(range($start, $end)) : drupal_map_assoc(range($start, $end));
1342
}
1343

    
1344
/**
1345
 * Constructs an array of days in a month.
1346
 *
1347
 * @param bool $required
1348
 *   (optional) If FALSE, the returned array will include a blank value.
1349
 *   Defaults to FALSE.
1350
 * @param int $month
1351
 *   (optional) The month in which to find the number of days.
1352
 * @param int $year
1353
 *   (optional) The year in which to find the number of days.
1354
 *
1355
 * @return array
1356
 *   An array of days for the selected month.
1357
 */
1358
function date_days($required = FALSE, $month = NULL, $year = NULL) {
1359
  // If we have a month and year, find the right last day of the month.
1360
  if (!empty($month) && !empty($year)) {
1361
    $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC');
1362
    $max = $date->format('t');
1363
  }
1364
  // If there is no month and year given, default to 31.
1365
  if (empty($max)) {
1366
    $max = 31;
1367
  }
1368
  $none = array(0 => '');
1369
  return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
1370
}
1371

    
1372
/**
1373
 * Constructs an array of hours.
1374
 *
1375
 * @param string $format
1376
 *   A date format string.
1377
 * @param bool $required
1378
 *   (optional) If FALSE, the returned array will include a blank value.
1379
 *   Defaults to FALSE.
1380
 *
1381
 * @return array
1382
 *   An array of hours in the selected format.
1383
 */
1384
function date_hours($format = 'H', $required = FALSE) {
1385
  $hours = array();
1386
  if ($format == 'h' || $format == 'g') {
1387
    $min = 1;
1388
    $max = 12;
1389
  }
1390
  else {
1391
    $min = 0;
1392
    $max = 23;
1393
  }
1394
  for ($i = $min; $i <= $max; $i++) {
1395
    $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
1396
  }
1397
  $none = array('' => '');
1398
  return !$required ? $none + $hours : $hours;
1399
}
1400

    
1401
/**
1402
 * Constructs an array of minutes.
1403
 *
1404
 * @param string $format
1405
 *   A date format string.
1406
 * @param bool $required
1407
 *   (optional) If FALSE, the returned array will include a blank value.
1408
 *   Defaults to FALSE.
1409
 *
1410
 * @return array
1411
 *   An array of minutes in the selected format.
1412
 */
1413
function date_minutes($format = 'i', $required = FALSE, $increment = 1) {
1414
  $minutes = array();
1415
  // Ensure $increment has a value so we don't loop endlessly.
1416
  if (empty($increment)) {
1417
    $increment = 1;
1418
  }
1419
  for ($i = 0; $i < 60; $i += $increment) {
1420
    $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i;
1421
  }
1422
  $none = array('' => '');
1423
  return !$required ? $none + $minutes : $minutes;
1424
}
1425

    
1426
/**
1427
 * Constructs an array of seconds.
1428
 *
1429
 * @param string $format
1430
 *   A date format string.
1431
 * @param bool $required
1432
 *   (optional) If FALSE, the returned array will include a blank value.
1433
 *   Defaults to FALSE.
1434
 *
1435
 * @return array
1436
 *   An array of seconds in the selected format.
1437
 */
1438
function date_seconds($format = 's', $required = FALSE, $increment = 1) {
1439
  $seconds = array();
1440
  // Ensure $increment has a value so we don't loop endlessly.
1441
  if (empty($increment)) {
1442
    $increment = 1;
1443
  }
1444
  for ($i = 0; $i < 60; $i += $increment) {
1445
    $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
1446
  }
1447
  $none = array('' => '');
1448
  return !$required ? $none + $seconds : $seconds;
1449
}
1450

    
1451
/**
1452
 * Constructs an array of AM and PM options.
1453
 *
1454
 * @param bool $required
1455
 *   (optional) If FALSE, the returned array will include a blank value.
1456
 *   Defaults to FALSE.
1457
 *
1458
 * @return array
1459
 *   An array of AM and PM options.
1460
 */
1461
function date_ampm($required = FALSE) {
1462
  $none = array('' => '');
1463
  $ampm = array(
1464
    'am' => t('am', array(), array('context' => 'ampm')),
1465
    'pm' => t('pm', array(), array('context' => 'ampm')),
1466
  );
1467
  return !$required ? $none + $ampm : $ampm;
1468
}
1469

    
1470
/**
1471
 * Constructs an array of regex replacement strings for date format elements.
1472
 *
1473
 * @param bool $strict
1474
 *   Whether or not to force 2 digits for elements that sometimes allow either
1475
 *   1 or 2 digits.
1476
 *
1477
 * @return array
1478
 *   An array of date() format letters and their regex equivalents.
1479
 */
1480
function date_format_patterns($strict = FALSE) {
1481
  return array(
1482
    'd' => '\d{' . ($strict ? '2' : '1,2') . '}',
1483
    'm' => '\d{' . ($strict ? '2' : '1,2') . '}',
1484
    'h' => '\d{' . ($strict ? '2' : '1,2') . '}',
1485
    'H' => '\d{' . ($strict ? '2' : '1,2') . '}',
1486
    'i' => '\d{' . ($strict ? '2' : '1,2') . '}',
1487
    's' => '\d{' . ($strict ? '2' : '1,2') . '}',
1488
    'j' => '\d{1,2}',
1489
    'N' => '\d',
1490
    'S' => '\w{2}',
1491
    'w' => '\d',
1492
    'z' => '\d{1,3}',
1493
    'W' => '\d{1,2}',
1494
    'n' => '\d{1,2}',
1495
    't' => '\d{2}',
1496
    'L' => '\d',
1497
    'o' => '\d{4}',
1498
    'Y' => '-?\d{1,6}',
1499
    'y' => '\d{2}',
1500
    'B' => '\d{3}',
1501
    'g' => '\d{1,2}',
1502
    'G' => '\d{1,2}',
1503
    'e' => '\w*',
1504
    'I' => '\d',
1505
    'T' => '\w*',
1506
    'U' => '\d*',
1507
    'z' => '[+-]?\d*',
1508
    'O' => '[+-]?\d{4}',
1509
    // Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like
1510
    // German umlaut. Per http://drupal.org/node/1101284, we may need as little
1511
    // as 2 and as many as 5 characters in some languages.
1512
    'D' => '\S{2,5}',
1513
    'l' => '\S*',
1514
    'M' => '\S{2,5}',
1515
    'F' => '\S*',
1516
    'P' => '[+-]?\d{2}\:\d{2}',
1517
    'O' => '[+-]\d{4}',
1518
    'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})',
1519
    'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?',
1520
  );
1521
}
1522

    
1523
/**
1524
 * Constructs an array of granularity options and their labels.
1525
 *
1526
 * @return array
1527
 *   An array of translated date parts, keyed by their machine name.
1528
 */
1529
function date_granularity_names() {
1530
  return array(
1531
    'year' => t('Year', array(), array('context' => 'datetime')),
1532
    'month' => t('Month', array(), array('context' => 'datetime')),
1533
    'day' => t('Day', array(), array('context' => 'datetime')),
1534
    'hour' => t('Hour', array(), array('context' => 'datetime')),
1535
    'minute' => t('Minute', array(), array('context' => 'datetime')),
1536
    'second' => t('Second', array(), array('context' => 'datetime')),
1537
  );
1538
}
1539

    
1540
/**
1541
 * Sorts a granularity array.
1542
 *
1543
 * @param array $granularity
1544
 *   An array of date parts.
1545
 */
1546
function date_granularity_sorted($granularity) {
1547
  return array_intersect(array(
1548
    'year',
1549
    'month',
1550
    'day',
1551
    'hour',
1552
    'minute',
1553
    'second',
1554
  ), $granularity);
1555
}
1556

    
1557
/**
1558
 * Constructs an array of granularity based on a given precision.
1559
 *
1560
 * @param string $precision
1561
 *   A granularity item.
1562
 *
1563
 * @return array
1564
 *   A granularity array containing the given precision and all those above it.
1565
 *   For example, passing in 'month' will return array('year', 'month').
1566
 */
1567
function date_granularity_array_from_precision($precision) {
1568
  $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second');
1569
  switch ($precision) {
1570
    case 'year':
1571
      return array_slice($granularity_array, -6, 1);
1572

    
1573
    case 'month':
1574
      return array_slice($granularity_array, -6, 2);
1575

    
1576
    case 'day':
1577
      return array_slice($granularity_array, -6, 3);
1578

    
1579
    case 'hour':
1580
      return array_slice($granularity_array, -6, 4);
1581

    
1582
    case 'minute':
1583
      return array_slice($granularity_array, -6, 5);
1584

    
1585
    default:
1586
      return $granularity_array;
1587
  }
1588
}
1589

    
1590
/**
1591
 * Give a granularity array, return the highest precision.
1592
 *
1593
 * @param array $granularity_array
1594
 *   An array of date parts.
1595
 *
1596
 * @return string
1597
 *   The most precise element in a granularity array.
1598
 */
1599
function date_granularity_precision($granularity_array) {
1600
  $input = date_granularity_sorted($granularity_array);
1601
  return array_pop($input);
1602
}
1603

    
1604
/**
1605
 * Constructs a valid DATETIME format string for the granularity of an item.
1606
 *
1607
 * @todo This function is no longer used as of
1608
 * http://drupalcode.org/project/date.git/commit/07efbb5.
1609
 */
1610
function date_granularity_format($granularity) {
1611
  if (is_array($granularity)) {
1612
    $granularity = date_granularity_precision($granularity);
1613
  }
1614
  $format = 'Y-m-d H:i:s';
1615
  switch ($granularity) {
1616
    case 'year':
1617
      return substr($format, 0, 1);
1618

    
1619
    case 'month':
1620
      return substr($format, 0, 3);
1621

    
1622
    case 'day':
1623
      return substr($format, 0, 5);
1624

    
1625
    case 'hour';
1626
      return substr($format, 0, 7);
1627

    
1628
    case 'minute':
1629
      return substr($format, 0, 9);
1630

    
1631
    default:
1632
      return $format;
1633
  }
1634
}
1635

    
1636
/**
1637
 * Returns a translated array of timezone names.
1638
 *
1639
 * Cache the untranslated array, make the translated array a static variable.
1640
 *
1641
 * @param bool $required
1642
 *   (optional) If FALSE, the returned array will include a blank value.
1643
 *   Defaults to FALSE.
1644
 * @param bool $refresh
1645
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1646
 *
1647
 * @return array
1648
 *   An array of timezone names.
1649
 */
1650
function date_timezone_names($required = FALSE, $refresh = FALSE) {
1651
  static $zonenames;
1652
  if (empty($zonenames) || $refresh) {
1653
    $cached = cache_get('date_timezone_identifiers_list');
1654
    $zonenames = !empty($cached) ? $cached->data : array();
1655
    if ($refresh || empty($cached) || empty($zonenames)) {
1656
      $data = timezone_identifiers_list();
1657
      asort($data);
1658
      foreach ($data as $delta => $zone) {
1659
        // Because many timezones exist in PHP only for backward compatibility
1660
        // reasons and should not be used, the list is filtered by a regular
1661
        // expression.
1662
        if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
1663
          $zonenames[$zone] = $zone;
1664
        }
1665
      }
1666

    
1667
      if (!empty($zonenames)) {
1668
        cache_set('date_timezone_identifiers_list', $zonenames);
1669
      }
1670
    }
1671
    foreach ($zonenames as $zone) {
1672
      $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone)));
1673
    }
1674
  }
1675
  $none = array('' => '');
1676
  return !$required ? $none + $zonenames : $zonenames;
1677
}
1678

    
1679
/**
1680
 * Returns an array of system-allowed timezone abbreviations.
1681
 *
1682
 * Cache an array of just the abbreviation names because the whole
1683
 * timezone_abbreviations_list() is huge, so we don't want to retrieve it more
1684
 * than necessary.
1685
 *
1686
 * @param bool $refresh
1687
 *   (optional) Whether to refresh the list. Defaults to TRUE.
1688
 *
1689
 * @return array
1690
 *   An array of allowed timezone abbreviations.
1691
 */
1692
function date_timezone_abbr($refresh = FALSE) {
1693
  $cached = cache_get('date_timezone_abbreviations');
1694
  $data = isset($cached->data) ? $cached->data : array();
1695
  if (empty($data) || $refresh) {
1696
    $data = array_keys(timezone_abbreviations_list());
1697
    cache_set('date_timezone_abbreviations', $data);
1698
  }
1699
  return $data;
1700
}
1701

    
1702
/**
1703
 * Formats a date, using a date type or a custom date format string.
1704
 *
1705
 * Reworked from Drupal's format_date function to handle pre-1970 and
1706
 * post-2038 dates and accept a date object instead of a timestamp as input.
1707
 * Translates formatted date results, unlike PHP function date_format().
1708
 * Should only be used for display, not input, because it can't be parsed.
1709
 *
1710
 * @param object $date
1711
 *   A date object.
1712
 * @param string $type
1713
 *   (optional) The date format to use. Can be 'small', 'medium' or 'large' for
1714
 *   the preconfigured date formats. If 'custom' is specified, then $format is
1715
 *   required as well. Defaults to 'medium'.
1716
 * @param string $format
1717
 *   (optional) A PHP date format string as required by date(). A backslash
1718
 *   should be used before a character to avoid interpreting the character as
1719
 *   part of a date format. Defaults to an empty string.
1720
 * @param string $langcode
1721
 *   (optional) Language code to translate to. Defaults to NULL.
1722
 *
1723
 * @return string
1724
 *   A translated date string in the requested format.
1725
 *
1726
 * @see format_date()
1727
 */
1728
function date_format_date($date, $type = 'medium', $format = '', $langcode = NULL) {
1729
  if (empty($date)) {
1730
    return '';
1731
  }
1732
  if ($type != 'custom') {
1733
    $format = variable_get('date_format_' . $type);
1734
  }
1735
  if ($type != 'custom' && empty($format)) {
1736
    $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
1737
  }
1738
  $format = date_limit_format($format, $date->granularity);
1739
  $max = strlen($format);
1740
  $datestring = '';
1741
  for ($i = 0; $i < $max; $i++) {
1742
    $c = $format[$i];
1743
    switch ($c) {
1744
      case 'l':
1745
        $datestring .= t($date->format('l'), array(), array('context' => '', 'langcode' => $langcode));
1746
        break;
1747

    
1748
      case 'D':
1749
        $datestring .= t($date->format('D'), array(), array('context' => '', 'langcode' => $langcode));
1750
        break;
1751

    
1752
      case 'F':
1753
        $datestring .= t($date->format('F'), array(), array('context' => 'Long month name', 'langcode' => $langcode));
1754
        break;
1755

    
1756
      case 'M':
1757
        $datestring .= t($date->format('M'), array(), array('langcode' => $langcode));
1758
        break;
1759

    
1760
      case 'A':
1761
      case 'a':
1762
        $datestring .= t($date->format($c), array(), array('context' => 'ampm', 'langcode' => $langcode));
1763
        break;
1764

    
1765
      // The timezone name translations can use t().
1766
      case 'e':
1767
      case 'T':
1768
        $datestring .= t($date->format($c));
1769
        break;
1770

    
1771
      // Remaining date parts need no translation.
1772
      case 'O':
1773
        $datestring .= sprintf('%s%02d%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1774
        break;
1775

    
1776
      case 'P':
1777
        $datestring .= sprintf('%s%02d:%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
1778
        break;
1779

    
1780
      case 'Z':
1781
        $datestring .= date_offset_get($date);
1782
        break;
1783

    
1784
      case '\\':
1785
        $datestring .= $format[++$i];
1786
        break;
1787

    
1788
      case 'r':
1789
        $datestring .= date_format_date($date, 'custom', 'D, d M Y H:i:s O', 'en');
1790
        break;
1791

    
1792
      default:
1793
        if (strpos('BdcgGhHiIjLmnNosStTuUwWYyz', $c) !== FALSE) {
1794
          $datestring .= $date->format($c);
1795
        }
1796
        else {
1797
          $datestring .= $c;
1798
        }
1799
    }
1800
  }
1801
  return $datestring;
1802
}
1803

    
1804
/**
1805
 * Formats a time interval with granularity, including past and future context.
1806
 *
1807
 * @param object $date
1808
 *   The current date object.
1809
 * @param int $granularity
1810
 *   (optional) Number of units to display in the string. Defaults to 2.
1811
 *
1812
 * @return string
1813
 *   A translated string representation of the interval.
1814
 *
1815
 * @see format_interval()
1816
 */
1817
function date_format_interval($date, $granularity = 2, $display_ago = TRUE) {
1818
  // If no date is sent, then return nothing.
1819
  if (empty($date)) {
1820
    return NULL;
1821
  }
1822

    
1823
  $interval = REQUEST_TIME - $date->format('U');
1824
  if ($interval > 0) {
1825
    return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) :
1826
      t('!time', array('!time' => format_interval($interval, $granularity)));
1827
  }
1828
  else {
1829
    return format_interval(abs($interval), $granularity);
1830
  }
1831
}
1832

    
1833
/**
1834
 * A date object for the current time.
1835
 *
1836
 * @param object|string|null $timezone
1837
 *   (optional) PHP DateTimeZone object, string or NULL allowed. Optionally
1838
 *   force time to a specific timezone, defaults to user timezone, if set,
1839
 *   otherwise site timezone. Defaults to NULL.
1840
 *
1841
 * @param bool $reset
1842
 *   (optional) Static cache reset.
1843
 *
1844
 * @return object
1845
 *   The current time as a date object.
1846
 */
1847
function date_now($timezone = NULL, $reset = FALSE) {
1848
  $static_var = __FUNCTION__ . $timezone;
1849
  if ($timezone instanceof DateTimeZone) {
1850
    $static_var = __FUNCTION__ . $timezone->getName();
1851
  }
1852

    
1853
  if ($reset) {
1854
    drupal_static_reset($static_var);
1855
  }
1856

    
1857
  $now = &drupal_static($static_var);
1858

    
1859
  if (!isset($now)) {
1860
    $now = new DateObject('now', $timezone);
1861
  }
1862

    
1863
  // Avoid unexpected manipulation of cached $now object
1864
  // by subsequent code execution
1865
  // @see https://drupal.org/node/2261395
1866
  $clone = clone $now;
1867
  return $clone;
1868
}
1869

    
1870
/**
1871
 * Determines if a timezone string is valid.
1872
 *
1873
 * @param string $timezone
1874
 *   A potentially invalid timezone string.
1875
 *
1876
 * @return bool
1877
 *   TRUE if the timezone is valid, FALSE otherwise.
1878
 */
1879
function date_timezone_is_valid($timezone) {
1880
  static $timezone_names;
1881
  if (empty($timezone_names)) {
1882
    $timezone_names = array_keys(date_timezone_names(TRUE));
1883
  }
1884
  return in_array($timezone, $timezone_names);
1885
}
1886

    
1887
/**
1888
 * Returns a timezone name to use as a default.
1889
 *
1890
 * @param bool $check_user
1891
 *   (optional) Whether or not to check for a user-configured timezone.
1892
 *   Defaults to TRUE.
1893
 *
1894
 * @return string
1895
 *   The default timezone for a user, if available, otherwise the site.
1896
 */
1897
function date_default_timezone($check_user = TRUE) {
1898
  global $user;
1899
  if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) {
1900
    return $user->timezone;
1901
  }
1902
  else {
1903
    $default = variable_get('date_default_timezone', '');
1904
    return empty($default) ? 'UTC' : $default;
1905
  }
1906
}
1907

    
1908
/**
1909
 * Returns a timezone object for the default timezone.
1910
 *
1911
 * @param bool $check_user
1912
 *   (optional) Whether or not to check for a user-configured timezone.
1913
 *   Defaults to TRUE.
1914
 *
1915
 * @return object
1916
 *   The default timezone for a user, if available, otherwise the site.
1917
 */
1918
function date_default_timezone_object($check_user = TRUE) {
1919
  return timezone_open(date_default_timezone($check_user));
1920
}
1921

    
1922
/**
1923
 * Identifies the number of days in a month for a date.
1924
 */
1925
function date_days_in_month($year, $month) {
1926
  // Pick a day in the middle of the month to avoid timezone shifts.
1927
  $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00';
1928
  $date = new DateObject($datetime);
1929
  if ($date->errors) {
1930
    return FALSE;
1931
  }
1932
  else {
1933
    return $date->format('t');
1934
  }
1935
}
1936

    
1937
/**
1938
 * Identifies the number of days in a year for a date.
1939
 *
1940
 * @param mixed $date
1941
 *   (optional) The current date object, or a date string. Defaults to NULL.
1942
 *
1943
 * @return int
1944
 *   The number of days in the year.
1945
 */
1946
function date_days_in_year($date = NULL) {
1947
  if (empty($date)) {
1948
    $date = date_now();
1949
  }
1950
  elseif (!is_object($date)) {
1951
    $date = new DateObject($date);
1952
  }
1953
  if (is_object($date)) {
1954
    if ($date->format('L')) {
1955
      return 366;
1956
    }
1957
    else {
1958
      return 365;
1959
    }
1960
  }
1961
  return NULL;
1962
}
1963

    
1964
/**
1965
 * Identifies the number of ISO weeks in a year for a date.
1966
 *
1967
 * December 28 is always in the last ISO week of the year.
1968
 *
1969
 * @param mixed $date
1970
 *   (optional) The current date object, or a date string. Defaults to NULL.
1971
 *
1972
 * @return int
1973
 *   The number of ISO weeks in a year.
1974
 */
1975
function date_iso_weeks_in_year($date = NULL) {
1976
  if (empty($date)) {
1977
    $date = date_now();
1978
  }
1979
  elseif (!is_object($date)) {
1980
    $date = new DateObject($date);
1981
  }
1982

    
1983
  if (is_object($date)) {
1984
    date_date_set($date, $date->format('Y'), 12, 28);
1985
    return $date->format('W');
1986
  }
1987
  return NULL;
1988
}
1989

    
1990
/**
1991
 * Returns day of week for a given date (0 = Sunday).
1992
 *
1993
 * @param mixed $date
1994
 *   (optional) A date, default is current local day. Defaults to NULL.
1995
 *
1996
 * @return int
1997
 *   The number of the day in the week.
1998
 */
1999
function date_day_of_week($date = NULL) {
2000
  if (empty($date)) {
2001
    $date = date_now();
2002
  }
2003
  elseif (!is_object($date)) {
2004
    $date = new DateObject($date);
2005
  }
2006

    
2007
  if (is_object($date)) {
2008
    return $date->format('w');
2009
  }
2010
  return NULL;
2011
}
2012

    
2013
/**
2014
 * Returns translated name of the day of week for a given date.
2015
 *
2016
 * @param mixed $date
2017
 *   (optional) A date, default is current local day. Defaults to NULL.
2018
 * @param string $abbr
2019
 *   (optional) Whether to return the abbreviated name for that day.
2020
 *   Defaults to TRUE.
2021
 *
2022
 * @return string
2023
 *   The name of the day in the week for that date.
2024
 */
2025
function date_day_of_week_name($date = NULL, $abbr = TRUE) {
2026
  if (!is_object($date)) {
2027
    $date = new DateObject($date);
2028
  }
2029
  $dow = date_day_of_week($date);
2030
  $days = $abbr ? date_week_days_abbr() : date_week_days();
2031
  return $days[$dow];
2032
}
2033

    
2034
/**
2035
 * Calculates the start and end dates for a calendar week.
2036
 *
2037
 * The dates are adjusted to use the chosen first day of week for this site.
2038
 *
2039
 * @param int $week
2040
 *   The week value.
2041
 * @param int $year
2042
 *   The year value.
2043
 *
2044
 * @return array
2045
 *   A numeric array containing the start and end dates of a week.
2046
 */
2047
function date_week_range($week, $year) {
2048
  if (variable_get('date_api_use_iso8601', FALSE)) {
2049
    return date_iso_week_range($week, $year);
2050
  }
2051
  $min_date = new DateObject($year . '-01-01 00:00:00');
2052
  $min_date->setTimezone(date_default_timezone_object());
2053

    
2054
  // Move to the right week.
2055
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
2056

    
2057
  // Move backwards to the first day of the week.
2058
  $first_day = variable_get('date_first_day', 0);
2059
  $day_wday = date_format($min_date, 'w');
2060
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
2061

    
2062
  // Move forwards to the last day of the week.
2063
  $max_date = clone($min_date);
2064
  date_modify($max_date, '+6 days');
2065

    
2066
  if (date_format($min_date, 'Y') != $year) {
2067
    $min_date = new DateObject($year . '-01-01 00:00:00');
2068
  }
2069
  return array($min_date, $max_date);
2070
}
2071

    
2072
/**
2073
 * Calculates the start and end dates for an ISO week.
2074
 *
2075
 * @param int $week
2076
 *   The week value.
2077
 * @param int $year
2078
 *   The year value.
2079
 *
2080
 * @return array
2081
 *   A numeric array containing the start and end dates of an ISO week.
2082
 */
2083
function date_iso_week_range($week, $year) {
2084
  // Get to the last ISO week of the previous year.
2085
  $min_date = new DateObject(($year - 1) . '-12-28 00:00:00');
2086
  date_timezone_set($min_date, date_default_timezone_object());
2087

    
2088
  // Find the first day of the first ISO week in the year.
2089
  // If it's already a Monday, date_modify won't add a Monday,
2090
  // it will remain the same day. So add a Sunday first, then a Monday.
2091
  date_modify($min_date, '+1 Sunday');
2092
  date_modify($min_date, '+1 Monday');
2093

    
2094
  // Jump ahead to the desired week for the beginning of the week range.
2095
  if ($week > 1) {
2096
    date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
2097
  }
2098

    
2099
  // Move forwards to the last day of the week.
2100
  $max_date = clone($min_date);
2101
  date_modify($max_date, '+6 days');
2102
  return array($min_date, $max_date);
2103
}
2104

    
2105
/**
2106
 * The number of calendar weeks in a year.
2107
 *
2108
 * PHP week functions return the ISO week, not the calendar week.
2109
 *
2110
 * @param int $year
2111
 *   A year value.
2112
 *
2113
 * @return int
2114
 *   Number of calendar weeks in selected year.
2115
 */
2116
function date_weeks_in_year($year) {
2117
  $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC');
2118
  date_modify($date, '-1 day');
2119
  return date_week($date->format('Y-m-d'));
2120
}
2121

    
2122
/**
2123
 * The calendar week number for a date.
2124
 *
2125
 * PHP week functions return the ISO week, not the calendar week.
2126
 *
2127
 * @param string $date
2128
 *   A date string in the format Y-m-d.
2129
 *
2130
 * @return int
2131
 *   The calendar week number.
2132
 */
2133
function date_week($date) {
2134
  $date = substr($date, 0, 10);
2135
  $parts = explode('-', $date);
2136

    
2137
  $date = new DateObject($date . ' 12:00:00', 'UTC');
2138

    
2139
  // If we are using ISO weeks, this is easy.
2140
  if (variable_get('date_api_use_iso8601', FALSE)) {
2141
    return intval($date->format('W'));
2142
  }
2143

    
2144
  $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
2145
  $week = intval($date->format('W'));
2146
  $year_week = intval(date_format($year_date, 'W'));
2147
  $date_year = intval($date->format('o'));
2148

    
2149
  // Remove the leap week if it's present.
2150
  if ($date_year > intval($parts[0])) {
2151
    $last_date = clone($date);
2152
    date_modify($last_date, '-7 days');
2153
    $week = date_format($last_date, 'W') + 1;
2154
  }
2155
  elseif ($date_year < intval($parts[0])) {
2156
    $week = 0;
2157
  }
2158

    
2159
  if ($year_week != 1) {
2160
    $week++;
2161
  }
2162

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

    
2166
  // If it's before the starting day, it's the previous week.
2167
  if (intval($date->format('N')) < $iso_first_day) {
2168
    $week--;
2169
  }
2170

    
2171
  // If the year starts before, it's an extra week at the beginning.
2172
  if (intval(date_format($year_date, 'N')) < $iso_first_day) {
2173
    $week++;
2174
  }
2175

    
2176
  return $week;
2177
}
2178

    
2179
/**
2180
 * Helper function to left pad date parts with zeros.
2181
 *
2182
 * Provided because this is needed so often with dates.
2183
 *
2184
 * @param int $value
2185
 *   The value to pad.
2186
 * @param int $size
2187
 *   (optional) Total size expected, usually 2 or 4. Defaults to 2.
2188
 *
2189
 * @return string
2190
 *   The padded value.
2191
 */
2192
function date_pad($value, $size = 2) {
2193
  return sprintf("%0" . $size . "d", $value);
2194
}
2195

    
2196
/**
2197
 * Determines if the granularity contains a time portion.
2198
 *
2199
 * @param array $granularity
2200
 *   An array of allowed date parts, all others will be removed.
2201
 *
2202
 * @return bool
2203
 *   TRUE if the granularity contains a time portion, FALSE otherwise.
2204
 */
2205
function date_has_time($granularity) {
2206
  if (!is_array($granularity)) {
2207
    $granularity = array();
2208
  }
2209
  $options = array('hour', 'minute', 'second');
2210
  return (bool) count(array_intersect($granularity, $options));
2211
}
2212

    
2213
/**
2214
 * Determines if the granularity contains a date portion.
2215
 *
2216
 * @param array $granularity
2217
 *   An array of allowed date parts, all others will be removed.
2218
 *
2219
 * @return bool
2220
 *   TRUE if the granularity contains a date portion, FALSE otherwise.
2221
 */
2222
function date_has_date($granularity) {
2223
  if (!is_array($granularity)) {
2224
    $granularity = array();
2225
  }
2226
  $options = array('year', 'month', 'day');
2227
  return (bool) count(array_intersect($granularity, $options));
2228
}
2229

    
2230
/**
2231
 * Helper function to get a format for a specific part of a date field.
2232
 *
2233
 * @param string $part
2234
 *   The date field part, either 'time' or 'date'.
2235
 * @param string $format
2236
 *   A date format string.
2237
 *
2238
 * @return string
2239
 *   The date format for the given part.
2240
 */
2241
function date_part_format($part, $format) {
2242
  switch ($part) {
2243
    case 'date':
2244
      return date_limit_format($format, array('year', 'month', 'day'));
2245

    
2246
    case 'time':
2247
      return date_limit_format($format, array('hour', 'minute', 'second'));
2248

    
2249
    default:
2250
      return date_limit_format($format, array($part));
2251
  }
2252
}
2253

    
2254
/**
2255
 * Limits a date format to include only elements from a given granularity array.
2256
 *
2257
 * Example:
2258
 *   date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
2259
 *   returns 'F j, Y'
2260
 *
2261
 * @param string $format
2262
 *   A date format string.
2263
 * @param array $granularity
2264
 *   An array of allowed date parts, all others will be removed.
2265
 *
2266
 * @return string
2267
 *   The format string with all other elements removed.
2268
 */
2269
function date_limit_format($format, $granularity) {
2270
  // Use the advanced drupal_static() pattern to improve performance.
2271
  static $drupal_static_fast;
2272
  if (!isset($drupal_static_fast)) {
2273
    $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__);
2274
  }
2275
  $formats = &$drupal_static_fast['formats'];
2276
  $format_granularity_cid = $format . '|' . implode(',', $granularity);
2277
  if (isset($formats[$format_granularity_cid])) {
2278
    return $formats[$format_granularity_cid];
2279
  }
2280

    
2281
  // If punctuation has been escaped, remove the escaping. Done using strtr()
2282
  // because it is easier than getting the escape character extracted using
2283
  // preg_replace().
2284
  $replace = array(
2285
    '\-' => '-',
2286
    '\:' => ':',
2287
    "\'" => "'",
2288
    '\. ' => ' . ',
2289
    '\,' => ',',
2290
  );
2291
  $format = strtr($format, $replace);
2292

    
2293
  // Get the 'T' out of ISO date formats that don't have both date and time.
2294
  if (!date_has_time($granularity) || !date_has_date($granularity)) {
2295
    $format = str_replace('\T', ' ', $format);
2296
    $format = str_replace('T', ' ', $format);
2297
  }
2298

    
2299
  $regex = array();
2300
  if (!date_has_time($granularity)) {
2301
    $regex[] = '((?<!\\\\)[a|A])';
2302
  }
2303
  // Create regular expressions to remove selected values from string.
2304
  // Use (?<!\\\\) to keep escaped letters from being removed.
2305
  foreach (date_nongranularity($granularity) as $element) {
2306
    switch ($element) {
2307
      case 'year':
2308
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
2309
        break;
2310

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

    
2315
      case 'month':
2316
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
2317
        break;
2318

    
2319
      case 'hour':
2320
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
2321
        break;
2322

    
2323
      case 'minute':
2324
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
2325
        break;
2326

    
2327
      case 'second':
2328
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
2329
        break;
2330

    
2331
      case 'timezone':
2332
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
2333
        break;
2334

    
2335
    }
2336
  }
2337
  // Remove empty parentheses, brackets, pipes.
2338
  $regex[] = '(\(\))';
2339
  $regex[] = '(\[\])';
2340
  $regex[] = '(\|\|)';
2341

    
2342
  // Remove selected values from string.
2343
  $format = trim(preg_replace($regex, array(), $format));
2344
  // Remove orphaned punctuation at the beginning of the string.
2345
  $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
2346
  // Remove orphaned punctuation at the end of the string.
2347
  $format = preg_replace('([\-/,:\']$)', '', $format);
2348
  $format = preg_replace('(\\$)', '', $format);
2349

    
2350
  // Trim any whitespace from the result.
2351
  $format = trim($format);
2352

    
2353
  // After removing the non-desired parts of the format, test if the only things
2354
  // left are escaped, non-date, characters. If so, return nothing.
2355
  // Using S instead of w to pick up non-ASCII characters.
2356
  $test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
2357
  if (empty($test)) {
2358
    $format = '';
2359
  }
2360

    
2361
  // Store the return value in the static array for performance.
2362
  $formats[$format_granularity_cid] = $format;
2363

    
2364
  return $format;
2365
}
2366

    
2367
/**
2368
 * Converts a format to an ordered array of granularity parts.
2369
 *
2370
 * Example:
2371
 *   date_format_order('m/d/Y H:i')
2372
 *   returns
2373
 *     array(
2374
 *       0 => 'month',
2375
 *       1 => 'day',
2376
 *       2 => 'year',
2377
 *       3 => 'hour',
2378
 *       4 => 'minute',
2379
 *     );
2380
 *
2381
 * @param string $format
2382
 *   A date format string.
2383
 *
2384
 * @return array
2385
 *   An array of ordered granularity elements from the given format string.
2386
 */
2387
function date_format_order($format) {
2388
  $order = array();
2389
  if (empty($format)) {
2390
    return $order;
2391
  }
2392

    
2393
  $max = strlen($format);
2394
  for ($i = 0; $i <= $max; $i++) {
2395
    if (!isset($format[$i])) {
2396
      break;
2397
    }
2398
    switch ($format[$i]) {
2399
      case 'd':
2400
      case 'j':
2401
        $order[] = 'day';
2402
        break;
2403

    
2404
      case 'F':
2405
      case 'M':
2406
      case 'm':
2407
      case 'n':
2408
        $order[] = 'month';
2409
        break;
2410

    
2411
      case 'Y':
2412
      case 'y':
2413
        $order[] = 'year';
2414
        break;
2415

    
2416
      case 'g':
2417
      case 'G':
2418
      case 'h':
2419
      case 'H':
2420
        $order[] = 'hour';
2421
        break;
2422

    
2423
      case 'i':
2424
        $order[] = 'minute';
2425
        break;
2426

    
2427
      case 's':
2428
        $order[] = 'second';
2429
        break;
2430
    }
2431
  }
2432
  return $order;
2433
}
2434

    
2435
/**
2436
 * Strips out unwanted granularity elements.
2437
 *
2438
 * @param array $granularity
2439
 *   An array like ('year', 'month', 'day', 'hour', 'minute', 'second');
2440
 *
2441
 * @return array
2442
 *   A reduced set of granularitiy elements.
2443
 */
2444
function date_nongranularity($granularity) {
2445
  $options = array(
2446
    'year',
2447
    'month',
2448
    'day',
2449
    'hour',
2450
    'minute',
2451
    'second',
2452
    'timezone',
2453
  );
2454
  return array_diff($options, (array) $granularity);
2455
}
2456

    
2457
/**
2458
 * Implements hook_element_info().
2459
 */
2460
function date_api_element_info() {
2461
  module_load_include('inc', 'date_api', 'date_api_elements');
2462
  return _date_api_element_info();
2463
}
2464

    
2465
/**
2466
 * Implements hook_theme().
2467
 */
2468
function date_api_theme($existing, $type, $theme, $path) {
2469
  $base = array(
2470
    'file' => 'theme.inc',
2471
    'path' => "$path/theme",
2472
  );
2473
  return array(
2474
    'date_nav_title' => $base + array(
2475
      'variables' => array(
2476
        'granularity' => NULL, 'view' => NULL, 'link' => NULL, 'format' => NULL
2477
      ),
2478
    ),
2479
    'date_timezone' => $base + array('render element' => 'element'),
2480
    'date_select' => $base + array('render element' => 'element'),
2481
    'date_text' => $base + array('render element' => 'element'),
2482
    'date_select_element' => $base + array('render element' => 'element'),
2483
    'date_textfield_element' => $base + array('render element' => 'element'),
2484
    'date_part_hour_prefix' => $base + array('render element' => 'element'),
2485
    'date_part_minsec_prefix' => $base + array('render element' => 'element'),
2486
    'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2487
    'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2488
    'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2489
    'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2490
    'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2491
    'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2492
    'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2493
    'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2494
    'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2495
    'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
2496
    'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'),
2497
    'date_calendar_day' => $base + array('variables' => array('date' => NULL)),
2498
    'date_time_ago' => $base + array(
2499
      'variables' => array(
2500
        'start_date' => NULL, 'end_date' => NULL, 'interval' => NULL
2501
      ),
2502
    ),
2503
  );
2504
}
2505

    
2506
/**
2507
 * Function to figure out which local timezone applies to a date and select it.
2508
 *
2509
 * @param string $handling
2510
 *   The timezone handling.
2511
 * @param string $timezone
2512
 *   (optional) A timezone string. Defaults to an empty string.
2513
 *
2514
 * @return string
2515
 *   The timezone string.
2516
 */
2517
function date_get_timezone($handling, $timezone = '') {
2518
  switch ($handling) {
2519
    case 'date':
2520
      $timezone = !empty($timezone) ? $timezone : date_default_timezone();
2521
      break;
2522

    
2523
    case 'utc':
2524
      $timezone = 'UTC';
2525
      break;
2526

    
2527
    default:
2528
      $timezone = date_default_timezone();
2529
  }
2530
  return $timezone > '' ? $timezone : date_default_timezone();
2531
}
2532

    
2533
/**
2534
 * Function to figure out which db timezone applies to a date.
2535
 *
2536
 * @param string $handling
2537
 *   The timezone handling.
2538
 * @param string $timezone
2539
 *   (optional) When $handling is 'date', date_get_timezone_db() returns this
2540
 *   value.
2541
 *
2542
 * @return string
2543
 *   The timezone string.
2544
 */
2545
function date_get_timezone_db($handling, $timezone = NULL) {
2546
  switch ($handling) {
2547
    case ('utc'):
2548
    case ('site'):
2549
    case ('user'):
2550
      // These handling modes all convert to UTC before storing in the DB.
2551
      $timezone = 'UTC';
2552
      break;
2553

    
2554
    case ('date'):
2555
      if ($timezone == NULL) {
2556
        // This shouldn't happen, since it's meaning is undefined. But we need
2557
        // to fall back to *something* that's a legal timezone.
2558
        $timezone = date_default_timezone();
2559
      }
2560
      break;
2561

    
2562
    case ('none'):
2563
    default:
2564
      $timezone = date_default_timezone();
2565
      break;
2566
  }
2567
  return $timezone;
2568
}
2569

    
2570
/**
2571
 * Helper function for converting back and forth from '+1' to 'First'.
2572
 */
2573
function date_order_translated() {
2574
  return array(
2575
    '+1' => t('First', array(), array('context' => 'date_order')),
2576
    '+2' => t('Second', array(), array('context' => 'date_order')),
2577
    '+3' => t('Third', array(), array('context' => 'date_order')),
2578
    '+4' => t('Fourth', array(), array('context' => 'date_order')),
2579
    '+5' => t('Fifth', array(), array('context' => 'date_order')),
2580
    '-1' => t('Last', array(), array('context' => 'date_order_reverse')),
2581
    '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')),
2582
    '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')),
2583
    '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')),
2584
    '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')),
2585
  );
2586
}
2587

    
2588
/**
2589
 * Creates an array of ordered strings, using English text when possible.
2590
 */
2591
function date_order() {
2592
  return array(
2593
    '+1' => 'First',
2594
    '+2' => 'Second',
2595
    '+3' => 'Third',
2596
    '+4' => 'Fourth',
2597
    '+5' => 'Fifth',
2598
    '-1' => 'Last',
2599
    '-2' => '-2',
2600
    '-3' => '-3',
2601
    '-4' => '-4',
2602
    '-5' => '-5',
2603
  );
2604
}
2605

    
2606
/**
2607
 * Tests validity of a date range string.
2608
 *
2609
 * @param string $string
2610
 *   A min and max year string like '-3:+1'a.
2611
 *
2612
 * @return bool
2613
 *   TRUE if the date range is valid, FALSE otherwise.
2614
 */
2615
function date_range_valid($string) {
2616
  $matches = preg_match('@^([\+\-][0-9]+|[0-9]{4}):([\+\-][0-9]+|[0-9]{4})$@', $string);
2617
  return $matches < 1 ? FALSE : TRUE;
2618
}
2619

    
2620
/**
2621
 * Splits a string like -3:+3 or 2001:2010 into an array of start and end years.
2622
 *
2623
 * Center the range around the current year, if any, but expand it far
2624
 * enough so it will pick up the year value in the field in case
2625
 * the value in the field is outside the initial range.
2626
 *
2627
 * @param string $string
2628
 *   A min and max year string like '-3:+1'.
2629
 * @param object $date
2630
 *   (optional) A date object. Defaults to NULL.
2631
 *
2632
 * @return array
2633
 *   A numerically indexed array, containing a start and end year.
2634
 */
2635
function date_range_years($string, $date = NULL) {
2636
  $this_year = date_format(date_now(), 'Y');
2637
  list($start_year, $end_year) = explode(':', $string);
2638

    
2639
  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
2640
  $plus_pattern = '@[\+\-][0-9]{1,4}@';
2641
  $year_pattern = '@^[0-9]{4}@';
2642
  if (!preg_match($year_pattern, $start_year, $matches)) {
2643
    if (preg_match($plus_pattern, $start_year, $matches)) {
2644
      $start_year = $this_year + $matches[0];
2645
    }
2646
    else {
2647
      $start_year = $this_year;
2648
    }
2649
  }
2650
  if (!preg_match($year_pattern, $end_year, $matches)) {
2651
    if (preg_match($plus_pattern, $end_year, $matches)) {
2652
      $end_year = $this_year + $matches[0];
2653
    }
2654
    else {
2655
      $end_year = $this_year;
2656
    }
2657
  }
2658
  // If there is a current value, stretch the range to include it.
2659
  $value_year = is_object($date) ? $date->format('Y') : '';
2660
  if (!empty($value_year)) {
2661
    if ($start_year <= $end_year) {
2662
      $start_year = min($value_year, $start_year);
2663
      $end_year = max($value_year, $end_year);
2664
    }
2665
    else {
2666
      $start_year = max($value_year, $start_year);
2667
      $end_year = min($value_year, $end_year);
2668
    }
2669
  }
2670
  return array($start_year, $end_year);
2671
}
2672

    
2673
/**
2674
 * Converts a min and max year into a string like '-3:+1'.
2675
 *
2676
 * @param array $years
2677
 *   A numerically indexed array, containing a minimum and maximum year.
2678
 *
2679
 * @return string
2680
 *   A min and max year string like '-3:+1'.
2681
 */
2682
function date_range_string($years) {
2683
  $this_year = date_format(date_now(), 'Y');
2684

    
2685
  if ($years[0] < $this_year) {
2686
    $min = '-' . ($this_year - $years[0]);
2687
  }
2688
  else {
2689
    $min = '+' . ($years[0] - $this_year);
2690
  }
2691

    
2692
  if ($years[1] < $this_year) {
2693
    $max = '-' . ($this_year - $years[1]);
2694
  }
2695
  else {
2696
    $max = '+' . ($years[1] - $this_year);
2697
  }
2698

    
2699
  return $min . ':' . $max;
2700
}
2701

    
2702
/**
2703
 * Temporary helper to re-create equivalent of content_database_info().
2704
 */
2705
function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) {
2706
  return array(
2707
    'columns' => $field['storage']['details']['sql'][$revision],
2708
    'table' => _field_sql_storage_tablename($field),
2709
  );
2710
}
2711

    
2712
/**
2713
 * Implements hook_form_FORM_ID_alter() for system_regional_settings().
2714
 *
2715
 * Add a form element to configure whether or not week numbers are ISO-8601, the
2716
 * default is FALSE (US/UK/AUS norm).
2717
 */
2718
function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) {
2719
  $form['locale']['date_api_use_iso8601'] = array(
2720
    '#type' => 'checkbox',
2721
    '#title' => t('Use ISO-8601 week numbers'),
2722
    '#default_value' => variable_get('date_api_use_iso8601', FALSE),
2723
    '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'),
2724
  );
2725
  $form['#validate'][] = 'date_api_form_system_settings_validate';
2726
}
2727

    
2728
/**
2729
 * Validate that the option to use ISO weeks matches first day of week choice.
2730
 */
2731
function date_api_form_system_settings_validate(&$form, &$form_state) {
2732
  $form_values = $form_state['values'];
2733
  if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) {
2734
    form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.'));
2735
  }
2736
}
2737

    
2738
/**
2739
 * Creates an array of date format types for use as an options list.
2740
 */
2741
function date_format_type_options() {
2742
  $options = array();
2743
  $format_types = system_get_date_types();
2744
  if (!empty($format_types)) {
2745
    foreach ($format_types as $type => $type_info) {
2746
      $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')';
2747
    }
2748
  }
2749
  return $options;
2750
}
2751

    
2752
/**
2753
 * Creates an example date.
2754
 *
2755
 * This ensures a clear difference between month and day, and 12 and 24 hours.
2756
 */
2757
function date_example_date() {
2758
  $now = date_now();
2759
  if (date_format($now, 'M') == date_format($now, 'F')) {
2760
    date_modify($now, '+1 month');
2761
  }
2762
  if (date_format($now, 'm') == date_format($now, 'd')) {
2763
    date_modify($now, '+1 day');
2764
  }
2765
  if (date_format($now, 'H') == date_format($now, 'h')) {
2766
    date_modify($now, '+12 hours');
2767
  }
2768
  return $now;
2769
}
2770

    
2771
/**
2772
 * Determine if a start/end date combination qualify as 'All day'.
2773
 *
2774
 * @param string $string1
2775
 *   A string date in datetime format for the 'start' date.
2776
 * @param string $string2
2777
 *   A string date in datetime format for the 'end' date.
2778
 * @param string $granularity
2779
 *   (optional) The granularity of the date. Defaults to 'second'.
2780
 * @param int $increment
2781
 *   (optional) The increment of the date. Defaults to 1.
2782
 *
2783
 * @return bool
2784
 *   TRUE if the date is all day, FALSE otherwise.
2785
 */
2786
function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) {
2787
  if (empty($string1) || empty($string2)) {
2788
    return FALSE;
2789
  }
2790
  elseif (!in_array($granularity, array('hour', 'minute', 'second'))) {
2791
    return FALSE;
2792
  }
2793

    
2794
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches);
2795
  $count = count($matches);
2796
  $date1 = $count > 1 ? $matches[1] : '';
2797
  $time1 = $count > 2 ? $matches[2] : '';
2798
  $hour1 = $count > 3 ? intval($matches[3]) : 0;
2799
  $min1 = $count > 4 ? intval($matches[4]) : 0;
2800
  $sec1 = $count > 5 ? intval($matches[5]) : 0;
2801
  preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches);
2802
  $count = count($matches);
2803
  $date2 = $count > 1 ? $matches[1] : '';
2804
  $time2 = $count > 2 ? $matches[2] : '';
2805
  $hour2 = $count > 3 ? intval($matches[3]) : 0;
2806
  $min2 = $count > 4 ? intval($matches[4]) : 0;
2807
  $sec2 = $count > 5 ? intval($matches[5]) : 0;
2808
  if (empty($date1) || empty($date2)) {
2809
    return FALSE;
2810
  }
2811
  if (empty($time1) || empty($time2)) {
2812
    return FALSE;
2813
  }
2814

    
2815
  $tmp = date_seconds('s', TRUE, $increment);
2816
  $max_seconds = intval(array_pop($tmp));
2817
  $tmp = date_minutes('i', TRUE, $increment);
2818
  $max_minutes = intval(array_pop($tmp));
2819

    
2820
  // See if minutes and seconds are the maximum allowed for an increment or the
2821
  // maximum possible (59), or 0.
2822
  switch ($granularity) {
2823
    case 'second':
2824
      $min_match = $time1 == '00:00:00'
2825
        || ($hour1 == 0 && $min1 == 0 && $sec1 == 0);
2826
      $max_match = $time2 == '00:00:00'
2827
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59)))
2828
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0);
2829
      break;
2830

    
2831
    case 'minute':
2832
      $min_match = $time1 == '00:00:00'
2833
        || ($hour1 == 0 && $min1 == 0);
2834
      $max_match = $time2 == '00:00:00'
2835
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
2836
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
2837
      break;
2838

    
2839
    case 'hour':
2840
      $min_match = $time1 == '00:00:00'
2841
        || ($hour1 == 0);
2842
      $max_match = $time2 == '00:00:00'
2843
        || ($hour2 == 23)
2844
        || ($hour1 == 0 && $hour2 == 0);
2845
      break;
2846

    
2847
    default:
2848
      $min_match = TRUE;
2849
      $max_match = FALSE;
2850
  }
2851

    
2852
  if ($min_match && $max_match) {
2853
    return TRUE;
2854
  }
2855

    
2856
  return FALSE;
2857
}
2858

    
2859
/**
2860
 * Helper function to round minutes and seconds to requested value.
2861
 */
2862
function date_increment_round(&$date, $increment) {
2863
  // Round minutes and seconds, if necessary.
2864
  if (is_object($date) && $increment > 1) {
2865
    $day = intval(date_format($date, 'j'));
2866
    $hour = intval(date_format($date, 'H'));
2867
    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
2868
    $minute = intval(date_format($date, 'i'));
2869
    if ($second == 60) {
2870
      $minute += 1;
2871
      $second = 0;
2872
    }
2873
    $minute = intval(round($minute / $increment) * $increment);
2874
    if ($minute == 60) {
2875
      $hour += 1;
2876
      $minute = 0;
2877
    }
2878
    date_time_set($date, $hour, $minute, $second);
2879
    if ($hour == 24) {
2880
      $day += 1;
2881
      $hour = 0;
2882
      $year = date_format($date, 'Y');
2883
      $month = date_format($date, 'n');
2884
      date_date_set($date, $year, $month, $day);
2885
    }
2886
  }
2887
  return $date;
2888
}
2889

    
2890
/**
2891
 * Determines if a date object is valid.
2892
 *
2893
 * @param object $date
2894
 *   The date object to check.
2895
 *
2896
 * @return bool
2897
 *   TRUE if the date is a valid date object, FALSE otherwise.
2898
 */
2899
function date_is_date($date) {
2900
  if (empty($date) || !is_object($date) || !empty($date->errors)) {
2901
    return FALSE;
2902
  }
2903
  return TRUE;
2904
}
2905

    
2906
/**
2907
 * Replace specific ISO values using patterns.
2908
 *
2909
 * Function will replace ISO values that have the pattern 9999-00-00T00:00:00
2910
 * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO dates
2911
 * and ensure that date objects created from this value contain a valid month
2912
 * and day.
2913
 * Without this fix, the ISO date '2020-00-00T00:00:00' would be created as
2914
 * November 30, 2019 (the previous day in the previous month).
2915
 *
2916
 * @param string $iso_string
2917
 *   An ISO string that needs to be made into a complete, valid date.
2918
 *
2919
 * @return mixed|string
2920
 *   replaced value, or incoming value.
2921
 *
2922
 * @TODO Expand on this to work with all sorts of partial ISO dates.
2923
 */
2924
function date_make_iso_valid($iso_string) {
2925
  // If this isn't a value that uses an ISO pattern, there is nothing to do.
2926
  if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) {
2927
    return $iso_string;
2928
  }
2929
  // First see if month and day parts are '-00-00'.
2930
  if (substr($iso_string, 4, 6) == '-00-00') {
2931
    return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string);
2932
  }
2933
  // Then see if the day part is '-00'.
2934
  elseif (substr($iso_string, 7, 3) == '-00') {
2935
    return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string);
2936
  }
2937

    
2938
  // Fall through, no changes required.
2939
  return $iso_string;
2940
}