Your ~(?<MOD>\d{1,2}[-/])(?&MOD)(?:\d{4}|\d{2})~
pattern takes 204 steps on your sample string.
The equivalent ~(?:\d{1,2}[-/]){2}(?:\d{4}|\d{2})~
pattern takes 188steps on your sample string.
The step count is a loose metric available to help you understand the efficiency of your pattern. The step count indicates that the recursion is doing more work than necessary.
But consider the string 5/6-1999
, neither of the above patterns are preventing mixed delimiters.
Using a capture group on the first delimiter and then matching that specific delimiting character as the second delimiter seems critical if you are not going to pre-sanitize the input string. ~\d{1,2}([-/])\d{1,2}\1(?:\d{4}|\d{2})~
This further reduces the step count to 161, so it not only improves accuracy, it improves efficiency, but at a trivial cost to pattern brevity.
What @KIKOSoftware mentions as a comment under the question is also important. Your list of valid formats is not world-ready.
So, what is your goal here? If you are trying to validate a date looking "kinda" like a date, regex is fine. If you are actually trying to validate a date as a real date, then regex is certainly the wrong tool for the job.
To validate the string, use a loose pattern to isolate the components of your string, then feed them to the datetime class for parsing. If the string is parsed as a valid date that matches the sanitized input, then it is a good date. (Demo)
function isValidDate(string $date, string $format = 'm/d/Y', string $yearPadding = '19'): bool {
$date = preg_replace_callback(
'~^(\d+)(\D)(\d+)\2(\d{2}|\d{4})$~',
fn($m) => sprintf('%02d/%02d/%d', $m[1], $m[3], str_pad($m[4], 4, $yearPadding, STR_PAD_LEFT)),
$date
); // sanitize
$d = DateTime::createFromFormat($format, $date); // attempt to parse
return $d && $d->format($format) === $date; // check if formatted string is same as sanitized string
}
To validate the string and return it if it is deemed to be valid, you can modify the above like this: (Demo)
function getFormattedDate(string $date, string $format = 'm/d/Y', string $yearPadding = '19'): string {
$sanitizedDate = preg_replace_callback(
'~^(\d+)(\D)(\d+)\2(\d{2}|\d{4})$~',
fn($m) => sprintf('%02d/%02d/%d', $m[1], $m[3], str_pad($m[4], 4, $yearPadding, STR_PAD_LEFT)),
$date
);
$d = DateTime::createFromFormat($format, $sanitizedDate);
if (!$d) {
throw new Exception("Could not parse date: $date");
}
$formattedDate = $d->format($format);
if ($formattedDate !== $sanitizedDate) {
throw new Exception("Date not in a desired American format: $date");
}
return $date;
}
foreach ($tests as $test) {
try {
echo getFormattedDate($test);
} catch (Exception $e) {
echo $e->getMessage();
}
echo "\n";
}