Skip to content

Commit 8f4c541

Browse files
authored
Fix TEXT and TIMEVALUE Functions 1.29 Branch (#4352)
* Fix TEXT and TIMEVALUE Functions 1.29 Branch Fix #4249. Technically speaking, only the 1.29 branch needs fixing, and only for TEXT. It was fixed for the other branches by PR #3898. However, in adding test cases for the fix, it became apparent that PhpSpreadsheet's parsing in TIMEVALUE (which is called from TEXT in the original issue) did not really match Excel's. There are probably still edge cases where it doesn't, but, in the absence of a spec for how it operates, this will do for now. We do not usually backport fixes from the master branch. Because this is more of a forward port from the earlier branch, there is an equivalent PR for each active branch. * Update Changelog
1 parent 9924c6c commit 8f4c541

File tree

5 files changed

+85
-13
lines changed

5 files changed

+85
-13
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com)
66
and this project adheres to [Semantic Versioning](https://semver.org).
77

8+
# TBD - 1.29.11
9+
10+
### Fixed
11+
12+
- TEXT and TIMEVALUE functions. [Issue #4249](https://github.com/PHPOffice/PhpSpreadsheet/issues/4249) [PR #4352](https://github.com/PHPOffice/PhpSpreadsheet/pull/4352)
13+
814
# 2025-02-07 - 1.29.10
915

1016
### Changed

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ parameters:
6060
count: 1
6161
path: src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php
6262

63-
-
64-
message: "#^Binary operation \"%%\" between string and 24 results in an error\\.$#"
65-
count: 1
66-
path: src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php
67-
6863
-
6964
message: "#^Binary operation \"\\-\" between 1 and array\\|float\\|string results in an error\\.$#"
7065
count: 1

src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
44

5+
use Composer\Pcre\Preg;
56
use Datetime;
67
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
78
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
@@ -12,6 +13,19 @@ class TimeValue
1213
{
1314
use ArrayEnabled;
1415

16+
private const EXTRACT_TIME = '/\b'
17+
. '(\d+)' // match[1] - hour
18+
. '(:' // start of match[2] (rest of string) - colon
19+
. '(\d+' // start of match[3] - minute
20+
. '(:\d+' // start of match[4] - colon and seconds
21+
. '([.]\d+)?' // match[5] - optional decimal point followed by fractional seconds
22+
. ')?' // end of match[4], which is optional
23+
. ')' // end of match 3
24+
// Excel does not require 'm' to trail 'a' or 'p'; Php does
25+
. '(\s*(a|p))?' // match[6] optional whitespace followed by optional match[7] a or p
26+
. ')' // end of match[2]
27+
. '/i';
28+
1529
/**
1630
* TIMEVALUE.
1731
*
@@ -48,12 +62,15 @@ public static function fromString($timeValue)
4862
}
4963

5064
$timeValue = trim($timeValue ?? '', '"');
51-
$timeValue = str_replace(['/', '.'], '-', $timeValue);
52-
53-
$arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: [];
54-
if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) {
55-
$arraySplit[0] = ($arraySplit[0] % 24);
56-
$timeValue = implode(':', $arraySplit);
65+
if (Preg::isMatch(self::EXTRACT_TIME, $timeValue, $matches)) {
66+
if (empty($matches[6])) { // am/pm
67+
$hour = (int) $matches[1];
68+
$timeValue = ($hour % 24) . $matches[2];
69+
} elseif ($matches[6] === $matches[7]) { // Excel wants space before am/pm
70+
return ExcelError::VALUE();
71+
} else {
72+
$timeValue = $matches[0] . 'm';
73+
}
5774
}
5875

5976
$PHPDateArray = Helpers::dateParse($timeValue);

src/PhpSpreadsheet/Calculation/TextData/Format.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
44

5+
use Composer\Pcre\Preg;
56
use DateTimeInterface;
67
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
78
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
@@ -128,8 +129,11 @@ public static function TEXTFORMAT($value, $format)
128129
$value = Helpers::extractString($value);
129130
$format = Helpers::extractString($format);
130131

131-
if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) {
132-
$value = DateTimeExcel\DateValue::fromString($value) + DateTimeExcel\TimeValue::fromString($value);
132+
if (!is_numeric($value) && Date::isDateTimeFormatCode($format) && !Preg::isMatch('/^\s*\d+(\s+\d+)+\s*$/', $value)) {
133+
$value1 = DateTimeExcel\DateValue::fromString($value);
134+
$value2 = DateTimeExcel\TimeValue::fromString($value);
135+
/** @var float|int|string */
136+
$value = (is_numeric($value1) && is_numeric($value2)) ? ($value1 + $value2) : (is_numeric($value1) ? $value1 : (is_numeric($value2) ? $value2 : $value));
133137
}
134138

135139
return (string) NumberFormat::toFormattedString($value, $format);

tests/data/Calculation/TextData/TEXT.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,56 @@
7171
'2014-02-15 16:17',
7272
'dd-mmm-yyyy HH:MM:SS AM/PM',
7373
],
74+
'datetime integer' => [
75+
'1900-01-06 00:00',
76+
6,
77+
'yyyy-mm-dd hh:mm',
78+
],
79+
'datetime integer as string' => [
80+
'1900-01-06 00:00',
81+
'6',
82+
'yyyy-mm-dd hh:mm',
83+
],
84+
'datetime 2 integers without date delimiters' => [
85+
'5 6',
86+
'5 6',
87+
'yyyy-mm-dd hh:mm',
88+
],
89+
'datetime 2 integers separated by hyphen' => [
90+
(new DateTimeImmutable())->format('Y') . '-05-13 00:00',
91+
'5-13',
92+
'yyyy-mm-dd hh:mm',
93+
],
94+
'datetime string date only' => [
95+
'1951-01-23 00:00',
96+
'January 23, 1951',
97+
'yyyy-mm-dd hh:mm',
98+
],
99+
'datetime string time followed by date' => [
100+
'1952-05-02 03:54',
101+
'3:54 May 2, 1952',
102+
'yyyy-mm-dd hh:mm',
103+
],
104+
'datetime string date followed by time pm' => [
105+
'1952-05-02 15:54',
106+
'May 2, 1952 3:54 pm',
107+
'yyyy-mm-dd hh:mm',
108+
],
109+
'datetime string date followed by time p' => [
110+
'1952-05-02 15:54',
111+
'May 2, 1952 3:54 p',
112+
'yyyy-mm-dd hh:mm',
113+
],
114+
'datetime decimal string interpreted as time' => [
115+
'1900-01-02 12:00',
116+
'2.5',
117+
'yyyy-mm-dd hh:mm',
118+
],
119+
'datetime unparseable string' => [
120+
'xyz',
121+
'xyz',
122+
'yyyy-mm-dd hh:mm',
123+
],
74124
[
75125
'1 3/4',
76126
1.75,

0 commit comments

Comments
 (0)