SQLite: Get average weekdays between two dates - sqlite

I have a table called Trucks with two date columns: Arrival and Released. I can calculate the average number of days between those dates like so:
SELECT avg(julianday(released) - julianday(arrival))
FROM Trucks
However, I only want to count weekdays--that is, I want to ignore Saturdays and Sundays. Is there any way to do this in SQLite? I have seen solutions for more robust RDBMSs like Oracle and MSSQL, but none that work for SQLite.

The raw difference in days must be adjusted depending (only) on the week days of the arrival and released days:
rel arr|0 1 2 3 4 5 6
-------+-+-+-+-+-+-+-
0 |2 0 0 0 0 0 1
1 |2 2 0 0 0 0 1
2 |2 2 2 0 0 0 1
3 |2 2 2 2 0 0 1
4 |2 2 2 2 2 0 1
5 |2 2 2 2 2 2 1
6 |2 2 2 2 2 2 2
This number can be computed with a simple expression (here: the inner CASE expression):
SELECT
AVG(julianday(released) - julianday(arrival) -
CASE WHEN julianday(released) = julianday(arrival) THEN 0
ELSE (CAST((julianday(released) - julianday(arrival)) / 7 AS INTEGER) * 2
) +
CASE WHEN strftime('%w', arrival) <= strftime('%w', released) THEN 2
ELSE strftime('%w', arrival) = '6'
END
END)
FROM trucks
(A boolean expression like x='6' returns 0 or 1.)

Okay, I figured out a very messy solution using lots of nested CASE statements. It checks the weekday number of released and then the weekday number of arrival and does a calculation to figure out how many weeks have passed. After that, I add 0, 1, or 2 as a base number of weekend days that must have passed between those two days (ie from Friday to Monday is always +2 weekend days, even if less than a whole week has passed between the dates).
Here it is, in case anyone finds it useful. Quite possibly the ugliest SQL I have ever written. If anyone figures out a better way, please let me know.
(Edited for simplicity based on CL's feedback)
SELECT
avg(
julianday(released) - julianday(arrival) -
CASE WHEN julianday(released) = julianday(arrival) THEN 0 ELSE
CASE strftime('%w', released)
WHEN '0' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '1' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '2' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '3' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '4' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 0)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '5' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 1)
END
WHEN '6' THEN
CASE strftime('%w', arrival)
WHEN '0' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '1' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '2' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '3' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '4' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '5' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
WHEN '6' THEN ((cast((julianday(released)-julianday(arrival)) / 7 as int) * 2) + 2)
END
END
END
), avg(julianday(released)-julianday(arrival))
from trucks
(Note: the avg(julianday(released)-julianday(arrival)) at the end is just for testing purposes to show that the new calculated average is in fact less than a straight average of the difference between the two dates).

I found this post while searching to see if SQLite had a weekday function. If you use the top answer you're not likely to get a very solid value for days of the week. Here is the code I used for my project, which calculates workdays between ticket creation and closure in SpiceWorks:
SELECT t.id
,t.summary
,t.category
,u.email
,(CAST(strftime('%j', t.closed_at) as INTEGER) - CAST(strftime('%j', t.created_at) as INTEGER) - /*I can't figure it out, but julianday() wasn't giving me the correct distances for some numbers. I used the day of the year, instead, which resolved things as expected*/
CASE
WHEN CAST(strftime('%W', t.closed_at) as INTEGER) - CAST(strftime('%W', t.created_at) as INTEGER) = 0 THEN 0 /*If they are in the same week then there is no weekend to count. If you closed a ticket, that was opened on a Monday, on Saturday, then you must of worked on Saturday and therefore counts as work day the same as if you finished it on Monday*/
WHEN CAST(strftime('%w', t.created_at) as INTEGER) = 0 THEN (CAST(strftime('%W', t.closed_at) as INTEGER) - CAST(strftime('%W', t.created_at) as INTEGER)) * 2 - 1 /*If they made their ticket on Sunday, don't count that Sunday*/
WHEN CAST(strftime('%w', t.created_at) as INTEGER) = 6 THEN (CAST(strftime('%W', t.closed_at) as INTEGER) - CAST(strftime('%W', t.created_at) as INTEGER)) * 2 - 2 /*If they made their ticket on Saturday, don't count that Saturday*/
ELSE (CAST(strftime('%W', t.closed_at) as INTEGER) - CAST(strftime('%W', t.created_at) as INTEGER)) * 2 /*Ignoring the possibility for closed dates that happen before open dates, take the number of weeks between each date and assume each had a Saturday and Sunday within them*/
END) as 'Week Days Elapsed' /*This equation in full represents: Full_Distance_In_Days_Between - Number of weekend days calculated*/
FROM Tickets as t
LEFT JOIN Users as u
on t.created_by = u.id
WHERE strftime('%Y-%m', t.created_at) = strftime('%Y-%m', 'now')

Related

Sqlite INSERT INTO with a group_concat which contains a subselect

I need to take data and combine to insert into an table as a menu code. The menu code takes the form LNNLLLL (L = Letter and N=Number) and it is made up of MenU Cycle (hard-coded, one off project), Week Number (1-3), Day Number (1-7 - Monday=1), MealCode, Customer Type Code (3 Letters). The following SELECT query returns exactly what I want insert. The hardcoded date is a known date from which the week number and day number can be derived ie it is a Week 1 Monday.
SELECT group_concat('C' || CASE
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 <= 6 THEN 1
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 <= 13 THEN 2
ELSE 3
END ||
CASE WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (1, 8, 15) THEN 1
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (2, 9, 16) THEN 2
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (3, 10, 17) THEN 3
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (4, 11, 18) THEN 4
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (5, 12, 19) THEN 5
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (6, 13, 20) THEN 6
WHEN (CAST(julianday(mealdate) AS INT) - cast(julianday('2022-08-22') AS INT)) % 21 + 1 IN (7, 14, 21) THEN 7
END ||
MealCode ||
(SELECT CustTypeCodeNew FROM CustomerNew WHERE CustCodeNew LIKE SalesOrderNew.CustomerCode)) AS MenuCode
FROM SalesOrderNew
GROUP BY SalesOrderID
Here is some output from that (I've added MealDate and MealCode to the select just to give better context to the resulting MenuCode column which is the one that needs inserted.
MealDate MealCode MenuCode
2022-08-23 K C12KNRE
2022-08-23 K C12KRES
2022-08-23 K C12KSHO
2022-08-23 K C12KRES
2022-08-25 T C14TNRE
2022-08-25 T C14TRES
2022-08-25 L C14LNRE
2022-08-25 L C14LNRE
2022-08-25 T C14TNRE
The MenuCode column is currently filled with NULL values.
However if I put add a line above to INSERT INTO SalesOrderNew (MenuCode) I get this error Result: NOT NULL constraint failed: SalesOrderNew.CustomerCode. It does not seem to like the subselect that used within the group_contact. How do I go about resolving this?

SQLite how to convert BLOB byte to INT with built-in functions?

i have an sqlite database that contains a column with BLOB data.
these BLOB data are 4 byte width. i want to split the 4 bytes appart and convert each part to an integer value to calculate with it.
i found out, that i can use SUBSTR(val, start, length) to take the BLOB value appart. the result is still of type BLOB.
but how can i convert the BLOB/byte to an integer value?
is there a built-in function that can convert byte BLOB values to an integer?
or is there a way to convert a hex-string-value into an integer value, so i could play with HEX(val) or QUOTE(val)
CREATE TEMP TABLE IF NOT EXISTS test AS SELECT x'cafe1a7e' AS val;
SELECT (val)
, TYPEOF(val)
, HEX(val)
, QUOTE(val)
, TYPEOF(HEX(val))
, TYPEOF(QUOTE(val))
, CAST(val AS INT)
, CAST(HEX(val) AS INT)
, CAST(QUOTE(val) AS INT)
, SUBSTR(val, 1, 1)
, TYPEOF(SUBSTR(val, 1, 1))
, HEX(SUBSTR(val, 1, 1))
, HEX(SUBSTR(val, 2, 1))
, HEX(SUBSTR(val, 3, 2))
, val + val
, SUBSTR(val, 1, 1) + 1
, CAST(SUBSTR(val, 1, 1) AS INT)
FROM test;
DROP TABLE test;
You can convert one hex digit at a time using instr:
SELECT hex(b), n, printf("%04X", n)
FROM (SELECT b,
(instr("123456789ABCDEF", substr(hex(b), -1, 1)) << 0) |
(instr("123456789ABCDEF", substr(hex(b), -2, 1)) << 4) |
(instr("123456789ABCDEF", substr(hex(b), -3, 1)) << 8) |
(instr("123456789ABCDEF", substr(hex(b), -4, 1)) << 12) |
(instr("123456789ABCDEF", substr(hex(b), -5, 1)) << 16) |
(instr("123456789ABCDEF", substr(hex(b), -6, 1)) << 20) |
(instr("123456789ABCDEF", substr(hex(b), -7, 1)) << 24) |
(instr("123456789ABCDEF", substr(hex(b), -8, 1)) << 28) AS n
FROM (SELECT randomblob(4) AS b))
Example output:
D91F8E91|3642723985|D91F8E91
(Simplification of idea from [1].)
There is no built in function that I know of, so this is how I do it - if you know how many bytes you want to convert:
--creates table h2i with numbers 0 to 255 in hex and int
CREATE TEMP TABLE bits (bit INTEGER PRIMARY KEY);INSERT INTO bits VALUES (0);INSERT INTO bits VALUES (1);
CREATE TEMP TABLE h2i (h TEXT, i INT);
INSERT INTO h2i (h, i) SELECT printf('%02X',num),num FROM (SELECT b7.bit * 128 + b6.bit * 64 + b5.bit * 32 + b4.bit * 16 + b3.bit * 8 + b2.bit * 4 + b1.bit * 2 + b0.bit AS num FROM bits b7, bits b6, bits b5,bits b4, bits b3, bits b2, bits b1, bits b0) as nums;
SELECT
HEX(SUBSTR(val, 1, 1)),h2i0.i
,HEX(SUBSTR(val, 2, 1)),h2i1.i
,HEX(SUBSTR(val, 3, 2)),h2i2.i*256+h2i3.i
,HEX(SUBSTR(val, 1, 4)),h2i0.i*16777216+h2i1.i*65536+h2i2.i*256+h2i3.i
FROM test
JOIN h2i h2i0 ON h2i0.h=HEX(SUBSTR(val, 1, 1))
JOIN h2i h2i1 ON h2i1.h=HEX(SUBSTR(val, 2, 1))
JOIN h2i h2i2 ON h2i2.h=HEX(SUBSTR(val, 3, 1))
JOIN h2i h2i3 ON h2i3.h=HEX(SUBSTR(val, 4, 1))
;
#rayzinnz, thank you for the hint.
in the meantime i gave up.
i puzzled together a kind of a solution, but i never got it work to set the initial x'cafe1a7e' value from outside the WITH RECURSIVE construction.
WITH RECURSIVE fx(val_hex, val_int, iter) AS (
VALUES(HEX(x'cafe1a7e'), 0, 0)
UNION ALL
SELECT
SUBSTR(val_hex, 1, LENGTH(val_hex) - 1),
val_int + (
CASE SUBSTR(val_hex, -1)
WHEN '0' THEN 0
WHEN '1' THEN 1
WHEN '2' THEN 2
WHEN '3' THEN 3
WHEN '4' THEN 4
WHEN '5' THEN 5
WHEN '6' THEN 6
WHEN '7' THEN 7
WHEN '8' THEN 8
WHEN '9' THEN 9
WHEN 'A' THEN 10
WHEN 'B' THEN 11
WHEN 'C' THEN 12
WHEN 'D' THEN 13
WHEN 'E' THEN 14
WHEN 'F' THEN 15
ELSE 0
END << (iter * 4)
),
iter + 1
FROM fx
WHERE val_hex != ''
LIMIT 9
)
--SELECT * FROM fx
SELECT val_int FROM fx WHERE val_hex == ''
;
the BLOB value there is hardcoded.
maybe you find a way.

11- Proof method in PL/SQL

A Dutch bank account consists of 9 digits e.g.: 1334.36.915.
To check whether the bank account is valid we use the so called ‘11-proef’ (11-test).
In this test each digit is multiplied with its place in the row.The result of this multiplication is added up.
(1*9)+(3*8)+(3*7)+(4*6)+(3*5)+(6*4)+(9*3)+(1*2)+(5*1) = R
This result has to be dividable by 11. That means the remainder of the division must be 0.
If the R is dividable by 11 the bank account number is valid!
Can someone help with this question?
A Dutch bank account number does not consist of 9 digits anymore, we now use Iban Numbers. If you want to do a check on it, you should have a look at https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN and implement that check.
For now, you can however still to the 11-check on the last 9 or 10 digits, but it's not guaranteed that will still work for new bank accounts in the future.
If you still want to do a 11-check, you can create a function like this:
CREATE OR REPLACE FUNCTION elfproof (accountnummer IN varchar2)
RETURN VARCHAR2
AS
multiplier int:= 10;
outcome varchar2(10);
total int := 0;
BEGIN
FOR i IN 1 .. 9
LOOP
multiplier := multiplier - 1;
total := total + (multiplier * TO_NUMBER (SUBSTR (accountnummer, i, 1)));
END LOOP;
IF MOD (total, 11) = 0
THEN
outcome := 'good';
ELSE
outcome := 'bad';
END IF;
return outcome;
END;
This gives the remainder of division on 11:
SELECT
MOD (SUM (TO_NUMBER (SUBSTR (str, LEVEL, 1)) * (10 - LEVEL)), 11) remdiv11
FROM
(
SELECT
REPLACE ('1334.36.915', '.') str
FROM
DUAL
) d
CONNECT BY LEVEL <= LENGTH (str)
and IBAN check;) :
SELECT
DECODE (MOD (TO_NUMBER (LISTAGG (n, '') WITHIN GROUP (ORDER BY l)), 97), 1, 'OK', 'Fail') AS iban_check
FROM
(
SELECT
TO_CHAR (CASE
WHEN ASCII (c) >= 65 THEN ASCII (c) - 55
ELSE ASCII (c) - 48 END) n, c, l
FROM
(
SELECT
SUBSTR (str, LEVEL, 1) c, LEVEL l
FROM
(
SELECT
SUBSTR (s, 5) || SUBSTR (s, 1, 4) str
FROM
(
SELECT
REPLACE ('GB82 WEST 1234 5698 7654 32', ' ') s
FROM
DUAL
)
)
CONNECT BY LEVEL <= LENGTH (str)
)
)

Why don't WolframAlpha's and Sage's answers match?

Consider this input to WolframAlpha,
solve [ 0 = x^4 - 6*x^2 - 8*x*cos( (2*pi )/5 ) - 2*cos( (4*pi)/5) - 1 ]
The solutions it gives are,
{x == (1 - Sqrt[5])/2 || x == (3 + Sqrt[5])/2 || x == (-2 - Sqrt[2 (5 - Sqrt[5])])/2 || x == (-2 + Sqrt[2 (5 - Sqrt[5])])/2}
But the same equation on sage gives the roots,
h(x) = x^4 - 6*x^2 - 8*x*cos( (2*pi )/5 ) - 2*cos( (4*pi)/5) - 1
h(x).solve(x)
[x == -1/2*sqrt(-2*sqrt(5) + 10) - 1, x == 1/2*sqrt(-2*sqrt(5) + 10) -
1, x == -1/2*sqrt(2*sqrt(5) + 6) + 1, x == 1/2*sqrt(2*sqrt(5) + 6) + 1]
It seems that the first two roots given by WolframAlpha differ from the last two roots given by Sage.
Why?
They're not different; they're exactly the same, simply listed in a different order.
sage: h(x) = x^4 - 6*x^2 - 8*x*cos( (2*pi )/5 ) - 2*cos( (4*pi)/5) - 1
sage: sols = h(x).solve(x, solution_dict=True)
sage: [CC(d[x]) for d in sols]
[-2.17557050458495, 0.175570504584946, -0.618033988749895, 2.61803398874989]
sage: wa = [ (1 - sqrt(5))/2 , (3 + sqrt(5))/2 , (-2 - sqrt(2* (5 - sqrt(5))))/2 , (-2 + sqrt(2* (5 - sqrt(5))))/2 ]
sage: [CC(v) for v in wa]
[-0.618033988749895, 2.61803398874989, -2.17557050458495, 0.175570504584946]

Function to return date of Easter for the given year

So, here's a funny little programming challenge. I was writing a quick method to determine all the market holidays for a particular year, and then I started reading about Easter and discovered just how crazy* the logic is for determining its date--the first Sunday after the Paschal Full Moon following the spring equinox! Does anybody know of an existing function to calculate the date of Easter for a given year?
Granted, it's probably not all that hard to do; I just figured I'd ask in case somebody's already done this. (And that seems very likely.)
UPDATE: Actually, I'm really looking for the date of Good Friday (the Friday before Easter)... I just figured Easter would get me there. And since I'm in the U.S., I assume I'm looking for the Catholic Easter? But perhaps someone can correct me on that if I'm wrong.
*By "crazy" I meant, like, involved. Not anything offensive...
Python: using dateutil's easter() function.
>>> from dateutil.easter import *
>>> print easter(2010)
2010-04-04
>>> print easter(2011)
2011-04-24
The functions gets, as an argument, the type of calculation you like:
EASTER_JULIAN = 1
EASTER_ORTHODOX = 2
EASTER_WESTERN = 3
You can pick the one relevant to the US.
Reducing two days from the result would give you Good Friday:
>>> from datetime import timedelta
>>> d = timedelta(days=-2)
>>> easter(2011)
datetime.date(2011, 4, 24)
>>> easter(2011)+d
datetime.date(2011, 4, 22)
Oddly enough, someone was iterating this, and published the results in Wikipedia's article about the algorithm:
in SQL Server Easter Sunday would look like this, scroll down for Good Friday
CREATE FUNCTION dbo.GetEasterSunday
( #Y INT )
RETURNS SMALLDATETIME
AS
BEGIN
DECLARE #EpactCalc INT,
#PaschalDaysCalc INT,
#NumOfDaysToSunday INT,
#EasterMonth INT,
#EasterDay INT
SET #EpactCalc = (24 + 19 * (#Y % 19)) % 30
SET #PaschalDaysCalc = #EpactCalc - (#EpactCalc / 28)
SET #NumOfDaysToSunday = #PaschalDaysCalc - (
(#Y + #Y / 4 + #PaschalDaysCalc - 13) % 7
)
SET #EasterMonth = 3 + (#NumOfDaysToSunday + 40) / 44
SET #EasterDay = #NumOfDaysToSunday + 28 - (
31 * (#EasterMonth / 4)
)
RETURN
(
SELECT CONVERT
( SMALLDATETIME,
RTRIM(#Y)
+ RIGHT('0'+RTRIM(#EasterMonth), 2)
+ RIGHT('0'+RTRIM(#EasterDay), 2)
)
)
END
GO
Good Friday is like this and it uses the Easter function above
CREATE FUNCTION dbo.GetGoodFriday
(
#Y INT
)
RETURNS SMALLDATETIME
AS
BEGIN
RETURN (SELECT dbo.GetEasterSunday(#Y) - 2)
END
GO
From here: http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
When it came for me to write this (traffic prediction based on day of week and holiday),
I gave up on trying to write it by myself. I found it somewhere on the net. The code was public domain, but...
sigh
see for yourself.
void dateOfEaster(struct tm* p)
{
int Y = p->tm_year;
int a = Y % 19;
int b = Y / 100;
int c = Y % 100;
int d = b / 4;
int e = b % 4;
int f = (b + 8) / 25;
int g = (b - f + 1) / 3;
int h = (19 * a + b - d - g + 15) % 30;
int i = c / 4;
int k = c % 4;
int L = (32 + 2 * e + 2 * i - h - k) % 7;
int m = (a + 11 * h + 22 * L) / 451;
p->tm_mon = ((h + L - 7 * m + 114) / 31 ) - 1;
p->tm_mday = ((h + L - 7 * m + 114) % 31) + 1;
p->tm_hour = 12;
const time_t tmp = mktime(p);
*p = *localtime(&tmp); //recover yday from mon+mday
}
Some questions are better left unasked.
I feel lucky that all moving holidays in my country are a fixed offset from the date of Easter.
The SQL Server function below is more general than the accepted answer
The accepted answer is only correct for the range (inclusive) : 1900-04-15 to 2099-04-12
It uses the algorithm provided by The United States Naval Observatory (USNO)
http://aa.usno.navy.mil/faq/docs/easter.php
CREATE FUNCTION dbo.GetEasterSunday (#Y INT)
RETURNS DATETIME
AS
BEGIN
-- Source of algorithm : http://aa.usno.navy.mil/faq/docs/easter.php
DECLARE #c INT = #Y / 100
DECLARE #n INT = #Y - 19 * (#Y / 19)
DECLARE #k INT = (#c - 17) / 25
DECLARE #i INT = #c - #c / 4 - (#c - #k) / 3 + 19 * #n + 15
SET #i = #i - 30 * (#i / 30)
SET #i = #i - (#i / 28) * (1 - (#i / 28) * (29 / (#i + 1)) * ((21 - #n) / 11))
DECLARE #j INT = #Y + #Y / 4 + #i + 2 - #c + #c / 4
SET #j = #j - 7 * (#j / 7)
DECLARE #l INT = #i - #j
DECLARE #m INT = 3 + (#l + 40) / 44
DECLARE #d INT = #l + 28 - 31 * (#m / 4)
RETURN
(
SELECT CONVERT
( DATETIME,
RTRIM(#Y)
+ RIGHT('0'+RTRIM(#m), 2)
+ RIGHT('0'+RTRIM(#d), 2)
)
)
END
GO
VB .NET Functions for Greek Orthodox and Catholic Easter:
Public Shared Function OrthodoxEaster(ByVal Year As Integer) As Date
Dim a = Year Mod 19
Dim b = Year Mod 7
Dim c = Year Mod 4
Dim d = (19 * a + 16) Mod 30
Dim e = (2 * c + 4 * b + 6 * d) Mod 7
Dim f = (19 * a + 16) Mod 30
Dim key = f + e + 3
Dim month = If((key > 30), 5, 4)
Dim day = If((key > 30), key - 30, key)
Return New DateTime(Year, month, day)
End Function
Public Shared Function CatholicEaster(ByVal Year As Integer) As DateTime
Dim month = 3
Dim a = Year Mod 19 + 1
Dim b = Year / 100 + 1
Dim c = (3 * b) / 4 - 12
Dim d = (8 * b + 5) / 25 - 5
Dim e = (5 * Year) / 4 - c - 10
Dim f = (11 * a + 20 + d - c) Mod 30
If f = 24 Then f += 1
If (f = 25) AndAlso (a > 11) Then f += 1
Dim g = 44 - f
If g < 21 Then g = g + 30
Dim day = (g + 7) - ((e + g) Mod 7)
If day > 31 Then
day = day - 31
month = 4
End If
Return New DateTime(Year, month, day)
End Function
The below code determines Easter through powershell:
function Get-DateOfEaster {
param(
[Parameter(ValueFromPipeline)]
$theYear=(Get-Date).Year
)
if($theYear -lt 1583) {
return $null
} else {
# Step 1: Divide the theYear by 19 and store the
# remainder in variable A. Example: If the theYear
# is 2000, then A is initialized to 5.
$a = $theYear % 19
# Step 2: Divide the theYear by 100. Store the integer
# result in B and the remainder in C.
$c = $theYear % 100
$b = ($theYear -$c) / 100
# Step 3: Divide B (calculated above). Store the
# integer result in D and the remainder in E.
$e = $b % 4
$d = ($b - $e) / 4
# Step 4: Divide (b+8)/25 and store the integer
# portion of the result in F.
$f = [math]::floor(($b + 8) / 25)
# Step 5: Divide (b-f+1)/3 and store the integer
# portion of the result in G.
$g = [math]::floor(($b - $f + 1) / 3)
# Step 6: Divide (19a+b-d-g+15)/30 and store the
# remainder of the result in H.
$h = (19 * $a + $b - $d - $g + 15) % 30
# Step 7: Divide C by 4. Store the integer result
# in I and the remainder in K.
$k = $c % 4
$i = ($c - $k) / 4
# Step 8: Divide (32+2e+2i-h-k) by 7. Store the
# remainder of the result in L.
$l = (32 + 2 * $e + 2 * $i - $h - $k) % 7
# Step 9: Divide (a + 11h + 22l) by 451 and
# store the integer portion of the result in M.
$m = [math]::floor(($a + 11 * $h + 22 * $l) / 451)
# Step 10: Divide (h + l - 7m + 114) by 31. Store
# the integer portion of the result in N and the
# remainder in P.
$p = ($h + $l - 7 * $m + 114) % 31
$n = (($h + $l - 7 * $m + 114) - $p) / 31
# At this point p+1 is the day on which Easter falls.
# n is 3 for March and 4 for April.
$DateTime = New-Object DateTime $theyear, $n, ($p+1), 0, 0, 0, ([DateTimeKind]::Utc)
return $DateTime
}
}
$eastersunday=Get-DateOfEaster 2015
Write-Host $eastersunday
Found this Excel formula somewhere
Assuming cell A1 contains year e.g. 2020
ROUND(DATE(A1;4;1)/7+MOD(19*MOD(A1;19)-7;30)*0,14;0)*7-6
Converted to T-SQL lead me to this:
DECLARE #yr INT=2020
SELECT DATEADD(dd, ROUND(DATEDIFF(dd, '1899-12-30', DATEFROMPARTS(#yr, 4, 1)) / 7.0 + ((19.0 * (#yr % 19) - 7) % 30) * 0.14, 0) * 7.0 - 6, -2)
In JS, taken from here.
var epoch=2444238.5,elonge=278.83354,elongp=282.596403,eccent=.016718,sunsmax=149598500,sunangsiz=.533128,mmlong=64.975464,mmlongp=349.383063,mlnode=151.950429,minc=5.145396,mecc=.0549,mangsiz=.5181,msmax=384401,mparallax=.9507,synmonth=29.53058868,lunatbase=2423436,earthrad=6378.16,PI=3.141592653589793,epsilon=1e-6;function sgn(x){return x<0?-1:x>0?1:0}function abs(x){return x<0?-x:x}function fixAngle(a){return a-360*Math.floor(a/360)}function toRad(d){return d*(PI/180)}function toDeg(d){return d*(180/PI)}function dsin(x){return Math.sin(toRad(x))}function dcos(x){return Math.cos(toRad(x))}function toJulianTime(date){var year,month,day;year=date.getFullYear();var m=(month=date.getMonth()+1)>2?month:month+12,y=month>2?year:year-1,d=(day=date.getDate())+date.getHours()/24+date.getMinutes()/1440+(date.getSeconds()+date.getMilliseconds()/1e3)/86400,b=isJulianDate(year,month,day)?0:2-y/100+y/100/4;return Math.floor(365.25*(y+4716)+Math.floor(30.6001*(m+1))+d+b-1524.5)}function isJulianDate(year,month,day){if(year<1582)return!0;if(year>1582)return!1;if(month<10)return!0;if(month>10)return!1;if(day<5)return!0;if(day>14)return!1;throw"Any date in the range 10/5/1582 to 10/14/1582 is invalid!"}function jyear(td,yy,mm,dd){var z,f,alpha,b,c,d,e;return f=(td+=.5)-(z=Math.floor(td)),b=(z<2299161?z:z+1+(alpha=Math.floor((z-1867216.25)/36524.25))-Math.floor(alpha/4))+1524,c=Math.floor((b-122.1)/365.25),d=Math.floor(365.25*c),e=Math.floor((b-d)/30.6001),{day:Math.floor(b-d-Math.floor(30.6001*e)+f),month:Math.floor(e<14?e-1:e-13),year:Math.floor(mm>2?c-4716:c-4715)}}function jhms(j){var ij;return j+=.5,ij=Math.floor(86400*(j-Math.floor(j))+.5),{hour:Math.floor(ij/3600),minute:Math.floor(ij/60%60),second:Math.floor(ij%60)}}function jwday(j){return Math.floor(j+1.5)%7}function meanphase(sdate,k){var t,t2;return 2415020.75933+synmonth*k+1178e-7*(t2=(t=(sdate-2415020)/36525)*t)-155e-9*(t2*t)+33e-5*dsin(166.56+132.87*t-.009173*t2)}function truephase(k,phase){var t,t2,t3,pt,m,mprime,f,apcor=!1;if(pt=2415020.75933+synmonth*(k+=phase)+1178e-7*(t2=(t=k/1236.85)*t)-155e-9*(t3=t2*t)+33e-5*dsin(166.56+132.87*t-.009173*t2),m=359.2242+29.10535608*k-333e-7*t2-347e-8*t3,mprime=306.0253+385.81691806*k+.0107306*t2+1236e-8*t3,f=21.2964+390.67050646*k-.0016528*t2-239e-8*t3,phase<.01||abs(phase-.5)<.01?(pt+=(.1734-393e-6*t)*dsin(m)+.0021*dsin(2*m)-.4068*dsin(mprime)+.0161*dsin(2*mprime)-4e-4*dsin(3*mprime)+.0104*dsin(2*f)-.0051*dsin(m+mprime)-.0074*dsin(m-mprime)+4e-4*dsin(2*f+m)-4e-4*dsin(2*f-m)-6e-4*dsin(2*f+mprime)+.001*dsin(2*f-mprime)+5e-4*dsin(m+2*mprime),apcor=!0):(abs(phase-.25)<.01||abs(phase-.75)<.01)&&(pt+=(.1721-4e-4*t)*dsin(m)+.0021*dsin(2*m)-.628*dsin(mprime)+.0089*dsin(2*mprime)-4e-4*dsin(3*mprime)+.0079*dsin(2*f)-.0119*dsin(m+mprime)-.0047*dsin(m-mprime)+3e-4*dsin(2*f+m)-4e-4*dsin(2*f-m)-6e-4*dsin(2*f+mprime)+.0021*dsin(2*f-mprime)+3e-4*dsin(m+2*mprime)+4e-4*dsin(m-2*mprime)-3e-4*dsin(2*m+mprime),pt+=phase<.5?.0028-4e-4*dcos(m)+3e-4*dcos(mprime):4e-4*dcos(m)-.0028-3e-4*dcos(mprime),apcor=!0),!apcor)throw"Error calculating moon phase!";return pt}function phasehunt(sdate,phases){var adate,k1,k2,nt1,nt2,yy,mm,dd,jyearResult=jyear(adate=sdate-45,yy,mm,dd);for(yy=jyearResult.year,mm=jyearResult.month,dd=jyearResult.day,adate=nt1=meanphase(adate,k1=Math.floor(12.3685*(yy+1/12*(mm-1)-1900)));nt2=meanphase(adate+=synmonth,k2=k1+1),!(nt1<=sdate&&nt2>sdate);)nt1=nt2,k1=k2;return phases[0]=truephase(k1,0),phases[1]=truephase(k1,.25),phases[2]=truephase(k1,.5),phases[3]=truephase(k1,.75),phases[4]=truephase(k2,0),phases}function kepler(m,ecc){var e,delta;e=m=toRad(m);do{e-=(delta=e-ecc*Math.sin(e)-m)/(1-ecc*Math.cos(e))}while(abs(delta)>epsilon);return e}function getMoonPhase(julianDate){var Day,N,M,Ec,Lambdasun,ml,MM,MN,Ev,Ae,MmP,mEc,lP,lPP,NP,y,x,MoonAge,MoonPhase,MoonDist,MoonDFrac,MoonAng,F,SunDist,SunAng;return N=fixAngle(360/365.2422*(Day=julianDate-epoch)),Ec=kepler(M=fixAngle(N+elonge-elongp),eccent),Ec=Math.sqrt((1+eccent)/(1-eccent))*Math.tan(Ec/2),Lambdasun=fixAngle((Ec=2*toDeg(Math.atan(Ec)))+elongp),F=(1+eccent*Math.cos(toRad(Ec)))/(1-eccent*eccent),SunDist=sunsmax/F,SunAng=F*sunangsiz,ml=fixAngle(13.1763966*Day+mmlong),MM=fixAngle(ml-.1114041*Day-mmlongp),MN=fixAngle(mlnode-.0529539*Day),MmP=MM+(Ev=1.2739*Math.sin(toRad(2*(ml-Lambdasun)-MM)))-(Ae=.1858*Math.sin(toRad(M)))-.37*Math.sin(toRad(M)),lPP=(lP=ml+Ev+(mEc=6.2886*Math.sin(toRad(MmP)))-Ae+.214*Math.sin(toRad(2*MmP)))+.6583*Math.sin(toRad(2*(lP-Lambdasun))),NP=MN-.16*Math.sin(toRad(M)),y=Math.sin(toRad(lPP-NP))*Math.cos(toRad(minc)),x=Math.cos(toRad(lPP-NP)),toDeg(Math.atan2(y,x)),NP,toDeg(Math.asin(Math.sin(toRad(lPP-NP))*Math.sin(toRad(minc)))),MoonAge=lPP-Lambdasun,MoonPhase=(1-Math.cos(toRad(MoonAge)))/2,MoonDist=msmax*(1-mecc*mecc)/(1+mecc*Math.cos(toRad(MmP+mEc))),MoonAng=mangsiz/(MoonDFrac=MoonDist/msmax),mparallax/MoonDFrac,{moonIllumination:MoonPhase,moonAgeInDays:synmonth*(fixAngle(MoonAge)/360),distanceInKm:MoonDist,angularDiameterInDeg:MoonAng,distanceToSun:SunDist,sunAngularDiameter:SunAng,moonPhase:fixAngle(MoonAge)/360}}function getMoonInfo(date){return null==date?{moonPhase:0,moonIllumination:0,moonAgeInDays:0,distanceInKm:0,angularDiameterInDeg:0,distanceToSun:0,sunAngularDiameter:0}:getMoonPhase(toJulianTime(date))}function getEaster(year){var previousMoonInfo,moonInfo,fullMoon=new Date(year,2,21),gettingDarker=void 0;do{previousMoonInfo=getMoonInfo(fullMoon),fullMoon.setDate(fullMoon.getDate()+1),moonInfo=getMoonInfo(fullMoon),void 0===gettingDarker?gettingDarker=moonInfo.moonIllumination<previousMoonInfo.moonIllumination:gettingDarker&&moonInfo.moonIllumination>previousMoonInfo.moonIllumination&&(gettingDarker=!1)}while(gettingDarker&&moonInfo.moonIllumination<previousMoonInfo.moonIllumination||!gettingDarker&&moonInfo.moonIllumination>previousMoonInfo.moonIllumination);for(fullMoon.setDate(fullMoon.getDate()-1);0!==fullMoon.getDay();)fullMoon.setDate(fullMoon.getDate()+1);return fullMoon}
Then run getEaster(2020); // -> Sun Apr 12 2020

Resources