lib_date.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * Author: Alexandre Alapetite https://alexandre.alapetite.fr
  5. * 2014-06-01
  6. * License: GNU AGPLv3 http://www.gnu.org/licenses/agpl-3.0.html
  7. *
  8. * Parser of ISO 8601 time intervals http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
  9. * Examples: "2014-02/2014-04", "2014-02/04", "2014-06", "P1M"
  10. */
  11. /*
  12. example('2014-03');
  13. example('201403');
  14. example('2014-03-30');
  15. example('2014-05-30T13');
  16. example('2014-05-30T13:30');
  17. example('2014-02/2014-04');
  18. example('2014-02--2014-04');
  19. example('2014-02/04');
  20. example('2014-02-03/05');
  21. example('2014-02-03T22:00/22:15');
  22. example('2014-02-03T22:00/15');
  23. example('2014-03/');
  24. example('/2014-03');
  25. example('2014-03/P1W');
  26. example('P1W/2014-05-25T23:59:59');
  27. example('P1Y/');
  28. example('P1Y');
  29. example('P2M/');
  30. example('P3W/');
  31. example('P4D/');
  32. example('PT5H/');
  33. example('PT6M/');
  34. example('PT7S/');
  35. example('P1DT1H/');
  36. function example(string $dateInterval): void {
  37. $dateIntervalArray = parseDateInterval($dateInterval);
  38. echo $dateInterval, "\t=>\t",
  39. $dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
  40. $dateIntervalArray[1] == null ? 'null' : @date('c', $dateIntervalArray[1]), "\n";
  41. }
  42. */
  43. function _dateFloor(string $isoDate): string {
  44. $x = explode('T', $isoDate, 2);
  45. $t = isset($x[1]) ? $x[1] : '';
  46. $timezone = '';
  47. if (preg_match('/([+-]\d{4}|Z)$/i', $t, $matches)) {
  48. $timezone = $matches[1];
  49. $t = substr($t, 0, -strlen($timezone));
  50. }
  51. $t = str_pad($t, 6, '0') . $timezone;
  52. return str_pad($x[0], 8, '01') . 'T' . $t;
  53. }
  54. function _dateCeiling(string $isoDate): string {
  55. $x = explode('T', $isoDate, 2);
  56. $t = isset($x[1]) ? $x[1] : '';
  57. $timezone = '';
  58. if (preg_match('/([+-]\d{4}|Z)$/i', $t, $matches)) {
  59. $timezone = $matches[1];
  60. $t = substr($t, 0, -strlen($timezone));
  61. }
  62. $t = strlen($t) > 1 ? str_pad($t, 6, '59') . $timezone : '235959' . $timezone;
  63. switch (strlen($x[0])) {
  64. case 4:
  65. return $x[0] . '1231T' . $t;
  66. case 6:
  67. $d = @strtotime($x[0] . '01');
  68. if ($d != false) {
  69. return $x[0] . date('t', $d) . 'T' . $t;
  70. }
  71. }
  72. return $x[0] . 'T' . $t;
  73. }
  74. /** @phpstan-return ($isoDate is null ? null : ($isoDate is '' ? null : string)) */
  75. function _noDelimit(?string $isoDate): ?string {
  76. if ($isoDate === null || $isoDate === '') {
  77. return null;
  78. }
  79. $timezone = '';
  80. if (preg_match('/([+-]\d{2}:?\d{2}|Z)$/i', $isoDate, $matches)) {
  81. $timezone = $matches[1];
  82. $isoDate = substr($isoDate, 0, -strlen($timezone));
  83. }
  84. return str_replace(['-', ':'], '', $isoDate) . str_replace(':', '', $timezone);
  85. }
  86. function _dateRelative(?string $d1, ?string $d2): ?string {
  87. if ($d2 === null) {
  88. return $d1 !== null && $d1[0] !== 'P' ? $d1 : null;
  89. }
  90. if ($d2 !== '' && $d2[0] !== 'P' && $d1 !== null && $d1[0] !== 'P') {
  91. $y2 = substr($d2, 0, 4);
  92. if (strlen($y2) < 4 || !ctype_digit($y2)) { //Does not start by a year
  93. $d2 = _noDelimit($d2);
  94. return substr($d1, 0, -strlen($d2)) . $d2; //Add prefix from $d1
  95. }
  96. }
  97. return _noDelimit($d2);
  98. }
  99. /**
  100. * Parameter $dateInterval is a string containing an ISO 8601 time interval.
  101. * @return array{int|null|false,int|null|false} an array with the minimum and maximum Unix timestamp of this interval,
  102. * or null if open interval, or false if error.
  103. */
  104. function parseDateInterval(string $dateInterval): array {
  105. $dateInterval = trim($dateInterval);
  106. $dateInterval = str_replace(['--', ' '], ['/', 'T'], $dateInterval);
  107. $dateInterval = strtoupper($dateInterval);
  108. $min = null;
  109. $max = null;
  110. $x = explode('/', $dateInterval, 2);
  111. $d1 = _noDelimit($x[0]);
  112. $d2 = _dateRelative($d1, count($x) > 1 ? $x[1] : null);
  113. if ($d1 !== null && $d1[0] !== 'P') {
  114. $min = @strtotime(_dateFloor($d1));
  115. }
  116. if ($d2 !== null) {
  117. if ($d2[0] === 'P') {
  118. try {
  119. $di2 = new DateInterval($d2);
  120. $dt1 = @date_create(); //new DateTime() would create an Exception if the default time zone is not defined
  121. if ($dt1 === false) {
  122. $max = false;
  123. } else {
  124. if ($min !== null && $min !== false) {
  125. $dt1->setTimestamp($min);
  126. }
  127. $max = $dt1->add($di2)->getTimestamp() - 1;
  128. }
  129. } catch (Exception $e) {
  130. $max = false;
  131. }
  132. } elseif ($d1 === null || $d1[0] !== 'P') {
  133. $max = @strtotime(_dateCeiling($d2));
  134. } else {
  135. $max = @strtotime($d2);
  136. }
  137. }
  138. if ($d1 !== null && $d1[0] === 'P') {
  139. try {
  140. $di1 = new DateInterval($d1);
  141. $dt2 = @date_create();
  142. if ($dt2 === false) {
  143. $min = false;
  144. } else {
  145. if ($max !== null && $max !== false) {
  146. $dt2->setTimestamp($max);
  147. }
  148. $min = $dt2->sub($di1)->getTimestamp() + 1;
  149. }
  150. } catch (Exception $e) {
  151. $min = false;
  152. }
  153. }
  154. return [$min, $max];
  155. }