70

I need to get previous month and year, relative to current date.

However, see following example.

// Today is 2011-03-30
echo date('Y-m-d', strtotime('last month'));

// Output:
2011-03-02

This behavior is understandable (to a certain point), due to different number of days in february and march, and code in example above is what I need, but works only 100% correctly for between 1st and 28th of each month.

So, how to get last month AND year (think of date("Y-m")) in the most elegant manner as possible, which works for every day of the year? Optimal solution will be based on strtotime argument parsing.

Update. To clarify requirements a bit.

I have a piece of code that gets some statistics of last couple of months, but I first show stats from last month, and then load other months when needed. That's intended purpose. So, during THIS month, I want to find out which month-year should I pull in order to load PREVIOUS month stats.

I also have a code that is timezone-aware (not really important right now), and that accepts strtotime-compatible string as input (to initialize internal date), and then allows date/time to be adjusted, also using strtotime-compatible strings.

I know it can be done with few conditionals and basic math, but that's really messy, compared to this, for example (if it worked correctly, of course):

echo tz::date('last month')->format('Y-d')

So, I ONLY need previous month and year, in a strtotime-compatible fashion.

Answer (thanks, @dnagirl):

// Today is 2011-03-30
echo date('Y-m-d', strtotime('first day of last month')); // Output: 2011-02-01
mr.b
  • 4,932
  • 11
  • 38
  • 55

15 Answers15

62

Have a look at the DateTime class. It should do the calculations correctly and the date formats are compatible with strttotime. Something like:

$datestring='2011-03-30 first day of last month';
$dt=date_create($datestring);
echo $dt->format('Y-m'); //2011-02
dnagirl
  • 20,196
  • 13
  • 80
  • 123
  • Also, my tz::date() function internally uses DateTime object to do timezone or time adjustments. – mr.b Mar 30 '11 at 17:58
  • @mr.b: Glad I could help. Are you using the DateTimeZone class in your tz::date() function? http://ca2.php.net/manual/en/class.datetimezone.php – dnagirl Mar 30 '11 at 18:27
  • tz class is a convenience wrapper around TzDate class. See http://pastebin.com/GzUcvvA0, perhaps you might find it useful. Code should be self-explanatory. tz class is here http://pastebin.com/8mcRu5qe; however, it's not standalone, as I use Kohana framework, and it is tied with some other classes it offers. – mr.b Mar 30 '11 at 19:46
40

if the day itself doesn't matter do this:

echo date('Y-m-d', strtotime(date('Y-m')." -1 month"));
ITroubs
  • 11,094
  • 4
  • 27
  • 25
  • 3
    Today, that still comes out as March. I'd expect it to be February. – Codecraft Mar 30 '11 at 17:51
  • @codecraft then try it like this: echo date('Y-m-d', strtotime(date('Y-m-d')." -1 month")); – ITroubs Mar 30 '11 at 19:25
  • For me, both those lines give me 2011-03-02. Weird. – Codecraft Mar 30 '11 at 21:09
  • 3
    damn >< had a litle error in it: echo date('Y-m-d', strtotime(date('Y-m-1')." -1 month")); this is what i ment – ITroubs Mar 30 '11 at 22:09
  • @Codecraft "-1 month" and " -1 month" I tried both and they yield different answer, space before hyphen works :D – ha_ryu Oct 31 '18 at 03:58
  • perfect, just what I needed. – carnini Aug 31 '21 at 16:57
  • @Codecraft Same day and twelve years later, I can't seem to reproduce this issue with PHP 7.4. `echo date('Y-m-d', strtotime(date('Y-m')." -1 month"));` is giving me `2023-02-01` as we wanted. The answer works in its original form. Either way, [SeanDowney's answer](https://stackoverflow.com/a/10837560/8958173) explaining why it's sometimes mistaken as a bug is worth a read. – joeljpa Mar 30 '23 at 05:44
33

I found an answer as I had the same issue today which is a 31st. It's not a bug in php as some would suggest, but is the expected functionality (in some since). According to this post what strtotime actually does is set the month back by one and does not modify the number of days. So in the event of today, May 31st, it's looking for April-31st which is an invalid date. So it then takes April 30 an then adds 1 day past it and yields May 1st.

In your example 2011-03-30, it would go back one month to February 30th, which is invalid since February only has 28 days. It then takes difference of those days (30-28 = 2) and then moves two days past February 28th which is March 2nd.

As others have pointed out, the best way to get "last month" is to add in either "first day of" or "last day of" using either strtotime or the DateTime object:

// Today being 2012-05-31
//All the following return 2012-04-30
echo date('Y-m-d', strtotime("last day of -1 month"));
echo date('Y-m-d', strtotime("last day of last month"));
echo date_create("last day of -1 month")->format('Y-m-d'); 

// All the following return 2012-04-01
echo date('Y-m-d', strtotime("first day of -1 month")); 
echo date('Y-m-d', strtotime("first day of last month"));
echo date_create("first day of -1 month")->format('Y-m-d');

So using these it's possible to create a date range if your making a query etc.

SeanDowney
  • 17,368
  • 20
  • 81
  • 90
18

If you want the previous year and month relative to a specific date and have DateTime available then you can do this:

$d = new \DateTimeImmutable('2013-01-01', new \DateTimeZone('UTC')); 
$firstDay = $d->modify('first day of previous month');
$year = $firstDay->format('Y'); //2012
$month = $firstDay->format('m'); //12
Timo Huovinen
  • 53,325
  • 33
  • 152
  • 143
  • This is a better solution to me. Though I would suggest cloning `$d` before modifying it as this will change the value of `$d` completely since its a `TimeDate` object. `$d2 = clone $d` and then call `modify()` on `$d2` like `$d2->modify('first day of previous month')` so that `$d` and `$d2` stay different. – Allan Dereal Apr 19 '21 at 15:37
  • @AllanDereal in that case DateTimeImmutable is more appropriate – Timo Huovinen Apr 21 '21 at 08:50
12
date('Y-m', strtotime('first day of last month'));
tomloprod
  • 7,472
  • 6
  • 48
  • 66
Marek
  • 2,608
  • 4
  • 25
  • 32
8

strtotime have second timestamp parameter that make the first parameter relative to second parameter. So you can do this:

date('Y-m', strtotime('-1 month', time()))
dieend
  • 2,231
  • 1
  • 24
  • 29
  • Really nice and short code, is there any same option for mysql? ie: CURDATE()-$day //30 but it's not return true month if current month is 31 days. – Milad Abooali Jan 20 '17 at 09:53
5

if i understand the question correctly you just want last month and the year it is in:

<?php

  $month = date('m');
  $year = date('Y');
  $last_month = $month-1%12;
  echo ($last_month==0?($year-1):$year)."-".($last_month==0?'12':$last_month);

?>

Here is the example: http://codepad.org/c99nVKG8

Naftali
  • 144,921
  • 39
  • 244
  • 303
5

ehh, its not a bug as one person mentioned. that is the expected behavior as the number of days in a month is often different. The easiest way to get the previous month using strtotime would probably be to use -1 month from the first of this month.

$date_string = date('Y-m', strtotime('-1 month', strtotime(date('Y-m-01'))));
dqhendricks
  • 19,030
  • 11
  • 50
  • 83
  • Programming aside, if I stopped you in the street today (30th March 2011) and asked you what the date was this time last month - and that you HAD to give an answer, would you honestly reply "3rd March"? I wouldn't! – Codecraft Mar 30 '11 at 17:38
  • @Codecraft: perhaps the right answer would be "there was no such time", as there really was no "this time, one month ago". But there really WAS this time one month ago, it just depends on definition of "one month ago". I guess this whole problem is caused by that definition. – mr.b Mar 30 '11 at 18:08
  • @dqhendricks: I didn't mention specific dates, did I? :) Also, interesting choice of days to subtract - how did strtotime come to a conclusion that I wanted to substract as many days as February had? Why not as many days as current month has? Just weird. – mr.b Mar 30 '11 at 18:15
  • @mr.b perhaps since we asked for last month, it figured out how many days were in last month, and subtracted them haha. if it subtracted this months numbers of days, there would be other types of errors. subtracting a month from march first would return a january date. – dqhendricks Mar 30 '11 at 18:18
3

I think you've found a bug in the strtotime function. Whenever I have to work around this, I always find myself doing math on the month/year values. Try something like this:

$LastMonth = (date('n') - 1) % 12;
$Year      =  date('Y') - !$LastMonth;
Zach Rattner
  • 20,745
  • 9
  • 59
  • 82
  • 1
    How is it not a bug? A month is an ill-defined unit without context, and the function lacks contextual support for it. – Zach Rattner Mar 31 '11 at 03:45
  • It's not a bug as discussed in the comments and explained in [SeanDowney's answer](https://stackoverflow.com/a/10837560/) above. But yes, most of us agree that adding some mention of this behaviour in the function would save many of us the trouble. A month is an ill-defined unit in the end and I'll quote this infamous comment by rasmus from [a closed 2003 bug report on php.net](https://bugs.php.net/bug.php?id=22486&edit=2), "If I told you on January 30 that I would come back in exactly one month to beat the crap out of you, when would you think I would show up?" – joeljpa Aug 31 '23 at 06:56
3
date("m-Y", strtotime("-1 months")); 

would solve this

Varshaan
  • 555
  • 9
  • 22
  • Your solution had worked well for me a long time back but now it came back to bite me. It has the same issue which some call it bug (it's not). It won't give you February when called on say 29-31 March because ignoring leap year, 29-31 Feb doesn't exist. In my case, I did `date("m-Y", strtotime("-6 months"))` (with my current date 31 Aug 2023) which returns "03-2023"--when we want Feb 2023. Using `date("m-Y", strtotime("last day of -6 month"))` helped me fix it. Adding this or a clarification in your answer would be good. – joeljpa Aug 31 '23 at 07:16
2

This is because the previous month has less days than the current month. I've fixed this by first checking if the previous month has less days that the current and changing the calculation based on it.

If it has less days get the last day of -1 month else get the current day -1 month:

if (date('d') > date('d', strtotime('last day of -1 month')))
{
    $first_end = date('Y-m-d', strtotime('last day of -1 month'));
}
else
{
    $first_end = date('Y-m-d', strtotime('-1 month'));
}
RJD22
  • 10,230
  • 3
  • 28
  • 35
1

Perhaps slightly more long winded than you want, but i've used more code than maybe nescessary in order for it to be more readable.

That said, it comes out with the same result as you are getting - what is it you want/expect it to come out with?

//Today is whenever I want it to be.
$today = mktime(0,0,0,3,31,2011);

$hour   = date("H",$today);
$minute = date("i",$today);
$second = date("s",$today);
$month  = date("m",$today);
$day    = date("d",$today);
$year   = date("Y",$today);

echo "Today: ".date('Y-m-d', $today)."<br/>";
echo "Recalulated: ".date("Y-m-d",mktime($hour,$minute,$second,$month-1,$day,$year));

If you just want the month and year, then just set the day to be '01' rather than taking 'todays' day:

 $day = 1;

That should give you what you need. You can just set the hour, minute and second to zero as well as you aren't interested in using those.

 date("Y-m",mktime(0,0,0,$month-1,1,$year);

Cuts it down quite a bit ;-)

Codecraft
  • 8,291
  • 4
  • 28
  • 45
  • Actually, thats really interesting. I would expect and want this code to result in 28th Feb or 1st March, but definately not 3rd March! I call bug! – Codecraft Mar 30 '11 at 17:16
  • Well, it all really depends on what do you expect, I think. That's why I've been careful enough not to call it *a bug* off the bat, because it might be *desired* functionality. If I asked for `date('Y-m-d', strtotime('last month'))`, on 31st of march, which date should it return? Last day of February? I'm not quite sure.. Perhaps someone with more experience in the area can shed some light on why is this how strtotime behaves.. – mr.b Mar 30 '11 at 17:56
  • You're quite right, for me, its strange behavior but I guess this is one of those questions that may not have a 'right' answer. – Codecraft Mar 30 '11 at 21:07
0

If a DateTime solution is acceptable this snippet returns the year of last month and month of last month avoiding the possible trap when you run this in January.

function fn_LastMonthYearNumber()
{
 $now = new DateTime();
 $lastMonth = $now->sub(new DateInterval('P1M'));
 $lm= $lastMonth->format('m');
 $ly= $lastMonth->format('Y');
 return array($lm,$ly);
}
zzapper
  • 4,743
  • 5
  • 48
  • 45
  • While this code snippet may solve the problem, it doesn't explain why or how it answers the question. Please [include an explanation for your code](//meta.stackexchange.com/q/114762/269535), as that really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. **Flaggers / reviewers:** [For code-only answers such as this one, downvote, don't delete!](//meta.stackoverflow.com/a/260413/2747593) – Scott Weldon Nov 11 '16 at 16:00
0
//return timestamp, use to format month, year as per requirement
function getMonthYear($beforeMonth = '') {
    if($beforeMonth !="" && $beforeMonth >= 1) {
        $date = date('Y')."-".date('m')."-15";
        $timestamp_before = strtotime( $date . ' -'.$beforeMonth.' month' );
        return $timestamp_before;
    } else {
        $time= time();
        return $time;
    }
}


//call function
$month_year = date("Y-m",getMonthYear(1));// last month before  current month
$month_year = date("Y-m",getMonthYear(2)); // second last month before current month
Mukesh
  • 1
0
function getOnemonthBefore($date){
    $day = intval(date("t", strtotime("$date")));//get the last day of the month
    $month_date = date("y-m-d",strtotime("$date -$day days"));//get the day 1 month before
    return $month_date;
}

The resulting date is dependent to the number of days the input month is consist of. If input month is february (28 days), 28 days before february 5 is january 8. If input is may 17, 31 days before is april 16. Likewise, if input is may 31, resulting date will be april 30.

NOTE: the input takes complete date ('y-m-d') and outputs ('y-m-d') you can modify this code to suit your needs.