Profil et extraction de voyage
ReCHor – étape 6
1. Introduction
Le but de cette sixième étape — la dernière de la première partie du projet — est d'une part de terminer la rédaction des classes représentant l'horaire aplati, et d'autre part d'écrire celles permettant de représenter ce que nous appellerons un profil, et d'en extraire des voyages.
Notez que cette étape devra être rendue deux fois :
- pour le rendu testé habituel (délai : le 28/3 à 18h00),
- pour le rendu intermédiaire (délai : le 4/4 à 18h00).
Le deuxième de ces rendus sera corrigé par lecture du code de vos étapes 1 à 6, et il vous faudra donc soigner sa qualité et sa documentation. Il est fortement conseillé de lire notre guide à ce sujet.
2. Concepts
2.1. « Mappage » de fichiers en mémoire
Les deux étapes précédentes ont été consacrées à la rédaction de classes permettant de manipuler les différents éléments de l'horaire aplati — gares, voies/quais, liaisons, etc. — se trouvant dans des tableaux d'octets. Il reste à savoir comment obtenir ces tableaux d'octets.
Comme expliqué à la §3, nous mettons à votre disposition un certain nombre de fichiers dont le contenu correspond exactement à celui des tableaux d'octets attendus. Par exemple, un des fichiers que nous vous fournissons, nommé stations.bin
, contient la totalité des gares aplaties au format décrit à la §2.3 de l'étape 4.
Il serait donc techniquement possible de charger le contenu de ce fichier et de le placer dans un tableau d'octets — de type ByteBuffer
— passé ensuite à la classe BufferedStations
. Cette solution aurait toutefois l'inconvénient de ralentir le démarrage du programme, la quantité de données à charger étant relativement grande.
Heureusement, une autre solution existe, le « mappage » de fichier en mémoire (memory mapping). Cet anglicisme désigne la possibilité de faire apparaître le contenu d'un fichier en mémoire, comme s'il avait effectivement été chargé, mais sans qu'il ne le soit réellement. Au lieu de cela, le contenu du fichier n'est chargé que lorsqu'on accède aux données, et seules les parties effectivement nécessaires de ce contenu sont chargées.
Cette possibilité est intéressante, car elle nous permet d'écrire le programme comme si la totalité des données de l'horaire était chargée en mémoire au démarrage du programme, alors qu'en réalité seules les parties effectivement utilisées le sont, au moment de leur première utilisation.
2.2. Profil
Comme nous le verrons à l'étape suivante, l'algorithme de recherche de voyage optimaux produit en résultat une table donnant, pour chaque gare du réseau, la frontière de Pareto des critères d'optimisation — heure de départ et d'arrivée, nombre de changements — des voyages permettant d'atteindre la destination choisie, le jour choisi. Nous nommerons cette table un profil (profile).
Par exemple, si on utilise l'algorithme de recherche pour trouver les voyages optimaux permettant de se rendre à Gruyères le 18 février 2025, il produit un profil associant une frontière de Pareto à chacune des plus de 30 000 gares existantes. Parmi ces frontières figure celle correspondant à la gare Ecublens VD, EPFL et contenant, entre autres, les tuples suivant :
\[ \ldots, (\textrm{15h55}, \textrm{17h57}, 3), (\textrm{16h13}, \textrm{17h57}, 4), (\textrm{16h38}, \textrm{18h21}, 3), (\textrm{16h38}, \textrm{18h28}, 2), \ldots \]
qui correspondent aux quatre premiers voyages visibles dans la copie d'écran de l'interface graphique finale du programme ci-dessous, déjà présentée dans l'introduction au projet.

Il est important de comprendre qu'un profil contient la frontière de Pareto de chacune des gares pour une destination et un jour de voyage donnés, et n'est donc pas lié à une gare de départ particulière.
2.3. Profil augmenté
Un profil contient, pour chaque gare, la frontière de Pareto de tous les voyages optimaux permettant de se rendre de cette gare-là à la gare de destination, à la date de voyage donnée. Il permet donc à un voyageur se trouvant dans une gare quelconque du réseau ce jour-là de déterminer quand il peut arriver à la destination, et au prix de combien de changements.
Lors du calcul des voyages optimaux, il se trouve qu'il est utile d'avoir cette information-là non seulement pour toutes les gares du réseau, mais aussi pour toutes les courses circulant ce jour-là. En d'autres termes, d'offrir un profil permettant également à un voyageur se trouvant à bord d'un véhicule effectuant une course donnée de déterminer quand il peut arriver à destination, et au prix de combien de changements. Contrairement aux frontières de Pareto associées aux gares, celles associées aux courses ne comportent que deux critères d'optimisation (l'heure d'arrivée et le nombre de changements), car l'heure de départ d'une course ne peut pas être choisie.
Nous appellerons profil augmenté un profil possédant non seulement les frontières de Pareto correspondant aux gares, mais aussi celles correspondant aux courses.
2.4. Extraction de voyages
À eux seuls, les tuples associés à une gare dans un profil ne contiennent pas assez d'information pour permettre à un voyageur de déterminer comment se rendre à destination. Par exemple, le tuple \((\textrm{15h55}, \textrm{17h57}, 3)\) de la frontière de Pareto donnée en exemple plus haut permet uniquement de savoir qu'il est possible de partir de l'EPFL à 15h55 pour arriver à Gruyères à 17h57 après avoir effectué 3 changements, mais sans savoir comment. Ainsi, si deux métros partent de l'EPFL à 15h55, l'un en direction de Renens, l'autre en direction de Lausanne, il est impossible de savoir lequel prendre sans autre information que le tuple.
Dès lors, pour pouvoir extraire d'un profil la totalité des voyages optimaux pour un arrêt de départ donné, des informations supplémentaires sont nécessaires. Ces informations seront calculées par l'algorithme de recherche des voyages optimaux, et stockées dans ce que nous avons appelé la charge utile (payload) associée aux tuples.
Pour les tuples des frontières de Pareto des gares, contenant trois critères (heure de départ et d'arrivée, nombre de changements), la charge utile contient deux informations :
- l'identité de la première liaison à emprunter pour débuter le voyage,
- le nombre d'arrêts intermédiaires à laisser passer avant de descendre du véhicule.
Ces deux informations sont empaquetées dans les 32 bits de la charge utile, les 24 bits de poids fort contenant l'identité de la liaison, les 8 bits de poids faible contenant le nombre d'arrêts intermédiaires.
Par exemple, au tuple \((\textrm{15h55}, \textrm{17h57}, 3)\) mentionné plus haut pourrait être associée une charge utile contenant :
- l'identité de la liaison partant à 15h55 de Ecublens VD, EPFL pour arriver à 15h56 à Ecublens VD, Bassenges,
- un nombre d'arrêts intermédiaires valant 4.
Grâce à cette information, on peut déterminer que la première étape du voyage part à 15h55 de Ecublens VD, EPFL pour arriver à 16h01 à Renens VD, gare, simplement en consultant les liaisons qui suivent la première dans la course à laquelle elle appartient.
Une fois la première étape déterminée, on peut consulter la frontière de Pareto de sa gare d'arrivée (Renens VD, gare) pour déterminer comment continuer le voyage. En effet, on sait que le tuple qui nous intéresse est celui permettant d'arriver à destination à 17h57 en effectuant encore 2 changements supplémentaires — étant donné que le premier des 3 changements totaux vient d'être fait.
En consultant la frontière de Pareto de Renens VD, gare, on y trouve le tuple \((\textrm{16h08}, \textrm{17h57}, 2)\) dont la charge utile contient :
- l'identité de la liaison partant à 16h08 de Renens VD pour arriver à 16h14 à Lausanne,
- un nombre d'arrêts intermédiaires valant 0.
On peut en déterminer :
- la prochaine étape du voyage, qui doit être une étape à pied allant de Renens VD, gare à Renens VD, car pour qu'un voyage soit valide il doit alterner les étapes en véhicule et à pied,
- la prochaine étape en véhicule du voyage, qui part de Renens VD à 16h08 et arrive à Lausanne à 16h14.
Pour continuer, il suffit de rechercher dans la frontière de Pareto associée à l'arrêt Lausanne le tuple permettant d'arriver à destination à 17h57 en effectuant encore 1 changement, puis de procéder comme ci-dessus. Et ainsi de suite jusqu'à ce qu'il ne reste plus de changements à effectuer.
2.5. Étapes initiales et finales
La technique d'extraction de voyage présentée à la section précédente produit toujours des voyages dont la première et la dernière étape se font en véhicule et non à pied. Or parfois, l'une ou l'autre, ou les deux, de ces étapes doivent se faire à pied.
Par exemple, si au lieu de partir depuis l'arrêt Ecublens VD, EPFL, on désire partir depuis l'arrêt Ecublens VD, EPFL (bus) — l'arrêt de bus se trouvant juste à côté de l'arrêt du m1 —, la première étape consiste à marcher jusqu'à l'arrêt du m1. Bien entendu, une situation similaire peut se produire pour la dernière étape d'un voyage.
Dès lors, la technique d'extraction mentionnée précédemment doit être légèrement augmentée afin d'ajouter une étape à pied initiale si la première liaison part d'un arrêt différent de l'arrêt de départ choisi. De même, une étape à pied finale doit être ajoutée si, après avoir effectué tous les changements requis, on se trouve à un arrêt qui n'est pas encore l'arrêt de destination.
3. Mise en œuvre Java
Avant de commencer à travailler à cette étape, il vous faut télécharger l'archive Zip que nous mettons à votre disposition et qui contient les données horaire aplaties. Cette archive est constituée d'un dossier nommé timetable
dans lequel se trouvent les fichiers suivants :
strings.txt
contenant la table des chaînes, avec une chaîne par ligne, et dont les caractères sont encodés en ISO 8859-1,stations.bin
contenant les gares,station-aliases.bin
contenant les noms alternatifs des gares,platforms.bin
contenant les voies/quais,routes.bin
contenant les lignes,transfers.bin
contenant les changements.
Dans le dossier timetable
se trouvent de plus 7 dossiers journaliers dont le nom est la date d'un jour de la semaine 12 de 2025 — qui va du 17 au 23 mars — et contenant chacun les trois fichiers dont les données varient de jour en jour, à savoir :
trips.bin
contenant les courses du jour,connections.bin
contenant les liaisons du jour,connections-succ.bin
contenant les successeurs des liaisons du jour.
Nous vous recommandons d'extraire le contenu de cette archive directement dans le dossier principal de votre projet, de manière à ce que le dossier timetable
qu'elle contient se trouve au même niveau que le dossier src
contenant votre code.
Attention : si vous utilisez git, ne stockez en aucun cas les données horaires dans votre entrepôt ! Pour éviter toute fausse manipulation, ajoutez une ligne à votre fichier .gitignore
pour que git ignore le dossier les contenant (/timetable/
).
Chaque lundi jusqu'à la fin du semestre, nous publierons sur la page consacrée au projet une nouvelle archive contenant les données de la semaine.
3.1. Enregistrement FileTimeTable
L'enregistrement FileTimeTable
du sous-paquetage timetable.mapped
, public, implémente l'interface TimeTable
et représente un horaire de transport public dont les données (aplaties) sont stockées dans des fichiers. Il possède les attributs suivants :
Path directory
- le chemin d'accès au dossier contenant les fichiers des données horaires,
List<String> stringTable
- la table des chaînes de caractères,
Stations stations
- les gares,
StationAliases stationAliases
- les noms alternatifs des gares,
Platforms platforms
- les voies/quai,
Routes routes
- les lignes,
Transfers transfers
- les changements.
FileTimeTable
possède une méthode publique et statique facilitant la création d'une instance à partir du chemin d'accès au dossier contenant les données horaires :
TimeTable in(Path directory) throws IOException
- qui retourne une nouvelle instance de
FileTimeTable
dont les données aplaties ont été obtenues à partir des fichiers se trouvant dans le dossier dont le chemin d'accès est donné, en faisant l'hypothèse que ce dossier est organisé de la même manière que le dossiertimetable
de l'archive que nous vous fournissons ; lève uneIOException
en cas d'erreur d'entrée/sortie.
En dehors de cette méthode statique, les seules méthodes publiques offertes par FileTimeTable
sont des versions concrètes des méthodes abstraites de TimeTable
.
3.1.1. Conseils de programmation
- Chemins d'accès
La classe
Path
de la bibliothèque Java représente un chemin d'accès à un fichier ou un dossier. Les deux seules méthodes de cette classe nécessaires à cette étape sont :- la méthode statique
of
, qui permet de créer un chemin d'accès à un dossier à partir de son nom, - la méthode
resolve
, qui permet d'obtenir un chemin d'accès à un fichier ou un sous-dossier inclu dans celui auquel on applique la méthode.
L'extrait de code suivant illustre leur utilisation en obtenant d'abord un chemin d'accès vers le dossier
timetable
contenant les données horaires, puis un chemin vers le fichierstrings.txt
contenant la table des chaînes :Path directory = Path.of("timetable"); Path strings = directory.resolve("strings.txt");
- la méthode statique
- Lecture de la table des chaînes
La lecture de la table des chaînes peut se faire aisément au moyen de la méthode
readAllLines
deFiles
. N'oubliez pas que les caractères du fichierstrings.txt
fourni sont encodés en ISO 8859-1, et pas en UTF-8.Comme sa documentation le mentionne,
readAllLines
retourne une liste qui peut être modifiable ou non. Pour garantir l'immuabilité de cette liste, on peut donc vouloir la copier au moyen decopyOf
avant de la passer aux constructeurs des différentes classesBuffered…
.Notez que cette copie est obligatoire lorsque les constructeurs des classes
Buffered…
eux-mêmes copient la table reçue aveccopyOf
. En effet, au vu de sa taille, il est crucial qu'une seule copie de la table des chaînes existe dans tout le programme. Or si les constructeurs utilisentcopyOf
, la seule manière de garantir l'unicité de la table est de leur fournir une liste qui a déjà été préalablement copiée avec cette méthode, car dans ce cas elle retourne la liste reçue sans la copier, comme l'explique sa documentation. - « Mappage » mémoire
Pour obtenir les tableaux d'octets de type
ByteBuffer
avec le contenu des différents fichiers, il faut utiliser le « mappage » mémoire, comme expliqué plus haut. Cela peut se faire au moyen de la classeFileChannel
du paquetagejava.nio
, qui n'a pas été vue au cours mais qui a un but similaire àInputStream
. Pour les besoins de ce projet, il n'est pas nécessaire de la comprendre en détail, le « mappage » d'un fichier pouvant se faire simplement en :- obtenant un « canal » de type
FileChannel
permettant de lire les données du fichier qui nous intéresse, au moyen de la méthodeopen
à laquelle on ne passe aucune option, - « mappant » le contenu du fichier au moyen de la méthode
map
, à laquelle on passeREAD_ONLY
comme mode, 0 comme position et la valeur retournée par la méthodesize
du canal comme taille — afin de « mapper » en lecture seule la totalité des données du fichier, - fermant le canal, de préférence de manière implicite avec un try-with-resource.
La méthode
map
retourne un tableau d'octets de typeByteBuffer
contenant la totalité des données du fichier. Toutefois, comme elle fait cela par « mappage » de son contenu en mémoire, ces données sont en réalité lues à la demande, au moment de leur première utilisation. Il faut noter que les tableaux retournés parmap
restent utilisables même après la fermeture du canal, et sont immuables. - obtenant un « canal » de type
- Gestion des exceptions
Les fichiers contenant les données qui ne dépendent pas de la date du voyage doivent tous être « mappés » dans la méthode
in
. Par contre, ceux qui dépendent de la date ne doivent l'être que dans les méthodesconnectionsFor
ettripsFor
. Cela pose un petit problème, car lors du « mappage », uneIOException
pourrait être levée, et il s'agit d'une exception de type checked. La manière standard de résoudre ce problème serait d'ajouter unthrows IOException
à ces méthodes, mais nous ne pouvons le faire car il s'agit de redéfinitions de méthodes, et les méthodes originales ne lèvent pas d'exception.Nous résoudrons donc ce problème en attrapant les exceptions de type
IOException
et en levant uneUncheckedIOException
qui, comme son nom l'indique, est de type unchecked. Cela peut se faire ainsi :try { // … code pouvant lever une IOException } catch (IOException e) { throw new UncheckedIOException(e); }
Attention : seules les méthodes
connectionsFor
ettripsFor
doivent utiliser cette technique !
3.2. Enregistrement Profile
L'enregistrement Profile
du sous-paquetage journey
, public et immuable, représente un profil. Il possède les attributs suivants :
TimeTable timeTable
- l'horaire auquel correspond le profil,
LocalDate date
- la date à laquelle correspond le profil,
int arrStationId
- l'index de la gare d'arrivée à laquelle correspond le profil,
List<ParetoFront> stationFront
- la table des frontières de Pareto de toutes les gares, qui contient, à un index donné, la frontière de la gare de même index.
Le constructeur compact de Profile
copie la table des frontières de Pareto afin de garantir l'immuabilité de la classe.
Profile
possède les méthodes publiques suivantes :
Connections connections()
- qui retourne les liaisons correspondant au profil, qui sont simplement celles de l'horaire, à la date à laquelle correspond le profil,
Trips trips()
- qui retourne les courses correspondant au profil, qui sont simplement celles de l'horaire, à la date à laquelle correspond le profil,
ParetoFront forStation(int stationId)
- qui retourne la frontière de Pareto pour la gare d'index donné, ou lève une
IndexOutOfBoundsException
si cet index est invalide.
3.3. Classe Profile.Builder
La classe Builder
, publique, finale et imbriquée statiquement dans l'enregistrement Profile
, représente un bâtisseur de profil. Ce bâtisseur étant destiné à être utilisé lors du calcul des voyages optimaux, il stocke non seulement les frontières de Pareto en cours de construction pour les gares, mais aussi pour les courses. En d'autres termes, il représente un profil augmenté en cours de construction, mais finit par bâtir un profil simple.
Builder
offre un unique constructeur public :
Builder(TimeTable timeTable, LocalDate date, int arrStationId)
- qui construit un bâtisseur de profil pour l'horaire, la date et la gare de destination donnés.
Le constructeur stocke non seulement les arguments qu'on lui a passé dans des attributs, afin de pouvoir les passer plus tard au constructeur de Profile
, mais il initialise également deux tableaux primitifs destinés à contenir les bâtisseurs des frontières de Pareto des gares et des courses. Ces tableaux ne doivent pas être remplis initialement, donc leurs éléments doivent tous être null
.
En plus de ce constructeur, Builder
offre les méthodes publiques suivantes :
ParetoFront.Builder forStation(int stationId)
- qui retourne le bâtisseur de la frontière de Pareto pour la gare d'index donné, qui est
null
si aucun appel àsetForStation
n'a été fait précédemment pour cette gare, ou lève uneIndexOutOfBoundsException
si l'index est invalide, void setForStation(int stationId, ParetoFront.Builder builder)
- qui associe le bâtisseur de frontière de Pareto donné à la gare d'index donné, ou lève une
IndexOutOfBoundsException
si l'index est invalide, ParetoFront.Builder forTrip(int tripId)
- qui fait la même chose que
forStation
mais pour la course d'index donné, void setForTrip(int tripId, ParetoFront.Builder builder)
- qui fait la même chose que
setForStation
mais pour la course d'index donné, Profile build()
- qui retourne le profil simple — sans les frontières de Pareto correspondant aux courses — en cours de construction.
3.3.1. Conseils de programmation
La liste des frontières de Pareto passée au constructeur de Profile
ne doit pas contenir de valeur null
, entre autres car cela provoquerait la levée d'une exception par copyOf
. La méthode build
doit donc utiliser la constante ParetoFront.EMPTY
chaque fois qu'elle rencontre un bâtisseur de gare valant null
.
3.4. Classe JourneyExtractor
La classe JourneyExtractor
du sous-paquetage journey
, publique et non instanciable, représente un « extracteur de voyages » et contient une seule méthode publique :
List<Journey> journeys(Profile profile, int depStationId)
- qui retourne la totalité des voyages optimaux correspondant au profil et à la gare de départ donnés, triés d'abord par heure de départ (croissante) puis par heure d'arrivée (croissante).
3.4.1. Conseils de programmation
- Parcours des frontières de Pareto
L'extraction de tous les voyages se fait en parcourant tous les éléments de la frontière de Pareto de la gare de départ. Ce parcours ne peut se faire qu'au moyen de la méthode
forEach
deParetoFront
, et la manière la plus simple d'utiliser cette méthode consiste à lui passer une lambda. Nous n'avons pas encore vu ce concept au cours, mais il n'est pas nécessaire de le comprendre en détail pour cette étape, il suffit juste de savoir qu'au moyen de la syntaxe suivante :ParetoFront pf = profile.forStation(stationId); pf.forEach((long criteria) -> { // … code qui utilise criteria });
on peut parcourir la totalité des critères de la frontière de Pareto
pf
, chacun d'entre eux étant à tour de rôle assigné à la variablecriteria
. En d'autres termes, cette syntaxe permet de faire la même chose que la syntaxe suivante, que vous connaissez déjà et que nous utiliserions si la frontière de Pareto était stockée dans un tableau de typelong[]
:long[] pf = profile.forStation(stationId); for (long criteria : pf) { // … code qui utilise criteria }
- Tri des voyages
Une fois les voyages extraits, il faut les trier par ordre croissant d'heure de départ, puis par ordre croissant d'heure d'arrivée. Une liste peut être triée au moyen de la méthode
sort
, qui attend en argument un comparateur. Comme la notion de comparateur n'a pas encore été vue au cours, et que la création du comparateur requis nécessite l'utilisation d'une syntaxe que vous ne connaissez pas encore, nous vous donnons le code à écrire pour effectuer le tri :List<Journey> journeys = …; journeys.sort(Comparator .comparing(Journey::depTime) .thenComparing(Journey::arrTime))
Même si cette syntaxe vous est encore inconnue, il ne devrait pas être difficile de comprendre que le tri est fait :
- d'abord par heure de départ (
comparing(Journey::depTime)
), - puis par heure d'arrivée (
thenComparing(Journey::arrTime)
).
Les arguments passés à
comparing
etthenComparing
sont des lambdas obtenues au moyen de ce qu'on appelle des références de méthodes. - d'abord par heure de départ (
- Gares et voies/quais
Lors de l'extraction des voyages, il faut faire bien attention au fait que les liaisons partent et arrivent à des arrêts qui peuvent être des voies/quais, et pas seulement des gares. Par contre, les changements se font toujours entre deux gares, ou au sein d'une seule gare. De même, les frontières de Pareto stockées dans le profil sont toujours associées aux gares, et jamais aux voies/quai.
Gardez toujours cela à l'esprit, et utilisez la méthode
stationId
deTimeTable
pour obtenir la gare correspondant à un arrêt chaque fois que cela est nécessaire.
3.5. Tests
Comme d'habitude, nous ne vous fournissons plus de tests mais un fichier de vérification de signatures à importer dans votre projet.
Pour vous aider à tester l'extraction de voyage, nous vous fournissons une archive Zip contenant un fichier nommé profile_2025-03-18_11486.txt
. Il contient la représentation textuelle d'un profil pour le 18 mars 2025, avec la gare 11486 (Gruyères) comme destination. Chaque ligne correspond à une gare, dont les éléments de la frontière de Pareto sont représentés en base 16, séparés par des virgules. Par exemple, la première ligne du fichier commence ainsi :
552b298408159611,56db1b070a9ad404,570b0b840ad9ae11,…
ce qui signifie que la frontière de Pareto associée à la gare d'index 0 dans le profil a comme premier tuple empaqueté 552b29840815961116 — soit \((\textrm{18h50}, \textrm{22h59}, 4)\) avec une charge utile dont la liaison est celle d'index 529814 (8159616) et le nombre d'arrêts intermédiaires est 17 (1116).
Pour lire ce profil, vous pouvez vous inspirer du code ci-dessous, qui n'utilise que des concepts que vous connaissez déjà et que vous devriez donc pouvoir comprendre sans difficulté majeure :
Profile readProfile(TimeTable timeTable, LocalDate date, int arrStationId) throws IOException { Path path = Path.of("profile_" + date + "_" + arrStationId + ".txt"); try (BufferedReader r = Files.newBufferedReader(path)) { Profile.Builder profileB = new Profile.Builder(timeTable, date, arrStationId); int stationId = -1; String line; while ((line = r.readLine()) != null) { stationId += 1; if (line.isEmpty()) continue; ParetoFront.Builder frontB = new ParetoFront.Builder(); for (String t : line.split(",")) frontB.add(Long.parseLong(t, 16)); profileB.setForStation(stationId, frontB); } return profileB.build(); } }
Une fois le profil lu, vous pouvez vérifier que votre code est correct en extrayant tous les voyages au départ de la gare 7872 (Ecublens VD, EPFL), et en convertissant celui d'index 32 en événement iCalendar, au moyen de code similaire à celui-ci :
TimeTable t = FileTimeTable.in(Path.of("timetable")); LocalDate date = LocalDate.of(2025, Month.MARCH, 18); Profile p = readProfile(t, date, 11486); List<Journey> js = JourneyExtractor.journeys(p, 7872); String j = JourneyIcalConverter.toIcalendar(js.get(32)); System.out.println(j);
Vous devriez voir un événement iCalendar presque identique à celui de la §2.1.8 de l'étape 2.
4. Résumé
Pour cette étape, vous devez :
- écrire les classes
FileTimeTable
,Profile
,Profile.Builder
etJourneyExtractor
selon les indications plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies,
- rendre votre code au plus tard le 28 mars 2025 à 18h00, au moyen du programme
Submit.java
fourni et des jetons disponibles sur votre page privée.
Ce rendu est un rendu testé, auquel 20 points sont attribués, au prorata des tests unitaires passés avec succès.
N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus.
Si vous manquez la date limite de rendu, vous avez encore la possibilité de faire un rendu tardif au moyen des jetons prévus à cet effet, et ce durant les 2 heures qui suivent, mais il vous en coûtera une pénalité inconditionnelle de 2 points.