web-dev-qa-db-fra.com

Quels sont quelques exemples de bugs des années bissextiles?

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:

  • Les années bissextiles ont un 29 février, contrairement aux années ordinaires.
  • Les années bissextiles totalisent 366 jours, tandis que les années ordinaires n'en comptent que 365.

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:

  • Catégorie 1 : celles qui conduisent à des conditions d'erreur, telles que des exceptions, des codes de retour d'erreur, des variables non initialisées ou des boucles sans fin
  • Catégorie 2 : celles qui conduisent à des données incorrectes, telles que des problèmes ponctuels dans les requêtes de plage ou d'agrégation

À 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.

17
Matt Johnson-Pint

.NET/C # - Construction à partir de pièces de date

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);

Variation courante - Anniversaires (et autres anniversaires)

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);
6
Matt Johnson-Pint

Win32/C++ - SYSTEMTIME manipulation de structure

Caté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.

3
Matt Johnson-Pint

Python - Remplacement de l'année

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)
3
Matt Johnson-Pint

Déterminer si une année est une année bissextile

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);
1
Matt Johnson-Pint

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.

0
rspring1975

JavaScript - Ajout d'année (s)

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);

Variation courante - Formulaire immuable

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);

Bibliothèques

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.

0
Matt Johnson-Pint