Je construis une application de calendrier de groupe qui doit prendre en charge les événements récurrents, mais toutes les solutions que j'ai proposées pour gérer ces événements semblent être un hack. Je peux limiter l’avenir, puis générer tous les événements en même temps. Ou je peux stocker les événements comme étant répétés et les afficher dynamiquement quand on regarde l'avenir, mais je devrai les convertir en un événement normal si quelqu'un veut modifier les détails d'une instance particulière de l'événement.
Je suis sûr qu'il existe 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 contraindre votre réponse. S'il existe une bibliothèque spécifique à Ruby ou quelque chose de ce genre, c'est bon à savoir.)
J'utiliserais un concept de "lien" pour tous les événements récurrents futurs. Ils sont affichés dynamiquement dans le calendrier et sont liés à un seul objet 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 essayez de modifier un événement récurrent, cliquez sur Invite pour modifier tous les éléments à venir (c'est-à-dire changer de référence liée unique) ou seulement cette instance (dans ce cas, convertissez-la en une instance autonome, puis apportez des modifications). Ce dernier cas est légèrement problématique car vous devez garder une trace dans votre liste récurrente de tous les événements futurs convertis en instance unique. Mais c'est tout à fait faisable.
Il existe donc essentiellement 2 classes d’événements: des instances uniques et des événements récurrents.
Martin Fowler - Evénements récurrents pour les calendriers contient des informations et des modèles intéressants.
Runt gem implémente ce modèle.
Il peut y avoir beaucoup de problèmes avec les événements récurrents, laissez-moi en souligner quelques-uns que je connais.
Stockez les données de rendez-vous d'origine + récurrence, ne stockez pas toutes les instances.
Problèmes:
Stockez tout à partir de 1, mais aussi toutes les instances, liées au rendez-vous d'origine.
Problèmes:
Bien sûr, si vous n'allez pas faire d'exceptions, alors l'une ou l'autre solution devrait convenir, et vous choisissez en gros un scénario de compensation temps/espace.
Vous voudrez peut-être examiner les implémentations logicielles iCalendar ou la norme elle-même (RFC 2445 RFC 5545 ). Les projets Mozilla http://www.mozilla.org/projects/calendar/ Une recherche rapide révèle http://icalendar.rubyforge.org/ aussi bien.
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 base de données? Vous utilisez quelque chose basé sur iCalendar, etc.?
Je travaille avec ce qui suit:
et une gem en cours qui étend formtastic avec un type d'entrée: récurrent (form.schedule :as => :recurring
), qui affiche une interface de type iCal et un before_filter
pour sérialiser de nouveau la vue dans un objet IceCube
, ghetto-ly.
Mon idée est de simplifier l'incrédibilité en ajoutant des attributs récurrents à un modèle et en le connectant facilement dans la vue. Tout en quelques lignes.
Alors qu'est-ce que cela me donne? Attributs indexés, modifiables, récurrents.
events
stocke une instance d'un jour et est utilisé dans la vue du calendrier/l'aide, par exemple task.schedule
stocke l’objet yaml'd IceCube
afin que vous puissiez faire des appels tels que: task.schedule.next_suggestion
.
Récapitulation: J'utilise deux modèles, un à plat pour l'affichage du calendrier et un attribut pour la fonctionnalité.
J'ai développé plusieurs applications basées sur un calendrier et j'ai également créé un ensemble de composants de calendrier JavaScript réutilisables prenant en charge la périodicité. J'ai écrit un aperçu de comment concevoir pour la récurrence qui pourrait être utile à quelqu'un. Bien que certains éléments soient spécifiques à la bibliothèque que j'ai écrite, la grande majorité des conseils offerts sont généraux pour toute implémentation de calendrier.
Quelques points clés:
C'est un sujet très compliqué avec beaucoup, beaucoup d'approches valables pour le mettre en œuvre. Je dirai que j’ai effectivement mis en œuvre la récurrence à plusieurs reprises avec succès et que je me garderais de prendre conseil à ce sujet de la part de ceux qui ne l’ont pas encore fait.
J'utilise le schéma de base de données décrit ci-dessous pour stocker les paramètres de récurrence
http://github.com/bakineggs/recurring_events_for
Ensuite, j'utilise runt pour calculer dynamiquement les dates.
Notez que si vous autorisez les règles de récurrence qui ne se terminent pas, vous devez réfléchir à la manière d'afficher votre quantité d'informations désormais infinie.
J'espère que ça t'as aidé!
Je recommanderais d'utiliser la puissance de la bibliothèque de dates et la sémantique du module range de Ruby. Un événement récurrent est en réalité une heure, une plage de dates (un début et une fin) et généralement un seul jour de la semaine. En utilisant date et 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\"]"
A partir de ces réponses, j'ai en quelque sorte trouvé 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 restent en place et la suppression d'un événement est également facile: vous dissociez simplement un événement, supprimez-le et reliez-le avant et après. Vous devez toujours interroger les événements récurrents chaque fois que quelqu'un regarde une nouvelle période de temps jamais vue auparavant dans le calendrier, mais sinon, c'est assez propre.
Recherchez dans l'article ci-dessous trois bonnes Ruby date/heure. Ice_cube en particulier semble être un choix judicieux pour les règles de récurrence et autres éléments nécessaires à un calendrier d'événements. http: // www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html
Vous pouvez stocker les événements répétés et, si une instance particulière a été modifiée, créer 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 sais pas si vous avez créé votre propre bibliothèque d'événements ou si vous utilisez une bibliothèque existante, ce qui risque de ne pas être possible.
En javascript:
Gestion des plannings récurrents: http://bunkat.github.io/later/
Gestion des événements complexes et des dépendances entre ces planifications: http://bunkat.github.io/schedule/
En gros, 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 sauvegarder dans votre modèle.
Si vous avez un événement récurrent et souhaitez modifier une seule récurrence, vous pouvez utiliser la fonction except () pour ignorer un jour particulier, puis en ajouter un nouveau. Evénement modifié pour cette entrée.
La bibliothèque prend en charge des modèles très complexes, des fuseaux horaires et même des événements cronaux.
Pour les programmeurs .NET prêts à payer des frais de licence, vous trouverez peut-être Aspose.Network utile ... il comprend une bibliothèque compatible iCalendar pour les rendez-vous périodiques.
Stockez les événements de manière répétée et affichez-les de manière dynamique. Toutefois, autorisez l'événement récurrent à contenir une liste d'événements spécifiques susceptibles de remplacer les informations par défaut 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 alors lui demander s'il souhaite mettre à jour pour toutes les instances (détails par défaut) ou juste pour ce jour-là (créer un nouvel événement spécifique et l'ajouter à la liste).
Si un utilisateur demande à supprimer toutes les récurrences de cet événement, vous disposez également de la liste des informations spécifiques et pouvez les supprimer facilement.
Le seul problème serait que l'utilisateur souhaite mettre à jour cet événement et tous les événements futurs. Dans ce cas, vous devrez scinder l'événement récurrent en deux. À ce stade, vous pouvez envisager de lier d’une manière ou d’une autre les événements récurrents afin de pouvoir tous les supprimer.
Vous stockez les événements au format iCalendar directement, ce qui permet la répétition à durée indéterminée, la localisation de fuseau horaire, etc.
Vous pouvez les stocker sur un serveur CalDAV puis, lorsque vous souhaitez afficher les événements, vous pouvez utiliser l'option du rapport défini dans CalDAV pour demander au serveur de développer les événements récurrents sur la période visualisée.
Vous pouvez également les stocker vous-même dans une base de données et utiliser une sorte de bibliothèque d’analyses iCalendar pour procéder à l’expansion, sans avoir besoin de PUT/GET/REPORT pour communiquer avec un serveur CalDAV principal. C’est probablement plus de travail - je suis sûr que les serveurs CalDAV cachent la complexité quelque part.
Le fait de disposer des événements au format iCalendar simplifiera probablement les choses à long terme, car les utilisateurs voudront toujours qu'ils soient exportés pour y intégrer d'autres logiciels.
J'ai simplement implémenté cette fonctionnalité! La logique est la suivante, vous avez d’abord besoin de deux tables. RuleTable stocke des événements généraux ou recycle. ItemTable est des événements de cycle stockés. 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 indéfiniment), se répète pendant 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, bout long), en fonction des heures de début et de fin, Weekly, vous pouvez calculer la collection souhaitée, <cycleA11.20, cycleA 11.27, cycleA 12.4 ......>. En plus du 6 novembre et du reste, je l'ai qualifié d'événement virtuel. Lorsque l'utilisateur modifie un nom d'événement virtuel après (cycleA11.27 par exemple), vous insérez des données dans un élément 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 (long début, long fin), vous utilisez ces données couvrant l'événement virtuel (cycleB11.27) désolé pour mon anglais, j'ai essayé.
Ceci est mon événement récurrentBE:
public static List<Map<String, Object>> recurringData(Context context,
long start, long end) { // 重复事件的模板处理,生成虚拟事件(根据日期段)
long a = System.currentTimeMillis();
List<Map<String, Object>> finalDataList = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> tDataList = BillsDao.selectTemplateBillRuleByBE(context); //RuleTable,just select recurringEvent
for (Map<String, Object> 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<Map<String, Object>> virtualDataList = new ArrayList<Map<String, Object>>();// 虚拟事件
if (virtualLong == startDate) { // 所要求的时间,小于等于父本时间,说明这个是父事件数据,即第一条父本数据
Map<String, Object> bMap = new HashMap<String, Object>();
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<String, Object> bMap = new HashMap<String, Object>();
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<Map<String, Object>>oDataList = BillsDao.selectBillItemByBE(context, start, end);
Log.v("mtest", "特例结果大小" +oDataList );
List<Map<String, Object>> delectDataListf = new ArrayList<Map<String, Object>>(); // finalDataList要删除的结果
List<Map<String, Object>> delectDataListO = new ArrayList<Map<String, Object>>(); // oDataList要删除的结果
for (Map<String, Object> fMap : finalDataList) { // 遍历虚拟事件
int pbill_id = (Integer) fMap.get("_id");
long pdue_date = (Long) fMap.get("ep_billDueDate");
for (Map<String, Object> 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<Map<String, Object>> mOrdinaryList = BillsDao.selectOrdinaryBillRuleByBE(context, start, end);
finalDataList.addAll(mOrdinaryList);
// Log.v("mtest", "finalDataList的大小"+finalDataList.size());
long b = System.currentTimeMillis();
Log.v("mtest", "算法耗时"+(b-a));
return finalDataList;
}