how to check a date input

hi all experts,

I want the user to insert a date.

Now I have to check if it’s valid.
Even here at home if I switch from MacBook to iMac there can be some issues
(not only but also because of date format {yy/mm/dd, dd/mm/yyy, and so on})

I had a look at a lot of Applesripts and didn’t find a routine good enough that
is checking the input of valid year, month and day …

That’s also very difficult because o the different date formats that may occur.

So I came at the end to totally other solution,
that is saving me a plausibility check:

AppleScript calls the App “Calendar” or “iCal” and the should be returned by the app.

I couldn’t find a solution …

My question:

  • Is there such a solution?
  • If not, is there another way to check date input?
  • is there a similar way for checking time input?

Thanks in advance

Kalle_Ger. I don’t know an answer to your first question. As to your second question, it seems that you might want to perform two checks, the first being whether the user input is seen as a valid date. For example,

try
	set theDate to "2/22"
	set enteredDate to date theDate --> error "Invalid date and time date 2/22 of «script»." number -30720
on error
	display alert "The date entered is not recognized as a valid date"
end try


Then you should probably check to see if the user input is within a valid range. The following demonstrates how this can go wrong (you already know this):

set theDate to "6/11/22" -- correct format on my computer
date theDate --> date "Saturday, June 11, 2022 at 12:00:00 AM"

set theDate to "11/6/22" -- incorrect format on my computer
date theDate --> date "Sunday, November 6, 2022 at 12:00:00 AM"

The manner in which you check if the date is within a valid range depends on what you consider a valid range. For example, the following checks to see if the entered date is within the next 90 days:

set theDate to "6/11/22"
set enteredDate to date theDate
if enteredDate < (current date) or enteredDate > ((current date) + 90 * days) then
	display alert "The entered date must be within the next 90 days"
end if

As regards time, a similar check is possible, but you need to provide a bit more information as to what you want to check.

Thanks Peavine,

but sorry, the check (script #1) is not of great help.
It doesn’t check if a valid date is entered.

I had several tests and didn’t get with your script en error message:

here the results:

(a) --set theDate to “2/22” – Result: Error Message
(b) --set theDate to “1/3/22”. – Result: NO ERROR MESSAGE, first of March 2022
(c) --set theDate to “1/13/22” – Result: NO ERROR MESSAGE, 1st of January 2023
(d) --set theDate to “1/23/22” – Result: NO ERROR MESSAGE, 1st of November 2023
(e) --set theDate to “1/83/22” – Result: NO ERROR MESSAGE, 1st of November 2028
(f) --set theDate to “18/3/22” – Result: NO ERROR MESSAGE, 18th of March 2022
(g) --set theDate to “78/3/22” – Result: NO ERROR MESSAGE, 17th of May 2022

in examples d, e and g I want an error message of a number in a date that is not expect for such data type…

cheers

You could force the choice:

--prepare a list of leap years:
set leapYears to {}
repeat with eachleapyear from 1980 to 2080 by 4
	set end of leapYears to eachleapyear as text
end repeat

--prepare a range of years:
set allYears to {}
repeat with thisYear from 1980 to 2080
	set end of allYears to thisYear as text
end repeat

-- user chooses a year:
try
	set chosenYear to item 1 of (choose from list allYears with prompt "Choose a year:")
on error --dialog was cancelled, stop the script
	return
end try
set leapYearChosen to false
if chosenYear is in leapYears then set leapYearChosen to true

--prepare a list of months:
set allmonths to {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}

--define months with 30 or 31 days (omitting February):
set monthsof30 to {"April", "June", "September", "November"}
set monthsof31 to {"January", "March", "May", "July", "August", "October", "December"}

-- user chooses a month:
try
	set chosenMonth to item 1 of (choose from list allmonths with prompt "Choose a month:")
on error --dialog was cancelled, stop the script
	return
end try

--determine number of days in the chosen month:
set lastDay to 28 -- February by default
if chosenMonth is in monthsof31 then set lastDay to 31
if chosenMonth is in monthsof30 then set lastDay to 30

--allow for leap years:
if chosenMonth is "February" and leapYearChosen is true then set lastDay to 29

--prepare list of days for selected month and year:
set allDays to {}
repeat with x from 1 to lastDay
	set end of allDays to x as text
end repeat

--user chooses a day:
try
	set chosenDay to item 1 of (choose from list allDays with prompt "Choose a day:")
on error --dialog was cancelled, stop the script
	return
end try


--return date from user's choices
set chosenDate to date (chosenDay & " " & chosenMonth & " " & chosenYear)

This offers a choice of years from 1980 to 2080. Depending on the date range you expect, you would need to modify the logic to allow for the fact that 1900 and 2100 are (were) not leap years, while 2000 is (was).

Kalle_Ger. Thanks for the explanation and examples, which are helpful.

I tested hubert0’s script, and it seems a reasonable approach which hopefully will do the job.

As regards the examples in your post, I don’t know a foolproof method to do what you want and let me explain by using your example (g), which is “78/3/22”. This is a perfectly valid date if the user’s date preference is set to year/month/date. Thus, on my computer:

set theDate to "78/3/22"
set enteredDate to date theDate --> date "Wednesday, March 22, 1978 at 12:00:00 AM"

The script could check to see if the month and day are within an expected range (less than 32 for the day and less than 13 for the month), although that would not help with the date discussed above.

hubert0. I tested your script and selected 1980 for the year, January for the month, and 1 for the date, and the returned result was:

I tested other dates and the returned results were also not as expected. I think the unexpected result arises from the following line, which assumes a certain user date preference:

set chosenDate to date (chosenDay & " " & chosenMonth & " " & chosenYear)

If I change this line as follows the script works as expected on my computer (but not on computers with a different date preference):

set chosenDate to date (chosenYear & " " & chosenMonth & " " & chosenDay)

Thanks, Peavine. I think that’s down to different date and time preferences on our different systems. Mine are set to a standard UK date format and when I run my original script and choose 1980 - January - I get the correct date:

date "Tuesday, 1 January 1980 at 00:00:00"

(It was indeed a Tuesday: https://www.dayoftheweek.org/?m=January&d=1&y=1980&go=Go.)

If I modify my script as you suggest, I get…

date "Thursday, 3 June 6 at 00:00:00"

Clearly my script does not work universally. But it does at least illustrate the non-trivial nature of checking for valid dates :slight_smile:

Here a development of a script originally written as an exercise to return any valid short date strings (ie. valid to the current user) that may be present in the input text. Any good to you?

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

on validShortDatesForMachine(theText)
	-- Divine the short-date order set for the current user.
	tell (current date) to set {its day, its month, (its year), testDate} to {1, 2, 2003, it}
	set testString to testDate's short date string
	set dmy to "dmy"
	set shortDateOrder to {missing value, missing value, missing value}
	repeat with i from 1 to 3
		set item i of shortDateOrder to character ((testString's word i) mod 100) of dmy
	end repeat
	
	-- Use it to select an appropriate regex pattern.
	if (shortDateOrder is {"d", "m", "y"}) then
		-- Regex for valid (d)d/(m)m/(y)(y)(y)y short dates in years 1-9999 AD of the proleptic Gregorian calendar.
		set regexPattern to "(?<![^\\s(–-])(?:(?:(?:0?[1-9]|1\\d|2[0-8])/(?:0?[1-9]|1[0-2])|(?:29|30)/(?:0?[13-9]|1[0-2])|31/(?:0?[13578]|1[02]))/\\d{0,3}(?:[1-9]|(?<!/0{2,3})0)|29/0?2/\\d{0,2}(?:[13579][26]|[02468]?(?:[48]|(?<!(?:[13579][048]?|/[02468]?[26])0)0)))\\b(?!/)"
	else if (shortDateOrder is {"m", "d", "y"}) then
		-- Regex for valid (m)m/(d)d/(y)(y)(y)y short dates ditto.
		set regexPattern to "(?<![^\\s(–-])(?:(?:(?:0?[13-9]|1[0-2])/(?:0?[1-9]|[12]\\d|30)|0?2/(?:0?[1-9]|1\\d|2[0-8])|(?:0?[13578]|1[02])/31)/\\d{0,3}(?:[1-9]|(?<!/0{2,3})0)|0?2/29/\\d{0,2}(?:[13579][26]|[02468]?(?:[48]|(?<!(?:[13579][048]?|/[02468]?[26])0)0)))\\b(?!/)"
	else -- if (shortDateOrder is {"y", "m", "d"}) then
		-- Regex for valid (y)(y)(y)y/(m)m/(d)d short dates ditto.
		set regexPattern to "(?<![^\\s(–-])(?:\\d{0,3}(?:[1-9]|(?<!\\b0{2,3})0)/(?:(?:0?[13-9]|1[0-2])/(?:0?[1-9]|[12]\\d|30)|0?2/(?:0?[1-9]|1\\d|2[0-8])|(?:0?[13578]|1[02])/31)|\\d{0,2}(?:[13579][26]|[02468]?(?:[48]|(?<!(?:[13579][048]?|\\b[02468]?[26])0)0))/0?2/29)\\b(?!/)"
	end if
	set shortDateRegex to current application's class "NSRegularExpression"'s regularExpressionWithPattern:(regexPattern) options:(0) |error|:(missing value)
	
	-- Locate any and all substrings of the text which match the pattern (are valid short-date strings for the user).
	set theText to current application's class "NSString"'s stringWithString:theText
	set shortDateMatches to shortDateRegex's matchesInString:(theText) options:(0) range:({0, theText's |length|()})
	
	-- Extract them from the text as an array of NSStrings.
	set dateStrings to current application's class "NSMutableArray"'s new()
	repeat with thisMatch in shortDateMatches
		tell dateStrings to addObject:(shortDateRegex's replacementStringForResult:(thisMatch) inString:(theText) |offset|:(0) template:("$0"))
	end repeat
	
	-- Return as a list of AS texts.
	return dateStrings as list
end validShortDatesForMachine

-- Original test text. All valid substrings returned as a list.
-- set testText to "4079/31/12/2018/X, 31/12/2018, 1/1/19, 31/11/18 (only 30 days in November), 29/02/2200 (2200 not a leap year), (29/02/1952-29/2/9996), 4/3/1, 17/4/541-01/01/00, 25/12/0000 (no year 0)."

-- Single short-date string parameter.
set testText to "1/3/22"
set testResult to validShortDatesForMachine(testText)
if (testResult = {}) then error testText & " is not a valid date on this machine."
return date string of date testText -- or date string of date (item 1 of testResult)

hubert0. I wonder if your script could be made universal by adding the following midpoint in the script:

repeat with i from 1 to (count allmonths) -- convert month to number
	if item i of allmonths = chosenMonth then
		set chosenMonthInteger to i as text
		exit repeat
	end if
end repeat

Then replace the last line of the script with:

set chosenDate to current date -- create date object
set year of chosenDate to chosenYear
set month of chosenDate to chosenMonthInteger
set day of chosenDate to chosenDay
return chosenDate

There’s probably a more elegant way of doing this, but In limited testing it does appear to work.

Peavine, thanks.

Your script works well on my machine, although it appends the time the script is run, rather than 00:00:00, to the returned date.

One could change this if required:

set chosenDate to current date -- create date object
set year of chosenDate to chosenYear
set month of chosenDate to chosenMonthInteger
set day of chosenDate to chosenDay
set time of chosenDate to 0
return chosenDate

returns

The tried & tested method for setting date properties is to use an actual date object. This will automagically conform to the user’s settings:

set d to current date

set chosenYear to 1954
set chosenMonth to 5 -- or ENGLISH AppleScript constant 'May'
set chosenDay to 13

tell d
	set its day to 1 -- overflow precaution
	set its year to chosenYear
	set its month to chosenMonth
	set its day to chosenDay
end tell

But, imho, the real problem (which hasn’t been discussed at all) is the way Kalle_Ger gets the data from the user. Users are notoriously unreliable…

Indeed, @alastor933. Hence my original approach, much improved by @Peavine and yourself, which will only allow the user to supply a valid date.

Whether or not it’s the correct date is another matter…

In post 1 Kalle_Ger appears to suggest two possible alternatives. One is to check the user input to insure it contains a valid year, month, and day. As Hubert0’s notes, his script accomplishes this by only allowing the user to select valid dates. I tested Nigel’s suggestion and it doesn’t appear to accomplish what Kalle_Ger wants. For example, I have my computer’s date format set to m/d/y, and with testText set to “78/3/1”, Nigels script returns “Friday, June 3, 7”. However, Nigel’s script could be modified to insure that the user input date falls within a valid range (a month of 78 would obviously fail).

As an alternative Kalle_Ger suggests calling the Calendar app to obtain the desired date information, which would avoid what he/she terms a “plausibility check”. This alternative hasn’t been discussed, although in a broad sense this is what hubert0’s script does (i.e. avoid the plausibility check). Apparently Shane wrote a date-picker script which works with the Calendar app, but I haven’t found a copy of Shane’s actual script or attempted to write one myself.

https://macscripter.net/viewtopic.php?id=36531

Ah. My apologies. [Blush.] The first two sections of the if … else if statement were both testing for d/m/y. The second should be for m/d/y. I’ve now corrected it.

I tested Nigel’s script against dates (b) thru (g) shown in post 3 and the results were all as expected. The test results demonstrate the impact of the user’s date System Preference on the results:

System Preference date set to m/d/y - e, f, g not valid

System Preference date set to d/m/y - c, d, e, g not valid

System Preference date set to y/m/d - c, d, e not valid

BTW, the script can return an incorrect result if the user’s date System Preference is not supported (e.g. d/y/m). In this unlikely event, the if statement can be modified to support that date format or, more easily, to report an error.

So, Kalle_Ger has two great options to select from.