From cb18163a1ddaf374babcf39373fbf3ecdacd4291 Mon Sep 17 00:00:00 2001 From: oleibman Date: Wed, 19 Feb 2020 11:22:31 -0800 Subject: [PATCH] Changes to WEEKNUM and YEARFRAC (#1316) * Changes to WEEKNUM and YEARFRAC The optional second parameter for WEEKNUM can take any of 10 values (1, 2, 11-17, and 21), but currently only 1 and 2 are supported. This change adds support for the other 8 possibilities. YEARFRAC in Excel does not require that end date be before start date, but PhpSpreadsheet was returning an error in that situation. YEARFRAC third parameter (method) of 1 was not correctly implemented. I was able to find a description of the algorithm, and documented that location in the code, and implemented according to that spec. PHPExcel had a (failing) test to assert the result of YEARFRAC("1960-12-19", "2008-06-28", 1). This test had been dropped from PhpSpreadsheet, and is now restored; several new tests have been added, and verified against Excel. * Add YEARFRAC Tests Scrutinizer reported a very mysterious failure with no details. project.metric_change("scrutinizer.test_coverage", < 0), without even a link to explain what it is reporting. It is possible that it was a complaint about code coverage. If so, I have added some tests which will, I hope, eliminate the problem. * Make Array Constant Responding to review from Mark Baker. * Merge with PR 1362 Bugfix 1161 Travis CI reported problem with Calculation.php (which is not part of this change). That was changed in master a few days ago (delete some unused code). Perhaps the lack of that change is the problem here. Merging it manually. --- src/PhpSpreadsheet/Calculation/DateTime.php | 134 +++++++++----- tests/data/Calculation/DateTime/WEEKNUM.php | 112 ++++++++++++ tests/data/Calculation/DateTime/YEARFRAC.php | 175 +++++++++++++++++++ 3 files changed, 376 insertions(+), 45 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 55740fc1..4f85edeb 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -878,6 +878,8 @@ class DateTime * * Excel Function: * YEARFRAC(startDate,endDate[,method]) + * See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html + * for description of algorithm used in Excel * * @category Date/Time Functions * @@ -906,6 +908,11 @@ class DateTime if (is_string($endDate = self::getDateValue($endDate))) { return Functions::VALUE(); } + if ($startDate > $endDate) { + $temp = $startDate; + $startDate = $endDate; + $endDate = $temp; + } if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) { switch ($method) { @@ -916,46 +923,43 @@ class DateTime $startYear = self::YEAR($startDate); $endYear = self::YEAR($endDate); $years = $endYear - $startYear + 1; - $leapDays = 0; + $startMonth = self::MONTHOFYEAR($startDate); + $startDay = self::DAYOFMONTH($startDate); + $endMonth = self::MONTHOFYEAR($endDate); + $endDay = self::DAYOFMONTH($endDate); + $startMonthDay = 100 * $startMonth + $startDay; + $endMonthDay = 100 * $endMonth + $endDay; if ($years == 1) { if (self::isLeapYear($endYear)) { - $startMonth = self::MONTHOFYEAR($startDate); - $endMonth = self::MONTHOFYEAR($endDate); - $endDay = self::DAYOFMONTH($endDate); - if (($startMonth < 3) || - (($endMonth * 100 + $endDay) >= (2 * 100 + 29))) { - $leapDays += 1; + $tmpCalcAnnualBasis = 366; + } else { + $tmpCalcAnnualBasis = 365; + } + } elseif ($years == 2 && $startMonthDay >= $endMonthDay) { + if (self::isLeapYear($startYear)) { + if ($startMonthDay <= 229) { + $tmpCalcAnnualBasis = 366; + } else { + $tmpCalcAnnualBasis = 365; } + } elseif (self::isLeapYear($endYear)) { + if ($endMonthDay >= 229) { + $tmpCalcAnnualBasis = 366; + } else { + $tmpCalcAnnualBasis = 365; + } + } else { + $tmpCalcAnnualBasis = 365; } } else { + $tmpCalcAnnualBasis = 0; for ($year = $startYear; $year <= $endYear; ++$year) { - if ($year == $startYear) { - $startMonth = self::MONTHOFYEAR($startDate); - $startDay = self::DAYOFMONTH($startDate); - if ($startMonth < 3) { - $leapDays += (self::isLeapYear($year)) ? 1 : 0; - } - } elseif ($year == $endYear) { - $endMonth = self::MONTHOFYEAR($endDate); - $endDay = self::DAYOFMONTH($endDate); - if (($endMonth * 100 + $endDay) >= (2 * 100 + 29)) { - $leapDays += (self::isLeapYear($year)) ? 1 : 0; - } - } else { - $leapDays += (self::isLeapYear($year)) ? 1 : 0; - } + $tmpCalcAnnualBasis += self::isLeapYear($year) ? 366 : 365; } - if ($years == 2) { - if (($leapDays == 0) && (self::isLeapYear($startYear)) && ($days > 365)) { - $leapDays = 1; - } elseif ($days < 366) { - $years = 1; - } - } - $leapDays /= $years; + $tmpCalcAnnualBasis /= $years; } - return $days / (365 + $leapDays); + return $days / $tmpCalcAnnualBasis; case 2: return self::DATEDIF($startDate, $endDate) / 360; case 3: @@ -1273,6 +1277,36 @@ class DateTime return $DoW; } + const STARTWEEK_SUNDAY = 1; + const STARTWEEK_MONDAY = 2; + const STARTWEEK_MONDAY_ALT = 11; + const STARTWEEK_TUESDAY = 12; + const STARTWEEK_WEDNESDAY = 13; + const STARTWEEK_THURSDAY = 14; + const STARTWEEK_FRIDAY = 15; + const STARTWEEK_SATURDAY = 16; + const STARTWEEK_SUNDAY_ALT = 17; + const DOW_SUNDAY = 1; + const DOW_MONDAY = 2; + const DOW_TUESDAY = 3; + const DOW_WEDNESDAY = 4; + const DOW_THURSDAY = 5; + const DOW_FRIDAY = 6; + const DOW_SATURDAY = 7; + const STARTWEEK_MONDAY_ISO = 21; + const METHODARR = [ + self::STARTWEEK_SUNDAY => self::DOW_SUNDAY, + self::DOW_MONDAY, + self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY, + self::DOW_TUESDAY, + self::DOW_WEDNESDAY, + self::DOW_THURSDAY, + self::DOW_FRIDAY, + self::DOW_SATURDAY, + self::DOW_SUNDAY, + self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO, + ]; + /** * WEEKNUM. * @@ -1291,41 +1325,51 @@ class DateTime * @param int $method Week begins on Sunday or Monday * 1 or omitted Week begins on Sunday. * 2 Week begins on Monday. + * 11 Week begins on Monday. + * 12 Week begins on Tuesday. + * 13 Week begins on Wednesday. + * 14 Week begins on Thursday. + * 15 Week begins on Friday. + * 16 Week begins on Saturday. + * 17 Week begins on Sunday. + * 21 ISO (Jan. 4 is week 1, begins on Monday). * * @return int|string Week Number */ - public static function WEEKNUM($dateValue = 1, $method = 1) + public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) { $dateValue = Functions::flattenSingleValue($dateValue); $method = Functions::flattenSingleValue($method); if (!is_numeric($method)) { return Functions::VALUE(); - } elseif (($method < 1) || ($method > 2)) { - return Functions::NAN(); } - $method = floor($method); + $method = (int) $method; + if (!array_key_exists($method, self::METHODARR)) { + return Functions::NaN(); + } + $method = self::METHODARR[$method]; - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + $dateValue = self::getDateValue($dateValue); + if (is_string($dateValue)) { return Functions::VALUE(); - } elseif ($dateValue < 0.0) { + } + if ($dateValue < 0.0) { return Functions::NAN(); } // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); + if ($method == self::STARTWEEK_MONDAY_ISO) { + return (int) $PHPDateObject->format('W'); + } $dayOfYear = $PHPDateObject->format('z'); $PHPDateObject->modify('-' . $dayOfYear . ' days'); $firstDayOfFirstWeek = $PHPDateObject->format('w'); $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; - $interval = $dayOfYear - $daysInFirstWeek; - $weekOfYear = floor($interval / 7) + 1; - - if ($daysInFirstWeek) { - ++$weekOfYear; - } + $daysInFirstWeek += 7 * !$daysInFirstWeek; + $endFirstWeek = $daysInFirstWeek - 1; + $weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); return (int) $weekOfYear; } diff --git a/tests/data/Calculation/DateTime/WEEKNUM.php b/tests/data/Calculation/DateTime/WEEKNUM.php index ceb84315..d73ee463 100644 --- a/tests/data/Calculation/DateTime/WEEKNUM.php +++ b/tests/data/Calculation/DateTime/WEEKNUM.php @@ -53,6 +53,10 @@ return [ '#NUM!', '3/7/1977', 0, ], + [ + '#NUM!', + '3/7/1977', -1, + ], [ '#VALUE!', 'Invalid', 1, @@ -61,4 +65,112 @@ return [ '#NUM!', -1, ], + [ + 53, + '2019-12-29', 1, + ], + [ + 52, + '2019-12-29', 2, + ], + [ + '#NUM!', + '2019-12-29', 3, + ], + [ + '#NUM!', + '2019-12-29', 10, + ], + [ + 52, + '2019-12-29', 11, + ], + [ + 52, + '2019-12-29', 12, + ], + [ + 53, + '2019-12-29', 13, + ], + [ + 53, + '2019-12-29', 14, + ], + [ + 53, + '2019-12-29', 15, + ], + [ + 53, + '2019-12-29', 16, + ], + [ + 53, + '2019-12-29', 17, + ], + [ + '#NUM!', + '2019-12-29', 18, + ], + [ + '#NUM!', + '2019-12-29', 20, + ], + [ + '#NUM!', + '2019-12-29', 22, + ], + [ + 52, + '2019-12-29', 21, + ], + [ + 53, + '2020-12-29', 21, + ], + [ + 52, + '2021-12-29', 21, + ], + [ + 52, + '2022-12-29', 21, + ], + [ + 1, + '2020-01-01', 21, + ], + [ + 53, + '2021-01-01', 21, + ], + [ + 52, + '2022-01-01', 21, + ], + [ + 52, + '2023-01-01', 21, + ], + [ + 2, + '2020-01-08', 21, + ], + [ + 1, + '2021-01-08', 21, + ], + [ + 1, + '2022-01-08', 21, + ], + [ + 1, + '2023-01-08', 21, + ], + [ + 1, + '2025-12-29', 21, + ], ]; diff --git a/tests/data/Calculation/DateTime/YEARFRAC.php b/tests/data/Calculation/DateTime/YEARFRAC.php index 9cb7005a..3e76087c 100644 --- a/tests/data/Calculation/DateTime/YEARFRAC.php +++ b/tests/data/Calculation/DateTime/YEARFRAC.php @@ -7,6 +7,12 @@ return [ '2007-1-10', 0, ], + [ + 0.025, + '2007-1-10', + '2007-1-1', + 0, + ], [ 0.024657534246580001, '2007-1-1', @@ -337,6 +343,12 @@ return [ '2008-6-28', 0, ], + [ + 47.52162252765670, + '1960-12-19', + '2008-6-28', + 1, + ], [ 48.216666666666697, '1960-12-19', @@ -385,4 +397,167 @@ return [ '2008-6-28', 4, ], + [ + 0.163934426, + '1960-01-01', + '1960-03-01', + 1, + ], + [ + 0.161643836, + '1961-01-01', + '1961-03-01', + 1, + ], + [ + 0.161643836, + '1963-03-01', + '1963-01-01', + 1, + ], + [ + 1.086183311, + '1960-01-01', + '1961-02-01', + 1, + ], + [ + 1.084931507, + '1961-01-01', + '1962-02-01', + 1, + ], + [ + 1.083447332, + '1963-01-01', + '1964-02-01', + 1, + ], + [ + 1.162790698, + '1963-01-01', + '1964-03-01', + 1, + ], + [ + 0.841530055, + '2020-02-28', + '2021-01-01', + 1, + ], + [ + 0.764383562, + '2020-03-28', + '2021-01-01', + 1, + ], + [ + 0.841530055, + '2023-04-28', + '2024-03-01', + 1, + ], + [ + 0.838797814, + '2023-04-28', + '2024-02-29', + 1, + ], + [ + 0.838356164, + '2023-04-28', + '2024-02-28', + 1, + ], + [ + 0.753424658, + '2023-04-28', + '2024-01-28', + 1, + ], + [ + 0.753424658, + '2022-04-28', + '2023-01-28', + 1, + ], + [ + 1.0, + '2020-01-01', + '2021-01-01', + 1, + ], + [ + 0.99726776, + '2020-02-28', + '2021-02-27', + 1, + ], + [ + 0.764383562, + '2020-03-28', + '2021-01-01', + 1, + ], + [ + 0.841530055, + '2023-04-28', + '2024-03-01', + 1, + ], + [ + 0.838797814, + '2023-04-28', + '2024-02-29', + 1, + ], + [ + 0.838356164, + '2023-04-28', + '2024-02-28', + 1, + ], + [ + 0.753424658, + '2023-04-28', + '2024-01-28', + 1, + ], + [ + 0.753424658, + '2022-04-28', + '2023-01-28', + 1, + ], + [ + 1.082191781, + '2022-04-28', + '2023-05-28', + 1, + ], + [ + 1.002739726, + '2022-04-27', + '2023-04-28', + 1, + ], + [ + 0.084699454, + '2024-04-27', + '2024-05-28', + 1, + ], + [ + 0.084931507, + '2023-04-27', + '2023-05-28', + 1, + ], + [ + 2.085766423, + '2023-04-27', + '2025-05-28', + 1, + ], + ];