PageRenderTime 23ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/library/Vanilla/Formatting/DateTimeFormatter.php

https://github.com/vanilla/vanilla
PHP | 419 lines | 239 code | 45 blank | 135 comment | 32 complexity | 8c7f6e09733c9c733794cc5160d82335 MD5 | raw file
Possible License(s): GPL-2.0, MIT, AGPL-1.0
  1. <?php
  2. /**
  3. * @author Adam Charron <adam.c@vanillaforums.com>
  4. * @copyright 2009-2019 Vanilla Forums Inc.
  5. * @license GPL-2.0-only
  6. */
  7. namespace Vanilla\Formatting;
  8. use Garden\StaticCacheTranslationTrait;
  9. use Vanilla\CurrentTimeStamp;
  10. /**
  11. * Formatting methods related to dates & times.
  12. */
  13. class DateTimeFormatter {
  14. use StaticCacheTranslationTrait;
  15. const NULL_TIMESTAMP_DEFALT_VALUE = '-';
  16. const FORCE_FULL_FORMAT = 'force-full-datetime-format';
  17. /** @var DateConfig */
  18. private $dateConfig;
  19. /**
  20. * @param DateConfig $dateConfig
  21. */
  22. public function __construct(DateConfig $dateConfig) {
  23. $this->dateConfig = $dateConfig;
  24. }
  25. /**
  26. * Format a MySQL DateTime string in the specified format.
  27. *
  28. * @link http://us.php.net/manual/en/function.strftime.php
  29. *
  30. * @param string|int $timestamp A timestamp or string in Mysql DateTime format. ie. YYYY-MM-DD HH:MM:SS
  31. * @param bool $isHtml Whether or not to output this as an HTML string.
  32. * @param string $format The format string to use. Defaults to the application's default format.
  33. * @return string
  34. */
  35. public function formatDate($timestamp = '', bool $isHtml = false, string $format = ''): string {
  36. // Was a mysqldatetime passed?
  37. if ($timestamp !== null && !is_numeric($timestamp)) {
  38. $timestamp = self::dateTimeToTimeStamp($timestamp);
  39. }
  40. if ($timestamp === null) {
  41. return self::t('Null Date', self::NULL_TIMESTAMP_DEFALT_VALUE);
  42. }
  43. $gmTimestamp = $timestamp;
  44. $timestamp = $this->adjustTimeStampForUser($timestamp);
  45. if ($format === '') {
  46. $format = $this->getDefaultFormatForTimestamp($timestamp);
  47. } elseif ($format === self::FORCE_FULL_FORMAT) {
  48. $format = $this->dateConfig->getDefaultDateTimeFormat();
  49. $format = $this->normalizeFormatForTimeStamp($format, $timestamp);
  50. }
  51. $result = strftime($format, $timestamp);
  52. if ($isHtml) {
  53. $fullFormat = $this->dateConfig->getDefaultDateTimeFormat();
  54. $fullFormat = $this->normalizeFormatForTimeStamp($fullFormat, $timestamp);
  55. $result = wrap(
  56. $result,
  57. 'time',
  58. [
  59. 'title' => strftime($fullFormat, $timestamp),
  60. 'datetime' => gmdate('c', $gmTimestamp)
  61. ]
  62. );
  63. }
  64. return $result;
  65. }
  66. /**
  67. * Show times relative to now, e.g. "4 hours ago".
  68. *
  69. * Credit goes to: http://byteinn.com/res/426/Fuzzy_Time_function/
  70. *
  71. * @param int|string|null $timestamp otherwise time() is used
  72. * @return string
  73. */
  74. public function formatRelativeTime($timestamp = null): string {
  75. if (is_null($timestamp)) {
  76. $timestamp = $this->getNowTimeStamp();
  77. } elseif (!is_numeric($timestamp)) {
  78. $timestamp = self::dateTimeToTimeStamp($timestamp);
  79. }
  80. $time = $timestamp;
  81. $now = $this->getNowTimeStamp();
  82. $secondsAgo = $now - $time;
  83. // sod = start of day :)
  84. $sod = mktime(0, 0, 0, date('m', $time), date('d', $time), date('Y', $time));
  85. $sod_now = mktime(0, 0, 0, date('m', $now), date('d', $now), date('Y', $now));
  86. // Today
  87. if ($sod_now == $sod) {
  88. if ($time > $now - (TimeUnit::ONE_MINUTE * 3)) {
  89. return self::t('just now');
  90. } elseif ($time > $now - (TimeUnit::ONE_MINUTE * 7)) {
  91. return self::t('a few minutes ago');
  92. } elseif ($time > $now - (TimeUnit::ONE_MINUTE * 30)) {
  93. $minutesAgo = ceil($secondsAgo / 60);
  94. return sprintf(self::t('%s minutes ago'), $minutesAgo);
  95. } elseif ($time > $now - (TimeUnit::ONE_HOUR)) {
  96. return self::t('less than an hour ago');
  97. }
  98. return sprintf(self::t('today at %s'), date('g:ia', $time));
  99. }
  100. // Yesterday
  101. if (($sod_now - $sod) <= TimeUnit::ONE_DAY) {
  102. if (date('i', $time) > (TimeUnit::ONE_MINUTE + 30)) {
  103. $time += TimeUnit::ONE_HOUR / 2;
  104. }
  105. return sprintf(self::t('yesterday around %s'), date('ga', $time));
  106. }
  107. // Within the last 5 days.
  108. if (($sod_now - $sod) <= (TimeUnit::ONE_DAY * 5)) {
  109. $str = date('l', $time);
  110. $hour = date('G', $time);
  111. if ($hour < 12) {
  112. $str .= self::t(' morning');
  113. } elseif ($hour < 17) {
  114. $str .= self::t(' afternoon');
  115. } elseif ($hour < 20) {
  116. $str .= self::t(' evening');
  117. } else {
  118. $str .= self::t(' night');
  119. }
  120. return $str;
  121. }
  122. // Number of weeks (between 1 and 3).
  123. if (($sod_now - $sod) < (TimeUnit::ONE_WEEK * 3.5)) {
  124. if (($sod_now - $sod) < TimeUnit::ONE_WEEK) {
  125. return self::t('about a week ago');
  126. } elseif (($sod_now - $sod) < (TimeUnit::ONE_WEEK * 2)) {
  127. return self::t('about two weeks ago');
  128. } else {
  129. return self::t('about three weeks ago');
  130. }
  131. }
  132. // Number of months (between 1 and 11).
  133. if (($sod_now - $sod) < (TimeUnit::ONE_MONTH * 11.5)) {
  134. for ($i = (TimeUnit::ONE_WEEK * 3.5), $m = 0; $i < TimeUnit::ONE_YEAR; $i += TimeUnit::ONE_MONTH, $m++) {
  135. if (($sod_now - $sod) <= $i) {
  136. return sprintf(
  137. self::t('about %s month%s ago'),
  138. $this->spell1To11($m),
  139. (($m > 1) ? 's' : '')
  140. );
  141. }
  142. }
  143. }
  144. // Number of years.
  145. for ($i = (TimeUnit::ONE_MONTH * 11.5), $y = 0; $i < (TimeUnit::ONE_YEAR * 10); $i += TimeUnit::ONE_YEAR, $y++) {
  146. if (($sod_now - $sod) <= $i) {
  147. return sprintf(
  148. self::t('about %s year%s ago'),
  149. $this->spell1To11($y),
  150. (($y > 1) ? 's' : '')
  151. );
  152. }
  153. }
  154. // More than ten years.
  155. return self::t('more than ten years ago');
  156. }
  157. /**
  158. * Formats seconds in a human-readable way
  159. * (ie. 45 seconds, 15 minutes, 2 hours, 4 days, 2 months, etc).
  160. *
  161. * @param int $seconds
  162. * @return string
  163. */
  164. public function formatSeconds(int $seconds): string {
  165. $minutes = round($seconds / TimeUnit::ONE_MINUTE);
  166. $hours = round($seconds / TimeUnit::ONE_HOUR);
  167. $days = round($seconds / TimeUnit::ONE_DAY);
  168. $weeks = round($seconds / TimeUnit::ONE_WEEK);
  169. $months = round($seconds / TimeUnit::ONE_MONTH);
  170. $years = round($seconds / TimeUnit::ONE_YEAR);
  171. if ($seconds < 60) {
  172. return sprintf(plural($seconds, '%s second', '%s seconds'), $seconds);
  173. } elseif ($minutes < 60) {
  174. return sprintf(plural($minutes, '%s minute', '%s minutes'), $minutes);
  175. } elseif ($hours < 24) {
  176. return sprintf(plural($hours, '%s hour', '%s hours'), $hours);
  177. } elseif ($days < 7) {
  178. return sprintf(plural($days, '%s day', '%s days'), $days);
  179. } elseif ($weeks < 4) {
  180. return sprintf(plural($weeks, '%s week', '%s weeks'), $weeks);
  181. } elseif ($months < 12) {
  182. return sprintf(plural($months, '%s month', '%s months'), $months);
  183. } else {
  184. return sprintf(plural($years, '%s year', '%s years'), $years);
  185. }
  186. }
  187. /**
  188. * Convert a datetime to a timestamp.
  189. *
  190. * @param string $dateTime The Mysql-formatted datetime to convert to a timestamp. Should be in one
  191. * of the following formats: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS.
  192. * @param mixed $fallback The value to return if the value couldn't be properly converted.
  193. * @param mixed $emptyFallback The fallback for an empty value. If not supplied then the `$fallback` will be used.
  194. * @return int|null A timestamp or now if it couldn't be parsed properly.
  195. */
  196. public static function dateTimeToTimeStamp(?string $dateTime, $fallback = false, $emptyFallback = false): ?int {
  197. if (empty($dateTime)) {
  198. $emptyFallback = $emptyFallback !== false ? $emptyFallback : $fallback;
  199. return $emptyFallback !== false ? $emptyFallback : time();
  200. } elseif (($testTime = strtotime($dateTime)) !== false) {
  201. return $testTime;
  202. } else {
  203. $fallback = $fallback !== false ? $fallback : time();
  204. trigger_error(__FUNCTION__ . 'called with bad input ' . $dateTime, E_USER_NOTICE);
  205. return $fallback;
  206. }
  207. }
  208. /**
  209. * Convert a timestamp into human readable seconds from now.
  210. *
  211. * @see DateTimeFormatter::formatSeconds()
  212. *
  213. * @param string $datetime The time to convert.
  214. * @param int|null $from What time to be relative to.
  215. * @return int
  216. */
  217. public static function dateTimeToSecondsAgo($datetime, $from = null): int {
  218. $from = $from ?? time();
  219. return abs($from - self::dateTimeToTimeStamp($datetime));
  220. }
  221. /**
  222. * Convert a timetstamp to time formatted as H::MM::SS (g:i:s).
  223. *
  224. * @param int $timestamp The timestamp to use.
  225. *
  226. * @return string The formatted value.
  227. */
  228. public static function timeStampToTime(int $timestamp): string {
  229. return date('g:i:s', $timestamp);
  230. }
  231. /**
  232. * Convert a timetstamp to date formatted as D-m-d
  233. *
  234. * @param int $timestamp The timestamp to use.
  235. *
  236. * @return string The formatted value.
  237. */
  238. public static function timeStampToDate(int $timestamp): string {
  239. return date('Y-m-d', $timestamp);
  240. }
  241. /**
  242. * Convert a timetstamp to datetime formatted as Y-m-d H:i:s.
  243. *
  244. * @param int $timestamp The timestamp to use.
  245. *
  246. * @return string The formatted value.
  247. */
  248. public static function timeStampToDateTime(int $timestamp): string {
  249. return date('Y-m-d H:i:s', $timestamp);
  250. }
  251. /**
  252. * Get the current time formatted as time string.
  253. *
  254. * @return string
  255. */
  256. public static function getCurrentDateTime(): string {
  257. return self::timeStampToDateTime(CurrentTimeStamp::get());
  258. }
  259. /**
  260. * Adjust a timestamp for the sessioned user's time offset.
  261. *
  262. * @param int $timestamp
  263. * @return int
  264. */
  265. public function adjustTimeStampForUser(int $timestamp): int {
  266. $hourOffset = $this->dateConfig->getHourOffset();
  267. $secondsOffset = $hourOffset * 3600;
  268. $timestamp += $secondsOffset;
  269. return $timestamp;
  270. }
  271. /** @var null|int */
  272. private $nowTimeStamp = null;
  273. /**
  274. * Get the current time while allowing it to be stubbed for tests.
  275. *
  276. * @return int|null
  277. * @internal Tests only!!!
  278. */
  279. private function getNowTimeStamp(): int {
  280. if ($this->nowTimeStamp === null) {
  281. return time();
  282. }
  283. return $this->nowTimeStamp;
  284. }
  285. /**
  286. * @param int|null $nowTimeStamp
  287. */
  288. public function setNowTimeStamp(?int $nowTimeStamp): void {
  289. $this->nowTimeStamp = $nowTimeStamp;
  290. }
  291. /**
  292. * Get the current timestamp adjusted for the user's hour offset.
  293. *
  294. * @return int
  295. */
  296. private function getUserNowTimeStamp(): int {
  297. $now = $this->getNowTimeStamp();
  298. return $this->adjustTimeStampForUser($now);
  299. }
  300. /**
  301. * Get a relative date format based on how old a timestamp is.
  302. *
  303. * @param int $timestamp
  304. * @return string The format.
  305. */
  306. private function getDefaultFormatForTimestamp(int $timestamp): string {
  307. $now = $this->getUserNowTimeStamp();
  308. // If the timestamp was during the current day
  309. if (date('Y m d', $timestamp) === date('Y m d', $now)) {
  310. // Use the time format
  311. $format = $this->dateConfig->getDefaultTimeFormat();
  312. } elseif (date('Y', $timestamp) === date('Y', $now)) {
  313. // If the timestamp is the same year, show the month and date
  314. $format = $this->dateConfig->getDefaultDayFormat();
  315. } else {
  316. // If the timestamp is not the same year, just show the year
  317. $format = $this->dateConfig->getDefaultYearFormat();
  318. }
  319. $format = $this->normalizeFormatForTimeStamp($format, $timestamp);
  320. return $format;
  321. }
  322. /**
  323. * Normalize a date format by emulating %l and %e for Windows for a given timestamp.
  324. *
  325. * @param string $format The format to normalize.
  326. * @param int $timestamp The timestamp to normalize for.
  327. *
  328. * @return string
  329. */
  330. private function normalizeFormatForTimeStamp(string $format, int $timestamp): string {
  331. if (strpos($format, '%l') !== false) {
  332. $format = str_replace('%l', ltrim(strftime('%I', $timestamp), '0'), $format);
  333. }
  334. if (strpos($format, '%e') !== false) {
  335. $format = str_replace('%e', ltrim(strftime('%d', $timestamp), '0'), $format);
  336. }
  337. return $format;
  338. }
  339. /**
  340. * Spell out a number with localization between 1 and 11.
  341. *
  342. * @param int $num
  343. * @return string
  344. */
  345. public function spell1To11(int $num): string {
  346. switch ($num) {
  347. case 0:
  348. case 1:
  349. return self::t('a');
  350. case 2:
  351. return self::t('two');
  352. case 3:
  353. return self::t('three');
  354. case 4:
  355. return self::t('four');
  356. case 5:
  357. return self::t('five');
  358. case 6:
  359. return self::t('six');
  360. case 7:
  361. return self::t('seven');
  362. case 8:
  363. return self::t('eight');
  364. case 9:
  365. return self::t('nine');
  366. case 10:
  367. return self::t('ten');
  368. case 11:
  369. return self::t('eleven');
  370. default:
  371. return (string) $num;
  372. }
  373. }
  374. }