Sunday, July 3, 2022

#1 2022-05-25 06:41:46 pm

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

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.

Applescript:

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.

Last edited by peavine (2022-05-25 06:45:37 pm)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#2 2022-05-25 07:49:52 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6779

Re: NSDate date calculations

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.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#3 2022-05-26 06:25:57 am

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

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:

Applescript:

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:

Applescript:

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

Last edited by peavine (2022-05-26 07:49:06 am)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#4 2022-05-26 06:45:42 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6779

Re: NSDate date calculations

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


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#5 2022-05-26 09:35:20 am

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

Shane Stanley wrote:

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



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

Applescript:

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}

Last edited by peavine (2022-05-27 09:19:13 am)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#6 2022-05-28 10:27:52 am

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

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.

Applescript:

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)

Last edited by peavine (2022-05-29 03:51:12 pm)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#7 2022-05-30 08:25:46 pm

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

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.

Applescript:

-- 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.

Applescript:

-- 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/documentati … guage=objc

The following is Apple documentation on historical dates:

https://developer.apple.com/library/arc … tHist.html

Last edited by peavine (2022-06-05 06:43:03 am)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#8 2022-05-31 09:55:51 am

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

peavine wrote:

The second script returns a future date as a string based on user-specified values. The returned result is not what I expected, and I need to do some research on this.



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:

Applescript:

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:

Applescript:

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.


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#9 2022-05-31 11:02:04 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5530

Re: NSDate date calculations

peavine wrote:

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

Applescript:

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"


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.


NG

Offline

 

#10 2022-05-31 02:23:01 pm

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

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/tr3 … t_Patterns

Last edited by peavine (2022-05-31 06:17:09 pm)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

#11 2022-06-08 10:09:16 am

peavine
Member
From:: Prescott, Arizona
Registered: 2018-09-04
Posts: 1370

Re: NSDate date calculations

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/

Applescript:

-- revised 2022.06.14

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 376 -- do not reduce
   set verticalSpace to 10 -- change as desired
   
   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 104 max width (dialogWidth div 2) label text "Date 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 104 max width (dialogWidth div 2) label text "Date Format:" popup left 270 initial choice "Long"
   set {addDateField, addDateLabel, theTop, fieldLeft} to create side labeled field "Y/M/D" placeholder text "Y/M/D" left inset 0 bottom (theTop + verticalSpace) total width dialogWidth label text "Add to Date:" field left 0
   set {startDateField, startDateLabel, theTop, fieldLeft} to create side labeled field "Y/M/D" placeholder text "Current Date" left inset 0 bottom (theTop + verticalSpace) total width dialogWidth label text "Start Date:" field left 0
   set {messageLabel, theTop} to create label "The current date is used if the \"Start Date\" field is empty. Days are used if the \"Add to Date\" field contains a single number." bottom theTop + verticalSpace max width dialogWidth control size small size
   set {boldLabel, theTop} to create label "Enter a start date and the date components to add" bottom theTop + verticalSpace max width dialogWidth control size regular size with bold type
   set allControls to {eraPopup, eraLabel, formatPopup, formatLabel, startDateField, startDateLabel, addDateField, addDateLabel, messageLabel, boldLabel}
   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 did not contain a required date component or contained an unrecognized character")
   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 Date 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:

In the Gregorian calendar, time is divided into two eras, the BCE era and the CE era. In the BCE era, time flows in a direction seemingly backwards, that is from higher year numbers to lower. However, days and months flow in the normal direction. For example February 1 follows January 31. This can be confusing if you ask what day follows December 31, 7 BCE. The correct answer is January 1, 6 BCE.


https://developer.apple.com/library/arc … tHist.html

Last edited by peavine (2022-06-14 07:03:47 am)


2018 Mac mini - macOS Monterey - Script Debugger 8

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)