Skip to content

Commit cb18163

Browse files
authored
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.
1 parent 0c52f17 commit cb18163

File tree

3 files changed

+376
-45
lines changed

3 files changed

+376
-45
lines changed

src/PhpSpreadsheet/Calculation/DateTime.php

+89-45
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,8 @@ public static function DAYS360($startDate = 0, $endDate = 0, $method = false)
878878
*
879879
* Excel Function:
880880
* YEARFRAC(startDate,endDate[,method])
881+
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html
882+
* for description of algorithm used in Excel
881883
*
882884
* @category Date/Time Functions
883885
*
@@ -906,6 +908,11 @@ public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0)
906908
if (is_string($endDate = self::getDateValue($endDate))) {
907909
return Functions::VALUE();
908910
}
911+
if ($startDate > $endDate) {
912+
$temp = $startDate;
913+
$startDate = $endDate;
914+
$endDate = $temp;
915+
}
909916

910917
if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) {
911918
switch ($method) {
@@ -916,46 +923,43 @@ public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0)
916923
$startYear = self::YEAR($startDate);
917924
$endYear = self::YEAR($endDate);
918925
$years = $endYear - $startYear + 1;
919-
$leapDays = 0;
926+
$startMonth = self::MONTHOFYEAR($startDate);
927+
$startDay = self::DAYOFMONTH($startDate);
928+
$endMonth = self::MONTHOFYEAR($endDate);
929+
$endDay = self::DAYOFMONTH($endDate);
930+
$startMonthDay = 100 * $startMonth + $startDay;
931+
$endMonthDay = 100 * $endMonth + $endDay;
920932
if ($years == 1) {
921933
if (self::isLeapYear($endYear)) {
922-
$startMonth = self::MONTHOFYEAR($startDate);
923-
$endMonth = self::MONTHOFYEAR($endDate);
924-
$endDay = self::DAYOFMONTH($endDate);
925-
if (($startMonth < 3) ||
926-
(($endMonth * 100 + $endDay) >= (2 * 100 + 29))) {
927-
$leapDays += 1;
928-
}
934+
$tmpCalcAnnualBasis = 366;
935+
} else {
936+
$tmpCalcAnnualBasis = 365;
929937
}
930-
} else {
931-
for ($year = $startYear; $year <= $endYear; ++$year) {
932-
if ($year == $startYear) {
933-
$startMonth = self::MONTHOFYEAR($startDate);
934-
$startDay = self::DAYOFMONTH($startDate);
935-
if ($startMonth < 3) {
936-
$leapDays += (self::isLeapYear($year)) ? 1 : 0;
937-
}
938-
} elseif ($year == $endYear) {
939-
$endMonth = self::MONTHOFYEAR($endDate);
940-
$endDay = self::DAYOFMONTH($endDate);
941-
if (($endMonth * 100 + $endDay) >= (2 * 100 + 29)) {
942-
$leapDays += (self::isLeapYear($year)) ? 1 : 0;
943-
}
938+
} elseif ($years == 2 && $startMonthDay >= $endMonthDay) {
939+
if (self::isLeapYear($startYear)) {
940+
if ($startMonthDay <= 229) {
941+
$tmpCalcAnnualBasis = 366;
944942
} else {
945-
$leapDays += (self::isLeapYear($year)) ? 1 : 0;
943+
$tmpCalcAnnualBasis = 365;
946944
}
947-
}
948-
if ($years == 2) {
949-
if (($leapDays == 0) && (self::isLeapYear($startYear)) && ($days > 365)) {
950-
$leapDays = 1;
951-
} elseif ($days < 366) {
952-
$years = 1;
945+
} elseif (self::isLeapYear($endYear)) {
946+
if ($endMonthDay >= 229) {
947+
$tmpCalcAnnualBasis = 366;
948+
} else {
949+
$tmpCalcAnnualBasis = 365;
953950
}
951+
} else {
952+
$tmpCalcAnnualBasis = 365;
954953
}
955-
$leapDays /= $years;
954+
} else {
955+
$tmpCalcAnnualBasis = 0;
956+
for ($year = $startYear; $year <= $endYear; ++$year) {
957+
$tmpCalcAnnualBasis += self::isLeapYear($year) ? 366 : 365;
958+
}
959+
$tmpCalcAnnualBasis /= $years;
956960
}
957961

958-
return $days / (365 + $leapDays);
962+
return $days / $tmpCalcAnnualBasis;
959963
case 2:
960964
return self::DATEDIF($startDate, $endDate) / 360;
961965
case 3:
@@ -1273,6 +1277,36 @@ public static function WEEKDAY($dateValue = 1, $style = 1)
12731277
return $DoW;
12741278
}
12751279

1280+
const STARTWEEK_SUNDAY = 1;
1281+
const STARTWEEK_MONDAY = 2;
1282+
const STARTWEEK_MONDAY_ALT = 11;
1283+
const STARTWEEK_TUESDAY = 12;
1284+
const STARTWEEK_WEDNESDAY = 13;
1285+
const STARTWEEK_THURSDAY = 14;
1286+
const STARTWEEK_FRIDAY = 15;
1287+
const STARTWEEK_SATURDAY = 16;
1288+
const STARTWEEK_SUNDAY_ALT = 17;
1289+
const DOW_SUNDAY = 1;
1290+
const DOW_MONDAY = 2;
1291+
const DOW_TUESDAY = 3;
1292+
const DOW_WEDNESDAY = 4;
1293+
const DOW_THURSDAY = 5;
1294+
const DOW_FRIDAY = 6;
1295+
const DOW_SATURDAY = 7;
1296+
const STARTWEEK_MONDAY_ISO = 21;
1297+
const METHODARR = [
1298+
self::STARTWEEK_SUNDAY => self::DOW_SUNDAY,
1299+
self::DOW_MONDAY,
1300+
self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY,
1301+
self::DOW_TUESDAY,
1302+
self::DOW_WEDNESDAY,
1303+
self::DOW_THURSDAY,
1304+
self::DOW_FRIDAY,
1305+
self::DOW_SATURDAY,
1306+
self::DOW_SUNDAY,
1307+
self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO,
1308+
];
1309+
12761310
/**
12771311
* WEEKNUM.
12781312
*
@@ -1291,41 +1325,51 @@ public static function WEEKDAY($dateValue = 1, $style = 1)
12911325
* @param int $method Week begins on Sunday or Monday
12921326
* 1 or omitted Week begins on Sunday.
12931327
* 2 Week begins on Monday.
1328+
* 11 Week begins on Monday.
1329+
* 12 Week begins on Tuesday.
1330+
* 13 Week begins on Wednesday.
1331+
* 14 Week begins on Thursday.
1332+
* 15 Week begins on Friday.
1333+
* 16 Week begins on Saturday.
1334+
* 17 Week begins on Sunday.
1335+
* 21 ISO (Jan. 4 is week 1, begins on Monday).
12941336
*
12951337
* @return int|string Week Number
12961338
*/
1297-
public static function WEEKNUM($dateValue = 1, $method = 1)
1339+
public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY)
12981340
{
12991341
$dateValue = Functions::flattenSingleValue($dateValue);
13001342
$method = Functions::flattenSingleValue($method);
13011343

13021344
if (!is_numeric($method)) {
13031345
return Functions::VALUE();
1304-
} elseif (($method < 1) || ($method > 2)) {
1305-
return Functions::NAN();
13061346
}
1307-
$method = floor($method);
1347+
$method = (int) $method;
1348+
if (!array_key_exists($method, self::METHODARR)) {
1349+
return Functions::NaN();
1350+
}
1351+
$method = self::METHODARR[$method];
13081352

1309-
if ($dateValue === null) {
1310-
$dateValue = 1;
1311-
} elseif (is_string($dateValue = self::getDateValue($dateValue))) {
1353+
$dateValue = self::getDateValue($dateValue);
1354+
if (is_string($dateValue)) {
13121355
return Functions::VALUE();
1313-
} elseif ($dateValue < 0.0) {
1356+
}
1357+
if ($dateValue < 0.0) {
13141358
return Functions::NAN();
13151359
}
13161360

13171361
// Execute function
13181362
$PHPDateObject = Date::excelToDateTimeObject($dateValue);
1363+
if ($method == self::STARTWEEK_MONDAY_ISO) {
1364+
return (int) $PHPDateObject->format('W');
1365+
}
13191366
$dayOfYear = $PHPDateObject->format('z');
13201367
$PHPDateObject->modify('-' . $dayOfYear . ' days');
13211368
$firstDayOfFirstWeek = $PHPDateObject->format('w');
13221369
$daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7;
1323-
$interval = $dayOfYear - $daysInFirstWeek;
1324-
$weekOfYear = floor($interval / 7) + 1;
1325-
1326-
if ($daysInFirstWeek) {
1327-
++$weekOfYear;
1328-
}
1370+
$daysInFirstWeek += 7 * !$daysInFirstWeek;
1371+
$endFirstWeek = $daysInFirstWeek - 1;
1372+
$weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7);
13291373

13301374
return (int) $weekOfYear;
13311375
}

tests/data/Calculation/DateTime/WEEKNUM.php

+112
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
'#NUM!',
5454
'3/7/1977', 0,
5555
],
56+
[
57+
'#NUM!',
58+
'3/7/1977', -1,
59+
],
5660
[
5761
'#VALUE!',
5862
'Invalid', 1,
@@ -61,4 +65,112 @@
6165
'#NUM!',
6266
-1,
6367
],
68+
[
69+
53,
70+
'2019-12-29', 1,
71+
],
72+
[
73+
52,
74+
'2019-12-29', 2,
75+
],
76+
[
77+
'#NUM!',
78+
'2019-12-29', 3,
79+
],
80+
[
81+
'#NUM!',
82+
'2019-12-29', 10,
83+
],
84+
[
85+
52,
86+
'2019-12-29', 11,
87+
],
88+
[
89+
52,
90+
'2019-12-29', 12,
91+
],
92+
[
93+
53,
94+
'2019-12-29', 13,
95+
],
96+
[
97+
53,
98+
'2019-12-29', 14,
99+
],
100+
[
101+
53,
102+
'2019-12-29', 15,
103+
],
104+
[
105+
53,
106+
'2019-12-29', 16,
107+
],
108+
[
109+
53,
110+
'2019-12-29', 17,
111+
],
112+
[
113+
'#NUM!',
114+
'2019-12-29', 18,
115+
],
116+
[
117+
'#NUM!',
118+
'2019-12-29', 20,
119+
],
120+
[
121+
'#NUM!',
122+
'2019-12-29', 22,
123+
],
124+
[
125+
52,
126+
'2019-12-29', 21,
127+
],
128+
[
129+
53,
130+
'2020-12-29', 21,
131+
],
132+
[
133+
52,
134+
'2021-12-29', 21,
135+
],
136+
[
137+
52,
138+
'2022-12-29', 21,
139+
],
140+
[
141+
1,
142+
'2020-01-01', 21,
143+
],
144+
[
145+
53,
146+
'2021-01-01', 21,
147+
],
148+
[
149+
52,
150+
'2022-01-01', 21,
151+
],
152+
[
153+
52,
154+
'2023-01-01', 21,
155+
],
156+
[
157+
2,
158+
'2020-01-08', 21,
159+
],
160+
[
161+
1,
162+
'2021-01-08', 21,
163+
],
164+
[
165+
1,
166+
'2022-01-08', 21,
167+
],
168+
[
169+
1,
170+
'2023-01-08', 21,
171+
],
172+
[
173+
1,
174+
'2025-12-29', 21,
175+
],
64176
];

0 commit comments

Comments
 (0)