(*
© 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 theDate ≠ date) 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 theDate ≠ date) 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 theDate ≠ date) 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