¿Cuál es la mejor manera de modelar eventos recurrentes en una aplicación de calendario?

Estoy construyendo una aplicación de calendario grupal que necesita admitir eventos recurrentes, pero todas las soluciones que he encontrado para manejar estos eventos parecen un truco. Puedo limitar qué tan adelante uno puede mirar, y luego generar todos los eventos a la vez. O puedo almacenar los eventos como repetitivos y mostrarlos dinámicamente cuando miro hacia adelante en el calendario, pero tendré que convertirlos a un evento normal si alguien quiere cambiar los detalles en una instancia particular del evento.

Estoy seguro de que hay una mejor manera de hacerlo, pero aún no lo he encontrado. ¿Cuál es la mejor manera de modelar eventos recurrentes, donde puede cambiar detalles o eliminar instancias de eventos particulares?

(Estoy usando Ruby, pero no permita que eso limite su respuesta. Sin embargo, si hay una biblioteca específica de Ruby o algo así, es bueno saberlo).

Usaría un concepto de ‘enlace’ para todos los eventos recurrentes futuros. Se muestran dinámicamente en el calendario y se vinculan a un único objeto de referencia. Cuando se han producido eventos, el enlace se rompe y el evento se convierte en una instancia independiente. Si intenta editar un evento recurrente, solicite que cambie todos los elementos futuros (es decir, cambie la referencia de enlace único) o cambie solo esa instancia (en cuyo caso conviértalo a una instancia independiente y luego realice el cambio). El último recuadro es ligeramente problemático, ya que necesita mantener un registro en su lista periódica de todos los eventos futuros que se convirtieron en instancia única. Pero, esto es completamente factible.

Entonces, en esencia, tienen 2 clases de eventos: instancias únicas y eventos recurrentes.

Martin Fowler – Eventos recurrentes para calendarios contiene algunas ideas y patrones interesantes.

Runt gem implementa este patrón.

Puede haber muchos problemas con los eventos recurrentes, permítanme destacar algunos que conozco.

Solución 1: no hay instancias

Almacene citas originales + datos recurrentes, no almacene todas las instancias.

Problemas:

  • Deberá calcular todas las instancias en una ventana de fecha cuando las necesite, costosas
  • No se pueden manejar las excepciones (es decir, se elimina una de las instancias o se mueve, o mejor dicho, no se puede hacer esto con esta solución)

Solución 2: instancias de la tienda

Almacene todo desde 1, pero también todas las instancias, vinculado a la cita original.

Problemas:

  • Toma mucho espacio (pero el espacio es barato, tan pequeño)
  • Las excepciones deben manejarse con elegancia, especialmente si regresa y edita la cita original después de hacer una excepción. Por ejemplo, si mueve la tercera instancia un día hacia adelante, ¿qué sucede si retrocede y edita la hora de la cita original, vuelve a insertar otra en el día original y abandona la que se movió? Desenlazar el movido? Intenta cambiar el movido apropiadamente?

Por supuesto, si no va a hacer excepciones, entonces cualquiera de las soluciones debería estar bien, y básicamente elige un escenario de intercambio de tiempo / espacio.

Es posible que desee consultar las implementaciones del software iCalendar o el estándar en sí ( RFC 2445 RFC 5545 ). Los que vienen a la mente rápidamente son los proyectos de Mozilla http://www.mozilla.org/projects/calendar/ Una búsqueda rápida también revela http://icalendar.rubyforge.org/ .

Se pueden considerar otras opciones dependiendo de cómo vaya a almacenar los eventos. ¿Estás construyendo tu propio esquema de base de datos? ¿Usando algo basado en iCalendar, etc.?

Estoy trabajando con lo siguiente:

y una gem en progreso que se extiende formtastic con un tipo de entrada: recurrente ( form.schedule :as => :recurring ), que representa una interfaz tipo iCal y un before_filter para serializar la vista en un objeto IceCube nuevo, ghetto-ly.

Mi idea es hacer que sea increíble la facilidad de agregar atributos recurrentes a un modelo y conectarlo fácilmente en la vista. Todo en un par de líneas.


Entonces, ¿qué me da esto? Indexado, editable, atributos recurrentes.

events almacena una instancia de un solo día, y se usa en la vista de calendario / helper say task.schedule almacena el objeto IceCube , por lo que puede hacer llamadas como: task.schedule.next_suggestion .

Recapitulación: utilizo dos modelos, uno plano, para la visualización del calendario, y uno atribuido para la funcionalidad.

He desarrollado múltiples aplicaciones basadas en calendario, y también he creado un conjunto de componentes de calendario JavaScript reutilizables que admiten recurrencia. Escribí una descripción general de cómo diseñar para la recurrencia que podría ser útil para alguien. Si bien hay algunos bits que son específicos de la biblioteca que escribí, la gran mayoría de los consejos que se ofrecen son generales para cualquier implementación de calendario.

Algunos de los puntos clave:

  • Repetición de tienda utilizando el formato iCal RRULE : esa es una rueda que realmente no desea reinventar
  • ¡NO almacene instancias individuales de eventos recurrentes como filas en su base de datos! Siempre almacene un patrón de recurrencia.
  • Hay muchas maneras de diseñar su esquema de evento / excepción, pero se proporciona un ejemplo básico de punto de partida
  • Todos los valores de fecha / hora deben almacenarse en UTC y convertirse a local para su visualización
  • La fecha de finalización almacenada para un evento recurrente siempre debe ser la fecha de finalización del rango de repetición (o la “fecha máxima” de su plataforma si se repite “para siempre”) y la duración del evento debe almacenarse por separado. Esto es para asegurar una forma sensata de consultar eventos posteriores.
  • Se incluye alguna discusión sobre la generación de instancias de eventos y estrategias de edición recurrente

Es un tema realmente complicado con muchos, muchos enfoques válidos para implementarlo. Diré que en realidad he implementado recurrencia varias veces con éxito, y que sería cauteloso de tomar consejos sobre este tema de cualquier persona que no lo haya hecho realmente.

Estoy usando el esquema de la base de datos como se describe a continuación para almacenar los parámetros de recurrencia

http://github.com/bakineggs/recurring_events_for

Luego uso Runt para calcular dinámicamente las fechas.

https://github.com/mlipper/runt

  1. Mantenga un registro de una regla de recurrencia (probablemente basada en iCalendar, por @ Kris K. ). Esto incluirá un patrón y un rango (Cada tercer martes, para 10 ocurrencias).
  2. Para cuando quiera editar / eliminar una ocurrencia específica, haga un seguimiento de las fechas de excepción para la regla de recurrencia anterior (fechas en las que el evento no ocurre como lo especifica la regla).
  3. Si eliminó, eso es todo lo que necesita, si ha editado, cree otro evento y asígnele un ID principal establecido para el evento principal. Puede elegir si desea incluir toda la información del evento principal en este registro, o si solo contiene los cambios y hereda todo lo que no cambia.

Tenga en cuenta que si permite reglas de recurrencia que no terminan, debe pensar en cómo mostrar su cantidad infinita de información.

¡Espero que ayude!

Recomiendo usar el poder de la biblioteca de fechas y la semántica del módulo de rango de ruby. Un evento recurrente es realmente un tiempo, un rango de fechas (un comienzo y un final) y generalmente un solo día de la semana. Usando fecha y rango puede responder cualquier pregunta:

 #!/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 

¡Produce todos los días del evento, incluido el año bisiesto!

 # =>"[\"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 estas respuestas, de alguna forma he filtrado una solución. Realmente me gusta la idea del concepto de enlace. Los eventos recurrentes podrían ser una lista vinculada, con la cola conociendo su regla de recurrencia. Cambiar un evento sería fácil, porque los enlaces permanecen en su lugar, y eliminar un evento también es fácil: simplemente desenlaza un evento, lo elimina y vuelve a vincular el evento antes y después. Todavía tiene que consultar eventos recurrentes cada vez que alguien mira un nuevo período de tiempo nunca antes visto en el calendario, pero de lo contrario, esto es bastante limpio.

Puede almacenar los eventos como repetidos, y si se editó una instancia en particular, cree un nuevo evento con el mismo ID de evento. Luego, al buscar el evento, busque todos los eventos con el mismo ID de evento para obtener toda la información. No estoy seguro de si ha creado su propia biblioteca de eventos, o si está utilizando una existente, por lo que es posible que no sea posible.

Consulte el siguiente artículo para ver tres buenas bibliotecas de fecha / hora de ruby. ice_cube, en particular, parece una opción sólida para las reglas de recurrencia y otras cosas que un calendario de eventos necesitaría. http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html

En javascript:

Manejando horarios recurrentes: http://bunkat.github.io/later/

Manejando eventos complejos y dependencias entre esos horarios: http://bunkat.github.io/schedule/

Básicamente, usted crea las reglas y le pide a la lib que calcule los próximos N eventos recurrentes (especificando un rango de fechas o no). Las reglas se pueden analizar / serializar para guardarlas en su modelo.

Si tiene un evento recurrente y desea modificar solo una repetición, puede usar la función except () para descartar un día en particular y luego agregar un nuevo evento modificado para esta entrada.

La lib admite patrones muy complejos, zonas horarias e incluso eventos de cronificación.

Almacene los eventos como repetidos y visualícelos dinámicamente, sin embargo permita que el evento recurrente contenga una lista de eventos específicos que podrían anular la información predeterminada en un día específico.

Cuando consulta el evento recurrente, puede verificar una anulación específica para ese día.

Si un usuario realiza cambios, puede preguntar si desea actualizar todas las instancias (detalles predeterminados) o solo ese día (crear un nuevo evento específico y agregarlo a la lista).

Si un usuario solicita eliminar todas las repeticiones de este evento, también tiene la lista de detalles a mano y puede eliminarlos fácilmente.

El único caso problemático sería si el usuario desea actualizar este evento y todos los eventos futuros. En ese caso, tendrá que dividir el evento recurrente en dos. En este punto, es posible que desee considerar vincular eventos recurrentes de alguna manera para que pueda eliminarlos todos.

Para los progtwigdores .NET que están dispuestos a pagar algunas tarifas de licencia, es posible que Aspose.Network sea útil … incluye una biblioteca compatible con iCalendar para citas recurrentes.

Almacena los eventos en formato iCalendar directamente, lo que permite la repetición abierta, la localización de la zona horaria, etc.

Puede almacenarlos en un servidor CalDAV y luego, cuando desee mostrar los eventos, puede usar la opción del informe definido en CalDAV para solicitar al servidor que amplíe los eventos recurrentes en el período de visualización.

O puede almacenarlos en una base de datos usted mismo y usar algún tipo de biblioteca de análisis iCalendar para hacer la expansión, sin necesidad de PUT / GET / REPORT para hablar con un servidor CalDAV de back-end. Probablemente sea más trabajo. Estoy seguro de que los servidores CalDAV ocultan la complejidad en alguna parte.

Tener los eventos en formato iCalendar probablemente hará las cosas más simples a largo plazo, ya que las personas siempre querrán que se exporten para poner en otro software de todos modos.

¡Simplemente implementé esta característica! La lógica es la siguiente, primero necesitas dos tablas. RuleTable store general o recicla eventos paternos. ItemTable es un evento de ciclo almacenado. Por ejemplo, cuando crea un evento cíclico, la hora de inicio para el 6 de noviembre de 2015, la hora de finalización del ciclo del 6 de diciembre (o para siempre), durante una semana. Inserta datos en una tabla de reglas, los campos son los siguientes:

 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. 

Ahora desea consultar los datos del 20 de noviembre al 20 de diciembre. Puede escribir una función RecurringEventBE (inicio largo, final largo), basada en la hora de inicio y finalización, WeekLy, puede calcular la recostackción que desea, . Además del 6 de noviembre, y el rest lo llamé un evento virtual. Cuando el usuario cambia el nombre de un evento virtual después de (cycleA11.27 por ejemplo), inserta un dato en un ItemTable. Los campos son los siguientes:

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

En la función RecurringEventBE (inicio largo, final largo), utiliza esta información que cubre el evento virtual (ciclo B11.27) lo siento sobre mi inglés, lo intenté.

Este es mi RecurringEventBE:

 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; } 

¿Qué sucede si tiene una cita recurrente sin fecha de finalización? Por barato que sea el espacio, no tiene espacio infinito, por lo que la Solución 2 no arranca allí …

Puedo sugerir que “sin fecha de finalización” se pueda resolver hasta una fecha final a fines de siglo. Incluso para un evento diario, la cantidad de espacio sigue siendo barata.