(*

© Jürgen Schell 2010 - 2012

http://www.j-schell.de


File provided unter Lesser GNU Public License.

http://www.gnu.org/licenses/lgpl.html


No guarantee for these functions whatsoever, regard them as

exercises to understand, how some problems may be solved.


Essential calendar functions that might be useful for some people.

Several functions are trivial, others are less so.

not that almost all functions indicate deficiencies of the AppleScript

date implementation. Mac OS implements almost everything, but alas, the

AppleScript developers do not thing that they should provide you with access

to those features.


(Minor change July 2010: Use current date now to initialize a date

in newDate and newDateTime. This makes it possible to copy / paste the

code from text files or HTML pages under any locale. J.S.)


(Major Changes January 2012, version 2: Added handlers for ISO week calendar.

Added handlers to check if a date exists.

Liberalized numeric day arguments for all integers and numeric

month arguments in Gregorian and Julian calendar.

Made function rosh_hashanah_cdn faster.


Documentation (English) on http://www.j-schell.de/node/482

Dokumentation (Deutsch) auf http://www.j-schell.de/node/484

*)


--============== General AppleScript Date Tools ===============


on newDate(YearVal, MonthVal, DayVal)

-- liberal version:

-- MonthVal and DayVal can be any integer

-- including zero and negatives

-- e.g. newDate(2012, 3, 0) returns February 29 2012, newDate(2013, 3, 0) returns February 28 2013

-- newDate(2012, 3 - 48, 1) returns March 1 2008

-- make sure that integers

set YearVal to YearVal as integer

set MonthVal to MonthVal as integer

set DayVal to DayVal as integer

-- raise errors for certain range limits

if YearVal < 1 then

error "Year for newDate too small" number 1700

end if

-- prepare months

set round_down to ((MonthVal - 1) mod 12 < 0) as integer -- yealds 1 if months are negative and not multiple of 12

set y_delta to (MonthVal - 1) div 12

set y_delta to y_delta - round_down

set MonthVal to ((MonthVal - 1) mod 12 + 12) mod 12 + 1 -- the double mod works with negative values

set YearVal to YearVal + y_delta

-- create a date object in a locale independent way

-- that survives copy / paste as code text

set temp to current date

try

set time of temp to 0

set day of temp to 1

set year of temp to YearVal

set month of temp to MonthVal

set temp to temp + (DayVal - 1) * days

on error

error "Some value out of range in newDate" number 1700

end try

return temp

end newDate


----------------

on newDateTime(YearVal, MonthVal, DayVal, HourVal, MinuteVal, SecondVal)

-- liberal version:

-- Year must be non-negative but all other values

-- can be any integer including zero and negatives

-- e.g. newDateTime(2012, 3, 1, -24, 0, 0) returns date "Wednesday, 29 February 2012 00:00:00"

set YearVal to YearVal as integer

set MonthVal to MonthVal as integer

set DayVal to DayVal as integer

set HourVal to HourVal as integer

set MinuteVal to MinuteVal as integer

set SecondVal to SecondVal as integer

-- raise errors for certain range limits

if YearVal < 1 then

error "Year for newDate too small" number 1700

end if

-- prepare months

set round_down to ((MonthVal - 1) mod 12 < 0) as integer -- yealds 1 if months are negative and not multiple of 12

set y_delta to (MonthVal - 1) div 12

set y_delta to y_delta - round_down

set MonthVal to ((MonthVal - 1) mod 12 + 12) mod 12 + 1 -- the double mod works with negative values

set YearVal to YearVal + y_delta

-- create a date object in a locale independent way

-- that survives copy / paste as code text

set time_val to HourVal * hours + MinuteVal * minutes + SecondVal

set temp to current date

try

set time of temp to 0

set day of temp to 1

set year of temp to YearVal

set month of temp to MonthVal

set temp to temp + (DayVal - 1) * days + time_val

on error

error "Some value out of range in newDate" number 1700

end try

return temp

end newDateTime


----------------


on DateToISOdayofweek(theDate)

-- Returns the ISO weekday number of date

-- raise error for wrong class

if (class of theDatedate) then

error "Argument of ISOdayofweek is no date" number 1700

end if

set x to (weekday of theDate) - 1

return ((x + 6) mod 7 + 1)

end DateToISOdayofweek


----------------


on DateToISOweekofyear(theDate)

-- Returns the number of the ISO week the date is in.

-- Basic ideas: Week 1 of year n is the first week having at least 4 days in year n (that's the definition).

-- This is equivalent to: Week 1 is the week having the first Thursday of year n.

-- By inversion, every Thursday falls into the same year, its week is counted in.

-- 1. find Thursday of the week, than count days since start of the same year.

-- Integer division by 7 + 1 gives the week number.

-- raise error for wrong class

if (class of theDatedate) then

error "Argument of ISOweekofyear is no date" number 1700

end if

copy theDate to tempDate

set x to ((weekday of theDate) + 5) mod 7 -- i.e. Monday = 0, Tuesday = 1, ...

set tempDate to tempDate - x * days + 3 * days -- Thursday of the week

copy tempDate to StartOfYear

set day of StartOfYear to 1

set month of StartOfYear to 1

set elapsedDays to (tempDate - StartOfYear) / days

set weekofyear to elapsedDays div 7 + 1

return weekofyear

end DateToISOweekofyear


--============== Calendar Conversion Functions / Easter / Holyday Calculations =======

----------------

on preceding_day(The_day, the_date_or_cdn)

-- finds the date of a weekday preceding the_date_or_cdn

-- if the_date_or_cdn is an AppleScript date object, the function

-- returns a date object.

-- If the_date_or_cdn is a cdn, the function returns a cdn.

-- the_day is a number from 1 to 7 for a weekday according

-- to ISO standards (Monday = 1).

-- AS day names can be used (e.g. "Monday")

-- Useful for calculations like Advent Sunday or Election Day

-- The following if corrects integers for ISO usage.

-- Remove, if you prefer

-- US counting of days

if class of The_day is integer then

set The_day to The_day mod 7 + 1

end if

-- end ISO conversion

try

set The_day to (The_day - 1) as integer

on error

error "Day value must be integer or day name" number 1700

end try

if (The_day < 0) or (The_day > 6) then

error "Day values run from 1 to 7" number 1700

end if

if class of the_date_or_cdn is date then

set week_day to ((weekday of the_date_or_cdn) - 1)

set delta to (week_day - The_day + 7) mod 7

if delta = 0 then set delta to 7

return the_date_or_cdn - (delta * 86400)

else if class of the_date_or_cdn is integer then

set week_day to (the_date_or_cdn - 6) mod 7

if week_day < 0 then set week_day to week_day + 7

set delta to (week_day - The_day + 7) mod 7

if delta = 0 then set delta to 7

return the_date_or_cdn - delta

else

error "Date value must be date object or integer" number 1700

end if

end preceding_day



----------------

on date_to_cdn(theDate)

-- raise error for wrong class

if (class of theDatedate) then

error "Argument of date_to_cdn is no date" number 1700

end if

set the_offset to 2086303 -- cdn of ref_date

set ref_date to current date

set {time of ref_date, day of ref_date, month of ref_date, year of ref_date} to {0, 1, 1, 1000}

set days_elapsed to (theDate - ref_date) div days

return days_elapsed + the_offset

end date_to_cdn


----------------


on greg_to_cdn(YearVal, MonthVal, DayVal)

-- Returns the chronological Julian Day Number for the given date values.

-- Argument values may be any integer.

-- greg_to_cdn(2012, 3, 0) is the cdn of February 29th 2012.

-- greg_to_cdn(2012, 1 + 48, 1) is the cdn of January 1st 2016

-- make sure that integers

set YearVal to YearVal as integer

set MonthVal to MonthVal as integer

set DayVal to DayVal as integer

-- raise errors for certain range limits

-- prepare months for cases month < 1 or > 12

set round_down to ((MonthVal - 1) mod 12 < 0) as integer -- yealds 1 if months are negative and not multiple of 12

set y_delta to (MonthVal - 1) div 12

set y_delta to y_delta - round_down

set MonthVal to ((MonthVal - 1) mod 12 + 12) mod 12 + 1 -- the double mod works with negative values

set YearVal to YearVal + y_delta

set epoch to 1721120 -- cdn of March 1, year 1 BCE

set days_400 to 146097 -- days in the 400 year cycle

-- change date to March based counting

set DayVal to DayVal - 1

if MonthVal < 3 then

set MonthVal to MonthVal + 9

set YearVal to YearVal - 1

else

set MonthVal to MonthVal - 3

end if

-- dealing with days before epoch:

-- due to behaviour of div (rounding towards 0 rather than nevative infinity)

-- it is faster to shift the years in such rare cases rather than defining

-- own proper division

set days_shift to 0

if YearVal < 0 then -- handle BCE cases

set years_shift_400 to ((YearVal div 400) * -1 + 1)

set YearVal to YearVal + years_shift_400 * 400

set days_shift to years_shift_400 * days_400

end if

-- YearVal is ≥ 0 from this point on

-- convert month and day to day number in year

set day_num to DayVal + (MonthVal * 30) + ((MonthVal + 1 + MonthVal div 5) div 2)

-- convert year to number of days

set day_num_2 to YearVal * 365 + YearVal div 4 - YearVal div 100 + YearVal div 400

return epoch + day_num + day_num_2 - days_shift

end greg_to_cdn


----------------


on cdn_to_date(cdn)

-- make sure that integers

set cdn to cdn as integer

-- raise errors for certain range limits

if (cdn < 1721426) then -- cdn of Jan 1 0001

error "Value of Julian day number too low to convert to date object in cdn_to_date" number 1700

end if

set the_offset to 2086303 -- cdn of ref_date

set ref_date to current date

set {time of ref_date, day of ref_date, month of ref_date, year of ref_date} to {0, 1, 1, 1000}

set days_elapsed to cdn - the_offset

return ref_date + days_elapsed * days

end cdn_to_date


----------------

on cdn_to_greg(cdn)

-- Computational life is easier if we start at the beginning of a 400

-- year leap year cycle with all exeptions (February 29)

-- at the end.

-- Hence we count from March 1 of year 0

-- Internal counting is zero based

-- This code is strictly drilling down the number using

-- cycles of constant length.

set cdn to cdn as integer

set epoch to 1721120 -- cdn of March 1, year 1 BCE

set day_num to cdn - epoch

set days_400 to 146097 -- days in the 400 year cycle

set days_100 to 36524 -- days in the 100 year cycle

set days_4 to 1461 -- days in the 4 year cycle

set days_1 to 365 -- days in one year

-- dealing with days before epoch:

-- due to behaviour of div (rounding towards 0 rather than nevative infinity)

-- and mod (is negative for negative first arguments in AppleScript)

-- it is faster to shift the years in such rare cases rather than defining

-- own proper division and modulo

set year_shift to 0

if day_num < 0 then

set shift_cycles to (day_num div days_400) * -1 + 1

set year_shift to shift_cycles * 400

set day_num to day_num + shift_cycles * days_400

end if

set The_year to (day_num div days_400) * 400

set year_day to day_num mod days_400

if (year_day + 1) = days_400 then

-- we are on a Feb 29 at the end of the cycle

set The_year to The_year + 399

set year_day to 365

else

set The_year to The_year + (year_day div days_100) * 100

set year_day to year_day mod days_100

set The_year to The_year + (year_day div days_4) * 4

set year_day to year_day mod days_4

if (year_day + 1) = days_4 then

-- we are on a Feb 29 at the end of the cycle

set The_year to The_year + 3

set year_day to 365

else

set The_year to The_year + (year_day div days_1)

set year_day to year_day mod days_1

end if

end if

set The_year to The_year - year_shift -- Needed for dates BCE

-- here, we have the year and the number of the day in the year, everything counted

-- zero based from March 1

-- the day number needs to be split in months and day in month now

set days_5_months to 153 -- number of days in a 5 month cycle

set days_2_months to 61

set The_month to (year_day div days_5_months) * 5

set The_day to year_day mod days_5_months

set The_month to The_month + (The_day div days_2_months) * 2

set The_day to The_day mod days_2_months

if The_day > 30 then

set The_month to The_month + 1

set The_day to The_day - 31

end if

-- converting the internal March based system to the usual one

set The_day to The_day + 1

if The_month > 9 then -- January or February of the next year

set The_year to The_year + 1

set The_month to The_month - 9

else

set The_month to The_month + 3

end if

return {year:The_year, month:The_month, day:The_day}

end cdn_to_greg


----------------


on julian_to_cdn(YearVal, MonthVal, DayVal)

-- Returns the chronological Julian Day Number for the given date values in the Julian calendar.

-- Argument values may be any integer.

-- julian_to_cdn(2012, 3, 0) is the cdn of February 29th 2012.

-- julian_to_cdn(2012, 1 + 48, 1) is the cdn of January 1st 2016

-- make sure that integers

set YearVal to YearVal as integer

set MonthVal to MonthVal as integer

set DayVal to DayVal as integer

-- raise errors for certain range limits

-- prepare months for cases month < 1 or > 12

set round_down to ((MonthVal - 1) mod 12 < 0) as integer -- yealds 1 if months are negative and not multiple of 12

set y_delta to (MonthVal - 1) div 12

set y_delta to y_delta - round_down

set MonthVal to ((MonthVal - 1) mod 12 + 12) mod 12 + 1 -- the double mod works with negative values

set YearVal to YearVal + y_delta

set epoch to 1721118 -- cdn of March 1, year 1 BCE

set days_4 to 1461 -- days in the 4 year cycle

-- chage date to March based counting

set DayVal to DayVal - 1

if MonthVal < 3 then

set MonthVal to MonthVal + 9

set YearVal to YearVal - 1

else

set MonthVal to MonthVal - 3

end if

-- dealing with days before epoch:

-- due to behaviour of div (rounding towards 0 rather than nevative infinity)

-- it is faster to shift the years in such rare cases rather than defining

-- own proper division

set days_shift to 0

if YearVal < 0 then -- handle BCE cases

set years_shift_4 to ((YearVal div 4) * -1 + 1)

set YearVal to YearVal + years_shift_4 * 4

set days_shift to years_shift_4 * days_4

end if

-- YearVal is ≥ 0 from this point on

-- convert month and day to day number in year

set day_num to DayVal + (MonthVal * 30) + ((MonthVal + 1 + MonthVal div 5) div 2)

-- convert year to number of days

set day_num_2 to YearVal * 365 + YearVal div 4

return epoch + day_num + day_num_2 - days_shift

end julian_to_cdn


----------------


on cdn_to_julian(cdn)

-- based on PHP function jdtojulian

-- make sure that integers

set cdn to cdn as integer

-- range check

if cdn < 0 then

error "Julian Day Number negative in cdn_to_julian: " & cdn as text number 1700

end if

set cdn_offset to 32083

set days_in_5_months to 153

set days_in_4_years to 1461

set temp to (cdn + cdn_offset) * 4 - 1

set The_year to temp div days_in_4_years

set day_of_year to (temp mod days_in_4_years) div 4 + 1

-- finding month and day in month.

-- Basic trick: Start the year with March.

-- This gives regular cycles of 5 months (the 3rd being incomplete) of same number of days.

-- Adjust to common usage (year starts with January) afterwards.

set temp to day_of_year * 5 - 3

set The_month to temp div days_in_5_months

set The_day to (temp mod days_in_5_months) div 5 + 1

if The_month < 10 then

set The_month to The_month + 3

else

set The_month to The_month - 9

set The_year to The_year + 1

end if

set The_year to The_year - 4800

if The_year < 1 then set The_year to The_year - 1

return {year:The_year, month:The_month, day:The_day}

end cdn_to_julian


----------------


on easter_greg_date(The_year)

-- Returns the date of Gregorian Easter for the year.

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- make sure that integers

set The_year to The_year as integer

-- raise errors for certain range limits

if The_year < 1 then

error "Year value for Gregorian Easter date too small." number 1700

end if

set Century to The_year div 100 + 1

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- The trick here is to calculate the epact for April 5 and subtract it from April 19

set Shifted_epact to (14 + 11 * (The_year mod 19) - (3 * Century div 4) + ((5 + 8 * Century) div 25)) mod 30

-- adjustment for implementation of mod operator in AppleScript

-- Yields negative results for negative first operand

-- Not wrong but useless in most cases

if Shifted_epact < 0 then

set Shifted_epact to Shifted_epact + 30

end if

set Adjusted_epact to Shifted_epact

if ((Shifted_epact = 0) or ((Shifted_epact = 1) and (10 < The_year mod 19))) then

set Adjusted_epact to Adjusted_epact + 1

end if

set April_date to current date

set {time of April_date, day of April_date, month of April_date, year of April_date} to {0, 19, 4, 1900}

set year of April_date to The_year

set paschal_moon to April_date - (Adjusted_epact * days)

set Easter_date to paschal_moon + (8 - (weekday of paschal_moon) as integer) * days

return Easter_date

end easter_greg_date


-----------------


on easter_greg_cdn(The_year)

-- requires greg_to_cdn

-- returns the chronological Julian Day number of Gregorian Easter for the year.

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- make sure that integers

set The_year to The_year as integer

-- raise errors for certain range limits

set Century to The_year div 100 + 1

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- The trick here is to calculate the epact for April 5 and subtract it from April 19

set Shifted_epact to (14 + 11 * (The_year mod 19) - (3 * Century div 4) + ((5 + 8 * Century) div 25)) mod 30

-- adjustment for implementation of mod operator in AppleScript

-- Yields negative results for negative first operand

-- Not wrong but useless in most cases

if Shifted_epact < 0 then

set Shifted_epact to Shifted_epact + 30

end if

set Adjusted_epact to Shifted_epact

if ((Shifted_epact = 0) or ((Shifted_epact = 1) and (10 < The_year mod 19))) then

set Adjusted_epact to Adjusted_epact + 1

end if

set April_date to greg_to_cdn(The_year, 4, 19)

set paschal_moon to April_date - Adjusted_epact

set week_day to (paschal_moon - 6) mod 7 -- Sunday based

if week_day < 0 then set week_day to week_day + 7

set Easter_date to paschal_moon + 7 - week_day

return Easter_date

end easter_greg_cdn


-----------------


on easter_jul_date(The_year)

-- compared with PHP calendar function from 1000 to 10000

-- requires julian_to_cdn

-- requires cdn_to_date

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- The trick here is to calculate the epact for April 5 and subtract it from April 19

-- make sure that integers

set The_year to The_year as integer

-- raise errors for certain range limits

if The_year < 1 then

error "Year value for Julian Easter date too small." number 1700

end if

set Shifted_epact to (14 + 11 * (The_year mod 19)) mod 30

set apr_19 to julian_to_cdn(The_year, 4, 19)

set paschal_moon_cdn to apr_19 - Shifted_epact

set paschal_moon to cdn_to_date(paschal_moon_cdn)

set Easter_date to paschal_moon + (8 - (weekday of paschal_moon) as integer) * days

return Easter_date

end easter_jul_date


-----------------


on easter_jul_cdn(The_year)

-- requires julian_to_cdn

-- Based on Dershowitz, Reingold: Calendrical Calculations, Cambridge 2008

-- The trick here is to calculate the epact for April 5 and subtract it from April 19

-- make sure that integers

set The_year to The_year as integer

-- raise errors for certain range limits

if The_year < 1 then

error "Year value for Julian Easter date too small." number 1700

end if

set Shifted_epact to (14 + 11 * (The_year mod 19)) mod 30

set apr_19 to julian_to_cdn(The_year, 4, 19)

set paschal_moon_cdn to apr_19 - Shifted_epact

set week_day to (paschal_moon_cdn - 6) mod 7 -- Sunday based

if week_day < 0 then set week_day to week_day + 7

set Easter_date to paschal_moon_cdn + 7 - week_day

return Easter_date

end easter_jul_cdn


-----------------

-- Returns the chronological Julian Day Number for Rosh haShanah in the year anno mundi

-- rosh_hashanah_cdn version 2 Dec 2010

-- Much faster compared to previous version,

-- due to use of reals and better arrangement of postponement rules

on rosh_hashanah_cdn(The_year)

-- Returns the chronlogical Julian Day Number for Tishri 1 in year anno mundi

set The_year to The_year as integer

-- raise errors for certain range limits

if The_year < 1 then

error "Year value for Hebrew calender too small." number 1700

end if

set halakimPerMonth to 765433

set halakimPerDay to 25920

set NewMoonOfChaos to 5604 -- fifth hour, 204 halakim

set cdnoffset to 347998 -- cdn of Tishri 1, year 1

set monthsElapsed to (7 * The_year - 6) div 19 + 12 * (The_year - 1)

-- the next operation is in float numbers since it exceeds maxint.

-- This is OK for 64 bit ISO floats

set total_halakim to monthsElapsed * halakimPerMonth + NewMoonOfChaos

set dayNum to total_halakim div halakimPerDay -- the day of the New Moon

set halakim to total_halakim mod halakimPerDay -- time of the New Moon

set roshhashanah to dayNum -- preliminary value

set weekDayMolad to (roshhashanah + 1) mod 7 -- 0 based day number of New Moon

-- check if a postponement rule applies. Starts with the most likely one.

set d to (halakim ≥ 19440) or ¬

(((The_year * 7 + 1) mod 19 ≥ 7) and (weekDayMolad = 2) and (halakim ≥ 9924)) or ¬

((((The_year - 1) * 7 + 1) mod 19 < 7) and (weekDayMolad = 1) and (halakim ≥ 16789)) -- Dehiyya 1, 4 or 5

if d then

set roshhashanah to roshhashanah + 1

set weekDayMolad to (weekDayMolad + 1) mod 7

end if

-- dehiyya 2, day is no possible "gate" of year

-- having this last uncontitionally combines with d1 to d3 and adjusts d4 properly

if (weekDayMolad = 0) or (weekDayMolad = 3) or (weekDayMolad = 5) then

set roshhashanah to roshhashanah + 1

end if

return roshhashanah + cdnoffset

end rosh_hashanah_cdn



----------------

on heb_to_cdn(The_year, The_month, The_day)

-- requires function "rosh_hashanah_cdn"

-- Returns the chronological Julian Day Number for the given date in the Hebrew calendar.

-- Month 13 is Adar in common years, Adar II in leap years.

-- Month 12 is Adar or Adar I

-- tested for millenia 1, 5000, 6000, 10000

-- and about 10000 arbitrary dates from 1 to 10000

set The_year to The_year as integer

set The_month to The_month as integer

set The_day to The_day as integer

-- check for ranges

if The_year < 1 then

error "Year below 1 in heb_to_date" number 1700

end if

if (The_month < 1) or (The_month > 13) then

error "Month value out of range in heb_to_date" number 1700

end if

-- Note:

-- If you prefer PHP like month counting

-- (i.e. Tishri = 1, Elul = 13),

-- uncomment the following line:

-- set The_month to (The_month + 5) mod 13 + 1

-- settings for leap years

set is_leap to (The_year * 7 + 1) mod 19 < 7

-- Knowing if the year is a leap year, all dates from

-- Tevet to next Cheshwan can be calculated knowing

-- Rosh HaShanah inbetween (optionally splling over Cheshwan 30 to Kislev 1).

-- Hence we use Tevet 1 as a starting point.

if (is_leap) then

set month_count to 13

set month_internal to (The_month + 3) mod month_count -- i.e. Tevet as 0

set tevet_offset to 295

else

set month_count to 12

if (The_month = 13) then

set The_month to 12

end if

set month_internal to (The_month + 2) mod month_count -- i.e. Tevet as 0

set tevet_offset to 265

end if

-- find Rosh haShanah

if (The_month < 7) or (The_month > 9) then -- Tevet to Elul

set rosh_hashanah to rosh_hashanah_cdn(The_year + 1)

else -- Tishri to Kislev

set rosh_hashanah to rosh_hashanah_cdn(The_year)

end if

set day_num to The_day

-- case 1: We are in the range where one Rosh haShanah date is sufficient

if (month_internal < (month_count - 1)) then -- range is from Tevet to Cheshvan

set month_start to rosh_hashanah - tevet_offset

if is_leap then

set day_num to day_num + (month_internal * 29) + ((month_internal + ((month_internal > 2) as integer)) div 2)

else

set day_num to day_num + (month_internal * 29) + ((month_internal) div 2)

end if

set the_result to rosh_hashanah - tevet_offset + day_num - 1

else

-- case 2: We need two Rosh HaShanah dates to determine the length of Cheshwan

set rosh_hashanah_2 to rosh_hashanah_cdn(The_year + 1)

set year_lenght to rosh_hashanah_2 - rosh_hashanah

set cheshvan_length to 29 + (((year_lenght = 355) or (year_lenght = 385)) as integer)

-- set kislev_length to 30 - (((year_lenght = 353) or (year_lenght = 383)) as integer) -- not really needed here

set the_result to rosh_hashanah + 30 + cheshvan_length + day_num - 1

end if -- Nisan to Cheshvan

return the_result

end heb_to_cdn


----------------

on cdn_to_heb(cdn)

-- requires function "rosh_hashanah_cdn"

-- Returns a record for the Hebrew date of the given chronological Julian Day Number.

-- State: Checked for consistency with heb_to_cdn for 10000 years in this version.

set cdn_of_epoch to 347998

set ave_month to 29.5305941358

try

set cdn to cdn as integer

on error

error "Julian Day Number no integer in cdn_to_heb" number 1700

end try

if cdn < cdn_of_epoch then

error "Julian Day Number too small in cdn_to_heb" number 1700

end if

set day_num to cdn - cdn_of_epoch -- set Tishri 1 of year 1 to count 0

-- try a good estimate in floating point world

-- +1 makes sure that error in year can be only in one direction

set est_month to (day_num + 1) / ave_month

set est_month_int to (est_month div 1)

-- find number of years in months

set est_year to ((est_month_int * 19 + 17) div 235)

-- estimate month in year. For this estimation, we count from Tishri

set est_month to est_month - ((7 * est_year + 1) div 19 + 12 * (est_year))

-- based on estimation, do real calculation, integer

if est_month < 2.5 then

-- Before middle of Kislev

-- Find preceding start of year

set p_rosh_hashanah to rosh_hashanah_cdn(est_year + 1)

set delta to cdn - p_rosh_hashanah

set which_r to 0

else

-- Find following start of year

set f_rosh_hashanah to rosh_hashanah_cdn(est_year + 2)

set delta to cdn - f_rosh_hashanah

set which_r to 1

end if

set final_year to est_year + 1

if (delta > -178) and (delta < 59) then

-- we are in the range Nisan 1 to Cheshvan 29 where no month can change

-- Our year may be wrong,

-- estimated value may be Tishri but date is in Elul:

if (delta < 0) and (which_r = 0) then

-- Date is in Elul, but estimated was Tishri

set final_year to final_year - 1

-- year correction b

end if

set delta to delta + 177 -- 0 based counting from Nisan

set final_month to ((2 * delta) div 59) + 1

set final_day to delta - ((final_month - 1) * 29 + (final_month) div 2) + 1

else if (((final_year) * 7 + 1) mod 19 < 7) and (delta > -296) and (delta < 59) then

-- in a leep year

-- Tevet or later in leap year

set delta to delta + 295 -- start from Tevet 1

if delta > 28 then -- beyond Tevet

set delta to delta + 1 -- allows to handle all months as 30 days

end if

set final_month to (delta div 30) + 10

set final_day to delta mod 30 + 1

else if (delta > -266) and (delta < 59) then

-- Tevet or later in common year

set delta to delta + 265 -- start from Tevet 1

if delta > 28 then -- beyond Tevet

set delta to delta + 1 -- allows to handle all months as 30 days

end if

set final_month to (delta div 30) + 10

set final_day to delta mod 30 + 1

else

-- We are in the range where the lenght of Cheshvan comes in.

-- The second Rosh HaShanah date is needed for that as well

if which_r = 0 then

set f_rosh_hashanah to rosh_hashanah_cdn(est_year + 2)

else

set p_rosh_hashanah to rosh_hashanah_cdn(est_year + 1)

end if

set year_lenght to f_rosh_hashanah - p_rosh_hashanah

if (year_lenght = 355) or (year_lenght = 385) then

set cheshvan_length to 30

else

set cheshvan_length to 29

end if

set delta to cdn - p_rosh_hashanah - 29 -- starting Tishri 29 

if delta = cheshvan_length then

set final_month to 8

set final_day to 30

else

set final_month to 9

set final_day to delta - cheshvan_length

end if

end if

return {year:final_year, month:final_month, day:final_day}

end cdn_to_heb


----------------


on cdn_to_iso_week_cal(cdn)

-- returns a record for the date in the ISO week calendar for the chronological Julian Day Number

-- make sure, argument is of proper type

try

set cdn to cdn as integer

on error "Argument no integer in cdn_to_iso_week_cal" number 1700

end try

set epoch to 1721425 -- Dec 31st year 0

set days_400 to 146097 -- days in the 400 year cycle

set days_100 to 36524 -- days in the 100 year cycle

set days_4 to 1461 -- days in the 4 year cycle

set days_1 to 365 -- days in one year

set wd0 to cdn mod 7

if wd0 < 0 then

set wd0 to wd0 + 7

end if

set wd to wd0 + 1

set the_thursday to cdn - wd0 + 3

--set the_thursday to cdn

set epoch to 1721426 -- January 1 year 1

set d_val to the_thursday - epoch

set delta_year to 0

if d_val < 0 then

-- due to AppleScript mod behaviour, we need some correction

-- if day number is negative. Must be made positive

set temp to (d_val * -1) div days_400 + 1 -- number of 400 year cycles to correct

set d_val to d_val + temp * days_400

set delta_year to -temp * 400

end if

-- the following code splits into a year and a day number within the year

-- Day number is 0 for January 1st

set y to (d_val div days_400) * 400

set d_val to d_val mod days_400

if (d_val + 1) = days_400 then

set y to y + 399

set d to 365

else

set y to y + (d_val div days_100) * 100

set d_val to d_val mod days_100

set y to y + (d_val div days_4) * 4

set d_val to d_val mod days_4

if (d_val + 1) = days_4 then

set y to y + 3

set d to 365

else

set y to y + d_val div days_1

set d to d_val mod days_1

end if

end if

set y to y + 1 + delta_year

-- formatting iso string

set sign to ""

set y_string to y

if y < 0 then

set sign to "-"

set y_string to -y

end if

if y_string < 1000 then

set y_string to sign & (text -4 thru -1 of ((10000 + y_string) as text))

else

set y_string to sign & y_string as text

end if

set w to d div 7 + 1

set w_string to text 2 thru 3 of ((w + 100) as text)

set iso_string to y_string & "-W" & w_string & "-" & wd

set r to {year:y, week:w, day:wd, iso_string:iso_string}

return r

end cdn_to_iso_week_cal



----------------


on iso_week_cal_to_cdn(YearVal, WeekVal, DayVal)

-- returns a CDN for the ISO week calendar date given

-- check for data types

try

set YearVal to YearVal as integer

on error

error "Year can not be converted to integer in iso_week_cal_to_cdn" number 1700

end try

try

set WeekVal to WeekVal as integer

on error

error "Week can not be converted to integer in iso_week_cal_to_cdn" number 1700

end try

try

set DayVal to DayVal as integer

on error

error "Day can not be converted to integer in iso_week_cal_to_cdn" number 1700

end try

set YearVal to YearVal - 1

set y_shift to 0

set cycle_shift to 0

if YearVal < 0 then

set cycle_shift to -YearVal div 400 + 1

set y_shift to cycle_shift * 400

end if

set YearVal to YearVal + y_shift

set days_400 to 146097

set epoch to 1721429 -- January 4th year 1

-- next line finds January 4th of the given year

set jan_day to epoch + YearVal * 365 + YearVal div 4 - YearVal div 100 + YearVal div 400

-- The Monday starting the calendar week year

set jan_day to jan_day - (jan_day mod 7)

set cdn to jan_day + 7 * (WeekVal - 1) + (DayVal - 1) - cycle_shift * days_400

end iso_week_cal_to_cdn


----------------


on iso_week_cal_to_date(y, w, d)

-- returns an AppleScript date object for the given date in ISO week calendar

-- check for proper types and ranges

try

set w to w as integer

on error

error "Argument “week” in iso_week_cal_to_date can not be converted to integer" number 1700

end try

try

set y to y as integer

on error

error "Argument “year” in iso_week_cal_to_date can not be converted to integer" number 1700

end try

try

d as integer

on error

error "Argument “day” in iso_week_cal_to_date can not be converted to integer" number 1700

end try

-- d may be an integer or dan AppleScript day constant. For consistent use,

-- integers are rotated to US counting.

if class of d is not integer then -- case of AppleScript day name

set d to (d + 5) mod 7 -- Convert to zero based day count starting with Monday = 0

else

set d to d - 1

end if

set local_date to current date

-- January 4th is always in week 1

set {time of local_date, day of local_date, month of local_date} to {0, 4, 1}

try

set year of local_date to y

on error

error "Year argument out of range in iso_week_cal_to_date" number 1700

end try

-- find Monday starting the first week of the year

set m_offset to ((weekday of local_date) + 5) mod 7

set local_date to local_date - m_offset * days

-- add calendar weeks and day

set local_date to local_date + (w - 1) * weeks

set local_date to local_date + d * days

return local_date

end iso_week_cal_to_date



----------------


on date_to_iso_week_cal(the_date)

-- Returns a record with the ISO week parts and an ISO string for the given date

copy the_date to local_date

try

local_date as date

on error

error "Argument no date in date_to_iso_week_cal" number 1700

end try

set time of local_date to 0

set day_0 to (((weekday of local_date) + 5) mod 7) -- Zero based week day, Monday = 0

set d to day_0 + 1 -- ISO week day, Monday = 1

set local_date to (local_date - (day_0 - 3) * days) -- Thursday of the week

set y to year of local_date

copy local_date to year_start

set day of year_start to 1

set month of year_start to 1

set w to ((local_date - year_start) / days) div 7 + 1

set w_string to (text -2 thru -1 of ((w + 100) as string))

if y < 10000 then -- for people calculating beyond year 9999 ;-)

set y_string to (text -4 thru -1 of ((y + 10000) as string))

else

set y_string to "+" & y as string

end if

set iso_string to y_string & "-W" & w_string & "-" & d as string

return {year:y, week:w, day:d, iso_string:iso_string}

end date_to_iso_week_cal




--============== Leap Year Functions ==========================


-----------------


on is_leap_greg(The_year)

-- is The_year a leap year in the Gregorian calendar?

-- make sure that integers

set The_year to The_year as integer

return ((The_year mod 4 = 0) and ((The_year mod 100 ≠ 0) or (The_year mod 400 = 0)))

end is_leap_greg


------------------


on is_leap_jul(The_year)

-- is The_year a leap year in the Julian calendar?

-- make sure that integers

set The_year to The_year as integer

return (The_year mod 4 = 0)

end is_leap_jul


----------------


on is_leap_heb(The_year)

-- is The_year a leap year in the Hebrew calendar?

-- make sure that integers

set The_year to The_year as integer

-- raise errors for certain range limits

if The_year < 1 then

error "Year value too low in is_leap_heb" number 1700

end if

return ((7 * The_year + 1) mod 19 < 7)

end is_leap_heb



------------------


on is_long_iso_week_year(y)

-- Checks if the year has 53 ISO weeks or not

-- or: is The_year a “leap year” in the Julian calendar?

-- value check

try

set y to y as integer

on error

error "Argument in iso_year_long can not be converted to integer" number 1700

end try

set y to y mod 400

if y < 0 then

set y to y + 400

end if

if (y = 100) then

return false

end if

set c to y div 100 + 1

set const to item c of {12, 8, 4, 0}

set is_long to (y * 5 + const) mod 28 < 5

return is_long

end is_long_iso_week_year


------------------

--============== Checking if a Date exists ====================


-- The following functions check, if a date exists

-- in the given calendar.

-- E.g. date_exists_greg(2000,2,29) returns true,

-- but date_exists_greg(2100,2,29) returns false


on day_exists_greg(The_year, The_month, The_day)

try

set The_year to The_year as integer

set The_month to The_month as integer

set The_day to The_day as integer

on error

error "Argument in date_exists_greg is no number" number 1700

end try

if The_year < 0 then

error "Year in date_exists_greg negative" number 1700

end if

-- most trivial cases

if (The_month > 0) and (The_month < 13) ¬

and (The_day > 0) and (The_day < 29) then

return true

end if

if (The_month < 1) or (The_month > 12) ¬

or (The_day < 1) or (The_day > 31) then

return false

end if

-- the_day is 29, 30 or 31, if code gets here

-- remaining cases always true in long months

if ((((The_month - 1) mod 7) mod 2) = 0) then

return true

else -- month is short

-- day 31 always false in short months

if (The_day = 31) then

return false

end if

end if

-- the_day can be 29 or 30 now

-- deal with February

if The_month = 2 then

if The_day = 30 then

return false

end if

-- leap year, Gregorian rule

if (The_year mod 4 = 0) and ((The_year mod 100 ≠ 0) or (The_year mod 400 = 0)) then

return true

end if

end if

return false

end day_exists_greg



------------------


on day_exists_jul(The_year, The_month, The_day)

try

set The_year to The_year as integer

set The_month to The_month as integer

set The_day to The_day as integer

on error

error "Argument in date_exists_greg is no number" number 1700

end try

if The_year < 0 then

error "Year in date_exists_greg negative" number 1700

end if

-- most trivial cases

if (The_month > 0) and (The_month < 13) ¬

and (The_day > 0) and (The_day < 29) then

return true

end if

if (The_month < 1) or (The_month > 12) ¬

or (The_day < 1) or (The_day > 31) then

return false

end if

-- the_day is 29, 30 or 31, if code gets here

-- remaining cases always true in long months

if ((((The_month - 1) mod 7) mod 2) = 0) then

return true

else -- month is short

-- day 31 always false in short months

if (The_day = 31) then

return false

end if

end if

-- the_day can be 29 or 30 now

-- deal with February

if The_month = 2 then

if The_day = 30 then

return false

end if

-- leap year, Julian rule

if The_year mod 4 = 0 then -- the only difference to the corresponding Gregorian function

return true

end if

end if

return false

end day_exists_jul



------------------


on day_exists_heb(The_year, The_month, The_day)

-- requires function "rosh_hashanah_cdn"

try

set The_year to The_year as integer

set The_month to The_month as integer

set The_day to The_day as integer

on error

error "Argument in date_exists_heb is no number" number 1700

end try

if The_year < 1 then

error "Year in date_exists_heb negative" number 1700

end if

-- deal with trivial cases

if (The_month > 0) and (The_month < 13) and (The_day > 0) and (The_day < 30) then

return true

end if

if (The_month < 1) or (The_month > 13) or (The_day < 1) or (The_day > 30) then

return false

end if

-- the_day is always 30 or month is 13, if code got to this point

-- leap years, Adar Rishon and Adar Sheni

if (The_month > 11) and ((The_year * 7 + 1) mod 19 < 7) then

-- shift 12 and 13 in leap years by -1.

-- Makes long months odd, short months even

set The_month to The_month - 1

end if

-- now, no month 13 should occur any longer.

if The_month = 13 then

return false

end if

-- case not Cheshvan or Kislev

if (The_month < 8) or (The_month > 9) then

if The_month mod 2 = 1 then -- long month

return true

else

return false

end if

end if

-- remaining cases are Cheshvan 30 or Kislev 30. Lenght of year needed

-- hence rosh_hashanah_cdn function used

set year_length to (rosh_hashanah_cdn(The_year + 1) - rosh_hashanah_cdn(The_year))

if (year_length = 355) or (year_length = 385) then -- both months are long

return true

end if

if (year_length = 353) or (year_length = 383) then -- both months are short

return false

end if

if The_month = 9 then -- remaining case: Kislev long, Cheshvan short

return true

else

return false

end if

end day_exists_heb


------------------



on day_exists_iso_week_cal(The_year, the_week, The_day)

-- requires is_long_iso_week_year

try

set The_year to The_year as integer

set the_week to the_week as integer

set The_day to The_day as integer

on error

error "Argument in date_exists_heb is no number" number 1700

end try

-- simple cases

if (the_week < 1) or (the_week > 53) then

return false

end if

if (The_day < 1) or (The_day > 7) then

return false

end if

if the_week < 53 then

return true

end if

return iso_week_year_long(The_year)

end day_exists_iso_week_cal