NSDate date calculations

As written, the following script returns an incorrect result, and this appears to result from the time of current date. The disabled code appears to fix this. Is there a better way to do this? I’m working on this just to learn a bit more about NSDate and NSCalendar. Thanks.

use framework "Foundation"
use scripting additions

set {futureYear, futureMonth, futureDay} to {2032, 5, 25}
set currentDate to (current date)
-- set {hours of currentDate, minutes of currentDate, seconds of currentDate} to {0, 0, 0}
set theCalendar to current application's NSCalendar's currentCalendar()
set futureDate to theCalendar's dateWithEra:1 |year|:futureYear |month|:futureMonth |day|:futureDay hour:0 minute:0 |second|:0 nanosecond:0
set dateComponents to (current application's NSCalendarUnitYear) + (get current application's NSCalendarUnitMonth) + (get current application's NSCalendarUnitDay) --> 28
set dateDifferences to theCalendar's components:dateComponents fromDate:currentDate toDate:futureDate options:0
--> Calendar Year: 9 - Month: 11 - Day: 29 as written
--> Calendar Year: 10 - Month: 0 - Day: 0 with commented code enabled

BTW, the above is my repurposing of a script written by Shane.

What’s being returned is actually elapsed years, months and days. If current date’s time is, say, midday, the day counter will only increment when futureDate’s time reaches midday.

Thanks Shane–your comment suggests a slightly different approach. My goal (which may or may not be useful) is to get the elapsed years, months, and days at the user’s locale without regard to time. So, another way to accomplish this without changing current date is to change futureDate’s time:

use framework "Foundation"
use scripting additions

set {futureYear, futureMonth, futureDay} to {2032, 5, 26}
set theCalendar to current application's NSCalendar's currentCalendar()
set futureDate to theCalendar's dateWithEra:1 |year|:futureYear |month|:futureMonth |day|:futureDay hour:24 minute:0 |second|:0 nanosecond:0
set dateComponents to (current application's NSCalendarUnitYear) + (get current application's NSCalendarUnitMonth) + (get current application's NSCalendarUnitDay) --> 28
set dateDifference to theCalendar's components:dateComponents fromDate:(current date) toDate:futureDate options:0 --> Calendar Year: 10 Month: 0 Day: 0

And, the corresponding script to get elapsed years, months, and days without regard to time from a date in the past would be:

use framework "Foundation"
use scripting additions

set {pastYear, pastMonth, pastDay} to {2012, 5, 26}
set theCalendar to current application's NSCalendar's currentCalendar()
set pastDate to theCalendar's dateWithEra:1 |year|:pastYear |month|:pastMonth |day|:pastDay hour:0 minute:0 |second|:0 nanosecond:0
set dateComponents to (current application's NSCalendarUnitYear) + (get current application's NSCalendarUnitMonth) + (get current application's NSCalendarUnitDay)
set dateDifference to theCalendar's components:dateComponents fromDate:pastDate toDate:(current date) options:0 --> Calendar Year: 10 Month: 0 Day: 0

That’s the most efficient, but there’s also an NSCalendar method -startOfDayForDate:.

Thanks Shane–I think I prefer that approach. My revised scripts:

use framework "Foundation"
use scripting additions

-- past to current date
set pastYear to 2012
set pastMonth to 5
set pastDay to 27
set theCalendar to current application's NSCalendar's currentCalendar()
set pastDate to theCalendar's dateWithEra:1 |year|:(pastYear) |month|:(pastMonth) |day|:pastDay hour:0 minute:0 |second|:0 nanosecond:0
set currentDate to theCalendar's startOfDayForDate:(current date)
set dateDifference to theCalendar's components:28 fromDate:pastDate toDate:currentDate options:0 -- use 16 instead of 28 to return days only

-- current to future date
set futureYear to 2032
set futureMonth to 5
set futureDay to 27
set theCalendar to current application's NSCalendar's currentCalendar()
set currentDate to theCalendar's startOfDayForDate:(current date)
set futureDate to theCalendar's dateWithEra:1 |year|:(futureYear) |month|:(futureMonth) |day|:futureDay hour:0 minute:0 |second|:0 nanosecond:0
set dateDifference to theCalendar's components:28 fromDate:currentDate toDate:futureDate options:0 -- use 16 instead of 28 to return days only

-- return values for either of the above
set yearDifference to dateDifference's |year|()
set monthDifference to dateDifference's |month|()
set dayDifference to dateDifference's |day|()
set dateDifference to {yearDifference, monthDifference, dayDifference}

I spent some more time on this and thought I’d post my scripts FWIW. They all calculate a future date based on user-specified values.

use framework "Foundation"
use scripting additions

-- future date from current date (returned time is current time)
set theDays to 365
set futureNSDate to current application's NSDate's dateWithTimeIntervalSinceNow:(theDays * days)
set futureASDate to (futureNSDate as date)

-- future date from current date (returned time is current time)
set dateComponent to 16 -- use 4 for year, 8 for month, and 16 for day
set dateComponentValue to 365
set startDate to current application's NSDate's now()
set theCalendar to current application's NSCalendar's currentCalendar()
set futureNSDate to theCalendar's dateByAddingUnit:dateComponent value:dateComponentValue toDate:startDate options:0
set futureASDate to (futureNSDate as date)

-- future date from specified start date (returned time is start of day)
set startYear to 2022
set startMonth to 5
set startDay to 29
set dateComponent to 16 -- use 4 for year, 8 for month, and 16 for day
set dateComponentValue to 365
set theCalendar to current application's NSCalendar's currentCalendar()
set startDate to theCalendar's dateWithEra:1 |year|:(startYear) |month|:(startMonth) |day|:startDay hour:0 minute:0 |second|:0 nanosecond:0
set futureNSDate to theCalendar's dateByAddingUnit:dateComponent value:dateComponentValue toDate:startDate options:0
set futureASDate to (futureNSDate as date)

The above scripts will not work with dates during the period designated by ASObjC as era 0, which is commonly known as BC or BCE, and I have included below scripts that are written to work with these dates. The first script calculates the difference between two dates.

-- revised 2022/06/02

use framework "Foundation"
use scripting additions

set startYear to 2020
set startMonth to 1
set startDay to 1
set startEra to 1 -- 0 for BC and 1 for AD

set endYear to 2024 -- a leap year
set endMonth to 2
set endDay to 29
set endEra to 1

set dateComponents to 28 -- use 4 for year, 8 for month, 16 for day, 28 for YMD

set theCalendar to current application's NSCalendar's currentCalendar()
set startDate to theCalendar's dateWithEra:startEra |year|:(startYear) |month|:(startMonth) |day|:startDay hour:0 minute:0 |second|:0 nanosecond:0
set endDate to theCalendar's dateWithEra:endEra |year|:(endYear) |month|:(endMonth) |day|:endDay hour:0 minute:0 |second|:0 nanosecond:0
set dateDifference to theCalendar's components:dateComponents fromDate:startDate toDate:endDate options:0
set dateDifference to integers of {dateDifference's |year|(), dateDifference's |month|(), dateDifference's |day|()}
--> {4, 1, 28}

The second script returns a future date as a string based on user-specified values.

-- revised 2022/06/05

use framework "Foundation"
use scripting additions

set startYear to 2020
set startMonth to 1
set startDay to 1
set startEra to 1 -- 0 for BC and 1 for AD

set addYears to 4
set addMonths to 1
set addDays to 28

set dateComponents to current application's NSDateComponents's new()
dateComponents's setYear:addYears
dateComponents's setMonth:addMonths
dateComponents's setDay:addDays

set theCalendar to current application's NSCalendar's currentCalendar()
set startDate to theCalendar's dateWithEra:startEra |year|:(startYear) |month|:(startMonth) |day|:startDay hour:0 minute:0 |second|:0 nanosecond:0
set futureDate to theCalendar's dateByAddingComponents:dateComponents toDate:(startDate) options:0

set dateFormatter to current application's NSDateFormatter's new()
dateFormatter's setDateFormat:"EEEE, MMMM d, yyy G"
set futureDate to ((dateFormatter's stringFromDate:futureDate) as text)
--> "Thursday, February 29, 2024 AD"

Just as an aside, the whole topic of date math and formatting is a complicated one, made even more complex by differing calendars and customs in various locales. These scripts should not be relied on for anything substantive without verification of the results by the user.

The documentation for NSCalendar provides a lot of useful information on this topic:

https://developer.apple.com/documentation/foundation/nscalendar?language=objc

The following is Apple documentation on historical dates:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtHist.html

I’ve pretty much completed my research on this topic but wanted to add a note concerning the second script in the immediately preceding post. It returns the expected result with the following values:

set startYear to 7
set startMonth to 12
set startDay to 31
set startEra to 1
set dateComponent to 16 -- i.e. days
set dateComponentValue to 1
--> "Sunday January 1, 008 AD"

To my way of thinking, it does not return the correct result with these values:

set startYear to 7
set startMonth to 12
set startDay to 31
set startEra to 0
set dateComponent to 16 -- i.e. days
set dateComponentValue to 1
--> "Friday January 1, -005 BC"

There is no year 0, and that may be the reason for the above. Anyways, in the unlikely event that anyone needs to add to BC dates, the above should be kept in mind.

Hi peavine.

That input returns “Friday January 1, -006 BC” on my Mojave machine, which is the correct year. I think the "y"s in the format string should be lower case, which gets rid of the minus sign.

Thanks Nigel. Correcting the year format fixed both issues–it got rid of the minus sign and the returned year is now correct. I’ve made necessary corrections above.

BTW, the following is a link to a Unicode document that details date format patterns. You posted it in another thread, and I thought other forum members might find it helpful.

http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns

I used Shane’s Dialog Toolkit to create dialogs for the second script in post 7 above. Dialog Toolkit is a freeware script library that can be downloaded from:

https://latenightsw.com/freeware/

-- revised 2022.09.28

use framework "Foundation"
use script "Dialog Toolkit Plus" version "1.1.0"
use scripting additions

on main()
	set dateData to getDateData()
	set {dateData, dateString} to getDateString(dateData)
	displayDate(dateData, dateString)
end main

on getDateData()
	set dialogWidth to 340
	set verticalSpace to 12
	
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} cancel button 1 default button 2
	set {eraPopup, eraLabel, theTop} to create labeled popup {"1 (AD)", "0 (BC)"} left inset 0 bottom 0 popup width 80 max width dialogWidth label text "Start Era:" popup left 0 initial choice "1 (AD)"
	set {formatPopup, formatLabel, theTop} to create labeled popup {"Long", "Standard", "Short"} left inset 0 bottom 0 popup width 100 max width dialogWidth label text "Date Format:" popup left 240 initial choice "Long"
	set {addDateField, theTop} to create field "" placeholder text "Date Components (Y/M/D)" left inset 0 bottom (theTop + verticalSpace) field width dialogWidth
	set {addDateMessage, theTop} to create label "Enter the date components to add to the start date. Days are used if a single number is entered." bottom theTop + verticalSpace max width dialogWidth control size regular size aligns left
	set {startDateField, theTop} to create field "" placeholder text "Start Date (Y/M/D)" left inset 0 bottom (theTop + verticalSpace) field width dialogWidth
	set {startDateMessage, theTop} to create label "Enter a start date. The current date is used if nothing is entered." bottom theTop + verticalSpace max width dialogWidth control size regular size aligns left
	
	set allControls to {eraPopup, eraLabel, formatPopup, formatLabel, startDateField, startDateMessage, addDateField, addDateMessage}
	set {buttonName, controlsResults} to display enhanced window "Date Calculator" acc view width dialogWidth acc view height theTop acc view controls allControls buttons theButtons initial position {} without align cancel button
	
	return {item 5, item 7, item 1, item 3} of controlsResults
end getDateData

on getDateString(dateData)
	set dateData to current application's NSMutableArray's arrayWithArray:dateData
	
	if ((dateData's objectAtIndex:0)'s isEqualToString:"") as boolean is true then
		if ((dateData's objectAtIndex:2)'s isEqualToString:"1 (AD)") as boolean is true then
			dateData's replaceObjectAtIndex:0 withObject:getCurrentDate()
		else
			errorAlert("Current date cannot be used when \"era 0 (BC)\" is selected")
		end if
	end if
	
	set startDate to ((dateData's objectAtIndex:0)'s componentsSeparatedByString:"/")
	set addDate to ((dateData's objectAtIndex:1)'s componentsSeparatedByString:"/")
	
	if addDate's |count|() = 1 then
		set addDate to current application's NSArray's arrayWithArray:{"0", "0", (addDate's objectAtIndex:0)}
		dateData's replaceObjectAtIndex:1 withObject:(addDate's componentsJoinedByString:"/")
	end if
	
	set thePredicate to current application's NSPredicate's predicateWithFormat:"self == ''"
	set startDateCheck to (startDate's filteredArrayUsingPredicate:thePredicate)'s |count|()
	set addDateCheck to (addDate's filteredArrayUsingPredicate:thePredicate)'s |count|()
	if startDateCheck > 0 or addDateCheck > 0 then errorAlert("The entered data did not contain a required date component")
	
	try
		set startYear to ((startDate's objectAtIndex:0) as integer)
		set startMonth to ((startDate's objectAtIndex:1) as integer)
		set startDay to ((startDate's objectAtIndex:2) as integer)
		set addYears to ((addDate's objectAtIndex:0) as integer)
		set addMonths to ((addDate's objectAtIndex:1) as integer)
		set addDays to ((addDate's objectAtIndex:2) as integer)
	on error
		errorAlert("The entered data either contained an unrecognized character or did not contain a required date component")
	end try
	
	if ((dateData's objectAtIndex:2)'s isEqualToString:"1 (AD)") as boolean is true then
		set startEra to 1
	else
		set startEra to 0
	end if
	
	set dateComponents to current application's NSDateComponents's new()
	dateComponents's setYear:addYears
	dateComponents's setMonth:addMonths
	dateComponents's setDay:addDays
	
	set theCalendar to current application's NSCalendar's currentCalendar()
	set startDate to theCalendar's dateWithEra:startEra |year|:(startYear) |month|:(startMonth) |day|:startDay hour:0 minute:0 |second|:0 nanosecond:0
	set futureDate to theCalendar's dateByAddingComponents:dateComponents toDate:(startDate) options:0
	
	set dateFormatter to current application's NSDateFormatter's new()
	if ((dateData's objectAtIndex:3)'s isEqualToString:"Long") as boolean is true then
		dateFormatter's setDateFormat:"EEEE, MMMM d, yyy G"
	else if ((dateData's objectAtIndex:3)'s isEqualToString:"Standard") as boolean is true then
		dateFormatter's setDateFormat:"MMMM d, yyy"
	else
		dateFormatter's setDateFormat:"yyyy/MM/dd"
	end if
	
	set futureDate to ((dateFormatter's stringFromDate:futureDate) as text)
	
	return {dateData, futureDate}
end getDateString

on displayDate(dateData, dateString)
	set dialogWidth to 280
	set verticalSpace to 10 -- change as desired
	
	set {theButtons, minWidth} to create buttons {"Cancel", "Copy to Clipboard"} cancel button 1 default button 2
	set {addDateLabel, theTop} to create label "Add Date (YMD): " & (item 2 of dateData) bottom 0 max width dialogWidth control size regular size
	set {eraLabel, theTop} to create label "Start Era: " & (item 3 of dateData) bottom (theTop + verticalSpace) max width dialogWidth control size regular size
	set {startDateLabel, theTop} to create label "Start Date (YMD): " & (item 1 of dateData) bottom (theTop + verticalSpace) max width dialogWidth control size regular size
	set {dateStringLabel, theTop} to create label dateString bottom (theTop + verticalSpace) max width dialogWidth control size regular size aligns center aligned with bold type
	set allControls to {addDateLabel, eraLabel, startDateLabel, dateStringLabel}
	set {buttonName, controlsResults} to display enhanced window "Date Calculator" acc view width dialogWidth acc view height theTop acc view controls allControls buttons theButtons initial position {} without align cancel button
	
	if buttonName = "Copy to Clipboard" then set the clipboard to dateString
end displayDate

on getCurrentDate()
	set theDate to current application's NSDate's now()
	set dateFormatter to current application's NSDateFormatter's new()
	dateFormatter's setDateFormat:"yyyy/MM/dd"
	set dateString to (dateFormatter's stringFromDate:theDate)
end getCurrentDate

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()

It should be noted that the calculation of era 0 dates can return unexpected results, and this is explained by Apple in their “Date and Time Programming Guide” as follows:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtHist.html