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.
This commit is contained in:
oleibman 2020-02-19 11:22:31 -08:00 committed by GitHub
parent 0c52f173aa
commit cb18163a1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 376 additions and 45 deletions

View File

@ -878,6 +878,8 @@ class DateTime
* *
* Excel Function: * Excel Function:
* YEARFRAC(startDate,endDate[,method]) * 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 * @category Date/Time Functions
* *
@ -906,6 +908,11 @@ class DateTime
if (is_string($endDate = self::getDateValue($endDate))) { if (is_string($endDate = self::getDateValue($endDate))) {
return Functions::VALUE(); return Functions::VALUE();
} }
if ($startDate > $endDate) {
$temp = $startDate;
$startDate = $endDate;
$endDate = $temp;
}
if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) { if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) {
switch ($method) { switch ($method) {
@ -916,46 +923,43 @@ class DateTime
$startYear = self::YEAR($startDate); $startYear = self::YEAR($startDate);
$endYear = self::YEAR($endDate); $endYear = self::YEAR($endDate);
$years = $endYear - $startYear + 1; $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 ($years == 1) {
if (self::isLeapYear($endYear)) { if (self::isLeapYear($endYear)) {
$startMonth = self::MONTHOFYEAR($startDate); $tmpCalcAnnualBasis = 366;
$endMonth = self::MONTHOFYEAR($endDate); } else {
$endDay = self::DAYOFMONTH($endDate); $tmpCalcAnnualBasis = 365;
if (($startMonth < 3) || }
(($endMonth * 100 + $endDay) >= (2 * 100 + 29))) { } elseif ($years == 2 && $startMonthDay >= $endMonthDay) {
$leapDays += 1; 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 { } else {
$tmpCalcAnnualBasis = 0;
for ($year = $startYear; $year <= $endYear; ++$year) { for ($year = $startYear; $year <= $endYear; ++$year) {
if ($year == $startYear) { $tmpCalcAnnualBasis += self::isLeapYear($year) ? 366 : 365;
$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;
}
} }
if ($years == 2) { $tmpCalcAnnualBasis /= $years;
if (($leapDays == 0) && (self::isLeapYear($startYear)) && ($days > 365)) {
$leapDays = 1;
} elseif ($days < 366) {
$years = 1;
}
}
$leapDays /= $years;
} }
return $days / (365 + $leapDays); return $days / $tmpCalcAnnualBasis;
case 2: case 2:
return self::DATEDIF($startDate, $endDate) / 360; return self::DATEDIF($startDate, $endDate) / 360;
case 3: case 3:
@ -1273,6 +1277,36 @@ class DateTime
return $DoW; 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. * WEEKNUM.
* *
@ -1291,41 +1325,51 @@ class DateTime
* @param int $method Week begins on Sunday or Monday * @param int $method Week begins on Sunday or Monday
* 1 or omitted Week begins on Sunday. * 1 or omitted Week begins on Sunday.
* 2 Week begins on Monday. * 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 * @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); $dateValue = Functions::flattenSingleValue($dateValue);
$method = Functions::flattenSingleValue($method); $method = Functions::flattenSingleValue($method);
if (!is_numeric($method)) { if (!is_numeric($method)) {
return Functions::VALUE(); 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 = self::getDateValue($dateValue);
$dateValue = 1; if (is_string($dateValue)) {
} elseif (is_string($dateValue = self::getDateValue($dateValue))) {
return Functions::VALUE(); return Functions::VALUE();
} elseif ($dateValue < 0.0) { }
if ($dateValue < 0.0) {
return Functions::NAN(); return Functions::NAN();
} }
// Execute function // Execute function
$PHPDateObject = Date::excelToDateTimeObject($dateValue); $PHPDateObject = Date::excelToDateTimeObject($dateValue);
if ($method == self::STARTWEEK_MONDAY_ISO) {
return (int) $PHPDateObject->format('W');
}
$dayOfYear = $PHPDateObject->format('z'); $dayOfYear = $PHPDateObject->format('z');
$PHPDateObject->modify('-' . $dayOfYear . ' days'); $PHPDateObject->modify('-' . $dayOfYear . ' days');
$firstDayOfFirstWeek = $PHPDateObject->format('w'); $firstDayOfFirstWeek = $PHPDateObject->format('w');
$daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7;
$interval = $dayOfYear - $daysInFirstWeek; $daysInFirstWeek += 7 * !$daysInFirstWeek;
$weekOfYear = floor($interval / 7) + 1; $endFirstWeek = $daysInFirstWeek - 1;
$weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7);
if ($daysInFirstWeek) {
++$weekOfYear;
}
return (int) $weekOfYear; return (int) $weekOfYear;
} }

View File

@ -53,6 +53,10 @@ return [
'#NUM!', '#NUM!',
'3/7/1977', 0, '3/7/1977', 0,
], ],
[
'#NUM!',
'3/7/1977', -1,
],
[ [
'#VALUE!', '#VALUE!',
'Invalid', 1, 'Invalid', 1,
@ -61,4 +65,112 @@ return [
'#NUM!', '#NUM!',
-1, -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,
],
]; ];

View File

@ -7,6 +7,12 @@ return [
'2007-1-10', '2007-1-10',
0, 0,
], ],
[
0.025,
'2007-1-10',
'2007-1-1',
0,
],
[ [
0.024657534246580001, 0.024657534246580001,
'2007-1-1', '2007-1-1',
@ -337,6 +343,12 @@ return [
'2008-6-28', '2008-6-28',
0, 0,
], ],
[
47.52162252765670,
'1960-12-19',
'2008-6-28',
1,
],
[ [
48.216666666666697, 48.216666666666697,
'1960-12-19', '1960-12-19',
@ -385,4 +397,167 @@ return [
'2008-6-28', '2008-6-28',
4, 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,
],
]; ];