Quelle est la meilleure façon de modéliser des événements récurrents dans une application de calendrier?

Je construis une application de calendrier de groupe qui doit prendre en charge des événements récurrents, mais toutes les solutions que je propose pour gérer ces événements semblent être un hack. Je peux limiter la distance à laquelle on peut aller et générer tous les événements en même temps. Ou je peux stocker les événements en les répétant et les afficher dynamicment quand on regarde en avant dans le calendrier, mais je devrai les convertir en un événement normal si quelqu’un veut changer les détails d’une instance particulière de l’événement.

Je suis sûr qu’il y a un meilleur moyen de le faire, mais je ne l’ai pas encore trouvé. Quelle est la meilleure façon de modéliser des événements récurrents, où vous pouvez modifier les détails ou supprimer des instances d’événements particulières?

(J’utilise Ruby, mais s’il vous plaît ne laissez pas cela limiter votre réponse. S’il y a une bibliothèque spécifique à Ruby ou quelque chose, c’est bon à savoir.)

J’utiliserais un concept de «lien» pour tous les événements récurrents futurs. Ils sont affichés dynamicment dans le calendrier et renvoient à un seul object de référence. Lorsque des événements ont eu lieu, le lien est rompu et l’événement devient une instance autonome. Si vous tentez d’éditer un événement récurrent, invitez-le à modifier tous les éléments futurs (par exemple, modifier une référence liée unique) ou modifiez simplement cette instance (auquel cas, convertissez-le en instance autonome, puis modifiez-le). Ce dernier cas est légèrement problématique, car vous devez suivre dans votre liste récurrente tous les événements futurs convertis en instance unique. Mais, c’est entièrement faisable.

Donc, en substance, ont 2 classes d’événements – instances uniques et événements récurrents.

Martin Fowler – Événements récurrents pour les calendriers contient des idées et des modèles intéressants.

Runt gem implémente ce motif.

Il peut y avoir beaucoup de problèmes avec les événements récurrents, laissez-moi en souligner quelques-uns que je connais.

Solution 1 – pas d’instances

Stocker le rendez-vous original + les données de récurrence, ne stockez pas toutes les instances.

Problèmes:

  • Vous devrez calculer toutes les instances d’une fenêtre de date lorsque vous en avez besoin, ce qui est coûteux
  • Impossible de gérer les exceptions (c’est-à-dire que vous supprimez l’une des instances ou que vous la déplacez, ou plutôt, vous ne pouvez pas faire cela avec cette solution)

Solution 2 – stocker les instances

Tout stocker à partir de 1, mais aussi toutes les instances, liées au rendez-vous d’origine.

Problèmes:

  • Prend beaucoup d’espace (mais l’espace est bon marché, donc mineur)
  • Les exceptions doivent être traitées avec élégance, surtout si vous revenez en arrière et modifiez le rendez-vous d’origine après avoir fait une exception. Par exemple, si vous déplacez la troisième instance un jour en avant, que se passe-t-il si vous revenez en arrière et modifiez l’heure du rendez-vous initial, réinsérez-en une autre le jour original et laissez la requête déplacée? Dissocier le déplacé? Essayez de changer le déplacé de manière appropriée?

Bien sûr, si vous ne faites pas d’exceptions, alors l’une ou l’autre solution devrait convenir, et vous choisissez essentiellement un compromis temps / espace.

Vous voudrez peut-être examiner les implémentations du logiciel iCalendar ou la norme elle-même ( RFC 2445 RFC 5545 ). Les projets de Mozilla sont les suivants: http://www.mozilla.org/projects/calendar/ Une recherche rapide révèle également http://icalendar.rubyforge.org/ .

D’autres options peuvent être envisagées en fonction de la manière dont vous allez stocker les événements. Construisez-vous votre propre schéma de firebase database? En utilisant quelque chose basé sur iCalendar, etc.

Je travaille avec les éléments suivants:

et un gem en cours qui étend formtastic avec un type d’entrée: form.schedule :as => :recurring ( form.schedule :as => :recurring ), qui rend une interface de type iCal et un before_filter pour sérialiser la vue dans un object IceCube , ghetto-ly.

Mon idée est de rendre l’incrédibilité facile pour append des atsortingbuts récurrents à un modèle et le connecter facilement dans la vue. Tout en quelques lignes.


Alors qu’est-ce que ça me donne? Atsortingbuts indexés, modifiables, récurrents.

events stocke une instance d’un jour et est utilisé dans la vue / l’assistant de l’ task.schedule que task.schedule stocke l’object IceCube sorte que vous pouvez faire des appels comme: task.schedule.next_suggestion .

Récapitulatif: J’utilise deux modèles, un plat, pour l’affichage du calendrier et un atsortingbut pour la fonctionnalité.

J’ai développé plusieurs applications basées sur un calendrier et j’ai créé un ensemble de composants de calendrier JavaScript réutilisables prenant en charge la récurrence. J’ai écrit un aperçu de la façon de concevoir une récurrence qui pourrait être utile à quelqu’un. Bien que quelques bits soient spécifiques à la bibliothèque que j’ai écrite, la grande majorité des conseils proposés sont généraux pour toute implémentation de calendrier.

Quelques points clés:

  • Stocker la récurrence à l’aide du format RRe d’iCal – c’est une roue que vous ne voulez vraiment pas réinventer
  • Ne stockez PAS les instances d’ événements récurrents individuels en tant que lignes dans votre firebase database! Toujours stocker un modèle de récurrence.
  • Il existe de nombreuses manières de concevoir votre schéma d’événement / exception, mais un exemple de base est fourni.
  • Toutes les valeurs de date / heure doivent être stockées en UTC et converties en local pour l’affichage
  • La date de fin stockée pour un événement récurrent doit toujours être la date de fin de la plage de récurrence (ou la “date maximale” de votre plate-forme si elle est récurrente “pour toujours”) et la durée de l’événement doit être stockée séparément. Cela permet de garantir une manière raisonnable d’interroger les événements ultérieurement.
  • Une discussion sur la génération d’instances d’événement et de stratégies d’édition de récurrence est incluse

C’est un sujet très compliqué avec de nombreuses approches valables pour sa mise en œuvre. Je dirai que j’ai effectivement mis en œuvre la récurrence plusieurs fois avec succès, et je me méfierais de prendre des conseils à ce sujet à toute personne qui ne l’a pas encore fait.

J’utilise le schéma de firebase database décrit ci-dessous pour stocker les parameters de récurrence

http://github.com/bakineggs/recurring_events_for

Ensuite, j’utilise runt pour calculer dynamicment les dates.

https://github.com/mlipper/runt

  1. Gardez une trace d’une règle de récurrence (probablement basée sur iCalendar, par @ Kris K. ). Cela inclura un motif et une plage (tous les troisièmes mardi, pour 10 occurrences).
  2. En effet, lorsque vous souhaitez modifier / supprimer une occurrence spécifique, suivez les dates d’exception de la règle de récurrence ci-dessus (dates où l’événement ne se produit pas comme la règle l’indique).
  3. Si vous avez supprimé, c’est tout ce dont vous avez besoin, si vous avez modifié, créez un autre événement et atsortingbuez-lui un ID parent défini pour l’événement principal. Vous pouvez choisir d’inclure toutes les informations de l’événement principal dans cet enregistrement ou s’il contient uniquement les modifications et hérite de tout ce qui ne change pas.

Notez que si vous autorisez des règles de récurrence qui ne se terminent pas, vous devez réfléchir à la manière d’afficher votre quantité d’informations infinie.

J’espère que cela pourra aider!

Je recommande l’utilisation de la puissance de la bibliothèque de dates et de la sémantique du module range de ruby. Un événement récurrent est vraiment une heure, une plage de dates (un début et une fin) et généralement un seul jour de la semaine. En utilisant la date et la plage, vous pouvez répondre à toutes les questions:

 #!/usr/bin/ruby require 'date' start_date = Date.parse('2008-01-01') end_date = Date.parse('2008-04-01') wday = 5 # friday (start_date..end_date).select{|d| d.wday == wday}.map{|d| d.to_s}.inspect 

Produit tous les jours de l’événement, y compris l’année bissextile!

 # =>"[\"2008-01-04\", \"2008-01-11\", \"2008-01-18\", \"2008-01-25\", \"2008-02-01\", \"2008-02-08\", \"2008-02-15\", \"2008-02-22\", \"2008-02-29\", \"2008-03-07\", \"2008-03-14\", \"2008-03-21\", \"2008-03-28\"]" 

À partir de ces réponses, j’ai en quelque sorte éliminé une solution. J’aime beaucoup l’idée du concept de lien. Les événements récurrents peuvent être une liste chaînée, la queue connaissant sa règle de récurrence. Changer un événement serait alors facile, car les liens restnt en place et la suppression d’un événement est également simple: vous supprimez le lien entre un événement, le supprimez et le reliez avant et après. Vous devez toujours interroger des événements récurrents chaque fois que quelqu’un regarde une nouvelle période de temps qui n’a jamais été regardée auparavant sur le calendrier, mais sinon, c’est plutôt propre.

Vous pouvez stocker les événements en tant que répétitions et, si une instance particulière a été modifiée, créez un nouvel événement avec le même ID d’événement. Ensuite, lors de la recherche de l’événement, recherchez tous les événements avec le même ID d’événement pour obtenir toutes les informations. Je ne suis pas sûr que vous ayez lancé votre propre bibliothèque d’événements ou si vous en utilisez une existante, cela pourrait ne pas être possible.

Consultez l’article ci-dessous pour trois bonnes bibliothèques date / heure rbuy. ice_cube en particulier semble être un choix solide pour les règles de récurrence et d’autres éléments dont un calendrier d’événements aurait besoin. http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html

En javascript:

Gestion des horaires récurrents: http://bunkat.github.io/later/

Gestion des événements complexes et des dépendances entre ces calendriers: http://bunkat.github.io/schedule/

Fondamentalement, vous créez les règles, puis vous demandez à la bibliothèque de calculer les N prochains événements récurrents (en spécifiant une plage de dates ou non). Les règles peuvent être analysées / sérialisées pour les enregistrer dans votre modèle.

Si vous avez un événement récurrent et que vous souhaitez modifier une seule récurrence, vous pouvez utiliser la fonction except () pour ignorer un jour particulier, puis append un nouvel événement modifié pour cette entrée.

La lib supporte des patterns très complexes, des fuseaux horaires et même des événements de croning.

Stockez les événements sous forme de répétitions et affichez-les dynamicment, mais laissez l’événement récurrent contenir une liste d’événements spécifiques susceptibles de remplacer les informations par défaut pour un jour donné.

Lorsque vous interrogez l’événement récurrent, il peut rechercher un remplacement spécifique pour ce jour.

Si un utilisateur apporte des modifications, vous pouvez demander s’il souhaite mettre à jour toutes les instances (détails par défaut) ou ce jour-là (créez un nouvel événement spécifique et ajoutez-le à la liste).

Si un utilisateur demande de supprimer toutes les récurrences de cet événement, vous avez également la liste des spécificités à remettre et vous pouvez les supprimer facilement.

Le seul cas problématique serait que l’utilisateur veuille mettre à jour cet événement et tous les événements futurs. Dans ce cas, vous devrez diviser l’événement récurrent en deux. À ce stade, vous pouvez envisager de lier des événements récurrents de manière à pouvoir les supprimer tous.

Pour les programmeurs .NET prêts à payer des frais de licence, Aspose.Network pourrait vous être utile … il inclut une bibliothèque compatible iCalendar pour les rendez-vous récurrents.

Vous stockez directement les événements au format iCalendar, ce qui permet une répétition ouverte, la localisation du fuseau horaire, etc.

Vous pouvez les stocker sur un serveur CalDAV et, lorsque vous souhaitez afficher les événements, vous pouvez utiliser l’option du rapport défini dans CalDAV pour demander au serveur de procéder à l’extension des événements récurrents sur la période affichée.

Ou vous pouvez les stocker vous-même dans une firebase database et utiliser une sorte de bibliothèque d’parsing iCalendar pour effectuer l’extension, sans avoir besoin de PUT / GET / REPORT pour parler à un serveur principal CalDAV. C’est probablement plus de travail – je suis sûr que les serveurs CalDAV cachent la complexité quelque part.

Avoir les événements au format iCalendar simplifiera probablement les choses à long terme, car les gens voudront toujours qu’ils soient exportés pour pouvoir utiliser d’autres logiciels.

J’ai simplement implémenté cette fonctionnalité! La logique est la suivante, il faut d’abord deux tables. RuleTable magasin général ou recycler les événements paternels. ItemTable est stocké des événements de cycle. Par exemple, lorsque vous créez un événement cyclique, l’heure de début du 6 novembre 2015, l’heure de fin du 6 décembre (ou pour toujours), effectuez un cycle d’une semaine. Vous insérez des données dans un RuleTable, les champs sont les suivants:

 TableID: 1 Name: cycleA StartTime: 6 November 2014 (I kept thenumber of milliseconds), EndTime: 6 November 2015 (if it is repeated forever, and you can keep the value -1) Cycletype: WeekLy. 

Vous souhaitez maintenant interroger les données du 20 novembre au 20 décembre. Vous pouvez écrire une fonction RecurringEventBE (début long, fin long), en fonction de l’heure de début et de fin, WeekLy, vous pouvez calculer la collection souhaitée, . En plus du 6 novembre et du rest, je l’ai appelé un événement virtuel. Lorsque l’utilisateur modifie un nom d’événement virtuel après (cycleA11.27 par exemple), vous insérez une donnée dans un object ItemTable. Les champs sont les suivants:

 TableID: 1 Name, cycleB StartTime, 27 November 2014 EndTime,November 6 2015 Cycletype, WeekLy Foreignkey, 1 (pointingto the table recycle paternal events). 

Dans la fonction RecurringEventBE (début long, fin long), vous avez utilisé cet événement virtuel couvrant les données (cycleB11.27).

Ceci est mon événement récurrent

 public static List> recurringData(Context context, long start, long end) { // 重复事件的模板处理,生成虚拟事件(根据日期段) long a = System.currentTimeMillis(); List> finalDataList = new ArrayList>(); List> tDataList = BillsDao.selectTemplateBillRuleByBE(context); //RuleTable,just select recurringEvent for (Map iMap : tDataList) { int _id = (Integer) iMap.get("_id"); long bk_billDuedate = (Long) iMap.get("ep_billDueDate"); // 相当于事件的开始日期 Start long bk_billEndDate = (Long) iMap.get("ep_billEndDate"); // 重复事件的截止日期 End int bk_billRepeatType = (Integer) iMap.get("ep_recurringType"); // recurring Type long startDate = 0; // 进一步精确判断日记起止点,保证了该段时间断获取的数据不未空,减少不必要的处理long endDate = 0; if (bk_billEndDate == -1) { // 永远重复事件的处理if (end >= bk_billDuedate) { endDate = end; startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空} } else { if (start <= bk_billEndDate && end >= bk_billDuedate) { // 首先判断起止时间是否落在重复区间,表示该段时间有重复事件endDate = (bk_billEndDate >= end) ? end : bk_billEndDate; startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空} } Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(bk_billDuedate); // 设置重复的开始日期long virtualLong = bk_billDuedate; // 虚拟时间,后面根据规则累加计算List> virtualDataList = new ArrayList>();// 虚拟事件if (virtualLong == startDate) { // 所要求的时间,小于等于父本时间,说明这个是父事件数据,即第一条父本数据Map bMap = new HashMap(); bMap.putAll(iMap); bMap.put("indexflag", 1); // 1表示父本事件virtualDataList.add(bMap); } long before_times = 0; // 计算从要求时间start到重复开始时间的次数,用于定位第一次发生在请求时间段落的时间点long remainder = -1; if (bk_billRepeatType == 1) { before_times = (startDate - bk_billDuedate) / (7 * DAYMILLIS); remainder = (startDate - bk_billDuedate) % (7 * DAYMILLIS); } else if (bk_billRepeatType == 2) { before_times = (startDate - bk_billDuedate) / (14 * DAYMILLIS); remainder = (startDate - bk_billDuedate) % (14 * DAYMILLIS); } else if (bk_billRepeatType == 3) { before_times = (startDate - bk_billDuedate) / (28 * DAYMILLIS); remainder = (startDate - bk_billDuedate) % (28 * DAYMILLIS); } else if (bk_billRepeatType == 4) { before_times = (startDate - bk_billDuedate) / (15 * DAYMILLIS); remainder = (startDate - bk_billDuedate) % (15 * DAYMILLIS); } else if (bk_billRepeatType == 5) { do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 1); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 1 + 1); virtualLong = calendar.getTimeInMillis(); } else { calendar.add(Calendar.MONTH, 1); virtualLong = calendar.getTimeInMillis(); } } while (virtualLong < startDate); } else if (bk_billRepeatType == 6) { do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 2); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 2 + 2); virtualLong = calendar.getTimeInMillis(); } else { calendar.add(Calendar.MONTH, 2); virtualLong = calendar.getTimeInMillis(); } } while (virtualLong < startDate); } else if (bk_billRepeatType == 7) { do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 3); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 3 + 3); virtualLong = calendar.getTimeInMillis(); } else { calendar.add(Calendar.MONTH, 3); virtualLong = calendar.getTimeInMillis(); } } while (virtualLong < startDate); } else if (bk_billRepeatType == 8) { do { calendar.add(Calendar.YEAR, 1); virtualLong = calendar.getTimeInMillis(); } while (virtualLong < startDate); } if (remainder == 0 && virtualLong != startDate) { // 当整除的时候,说明当月的第一天也是虚拟事件,判断排除为父本,然后添加。不处理,一个月第一天事件会丢失before_times = before_times - 1; } if (bk_billRepeatType == 1) { // 单独处理天事件,计算出第一次出现在时间段的事件时间virtualLong = bk_billDuedate + (before_times + 1) * 7 * (DAYMILLIS); calendar.setTimeInMillis(virtualLong); } else if (bk_billRepeatType == 2) { virtualLong = bk_billDuedate + (before_times + 1) * (2 * 7) * DAYMILLIS; calendar.setTimeInMillis(virtualLong); } else if (bk_billRepeatType == 3) { virtualLong = bk_billDuedate + (before_times + 1) * (4 * 7) * DAYMILLIS; calendar.setTimeInMillis(virtualLong); } else if (bk_billRepeatType == 4) { virtualLong = bk_billDuedate + (before_times + 1) * (15) * DAYMILLIS; calendar.setTimeInMillis(virtualLong); } while (startDate <= virtualLong && virtualLong <= endDate) { // 插入虚拟事件Map bMap = new HashMap(); bMap.putAll(iMap); bMap.put("ep_billDueDate", virtualLong); bMap.put("indexflag", 2); // 2表示虚拟事件virtualDataList.add(bMap); if (bk_billRepeatType == 1) { calendar.add(Calendar.DAY_OF_MONTH, 7); } else if (bk_billRepeatType == 2) { calendar.add(Calendar.DAY_OF_MONTH, 2 * 7); } else if (bk_billRepeatType == 3) { calendar.add(Calendar.DAY_OF_MONTH, 4 * 7); } else if (bk_billRepeatType == 4) { calendar.add(Calendar.DAY_OF_MONTH, 15); } else if (bk_billRepeatType == 5) { Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 1); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 1 + 1); } else { calendar.add(Calendar.MONTH, 1); } }else if (bk_billRepeatType == 6) { Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 2); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 2 + 2); } else { calendar.add(Calendar.MONTH, 2); } }else if (bk_billRepeatType == 7) { Calendar calendarCloneCalendar = (Calendar) calendar .clone(); int currentMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); calendarCloneCalendar.add(Calendar.MONTH, 3); int nextMonthDay = calendarCloneCalendar .get(Calendar.DAY_OF_MONTH); if (currentMonthDay > nextMonthDay) { calendar.add(Calendar.MONTH, 3 + 3); } else { calendar.add(Calendar.MONTH, 3); } } else if (bk_billRepeatType == 8) { calendar.add(Calendar.YEAR, 1); } virtualLong = calendar.getTimeInMillis(); } finalDataList.addAll(virtualDataList); }// 遍历模板结束,产生结果为一个父本加若干虚事件的list /* * 开始处理重复特例事件特例事件,并且来时合并*/ List>oDataList = BillsDao.selectBillItemByBE(context, start, end); Log.v("mtest", "特例结果大小" +oDataList ); List> delectDataListf = new ArrayList>(); // finalDataList要删除的结果List> delectDataListO = new ArrayList>(); // oDataList要删除的结果for (Map fMap : finalDataList) { // 遍历虚拟事件int pbill_id = (Integer) fMap.get("_id"); long pdue_date = (Long) fMap.get("ep_billDueDate"); for (Map oMap : oDataList) { int cbill_id = (Integer) oMap.get("billItemHasBillRule"); long cdue_date = (Long) oMap.get("ep_billDueDate"); int bk_billsDelete = (Integer) oMap.get("ep_billisDelete"); if (cbill_id == pbill_id) { if (bk_billsDelete == 2) {// 改变了duedate的特殊事件long old_due = (Long) oMap.get("ep_billItemDueDateNew"); if (old_due == pdue_date) { delectDataListf.add(fMap);//该改变事件在时间范围内,保留oMap } } else if (bk_billsDelete == 1) { if (cdue_date == pdue_date) { delectDataListf.add(fMap); delectDataListO.add(oMap); } } else { if (cdue_date == pdue_date) { delectDataListf.add(fMap); } } } }// 遍历特例事件结束}// 遍历虚拟事件结束// Log.v("mtest", "delectDataListf的大小"+delectDataListf.size()); // Log.v("mtest", "delectDataListO的大小"+delectDataListO.size()); finalDataList.removeAll(delectDataListf); oDataList.removeAll(delectDataListO); finalDataList.addAll(oDataList); List> mOrdinaryList = BillsDao.selectOrdinaryBillRuleByBE(context, start, end); finalDataList.addAll(mOrdinaryList); // Log.v("mtest", "finalDataList的大小"+finalDataList.size()); long b = System.currentTimeMillis(); Log.v("mtest", "算法耗时"+(ba)); return finalDataList; } 

Et si vous avez un rendez-vous récurrent sans date de fin? Aussi bon marché que soit l’espace, vous n’avez pas d’espace infini, alors la solution 2 n’y trouve pas de place …

Puis-je suggérer que “pas de date de fin” peut être résolu à la fin du siècle. Même pour un événement quotidien, la quantité d’espace rest bon marché.