DateTimeHelper.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. namespace fphammerle\helpers;
  3. class DateTimeHelper
  4. {
  5. const ISO8601_DATE_FORMAT = 'Y-m-d';
  6. const ISO8601_TIME_FORMAT = 'H:i:s';
  7. const _timezone_iso_pattern = '(?P<tz>Z|[\+-]\d{2}.\d{2})';
  8. /**
  9. * @param integer|null $timestamp unix timestamp
  10. * @return \DateTime|null
  11. */
  12. public static function timestampToDateTime($timestamp)
  13. {
  14. if($timestamp === null) {
  15. return null;
  16. } elseif(is_int($timestamp)) {
  17. $dt = new \DateTime();
  18. $dt->setTimestamp($timestamp);
  19. return $dt;
  20. } else {
  21. throw new \InvalidArgumentException('expected integer or null');
  22. }
  23. }
  24. /**
  25. * @param string|null $text
  26. * @return \DatePeriod|\DateInterval|null
  27. */
  28. public static function parse($text)
  29. {
  30. if($text === null) {
  31. return null;
  32. } elseif(preg_match(
  33. '/^(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})'
  34. .'([ T](?P<h>\d{2}):(?P<i>\d{2})(:(?P<s>\d{2}))?)?'
  35. . '(' . self::_timezone_iso_pattern . ')?$/',
  36. $text,
  37. $attr
  38. )) {
  39. $start = new \DateTime($text);
  40. if(!empty($attr['s'])) {
  41. $interval = new \DateInterval('PT1S');
  42. } elseif(!empty($attr['i'])) {
  43. $interval = new \DateInterval('PT1M');
  44. } else {
  45. $interval = new \DateInterval('P1D');
  46. }
  47. return new \DatePeriod($start, $interval, 0);
  48. } elseif(preg_match('/^\d{4}-(?P<m>\d{2})(( (?=-)| ?(?!-))' . self::_timezone_iso_pattern . ')?$/', $text, $attr)) {
  49. return new \DatePeriod(
  50. new \DateTime($text),
  51. new \DateInterval('P1M'),
  52. 0
  53. );
  54. } elseif(preg_match('/^(?P<y>\d{4})( ?' . self::_timezone_iso_pattern . ')?$/', $text, $attr)) {
  55. return new \DatePeriod(
  56. new \DateTime(sprintf(
  57. '%s-01-01 %s',
  58. $attr['y'],
  59. isset($attr['tz']) ? $attr['tz'] : ''
  60. )),
  61. new \DateInterval('P1Y'),
  62. 0
  63. );
  64. } else {
  65. try {
  66. return new \DateInterval($text);
  67. } catch(\Exception $ex) {
  68. throw new \InvalidArgumentException(
  69. sprintf("could not parse string '%s'", $text)
  70. );
  71. }
  72. }
  73. }
  74. /**
  75. * @param string|null $text
  76. * @return \DateTime|null
  77. */
  78. public static function parseGetStart($text)
  79. {
  80. $period = self::parse($text);
  81. if($period instanceof \DateInterval) {
  82. throw new \InvalidArgumentException(
  83. sprintf("'%s' defines a duration and does not provide any start date", $text)
  84. );
  85. }
  86. if($period) {
  87. return $period->start;
  88. } else {
  89. return null;
  90. }
  91. }
  92. public static function deinvertInterval(\DateInterval $source = null)
  93. {
  94. // \DateInterval does not implement clone.
  95. // @see https://bugs.php.net/bug.php?id=50559
  96. $result = unserialize(serialize($source));
  97. if($result->invert) {
  98. $result->y *= -1;
  99. $result->m *= -1;
  100. $result->d *= -1;
  101. $result->h *= -1;
  102. $result->i *= -1;
  103. $result->s *= -1;
  104. $result->invert = 0;
  105. }
  106. return $result;
  107. }
  108. /**
  109. * @param \DateInterval|\DatePeriod|null $i
  110. * @return string|null
  111. */
  112. public static function iso($i)
  113. {
  114. if(is_null($i)) {
  115. return null;
  116. } elseif(sizeof(get_object_vars($i)) == 0) {
  117. throw new \InvalidArgumentException(
  118. sprintf("given instance is invalid\n%s", print_r($i, true))
  119. );
  120. } elseif($i instanceof \DateTime) {
  121. return $i->format(\DateTime::ATOM);
  122. } elseif($i instanceof \DateInterval) {
  123. $i = self::deinvertInterval($i);
  124. if($i->y < 0 || $i->m < 0 || $i->d < 0 || $i->h < 0 || $i->i < 0 || $i->s < 0) {
  125. throw new \InvalidArgumentException(
  126. sprintf("negative intervals are not supported\n%s", print_r($i, true))
  127. );
  128. } else {
  129. return StringHelper::prepend('P', StringHelper::unite([
  130. StringHelper::append($i->y ?: null, 'Y'),
  131. StringHelper::append($i->m ?: null, 'M'),
  132. StringHelper::append($i->d ?: null, 'D'),
  133. StringHelper::prepend('T', StringHelper::unite([
  134. StringHelper::append($i->h ?: null, 'H'),
  135. StringHelper::append($i->i ?: null, 'M'),
  136. StringHelper::append($i->s ?: null, 'S'),
  137. ])),
  138. ])) ?: 'P0S';
  139. }
  140. } elseif($i instanceof \DatePeriod) {
  141. // Cave:
  142. // (new \DatePeriod(
  143. // new \DateTime('2016-08-05T14:50:14Z'),
  144. // new \DateInterval('P1D'),
  145. // -1
  146. // )->recurrences == 1
  147. if($i->recurrences <= 0) {
  148. throw new \Exception(
  149. 'conversion of periods with number of occurances'
  150. . ' being negative is not supported'
  151. );
  152. }
  153. $repetitions = -1;
  154. foreach($i as $dt) {
  155. $repetitions++;
  156. // printf("%d. %s\n", $repetitions, $dt->format(\DateTime::ATOM));
  157. }
  158. // \DatePeriod::getStartDate() is available from php 5.6.5.
  159. $start_iso = self::iso($i->start);
  160. // \DatePeriod::getDateInterval() is available from php 5.6.5.
  161. // \DatePeriod::$interval returned an invalid \DatePeriod instance
  162. // in php 7.0.8
  163. $interval_iso = self::iso(get_object_vars($i)['interval']);
  164. switch($repetitions) {
  165. case -1:
  166. // no valid date within period
  167. // e.g. new \DatePeriod(
  168. // new \DateTime('2016-08-05T14:50:14+08:00'),
  169. // new \DateInterval('PT1S'),
  170. // new \DateTime('2016-08-05T14:50:14+08:00')
  171. // )
  172. throw new \InvalidArgumentException(
  173. 'given period does not contain any valid date'
  174. );
  175. case 0:
  176. return sprintf('%s/%s', $start_iso, $interval_iso);
  177. default:
  178. return sprintf('R%d/%s/%s', $repetitions, $start_iso, $interval_iso);
  179. }
  180. } else {
  181. throw new \InvalidArgumentException(sprintf(
  182. "expected \\DateTime, \\DateInterval or \\DatePeriod\n%s",
  183. print_r($i, true)
  184. ));
  185. }
  186. }
  187. }