Date.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. <?php
  2. namespace Jenssegers\Date;
  3. use Carbon\Carbon;
  4. use DateInterval;
  5. use DateTimeZone;
  6. use ReflectionMethod;
  7. use Symfony\Component\Translation\Loader\ArrayLoader;
  8. use Symfony\Component\Translation\Translator;
  9. use Symfony\Component\Translation\TranslatorInterface;
  10. class Date extends Carbon
  11. {
  12. /**
  13. * The Translator implementation.
  14. *
  15. * @var Translator
  16. */
  17. protected static $translator;
  18. /**
  19. * The fallback locale when a locale is not available.
  20. *
  21. * @var string
  22. */
  23. protected static $fallbackLocale = 'en';
  24. /**
  25. * The errors that can occur.
  26. *
  27. * @var array
  28. */
  29. protected static $lastErrors;
  30. /**
  31. * Returns new DateTime object.
  32. *
  33. * @param string $time
  34. * @param string|DateTimeZone $timezone
  35. */
  36. public function __construct($time = null, $timezone = null)
  37. {
  38. if (is_int($time)) {
  39. $timestamp = $time;
  40. $time = null;
  41. } else {
  42. $timestamp = null;
  43. }
  44. parent::__construct($time, $timezone);
  45. if ($timestamp !== null) {
  46. $this->setTimestamp($timestamp);
  47. }
  48. }
  49. /**
  50. * Create and return new Date instance.
  51. *
  52. * @param string $time
  53. * @param string|DateTimeZone $timezone
  54. * @return Date
  55. */
  56. public static function make($time = null, $timezone = null)
  57. {
  58. return static::parse($time, $timezone);
  59. }
  60. /**
  61. * Create a Date instance from a string.
  62. *
  63. * @param string $time
  64. * @param string|DateTimeZone $timezone
  65. * @return Date
  66. */
  67. public static function parse($time = null, $timezone = null)
  68. {
  69. if ($time instanceof Carbon) {
  70. return new static(
  71. $time->toDateTimeString(),
  72. $timezone ?: $time->getTimezone()
  73. );
  74. }
  75. if (!is_int($time)) {
  76. $time = static::translateTimeString($time);
  77. }
  78. return new static($time, $timezone);
  79. }
  80. /**
  81. * @inheritdoc
  82. */
  83. public static function createFromFormat($format, $time, $timezone = null)
  84. {
  85. $time = static::translateTimeString($time);
  86. return parent::createFromFormat($format, $time, $timezone);
  87. }
  88. /**
  89. * Alias for diffForHumans.
  90. *
  91. * @param Date $since
  92. * @param bool $syntax Removes time difference modifiers ago, after, etc
  93. * @param bool $short (Carbon 2 only) displays short format of time units
  94. * @param int $parts (Carbon 2 only) maximum number of parts to display (default value: 1: single unit)
  95. * @param int $options (Carbon 2 only) human diff options
  96. * @return string
  97. */
  98. public function ago($since = null, $syntax = null, $short = false, $parts = 1, $options = null)
  99. {
  100. return $this->diffForHumans($since, $syntax, $short, $parts, $options);
  101. }
  102. /**
  103. * Alias for diffForHumans.
  104. *
  105. * @param Date $since
  106. * @param bool $syntax Removes time difference modifiers ago, after, etc
  107. * @param bool $short (Carbon 2 only) displays short format of time units
  108. * @param int $parts (Carbon 2 only) maximum number of parts to display (default value: 1: single unit)
  109. * @param int $options (Carbon 2 only) human diff options
  110. * @return string
  111. */
  112. public function until($since = null, $syntax = null, $short = false, $parts = 1, $options = null)
  113. {
  114. return $this->ago($since, $syntax, $short, $parts, $options);
  115. }
  116. /**
  117. * @inheritdoc
  118. */
  119. public function format($format)
  120. {
  121. $replace = [];
  122. // Loop all format characters and check if we can translate them.
  123. for ($i = 0; $i < strlen($format); $i++) {
  124. $character = $format[$i];
  125. // Check if we can replace it with a translated version.
  126. if (in_array($character, ['D', 'l', 'F', 'M'])) {
  127. // Check escaped characters.
  128. if ($i > 0 and $format[$i - 1] == '\\') {
  129. continue;
  130. }
  131. switch ($character) {
  132. case 'D':
  133. $key = parent::format('l');
  134. break;
  135. case 'M':
  136. $key = parent::format('F');
  137. break;
  138. default:
  139. $key = parent::format($character);
  140. }
  141. // The original result.
  142. $original = parent::format($character);
  143. // Translate.
  144. $lang = $this->getTranslator();
  145. // For declension support, we need to check if the month is lead by a day number.
  146. // If so, we will use the second translation choice if it is available.
  147. if (in_array($character, ['F', 'M'])) {
  148. $choice = preg_match('#[dj][ .]*$#', substr($format, 0, $i)) ? 1 : 0;
  149. $translated = $lang->transChoice(mb_strtolower($key), $choice);
  150. } else {
  151. $translated = $lang->trans(mb_strtolower($key));
  152. }
  153. // Short notations.
  154. if (in_array($character, ['D', 'M'])) {
  155. $toTranslate = mb_strtolower($original);
  156. $shortTranslated = $lang->trans($toTranslate);
  157. if ($shortTranslated === $toTranslate) {
  158. // use the first 3 characters as short notation
  159. $translated = mb_substr($translated, 0, 3);
  160. } else {
  161. // use translated version
  162. $translated = $shortTranslated;
  163. }
  164. }
  165. // Add to replace list.
  166. if ($translated and $original != $translated) {
  167. $replace[$original] = $translated;
  168. }
  169. }
  170. }
  171. // Replace translations.
  172. if ($replace) {
  173. return str_replace(array_keys($replace), array_values($replace), parent::format($format));
  174. }
  175. return parent::format($format);
  176. }
  177. /**
  178. * Gets the timespan between this date and another date.
  179. *
  180. * @param Date|int $time
  181. * @param string|DateTimeZone $timezone
  182. * @return string
  183. */
  184. public function timespan($time = null, $timezone = null)
  185. {
  186. // Get translator
  187. $lang = $this->getTranslator();
  188. // Create Date instance if needed
  189. if (!$time instanceof static) {
  190. $time = Date::parse($time, $timezone);
  191. }
  192. $units = [
  193. 'y' => 'year',
  194. 'm' => 'month',
  195. 'w' => 'week',
  196. 'd' => 'day',
  197. 'h' => 'hour',
  198. 'i' => 'minute',
  199. 's' => 'second',
  200. ];
  201. // Get DateInterval and cast to array
  202. $interval = (array) $this->diff($time);
  203. // Get weeks
  204. $interval['w'] = (int) ($interval['d'] / 7);
  205. $interval['d'] = $interval['d'] % 7;
  206. // Get ready to build
  207. $str = [];
  208. // Loop all units and build string
  209. foreach ($units as $k => $unit) {
  210. if ($interval[$k]) {
  211. $str[] = $lang->transChoice($unit, $interval[$k], [':count' => $interval[$k]]);
  212. }
  213. }
  214. return implode(', ', $str);
  215. }
  216. /**
  217. * Adds an amount of days, months, years, hours, minutes and seconds to a Date object.
  218. *
  219. * @param DateInterval|string $interval
  220. * @param int $value (only effective if using Carbon 2)
  221. * @param bool|null $overflow (only effective if using Carbon 2)
  222. * @return Date|bool
  223. */
  224. public function add($interval, $value = 1, $overflow = null)
  225. {
  226. if (is_string($interval)) {
  227. // Check for ISO 8601
  228. if (strtoupper(substr($interval, 0, 1)) == 'P') {
  229. $interval = new DateInterval($interval);
  230. } else {
  231. $interval = DateInterval::createFromDateString($interval);
  232. }
  233. }
  234. $method = new ReflectionMethod(parent::class, 'add');
  235. $result = $method->getNumberOfRequiredParameters() === 1
  236. ? parent::add($interval)
  237. : parent::add($interval, $value, $overflow);
  238. return $result ? $this : false;
  239. }
  240. /**
  241. * Subtracts an amount of days, months, years, hours, minutes and seconds from a DateTime object.
  242. *
  243. * @param DateInterval|string $interval
  244. * @param int $value (only effective if using Carbon 2)
  245. * @param bool|null $overflow (only effective if using Carbon 2)
  246. * @return Date|bool
  247. */
  248. public function sub($interval, $value = 1, $overflow = null)
  249. {
  250. if (is_string($interval)) {
  251. // Check for ISO 8601
  252. if (strtoupper(substr($interval, 0, 1)) == 'P') {
  253. $interval = new DateInterval($interval);
  254. } else {
  255. $interval = DateInterval::createFromDateString($interval);
  256. }
  257. }
  258. $method = new ReflectionMethod(parent::class, 'sub');
  259. $result = $method->getNumberOfRequiredParameters() === 1
  260. ? parent::sub($interval)
  261. : parent::sub($interval, $value, $overflow);
  262. return $result ? $this : false;
  263. }
  264. /**
  265. * @inheritdoc
  266. */
  267. public static function getLocale()
  268. {
  269. return static::getTranslator()->getLocale();
  270. }
  271. /**
  272. * @inheritdoc
  273. */
  274. public static function setLocale($locale)
  275. {
  276. // Use RFC 5646 for filenames.
  277. $files = array_unique([
  278. str_replace('_', '-', $locale),
  279. static::getLanguageFromLocale($locale),
  280. str_replace('_', '-', static::getFallbackLocale()),
  281. static::getLanguageFromLocale(static::getFallbackLocale()),
  282. ]);
  283. $found = false;
  284. foreach ($files as $file) {
  285. $resource = __DIR__.'/Lang/'.$file.'.php';
  286. if (file_exists($resource)) {
  287. $found = true;
  288. $locale = $file;
  289. break;
  290. }
  291. }
  292. if (!$found) {
  293. return;
  294. }
  295. // Symfony locale format.
  296. $locale = str_replace('-', '_', $locale);
  297. // Set locale and load translations.
  298. static::getTranslator()->setLocale($locale);
  299. static::getTranslator()->addResource('array', require $resource, $locale);
  300. }
  301. /**
  302. * Set the fallback locale.
  303. *
  304. * @param string $locale
  305. */
  306. public static function setFallbackLocale($locale)
  307. {
  308. static::$fallbackLocale = $locale;
  309. static::getTranslator()->setFallbackLocales([$locale]);
  310. }
  311. /**
  312. * Get the fallback locale.
  313. *
  314. * @return string
  315. */
  316. public static function getFallbackLocale()
  317. {
  318. return static::$fallbackLocale;
  319. }
  320. /**
  321. * @inheritdoc
  322. */
  323. public static function getTranslator()
  324. {
  325. if (static::$translator === null) {
  326. static::$translator = new Translator('en');
  327. static::$translator->addLoader('array', new ArrayLoader());
  328. static::setLocale('en');
  329. }
  330. return static::$translator;
  331. }
  332. /**
  333. * @inheritdoc
  334. */
  335. public static function setTranslator(TranslatorInterface $translator)
  336. {
  337. static::$translator = $translator;
  338. }
  339. /**
  340. * Translate a locale based time string to its english equivalent.
  341. *
  342. * @param string $time
  343. * @return string
  344. */
  345. public static function translateTimeString($time, $from = null, $to = null, $mode = -1)
  346. {
  347. // Don't run translations for english.
  348. if (static::getLocale() == 'en') {
  349. return $time;
  350. }
  351. // All the language file items we can translate.
  352. $keys = [
  353. 'january',
  354. 'february',
  355. 'march',
  356. 'april',
  357. 'may',
  358. 'june',
  359. 'july',
  360. 'august',
  361. 'september',
  362. 'october',
  363. 'november',
  364. 'december',
  365. 'monday',
  366. 'tuesday',
  367. 'wednesday',
  368. 'thursday',
  369. 'friday',
  370. 'saturday',
  371. 'sunday',
  372. ];
  373. // Get all the language lines of the current locale.
  374. $all = static::getTranslator()->getCatalogue()->all();
  375. $terms = array_intersect_key($all['messages'], array_flip((array) $keys));
  376. // Split terms with a | sign.
  377. foreach ($terms as $i => $term) {
  378. if (strpos($term, '|') === false) {
  379. continue;
  380. }
  381. // Split term options.
  382. $options = explode('|', $term);
  383. // Remove :count and {count} placeholders.
  384. $options = array_map(function ($option) {
  385. $option = trim(str_replace(':count', '', $option));
  386. $option = preg_replace('/({\d+(,(\d+|Inf))?}|\[\d+(,(\d+|Inf))?\])/', '', $option);
  387. return $option;
  388. }, $options);
  389. $terms[$i] = $options;
  390. }
  391. // Replace the localized words with English words.
  392. $translated = $time;
  393. foreach ($terms as $english => $localized) {
  394. $translated = str_ireplace($localized, $english, $translated);
  395. }
  396. return $translated;
  397. }
  398. /**
  399. * Get the language portion of the locale.
  400. *
  401. * @param string $locale
  402. * @return string
  403. */
  404. public static function getLanguageFromLocale($locale)
  405. {
  406. $parts = explode('_', str_replace('-', '_', $locale));
  407. return $parts[0];
  408. }
  409. }