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 @ ee46a8ed

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
  return $date->format('t');
1930
}
1931

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

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

    
1978
  if (is_object($date)) {
1979
    date_date_set($date, $date->format('Y'), 12, 28);
1980
    return $date->format('W');
1981
  }
1982
  return NULL;
1983
}
1984

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

    
2002
  if (is_object($date)) {
2003
    return $date->format('w');
2004
  }
2005
  return NULL;
2006
}
2007

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

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

    
2049
  // Move to the right week.
2050
  date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
2051

    
2052
  // Move backwards to the first day of the week.
2053
  $first_day = variable_get('date_first_day', 0);
2054
  $day_wday = date_format($min_date, 'w');
2055
  date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
2056

    
2057
  // Move forwards to the last day of the week.
2058
  $max_date = clone($min_date);
2059
  date_modify($max_date, '+6 days');
2060

    
2061
  if (date_format($min_date, 'Y') != $year) {
2062
    $min_date = new DateObject($year . '-01-01 00:00:00');
2063
  }
2064
  return array($min_date, $max_date);
2065
}
2066

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

    
2083
  // Find the first day of the first ISO week in the year.
2084
  // If it's already a Monday, date_modify won't add a Monday,
2085
  // it will remain the same day. So add a Sunday first, then a Monday.
2086
  date_modify($min_date, '+1 Sunday');
2087
  date_modify($min_date, '+1 Monday');
2088

    
2089
  // Jump ahead to the desired week for the beginning of the week range.
2090
  if ($week > 1) {
2091
    date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
2092
  }
2093

    
2094
  // Move forwards to the last day of the week.
2095
  $max_date = clone($min_date);
2096
  date_modify($max_date, '+6 days');
2097
  return array($min_date, $max_date);
2098
}
2099

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

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

    
2132
  $date = new DateObject($date . ' 12:00:00', 'UTC');
2133

    
2134
  // If we are using ISO weeks, this is easy.
2135
  if (variable_get('date_api_use_iso8601', FALSE)) {
2136
    return intval($date->format('W'));
2137
  }
2138

    
2139
  $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
2140
  $week = intval($date->format('W'));
2141
  $year_week = intval(date_format($year_date, 'W'));
2142
  $date_year = intval($date->format('o'));
2143

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

    
2154
  if ($year_week != 1) {
2155
    $week++;
2156
  }
2157

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

    
2161
  // If it's before the starting day, it's the previous week.
2162
  if (intval($date->format('N')) < $iso_first_day) {
2163
    $week--;
2164
  }
2165

    
2166
  // If the year starts before, it's an extra week at the beginning.
2167
  if (intval(date_format($year_date, 'N')) < $iso_first_day) {
2168
    $week++;
2169
  }
2170

    
2171
  return $week;
2172
}
2173

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

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

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

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

    
2241
    case 'time':
2242
      return date_limit_format($format, array('hour', 'minute', 'second'));
2243

    
2244
    default:
2245
      return date_limit_format($format, array($part));
2246
  }
2247
}
2248

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

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

    
2288
  // Get the 'T' out of ISO date formats that don't have both date and time.
2289
  if (!date_has_time($granularity) || !date_has_date($granularity)) {
2290
    $format = str_replace('\T', ' ', $format);
2291
    $format = str_replace('T', ' ', $format);
2292
  }
2293

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

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

    
2310
      case 'month':
2311
        $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
2312
        break;
2313

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

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

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

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

    
2330
    }
2331
  }
2332
  // Remove empty parentheses, brackets, pipes.
2333
  $regex[] = '(\(\))';
2334
  $regex[] = '(\[\])';
2335
  $regex[] = '(\|\|)';
2336

    
2337
  // Remove selected values from string.
2338
  $format = trim(preg_replace($regex, array(), $format));
2339
  // Remove orphaned punctuation at the beginning of the string.
2340
  $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
2341
  // Remove orphaned punctuation at the end of the string.
2342
  $format = preg_replace('([\-/,:\']$)', '', $format);
2343
  $format = preg_replace('(\\$)', '', $format);
2344

    
2345
  // Trim any whitespace from the result.
2346
  $format = trim($format);
2347

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

    
2356
  // Store the return value in the static array for performance.
2357
  $formats[$format_granularity_cid] = $format;
2358

    
2359
  return $format;
2360
}
2361

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

    
2388
  $max = strlen($format);
2389
  for ($i = 0; $i <= $max; $i++) {
2390
    if (!isset($format[$i])) {
2391
      break;
2392
    }
2393
    switch ($format[$i]) {
2394
      case 'd':
2395
      case 'j':
2396
        $order[] = 'day';
2397
        break;
2398

    
2399
      case 'F':
2400
      case 'M':
2401
      case 'm':
2402
      case 'n':
2403
        $order[] = 'month';
2404
        break;
2405

    
2406
      case 'Y':
2407
      case 'y':
2408
        $order[] = 'year';
2409
        break;
2410

    
2411
      case 'g':
2412
      case 'G':
2413
      case 'h':
2414
      case 'H':
2415
        $order[] = 'hour';
2416
        break;
2417

    
2418
      case 'i':
2419
        $order[] = 'minute';
2420
        break;
2421

    
2422
      case 's':
2423
        $order[] = 'second';
2424
        break;
2425
    }
2426
  }
2427
  return $order;
2428
}
2429

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

    
2452
/**
2453
 * Implements hook_element_info().
2454
 */
2455
function date_api_element_info() {
2456
  module_load_include('inc', 'date_api', 'date_api_elements');
2457
  return _date_api_element_info();
2458
}
2459

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

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

    
2518
    case 'utc':
2519
      $timezone = 'UTC';
2520
      break;
2521

    
2522
    default:
2523
      $timezone = date_default_timezone();
2524
  }
2525
  return $timezone > '' ? $timezone : date_default_timezone();
2526
}
2527

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

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

    
2557
    case ('none'):
2558
    default:
2559
      $timezone = date_default_timezone();
2560
      break;
2561
  }
2562
  return $timezone;
2563
}
2564

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

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

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

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

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

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

    
2680
  if ($years[0] < $this_year) {
2681
    $min = '-' . ($this_year - $years[0]);
2682
  }
2683
  else {
2684
    $min = '+' . ($years[0] - $this_year);
2685
  }
2686

    
2687
  if ($years[1] < $this_year) {
2688
    $max = '-' . ($this_year - $years[1]);
2689
  }
2690
  else {
2691
    $max = '+' . ($years[1] - $this_year);
2692
  }
2693

    
2694
  return $min . ':' . $max;
2695
}
2696

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

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

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

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

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

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

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

    
2810
  $tmp = date_seconds('s', TRUE, $increment);
2811
  $max_seconds = intval(array_pop($tmp));
2812
  $tmp = date_minutes('i', TRUE, $increment);
2813
  $max_minutes = intval(array_pop($tmp));
2814

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

    
2826
    case 'minute':
2827
      $min_match = $time1 == '00:00:00'
2828
        || ($hour1 == 0 && $min1 == 0);
2829
      $max_match = $time2 == '00:00:00'
2830
        || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
2831
        || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
2832
      break;
2833

    
2834
    case 'hour':
2835
      $min_match = $time1 == '00:00:00'
2836
        || ($hour1 == 0);
2837
      $max_match = $time2 == '00:00:00'
2838
        || ($hour2 == 23)
2839
        || ($hour1 == 0 && $hour2 == 0);
2840
      break;
2841

    
2842
    default:
2843
      $min_match = TRUE;
2844
      $max_match = FALSE;
2845
  }
2846

    
2847
  if ($min_match && $max_match) {
2848
    return TRUE;
2849
  }
2850

    
2851
  return FALSE;
2852
}
2853

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

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

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

    
2933
  // Fall through, no changes required.
2934
  return $iso_string;
2935
}