Essential AppleScript Calendar Handlers (v.2)

14 Jan 2012 - 13:23

This set of AppleScript handlers provides tools for Holiday calculations and calendar conversions. Supported are the calendars Gregorian, Julian (both including Easter), Hebrew / Jewish and the ISO week calendar. A few general tools are found at the beginning.

Zur deutschen Version

© Jürgen Schell 2009 – 2012
http://www.j-schell.de

Documentation and Code provided unter Lesser GNU Public License.
http://www.gnu.org/licenses/lgpl.txt

No guarantee for proper behaviour of these functions is given whatsoever, regard all code as training implementations.

Essential calendar functions might be useful for some people.
Some of these functions are trivial, others are less so.
Note that almost all functions indicate deficiencies of the AppleScript
date implementation. Mac OS implements almost everything (plus a lot of useful other things), but alas, the AppleScript developers do not think that they should provide you with access to those features.

Several functions here have been inspired by the book "Calandrical Calculations" by Nachum Dershowitz and Edward M. Reingold, Cambridge 2008. Particularly the Easter algorithms are very close. Some parts use ideas from the source of the PHP calender extension. Still, this is a different implementation.

The AppleScript code as HTML is on this code page.

Jürgen Schell, Langenfeld, Germany, May 2009 - 2012

Some General Notes on the Functions

If an argument is an integer, the code does a type cast. If you provide a floating point value, the value will be rounded to an integer.

Arguments of the type date are checked for their class.

If an argument is of the wrong class or out of range, an error 1700 will be raised. The error message is more detailed. Actually, all errors I raise are 1700. If you need other numbers, search for the number and change it.

Where a function name has the word "date" in it, the function requires an AppleScript date object as argument or delivers one, as appropriate.

Where a function name has the word "cdn" in it, the function requires a chronological Julian day number as argument or delivers one, as appropriate. Both are just two different concepts to represent a day in some general way. A note on chronological Julian day numbers is at the end.

When a function needs to create an AppleScript date object, it uses “current date” and changes the properties. The reason is simple: Only this way, the functions survive copy / paste as text. In a compiled script, an expression like “date "Saturday, 1 January 2000 00:00:00"” is perfectly stable, but it may break, if the code is copied as text and inserted into some script and the text does not fit the system settings for date formats.

There are some strange thing about date display in AppleScript for dates sufficiently far in the past. Check note “AppleScript Dates in the Past” at the end.

I tried to keep handlers as self contained as possible. That is: Simple code that is needed in several handlers is rather repeated than turned into a separate handler. Only if a complex code is used several times, I use a separate handler. That should make it simpler to copy / paste the handlers needed.

Functions for the Hebrew calendar count months from Nisan as 1. This is consistent with most calendar calculations I have seen, but different from Mac OS or PHP. Both count from Tishri as 1.

Changes in this Version

In this second instance of the calendar function. This is an overview of changes compared to the first version:

  • Functions to convert from and to the ISO week calendar have been added, including the corresponding “leap year” and date exists functions.
  • When a numerical argument for a day is required, it will accept any integer now (including 0 or negatives).
  • For the Gregorian and Julian calendar, the same has been done for months.
  • To compensate for cases where the two previous changes are too liberal, “day exists” functions have been added for all four calendars, checking, if a combination of year, month (or week) and day is a valid date.
  • Some minor changes to code that makes some functions faster. Speed improvement is particularly strong for the function “rosh_hashanah_cdn”.

(The documentation for the first version has been archived.)

List of all Handler Names

  • newDate( Year, Month, Day ) create a date object
  • newDateTime(Year, Month, Day, Hour, Minute, Second) create a date object
  • DateToISOdayofweek ( date object ) Find ISO weekday number
  • DateToISOweekofyear ( date object ) Find ISO week number
  • preceeding_day( day number, date object or cdn ) Find certain weekday preceding a date
  • date_to_cdn ( date object ) CDN of a date object
  • greg_to_cdn( Year, Month, Day ) Gregorian date to CDN
  • cdn_to_date( cdn ) CDN to date object
  • cdn_to_greg( cdn ) CDN to Gregorian date
  • julian_to_cdn( Year_ad, Month, Day ) Julian date to CDN
  • cdn_to_julian( cdn ) CDN to Julian date
  • easter_greg_date ( Year_ad ) Gregorian Easter in year as date object
  • easter_greg_cdn( Year_ad ) Gregorian Easter in year as CDN
  • easter_jul_date ( Year_ad ) Julian Easter in year as date object
  • easter_jul_cdn( Year_ad ) Julian Easter in year as CDN
  • rosh_hashanah_cdn( Year_am ) Jewish New Year of year anno mundi as CDN
  • heb_to_cdn( Year_am, Month, Day ) Hebrew date to CDN
  • cdn_to_heb( day number ) CDN to Hebrew date
  • cdn_to_iso_week_cal( cdn ) CDN to ISO week calendar date
  • iso_week_cal_to_cdn( year, week, day ) ISO week calendar date to CDN
  • iso_week_cal_to_date(year, week, day) ISO week calendar date to date object
  • date_to_iso_week_cal ( date object ) date object to ISO week calendar date
  • is_leap_greg ( Year ) Is year a leap year in Gregorian calendar?
  • is_leap_jul ( Year ) Is year a leap year in Julian calendar?
  • is_leap_heb ( Year ) Is year a leap year in Hebrew calendar?
  • is_long_iso_week_year( year ) Has year 53 weeks in ISO week calendar?
  • day_exists_greg( Year, Month, Day ) Does that date exist in the Gregorian calendar?
  • day_exists_jul( Year, Month, Day ) Does that date exist in the Julian calendar?
  • on day_exists_heb( Year_am, Month, Day ) Does that date exist in the Hebrew calendar?
  • day_exists_iso_week_cal( Year, Week, Day ) Does that date exist in the ISO week calendar?

List of Handler Descriptions

newDate( Year, Month, Day )

Result: AS date object
Purpose: Locale independent creation of date from variables

Year, Month and Day are integers, floats will be rounded.
Valid ranges:
1 <= Year

An error will occur if an argument is out of range.

Creates a date by setting the properties. The time is set to midnight. Months and days may be any integer.

Allowing any integer for month and day has some nice effects. E.g. to find the last day of month M in year Y, use newDate( Y, M + 1, 0 ).

newDateTime(Year, Month, Day, Hour, Minute, Second)

Result: AS date object
Purpose: Locale independent creation of date/time from variables

Year, Month and Day, Hour, Minute, Second are integers, floats will be rounded.
Valid ranges:
1 <= Year

An error will occur if an argument is out of range.

Date behaviour for Month and Day is like in newDate. Time elements are added to that date. Hence, 48 hours add two days to the date.

DateToISOdayofweek ( date object )

Result: integer
Purpose: Day of week for countries starting day count of a week on Monday

Returns the number of a day in the week, according to the ISO way of counting, Monday = 1, ..., Sunday = 7.

DateToISOweekofyear ( date object )

Result: integer
Purpose: Week of year for countries using ISO counting of week numbers.

The function returns the number of the week the date belongs to, counted in the ISO fashion. (An ISO week has seven days always. January 1 of year XXXX may belong to the first week of XXXX, or to the last week of the preceeding year. The first week of the year is the first one, having 4 or more days in year XXXX.)

preceding_day( day number, date object or cdn )

Result: date object or integer, depending on second argument.
Purpose: Find date of a certain day of week, preceeding a given date. Useful for several special days like Advent Sunday or US Election Day.

The function returns the date of the given weekday preceding the given date. day number can be an integer from 1 to 7. ISO convention is used, i.e. Monday is 1, Sunday is 7. AppleScript day names (Sunday, Monday...) may be used instead.

date object or cdn is either an AppleScript date object or a chronological Julian day number. If a date object is used, the function returns a date object. If a number is used, the function returns a number.

date_to_cdn ( date object )

Result: Integer
Purpose: Get the chronological Julian day number of date

Returns the chronological Julian day number of the date.

greg_to_cdn( Year, Month, Day )

Result: integer
Purpose: Convert a date given as three values to a chronological Julian day number, exceeding the limits of the AppleScript date object.

Arguments are integers.

The function returns the chronological Julian day number for a date in the Gregorian calender, given as year, month and day.

Day and month values spill over, i.e. February 29 of a common year will yield the same result as March 1, March 0 returns the last of February in any year. Adding 30 to a month is two and a half years later.

Since it is more plausible for computations, we allow for a year 0. That is: 0 stands for year 1 BCE, -1 for year 2 BCE and so on. (For the Gregorian calendar, that notation is consistent with ISO.)

cdn_to_date( day_number )

Result: AS date object
Purpose: Convert a chronological Julian day number to a date

Argument is integer. If day_number is a float, it will be rounded.
Range:
day_number >= 1721426
(That is January 1 0001).

Returns a date object for the given chronological Julian day number.

cdn_to_greg( day_number )

Result: Record
Purpose: Converting a chronological Julian day number to a date record, exceeding the time limitation of AppleScript date objects

day_number is an integer.
If day number is a float, it will be rounded.

Returns a record with the elements year, month and day.

Year in the result record uses the ISO concept allowing for a year 0. That is: Year -99 in this numbering scheme is the same as Year 100 BCE in the conventional system.

julian_to_cdn( Year_ad, Month, Day )

Result: Integer
Purpose: Find the chronological Julian day number for a date in Julian calendar.

Arguments are integers

The function returns the chronological Julian day number for a date in the Julian calender, given as year anno domini, month and day.

Days and months may be any integer. E.g. Day 0 refers to the last day of the previous month. Adding 30 to a month is two and a half year later.

Since it is more plausible for computations, we allow for a year 0. That is: 0 stands for year 1 BCE, -1 for year 2 BC and so on. This is not standard for the Julian calendar, but convenient.

cdn_to_julian( day_number )

Result: Record {year, month, day}
Purpose: Get a date description in Julian calendar from chronological Julian day number

The argument is an integer.
Valid range:
day_number >= 0

day_number is the requested chronological julian day number. Negative arguments raise an error. The result is a record with the components year, month and day, an integer each.

Years BC are returned as 0 or negative values. Hence "0" means year 1 BC, "-1" means year 2 BC…

easter_greg_date ( Year_ad )

Result: AS date object
Purpose: Find gregorian Easter date for a Year

The argument Year is an integer

Valid range:
Year_ad >= 1
Lower values return an error, floating point numbers are rounded.

For a year, given as an integer, the function returns the date of easter in that year as an AppleScript date object. The code is heavily based on Dershowitz/Reingold.

easter_greg_cdn( Year_ad )

Result: Integer
Requires: greg_to_cdn
Purpose: Finding the Gregorian Easter date for Year as a Chronological Julian Day Number

Basically the same code as easter_greg_date, but the function does not return an AppleScript date object. Rather it works within the concept of Chronologial Julian day numbers.

Argument is an integer.
Floating point numbers are rounded.

easter_jul_date ( Year_ad )

Result: AS date object
Requires: julian_to_cdn, cdn_to_date
Purpose: Find the date of Julian (basically: Orthodox) Easter in Gregorian calendar.
Requires: julian_to_cdn, cdn_to_date

Argument is an integer. Floating point numbers will be rounded.
Valid range:
Year_ad >= 1

Returns an AppleScript date object for the orthodox Easter date of the given year anno domini. Properties of that date object belong to the Gregorian calendar, just as in any AS date object.

easter_jul_cdn( Year_ad )

Result: Integer
Requires: julian_to_cdn
Purpose: Finding the Julian Easter date for Year_ad as a Chronological Julian Day Number

Argument is an integer. Floating point numbers will be rounded.
Valid range:
Year_ad >= 1

Basically the same code as easter_jul_date, but the function does not return an AppleScript date object. Rather it works within the concept of Chronologial Julian day numbers.

rosh_hashanah_cdn( Year_am )

Result: Integer
Purpose: Find chronological Julian day number of Tishri 1 of year anno mundi.

The argument is an integer (year number in the Hebrew calendar).
Valid range:
Year_am >= 1

The function returns the chronological Julian day number of the day the year starts.

(Note: Year AM = Year AD + 3760 for days from January 1 to Elul 29. Year AM = Year AD + 3761 for days from Tishri 1 to Decembre 31.)

heb_to_cdn( Year_am, Month, Day )

Result: Integer
Requires: rosh_hashanah_cdn
Purpose: Convert a Hebrew date to a chronolocial Julian day number

all arguments are integers in the following valid ranges:

Year_am >= 1
1 <= Month <= 13
Arguments out of these ranges raise an exception.

Year_am is the anno mundi year of the Hebrew date. Month is the month counting from Nisan as 1. Day is the day number within the month.

Month and leap year behaviour: Month 12 is Adar in common years, Adar I in leap years. Month 13 is Adar in common years, Adar II in leap years.

Spill over behaviour of Day: Day value - 1 is simply added to the beginning of the month. Examples: “heb_to_cdn(5772, 9, 32)” will yield 2nd of Tevet, “heb_to_cdn(5773, 9, 32)” will yield 3rd of Tevet (both end of Hanukkah). Giving month as 10 and day as 0 will return the last day of Kislev.

cdn_to_heb( day number )

Result: Record of year, month and day
Requires: rosh_hashanah_cdn
Purpose: Convert a chronological Julian Day Number to a Hebrew date

Argument is integer.
Valid range:
day number >= 347998 (epoch of the Hebrew calendar)

Returns a record with the elements year, month and day, representing the date in the Hebrew calendar. For months, Nisan is counted as 1, Adar Sheni in leap years is 13.

cdn_to_iso_week_cal( cdn )

Result: Record of year, week, day and iso_string
Purpose: Convert a chronological Julian Day Number to a date in the ISO week calendar. iso_string is the ISO formated text for the date.

All arguments are integers.

Returns a record with the elements year, week and day, representing the day in the ISO week calendar.

iso_week_cal_to_cdn( year, week, day )

Result: Integer.
Purpose: Convert a date given in the ISO week calender to a chronological Julian Day Number.

Argument is integer.

Returns the chronological Julian Day number for the date given als an ISO week calendar date. Days and weeks spill over. Hence, day 0 of week 1 of a year will return the last day of the previous week year.

iso_week_cal_to_date(year, week, day)

Result: AppleScript date object
Purpose: Convert a date given in the ISO week calender to an AS date object.

All arguments are integer, day may be an AS day constant.

Returns an AppleScript date object for the day given as year, week and day in the ISO week calendar.
Both weeks and days may be any integer. The handler simply finds the start of the given year and adds weeks and days. Hence, specifying day 0 of week 1 will return the last day of the previous week year.

date_to_iso_week_cal ( date object )

Result: Record of year, week, day and iso_string.
Purpose: Convert an AS date object to a date in the ISO week calendar.

Argument is an AS date object

Returns a record with the elements year, week, day, representing the date in the ISO week calendar. iso_string is an ISO formatted string version of the date.

is_leap_greg ( Year )

Result: boolean
Purpose: Test, if a given year is a leap year according to Gregorian rules

The argument is an integer, giving the year. The result is true if that year is a leap year, otherwise it is false.

is_leap_jul ( Year )

Result: boolean
Purpose: Test, if a given year is a leap year according to Julian rules

The argument is an integer, giving the year. The result is true if that year is a leap year, otherwise it is false.

is_leap_heb ( Year )

Result: boolean
Purpose: Test, if a given year is a leap year according to Hebrew calender rules

The argument is an integer >= 1, giving the year anno mundi. (Floats will be rounded.) The result is true if that year is a leap year, otherwise it is false.

is_long_iso_week_year( year )

Result: boolean
Purpose: Test, if a given year has 53 weeks (“leap year”) in the ISO week calendar.

The argument is an integer, giving the year. The result is true if that year has 53 calendar weeks, according to ISO week counting. Otherwise it is false.

day_exists_greg( Year, Month, Day )

Result: boolean
Purpose: Test, if a given combination of values is a legal date in the Gregorian calendar.

Arguments are integers.

The handler checks, if Month is in the legal range and if day exists in that month. Hence, “day_exists_greg(2012, 2, 29)” returns true but “day_exists_greg(2013, 2, 29)” returns false.

day_exists_jul( Year, Month, Day )

Result: boolean
Purpose: Test, if a given combination of values is a legal date in the Julian calendar.

Arguments are integers.

The handler checks, if Month is in the legal range and if day exists in that month. Hence, “day_exists_jul(2100, 2, 29)” returns true but “day_exists_jul(2101, 2, 29)” returns false. (The only difference to “day_exists_greg” is in the leap year rule for centuries.)

on day_exists_heb( Year_am, Month, Day )

Result: boolean
Requires: rosh_hashanah_cdn
Purpose: Test, if a given combination of values is a legal date in the Hebrew calendar.

Arguments are integers.
Year >= 1

Checks, if the given date exists in the Hebrew calendar. 12 means Adar in common years, Adar I in leap years. 13 means Adar II. Examples: “day_exists_heb(5774, 12, 30)” returns true. Leap year, so month 12 has 30 days. “day_exists_heb(5775, 12, 30)” returns false. No leap year, Month 12 has only 29 days. “day_exists_heb(5777, 9, 30)” returns false, Kislev is short in that year.

day_exists_iso_week_cal( Year, Week, Day )

Result: boolean
Requires: is_long_iso_week_year
Purpose: Test, if a given combination of values is a legal date in the ISO week calendar.

Arguments are integers.

The handler checks, if Day and Week are in the legal range. If week is 53, it is checked if the year has 53 weeks.

_______________________________

Notes

Spill over Behaviour for Days and Months

Functions that take a year, month (or week in case of the ISO week calendar) and a day as arguments, day may be any integer. The semantics is: The beginning of the month (week) is calculated, and day - 1 is added. This allows for some nice tricks: The last day of any month can be specified by day 0 of the following month. This includes months with variable length like February. If for example some event spans 8 days and starts February 25, the end can be specified as February 32. If an invoice is created December 5th 2013 and should be payed within four weeks, that day could be specified as year 2013, month 12, day 5 + 4 * 7.

Months in the Gregorian calendar and Julian calendar behave the same. Months outside the range 1 to 12 are first converted to a year difference and a month within that range. The year is adjusted. Finally the beginning of that month is calculated and the days are added. A subscription starting May 15th 2013 and is for 18 months, the end is year 2013, month 5 + 18, day 15. Weeks in the ISO week calendar behave like days: They are simply calculated as seven days. Due to the special interpretation of month 12 and 13 in the Hebrew calendar, this logic is not used in that case.

Time values in the function newDateTime are first converted to seconds and added finally. Again, every integer value is possible.

(Built in spill over in AppleScript is different.

Months seem to behave OK up to 13. Less then 1 is not possible.

Days spill over as expected in the range 1 - 127. For higher values, things get strange. It seems that the lowest 8 bits if the integer are intepreted in a two's complement fashion: 256 behaves like 0 and yields the last day of the previous month, 255 is -1 and yields two days before... I am not aware of any technical documentation specifying this, so be very careful when using this "feature".

hour: Same two's complement behaviour as day. I.e. behaviour is reasonable in the range 0 to 127.

minute: Same as hour

second: Range is 0 to 32767)

Chronological Julian Day numbers

"Julian Dates" count time in days elapsed since noon January 1 4713 BC of the Julian Calendar (= November 24 4714 BCE). When used as a calender, these values are normalized to midnight, resulting in values having a ".5" decimal value. To simplify matters for calendrical calculations, the concept of a chronological day number has been added, using integer values.

These functions use Chronological Julian Day numbers as an intermediate calender. This allows conversion of dates between any two implemented calenders.

AppleScript Dates in the Past

The following tests have been performed under OS 10.7.2 “Lion”. The strange behaviour of time was introduced with Lion. The automatic switch to the Julian calendar before the introduction of the Gregorian calendar has been introduces with system 10.6 “Snow Leopard”. That latter change was intentional.

Some really strange thing happens with AppleScript dates on April 1st 1893 and before.

Look at the following code:

set x to date ("1-4-1893") -- British format
log x
log x as text
log minutes of x
log seconds of x
set time of x to 0
log x
log x as text
log minutes of x
log seconds of x

Running it will create the following log:

(*date Saturday, 1 April 1893 00:06:32*)
(*Saturday, 1 April 1893 00:06:32*)
(*6*)
(*32*)
(*date Friday, 31 March 1893 23:53:28*)
(*Friday, 31 March 1893 23:53:28*)
(*0*)
(*0*)

The resulting date is 6 minutes, 32 seconds later than midnight. You can set the time property to 0. That date will behave OK in calculations, but it will convert to a string wrongly. I have no idea what happens in this case.

Another behaviour that might look strange, is intentional: The Gregorian calendar became official October 15th 1582 – for the Vatican at least. For all dates prior to that, the internal properties are Gregorian still, but the string conversion will use the Julian calendar.

Again some code:

set x to date ("15-1-1000") -- British format
log x
log x as text
log day of x
log month of x
log year of x
log minutes of x
log seconds of x

The resulting log is:

(*date Monday, 15 January 1000 00:00:00*)
(*Monday, 15 January 1000 00:00:00*)
(*20*)
(*January*)
(*1000*)
(*6*)
(*32*)

Note that the conversions string to date and date to string are consistent in the Julian calendar. But the actual properties are Gregorian. The error about minutes and seconds is still there.

What does that mean for the functions here? Some sample code using the newDate function from this collection:

set x to newDate(1000, 1, 15)
log x
log x as text
log day of x
log month of x
log year of x
log minutes of x
log seconds of x

The resulting log (I printed just the part with the log commands) is:

(*date Tuesday, 9 January 1000 23:53:28*)
(*Tuesday, 9 January 1000 23:53:28*)
(*15*)
(*January*)
(*1000*)
(*0*)
(*0*)

The display and the string conversion are using the Julian calendar and the time display is wrong. But the properties themselves are OK.