Date.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Shared;
  3. use DateTimeInterface;
  4. use DateTimeZone;
  5. use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
  6. use PhpOffice\PhpSpreadsheet\Calculation\Functions;
  7. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  8. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  9. class Date
  10. {
  11. /** constants */
  12. const CALENDAR_WINDOWS_1900 = 1900; // Base date of 1st Jan 1900 = 1.0
  13. const CALENDAR_MAC_1904 = 1904; // Base date of 2nd Jan 1904 = 1.0
  14. /**
  15. * Names of the months of the year, indexed by shortname
  16. * Planned usage for locale settings.
  17. *
  18. * @var string[]
  19. */
  20. public static $monthNames = [
  21. 'Jan' => 'January',
  22. 'Feb' => 'February',
  23. 'Mar' => 'March',
  24. 'Apr' => 'April',
  25. 'May' => 'May',
  26. 'Jun' => 'June',
  27. 'Jul' => 'July',
  28. 'Aug' => 'August',
  29. 'Sep' => 'September',
  30. 'Oct' => 'October',
  31. 'Nov' => 'November',
  32. 'Dec' => 'December',
  33. ];
  34. /**
  35. * @var string[]
  36. */
  37. public static $numberSuffixes = [
  38. 'st',
  39. 'nd',
  40. 'rd',
  41. 'th',
  42. ];
  43. /**
  44. * Base calendar year to use for calculations
  45. * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904).
  46. *
  47. * @var int
  48. */
  49. protected static $excelCalendar = self::CALENDAR_WINDOWS_1900;
  50. /**
  51. * Default timezone to use for DateTime objects.
  52. *
  53. * @var null|\DateTimeZone
  54. */
  55. protected static $defaultTimeZone;
  56. /**
  57. * Set the Excel calendar (Windows 1900 or Mac 1904).
  58. *
  59. * @param int $baseDate Excel base date (1900 or 1904)
  60. *
  61. * @return bool Success or failure
  62. */
  63. public static function setExcelCalendar($baseDate)
  64. {
  65. if (($baseDate == self::CALENDAR_WINDOWS_1900) ||
  66. ($baseDate == self::CALENDAR_MAC_1904)) {
  67. self::$excelCalendar = $baseDate;
  68. return true;
  69. }
  70. return false;
  71. }
  72. /**
  73. * Return the Excel calendar (Windows 1900 or Mac 1904).
  74. *
  75. * @return int Excel base date (1900 or 1904)
  76. */
  77. public static function getExcelCalendar()
  78. {
  79. return self::$excelCalendar;
  80. }
  81. /**
  82. * Set the Default timezone to use for dates.
  83. *
  84. * @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
  85. *
  86. * @throws \Exception
  87. *
  88. * @return bool Success or failure
  89. * @return bool Success or failure
  90. */
  91. public static function setDefaultTimezone($timeZone)
  92. {
  93. if ($timeZone = self::validateTimeZone($timeZone)) {
  94. self::$defaultTimeZone = $timeZone;
  95. return true;
  96. }
  97. return false;
  98. }
  99. /**
  100. * Return the Default timezone being used for dates.
  101. *
  102. * @return DateTimeZone The timezone being used as default for Excel timestamp to PHP DateTime object
  103. */
  104. public static function getDefaultTimezone()
  105. {
  106. if (self::$defaultTimeZone === null) {
  107. self::$defaultTimeZone = new DateTimeZone('UTC');
  108. }
  109. return self::$defaultTimeZone;
  110. }
  111. /**
  112. * Validate a timezone.
  113. *
  114. * @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
  115. *
  116. * @throws \Exception
  117. *
  118. * @return DateTimeZone The timezone as a timezone object
  119. * @return DateTimeZone The timezone as a timezone object
  120. */
  121. protected static function validateTimeZone($timeZone)
  122. {
  123. if (is_object($timeZone) && $timeZone instanceof DateTimeZone) {
  124. return $timeZone;
  125. } elseif (is_string($timeZone)) {
  126. return new DateTimeZone($timeZone);
  127. }
  128. throw new \Exception('Invalid timezone');
  129. }
  130. /**
  131. * Convert a MS serialized datetime value from Excel to a PHP Date/Time object.
  132. *
  133. * @param float|int $excelTimestamp MS Excel serialized date/time value
  134. * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
  135. * if you don't want to treat it as a UTC value
  136. * Use the default (UST) unless you absolutely need a conversion
  137. *
  138. * @throws \Exception
  139. *
  140. * @return \DateTime PHP date/time object
  141. */
  142. public static function excelToDateTimeObject($excelTimestamp, $timeZone = null)
  143. {
  144. $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
  145. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
  146. if ($excelTimestamp < 1.0) {
  147. // Unix timestamp base date
  148. $baseDate = new \DateTime('1970-01-01', $timeZone);
  149. } else {
  150. // MS Excel calendar base dates
  151. if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
  152. // Allow adjustment for 1900 Leap Year in MS Excel
  153. $baseDate = ($excelTimestamp < 60) ? new \DateTime('1899-12-31', $timeZone) : new \DateTime('1899-12-30', $timeZone);
  154. } else {
  155. $baseDate = new \DateTime('1904-01-01', $timeZone);
  156. }
  157. }
  158. } else {
  159. $baseDate = new \DateTime('1899-12-30', $timeZone);
  160. }
  161. $days = floor($excelTimestamp);
  162. $partDay = $excelTimestamp - $days;
  163. $hours = floor($partDay * 24);
  164. $partDay = $partDay * 24 - $hours;
  165. $minutes = floor($partDay * 60);
  166. $partDay = $partDay * 60 - $minutes;
  167. $seconds = round($partDay * 60);
  168. if ($days >= 0) {
  169. $days = '+' . $days;
  170. }
  171. $interval = $days . ' days';
  172. return $baseDate->modify($interval)
  173. ->setTime($hours, $minutes, $seconds);
  174. }
  175. /**
  176. * Convert a MS serialized datetime value from Excel to a unix timestamp.
  177. *
  178. * @param float|int $excelTimestamp MS Excel serialized date/time value
  179. * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
  180. * if you don't want to treat it as a UTC value
  181. * Use the default (UST) unless you absolutely need a conversion
  182. *
  183. * @throws \Exception
  184. *
  185. * @return int Unix timetamp for this date/time
  186. */
  187. public static function excelToTimestamp($excelTimestamp, $timeZone = null)
  188. {
  189. return (int) self::excelToDateTimeObject($excelTimestamp, $timeZone)
  190. ->format('U');
  191. }
  192. /**
  193. * Convert a date from PHP to an MS Excel serialized date/time value.
  194. *
  195. * @param mixed $dateValue Unix Timestamp or PHP DateTime object or a string
  196. *
  197. * @return bool|float Excel date/time value
  198. * or boolean FALSE on failure
  199. */
  200. public static function PHPToExcel($dateValue)
  201. {
  202. if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
  203. return self::dateTimeToExcel($dateValue);
  204. } elseif (is_numeric($dateValue)) {
  205. return self::timestampToExcel($dateValue);
  206. } elseif (is_string($dateValue)) {
  207. return self::stringToExcel($dateValue);
  208. }
  209. return false;
  210. }
  211. /**
  212. * Convert a PHP DateTime object to an MS Excel serialized date/time value.
  213. *
  214. * @param DateTimeInterface $dateValue PHP DateTime object
  215. *
  216. * @return float MS Excel serialized date/time value
  217. */
  218. public static function dateTimeToExcel(DateTimeInterface $dateValue)
  219. {
  220. return self::formattedPHPToExcel(
  221. $dateValue->format('Y'),
  222. $dateValue->format('m'),
  223. $dateValue->format('d'),
  224. $dateValue->format('H'),
  225. $dateValue->format('i'),
  226. $dateValue->format('s')
  227. );
  228. }
  229. /**
  230. * Convert a Unix timestamp to an MS Excel serialized date/time value.
  231. *
  232. * @param int $dateValue Unix Timestamp
  233. *
  234. * @return float MS Excel serialized date/time value
  235. */
  236. public static function timestampToExcel($dateValue)
  237. {
  238. if (!is_numeric($dateValue)) {
  239. return false;
  240. }
  241. return self::dateTimeToExcel(new \DateTime('@' . $dateValue));
  242. }
  243. /**
  244. * formattedPHPToExcel.
  245. *
  246. * @param int $year
  247. * @param int $month
  248. * @param int $day
  249. * @param int $hours
  250. * @param int $minutes
  251. * @param int $seconds
  252. *
  253. * @return float Excel date/time value
  254. */
  255. public static function formattedPHPToExcel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0)
  256. {
  257. if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
  258. //
  259. // Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
  260. // This affects every date following 28th February 1900
  261. //
  262. $excel1900isLeapYear = true;
  263. if (($year == 1900) && ($month <= 2)) {
  264. $excel1900isLeapYear = false;
  265. }
  266. $myexcelBaseDate = 2415020;
  267. } else {
  268. $myexcelBaseDate = 2416481;
  269. $excel1900isLeapYear = false;
  270. }
  271. // Julian base date Adjustment
  272. if ($month > 2) {
  273. $month -= 3;
  274. } else {
  275. $month += 9;
  276. --$year;
  277. }
  278. // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
  279. $century = substr($year, 0, 2);
  280. $decade = substr($year, 2, 2);
  281. $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear;
  282. $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
  283. return (float) $excelDate + $excelTime;
  284. }
  285. /**
  286. * Is a given cell a date/time?
  287. *
  288. * @param Cell $pCell
  289. *
  290. * @return bool
  291. */
  292. public static function isDateTime(Cell $pCell)
  293. {
  294. return is_numeric($pCell->getValue()) &&
  295. self::isDateTimeFormat(
  296. $pCell->getWorksheet()->getStyle(
  297. $pCell->getCoordinate()
  298. )->getNumberFormat()
  299. );
  300. }
  301. /**
  302. * Is a given number format a date/time?
  303. *
  304. * @param NumberFormat $pFormat
  305. *
  306. * @return bool
  307. */
  308. public static function isDateTimeFormat(NumberFormat $pFormat)
  309. {
  310. return self::isDateTimeFormatCode($pFormat->getFormatCode());
  311. }
  312. private static $possibleDateFormatCharacters = 'eymdHs';
  313. /**
  314. * Is a given number format code a date/time?
  315. *
  316. * @param string $pFormatCode
  317. *
  318. * @return bool
  319. */
  320. public static function isDateTimeFormatCode($pFormatCode)
  321. {
  322. if (strtolower($pFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
  323. // "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check)
  324. return false;
  325. }
  326. if (preg_match('/[0#]E[+-]0/i', $pFormatCode)) {
  327. // Scientific format
  328. return false;
  329. }
  330. // Switch on formatcode
  331. switch ($pFormatCode) {
  332. // Explicitly defined date formats
  333. case NumberFormat::FORMAT_DATE_YYYYMMDD:
  334. case NumberFormat::FORMAT_DATE_YYYYMMDD2:
  335. case NumberFormat::FORMAT_DATE_DDMMYYYY:
  336. case NumberFormat::FORMAT_DATE_DMYSLASH:
  337. case NumberFormat::FORMAT_DATE_DMYMINUS:
  338. case NumberFormat::FORMAT_DATE_DMMINUS:
  339. case NumberFormat::FORMAT_DATE_MYMINUS:
  340. case NumberFormat::FORMAT_DATE_DATETIME:
  341. case NumberFormat::FORMAT_DATE_TIME1:
  342. case NumberFormat::FORMAT_DATE_TIME2:
  343. case NumberFormat::FORMAT_DATE_TIME3:
  344. case NumberFormat::FORMAT_DATE_TIME4:
  345. case NumberFormat::FORMAT_DATE_TIME5:
  346. case NumberFormat::FORMAT_DATE_TIME6:
  347. case NumberFormat::FORMAT_DATE_TIME7:
  348. case NumberFormat::FORMAT_DATE_TIME8:
  349. case NumberFormat::FORMAT_DATE_YYYYMMDDSLASH:
  350. case NumberFormat::FORMAT_DATE_XLSX14:
  351. case NumberFormat::FORMAT_DATE_XLSX15:
  352. case NumberFormat::FORMAT_DATE_XLSX16:
  353. case NumberFormat::FORMAT_DATE_XLSX17:
  354. case NumberFormat::FORMAT_DATE_XLSX22:
  355. return true;
  356. }
  357. // Typically number, currency or accounting (or occasionally fraction) formats
  358. if ((substr($pFormatCode, 0, 1) == '_') || (substr($pFormatCode, 0, 2) == '0 ')) {
  359. return false;
  360. }
  361. // Try checking for any of the date formatting characters that don't appear within square braces
  362. if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $pFormatCode)) {
  363. // We might also have a format mask containing quoted strings...
  364. // we don't want to test for any of our characters within the quoted blocks
  365. if (strpos($pFormatCode, '"') !== false) {
  366. $segMatcher = false;
  367. foreach (explode('"', $pFormatCode) as $subVal) {
  368. // Only test in alternate array entries (the non-quoted blocks)
  369. if (($segMatcher = !$segMatcher) &&
  370. (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $subVal))) {
  371. return true;
  372. }
  373. }
  374. return false;
  375. }
  376. return true;
  377. }
  378. // No date...
  379. return false;
  380. }
  381. /**
  382. * Convert a date/time string to Excel time.
  383. *
  384. * @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10'
  385. *
  386. * @return false|float Excel date/time serial value
  387. */
  388. public static function stringToExcel($dateValue)
  389. {
  390. if (strlen($dateValue) < 2) {
  391. return false;
  392. }
  393. if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2})?)?$/iu', $dateValue)) {
  394. return false;
  395. }
  396. $dateValueNew = DateTime::DATEVALUE($dateValue);
  397. if ($dateValueNew === Functions::VALUE()) {
  398. return false;
  399. }
  400. if (strpos($dateValue, ':') !== false) {
  401. $timeValue = DateTime::TIMEVALUE($dateValue);
  402. if ($timeValue === Functions::VALUE()) {
  403. return false;
  404. }
  405. $dateValueNew += $timeValue;
  406. }
  407. return $dateValueNew;
  408. }
  409. /**
  410. * Converts a month name (either a long or a short name) to a month number.
  411. *
  412. * @param string $month Month name or abbreviation
  413. *
  414. * @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name
  415. */
  416. public static function monthStringToNumber($month)
  417. {
  418. $monthIndex = 1;
  419. foreach (self::$monthNames as $shortMonthName => $longMonthName) {
  420. if (($month === $longMonthName) || ($month === $shortMonthName)) {
  421. return $monthIndex;
  422. }
  423. ++$monthIndex;
  424. }
  425. return $month;
  426. }
  427. /**
  428. * Strips an ordinal from a numeric value.
  429. *
  430. * @param string $day Day number with an ordinal
  431. *
  432. * @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric
  433. */
  434. public static function dayStringToNumber($day)
  435. {
  436. $strippedDayValue = (str_replace(self::$numberSuffixes, '', $day));
  437. if (is_numeric($strippedDayValue)) {
  438. return (int) $strippedDayValue;
  439. }
  440. return $day;
  441. }
  442. }