Skip to content

Commit

Permalink
Merge pull request #2181 from kylekatarnls/feature/issue-2180-fix-has…
Browse files Browse the repository at this point in the history
…Format

Fix #2180 hasFormat() with slashes
  • Loading branch information
kylekatarnls committed Sep 4, 2020
2 parents 6b7d48c + 915b8d0 commit 7af4678
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 32 deletions.
71 changes: 46 additions & 25 deletions src/Carbon/Traits/Comparison.php
Original file line number Diff line number Diff line change
Expand Up @@ -851,48 +851,69 @@ public function isMidday()
* Carbon::hasFormat('13:12:45', 'h:i:s'); // false
* ```
*
* @SuppressWarnings(PHPMD.EmptyCatchBlock)
*
* @param string $date
* @param string $format
*
* @return bool
*/
public static function hasFormat($date, $format)
{
try {
// Try to create a DateTime object. Throws an InvalidArgumentException if the provided time string
// doesn't match the format in any way.
static::rawCreateFromFormat($format, $date);
// createFromFormat() is known to handle edge cases silently.
// E.g. "1975-5-1" (Y-n-j) will still be parsed correctly when "Y-m-d" is supplied as the format.
// To ensure we're really testing against our desired format, perform an additional regex validation.

// createFromFormat() is known to handle edge cases silently.
// E.g. "1975-5-1" (Y-n-j) will still be parsed correctly when "Y-m-d" is supplied as the format.
// To ensure we're really testing against our desired format, perform an additional regex validation.
// Preg quote, but remove escaped backslashes since we'll deal with escaped characters in the format string.
$quotedFormat = str_replace('\\\\', '\\', preg_quote($format, '/'));

// Preg quote, but remove escaped backslashes since we'll deal with escaped characters in the format string.
$quotedFormat = str_replace('\\\\', '\\', preg_quote($format, '/'));
// Build the regex string
$regex = '';

// Build the regex string
$regex = '';
for ($i = 0; $i < strlen($quotedFormat); ++$i) {
// Backslash – the next character does not represent a date token so add it on as-is and continue.
// We're doing an extra ++$i here to increment the loop by 2.
if ($quotedFormat[$i] === '\\') {
$char = $quotedFormat[++$i];
$regex .= $char === '\\' ? '\\\\' : $char;

for ($i = 0; $i < strlen($quotedFormat); ++$i) {
// Backslash – the next character does not represent a date token so add it on as-is and continue.
// We're doing an extra ++$i here to increment the loop by 2.
if ($quotedFormat[$i] === '\\') {
$char = $quotedFormat[++$i];
$regex .= $char === '\\' ? '\\\\' : $char;
continue;
}

continue;
}
$regex .= strtr($quotedFormat[$i], static::$regexFormats);
}

$regex .= strtr($quotedFormat[$i], static::$regexFormats);
}
$regex = preg_replace('#(?<!\\\\)((?:\\\\{2})*)/#', '$1\\/', $regex);
return (bool) preg_match('/^'.str_replace('/', '\\/', $regex).'$/', $date);
return (bool) @preg_match('/^'.$regex.'$/', $date);
}
/**
* Checks if the (date)time string is in a given format and valid to create a
* new instance.
*
* @example
* ```
* Carbon::canBeCreatedFromFormat('11:12:45', 'h:i:s'); // true
* Carbon::canBeCreatedFromFormat('13:12:45', 'h:i:s'); // false
* ```
*
* @param string $date
* @param string $format
*
* @return bool
*/
public static function canBeCreatedFromFormat($date, $format)
{
try {
// Try to create a DateTime object. Throws an InvalidArgumentException if the provided time string
// doesn't match the format in any way.
if (!static::rawCreateFromFormat($format, $date)) {
return false;
}
} catch (InvalidArgumentException $e) {
return false;
}
return false;
return static::hasFormat($date, $format);
}
/**
Expand Down
14 changes: 7 additions & 7 deletions src/Carbon/Traits/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ trait Options
*/
protected static $regexFormats = [
'd' => '(3[01]|[12][0-9]|0[1-9])',
'D' => '([a-zA-Z]{3})',
'D' => '(Sun|Mon|Tue|Wed|Thu|Fri|Sat)',
'j' => '([123][0-9]|[1-9])',
'l' => '([a-zA-Z]{2,})',
'N' => '([1-7])',
'S' => '([a-zA-Z]{2})',
'S' => '(st|nd|rd|th)',
'w' => '([0-6])',
'z' => '(36[0-5]|3[0-5][0-9]|[12][0-9]{2}|[1-9]?[0-9])',
'W' => '(5[012]|[1-4][0-9]|[1-9])',
Expand All @@ -92,17 +92,17 @@ trait Options
's' => '([0-5][0-9])',
'u' => '([0-9]{1,6})',
'v' => '([0-9]{1,3})',
'e' => '([a-zA-Z]{1,5})|([a-zA-Z]*\/[a-zA-Z]*)',
'e' => '([a-zA-Z]{1,5})|([a-zA-Z]*\\/[a-zA-Z]*)',
'I' => '(0|1)',
'O' => '([\+\-](1[012]|0[0-9])[0134][05])',
'P' => '([\+\-](1[012]|0[0-9]):[0134][05])',
'O' => '([+-](1[012]|0[0-9])[0134][05])',
'P' => '([+-](1[012]|0[0-9]):[0134][05])',
'T' => '([a-zA-Z]{1,5})',
'Z' => '(-?[1-5]?[0-9]{1,4})',
'U' => '([0-9]*)',

// The formats below are combinations of the above formats.
'c' => '(([1-9]?[0-9]{4})\-(1[012]|0[1-9])\-(3[01]|[12][0-9]|0[1-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])[\+\-](1[012]|0[0-9]):([0134][05]))', // Y-m-dTH:i:sP
'r' => '(([a-zA-Z]{3}), ([123][0-9]|[1-9]) ([a-zA-Z]{3}) ([1-9]?[0-9]{4}) (2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]) [\+\-](1[012]|0[0-9])([0134][05]))', // D, j M Y H:i:s O
'c' => '(([1-9]?[0-9]{4})-(1[012]|0[1-9])-(3[01]|[12][0-9]|0[1-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])[+-](1[012]|0[0-9]):([0134][05]))', // Y-m-dTH:i:sP
'r' => '(([a-zA-Z]{3}), ([123][0-9]|0[1-9]) ([a-zA-Z]{3}) ([1-9]?[0-9]{4}) (2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]) [+-](1[012]|0[0-9])([0134][05]))', // D, d M Y H:i:s O
];

/**
Expand Down
70 changes: 70 additions & 0 deletions tests/Carbon/IsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,9 @@ public function testHasFormat()
$this->assertTrue(Carbon::hasFormat('2000-07-01T00:00:00+00:00', Carbon::ATOM));
$this->assertTrue(Carbon::hasFormat('Y-01-30\\', '\\Y-m-d\\\\'));

// @see https://github.com/briannesbitt/Carbon/issues/2180
$this->assertTrue(Carbon::hasFormat('2020-09-01 12:00:00Europe/Moscow', 'Y-m-d H:i:se'));

if (version_compare(PHP_VERSION, '7.3.0-dev', '>=')) {
// Due to https://bugs.php.net/bug.php?id=75577, proper "v" format support can only works from PHP 7.3.0.
$this->assertTrue(Carbon::hasFormat('2012-12-04 22:59.32130', 'Y-m-d H:s.vi'));
Expand All @@ -897,6 +900,73 @@ public function testHasFormat()
$this->assertFalse(Carbon::hasFormat('12/30/2019', 'd/m/Y'));
}

public function getFormatLetters()
{
return array_map(function ($letter) {
return [$letter];
}, [
'd',
'D',
'j',
'l',
'N',
'S',
'w',
'z',
'W',
'F',
'm',
'M',
'n',
't',
'L',
'o',
'Y',
'y',
'a',
'A',
'B',
'g',
'G',
'h',
'H',
'i',
's',
'u',
'v',
'e',
'I',
'O',
'P',
'T',
'Z',
'U',
'c',
'r',
]);
}

/**
* @dataProvider getFormatLetters
*/
public function testHasFormatWithSingleLetter($letter)
{
$output = Carbon::now()->format($letter);
$this->assertTrue(Carbon::hasFormat($output, $letter), "'$letter' format should match '$output'");
}

public function testCanBeCreatedFromFormat()
{
$this->assertTrue(Carbon::canBeCreatedFromFormat('1975-05-01', 'Y-m-d'));
$this->assertTrue(Carbon::canBeCreatedFromFormat('12/30/2019', 'm/d/Y'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('1975-05-01', 'd m Y'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('1975-5-1', 'Y-m-d'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('19-05-01', 'Y-m-d'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('30/12/2019', 'm/d/Y'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('12/30/2019', 'd/m/Y'));
$this->assertFalse(Carbon::canBeCreatedFromFormat('5', 'N'));
}

public function testIsSameFoobar()
{
$this->expectException(\BadMethodCallException::class);
Expand Down
58 changes: 58 additions & 0 deletions tests/CarbonImmutable/IsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,9 @@ public function testHasFormat()
$this->assertTrue(Carbon::hasFormat('2000-07-01T00:00:00+00:00', DateTime::ATOM));
$this->assertTrue(Carbon::hasFormat('Y-01-30\\', '\\Y-m-d\\\\'));

// @see https://github.com/briannesbitt/Carbon/issues/2180
$this->assertTrue(Carbon::hasFormat('2020-09-01 12:00:00Europe/Moscow', 'Y-m-d H:i:se'));

if (version_compare(PHP_VERSION, '7.3.0-dev', '>=')) {
// Due to https://bugs.php.net/bug.php?id=75577, proper "v" format support can only works from PHP 7.3.0.
$this->assertTrue(Carbon::hasFormat('2012-12-04 22:59.32130', 'Y-m-d H:s.vi'));
Expand All @@ -909,6 +912,61 @@ public function testHasFormat()
$this->assertFalse(Carbon::hasFormat('12/30/2019', 'd/m/Y'));
}

public function getFormatLetters()
{
return array_map(function ($letter) {
return [$letter];
}, [
'd',
'D',
'j',
'l',
'N',
'S',
'w',
'z',
'W',
'F',
'm',
'M',
'n',
't',
'L',
'o',
'Y',
'y',
'a',
'A',
'B',
'g',
'G',
'h',
'H',
'i',
's',
'u',
'v',
'e',
'I',
'O',
'P',
'T',
'Z',
'U',
'c',
'r',
]);
}

/**
* @dataProvider getFormatLetters
*/
public function testHasFormatWithSingleLetter($letter)
{
$output = Carbon::now()->format($letter);
$this->assertTrue(Carbon::hasFormat($output, $letter), "'$letter' format should match '$output'");
}

public function testIs()
{
$this->assertTrue(Carbon::parse('2019-06-02 12:23:45')->is('2019'));
Expand Down

0 comments on commit 7af4678

Please sign in to comment.