Skip to content

Commit 763e0de

Browse files
authored
Merge pull request #2772 from oleibman/issue2768
Time Interval Formatting
2 parents 27221ee + da76f0d commit 763e0de

File tree

3 files changed

+169
-61
lines changed

3 files changed

+169
-61
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4075,36 +4075,6 @@ parameters:
40754075
count: 1
40764076
path: src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php
40774077

4078-
-
4079-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:escapeQuotesCallback\\(\\) has parameter \\$matches with no type specified\\.$#"
4080-
count: 1
4081-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4082-
4083-
-
4084-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#"
4085-
count: 1
4086-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4087-
4088-
-
4089-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:setLowercaseCallback\\(\\) has parameter \\$matches with no type specified\\.$#"
4090-
count: 1
4091-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4092-
4093-
-
4094-
message: "#^Parameter \\#1 \\$format of method DateTime\\:\\:format\\(\\) expects string, string\\|null given\\.$#"
4095-
count: 1
4096-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4097-
4098-
-
4099-
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, int given\\.$#"
4100-
count: 1
4101-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4102-
4103-
-
4104-
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
4105-
count: 1
4106-
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
4107-
41084078
-
41094079
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\Formatter\\:\\:splitFormat\\(\\) has no return type specified\\.$#"
41104080
count: 1

src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ class DateFormatter
88
{
99
/**
1010
* Search/replace values to convert Excel date/time format masks to PHP format masks.
11-
*
12-
* @var array
1311
*/
14-
private static $dateFormatReplacements = [
12+
private const DATE_FORMAT_REPLACEMENTS = [
1513
// first remove escapes related to non-format characters
1614
'\\' => '',
1715
// 12-hour suffix
@@ -32,10 +30,6 @@ class DateFormatter
3230
// It isn't perfect, but the best way I know how
3331
':mm' => ':i',
3432
'mm:' => 'i:',
35-
// month leading zero
36-
'mm' => 'm',
37-
// month no leading zero
38-
'm' => 'n',
3933
// full day of week name
4034
'dddd' => 'l',
4135
// short day of week name
@@ -44,44 +38,97 @@ class DateFormatter
4438
'dd' => 'd',
4539
// days no leading zero
4640
'd' => 'j',
47-
// seconds
48-
'ss' => 's',
4941
// fractional seconds - no php equivalent
5042
'.s' => '',
5143
];
5244

5345
/**
5446
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
55-
*
56-
* @var array
5747
*/
58-
private static $dateFormatReplacements24 = [
48+
private const DATE_FORMAT_REPLACEMENTS24 = [
5949
'hh' => 'H',
6050
'h' => 'G',
51+
// month leading zero
52+
'mm' => 'm',
53+
// month no leading zero
54+
'm' => 'n',
55+
// seconds
56+
'ss' => 's',
6157
];
6258

6359
/**
6460
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
65-
*
66-
* @var array
6761
*/
68-
private static $dateFormatReplacements12 = [
62+
private const DATE_FORMAT_REPLACEMENTS12 = [
6963
'hh' => 'h',
7064
'h' => 'g',
65+
// month leading zero
66+
'mm' => 'm',
67+
// month no leading zero
68+
'm' => 'n',
69+
// seconds
70+
'ss' => 's',
71+
];
72+
73+
private const HOURS_IN_DAY = 24;
74+
private const MINUTES_IN_DAY = 60 * self::HOURS_IN_DAY;
75+
private const SECONDS_IN_DAY = 60 * self::MINUTES_IN_DAY;
76+
private const INTERVAL_PRECISION = 10;
77+
private const INTERVAL_LEADING_ZERO = [
78+
'[hh]',
79+
'[mm]',
80+
'[ss]',
81+
];
82+
private const INTERVAL_ROUND_PRECISION = [
83+
// hours and minutes truncate
84+
'[h]' => self::INTERVAL_PRECISION,
85+
'[hh]' => self::INTERVAL_PRECISION,
86+
'[m]' => self::INTERVAL_PRECISION,
87+
'[mm]' => self::INTERVAL_PRECISION,
88+
// seconds round
89+
'[s]' => 0,
90+
'[ss]' => 0,
91+
];
92+
private const INTERVAL_MULTIPLIER = [
93+
'[h]' => self::HOURS_IN_DAY,
94+
'[hh]' => self::HOURS_IN_DAY,
95+
'[m]' => self::MINUTES_IN_DAY,
96+
'[mm]' => self::MINUTES_IN_DAY,
97+
'[s]' => self::SECONDS_IN_DAY,
98+
'[ss]' => self::SECONDS_IN_DAY,
7199
];
72100

101+
/** @param mixed $value */
102+
private static function tryInterval(bool &$seekingBracket, string &$block, $value, string $format): void
103+
{
104+
if ($seekingBracket) {
105+
if (false !== strpos($block, $format)) {
106+
$hours = (string) (int) round(
107+
self::INTERVAL_MULTIPLIER[$format] * $value,
108+
self::INTERVAL_ROUND_PRECISION[$format]
109+
);
110+
if (strlen($hours) === 1 && in_array($format, self::INTERVAL_LEADING_ZERO, true)) {
111+
$hours = "0$hours";
112+
}
113+
$block = str_replace($format, $hours, $block);
114+
$seekingBracket = false;
115+
}
116+
}
117+
}
118+
119+
/** @param mixed $value */
73120
public static function format($value, string $format): string
74121
{
75122
// strip off first part containing e.g. [$-F800] or [$USD-409]
76123
// general syntax: [$<Currency string>-<language info>]
77124
// language info is in hexadecimal
78125
// strip off chinese part like [DBNum1][$-804]
79-
$format = preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format) ?? '';
126+
$format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format);
80127

81128
// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
82129
// but we don't want to change any quoted strings
83130
/** @var callable */
84-
$callable = ['self', 'setLowercaseCallback'];
131+
$callable = [self::class, 'setLowercaseCallback'];
85132
$format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format);
86133

87134
// Only process the non-quoted blocks for date format characters
@@ -90,45 +137,46 @@ public static function format($value, string $format): string
90137
$blocks = explode('"', $format);
91138
foreach ($blocks as $key => &$block) {
92139
if ($key % 2 == 0) {
93-
$block = strtr($block, self::$dateFormatReplacements);
140+
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS);
94141
if (!strpos($block, 'A')) {
95142
// 24-hour time format
96143
// when [h]:mm format, the [h] should replace to the hours of the value * 24
97-
if (false !== strpos($block, '[h]')) {
98-
$hours = (int) ($value * 24);
99-
$block = str_replace('[h]', $hours, $block);
100-
101-
continue;
102-
}
103-
$block = strtr($block, self::$dateFormatReplacements24);
144+
$seekingBracket = true;
145+
self::tryInterval($seekingBracket, $block, $value, '[h]');
146+
self::tryInterval($seekingBracket, $block, $value, '[hh]');
147+
self::tryInterval($seekingBracket, $block, $value, '[mm]');
148+
self::tryInterval($seekingBracket, $block, $value, '[m]');
149+
self::tryInterval($seekingBracket, $block, $value, '[s]');
150+
self::tryInterval($seekingBracket, $block, $value, '[ss]');
151+
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS24);
104152
} else {
105153
// 12-hour time format
106-
$block = strtr($block, self::$dateFormatReplacements12);
154+
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS12);
107155
}
108156
}
109157
}
110158
$format = implode('"', $blocks);
111159

112160
// escape any quoted characters so that DateTime format() will render them correctly
113161
/** @var callable */
114-
$callback = ['self', 'escapeQuotesCallback'];
115-
$format = preg_replace_callback('/"(.*)"/U', $callback, $format);
162+
$callback = [self::class, 'escapeQuotesCallback'];
163+
$format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format);
116164

117165
$dateObj = Date::excelToDateTimeObject($value);
118166
// If the colon preceding minute had been quoted, as happens in
119167
// Excel 2003 XML formats, m will not have been changed to i above.
120168
// Change it now.
121-
$format = \preg_replace('/\\\\:m/', ':i', $format);
169+
$format = (string) \preg_replace('/\\\\:m/', ':i', $format);
122170

123171
return $dateObj->format($format);
124172
}
125173

126-
private static function setLowercaseCallback($matches): string
174+
private static function setLowercaseCallback(array $matches): string
127175
{
128176
return mb_strtolower($matches[0]);
129177
}
130178

131-
private static function escapeQuotesCallback($matches): string
179+
private static function escapeQuotesCallback(array $matches): string
132180
{
133181
return '\\' . implode('\\', str_split($matches[1]));
134182
}

tests/data/Style/NumberFormatDates.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,94 @@
7272
12345.6789,
7373
'[DBNum3][$-zh-CN]yyyymmdd;@',
7474
],
75+
'hour with leading 0 and minute' => [
76+
'03:36',
77+
1.15,
78+
'hh:mm',
79+
],
80+
'hour without leading 0 and minute' => [
81+
'3:36',
82+
1.15,
83+
'h:mm',
84+
],
85+
'hour truncated not rounded' => [
86+
'27',
87+
1.15,
88+
'[hh]',
89+
],
90+
'interval hour > 10 so no need for leading 0 and minute' => [
91+
'27:36',
92+
1.15,
93+
'[hh]:mm',
94+
],
95+
'interval hour > 10 no leading 0 and minute' => [
96+
'27:36',
97+
1.15,
98+
'[h]:mm',
99+
],
100+
'interval hour with leading 0 and minute' => [
101+
'03:36',
102+
0.15,
103+
'[hh]:mm',
104+
],
105+
'interval hour no leading 0 and minute' => [
106+
'3:36',
107+
0.15,
108+
'[h]:mm',
109+
],
110+
'interval hours > 100 and minutes no need for leading 0' => [
111+
'123:36',
112+
5.15,
113+
'[hh]:mm',
114+
],
115+
'interval hours > 100 and minutes no leading 0' => [
116+
'123:36',
117+
5.15,
118+
'[h]:mm',
119+
],
120+
'interval minutes > 10 no need for leading 0' => [
121+
'1656',
122+
1.15,
123+
'[mm]',
124+
],
125+
'interval minutes > 10 no leading 0' => [
126+
'1656',
127+
1.15,
128+
'[m]',
129+
],
130+
'interval minutes < 10 leading 0' => [
131+
'07',
132+
0.005,
133+
'[mm]',
134+
],
135+
'interval minutes < 10 no leading 0' => [
136+
'7',
137+
0.005,
138+
'[m]',
139+
],
140+
'interval minutes and seconds' => [
141+
'07:12',
142+
0.005,
143+
'[mm]:ss',
144+
],
145+
'interval seconds' => [
146+
'432',
147+
0.005,
148+
'[ss]',
149+
],
150+
'interval seconds rounded up leading 0' => [
151+
'09',
152+
0.0001,
153+
'[ss]',
154+
],
155+
'interval seconds rounded up no leading 0' => [
156+
'9',
157+
0.0001,
158+
'[s]',
159+
],
160+
'interval seconds rounded down' => [
161+
'6',
162+
0.00007,
163+
'[s]',
164+
],
75165
];

0 commit comments

Comments
 (0)