DateTime.php 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Calculation;
  3. use PhpOffice\PhpSpreadsheet\Shared\Date;
  4. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  5. class DateTime
  6. {
  7. /**
  8. * Identify if a year is a leap year or not.
  9. *
  10. * @param int|string $year The year to test
  11. *
  12. * @return bool TRUE if the year is a leap year, otherwise FALSE
  13. */
  14. public static function isLeapYear($year)
  15. {
  16. return (($year % 4) == 0) && (($year % 100) != 0) || (($year % 400) == 0);
  17. }
  18. /**
  19. * Return the number of days between two dates based on a 360 day calendar.
  20. *
  21. * @param int $startDay Day of month of the start date
  22. * @param int $startMonth Month of the start date
  23. * @param int $startYear Year of the start date
  24. * @param int $endDay Day of month of the start date
  25. * @param int $endMonth Month of the start date
  26. * @param int $endYear Year of the start date
  27. * @param bool $methodUS Whether to use the US method or the European method of calculation
  28. *
  29. * @return int Number of days between the start date and the end date
  30. */
  31. private static function dateDiff360($startDay, $startMonth, $startYear, $endDay, $endMonth, $endYear, $methodUS)
  32. {
  33. if ($startDay == 31) {
  34. --$startDay;
  35. } elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !self::isLeapYear($startYear))))) {
  36. $startDay = 30;
  37. }
  38. if ($endDay == 31) {
  39. if ($methodUS && $startDay != 30) {
  40. $endDay = 1;
  41. if ($endMonth == 12) {
  42. ++$endYear;
  43. $endMonth = 1;
  44. } else {
  45. ++$endMonth;
  46. }
  47. } else {
  48. $endDay = 30;
  49. }
  50. }
  51. return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360;
  52. }
  53. /**
  54. * getDateValue.
  55. *
  56. * @param string $dateValue
  57. *
  58. * @return mixed Excel date/time serial value, or string if error
  59. */
  60. public static function getDateValue($dateValue)
  61. {
  62. if (!is_numeric($dateValue)) {
  63. if ((is_string($dateValue)) &&
  64. (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC)) {
  65. return Functions::VALUE();
  66. }
  67. if ((is_object($dateValue)) && ($dateValue instanceof \DateTimeInterface)) {
  68. $dateValue = Date::PHPToExcel($dateValue);
  69. } else {
  70. $saveReturnDateType = Functions::getReturnDateType();
  71. Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
  72. $dateValue = self::DATEVALUE($dateValue);
  73. Functions::setReturnDateType($saveReturnDateType);
  74. }
  75. }
  76. return $dateValue;
  77. }
  78. /**
  79. * getTimeValue.
  80. *
  81. * @param string $timeValue
  82. *
  83. * @return mixed Excel date/time serial value, or string if error
  84. */
  85. private static function getTimeValue($timeValue)
  86. {
  87. $saveReturnDateType = Functions::getReturnDateType();
  88. Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
  89. $timeValue = self::TIMEVALUE($timeValue);
  90. Functions::setReturnDateType($saveReturnDateType);
  91. return $timeValue;
  92. }
  93. private static function adjustDateByMonths($dateValue = 0, $adjustmentMonths = 0)
  94. {
  95. // Execute function
  96. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  97. $oMonth = (int) $PHPDateObject->format('m');
  98. $oYear = (int) $PHPDateObject->format('Y');
  99. $adjustmentMonthsString = (string) $adjustmentMonths;
  100. if ($adjustmentMonths > 0) {
  101. $adjustmentMonthsString = '+' . $adjustmentMonths;
  102. }
  103. if ($adjustmentMonths != 0) {
  104. $PHPDateObject->modify($adjustmentMonthsString . ' months');
  105. }
  106. $nMonth = (int) $PHPDateObject->format('m');
  107. $nYear = (int) $PHPDateObject->format('Y');
  108. $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12);
  109. if ($monthDiff != $adjustmentMonths) {
  110. $adjustDays = (int) $PHPDateObject->format('d');
  111. $adjustDaysString = '-' . $adjustDays . ' days';
  112. $PHPDateObject->modify($adjustDaysString);
  113. }
  114. return $PHPDateObject;
  115. }
  116. /**
  117. * DATETIMENOW.
  118. *
  119. * Returns the current date and time.
  120. * The NOW function is useful when you need to display the current date and time on a worksheet or
  121. * calculate a value based on the current date and time, and have that value updated each time you
  122. * open the worksheet.
  123. *
  124. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
  125. * and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  126. *
  127. * Excel Function:
  128. * NOW()
  129. *
  130. * @category Date/Time Functions
  131. *
  132. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  133. * depending on the value of the ReturnDateType flag
  134. */
  135. public static function DATETIMENOW()
  136. {
  137. $saveTimeZone = date_default_timezone_get();
  138. date_default_timezone_set('UTC');
  139. $retValue = false;
  140. switch (Functions::getReturnDateType()) {
  141. case Functions::RETURNDATE_EXCEL:
  142. $retValue = (float) Date::PHPToExcel(time());
  143. break;
  144. case Functions::RETURNDATE_PHP_NUMERIC:
  145. $retValue = (int) time();
  146. break;
  147. case Functions::RETURNDATE_PHP_OBJECT:
  148. $retValue = new \DateTime();
  149. break;
  150. }
  151. date_default_timezone_set($saveTimeZone);
  152. return $retValue;
  153. }
  154. /**
  155. * DATENOW.
  156. *
  157. * Returns the current date.
  158. * The NOW function is useful when you need to display the current date and time on a worksheet or
  159. * calculate a value based on the current date and time, and have that value updated each time you
  160. * open the worksheet.
  161. *
  162. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
  163. * and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  164. *
  165. * Excel Function:
  166. * TODAY()
  167. *
  168. * @category Date/Time Functions
  169. *
  170. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  171. * depending on the value of the ReturnDateType flag
  172. */
  173. public static function DATENOW()
  174. {
  175. $saveTimeZone = date_default_timezone_get();
  176. date_default_timezone_set('UTC');
  177. $retValue = false;
  178. $excelDateTime = floor(Date::PHPToExcel(time()));
  179. switch (Functions::getReturnDateType()) {
  180. case Functions::RETURNDATE_EXCEL:
  181. $retValue = (float) $excelDateTime;
  182. break;
  183. case Functions::RETURNDATE_PHP_NUMERIC:
  184. $retValue = (int) Date::excelToTimestamp($excelDateTime);
  185. break;
  186. case Functions::RETURNDATE_PHP_OBJECT:
  187. $retValue = Date::excelToDateTimeObject($excelDateTime);
  188. break;
  189. }
  190. date_default_timezone_set($saveTimeZone);
  191. return $retValue;
  192. }
  193. /**
  194. * DATE.
  195. *
  196. * The DATE function returns a value that represents a particular date.
  197. *
  198. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
  199. * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  200. *
  201. * Excel Function:
  202. * DATE(year,month,day)
  203. *
  204. * PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
  205. * A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
  206. * as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
  207. *
  208. * @category Date/Time Functions
  209. *
  210. * @param int $year The value of the year argument can include one to four digits.
  211. * Excel interprets the year argument according to the configured
  212. * date system: 1900 or 1904.
  213. * If year is between 0 (zero) and 1899 (inclusive), Excel adds that
  214. * value to 1900 to calculate the year. For example, DATE(108,1,2)
  215. * returns January 2, 2008 (1900+108).
  216. * If year is between 1900 and 9999 (inclusive), Excel uses that
  217. * value as the year. For example, DATE(2008,1,2) returns January 2,
  218. * 2008.
  219. * If year is less than 0 or is 10000 or greater, Excel returns the
  220. * #NUM! error value.
  221. * @param int $month A positive or negative integer representing the month of the year
  222. * from 1 to 12 (January to December).
  223. * If month is greater than 12, month adds that number of months to
  224. * the first month in the year specified. For example, DATE(2008,14,2)
  225. * returns the serial number representing February 2, 2009.
  226. * If month is less than 1, month subtracts the magnitude of that
  227. * number of months, plus 1, from the first month in the year
  228. * specified. For example, DATE(2008,-3,2) returns the serial number
  229. * representing September 2, 2007.
  230. * @param int $day A positive or negative integer representing the day of the month
  231. * from 1 to 31.
  232. * If day is greater than the number of days in the month specified,
  233. * day adds that number of days to the first day in the month. For
  234. * example, DATE(2008,1,35) returns the serial number representing
  235. * February 4, 2008.
  236. * If day is less than 1, day subtracts the magnitude that number of
  237. * days, plus one, from the first day of the month specified. For
  238. * example, DATE(2008,1,-15) returns the serial number representing
  239. * December 16, 2007.
  240. *
  241. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  242. * depending on the value of the ReturnDateType flag
  243. */
  244. public static function DATE($year = 0, $month = 1, $day = 1)
  245. {
  246. $year = Functions::flattenSingleValue($year);
  247. $month = Functions::flattenSingleValue($month);
  248. $day = Functions::flattenSingleValue($day);
  249. if (($month !== null) && (!is_numeric($month))) {
  250. $month = Date::monthStringToNumber($month);
  251. }
  252. if (($day !== null) && (!is_numeric($day))) {
  253. $day = Date::dayStringToNumber($day);
  254. }
  255. $year = ($year !== null) ? StringHelper::testStringAsNumeric($year) : 0;
  256. $month = ($month !== null) ? StringHelper::testStringAsNumeric($month) : 0;
  257. $day = ($day !== null) ? StringHelper::testStringAsNumeric($day) : 0;
  258. if ((!is_numeric($year)) ||
  259. (!is_numeric($month)) ||
  260. (!is_numeric($day))) {
  261. return Functions::VALUE();
  262. }
  263. $year = (int) $year;
  264. $month = (int) $month;
  265. $day = (int) $day;
  266. $baseYear = Date::getExcelCalendar();
  267. // Validate parameters
  268. if ($year < ($baseYear - 1900)) {
  269. return Functions::NAN();
  270. }
  271. if ((($baseYear - 1900) != 0) && ($year < $baseYear) && ($year >= 1900)) {
  272. return Functions::NAN();
  273. }
  274. if (($year < $baseYear) && ($year >= ($baseYear - 1900))) {
  275. $year += 1900;
  276. }
  277. if ($month < 1) {
  278. // Handle year/month adjustment if month < 1
  279. --$month;
  280. $year += ceil($month / 12) - 1;
  281. $month = 13 - abs($month % 12);
  282. } elseif ($month > 12) {
  283. // Handle year/month adjustment if month > 12
  284. $year += floor($month / 12);
  285. $month = ($month % 12);
  286. }
  287. // Re-validate the year parameter after adjustments
  288. if (($year < $baseYear) || ($year >= 10000)) {
  289. return Functions::NAN();
  290. }
  291. // Execute function
  292. $excelDateValue = Date::formattedPHPToExcel($year, $month, $day);
  293. switch (Functions::getReturnDateType()) {
  294. case Functions::RETURNDATE_EXCEL:
  295. return (float) $excelDateValue;
  296. case Functions::RETURNDATE_PHP_NUMERIC:
  297. return (int) Date::excelToTimestamp($excelDateValue);
  298. case Functions::RETURNDATE_PHP_OBJECT:
  299. return Date::excelToDateTimeObject($excelDateValue);
  300. }
  301. }
  302. /**
  303. * TIME.
  304. *
  305. * The TIME function returns a value that represents a particular time.
  306. *
  307. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
  308. * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  309. *
  310. * Excel Function:
  311. * TIME(hour,minute,second)
  312. *
  313. * @category Date/Time Functions
  314. *
  315. * @param int $hour A number from 0 (zero) to 32767 representing the hour.
  316. * Any value greater than 23 will be divided by 24 and the remainder
  317. * will be treated as the hour value. For example, TIME(27,0,0) =
  318. * TIME(3,0,0) = .125 or 3:00 AM.
  319. * @param int $minute A number from 0 to 32767 representing the minute.
  320. * Any value greater than 59 will be converted to hours and minutes.
  321. * For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM.
  322. * @param int $second A number from 0 to 32767 representing the second.
  323. * Any value greater than 59 will be converted to hours, minutes,
  324. * and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148
  325. * or 12:33:20 AM
  326. *
  327. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  328. * depending on the value of the ReturnDateType flag
  329. */
  330. public static function TIME($hour = 0, $minute = 0, $second = 0)
  331. {
  332. $hour = Functions::flattenSingleValue($hour);
  333. $minute = Functions::flattenSingleValue($minute);
  334. $second = Functions::flattenSingleValue($second);
  335. if ($hour == '') {
  336. $hour = 0;
  337. }
  338. if ($minute == '') {
  339. $minute = 0;
  340. }
  341. if ($second == '') {
  342. $second = 0;
  343. }
  344. if ((!is_numeric($hour)) || (!is_numeric($minute)) || (!is_numeric($second))) {
  345. return Functions::VALUE();
  346. }
  347. $hour = (int) $hour;
  348. $minute = (int) $minute;
  349. $second = (int) $second;
  350. if ($second < 0) {
  351. $minute += floor($second / 60);
  352. $second = 60 - abs($second % 60);
  353. if ($second == 60) {
  354. $second = 0;
  355. }
  356. } elseif ($second >= 60) {
  357. $minute += floor($second / 60);
  358. $second = $second % 60;
  359. }
  360. if ($minute < 0) {
  361. $hour += floor($minute / 60);
  362. $minute = 60 - abs($minute % 60);
  363. if ($minute == 60) {
  364. $minute = 0;
  365. }
  366. } elseif ($minute >= 60) {
  367. $hour += floor($minute / 60);
  368. $minute = $minute % 60;
  369. }
  370. if ($hour > 23) {
  371. $hour = $hour % 24;
  372. } elseif ($hour < 0) {
  373. return Functions::NAN();
  374. }
  375. // Execute function
  376. switch (Functions::getReturnDateType()) {
  377. case Functions::RETURNDATE_EXCEL:
  378. $date = 0;
  379. $calendar = Date::getExcelCalendar();
  380. if ($calendar != Date::CALENDAR_WINDOWS_1900) {
  381. $date = 1;
  382. }
  383. return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second);
  384. case Functions::RETURNDATE_PHP_NUMERIC:
  385. return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600
  386. case Functions::RETURNDATE_PHP_OBJECT:
  387. $dayAdjust = 0;
  388. if ($hour < 0) {
  389. $dayAdjust = floor($hour / 24);
  390. $hour = 24 - abs($hour % 24);
  391. if ($hour == 24) {
  392. $hour = 0;
  393. }
  394. } elseif ($hour >= 24) {
  395. $dayAdjust = floor($hour / 24);
  396. $hour = $hour % 24;
  397. }
  398. $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second);
  399. if ($dayAdjust != 0) {
  400. $phpDateObject->modify($dayAdjust . ' days');
  401. }
  402. return $phpDateObject;
  403. }
  404. }
  405. /**
  406. * DATEVALUE.
  407. *
  408. * Returns a value that represents a particular date.
  409. * Use DATEVALUE to convert a date represented by a text string to an Excel or PHP date/time stamp
  410. * value.
  411. *
  412. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
  413. * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  414. *
  415. * Excel Function:
  416. * DATEVALUE(dateValue)
  417. *
  418. * @category Date/Time Functions
  419. *
  420. * @param string $dateValue Text that represents a date in a Microsoft Excel date format.
  421. * For example, "1/30/2008" or "30-Jan-2008" are text strings within
  422. * quotation marks that represent dates. Using the default date
  423. * system in Excel for Windows, date_text must represent a date from
  424. * January 1, 1900, to December 31, 9999. Using the default date
  425. * system in Excel for the Macintosh, date_text must represent a date
  426. * from January 1, 1904, to December 31, 9999. DATEVALUE returns the
  427. * #VALUE! error value if date_text is out of this range.
  428. *
  429. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  430. * depending on the value of the ReturnDateType flag
  431. */
  432. public static function DATEVALUE($dateValue = 1)
  433. {
  434. $dateValueOrig = $dateValue;
  435. $dateValue = trim(Functions::flattenSingleValue($dateValue), '"');
  436. // Strip any ordinals because they're allowed in Excel (English only)
  437. $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue);
  438. // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany)
  439. $dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue);
  440. $yearFound = false;
  441. $t1 = explode(' ', $dateValue);
  442. foreach ($t1 as &$t) {
  443. if ((is_numeric($t)) && ($t > 31)) {
  444. if ($yearFound) {
  445. return Functions::VALUE();
  446. }
  447. if ($t < 100) {
  448. $t += 1900;
  449. }
  450. $yearFound = true;
  451. }
  452. }
  453. if ((count($t1) == 1) && (strpos($t, ':') != false)) {
  454. // We've been fed a time value without any date
  455. return 0.0;
  456. } elseif (count($t1) == 2) {
  457. // We only have two parts of the date: either day/month or month/year
  458. if ($yearFound) {
  459. array_unshift($t1, 1);
  460. } else {
  461. if ($t1[1] > 29) {
  462. $t1[1] += 1900;
  463. array_unshift($t1, 1);
  464. } else {
  465. $t1[] = date('Y');
  466. }
  467. }
  468. }
  469. unset($t);
  470. $dateValue = implode(' ', $t1);
  471. $PHPDateArray = date_parse($dateValue);
  472. if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) {
  473. $testVal1 = strtok($dateValue, '- ');
  474. if ($testVal1 !== false) {
  475. $testVal2 = strtok('- ');
  476. if ($testVal2 !== false) {
  477. $testVal3 = strtok('- ');
  478. if ($testVal3 === false) {
  479. $testVal3 = strftime('%Y');
  480. }
  481. } else {
  482. return Functions::VALUE();
  483. }
  484. } else {
  485. return Functions::VALUE();
  486. }
  487. if ($testVal1 < 31 && $testVal2 < 12 && $testVal3 < 12 && strlen($testVal3) == 2) {
  488. $testVal3 += 2000;
  489. }
  490. $PHPDateArray = date_parse($testVal1 . '-' . $testVal2 . '-' . $testVal3);
  491. if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) {
  492. $PHPDateArray = date_parse($testVal2 . '-' . $testVal1 . '-' . $testVal3);
  493. if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) {
  494. return Functions::VALUE();
  495. }
  496. }
  497. }
  498. if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) {
  499. // Execute function
  500. if ($PHPDateArray['year'] == '') {
  501. $PHPDateArray['year'] = strftime('%Y');
  502. }
  503. if ($PHPDateArray['year'] < 1900) {
  504. return Functions::VALUE();
  505. }
  506. if ($PHPDateArray['month'] == '') {
  507. $PHPDateArray['month'] = strftime('%m');
  508. }
  509. if ($PHPDateArray['day'] == '') {
  510. $PHPDateArray['day'] = strftime('%d');
  511. }
  512. if (!checkdate($PHPDateArray['month'], $PHPDateArray['day'], $PHPDateArray['year'])) {
  513. return Functions::VALUE();
  514. }
  515. $excelDateValue = floor(
  516. Date::formattedPHPToExcel(
  517. $PHPDateArray['year'],
  518. $PHPDateArray['month'],
  519. $PHPDateArray['day'],
  520. $PHPDateArray['hour'],
  521. $PHPDateArray['minute'],
  522. $PHPDateArray['second']
  523. )
  524. );
  525. switch (Functions::getReturnDateType()) {
  526. case Functions::RETURNDATE_EXCEL:
  527. return (float) $excelDateValue;
  528. case Functions::RETURNDATE_PHP_NUMERIC:
  529. return (int) Date::excelToTimestamp($excelDateValue);
  530. case Functions::RETURNDATE_PHP_OBJECT:
  531. return new \DateTime($PHPDateArray['year'] . '-' . $PHPDateArray['month'] . '-' . $PHPDateArray['day'] . ' 00:00:00');
  532. }
  533. }
  534. return Functions::VALUE();
  535. }
  536. /**
  537. * TIMEVALUE.
  538. *
  539. * Returns a value that represents a particular time.
  540. * Use TIMEVALUE to convert a time represented by a text string to an Excel or PHP date/time stamp
  541. * value.
  542. *
  543. * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
  544. * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
  545. *
  546. * Excel Function:
  547. * TIMEVALUE(timeValue)
  548. *
  549. * @category Date/Time Functions
  550. *
  551. * @param string $timeValue A text string that represents a time in any one of the Microsoft
  552. * Excel time formats; for example, "6:45 PM" and "18:45" text strings
  553. * within quotation marks that represent time.
  554. * Date information in time_text is ignored.
  555. *
  556. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  557. * depending on the value of the ReturnDateType flag
  558. */
  559. public static function TIMEVALUE($timeValue)
  560. {
  561. $timeValue = trim(Functions::flattenSingleValue($timeValue), '"');
  562. $timeValue = str_replace(['/', '.'], '-', $timeValue);
  563. $arraySplit = preg_split('/[\/:\-\s]/', $timeValue);
  564. if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) {
  565. $arraySplit[0] = ($arraySplit[0] % 24);
  566. $timeValue = implode(':', $arraySplit);
  567. }
  568. $PHPDateArray = date_parse($timeValue);
  569. if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) {
  570. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) {
  571. $excelDateValue = Date::formattedPHPToExcel(
  572. $PHPDateArray['year'],
  573. $PHPDateArray['month'],
  574. $PHPDateArray['day'],
  575. $PHPDateArray['hour'],
  576. $PHPDateArray['minute'],
  577. $PHPDateArray['second']
  578. );
  579. } else {
  580. $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1;
  581. }
  582. switch (Functions::getReturnDateType()) {
  583. case Functions::RETURNDATE_EXCEL:
  584. return (float) $excelDateValue;
  585. case Functions::RETURNDATE_PHP_NUMERIC:
  586. return (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600;
  587. case Functions::RETURNDATE_PHP_OBJECT:
  588. return new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
  589. }
  590. }
  591. return Functions::VALUE();
  592. }
  593. /**
  594. * DATEDIF.
  595. *
  596. * @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object
  597. * or a standard date string
  598. * @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object
  599. * or a standard date string
  600. * @param string $unit
  601. *
  602. * @return int|string Interval between the dates
  603. */
  604. public static function DATEDIF($startDate = 0, $endDate = 0, $unit = 'D')
  605. {
  606. $startDate = Functions::flattenSingleValue($startDate);
  607. $endDate = Functions::flattenSingleValue($endDate);
  608. $unit = strtoupper(Functions::flattenSingleValue($unit));
  609. if (is_string($startDate = self::getDateValue($startDate))) {
  610. return Functions::VALUE();
  611. }
  612. if (is_string($endDate = self::getDateValue($endDate))) {
  613. return Functions::VALUE();
  614. }
  615. // Validate parameters
  616. if ($startDate > $endDate) {
  617. return Functions::NAN();
  618. }
  619. // Execute function
  620. $difference = $endDate - $startDate;
  621. $PHPStartDateObject = Date::excelToDateTimeObject($startDate);
  622. $startDays = $PHPStartDateObject->format('j');
  623. $startMonths = $PHPStartDateObject->format('n');
  624. $startYears = $PHPStartDateObject->format('Y');
  625. $PHPEndDateObject = Date::excelToDateTimeObject($endDate);
  626. $endDays = $PHPEndDateObject->format('j');
  627. $endMonths = $PHPEndDateObject->format('n');
  628. $endYears = $PHPEndDateObject->format('Y');
  629. $retVal = Functions::NAN();
  630. switch ($unit) {
  631. case 'D':
  632. $retVal = (int) $difference;
  633. break;
  634. case 'M':
  635. $retVal = (int) ($endMonths - $startMonths) + ((int) ($endYears - $startYears) * 12);
  636. // We're only interested in full months
  637. if ($endDays < $startDays) {
  638. --$retVal;
  639. }
  640. break;
  641. case 'Y':
  642. $retVal = (int) ($endYears - $startYears);
  643. // We're only interested in full months
  644. if ($endMonths < $startMonths) {
  645. --$retVal;
  646. } elseif (($endMonths == $startMonths) && ($endDays < $startDays)) {
  647. // Remove start month
  648. --$retVal;
  649. // Remove end month
  650. --$retVal;
  651. }
  652. break;
  653. case 'MD':
  654. if ($endDays < $startDays) {
  655. $retVal = $endDays;
  656. $PHPEndDateObject->modify('-' . $endDays . ' days');
  657. $adjustDays = $PHPEndDateObject->format('j');
  658. $retVal += ($adjustDays - $startDays);
  659. } else {
  660. $retVal = $endDays - $startDays;
  661. }
  662. break;
  663. case 'YM':
  664. $retVal = (int) ($endMonths - $startMonths);
  665. if ($retVal < 0) {
  666. $retVal += 12;
  667. }
  668. // We're only interested in full months
  669. if ($endDays < $startDays) {
  670. --$retVal;
  671. }
  672. break;
  673. case 'YD':
  674. $retVal = (int) $difference;
  675. if ($endYears > $startYears) {
  676. $isLeapStartYear = $PHPStartDateObject->format('L');
  677. $wasLeapEndYear = $PHPEndDateObject->format('L');
  678. // Adjust end year to be as close as possible as start year
  679. while ($PHPEndDateObject >= $PHPStartDateObject) {
  680. $PHPEndDateObject->modify('-1 year');
  681. $endYears = $PHPEndDateObject->format('Y');
  682. }
  683. $PHPEndDateObject->modify('+1 year');
  684. // Get the result
  685. $retVal = $PHPEndDateObject->diff($PHPStartDateObject)->days;
  686. // Adjust for leap years cases
  687. $isLeapEndYear = $PHPEndDateObject->format('L');
  688. $limit = new \DateTime($PHPEndDateObject->format('Y-02-29'));
  689. if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) {
  690. --$retVal;
  691. }
  692. }
  693. break;
  694. default:
  695. $retVal = Functions::VALUE();
  696. }
  697. return $retVal;
  698. }
  699. /**
  700. * DAYS.
  701. *
  702. * Returns the number of days between two dates
  703. *
  704. * Excel Function:
  705. * DAYS(endDate, startDate)
  706. *
  707. * @category Date/Time Functions
  708. *
  709. * @param \DateTimeImmutable|float|int|string $endDate Excel date serial value (float),
  710. * PHP date timestamp (integer), PHP DateTime object, or a standard date string
  711. * @param \DateTimeImmutable|float|int|string $startDate Excel date serial value (float),
  712. * PHP date timestamp (integer), PHP DateTime object, or a standard date string
  713. *
  714. * @return int|string Number of days between start date and end date or an error
  715. */
  716. public static function DAYS($endDate = 0, $startDate = 0)
  717. {
  718. $startDate = Functions::flattenSingleValue($startDate);
  719. $endDate = Functions::flattenSingleValue($endDate);
  720. $startDate = self::getDateValue($startDate);
  721. if (is_string($startDate)) {
  722. return Functions::VALUE();
  723. }
  724. $endDate = self::getDateValue($endDate);
  725. if (is_string($endDate)) {
  726. return Functions::VALUE();
  727. }
  728. // Execute function
  729. $PHPStartDateObject = Date::excelToDateTimeObject($startDate);
  730. $PHPEndDateObject = Date::excelToDateTimeObject($endDate);
  731. $diff = $PHPStartDateObject->diff($PHPEndDateObject);
  732. $days = $diff->days;
  733. if ($diff->invert) {
  734. $days = -$days;
  735. }
  736. return $days;
  737. }
  738. /**
  739. * DAYS360.
  740. *
  741. * Returns the number of days between two dates based on a 360-day year (twelve 30-day months),
  742. * which is used in some accounting calculations. Use this function to help compute payments if
  743. * your accounting system is based on twelve 30-day months.
  744. *
  745. * Excel Function:
  746. * DAYS360(startDate,endDate[,method])
  747. *
  748. * @category Date/Time Functions
  749. *
  750. * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
  751. * PHP DateTime object, or a standard date string
  752. * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
  753. * PHP DateTime object, or a standard date string
  754. * @param bool $method US or European Method
  755. * FALSE or omitted: U.S. (NASD) method. If the starting date is
  756. * the last day of a month, it becomes equal to the 30th of the
  757. * same month. If the ending date is the last day of a month and
  758. * the starting date is earlier than the 30th of a month, the
  759. * ending date becomes equal to the 1st of the next month;
  760. * otherwise the ending date becomes equal to the 30th of the
  761. * same month.
  762. * TRUE: European method. Starting dates and ending dates that
  763. * occur on the 31st of a month become equal to the 30th of the
  764. * same month.
  765. *
  766. * @return int|string Number of days between start date and end date
  767. */
  768. public static function DAYS360($startDate = 0, $endDate = 0, $method = false)
  769. {
  770. $startDate = Functions::flattenSingleValue($startDate);
  771. $endDate = Functions::flattenSingleValue($endDate);
  772. if (is_string($startDate = self::getDateValue($startDate))) {
  773. return Functions::VALUE();
  774. }
  775. if (is_string($endDate = self::getDateValue($endDate))) {
  776. return Functions::VALUE();
  777. }
  778. if (!is_bool($method)) {
  779. return Functions::VALUE();
  780. }
  781. // Execute function
  782. $PHPStartDateObject = Date::excelToDateTimeObject($startDate);
  783. $startDay = $PHPStartDateObject->format('j');
  784. $startMonth = $PHPStartDateObject->format('n');
  785. $startYear = $PHPStartDateObject->format('Y');
  786. $PHPEndDateObject = Date::excelToDateTimeObject($endDate);
  787. $endDay = $PHPEndDateObject->format('j');
  788. $endMonth = $PHPEndDateObject->format('n');
  789. $endYear = $PHPEndDateObject->format('Y');
  790. return self::dateDiff360($startDay, $startMonth, $startYear, $endDay, $endMonth, $endYear, !$method);
  791. }
  792. /**
  793. * YEARFRAC.
  794. *
  795. * Calculates the fraction of the year represented by the number of whole days between two dates
  796. * (the start_date and the end_date).
  797. * Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or
  798. * obligations to assign to a specific term.
  799. *
  800. * Excel Function:
  801. * YEARFRAC(startDate,endDate[,method])
  802. *
  803. * @category Date/Time Functions
  804. *
  805. * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
  806. * PHP DateTime object, or a standard date string
  807. * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
  808. * PHP DateTime object, or a standard date string
  809. * @param int $method Method used for the calculation
  810. * 0 or omitted US (NASD) 30/360
  811. * 1 Actual/actual
  812. * 2 Actual/360
  813. * 3 Actual/365
  814. * 4 European 30/360
  815. *
  816. * @return float fraction of the year
  817. */
  818. public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0)
  819. {
  820. $startDate = Functions::flattenSingleValue($startDate);
  821. $endDate = Functions::flattenSingleValue($endDate);
  822. $method = Functions::flattenSingleValue($method);
  823. if (is_string($startDate = self::getDateValue($startDate))) {
  824. return Functions::VALUE();
  825. }
  826. if (is_string($endDate = self::getDateValue($endDate))) {
  827. return Functions::VALUE();
  828. }
  829. if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) {
  830. switch ($method) {
  831. case 0:
  832. return self::DAYS360($startDate, $endDate) / 360;
  833. case 1:
  834. $days = self::DATEDIF($startDate, $endDate);
  835. $startYear = self::YEAR($startDate);
  836. $endYear = self::YEAR($endDate);
  837. $years = $endYear - $startYear + 1;
  838. $leapDays = 0;
  839. if ($years == 1) {
  840. if (self::isLeapYear($endYear)) {
  841. $startMonth = self::MONTHOFYEAR($startDate);
  842. $endMonth = self::MONTHOFYEAR($endDate);
  843. $endDay = self::DAYOFMONTH($endDate);
  844. if (($startMonth < 3) ||
  845. (($endMonth * 100 + $endDay) >= (2 * 100 + 29))) {
  846. $leapDays += 1;
  847. }
  848. }
  849. } else {
  850. for ($year = $startYear; $year <= $endYear; ++$year) {
  851. if ($year == $startYear) {
  852. $startMonth = self::MONTHOFYEAR($startDate);
  853. $startDay = self::DAYOFMONTH($startDate);
  854. if ($startMonth < 3) {
  855. $leapDays += (self::isLeapYear($year)) ? 1 : 0;
  856. }
  857. } elseif ($year == $endYear) {
  858. $endMonth = self::MONTHOFYEAR($endDate);
  859. $endDay = self::DAYOFMONTH($endDate);
  860. if (($endMonth * 100 + $endDay) >= (2 * 100 + 29)) {
  861. $leapDays += (self::isLeapYear($year)) ? 1 : 0;
  862. }
  863. } else {
  864. $leapDays += (self::isLeapYear($year)) ? 1 : 0;
  865. }
  866. }
  867. if ($years == 2) {
  868. if (($leapDays == 0) && (self::isLeapYear($startYear)) && ($days > 365)) {
  869. $leapDays = 1;
  870. } elseif ($days < 366) {
  871. $years = 1;
  872. }
  873. }
  874. $leapDays /= $years;
  875. }
  876. return $days / (365 + $leapDays);
  877. case 2:
  878. return self::DATEDIF($startDate, $endDate) / 360;
  879. case 3:
  880. return self::DATEDIF($startDate, $endDate) / 365;
  881. case 4:
  882. return self::DAYS360($startDate, $endDate, true) / 360;
  883. }
  884. }
  885. return Functions::VALUE();
  886. }
  887. /**
  888. * NETWORKDAYS.
  889. *
  890. * Returns the number of whole working days between start_date and end_date. Working days
  891. * exclude weekends and any dates identified in holidays.
  892. * Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days
  893. * worked during a specific term.
  894. *
  895. * Excel Function:
  896. * NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]])
  897. *
  898. * @category Date/Time Functions
  899. *
  900. * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
  901. * PHP DateTime object, or a standard date string
  902. * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
  903. * PHP DateTime object, or a standard date string
  904. *
  905. * @return int|string Interval between the dates
  906. */
  907. public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs)
  908. {
  909. // Retrieve the mandatory start and end date that are referenced in the function definition
  910. $startDate = Functions::flattenSingleValue($startDate);
  911. $endDate = Functions::flattenSingleValue($endDate);
  912. // Get the optional days
  913. $dateArgs = Functions::flattenArray($dateArgs);
  914. // Validate the start and end dates
  915. if (is_string($startDate = $sDate = self::getDateValue($startDate))) {
  916. return Functions::VALUE();
  917. }
  918. $startDate = (float) floor($startDate);
  919. if (is_string($endDate = $eDate = self::getDateValue($endDate))) {
  920. return Functions::VALUE();
  921. }
  922. $endDate = (float) floor($endDate);
  923. if ($sDate > $eDate) {
  924. $startDate = $eDate;
  925. $endDate = $sDate;
  926. }
  927. // Execute function
  928. $startDoW = 6 - self::WEEKDAY($startDate, 2);
  929. if ($startDoW < 0) {
  930. $startDoW = 0;
  931. }
  932. $endDoW = self::WEEKDAY($endDate, 2);
  933. if ($endDoW >= 6) {
  934. $endDoW = 0;
  935. }
  936. $wholeWeekDays = floor(($endDate - $startDate) / 7) * 5;
  937. $partWeekDays = $endDoW + $startDoW;
  938. if ($partWeekDays > 5) {
  939. $partWeekDays -= 5;
  940. }
  941. // Test any extra holiday parameters
  942. $holidayCountedArray = [];
  943. foreach ($dateArgs as $holidayDate) {
  944. if (is_string($holidayDate = self::getDateValue($holidayDate))) {
  945. return Functions::VALUE();
  946. }
  947. if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) {
  948. if ((self::WEEKDAY($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) {
  949. --$partWeekDays;
  950. $holidayCountedArray[] = $holidayDate;
  951. }
  952. }
  953. }
  954. if ($sDate > $eDate) {
  955. return 0 - ($wholeWeekDays + $partWeekDays);
  956. }
  957. return $wholeWeekDays + $partWeekDays;
  958. }
  959. /**
  960. * WORKDAY.
  961. *
  962. * Returns the date that is the indicated number of working days before or after a date (the
  963. * starting date). Working days exclude weekends and any dates identified as holidays.
  964. * Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected
  965. * delivery times, or the number of days of work performed.
  966. *
  967. * Excel Function:
  968. * WORKDAY(startDate,endDays[,holidays[,holiday[,...]]])
  969. *
  970. * @category Date/Time Functions
  971. *
  972. * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
  973. * PHP DateTime object, or a standard date string
  974. * @param int $endDays The number of nonweekend and nonholiday days before or after
  975. * startDate. A positive value for days yields a future date; a
  976. * negative value yields a past date.
  977. *
  978. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  979. * depending on the value of the ReturnDateType flag
  980. */
  981. public static function WORKDAY($startDate, $endDays, ...$dateArgs)
  982. {
  983. // Retrieve the mandatory start date and days that are referenced in the function definition
  984. $startDate = Functions::flattenSingleValue($startDate);
  985. $endDays = Functions::flattenSingleValue($endDays);
  986. // Get the optional days
  987. $dateArgs = Functions::flattenArray($dateArgs);
  988. if ((is_string($startDate = self::getDateValue($startDate))) || (!is_numeric($endDays))) {
  989. return Functions::VALUE();
  990. }
  991. $startDate = (float) floor($startDate);
  992. $endDays = (int) floor($endDays);
  993. // If endDays is 0, we always return startDate
  994. if ($endDays == 0) {
  995. return $startDate;
  996. }
  997. $decrementing = $endDays < 0;
  998. // Adjust the start date if it falls over a weekend
  999. $startDoW = self::WEEKDAY($startDate, 3);
  1000. if (self::WEEKDAY($startDate, 3) >= 5) {
  1001. $startDate += ($decrementing) ? -$startDoW + 4 : 7 - $startDoW;
  1002. ($decrementing) ? $endDays++ : $endDays--;
  1003. }
  1004. // Add endDays
  1005. $endDate = (float) $startDate + ((int) ($endDays / 5) * 7) + ($endDays % 5);
  1006. // Adjust the calculated end date if it falls over a weekend
  1007. $endDoW = self::WEEKDAY($endDate, 3);
  1008. if ($endDoW >= 5) {
  1009. $endDate += ($decrementing) ? -$endDoW + 4 : 7 - $endDoW;
  1010. }
  1011. // Test any extra holiday parameters
  1012. if (!empty($dateArgs)) {
  1013. $holidayCountedArray = $holidayDates = [];
  1014. foreach ($dateArgs as $holidayDate) {
  1015. if (($holidayDate !== null) && (trim($holidayDate) > '')) {
  1016. if (is_string($holidayDate = self::getDateValue($holidayDate))) {
  1017. return Functions::VALUE();
  1018. }
  1019. if (self::WEEKDAY($holidayDate, 3) < 5) {
  1020. $holidayDates[] = $holidayDate;
  1021. }
  1022. }
  1023. }
  1024. if ($decrementing) {
  1025. rsort($holidayDates, SORT_NUMERIC);
  1026. } else {
  1027. sort($holidayDates, SORT_NUMERIC);
  1028. }
  1029. foreach ($holidayDates as $holidayDate) {
  1030. if ($decrementing) {
  1031. if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) {
  1032. if (!in_array($holidayDate, $holidayCountedArray)) {
  1033. --$endDate;
  1034. $holidayCountedArray[] = $holidayDate;
  1035. }
  1036. }
  1037. } else {
  1038. if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) {
  1039. if (!in_array($holidayDate, $holidayCountedArray)) {
  1040. ++$endDate;
  1041. $holidayCountedArray[] = $holidayDate;
  1042. }
  1043. }
  1044. }
  1045. // Adjust the calculated end date if it falls over a weekend
  1046. $endDoW = self::WEEKDAY($endDate, 3);
  1047. if ($endDoW >= 5) {
  1048. $endDate += ($decrementing) ? -$endDoW + 4 : 7 - $endDoW;
  1049. }
  1050. }
  1051. }
  1052. switch (Functions::getReturnDateType()) {
  1053. case Functions::RETURNDATE_EXCEL:
  1054. return (float) $endDate;
  1055. case Functions::RETURNDATE_PHP_NUMERIC:
  1056. return (int) Date::excelToTimestamp($endDate);
  1057. case Functions::RETURNDATE_PHP_OBJECT:
  1058. return Date::excelToDateTimeObject($endDate);
  1059. }
  1060. }
  1061. /**
  1062. * DAYOFMONTH.
  1063. *
  1064. * Returns the day of the month, for a specified date. The day is given as an integer
  1065. * ranging from 1 to 31.
  1066. *
  1067. * Excel Function:
  1068. * DAY(dateValue)
  1069. *
  1070. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1071. * PHP DateTime object, or a standard date string
  1072. *
  1073. * @return int|string Day of the month
  1074. */
  1075. public static function DAYOFMONTH($dateValue = 1)
  1076. {
  1077. $dateValue = Functions::flattenSingleValue($dateValue);
  1078. if ($dateValue === null) {
  1079. $dateValue = 1;
  1080. } elseif (is_string($dateValue = self::getDateValue($dateValue))) {
  1081. return Functions::VALUE();
  1082. }
  1083. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
  1084. if ($dateValue < 0.0) {
  1085. return Functions::NAN();
  1086. } elseif ($dateValue < 1.0) {
  1087. return 0;
  1088. }
  1089. }
  1090. // Execute function
  1091. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1092. return (int) $PHPDateObject->format('j');
  1093. }
  1094. /**
  1095. * WEEKDAY.
  1096. *
  1097. * Returns the day of the week for a specified date. The day is given as an integer
  1098. * ranging from 0 to 7 (dependent on the requested style).
  1099. *
  1100. * Excel Function:
  1101. * WEEKDAY(dateValue[,style])
  1102. *
  1103. * @param int $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1104. * PHP DateTime object, or a standard date string
  1105. * @param int $style A number that determines the type of return value
  1106. * 1 or omitted Numbers 1 (Sunday) through 7 (Saturday).
  1107. * 2 Numbers 1 (Monday) through 7 (Sunday).
  1108. * 3 Numbers 0 (Monday) through 6 (Sunday).
  1109. *
  1110. * @return int|string Day of the week value
  1111. */
  1112. public static function WEEKDAY($dateValue = 1, $style = 1)
  1113. {
  1114. $dateValue = Functions::flattenSingleValue($dateValue);
  1115. $style = Functions::flattenSingleValue($style);
  1116. if (!is_numeric($style)) {
  1117. return Functions::VALUE();
  1118. } elseif (($style < 1) || ($style > 3)) {
  1119. return Functions::NAN();
  1120. }
  1121. $style = floor($style);
  1122. if ($dateValue === null) {
  1123. $dateValue = 1;
  1124. } elseif (is_string($dateValue = self::getDateValue($dateValue))) {
  1125. return Functions::VALUE();
  1126. } elseif ($dateValue < 0.0) {
  1127. return Functions::NAN();
  1128. }
  1129. // Execute function
  1130. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1131. $DoW = $PHPDateObject->format('w');
  1132. $firstDay = 1;
  1133. switch ($style) {
  1134. case 1:
  1135. ++$DoW;
  1136. break;
  1137. case 2:
  1138. if ($DoW == 0) {
  1139. $DoW = 7;
  1140. }
  1141. break;
  1142. case 3:
  1143. if ($DoW == 0) {
  1144. $DoW = 7;
  1145. }
  1146. $firstDay = 0;
  1147. --$DoW;
  1148. break;
  1149. }
  1150. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
  1151. // Test for Excel's 1900 leap year, and introduce the error as required
  1152. if (($PHPDateObject->format('Y') == 1900) && ($PHPDateObject->format('n') <= 2)) {
  1153. --$DoW;
  1154. if ($DoW < $firstDay) {
  1155. $DoW += 7;
  1156. }
  1157. }
  1158. }
  1159. return (int) $DoW;
  1160. }
  1161. /**
  1162. * WEEKNUM.
  1163. *
  1164. * Returns the week of the year for a specified date.
  1165. * The WEEKNUM function considers the week containing January 1 to be the first week of the year.
  1166. * However, there is a European standard that defines the first week as the one with the majority
  1167. * of days (four or more) falling in the new year. This means that for years in which there are
  1168. * three days or less in the first week of January, the WEEKNUM function returns week numbers
  1169. * that are incorrect according to the European standard.
  1170. *
  1171. * Excel Function:
  1172. * WEEKNUM(dateValue[,style])
  1173. *
  1174. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1175. * PHP DateTime object, or a standard date string
  1176. * @param int $method Week begins on Sunday or Monday
  1177. * 1 or omitted Week begins on Sunday.
  1178. * 2 Week begins on Monday.
  1179. *
  1180. * @return int|string Week Number
  1181. */
  1182. public static function WEEKNUM($dateValue = 1, $method = 1)
  1183. {
  1184. $dateValue = Functions::flattenSingleValue($dateValue);
  1185. $method = Functions::flattenSingleValue($method);
  1186. if (!is_numeric($method)) {
  1187. return Functions::VALUE();
  1188. } elseif (($method < 1) || ($method > 2)) {
  1189. return Functions::NAN();
  1190. }
  1191. $method = floor($method);
  1192. if ($dateValue === null) {
  1193. $dateValue = 1;
  1194. } elseif (is_string($dateValue = self::getDateValue($dateValue))) {
  1195. return Functions::VALUE();
  1196. } elseif ($dateValue < 0.0) {
  1197. return Functions::NAN();
  1198. }
  1199. // Execute function
  1200. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1201. $dayOfYear = $PHPDateObject->format('z');
  1202. $PHPDateObject->modify('-' . $dayOfYear . ' days');
  1203. $firstDayOfFirstWeek = $PHPDateObject->format('w');
  1204. $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7;
  1205. $interval = $dayOfYear - $daysInFirstWeek;
  1206. $weekOfYear = floor($interval / 7) + 1;
  1207. if ($daysInFirstWeek) {
  1208. ++$weekOfYear;
  1209. }
  1210. return (int) $weekOfYear;
  1211. }
  1212. /**
  1213. * ISOWEEKNUM.
  1214. *
  1215. * Returns the ISO 8601 week number of the year for a specified date.
  1216. *
  1217. * Excel Function:
  1218. * ISOWEEKNUM(dateValue)
  1219. *
  1220. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1221. * PHP DateTime object, or a standard date string
  1222. *
  1223. * @return int|string Week Number
  1224. */
  1225. public static function ISOWEEKNUM($dateValue = 1)
  1226. {
  1227. $dateValue = Functions::flattenSingleValue($dateValue);
  1228. if ($dateValue === null) {
  1229. $dateValue = 1;
  1230. } elseif (is_string($dateValue = self::getDateValue($dateValue))) {
  1231. return Functions::VALUE();
  1232. } elseif ($dateValue < 0.0) {
  1233. return Functions::NAN();
  1234. }
  1235. // Execute function
  1236. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1237. return (int) $PHPDateObject->format('W');
  1238. }
  1239. /**
  1240. * MONTHOFYEAR.
  1241. *
  1242. * Returns the month of a date represented by a serial number.
  1243. * The month is given as an integer, ranging from 1 (January) to 12 (December).
  1244. *
  1245. * Excel Function:
  1246. * MONTH(dateValue)
  1247. *
  1248. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1249. * PHP DateTime object, or a standard date string
  1250. *
  1251. * @return int|string Month of the year
  1252. */
  1253. public static function MONTHOFYEAR($dateValue = 1)
  1254. {
  1255. $dateValue = Functions::flattenSingleValue($dateValue);
  1256. if (empty($dateValue)) {
  1257. $dateValue = 1;
  1258. }
  1259. if (is_string($dateValue = self::getDateValue($dateValue))) {
  1260. return Functions::VALUE();
  1261. } elseif ($dateValue < 0.0) {
  1262. return Functions::NAN();
  1263. }
  1264. // Execute function
  1265. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1266. return (int) $PHPDateObject->format('n');
  1267. }
  1268. /**
  1269. * YEAR.
  1270. *
  1271. * Returns the year corresponding to a date.
  1272. * The year is returned as an integer in the range 1900-9999.
  1273. *
  1274. * Excel Function:
  1275. * YEAR(dateValue)
  1276. *
  1277. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1278. * PHP DateTime object, or a standard date string
  1279. *
  1280. * @return int|string Year
  1281. */
  1282. public static function YEAR($dateValue = 1)
  1283. {
  1284. $dateValue = Functions::flattenSingleValue($dateValue);
  1285. if ($dateValue === null) {
  1286. $dateValue = 1;
  1287. } elseif (is_string($dateValue = self::getDateValue($dateValue))) {
  1288. return Functions::VALUE();
  1289. } elseif ($dateValue < 0.0) {
  1290. return Functions::NAN();
  1291. }
  1292. // Execute function
  1293. $PHPDateObject = Date::excelToDateTimeObject($dateValue);
  1294. return (int) $PHPDateObject->format('Y');
  1295. }
  1296. /**
  1297. * HOUROFDAY.
  1298. *
  1299. * Returns the hour of a time value.
  1300. * The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.).
  1301. *
  1302. * Excel Function:
  1303. * HOUR(timeValue)
  1304. *
  1305. * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
  1306. * PHP DateTime object, or a standard time string
  1307. *
  1308. * @return int|string Hour
  1309. */
  1310. public static function HOUROFDAY($timeValue = 0)
  1311. {
  1312. $timeValue = Functions::flattenSingleValue($timeValue);
  1313. if (!is_numeric($timeValue)) {
  1314. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
  1315. $testVal = strtok($timeValue, '/-: ');
  1316. if (strlen($testVal) < strlen($timeValue)) {
  1317. return Functions::VALUE();
  1318. }
  1319. }
  1320. $timeValue = self::getTimeValue($timeValue);
  1321. if (is_string($timeValue)) {
  1322. return Functions::VALUE();
  1323. }
  1324. }
  1325. // Execute function
  1326. if ($timeValue >= 1) {
  1327. $timeValue = fmod($timeValue, 1);
  1328. } elseif ($timeValue < 0.0) {
  1329. return Functions::NAN();
  1330. }
  1331. $timeValue = Date::excelToTimestamp($timeValue);
  1332. return (int) gmdate('G', $timeValue);
  1333. }
  1334. /**
  1335. * MINUTE.
  1336. *
  1337. * Returns the minutes of a time value.
  1338. * The minute is given as an integer, ranging from 0 to 59.
  1339. *
  1340. * Excel Function:
  1341. * MINUTE(timeValue)
  1342. *
  1343. * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
  1344. * PHP DateTime object, or a standard time string
  1345. *
  1346. * @return int|string Minute
  1347. */
  1348. public static function MINUTE($timeValue = 0)
  1349. {
  1350. $timeValue = $timeTester = Functions::flattenSingleValue($timeValue);
  1351. if (!is_numeric($timeValue)) {
  1352. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
  1353. $testVal = strtok($timeValue, '/-: ');
  1354. if (strlen($testVal) < strlen($timeValue)) {
  1355. return Functions::VALUE();
  1356. }
  1357. }
  1358. $timeValue = self::getTimeValue($timeValue);
  1359. if (is_string($timeValue)) {
  1360. return Functions::VALUE();
  1361. }
  1362. }
  1363. // Execute function
  1364. if ($timeValue >= 1) {
  1365. $timeValue = fmod($timeValue, 1);
  1366. } elseif ($timeValue < 0.0) {
  1367. return Functions::NAN();
  1368. }
  1369. $timeValue = Date::excelToTimestamp($timeValue);
  1370. return (int) gmdate('i', $timeValue);
  1371. }
  1372. /**
  1373. * SECOND.
  1374. *
  1375. * Returns the seconds of a time value.
  1376. * The second is given as an integer in the range 0 (zero) to 59.
  1377. *
  1378. * Excel Function:
  1379. * SECOND(timeValue)
  1380. *
  1381. * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
  1382. * PHP DateTime object, or a standard time string
  1383. *
  1384. * @return int|string Second
  1385. */
  1386. public static function SECOND($timeValue = 0)
  1387. {
  1388. $timeValue = Functions::flattenSingleValue($timeValue);
  1389. if (!is_numeric($timeValue)) {
  1390. if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
  1391. $testVal = strtok($timeValue, '/-: ');
  1392. if (strlen($testVal) < strlen($timeValue)) {
  1393. return Functions::VALUE();
  1394. }
  1395. }
  1396. $timeValue = self::getTimeValue($timeValue);
  1397. if (is_string($timeValue)) {
  1398. return Functions::VALUE();
  1399. }
  1400. }
  1401. // Execute function
  1402. if ($timeValue >= 1) {
  1403. $timeValue = fmod($timeValue, 1);
  1404. } elseif ($timeValue < 0.0) {
  1405. return Functions::NAN();
  1406. }
  1407. $timeValue = Date::excelToTimestamp($timeValue);
  1408. return (int) gmdate('s', $timeValue);
  1409. }
  1410. /**
  1411. * EDATE.
  1412. *
  1413. * Returns the serial number that represents the date that is the indicated number of months
  1414. * before or after a specified date (the start_date).
  1415. * Use EDATE to calculate maturity dates or due dates that fall on the same day of the month
  1416. * as the date of issue.
  1417. *
  1418. * Excel Function:
  1419. * EDATE(dateValue,adjustmentMonths)
  1420. *
  1421. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1422. * PHP DateTime object, or a standard date string
  1423. * @param int $adjustmentMonths The number of months before or after start_date.
  1424. * A positive value for months yields a future date;
  1425. * a negative value yields a past date.
  1426. *
  1427. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  1428. * depending on the value of the ReturnDateType flag
  1429. */
  1430. public static function EDATE($dateValue = 1, $adjustmentMonths = 0)
  1431. {
  1432. $dateValue = Functions::flattenSingleValue($dateValue);
  1433. $adjustmentMonths = Functions::flattenSingleValue($adjustmentMonths);
  1434. if (!is_numeric($adjustmentMonths)) {
  1435. return Functions::VALUE();
  1436. }
  1437. $adjustmentMonths = floor($adjustmentMonths);
  1438. if (is_string($dateValue = self::getDateValue($dateValue))) {
  1439. return Functions::VALUE();
  1440. }
  1441. // Execute function
  1442. $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths);
  1443. switch (Functions::getReturnDateType()) {
  1444. case Functions::RETURNDATE_EXCEL:
  1445. return (float) Date::PHPToExcel($PHPDateObject);
  1446. case Functions::RETURNDATE_PHP_NUMERIC:
  1447. return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject));
  1448. case Functions::RETURNDATE_PHP_OBJECT:
  1449. return $PHPDateObject;
  1450. }
  1451. }
  1452. /**
  1453. * EOMONTH.
  1454. *
  1455. * Returns the date value for the last day of the month that is the indicated number of months
  1456. * before or after start_date.
  1457. * Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month.
  1458. *
  1459. * Excel Function:
  1460. * EOMONTH(dateValue,adjustmentMonths)
  1461. *
  1462. * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
  1463. * PHP DateTime object, or a standard date string
  1464. * @param int $adjustmentMonths The number of months before or after start_date.
  1465. * A positive value for months yields a future date;
  1466. * a negative value yields a past date.
  1467. *
  1468. * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
  1469. * depending on the value of the ReturnDateType flag
  1470. */
  1471. public static function EOMONTH($dateValue = 1, $adjustmentMonths = 0)
  1472. {
  1473. $dateValue = Functions::flattenSingleValue($dateValue);
  1474. $adjustmentMonths = Functions::flattenSingleValue($adjustmentMonths);
  1475. if (!is_numeric($adjustmentMonths)) {
  1476. return Functions::VALUE();
  1477. }
  1478. $adjustmentMonths = floor($adjustmentMonths);
  1479. if (is_string($dateValue = self::getDateValue($dateValue))) {
  1480. return Functions::VALUE();
  1481. }
  1482. // Execute function
  1483. $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths + 1);
  1484. $adjustDays = (int) $PHPDateObject->format('d');
  1485. $adjustDaysString = '-' . $adjustDays . ' days';
  1486. $PHPDateObject->modify($adjustDaysString);
  1487. switch (Functions::getReturnDateType()) {
  1488. case Functions::RETURNDATE_EXCEL:
  1489. return (float) Date::PHPToExcel($PHPDateObject);
  1490. case Functions::RETURNDATE_PHP_NUMERIC:
  1491. return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject));
  1492. case Functions::RETURNDATE_PHP_OBJECT:
  1493. return $PHPDateObject;
  1494. }
  1495. }
  1496. }