Épreuve pratique · 2026 · Sujet 3

Bac NSI pratique 2026 sujet 3 : Calendrier, années bissextiles et fichier iCalendar

L’application du sujet calcule des dates à partir d’un modèle simplifié de cycle régulier de 28 jours. Les phases sont les suivantes. Il s’agit d’un modèle pédagogique imposé pour programmer, pas d’un outil de prédiction médicale : la régularité et la durée ne sont pas universelles.

Dans ce sujet

Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.

Application

Calendrier, années bissextiles et fichier iCalendar

L’application du sujet calcule des dates à partir d’un modèle simplifié de cycle régulier de 28 jours. Les phases sont les suivantes. Il s’agit d’un modèle pédagogique imposé pour programmer, pas d’un outil de prédiction médicale : la régularité et la durée ne sont pas universelles.

PhaseNuméroJours du modèleDescription
Règles11 à 5Écoulement menstruel
Folliculaire26 à 13Développement d’un follicule
Ovulation314Libération de l’ovocyte
Lutéale415 à 28Développement de la muqueuse utérine

On représente une date valide du calendrier grégorien par (jour, mois, année), par exemple (7,9,2025). Une année est bissextile si elle est divisible par 4, sauf les multiples de 100 qui ne sont pas multiples de 400. Ainsi 2024 et 2000 le sont ;2100 ne l’est pas. Le module calendar est fourni et la lecture du calendrier de test utilise ics.

Le dossier comporte le sujet et les fichiers listés dans les téléchargements. Les codes de départ sont reproduits ci-dessous. Travaillez sur une copie. Les appels professeur signalés dans les questions servent à présenter votre démarche ; vous pouvez aussi demander de l’aide en cas de difficulté.

Code fourni : cycle_menstruel.py
import calendar

#############################################################################
# Écrire le code de la fonction est_bissextile de la question 1             #
#############################################################################


#############################################################################
# Écrire le code de la fonction determiner_phase de la question 2           #
#############################################################################


#############################################################################
# Fonctions fournies pour la question 3                                     #
#############################################################################
def jours_dans_mois(annee, mois):
    """Renvoie le nombre de jours dans un mois donné d'une année donnée.
       Utilise le module calendar pour gérer les années bissextiles."""
    if mois == 2:  # février
        return 29 if calendar.isleap(annee) else 28
    elif mois in [1, 3, 5, 7, 8, 10, 12]:
        return 31
    else:
        return 30


def ajouter_jours(date, nb_jours):
    """Ajoute nb_jours à une date donnée et renvoie la nouvelle date.
       La date est représentée par un tuple (jour, mois, année)."""
    jour, mois, annee = date
    jour = jour + nb_jours

    # Ajustement du jour et du mois si dépassement
    while jour > jours_dans_mois(annee, mois):
        jour = jour - jours_dans_mois(annee, mois)
        mois = mois + 1
        if mois > 12:  # passage à l'année suivante
            mois = 1
            annee = annee + 1

    return (jour, mois, annee)


def test_ajouter_jours():
    assert ajouter_jours((7, 9, 2025), 3) == (10, 9, 2025)

#############################################################################
# Fonction fournie pour la question 4                                       #
#############################################################################


def calendrier_cycles(date_regles):
    """Renvoie une chaîne de caractère contenant au format iCalendar, l'ensemble
    des dates de début de règles qui se présentent dans les 100 jours suivants 
    `date_regles`, date incluse.

    Hypothèse : cycle régulier de 28 jours. """

    cal_lignes = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:']

    date_courante = date_regles
    jours_ecoules = 0

    # On ajoute les dates tant que l'on ne dépasse pas 100 jours écoulés
    while jours_ecoules + 28 <= 100:
        jour, mois, annee = date_courante
        cal_lignes.append('BEGIN:VEVENT')
        cal_lignes.append('SUMMARY: Règles')
        date = str(annee)+str(mois)+str(jour)
        cal_lignes.append('DTSTART:'+date)
        cal_lignes.append('END:VEVENT')
        date_courante = ajouter_jours(date_courante, 28)
        jours_ecoules += 28

    cal_lignes.append('END:VCALENDAR')

    # La méthode join va renvoyer ici une unique chaîne contenant toutes les
    # chaînes de la liste séparées par des sauts de lignes.
    return '\n'.join(cal_lignes)


def test_calendrier_cycles():
    '''Crée un calendrier et le charge avec le module ics pour vérifier sa
    validité.

    Nécessite que le module ics soit présent sur la machine (pip install ics).
    '''
    from ics import Calendar
    c = calendrier_cycles((12, 3, 2026))
    print(c)
    cal = Calendar(c)
    print(cal.events)
RèglesPhase folliculairePhase lutéaleJour1Jour283 à5 jours (schéma du PDF)Ovulation14 jours
Schéma contextuel du PDF, modèle simplifié. Pour programmer la question 2, appliquer les intervalles explicites du tableau :1-5,6-13,14,15-28.

Question 1

#

Écrire est_bissextile, qui prend un entier représentant une année et renvoie le booléen correspondant à la règle de divisibilité donnée.

Indice

Traiter séparément les multiples de 400.

Comprendre la correction
def est_bissextile(annee):
    return annee % 400 == 0 or (annee % 4 == 0 and annee % 100 != 0)

Le premier terme protège les multiples de 400, même s’ils sont multiples de 100. Le second accepte les autres multiples de 4. Vérifier seulement la divisibilité par 4 échouerait en 2100.

assert est_bissextile(2024)
assert est_bissextile(2000)
assert not est_bissextile(2100)
assert not est_bissextile(2025)
Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 2

#

Écrire determiner_phase, qui reçoit un jour entier entre 1 et 28 inclus et renvoie le numéro de la phase. Une assertion doit garantir la borne de l’argument.

PythonPasser une frontière sans perdre un jourÉcrivez votre solution et mettez-la à l’épreuve

Écrivez determiner_phase(jour). Pour un entier de 1 à 28 : phase 1 du jour 1 au jour 5, phase 2 du 6 au 13, phase 3 le 14 et phase 4 du 15 au 28. Une assertion doit refuser les jours hors intervalle.

def determiner_phase(jour):
    # À vous de jouer
    pass

Les cas de test proposés :

  • Toute la première phase : Le jour 5 appartient encore à la première phase.
  • La seconde phase : Les jours 6 et 13 sont tous deux inclus.
  • Une phase d’un seul jour : Le jour 14 ne doit être absorbé par aucun intervalle voisin.
  • La dernière phase : Le jour 28 reste valide.
  • Refuser avant le cycle : L’assertion protège la borne inférieure.
  • Refuser après le cycle : L’assertion protège la borne supérieure.
Indice

Commencer par l’assertion, puis parcourir les intervalles dans l’ordre.

Comprendre la correction
def determiner_phase(jour):
    assert 1 <= jour <= 28
    if jour <= 5:
        return 1
    if jour <= 13:
        return 2
    if jour == 14:
        return 3
    return 4

L’ordre des tests rend inutiles certaines bornes inférieures : lorsque jour≤5 est faux, on sait déjà que jour≥6. Tester tous les jours de changement de phase permet de repérer une confusion entre < et≤.

assert [determiner_phase(j) for j in [1,5,6,13,14,15,28]] == [1,1,2,2,3,4,4]
Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 3

#

Compléter test_ajouter_jours avec au moins trois tests pertinents, chacun accompagné d’une justification. La fonction fournie ajoute un nombre de jours à une date.

Comprendre la correction
assert ajouter_jours((31, 1, 2026), 1) == (1, 2, 2026)  # mois de31 jours
assert ajouter_jours((28, 2, 2024), 1) == (29, 2, 2024)  # année bissextile
assert ajouter_jours((28, 2, 2100), 1) == (1, 3, 2100)  # exception séculaire
assert ajouter_jours((31, 12, 2025), 1) == (1, 1, 2026)  # changement d’année
assert ajouter_jours((7, 9, 2025), 0) == (7, 9, 2025)  # identité
assert ajouter_jours((1, 1, 2024), 366) == (1, 1, 2025)  # plusieurs mois

Ces tests ne répètent pas six additions au milieu d’un mois : ils ciblent six situations différentes. La boucle doit pouvoir franchir plusieurs mois et remettre le mois à 1 en augmentant l’année. La fonction fournie traite les ajouts positifs ou nuls ; elle ne prétend pas soustraire des jours.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Le format simplifié fourni contient BEGIN: VCALENDAR, VERSION:2.0, PRODID, les événements puis END: VCALENDAR. Chaque événement commence par BEGIN: VEVENT, contient DTSTART au format AAAAMMJJ et SUMMARY, puis END: VEVENT. Les mois et jours inférieurs à 10 doivent être complétés à gauche par 0.

Question 4

#

Le calendrier doit contenir les débuts de règles dans les 100 jours suivant la date initiale, date incluse. Observer l’invalidité avec test_calendrier_cycles, identifier le problème, proposer et implémenter une résolution.

Indice 1

Comparer la longueur de 2026312 à la longueur requise.

Indice 2

Lister les décalages successifs de 28 jours avant de relire la condition.

Comprendre la correction

La concaténation str(annee)+str(mois)+str(jour) ne remplit pas les zéros : le 12 mars 2026 devient 2026312 au lieu de 20260312. Les formats :02d imposent deux chiffres au mois et au jour. Un second défaut omet l’événement du jour 84 : la condition jours_ecoules+28≤100 arrête la boucle trop tôt. Il faut émettre les dates aux décalages 0,28,56 et 84. Voici la fonction complète, avec un identifiant de calendrier non vide et des événements de journée entière :

def calendrier_cycles(date_regles):
    from datetime import datetime, timezone
    from uuid import uuid4
    horodatage = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
    lignes = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//NSI//Cycles//FR']
    date_courante = date_regles
    jours_ecoules = 0
    while jours_ecoules < 100:
        jour, mois, annee = date_courante
        date_texte = f'{annee:04d}{mois:02d}{jour:02d}'
        lignes += ['BEGIN:VEVENT',
                   'UID:' + str(uuid4()) + '@nsi-exemple.invalid',
                   'DTSTAMP:' + horodatage,
                   'SUMMARY:Règles',
                   'DTSTART;VALUE=DATE:' + date_texte,
                   'END:VEVENT']
        date_courante = ajouter_jours(date_courante, 28)
        jours_ecoules += 28
    lignes.append('END:VCALENDAR')
    return '\r\n'.join(lignes) + '\r\n'

Pour le 12 mars 2026, les dates sont 20260312,20260409,20260507 et 20260604. La propriété VALUE=DATE indique une journée entière. Le test fourni permet de parser le fichier avec ics. La norme iCalendar complète exige également UID et DTSTAMP dans chaque événement : la fonction ci-dessus les fournit. Le format simplifié de l’énoncé les omet, et un parseur tolérant peut malgré cela accepter le fichier. UID identifie l’événement ; DTSTAMP indique son horodatage de création en UTC. Voir la RFC 5545, section 3.6.1.

texte = calendrier_cycles((12, 3, 2026))
dates = [ligne.split(':',1)[1] for ligne in texte.splitlines()
         if ligne.startswith('DTSTART')]
assert dates == ['20260312', '20260409', '20260507', '20260604']
test_calendrier_cycles()

Pour vérifier la borne de boucle, écrivez la table des décalages avant de programmer : 0, 28, 56, 84, 112. Les quatre premiers appartiennent à la fenêtre des 100 jours, le dernier non. La version initiale teste le décalage suivant avant d’enregistrer la date courante : lorsque le compteur vaut 84, elle teste déjà 112 et abandonne une date encore admissible. C’est un décalage logique d’une itération. Un calendrier peut donc être parfaitement lisible par un parseur tout en étant incomplet ; les tests de syntaxe et de contenu se complètent.

Trouver les deux défauts du calendrierUn atelier pour expérimenter

Choisissez une réponse puis confrontez votre intuition à l’explication. Vous pouvez modifier vos choix.

Lire les réponses et leurs explications
  1. Quel décalage appartient encore aux 100 jours ?

    84. 84&lt;100 : le quatrième événement est attendu.

  2. Quelle chaîne représente le 3 juillet 2026 ?

    20260703. Quatre chiffres, puis deux et deux.

Appuyez votre explication sur les valeurs et les conditions exactes du programme.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)
Explorer les frontières du calendrierUn atelier pour expérimenter

Changez l’année et le jour du cycle. La première partie vérifie la règle grégorienne, la seconde les intervalles du modèle simplifié.

Lire le résultat de l’expérience initiale

2100 : non bissextile ; jour 14 : phase 3.

Les deux décisions dépendent de bornes précises. Faites varier2100 vers2000, puis13 vers14 et15.

DivisibilitéRésultat
2100 %40
2100 %1000
2100 %400100

Une règle peut être simple et pourtant exiger plusieurs tests aux frontières.

Revoir les notions de cet exercice

Du sujet à la méthode

Votre prochaine séance de révision

  • Écrire les dates attendues avant de tester le programme.
  • Une chaîne bien formée et un ensemble complet d’événements sont deux propriétés distinctes.

Retrouver ces notions dans d’autres sujets

Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027

Énoncé : sujet 26_BCG_NSI_3 (PDF). Corrigé et explications pédagogiques proposés par Sofien.