3

I'm trying to use DateTime to check if a credit card expiry date has expired but I'm a bit lost.

I only want to compare the mm/yy date.

Here is my code so far

$expmonth = $_POST['expMonth']; //e.g 08
$expyear = $_POST['expYear']; //e.g 15

$rawExpiry = $expmonth . $expyear;

$expiryDateTime = \DateTime::createFromFormat('my', $rawExpiry);
$expiryDate = $expiryDateTime->format('m y');

$currentDateTime = new \DateTime();
$currentDate = $currentDateTime->format('m y');

if ($expiryDate < $currentDate) {
    echo 'Expired';
} else {
    echo 'Valid';
}

I feel i'm almost there but the if statement is producing incorrect results. Any help would be appreciated.

kjones
  • 1,339
  • 1
  • 13
  • 28
paul
  • 731
  • 2
  • 9
  • 13

8 Answers8

11

It's simpler than you think. The format of the datess you are working with is not important as PHP does the comparison internally.

$expires = \DateTime::createFromFormat('my', $_POST['expMonth'].$_POST['expYear']);
$now     = new \DateTime();

if ($expires < $now) {
    // expired
}
John Conde
  • 217,595
  • 99
  • 455
  • 496
  • This even works when the date is the current month ! – Francisco Corrales Morales Oct 08 '14 at 21:53
  • 4
    Tripped me up a little bit, make sure your expiration date is only 2 digits. If it is 4 digits (ex: 2017 instead of 17), then you need to use: `$expires = \DateTime::createFromFormat('mY', $_POST['expMonth'].$_POST['expYear']);` (capitol Y instead of lowercase) – Jimmy Pelton Jan 02 '17 at 15:50
  • 2
    This is a good answer, but does not take not take into account the actual expiry DATE of the credit/debit card. `DateTime::createFromFormat` will use todays date of the month (e.g. 17) if is not specified. This means that a credit card could appear expired when it still has several days to go. If a card expires 06/20 (i.e. June 2020) then it actually stops working at 00:00:00 on 1st July 2020. The modify method fixes this. – featherbelly Jul 17 '17 at 14:24
  • I've posted a full answer below. – featherbelly Jul 17 '17 at 14:55
  • This won't work if the expiry month is single-digit… plus the issue pointed out by @featherbelly. – Fabien Snauwaert Apr 13 '20 at 23:11
  • @FabienSnauwaert This isn't supposed to work with single digits as the OP is specifically working with double digits months. – John Conde Apr 13 '20 at 23:59
  • Not working for the current month/year because $expires microseconds are .000000 and $now has a non-zero value. – user151851 Jun 02 '21 at 15:43
9

You can use the DateTime class to generate a DateTime object matching the format of your given date string using the DateTime::createFromFormat() constructor.

The format ('my') would match any date string with the string pattern 'mmyy', e.g. '0620'. Or for dates with 4 digit years use the format 'mY' which will match dates with the following string pattern 'mmyyyy', e.g. '062020'. It's also sensible to specify the timezone using the DateTimeZone class.

$expiryMonth = 06;
$expiryYear = 20;
$timezone = new DateTimeZone('Europe/London');
$expiryTime = \DateTime::createFromFormat('my', $expiryMonth.$expiryYear, $timezone);

See the DateTime::createFromFormat page for more formats.

However - for credit/debit card expiry dates you will also need to take into account the full expiry DATE and TIME - not just the month and year.

DateTime::createFromFormat will by default use todays day of the month (e.g. 17) if it is not specified. This means that a credit card could appear expired when it still has several days to go. If a card expires 06/20 (i.e. June 2020) then it actually stops working at 00:00:00 on 1st July 2020. The modify method fixes this. E.g.

$expiryTime = \DateTime::createFromFormat('my', $expiryMonth.$expiryYear, $timezone)->modify('+1 month first day of midnight');

The string '+1 month first day of midnight' does three things.

  • '+1 month' - add one month.
  • 'first day of' - switch to the first day of the month
  • 'midnight' - change the time to 00:00:00

The modify method is really useful for many date manipulations!

So to answer the op, this is what you need — with a slight adjustment to format to cater for single digit months:

$expiryMonth = 6;
$expiryYear = 20;
$timezone = new DateTimeZone('Europe/London');
$expiryTime = \DateTime::createFromFormat(
  'm-y',
   $expiryMonth.'-'.$expiryYear,
   $timezone
)->modify('+1 month first day of midnight');

$currentTime = new \DateTime('now', $timezone);

if ($expiryTime < $currentTime) {
    // Card has expired.
}
featherbelly
  • 935
  • 11
  • 16
  • This won't work if the month is single-digit. A simple fix is to use 'm-y' (or 'm-Y' if the year is four-digit.) – Fabien Snauwaert Apr 13 '20 at 23:13
  • Months on credit/debit cards are never single digit — numbers under 10 begin with a zero, e.g. 06. – featherbelly Apr 20 '20 at 12:19
  • Never say "never". They're two-digit as written on credit cards, yes; but you can't be sure what the data will be. For example, the Stripe API returns `int(4)` for an expiry month in April – effectively single-digit. – Fabien Snauwaert Apr 20 '20 at 19:07
6

An addition to the above answers. Be aware that by default the days will also be in the calculation. For example today is 2019-10-31 and if you run this:

\DateTime::createFromFormat('Ym', '202111');

It will output 2021-12-01, because day 31 does not exist in November and it will add 1 extra day to your DateTime object with a side effect that you will be in the month December instead of the expected November.

My suggestion is always use the day in your code.

EricNL
  • 107
  • 1
  • 3
2

For op's question:

$y=15;
$m=05;

if(strtotime( substr(date('Y'), 0, 2)."{$y}-{$m}" ) < strtotime( date("Y-m")  ))
{
  echo 'card is expired';
}

For others with full year:

$y=2015;
$m=5;

if(strtotime("{$y}-{$m}") < strtotime( date("Y-m")  ))
{
  echo 'card is expired';
}
angelcool.net
  • 2,505
  • 1
  • 24
  • 26
1

Would it not be simpler to just compare the string "201709" to the current year-month? Creating datetime objects will cost php some effort, I suppose.

if($_POST['expYear']. str_pad($_POST['expMonth'],2,'0', STR_PAD_LEFT ) < date('Ym')) {
   echo 'expired';

}

edited as Adam states

Ivo P
  • 1,722
  • 1
  • 7
  • 18
0

The best answer is provided by John Conde above. It it does the minimum amount of processing: creates two correct DateTime objects, compares them and that's all it needs.

It could work also as you started but you must format the dates in a way that puts the year first.

Think a bit about it: as dates, 08/15 (August 2015) is after 12/14 (December 2014) but as strings, '08 15' is before '12 14'.

When the year is in front, even as strings the years are compared first and then, only when the years are equal the months are compared:

$expiryDate  = $expiryDateTime->format('y m');
$currentDate = $currentDateTime->format('y m');

if ($expiryDate < $currentDate) {
    echo 'Expired';
} else {
    echo 'Valid';
}
Community
  • 1
  • 1
axiac
  • 68,258
  • 9
  • 99
  • 134
0

Keep it simple, as the answer above me says except you need to string pad to the left:

isCardExpired($month, $year)
{
    $expires = $year.str_pad($month, 2, '0', STR_PAD_LEFT);
    $now = date('Ym');

    return $expires < $now;
}

No need to add extra PHP load using DateTime

Adam Moss
  • 5,582
  • 13
  • 46
  • 64
  • When working with dates it's always best to use DateTime as it designed specifically for this purpose. The overhead is negligible. – John Conde Apr 16 '19 at 14:31
  • Having worked a lot with dates I agree, but I find this is more of an integer comparison and I like its simplicity. – Adam Moss Apr 17 '19 at 15:17
0

If you are using Carbon, which is a very popular Datetime extension library. Then this should be:

$expMonth = $_POST['month'];
$expYear = $_POST['year'];
$format_m_y = str_pad($expMonth,2,'0', STR_PAD_LEFT).'-'.substr($expYear, 2);

$date = \Carbon\Carbon::createFromFormat('m-y', $format_m_y)
         ->endOfMonth()
         ->startOfDay();

if ($date->isPast()) {
  // this card is expired
}

Also take into consideration the exact date and time expiration:

Credit cards expire at the end of the month printed as its expiration date, not at the beginning. Many cards actually technically expire one day after the end of that month. In any case, unless they list a specific day of expiration along with month and year, they should work all the way through the end of their expiration month. Cardholders should not wait until the last moment to secure a replacement card. Source

Ner
  • 158
  • 7