Merge pull request #2772 from oleibman/issue2768
Time Interval Formatting
This commit is contained in:
commit
763e0de229
|
|
@ -4075,36 +4075,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php
|
path: src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:escapeQuotesCallback\\(\\) has parameter \\$matches with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\DateFormatter\\:\\:setLowercaseCallback\\(\\) has parameter \\$matches with no type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$format of method DateTime\\:\\:format\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, int given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\Formatter\\:\\:splitFormat\\(\\) has no return type specified\\.$#"
|
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\Formatter\\:\\:splitFormat\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@ class DateFormatter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Search/replace values to convert Excel date/time format masks to PHP format masks.
|
* Search/replace values to convert Excel date/time format masks to PHP format masks.
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private static $dateFormatReplacements = [
|
private const DATE_FORMAT_REPLACEMENTS = [
|
||||||
// first remove escapes related to non-format characters
|
// first remove escapes related to non-format characters
|
||||||
'\\' => '',
|
'\\' => '',
|
||||||
// 12-hour suffix
|
// 12-hour suffix
|
||||||
|
|
@ -32,10 +30,6 @@ class DateFormatter
|
||||||
// It isn't perfect, but the best way I know how
|
// It isn't perfect, but the best way I know how
|
||||||
':mm' => ':i',
|
':mm' => ':i',
|
||||||
'mm:' => 'i:',
|
'mm:' => 'i:',
|
||||||
// month leading zero
|
|
||||||
'mm' => 'm',
|
|
||||||
// month no leading zero
|
|
||||||
'm' => 'n',
|
|
||||||
// full day of week name
|
// full day of week name
|
||||||
'dddd' => 'l',
|
'dddd' => 'l',
|
||||||
// short day of week name
|
// short day of week name
|
||||||
|
|
@ -44,44 +38,97 @@ class DateFormatter
|
||||||
'dd' => 'd',
|
'dd' => 'd',
|
||||||
// days no leading zero
|
// days no leading zero
|
||||||
'd' => 'j',
|
'd' => 'j',
|
||||||
// seconds
|
|
||||||
'ss' => 's',
|
|
||||||
// fractional seconds - no php equivalent
|
// fractional seconds - no php equivalent
|
||||||
'.s' => '',
|
'.s' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
|
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private static $dateFormatReplacements24 = [
|
private const DATE_FORMAT_REPLACEMENTS24 = [
|
||||||
'hh' => 'H',
|
'hh' => 'H',
|
||||||
'h' => 'G',
|
'h' => 'G',
|
||||||
|
// month leading zero
|
||||||
|
'mm' => 'm',
|
||||||
|
// month no leading zero
|
||||||
|
'm' => 'n',
|
||||||
|
// seconds
|
||||||
|
'ss' => 's',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
|
* Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private static $dateFormatReplacements12 = [
|
private const DATE_FORMAT_REPLACEMENTS12 = [
|
||||||
'hh' => 'h',
|
'hh' => 'h',
|
||||||
'h' => 'g',
|
'h' => 'g',
|
||||||
|
// month leading zero
|
||||||
|
'mm' => 'm',
|
||||||
|
// month no leading zero
|
||||||
|
'm' => 'n',
|
||||||
|
// seconds
|
||||||
|
'ss' => 's',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private const HOURS_IN_DAY = 24;
|
||||||
|
private const MINUTES_IN_DAY = 60 * self::HOURS_IN_DAY;
|
||||||
|
private const SECONDS_IN_DAY = 60 * self::MINUTES_IN_DAY;
|
||||||
|
private const INTERVAL_PRECISION = 10;
|
||||||
|
private const INTERVAL_LEADING_ZERO = [
|
||||||
|
'[hh]',
|
||||||
|
'[mm]',
|
||||||
|
'[ss]',
|
||||||
|
];
|
||||||
|
private const INTERVAL_ROUND_PRECISION = [
|
||||||
|
// hours and minutes truncate
|
||||||
|
'[h]' => self::INTERVAL_PRECISION,
|
||||||
|
'[hh]' => self::INTERVAL_PRECISION,
|
||||||
|
'[m]' => self::INTERVAL_PRECISION,
|
||||||
|
'[mm]' => self::INTERVAL_PRECISION,
|
||||||
|
// seconds round
|
||||||
|
'[s]' => 0,
|
||||||
|
'[ss]' => 0,
|
||||||
|
];
|
||||||
|
private const INTERVAL_MULTIPLIER = [
|
||||||
|
'[h]' => self::HOURS_IN_DAY,
|
||||||
|
'[hh]' => self::HOURS_IN_DAY,
|
||||||
|
'[m]' => self::MINUTES_IN_DAY,
|
||||||
|
'[mm]' => self::MINUTES_IN_DAY,
|
||||||
|
'[s]' => self::SECONDS_IN_DAY,
|
||||||
|
'[ss]' => self::SECONDS_IN_DAY,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @param mixed $value */
|
||||||
|
private static function tryInterval(bool &$seekingBracket, string &$block, $value, string $format): void
|
||||||
|
{
|
||||||
|
if ($seekingBracket) {
|
||||||
|
if (false !== strpos($block, $format)) {
|
||||||
|
$hours = (string) (int) round(
|
||||||
|
self::INTERVAL_MULTIPLIER[$format] * $value,
|
||||||
|
self::INTERVAL_ROUND_PRECISION[$format]
|
||||||
|
);
|
||||||
|
if (strlen($hours) === 1 && in_array($format, self::INTERVAL_LEADING_ZERO, true)) {
|
||||||
|
$hours = "0$hours";
|
||||||
|
}
|
||||||
|
$block = str_replace($format, $hours, $block);
|
||||||
|
$seekingBracket = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param mixed $value */
|
||||||
public static function format($value, string $format): string
|
public static function format($value, string $format): string
|
||||||
{
|
{
|
||||||
// strip off first part containing e.g. [$-F800] or [$USD-409]
|
// strip off first part containing e.g. [$-F800] or [$USD-409]
|
||||||
// general syntax: [$<Currency string>-<language info>]
|
// general syntax: [$<Currency string>-<language info>]
|
||||||
// language info is in hexadecimal
|
// language info is in hexadecimal
|
||||||
// strip off chinese part like [DBNum1][$-804]
|
// strip off chinese part like [DBNum1][$-804]
|
||||||
$format = preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format) ?? '';
|
$format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format);
|
||||||
|
|
||||||
// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
|
// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
|
||||||
// but we don't want to change any quoted strings
|
// but we don't want to change any quoted strings
|
||||||
/** @var callable */
|
/** @var callable */
|
||||||
$callable = ['self', 'setLowercaseCallback'];
|
$callable = [self::class, 'setLowercaseCallback'];
|
||||||
$format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format);
|
$format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format);
|
||||||
|
|
||||||
// Only process the non-quoted blocks for date format characters
|
// Only process the non-quoted blocks for date format characters
|
||||||
|
|
@ -90,20 +137,21 @@ class DateFormatter
|
||||||
$blocks = explode('"', $format);
|
$blocks = explode('"', $format);
|
||||||
foreach ($blocks as $key => &$block) {
|
foreach ($blocks as $key => &$block) {
|
||||||
if ($key % 2 == 0) {
|
if ($key % 2 == 0) {
|
||||||
$block = strtr($block, self::$dateFormatReplacements);
|
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS);
|
||||||
if (!strpos($block, 'A')) {
|
if (!strpos($block, 'A')) {
|
||||||
// 24-hour time format
|
// 24-hour time format
|
||||||
// when [h]:mm format, the [h] should replace to the hours of the value * 24
|
// when [h]:mm format, the [h] should replace to the hours of the value * 24
|
||||||
if (false !== strpos($block, '[h]')) {
|
$seekingBracket = true;
|
||||||
$hours = (int) ($value * 24);
|
self::tryInterval($seekingBracket, $block, $value, '[h]');
|
||||||
$block = str_replace('[h]', $hours, $block);
|
self::tryInterval($seekingBracket, $block, $value, '[hh]');
|
||||||
|
self::tryInterval($seekingBracket, $block, $value, '[mm]');
|
||||||
continue;
|
self::tryInterval($seekingBracket, $block, $value, '[m]');
|
||||||
}
|
self::tryInterval($seekingBracket, $block, $value, '[s]');
|
||||||
$block = strtr($block, self::$dateFormatReplacements24);
|
self::tryInterval($seekingBracket, $block, $value, '[ss]');
|
||||||
|
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS24);
|
||||||
} else {
|
} else {
|
||||||
// 12-hour time format
|
// 12-hour time format
|
||||||
$block = strtr($block, self::$dateFormatReplacements12);
|
$block = strtr($block, self::DATE_FORMAT_REPLACEMENTS12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,24 +159,24 @@ class DateFormatter
|
||||||
|
|
||||||
// escape any quoted characters so that DateTime format() will render them correctly
|
// escape any quoted characters so that DateTime format() will render them correctly
|
||||||
/** @var callable */
|
/** @var callable */
|
||||||
$callback = ['self', 'escapeQuotesCallback'];
|
$callback = [self::class, 'escapeQuotesCallback'];
|
||||||
$format = preg_replace_callback('/"(.*)"/U', $callback, $format);
|
$format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format);
|
||||||
|
|
||||||
$dateObj = Date::excelToDateTimeObject($value);
|
$dateObj = Date::excelToDateTimeObject($value);
|
||||||
// If the colon preceding minute had been quoted, as happens in
|
// If the colon preceding minute had been quoted, as happens in
|
||||||
// Excel 2003 XML formats, m will not have been changed to i above.
|
// Excel 2003 XML formats, m will not have been changed to i above.
|
||||||
// Change it now.
|
// Change it now.
|
||||||
$format = \preg_replace('/\\\\:m/', ':i', $format);
|
$format = (string) \preg_replace('/\\\\:m/', ':i', $format);
|
||||||
|
|
||||||
return $dateObj->format($format);
|
return $dateObj->format($format);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function setLowercaseCallback($matches): string
|
private static function setLowercaseCallback(array $matches): string
|
||||||
{
|
{
|
||||||
return mb_strtolower($matches[0]);
|
return mb_strtolower($matches[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function escapeQuotesCallback($matches): string
|
private static function escapeQuotesCallback(array $matches): string
|
||||||
{
|
{
|
||||||
return '\\' . implode('\\', str_split($matches[1]));
|
return '\\' . implode('\\', str_split($matches[1]));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,94 @@ return [
|
||||||
12345.6789,
|
12345.6789,
|
||||||
'[DBNum3][$-zh-CN]yyyymmdd;@',
|
'[DBNum3][$-zh-CN]yyyymmdd;@',
|
||||||
],
|
],
|
||||||
|
'hour with leading 0 and minute' => [
|
||||||
|
'03:36',
|
||||||
|
1.15,
|
||||||
|
'hh:mm',
|
||||||
|
],
|
||||||
|
'hour without leading 0 and minute' => [
|
||||||
|
'3:36',
|
||||||
|
1.15,
|
||||||
|
'h:mm',
|
||||||
|
],
|
||||||
|
'hour truncated not rounded' => [
|
||||||
|
'27',
|
||||||
|
1.15,
|
||||||
|
'[hh]',
|
||||||
|
],
|
||||||
|
'interval hour > 10 so no need for leading 0 and minute' => [
|
||||||
|
'27:36',
|
||||||
|
1.15,
|
||||||
|
'[hh]:mm',
|
||||||
|
],
|
||||||
|
'interval hour > 10 no leading 0 and minute' => [
|
||||||
|
'27:36',
|
||||||
|
1.15,
|
||||||
|
'[h]:mm',
|
||||||
|
],
|
||||||
|
'interval hour with leading 0 and minute' => [
|
||||||
|
'03:36',
|
||||||
|
0.15,
|
||||||
|
'[hh]:mm',
|
||||||
|
],
|
||||||
|
'interval hour no leading 0 and minute' => [
|
||||||
|
'3:36',
|
||||||
|
0.15,
|
||||||
|
'[h]:mm',
|
||||||
|
],
|
||||||
|
'interval hours > 100 and minutes no need for leading 0' => [
|
||||||
|
'123:36',
|
||||||
|
5.15,
|
||||||
|
'[hh]:mm',
|
||||||
|
],
|
||||||
|
'interval hours > 100 and minutes no leading 0' => [
|
||||||
|
'123:36',
|
||||||
|
5.15,
|
||||||
|
'[h]:mm',
|
||||||
|
],
|
||||||
|
'interval minutes > 10 no need for leading 0' => [
|
||||||
|
'1656',
|
||||||
|
1.15,
|
||||||
|
'[mm]',
|
||||||
|
],
|
||||||
|
'interval minutes > 10 no leading 0' => [
|
||||||
|
'1656',
|
||||||
|
1.15,
|
||||||
|
'[m]',
|
||||||
|
],
|
||||||
|
'interval minutes < 10 leading 0' => [
|
||||||
|
'07',
|
||||||
|
0.005,
|
||||||
|
'[mm]',
|
||||||
|
],
|
||||||
|
'interval minutes < 10 no leading 0' => [
|
||||||
|
'7',
|
||||||
|
0.005,
|
||||||
|
'[m]',
|
||||||
|
],
|
||||||
|
'interval minutes and seconds' => [
|
||||||
|
'07:12',
|
||||||
|
0.005,
|
||||||
|
'[mm]:ss',
|
||||||
|
],
|
||||||
|
'interval seconds' => [
|
||||||
|
'432',
|
||||||
|
0.005,
|
||||||
|
'[ss]',
|
||||||
|
],
|
||||||
|
'interval seconds rounded up leading 0' => [
|
||||||
|
'09',
|
||||||
|
0.0001,
|
||||||
|
'[ss]',
|
||||||
|
],
|
||||||
|
'interval seconds rounded up no leading 0' => [
|
||||||
|
'9',
|
||||||
|
0.0001,
|
||||||
|
'[s]',
|
||||||
|
],
|
||||||
|
'interval seconds rounded down' => [
|
||||||
|
'6',
|
||||||
|
0.00007,
|
||||||
|
'[s]',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue