Comme nous sommes en période de Pâques, je me suis souvenu que PHP propose une fonction pour retourner la date de Pâques à partir de l’année passée en argument. Et je me suis fait également la réflexion que MySQL 8 ne propose pas un équivalent. J’ai donc été voir ce que propose le monde libre à ce sujet et du coup, j’en profite pour coder une fonction afin d’avoir les congés en France en fonction de la date fournie.


Partager l’article Pâques et autres congés avec MySQL sur les réseaux sociaux


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 !

Photo de Pixabay sur Pexels