First & Last Day of Last Month

I am trying to return the first day of last month and the last day of last month, is there a better way to do this?


tell (the current date) to set LastDayofLastMonth to it - (its day) * days
set LastDayofLastMonth's time to 86399
set FirstDayofLastMonth to month of LastDayofLastMonth & "/1/" & year of LastDayofLastMonth as string
set FirstDayofLastMonth to date FirstDayofLastMonth
set FirstDayofLastMonth's time to 0

Hi.

A couple of alternatives:


tell (the current date) to set LastDayofLastMonth to it - (its day) * days
set LastDayofLastMonth's time to 86399
copy LastDayofLastMonth to FirstDayofLastMonth
set FirstDayofLastMonth's day to 1
set FirstDayofLastMonth's time to 0

Or:


tell (the current date) to set LastDayofLastMonth to it - ((its day) * days - (86399 - (its time)))
tell LastDayofLastMonth to set FirstDayofLastMonth to it - (((its day) - 1) * days + 86399)

Awesome Nigel, thank you!

These calculations are arranged so that there’s only one piece of date-object arithmetic in each. The rest is all done by integer arithmetic, which is parenthesised for precalculation at the end of each line. This is the most efficient way computationally, but can make it difficult both to work out how to arrange nested signs when you’re writing it and to work out how they work afterwards! These versions may be better:


tell (current date) to set LastDayofLastMonth to it + (86399 - (its time) - (its day) * days)
tell LastDayofLastMonth to set FirstDayofLastMonth to it + (1 - (its day) * days)

This is the Foundation version, looks pretty cumbersome but is very reliable, because it avoids the ugly 86400 math which could brake in months with daylight saving time changes.


use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

property |⌘| : a reference to current application

-- get the current calendar
set currentCalendar to |⌘|'s NSCalendar's currentCalendar()
-- create date components month -1
set minusOneMonthComponent to |⌘|'s NSDateComponents's new()
set |month| of minusOneMonthComponent to -1
-- subtract one month from the current date
set lastMonthDate to currentCalendar's dateByAddingComponents:minusOneMonthComponent toDate:(current date) options: |⌘|'s NSCalendarMatchNextTime
-- get startDate (as NSDate) and the interval (in seconds) for last month
set {success, startDate, interval} to currentCalendar's rangeOfUnit:(|⌘|'s NSMonthCalendarUnit) startDate:(reference) interval:(reference) forDate:lastMonthDate
-- add the interval - 1 to the startDate to get the endDate
set endDate to currentCalendar's dateByAddingUnit:(|⌘|'s NSSecondCalendarUnit) value:(interval - 1) toDate:startDate options:0

display dialog ((startDate as date) as string) & space & ((endDate as date) as string)

Hello Stefan

I’m puzzled.
Here your script returns:
display dialog “mercredi 2 janvier 2019 à 00:00:00 mercredi 2 janvier 2019 à 23:59:59”

which doesn’t match the question asked in this thread.

The Nigel’s script return correctly:
{date “mardi 1 janvier 2019 à 00:00:00”, date “jeudi 31 janvier 2019 à 23:59:59”}

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) samedi 2 février 2019 12:30:30

Yvan,

you’re right, I specified the wrong calendar unit (day instead of month). I updated the post

Hi Stefan.

AppleScript dates are currently based entirely on the proleptic Gregorian calendar and local daylight saving considerations aren’t relevant in this kind of problem — which doesn’t take away from the usefulness of your script, of course. :slight_smile:

@ StefanK

Thank you.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) samedi 2 février 2019 14:35:44

Here’s a simpler ASObjC version:

use AppleScript version "2.5" -- El Capitan (10.11) or later (for date <-> NSDate coercions).
use framework "Foundation"
use scripting additions

set |⌘| to current application

-- get the current calendar
set currentCalendar to |⌘|'s NSCalendar's currentCalendar()
-- Get the beginning of this month.
set {success, startOfMonth} to currentCalendar's rangeOfUnit:(|⌘|'s NSMonthCalendarUnit) startDate:(reference) interval:(missing value) forDate:(current date)
-- Set endDate to one second before that.
set endDate to startOfMonth's dateByAddingTimeInterval:-1
-- Set startDate to the beginning of endDate's month.
set {success, startDate} to currentCalendar's rangeOfUnit:(|⌘|'s NSMonthCalendarUnit) startDate:(reference) interval:(missing value) forDate:endDate

display dialog ((startDate as date) as text) & linefeed & (endDate as date)

Or:

use AppleScript version "2.5" -- El Capitan (10.11) or later (for date <-> NSDate coercions).
use framework "Foundation"
use scripting additions

set monthUnit to current application's NSMonthCalendarUnit
-- get the current calendar
set currentCalendar to current application's NSCalendar's currentCalendar()
--  get  a date last month
set lastMonth to currentCalendar's dateByAddingUnit:monthUnit value:-1 toDate:(current date) options:0
--  get start of month and its length
set {success, startDate, monthLength} to currentCalendar's rangeOfUnit:monthUnit startDate:(reference) interval:(reference) forDate:lastMonth
-- create end-of-month date
set endDate to startDate's dateByAddingTimeInterval:(monthLength - 1)

return ((startDate as date) as text) & linefeed & (endDate as date)

Ah. Essentially the same idea as Stefan’s but using dateByAddingUnit:value:toDate:options: instead of dateByAddingComponents:toDate:options: to subtract the month, which saves having to set up an NSDateComponents instance.

I have to say I’m totally defeated by the Xcode documentation’s explanations of the options which can be specified for both methods. The explanations are complete gobbledygook and none of the options described makes the slightest difference to what the methods return. :confused:

Nigel, please watch WWDC 2013 Session 227: Solutions to Common Date and Time Challenges from 37:00

Hi Stefan.

Thanks for the link and for taking the trouble to locate the point of interest! It’s clarified NSCalendarOptions for me to some extent and is something I can consult again if I ever need to use them. I’ll watch the whole video later on too as it looks very interesting.

It only emphasises the fact that the vanilla solutions in posts #2 and #4 are the best for maweir’s purposes. It’s the Foundation methods which introduce the local time-shift complications. With AS date arithmetic, it’s very simply to arrive at date objects representing the required dates and times without having to think about whether those dates and times are the same distance apart in real life.

Addendum: Well! It appears that starting last year, the Brazilian states which observe DST put their clocks forward at 12 am on the first Sunday in November. This means that if the vanilla AS scripts are run in one of those states during December in a year in which November starts on a Sunday, they’ll still return the November start date as 00:00 on the 1st, whereas it could be argued that November actually started at 01:00 that year. The ASObjC scripts may give the correct result, but only if the system database on which they depend has been updated since the Brazilian president’s decree was signed at the end of 2017.

Another vanilla approach using concatenation for formatting:

#ordered for United States:

set {isMonth, isDay, isYear} to (current date)'s {month, its day, year}
set Antecedent to (isMonth - 1) & "-" & 1 & "-" & isYear as text
set Current to isMonth & "-" & 1 & "-" & isYear as text
{date Antecedent, (date Current) - 1 * days - 1}

#ordered for other regions:

set {isDay, isMonth, isYear} to (current date)'s {its day, month, year}
set Antecedent to 1 & "-" & (isMonth - 1) & "-" & isYear as text
set Current to 1 & "-" & isMonth & "-" & isYear as text
{date Antecedent, (date Current) - 1 * days - 1}

–edited for regional differences, clarity among variants

Hi Marc.

Your script only works in the States. :slight_smile:

Hi Marc.

Does this work unedited on your machine too?

-- Use a test date to set indices for a three-item list according to the user's date-order preference.
tell date ("01/02/03") to set {d, m, y} to {its day, (its month) as integer, (its year) mod 10}

set Antecedent to {missing value, missing value, missing value}
copy Antecedent to Current

-- Get the current date's day, month, and year.
set {isDay, isMonth, isYear} to (current date)'s {day, month, year}
-- Insert the relevant figures into the relevant slots of the relevant lists.
if (isMonth is January) then
	tell Antecedent to set {item d, item m, item y} to {1, 12, isYear - 1}
else
	tell Antecedent to set {item d, item m, item y} to {1, isMonth - 1, isYear}
end if
tell Current to set {item d, item m, item y} to {1, isMonth, isYear}
-- Coerce the lists to text using a suitable delimiter.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "-"
set Antecedent to Antecedent as text
set Current to Current as text
set AppleScript's text item delimiters to astid
-- Return the required date objects.
{date Antecedent, (date Current) - 1}

Edit: First two lines combined and made more straightforward. I originally used to specify the test date as date (“1/2/3” as text), but this now produces a date near the beginning of the era which doesn’t match the date string. However, it turns out that two-digit years are still interpreted as being in the current century, where the date string bug doesn’t occur, so I used “10/11/12” with additional math here. It only occurred to me this morning to try using leading zeros with the original numbers!

Yes, that’s working fine. I often forget that possibly the majority of the people on this forum are abroad.

They’re not flash. That’s why I tried to use an option of 0.