Un bogue d'année bissextile est un défaut de code qui crée un résultat problématique et imprévu lorsqu'il est exécuté dans le contexte d'un année bissextile , généralement dans le système de calendrier grégorien proleptique.
La dernière année bissextile était 2016. Les prochaines années bissextiles sont 2020 et 2024.
Il existe deux attributs propres aux années bissextiles:
Ce message est destiné à aider les autres à comprendre la nature des bogues des années bissextiles, à quoi ils ressemblent dans différentes langues et comment les corriger.
Les bogues des années bissextiles se répartissent généralement en deux catégories d'impact:
À chaque réponse, veuillez indiquer le langage de programmation et/ou la plateforme, ainsi que la catégorie d'impact définie ci-dessus. (Veuillez suivre le modèle utilisé par les réponses existantes.)
Veuillez créer une réponse distincte par langue et type de défaut, et votez pour votre préférée, en particulier celles que vous avez personnellement rencontrées (en laissant des commentaires avec des anecdotes si possible).
Je vais semer quelques réponses pour commencer et mettre à jour avec des exemples supplémentaires au fil du temps.
Catégorie d'impact 1
Code défectueux
DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);
Ce code fonctionnera correctement jusqu'à ce que dt
devienne le 29 février. Ensuite, il tentera de créer un 29 février d'une année commune, qui n'existe pas. Le constructeur DateTime
lancera un ArgumentOutOfRangeException
.
Les variations incluent toute forme de constructeur DateTime
ou DateTimeOffset
qui accepte les paramètres année, mois et jour, lorsque ces valeurs sont dérivées de différentes sources ou manipulées sans égard à la validité dans son ensemble.
Code corrigé
DateTime dt = DateTime.Now;
DateTime result = dt.AddYears(1);
Une variante consiste à déterminer l'anniversaire actuel d'un utilisateur sans tenir compte des sauts (personnes nées le 29 février). Elle s'applique également à d'autres types d'anniversaires, tels que la date de location, la date de service, la date de facturation, etc.
Code défectueux
DateTime birthdayThisYear = new DateTime(DateTime.Now.Year, dob.Month, dob.Day);
Cette approche doit être ajustée, comme la suivante qui utilise le 28 février pour les années communes. (Cependant, le 1er mars peut être préféré selon le cas d'utilisation.)
Code corrigé
int year = DateTime.Now.Year;
int month = dob.Month;
int day = dob.Day;
if (month == 2 && day == 29 && !DateTime.IsLeapYear(year))
day--;
DateTime birthdayThisYear = new DateTime(year, month, day);
Code corrigé (implémentation alternative)
DateTime birthdayThisYear = dob.AddYears(DateTime.Now.Year - dob.Year);
SYSTEMTIME
manipulation de structureCatégorie d'impact 1
Code défectueux
SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);
st.wYear++;
SystemTimeToFileTime(&st, &ft);
Ce code fonctionnera correctement jusqu'à ce que st
devienne le 29 février. Ensuite, il tentera de créer un 29 février d'une année commune, qui n'existe pas. Passer ceci à n'importe quelle fonction qui accepte une structure SYSTEMTIME
échouera probablement.
Par exemple, l'appel SystemTimeToFileTime
affiché ici renverra un code d'erreur. Puisque cette valeur de retour n'est pas cochée (ce qui est extrêmement courant), cela entraînera que ft
ne sera pas initialisé.
Code corrigé
SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);
st.wYear++;
bool isLeapYear = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
st.wDay = st.wMonth == 2 && st.wDay == 29 && !isLeapYear ? 28 : st.wDay;
bool ok = SystemTimeToFileTime(&st, &ft);
if (!ok)
{
// handle error
}
Ce correctif vérifie le 29 février d'une année commune et le corrige au 28 février.
Impact Catégorie 1
Code défectueux
from datetime import date
today = date.today()
later = today.replace(year = today.year + 1)
Ce code fonctionnera correctement jusqu'à ce que today
devienne le 29 février. Ensuite, il tentera de créer un 29 février d'une année commune, qui n'existe pas. Le constructeur date
lèvera un ValueError
avec le message "day is out of range for month"
.
Variations:
from datetime import date
today = date.today()
later = date(today.year + 1, today.month, today.day)
Code corrigé
Sans utiliser de bibliothèques supplémentaires, on peut intercepter l'erreur:
from datetime import date
today = date.today()
try:
later = today.replace(year = today.year + 1)
except ValueError:
later = date(today.year + 1, 2, 28)
Cependant, il est généralement préférable d'utiliser une bibliothèque telle que dateutil :
from datetime import date
from dateutil.relativedelta import relativedelta
today = date.today()
later = today + relativedelta(years=1)
C # est utilisé dans les exemples pour illustrer, mais le modèle est commun à tous les langages.
Impact catégorie 2
Code défectueux
bool isLeapYear = year % 4 == 0;
Ce code suppose à tort qu'une année bissextile se produit exactement tous les quatre ans. Ce n'est pas le cas avec le système de calendrier grégorien que nous utilisons le plus souvent en entreprise et en informatique.
Code corrigé
L'algorithme complet ( de Wikipedia ) est le suivant:
si ( année n'est pas divisible par 4) alors (c'est une année commune)
sinon si ( année n'est pas divisible par 100) puis (c'est une année bissextile)
sinon si ( année n'est pas divisible par 400) puis (c'est une année commune)
else (c'est une année bissextile)
Une implémentation de cet algorithme est la suivante:
bool isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
Dans de nombreuses plates-formes, cette fonction est intégrée. Par exemple, dans .Net, ce qui suit est préférable:
bool isLeapYear = DateTime.IsLeapyear(year);
En voici une intéressante que je viens de rencontrer. Juste un autre argument pour utiliser UTC autant que possible.
// **** Appears to "Leap" forward **** //
moment('2020-02-29T00:00:00Z').toISOString();
// or
moment('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z
moment('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-03-01T00:00:00.000Z
// **** Falls back **** //
moment.utc('2020-02-29T00:00:00Z').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-02-28T00:00:00.000Z
Étant donné que le comportement par défaut de C # est de se replier ...
DateTime dt = new DateTime(2020, 02, 29, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.AddYears(1);
// 2021-02-28T00:00:00.0000000Z
Cela pourrait être crucial pour s'assurer que le front et le back-end conviennent de se replier ou de sauter un jour.
Impact catégorie 2
Code défectueux
var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);
Ce code fonctionnera correctement jusqu'à ce que dt
devienne le 29 février, comme sur 2020-02-29
. Il tentera ensuite de régler l'année sur 2021. Depuis 2021-02-29
n'existe pas, l'objet Date
sera reporté à la prochaine date valide, qui est 2020-03-01
.
Cela peut être le comportement souhaité dans certains cas. Dans d'autres cas, un jour de congé peut avoir un impact négligeable (comme une date d'expiration).
Cependant, dans de nombreux cas, l'intention de progresser d'une année est de rester à peu près dans la même position pendant le mois et le jour. En d'autres termes, si vous avez commencé fin février (2020-02-29
) et avancé d'un an, vous souhaitiez probablement que le résultat soit également fin février (2021-02-28
), pas début mars (2021-03-01
).
Code corrigé
Pour ajouter des années en JavaScript tout en conservant un comportement de fin février, utilisez la fonction suivante.
function addYears(dt, n) {
var m = dt.getMonth();
dt.setFullYear(dt.getFullYear() + n);
if (dt.getMonth() !== m)
dt.setDate(dt.getDate() - 1);
}
// example usage
addYears(dt, 1);
On peut souvent avoir du code qui ne mute pas l'objet d'origine, tel que le suivant:
Code défectueux
var dt = new Date();
var result = new Date(dt.getFullYear() + 1, dt.getMonth(), dt.getDate());
La forme immuable qui conserve le comportement de fin février est la suivante:
Code corrigé
function addYears(dt, n) {
var result = new Date(dt);
result.setFullYear(result.getFullYear() + n);
if (result.getMonth() !== dt.getMonth())
result.setDate(result.getDate() - 1);
return result;
}
// example usage
var result = addYears(dt, 1);
JavaScript possède de nombreuses bibliothèques de date/heure, telles que Luxon , Date-Fns , Moment , et js-Joda . Toutes ces bibliothèques utilisent déjà un comportement de fin février pour leurs fonctions d'ajout d'années. Aucun ajustement supplémentaire n'est nécessaire à moins que vous ne souhaitiez plutôt un comportement début mars.