Prérequis
Tout ce qui suit a été réalisé sous MySQL 8.
Une petite recherche
Une recherche repide sur le Net permet d’identifier deux fonctions candidates intéressantes pour le calcul de la date de Pâques en Occident (désolés les Orthodoxes, je penserai à vous possiblement dans un futur article) sous MySQL.
La première, sous licence BSD, par Jan Weiher dans le dépôt Github https://github.com/jweiher/mysql-easter.
La voici, avec un petit ajout de ma part, car il faut préciser si la fonction est déterministe ou non, vu que son code est ancien, il ne contenait pas ce mot-clé :
delimiter $$
CREATE FUNCTION `easter_f`(X INTEGER) RETURNS date deterministic
BEGIN
DECLARE K,M,S,A,D,R,OG,SZ,OE,OS INT;
DECLARE EASTERDATE DATE;
SET K = X DIV 100;
SET M = 15 + (3*K + 3) DIV 4 - (8*K + 13) DIV 25;
SET S = 2 - (3*K + 3) DIV 4;
SET A = X MOD 19;
SET D = (19*A + M) MOD 30;
SET R = (D + A DIV 11) DIV 29;
SET OG = 21 + D - R ;
SET SZ = 7 - (X + X DIV 4 + S) MOD 7;
SET OE = 7 - (OG - SZ) MOD 7;
SET OS = OG + OE;
SET EASTERDATE = date_add(concat(X, '-03-01'), INTERVAL OS-1 DAY);
RETURN EASTERDATE;
END$$
delimiter ;
La fonction se crée bien sous MySQL 8 et s’exécute bien.
La seconde est celle de Scott Noyes sur Planet MySQL,
dont j’ai changé le typage YEAR
en INTEGER
pour ne pas se limiter à
l’intervalle d’années 1901 à 2155 :
DROP function easter;
DELIMITER //
CREATE FUNCTION easter(inYear INTEGER) RETURNS DATE DETERMINISTIC
BEGIN
DECLARE a, b, c, d, e, k, m, n, p, q INT;
DECLARE easter DATE;
SET k = FLOOR(inYear / 100);
SET a = MOD(inYear, 19);
SET b = MOD(inYear, 4);
SET c = MOD(inYear, 7);
SET q = FLOOR(k / 4);
SET p = FLOOR((13 + 8 * k) / 25);
SET m = MOD((15-p+k-q), 30);
SET d = MOD((19 * a + m), 30);
SET n = MOD((4+k-q), 7);
SET e = MOD((2*b+4*c+6*d+n), 7);
SET easter = CASE
WHEN d + e <= 9 THEN CONCAT_WS('-', inYear, '03', 22 + d + e)
WHEN d = 29 AND e = 6 THEN CONCAT_WS('-', inYear, '04-19')
WHEN d = 28 AND e = 6 AND a > 10 THEN CONCAT_WS('-', inYear, '04-18')
ELSE CONCAT_WS('-', inYear, '04', LPAD(d + e - 9, 2, 0))
END;
RETURN easter;
END
//
DELIMITER ;
Elle aussi s’installe et s’exécute correctement sur MySQL 8.
Voyons maintenant s’il y a des différences (bugs donc) entre les deux en les testant à partir de l’année 1200 et pour les 1000 ans suivant :
with recursive cte_years as (
select 1200 y, 1 inc
union all
select y + 1 y, inc + 1 inc from cte_years where inc < 1000
),
cte_compare as (
select easter_f(y) = easter(y) test from cte_years
)
select * from cte_compare where test = 0;
Ce qui donne :
Empty set (0,06 sec)
OK, même résultat. Voyons en terme de performance maintenant :
with recursive cte_years as (
select 1200 y, 1 inc
union all
select y + 1 y, inc + 1 inc from cte_years where inc < 1000
),
cte_compare as (
select easter_f(y) from cte_years
)
select * from cte_compare;
La première fonction donne un temps d’exécution de 0,03 ms.
with recursive cte_years as (
select 1200 y, 1 inc
union all
select y + 1 y, inc + 1 inc from cte_years where inc < 1000
),
cte_compare as (
select easter(y) from cte_years
)
select * from cte_compare;
La deuxième met 0,04 ms. Petite différence, mais cela peut jouer pour des appels fréquents. Je vais utiliser la première.
Exploitons ça !
Maintenant que nous avons une fonction toute prête pour nous retourner la date de Pâques pour une année donnée, on peut déterminer les dates d’autres jours fériés qui dépendent de Pâques :
- le lundi de Pâques, 1 jour après Pâques,
- l’Ascension, 39 jours après Pâques,
- et la Pentecôte, 49 jours après Pâques.
Pour les autres, ce sont des dates fixes :
- Jour de l’an, le permier janvier,
- Fête des Travailleurs, le premier mai,
- Victoire des Alliés, le 8 mai
- Fête Nationale, le 14 juillet,
- Assomption, le 15 août,
- Toussaint, le permier novembre
- Armistice, le 11 novembre
- et enfin Noël, le 25 décembre.
Notre fonction devra prendre une date en entrée, et en sortie donner le nom du
jour férié (en STRING
) ou NULL
si le jour ne correspond pas à un jour férié.
Un exemple d’implémentation pourrait être le suivant :
DELIMITER //
CREATE FUNCTION `is_fr_public_holiday`(someday DATE) RETURNS varchar(25)
DETERMINISTIC
BEGIN
-- oui c’est franco-français, alors les variables sont en français :)
DECLARE annee INTEGER;
DECLARE mois INTEGER;
DECLARE jour INTEGER;
DECLARE paques DATE;
DECLARE lundi_de_paques DATE;
DECLARE ascension DATE;
DECLARE pentecote DATE;
SET annee = YEAR(someday);
SET mois = MONTH(someday);
SET jour = DAY(someday);
-- on commence par les cas simples
IF mois = 1 AND jour = 1 THEN
RETURN 'Jour de l’an';
END IF;
IF mois = 5 AND jour = 1 THEN
RETURN 'Fête des Travailleurs';
END IF;
IF mois = 5 AND jour = 8 THEN
RETURN 'Victoire des Alliés';
END IF;
IF mois = 7 AND jour = 14 THEN
RETURN 'Fête Nationale';
END IF;
IF mois = 8 AND jour = 15 THEN
RETURN 'Assomption';
END IF;
IF mois = 11 AND jour = 1 THEN
RETURN 'Toussaint';
END IF;
IF mois = 11 AND jour = 11 THEN
RETURN 'Armistice';
END IF;
IF mois = 12 AND jour = 25 THEN
RETURN 'Noël';
END IF;
SET paques = (SELECT easter_f(annee));
SET lundi_de_paques = (SELECT date_add(paques, INTERVAL +1 DAY));
SET ascension = (SELECT date_add(paques, INTERVAL +39 DAY));
SET pentecote = (SELECT date_add(paques, INTERVAL +49 DAY));
IF someday = paques THEN
RETURN 'Pâques';
END IF;
IF someday = lundi_de_paques THEN
RETURN 'Lundi de Pâques';
END IF;
IF someday = ascension THEN
RETURN 'Ascension';
END IF;
IF someday = pentecote THEN
RETURN 'Pentecôte';
END IF;
RETURN NULL;
END //
DELIMITER ;
Testons avec la fête du moment :
SELECT is_fr_public_holiday('2022-04-17');
Qui donne :
+------------------------------------+
| is_fr_public_holiday('2022-04-17') |
+------------------------------------+
| Pâques |
+------------------------------------+
1 row in set (0,01 sec)
Le lundi de Pâque :
SELECT is_fr_public_holiday('2022-04-18');
+------------------------------------+
| is_fr_public_holiday('2022-04-18') |
+------------------------------------+
| Lundi de Pâques |
+------------------------------------+
1 row in set (0,00 sec)
Un jour quelconque :
SELECT is_fr_public_holiday('1978-01-14');
+------------------------------------+
| is_fr_public_holiday('1978-01-14') |
+------------------------------------+
| NULL |
+------------------------------------+
1 row in set (0,00 sec)
Ça ne semble pas mal non ?
Du coup, pour toute l’année courante, nous pouvons faire la requête suivante pour avoir les jours fériés ou non :
with recursive cte_current_year as (
select cast(concat(year(now()), '-01-01') as date) d
union all
select cast(date_add(d, interval +1 day) as date) d
from cte_current_year
where d < concat(year(now()), '-12-31')
),
cte_name as (
select d,
is_fr_public_holiday(d) holiday_name
from cte_current_year
)
select d,
holiday_name,
holiday_name is not null is_holiday
from cte_name
;
Ce qui nous donne, en tronqué :
+------------+------------------------+------------+
| d | holiday_name | is_holiday |
+------------+------------------------+------------+
| 2022-01-01 | Jour de l’an | 1 |
| 2022-01-02 | NULL | 0 |
| 2022-01-03 | NULL | 0 |
[...]
| 2022-04-15 | NULL | 0 |
| 2022-04-16 | NULL | 0 |
| 2022-04-17 | Pâques | 1 |
| 2022-04-18 | Lundi de Pâques | 1 |
| 2022-04-19 | NULL | 0 |
| 2022-04-20 | NULL | 0 |
[...]
| 2022-04-28 | NULL | 0 |
| 2022-04-29 | NULL | 0 |
| 2022-04-30 | NULL | 0 |
| 2022-05-01 | Fête des Travailleurs | 1 |
| 2022-05-02 | NULL | 0 |
| 2022-05-03 | NULL | 0 |
| 2022-05-04 | NULL | 0 |
| 2022-05-05 | NULL | 0 |
| 2022-05-06 | NULL | 0 |
| 2022-05-07 | NULL | 0 |
| 2022-05-08 | Victoire des Alliés | 1 |
| 2022-05-09 | NULL | 0 |
| 2022-05-10 | NULL | 0 |
[...]
| 2022-05-24 | NULL | 0 |
| 2022-05-25 | NULL | 0 |
| 2022-05-26 | Ascension | 1 |
| 2022-05-27 | NULL | 0 |
| 2022-05-28 | NULL | 0 |
[...]
| 2022-06-03 | NULL | 0 |
| 2022-06-04 | NULL | 0 |
| 2022-06-05 | Pentecôte | 1 |
| 2022-06-06 | NULL | 0 |
| 2022-06-07 | NULL | 0 |
[...]
| 2022-07-12 | NULL | 0 |
| 2022-07-13 | NULL | 0 |
| 2022-07-14 | Fête Nationale | 1 |
| 2022-07-15 | NULL | 0 |
| 2022-07-16 | NULL | 0 |
[...]
| 2022-08-13 | NULL | 0 |
| 2022-08-14 | NULL | 0 |
| 2022-08-15 | Assomption | 1 |
| 2022-08-16 | NULL | 0 |
| 2022-08-17 | NULL | 0 |
[...]
| 2022-10-30 | NULL | 0 |
| 2022-10-31 | NULL | 0 |
| 2022-11-01 | Toussaint | 1 |
| 2022-11-02 | NULL | 0 |
| 2022-11-03 | NULL | 0 |
[...]
| 2022-11-09 | NULL | 0 |
| 2022-11-10 | NULL | 0 |
| 2022-11-11 | Armistice | 1 |
| 2022-11-12 | NULL | 0 |
| 2022-11-13 | NULL | 0 |
[...]
| 2022-12-23 | NULL | 0 |
| 2022-12-24 | NULL | 0 |
| 2022-12-25 | Noël | 1 |
| 2022-12-26 | NULL | 0 |
| 2022-12-27 | NULL | 0 |
| 2022-12-28 | NULL | 0 |
| 2022-12-29 | NULL | 0 |
| 2022-12-30 | NULL | 0 |
| 2022-12-31 | NULL | 0 |
+------------+------------------------+------------+
365 rows in set (0,05 sec)
Calendrier des dates de jours fériés
La dernière requête est intéressante : intégrons-la dans une procédure stockée qui prend comme entrée l’année désirée :
DELIMITER //
CREATE PROCEDURE cal_fr_public_holiday(IN y YEAR)
BEGIN
with recursive cte_current_year as (
select cast(concat(y, '-01-01') as date) d
union all
select cast(date_add(d, interval +1 day) as date) d
from cte_current_year
where d < concat(y, '-12-31')
),
cte_name as (
select d,
is_fr_public_holiday(d) holiday_name
from cte_current_year
),
cte_filter as (
select d,
holiday_name,
holiday_name is not null is_holiday
from cte_name
)
select d `day`,
`holiday_name`
from cte_filter
where is_holiday = 1
;
END
DELIMITER ;
Remarquez que cette-fois-ci je signe la procédure avec le type YEAR
et non INTEGER
, car certaines fêtes n’ont pas de sens trop loin dans le passé (1er mai, Armistice, etc.)
Exécutons cette nouvelle procédure :
call cal_fr_public_holiday(2022);
Ce qui donne :
+------------+------------------------+
| day | holiday_name |
+------------+------------------------+
| 2022-01-01 | Jour de l’an |
| 2022-04-17 | Pâques |
| 2022-04-18 | Lundi de Pâques |
| 2022-05-01 | Fête des Travailleurs |
| 2022-05-08 | Victoire des Alliés |
| 2022-05-26 | Ascension |
| 2022-06-05 | Pentecôte |
| 2022-07-14 | Fête Nationale |
| 2022-08-15 | Assomption |
| 2022-11-01 | Toussaint |
| 2022-11-11 | Armistice |
| 2022-12-25 | Noël |
+------------+------------------------+
12 rows in set (0,03 sec)
Cela pourrait être affiné en tenant compte de l’année d’apparition de certains congés. Mais dans la majeure partie des cas, ce code suffira amplement.
Conclusion
Nous avons découvert la très pratique fonction de Jan Weiher retournant la date de Pâques pour une année donnée, puis nous avons créé une autre fonction l’utilisant pour déterminer si une date donnée correspond à un jour férié en France et enfin, nous avons une procédure stockée permettant d’avoir la liste des jours fériés en France pour une année donnée.
Vous n’avez plus d’excuse pour oublier vos jours fériés maintenant !