Taking a Close Look at the HowLong() Function for Calculating Years, Months, and Days
In this blog, I discuss in its entirety the most recent AutoHotkey code for the HowLongYearsMonthsDays.ahk script (introduced in my last blog). I’ve broken it up into snippets in order to explain the purpose of each piece. To get a complete copy of the script check out HowLongYearsMonthsDays.ahk at the “ComputorEdge Free AutoHotkey Scripts” page or for a barebones version (without comments and inactive code) see “Function Calculating Timespan in Years, Months, and Days” at the AutoHotkey Forum. This blog reviews the nuts and bolts of calculating the timespan between two dates.
The DateTime GUI Control
The script first sets up a GUI pop-up window with two DateTime controls—one for the start date and the other for the stop date. After the user selects the dates, AutoHotkey feeds them into the HowLong(StartDate,StopDate) function which returns the number of Years, Months, and Days of the timespan. The following code creates and opens the Calculate How Long GUI window:
Gui, SpanCalc:Add, Text, , Enter Start Date: Gui, SpanCalc:Add, DateTime, vDate1, LongDate Gui, SpanCalc:Add, Text, , Enter Stop Date: Gui, SpanCalc:Add, DateTime, vDate2, LongDate Gui, SpanCalc:Add, Button, , Calculate Gui, SpanCalc:Show, , Calculate How Long Return
Note: The HowLongYearsMonthsDays.ahk script adds a mouse wheel scroll feature to the DateTime control. Normally, these settings only respond either to directly editing the numbers or pressing the up/down arrows to increment the values. To facilitate changing all values, I added mouse scroll wheel action using the #If directive to call the up/down arrow keys. (Code found at the end of the script.)
Calling the HowLong() Function
Clicking the Calculate button calls the built-in Label name SpanCalcButtonCalculate (composed of the GUI name, i.e. SpanCalc, the control type, i.e. Button, plus the button name, i.e. Calculate) subroutine:
SpanCalcButtonCalculate: Gui, Submit, NoHide HowLong(Date1,Date2) MsgBox, , TimeSpan Calculation , Years %Years%`rMonths %Months%`rDays %Days% Return
After executing the Gui, Submit command to save the current input to the vVariables (Date1 and Date2), AutoHotkey sends those values to the HowLong(Date1,Date2) function as parameters. The HowLong() function calculates the global variables Years, Months, and Days. The function takes the form:
HowLong(FromDay,ToDay) { Global Years,Months,Days ⇑ [Remainder of the code] ⇓ }
Variables inside a function default to Local status. That means they only affect code inside the function—invisible outside the function. To make the variables universally available in the script as well as the function, you must either declare the variables (Years, Months, Days) Global within the function or in the main script—making the variables available to the entire script and all functions.
Conforming the Function Parameters
If the DateTime stamps don’t match in length, they can return inconsistent results when compared to each other. To prevent these types of errors, trim the time components off the input variables:
FromDay := SubStr(FromDay,1,8) ToDay := SubStr(ToDay,1,8)
Check for Proper Date Order
You’ll find it easy to accidentally input a start date later than the stop date. Calculating in that direction (HowLongAgo()?) offers a different problem. To trap this error, we add the following code:
If (ToDay <= FromDay) { Years := 0, Months := 0, Days := 0 MsgBox, Start date after end date! Return }
Whenever the target date is less than or equal to the beginning date, AutoHotkey sets all values (Years, Months, Days) to 0, then displays the error message, “Start date after end date!” and exits the function with the Return command.
Determining Leap Years
Initially, I used the following routine to detect leap years by evaluating the existence of February 29th in the target year:
Feb29 := SubStr(ToDay,1,4) . "0229" Feb29 += 1, days ; Test for Leap Year If (Feb29 != "") Feb := 29 Else Feb := 28
An invalid date calculation for any year not containing a February 29th (Feb29 += 1, days) returns blank. If not blank, it’s a leap year.
However, I switched to another calculating technique for determining the length of any given month (posted on the AutoHotkey Forum):
Date1 := 20200201 Date2 := 20200301 Date2 -= Date1, Days ; Uses the EnvSub command MsgBox, % Date2 ; Returns 29
Now, since AutoHotkey directly calculates the length of the previous month in the target year, the function no longer requires the original leap year test. I’ve left the original code (which I commented-out and does not run) intact in the script for people who may want an alternative approach to identifying a leap year.
Calculating Years
Possibly the simplest calculations to grasp in this function, when the target month occurs after the start month in any given year, merely subtract the start year from the stop year. Otherwise, subtract one from that difference to account for the partial year. I used the ternary operator to code this conditional calculation in one line:
Years := % SubStr(ToDay,5,4) - SubStr(FromDay,5,4) < 0 ? SubStr(ToDay,1,4)-SubStr(FromDay,1,4)-1 : SubStr(ToDay,1,4)-SubStr(FromDay,1,4)
(I word-wrapped this one line of code, and many of the other long code lines, using AutoHotkey line continuation techniques for display purposes in this blog. AutoHotkey reads each wrapped line as one continuous line.)
Remove years from the calculation by increasing the start date by the number of whole years found in the difference. This sets the Years value and eliminates it from further calculations:
FromYears := Substr(FromDay,1,4)+years . SubStr(FromDay,5,4)
Notice that DateTime timestamps require string concatenation to properly form the variable.
Calculating Months
Calculate the number of months between the new start date value (FromDay with whole years removed. i.e. FromYears) and the stop date (ToDay).
If the month number in the adjusted start date is less than the month number in the stop date, then either use the difference in the month numbers or use that number minus 1—depending upon the comparison between the two month days. If the start day of the month is less than the stop day of the month use the same month. Otherwise, use the previous month.
If the month number in the adjusted start date is greater than the month number in the stop date, then either add 11 (one month prior to the stop month) or 12 months to the calculation—depending upon the comparison between the two month days. If the start day of the month is less than the stop day of the month use the same month. Otherwise, use the previous month:
If (Substr(FromYears,5,2) <= Substr(ToDay,5,2)) and (Substr(FromYears,7,2) <= Substr(ToDay,7,2)) Months := Substr(ToDay,5,2) - Substr(FromYears,5,2) Else If (Substr(FromYears,5,2) < Substr(ToDay,5,2)) and (Substr(FromYears,7,2) > Substr(ToDay,7,2)) Months := Substr(ToDay,5,2) - Substr(FromYears,5,2) - 1 Else If (Substr(FromYears,5,2) > Substr(ToDay,5,2)) and (Substr(FromYears,7,2) <= Substr(ToDay,7,2)) Months := Substr(ToDay,5,2) - Substr(FromYears,5,2) +12 Else If (Substr(FromYears,5,2) >= Substr(ToDay,5,2)) and (Substr(FromYears,7,2) > Substr(ToDay,7,2)) Months := Substr(ToDay,5,2) - Substr(FromYears,5,2) +11
You may need to think about this one. You might want to take out a calendar and test a number of variations to digest what these conditionals do.
Ultimately, we remove the months from the calculation (FromMonth) by adjusting the month forward to either the stop month or one month prior to the stop month:
If (Substr(FromYears,7,2) <= Substr(ToDay,7,2)) FromMonth := Substr(ToDay,1,4) . SubStr(ToDay,5,2) . Substr(FromDay,7,2) Else If Substr(ToDay,5,2) = "01" FromMonth := Substr(ToDay,1,4)-1 . "12" . Substr(FromDay,7,2) Else FromMonth := Substr(ToDay,1,4) . Format("{:02}", SubStr(ToDay,5,2)-1) . Substr(FromDay,7,2)
We must account for the transition between January (01) back to December (12) (shown in green) when the start month day number is greater than the stop month day number.
One formatting problem occurs in the recreation of the DateTime stamp when the calculation returns a single digit for the earlier months. For the function to work, we must correct the date variable by padding those single digit months with a “0” as the first digit (i.e. 01, 02, 03, …). Initially, I used the following trick to change the format:
Substr("0" . SubStr(ToDay,5,2)-1,-1)
However, I changed it to the Format() function in the latest version (as shown above in red). Both approaches produce valid DateTime stamps, but the Format() function offers consistency without any sleight of hand.
Note: The Format() function deserves a little more attention to develop a proper understanding of how it works and its practical application. Definitely a topic worth pursuing in the future.
Months with Less Than 31 Days
If AutoHotkey attempts a date calculation with a nonexistent day of the month (e.g. September 31), it returns a null value. That means we must adjust any FromMonth containing a non-existent date to the appropriate lower month length.
Originally, I included a set of conditionals which adjusted all the short months (including February in a leap year after the test mentioned toward the beginning of this blog):
If (Substr(FromMonth,5,2) = "02") and (Substr(FromDay,7,2) > "28") FromMonth := Substr(FromMonth,1,6) . Feb If (Substr(FromMonth,5,2) = "04") and (Substr(FromDay,7,2) > "30") FromMonth := Substr(FromMonth,1,6) . "30" If (Substr(FromMonth,5,2) = "06") and (Substr(FromDay,7,2) > "30") FromMonth := Substr(FromMonth,1,6) . "30" If (Substr(FromMonth,5,2) = "09") and (Substr(FromDay,7,2) > "30") FromMonth := Substr(FromMonth,1,6) . "30" If (Substr(FromMonth,5,2) = "11") and (Substr(FromDay,7,2) > "30") FromMonth := Substr(FromMonth,1,6) . "30"
This worked fine but after I noted that I could easily calculate the length of the previous month, I replaced the month length adjustment conditionals above with the following:
Date1 := Substr(FromMonth,1,6) . "01" Date2 := Substr(ToDay,1,6) . "01" Date2 -= Date1, Days If (Date2 < Substr(FromDay,7,2)) and (Date2 != 0) FromMonth := Substr(FromMonth,1,6) . Date2
Since in any leap year the snippet above calculates February as 29 days, this code also eliminates the need for the leap year test found early in this function.
Calculate Remaining Days
We calculate the remaining days using the EnvSub command. Peculiarly, with time calculations, this operation (EnvSub) alters the type of the original variable from a DateTime stamp into numeric time units (days, hours, minutes, or seconds). If we need to keep the original variable type and value unchanged, then we must create a new variable set to its original value. However, after this final day-calculating step, I no longer need the ToDay variable in its original form. The function has completed all calculations:
ToDay -= %FromMonth% , d Days := ToDay
If you prefer a cleaner form of the routine, set Days to ToDay before the calculation:
Days := ToDay Days -= %FromMonth% , d ; Returns the number of days
AutoHotkey Version 2.0 Note: The coming V2.0 resolves this EnvSub/EnvAdd variable altering oddity by replacing time calculations with the DateDiff() and DateAdd() functions—both of which work in a standard manner:
Result := DateDiff(DateTime1, DateTime2, TimeUnits)
DateDiff() returns time units while DateAdd() returns a DateTime stamp without altering any other variables.
That completes the HowLong(StartDate,StopDate) function. The global variables Years, Months, and Days contain the timespan from the start date to the stop date.
Enable the Mouse Wheel for Scrolling Incremental Controls in AutoHotkey GUIs
When I first set up the DateTime GUI control, I was annoyed at needing to use the up/down cursor keys to increment the control values. By default, the mouse scroll wheel does not work.
Noting that I control the volume level of my speakers by hovering the mouse cursor over the Windows Taskbar while scrolling the mouse wheel (ChangeVolume.ahk), I added a variation of that capability to this script and it works like a charm:
#If MouseIsOver("ahk_class AutoHotkeyGUI") WheelUp::Send {Up} WheelDown::Send {Down} #If MouseIsOver(WinTitle) { MouseGetPos,,, Win Return WinExist(WinTitle . " ahk_id " . Win) }
I discuss how the ChangeVolume.ahk script works in Chapter Six of the book AutoHotkey Hotkey Techniques.
This post was proofread by Grammarly
(Any other mistakes are all mine.)
(Full disclosure: If you sign up for a free Grammarly account, I get 20¢. I use the spelling/grammar checking service all the time, but, then again, I write a lot more than most people. I recommend Grammarly because it works and it’s free.)