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.
| Phase | Numéro | Jours du modèle | Description |
|---|---|---|---|
| Règles | 1 | 1 à 5 | Écoulement menstruel |
| Folliculaire | 2 | 6 à 13 | Développement d’un follicule |
| Ovulation | 3 | 14 | Libération de l’ovocyte |
| Lutéale | 4 | 15 à 28 | Dé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)
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)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 4L’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]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 moisCes 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.
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
- Quel décalage appartient encore aux 100 jours ?
84. 84<100 : le quatrième événement est attendu.
- 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.
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 %4 | 0 |
| 2100 %100 | 0 |
| 2100 %400 | 100 |
Une règle peut être simple et pourtant exiger plusieurs tests aux frontières.
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.
