| 1 |
<?php |
| 2 |
|
| 3 |
namespace OpenSpout\Reader\XLSX\Helper; |
| 4 |
|
| 5 |
/** |
| 6 |
* This class provides helper functions to format Excel dates. |
| 7 |
*/ |
| 8 |
class DateFormatHelper |
| 9 |
{ |
| 10 |
public const KEY_GENERAL = 'general'; |
| 11 |
public const KEY_HOUR_12 = '12h'; |
| 12 |
public const KEY_HOUR_24 = '24h'; |
| 13 |
|
| 14 |
/** |
| 15 |
* This map is used to replace Excel format characters by their PHP equivalent. |
| 16 |
* Keys should be ordered from longest to smallest. |
| 17 |
* |
| 18 |
* @var array Mapping between Excel format characters and PHP format characters |
| 19 |
*/ |
| 20 |
private static $excelDateFormatToPHPDateFormatMapping = [ |
| 21 |
self::KEY_GENERAL => [ |
| 22 |
// Time |
| 23 |
'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem |
| 24 |
':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month) |
| 25 |
'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month) |
| 26 |
'ss' => 's', // Seconds, with leading zeros |
| 27 |
'.s' => '', // Ignore (fractional seconds format does not exist in PHP) |
| 28 |
|
| 29 |
// Date |
| 30 |
'e' => 'Y', // Full numeric representation of a year, 4 digits |
| 31 |
'yyyy' => 'Y', // Full numeric representation of a year, 4 digits |
| 32 |
'yy' => 'y', // Two digit representation of a year |
| 33 |
'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...) |
| 34 |
'mmmm' => 'F', // Full textual representation of a month |
| 35 |
'mmm' => 'M', // Short textual representation of a month, three letters |
| 36 |
'mm' => 'm', // Numeric representation of a month, with leading zeros |
| 37 |
'm' => 'n', // Numeric representation of a month, without leading zeros |
| 38 |
'dddd' => 'l', // Full textual representation of the day of the week |
| 39 |
'ddd' => 'D', // Textual representation of a day, three letters |
| 40 |
'dd' => 'd', // Day of the month, 2 digits with leading zeros |
| 41 |
'd' => 'j', // Day of the month without leading zeros |
| 42 |
], |
| 43 |
self::KEY_HOUR_12 => [ |
| 44 |
'hh' => 'h', // 12-hour format of an hour without leading zeros |
| 45 |
'h' => 'g', // 12-hour format of an hour without leading zeros |
| 46 |
], |
| 47 |
self::KEY_HOUR_24 => [ |
| 48 |
'hh' => 'H', // 24-hour hours with leading zero |
| 49 |
'h' => 'G', // 24-hour format of an hour without leading zeros |
| 50 |
], |
| 51 |
]; |
| 52 |
|
| 53 |
/** |
| 54 |
* Converts the given Excel date format to a format understandable by the PHP date function. |
| 55 |
* |
| 56 |
* @param string $excelDateFormat Excel date format |
| 57 |
* |
| 58 |
* @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php) |
| 59 |
*/ |
| 60 |
public static function toPHPDateFormat($excelDateFormat) |
| 61 |
{ |
| 62 |
// Remove brackets potentially present at the beginning of the format string |
| 63 |
// and text portion of the format at the end of it (starting with ";") |
| 64 |
// See §18.8.31 of ECMA-376 for more detail. |
| 65 |
$dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat); |
| 66 |
|
| 67 |
// Double quotes are used to escape characters that must not be interpreted. |
| 68 |
// For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" |
| 69 |
// By exploding the format string using double quote as a delimiter, we can get all parts |
| 70 |
// that must be transformed (even indexes) and all parts that must not be (odd indexes). |
| 71 |
$dateFormatParts = explode('"', $dateFormat); |
| 72 |
|
| 73 |
foreach ($dateFormatParts as $partIndex => $dateFormatPart) { |
| 74 |
// do not look at odd indexes |
| 75 |
if (1 === $partIndex % 2) { |
| 76 |
continue; |
| 77 |
} |
| 78 |
|
| 79 |
// Make sure all characters are lowercase, as the mapping table is using lowercase characters |
| 80 |
$transformedPart = strtolower($dateFormatPart); |
| 81 |
|
| 82 |
// Remove escapes related to non-format characters |
| 83 |
$transformedPart = str_replace('\\', '', $transformedPart); |
| 84 |
|
| 85 |
// Apply general transformation first... |
| 86 |
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); |
| 87 |
|
| 88 |
// ... then apply hour transformation, for 12-hour or 24-hour format |
| 89 |
if (self::has12HourFormatMarker($dateFormatPart)) { |
| 90 |
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); |
| 91 |
} else { |
| 92 |
$transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); |
| 93 |
} |
| 94 |
|
| 95 |
// overwrite the parts array with the new transformed part |
| 96 |
$dateFormatParts[$partIndex] = $transformedPart; |
| 97 |
} |
| 98 |
|
| 99 |
// Merge all transformed parts back together |
| 100 |
$phpDateFormat = implode('"', $dateFormatParts); |
| 101 |
|
| 102 |
// Finally, to have the date format compatible with the DateTime::format() function, we need to escape |
| 103 |
// all characters that are inside double quotes (and double quotes must be removed). |
| 104 |
// For instance, ["Day " dd] should become [\D\a\y\ dd] |
| 105 |
return preg_replace_callback('/"(.+?)"/', function ($matches) { |
| 106 |
$stringToEscape = $matches[1]; |
| 107 |
$letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); |
| 108 |
|
| 109 |
return '\\'.implode('\\', $letters); |
| 110 |
}, $phpDateFormat); |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* @param string $excelDateFormat Date format as defined by Excel |
| 115 |
* |
| 116 |
* @return bool Whether the given date format has the 12-hour format marker |
| 117 |
*/ |
| 118 |
private static function has12HourFormatMarker($excelDateFormat) |
| 119 |
{ |
| 120 |
return false !== stripos($excelDateFormat, 'am/pm'); |
| 121 |
} |
| 122 |
} |
| 123 |
|