Calculating Dates From an Input Date and Offsets

Continuing the discussion from Date math with Keyboard Maestro:

Thanks to @ComplexPoint for the shell script using date command in the above post.
I just reviewed this subject, this need, and I don't see a succinct way of doing it using all native KM ICU tokens/functions, starting with a BaseDate in this format: YYYY-MM-DD, that is NOT the current date.

But maybe I've missed something. @peternlewis, or anyone, how would this be done in KM?

I've read the KM Wiki Dates and Times and several other articles and forum posts. Didn't find an example or solution anywhere.

Based on the script by @ComplexPoint, I built this script:

image

Bash Script

# Enter KM Variables (or text) for these three Bash Variables
# Both input and output date format is YYYY-MM-DD

read myBaseDate myAdjust myAdjustUnits

#              Adjustment           Input Format            Output Format
date -j -v$myAdjust$myAdjustUnits -f %Y-%m-%d "$myBaseDate" '+%Y-%m-%d'

These two KM Variables would obviously need to be set in the KM Macro prior to this:
%Variable%Local__BaseDate% %Variable%Local__DayDelta1%

I'm trying to make it as reusable as possible, and by putting the KM Variables outside the script but at the top of the Script Action really helps.

I am a very novice shell coder, so I'd really appreciate a good review/revision of this script.
Is there a better way to read stdin?

To make it completely reusable, the format for the input date and output date needs to be an input to the script. How could this be done, but use the current format as a default?

All suggestions welcome!

2 Likes

That's a fine way to do it. In fact, it's the most sensible way I can think of.

As always, just to provide an alternative way of doing things (but, by no means better, and, to some, arguably worse), here's the AppleScript equivalent:

tell application "Keyboard Maestro Engine" to set ¬
	[baseDate, |𝚫d|] to [¬
	getvariable "Local__BaseDate", ¬
	getvariable "Local__DayDelta1"]

set [y, m, d] to baseDate's words -- "yyyy-mm-dd"

tell the (current date) to set ¬
	[ASbaseDate, year, its month, day, time] to ¬
	[it, y, m, d, 0 * hours + 0 * minutes + 0]

(ASbaseDate + |𝚫d| * days) as «class isot» as string
result's text 1 thru 10

Getting the user to declare the format and then declare the date could seem a bit cumbersome and a bit-too-much-like-hard-work. Often, users are content when the choice is made for them, which they then have to subscribe to by declaring just the date in the format as stated.

However, what would be really neat is if you allowed inputting of a date in any (sensible) format, which is then parsed and decomposed into its components.

...so I went ahead and did that:

--------------------------------------------------------------------------------
property ordinals : {"st", "nd", "rd", "th"}
property text item delimiters : {space} & ordinals
property daysOfTheWeek : {¬
	"Sunday", "Monday", "Tuesday", "Wednesday", ¬
	"Thursday", "Friday", "Saturday"} as text
property monthsOfTheYear : {¬
	"January", "February", "March", "April", ¬
	"May", "June", "July", "August", "September", ¬
	"October", "November", "December"} as text
--------------------------------------------------------------------------------
global ASbaseDate
set ASbaseDate to {}
--------------------------------------------------------------------------------
set input to ["Monday, June 04 2018", "4 June 2018", "4 Jun 2018", "4 Jun 18", ¬
	"4/6/18", "6/4/18", "Sunday, 4 June 2018", "4th June 2018", "2018-06-04", ¬
	"Jun 4, 2018", "March 15 2018", "15 March 2018", "15 Mar 2018", ¬
	"15 Mar 18", "15/3/18", "3/15/18", "Sunday, 15 March 2018", ¬
	"15th March 2018", "2018-03-15", "Mar 15, 2018"]

parse(some item of the input)
ASbaseDate
--------------------------------------------------------------------------------
###HANDLERS
#
#
to parse(input as text)
	local input
	
	if the input is "" then
		if ASbaseDate's middle item > 12 then swap(ASbaseDate, 1, 2)
		set [d, m, y] to ASbaseDate
		
		tell (current date) to set ¬
			[ASbaseDate, year, its month, day, time] to ¬
			[it, y, m, d, 0 * hours + 0 * minutes + 0]
		
		return
	end if
	
	script ds
		property x0 : first text item of input's first word
		property xN : rest of input's words as text
		property |?1| : x0 is not in daysOfTheWeek & monthsOfTheYear
		
		to getMonth(m)
			script mw
				property m0 : m's first item
				property mN : rest of m
				property |?2| : x0 is in m0
			end script
			
			tell mw
				if its |?2| is true then return 13 - (m's length)
				getMonth(its mN)
			end tell
		end getMonth
	end script
	
	tell ds
		if its |?1| is true then -- numerals
			tell (its x0 as integer)
				if ds's xN = "" and my sum(ASbaseDate) < 1000 then
					if it < 1000 then
						set end of ASbaseDate to it + 2000
					else
						set end of ASbaseDate to it
					end if
				else
					set beginning of ASbaseDate to it
				end if
			end tell
		else -- words
			if its x0 is in monthsOfTheYear then
				its getMonth(monthsOfTheYear's words)
				set end of ASbaseDate to the result
			end if
		end if
		
		parse(its xN)
	end tell
end parse

to swap(L as list, i as integer, j as integer)
	local L, i, j
	
	set x to item i of L
	set item i of L to item j of L
	set item j of L to x
end swap

to sum(L as list)
	local L
	
	if L = {} then return 0
	
	script Array
		property x0 : L's first item
		property xN : rest of L
		property fn : sum(xN)
	end script
	
	tell the Array to return (its x0) + (its fn)
end sum
---------------------------------------------------------------------------❮END❯

I've only done limited case testing, using the input values I've included in the script. It's worth noting that, whilst the dates written as 15/3/18 and 3/15/18 are unequivocally both representations of the Ides of March, the dates written as 4/6/18 and 6/4/18 are ambiguous. Currently, the script defaults to the US-standard, returning dates 6 April 2018 and 4 June 2018 respectively. But, ideally, it's best to advise the user to avoid this format as input, and elect for one of the other myriad date representations.

Thanks for the confirmation.

I generally prefer AppleScript over Shell Script, but in this case I like the Shell Script because:

  1. Does not embed the KM Variables in the script
  2. Much more concise at 2 lines
  3. Is easier to modify to change date formats

I'm not sure I follow you here. The Shell Script has a default date format of the ISO 8601 standard (international), so should be acceptable to most. If the macro builder wants to use a different format, that's easy enough to change in the script. But there are lots of utilities, scripts, etc that will convert dates to ISO.

My thought is to add two lines to the SS to set the input and output date formats.

While it might be neat, it is out of scope of this task, date calculation. And, as you have seen with your AppleScript, it takes a lot of coding. I want to keep this simple.

There are many solutions at there for recognizing and converting dates. We could just provide a reference to those, including yours.

Thanks for your feedback, and your enthusiasm about this.

Well, after a bit of research, and a lot of testing, and some help from my friends, I think I have a good solution, just posted:

Macro: Date Calculator [Reusable Action]

The above macro has a come complex example, but to demonstrate how to use default values with stdin, here's a simple example:

image

read x y z
z=${z:-DefaultValue}
echo $z


Result of above is "DefaultValue"