33

I'm going to make a Mysql based calendar system where you can have repeating pattern for lets say every monday forever and ever. It must also cover static/once-only events. What I'm wondering about, is which solution would be most logical (and best) for me to use. I have four methods which I'm wondering to chose between.

Method #1

Make a function which accepts parameters from and to. This function would create a temporary table table which imports existing static schedule through INSERT ... SELECT. Afterward it would read of the pattern table and populate the temporary table through the peroid based on from and to.

This solution seems nice from the point of view that queries will be simplier to fetch data with and it works into infinity since you can just repopulate the table depending of which month you're loading. What I'm curious about is whenever this might be a laggy way to do it or not.

Method #2

Create and join given patterns through a subquery and JOIN with static calendar.

This seems to be rather annoying since the queries would be a lot more bigger and would probably not be good at all(?).

Method #3

Basicly just INSERT pattern for lets say one year ahead. Then I guess a cron job would repopulate to make it one year ahead always.

This is a simple way to do it, but it feels like a lot of unneeded data stored and it doesn't really give the infinity which I'm after.

Method #4 (Suggested by Veger)

If I understand correctly, this method would fetch the pattern from another query and creates events upon execution. It's similar to my thoughts regarding Method #1 in that way that I consider simple pattern to create several rows.

However if this would be implemented outside Mysql, I would loose some database functionality which I'm after.


I hope you guys understood my situation, and if you could suggest either given and argue why it's the best or give another solution.

Personally I like the Method #1 the most, but I'm curious if it's laggy to repopulate the calendar table each and every call.

hakre
  • 193,403
  • 52
  • 435
  • 836
Robin Castlin
  • 10,956
  • 1
  • 28
  • 44
  • #3 is what most calendaring systems do but they have a config item that lets you set how far into the future to "look." that way it's fast for the period you defined to look a head to; but slower and less data on the period after. Not a this or that answer but a this AND that answer. – xQbert Jun 10 '12 at 22:03
  • Yeah that's a rather good solution which I will consider. I somehow can't drop the feeling "This isn't true pattern", but maybe I'll disregard that feeling if needed :) – Robin Castlin Jun 10 '12 at 22:07
  • 2
    How about a method #4 where you grab the pattern repetitions separately (maybe even from another table) and add the events using this information for the period that was requested? This only stores the pattern and not all occurrences, might be more resource efficient..? Although I do not know for sure... – Veger Jun 10 '12 at 22:22
  • Thanks, I've added your method to the question, and I hope I understood it fully. – Robin Castlin Jun 11 '12 at 07:14
  • Do you also plan to store once-only events in that calendar? – Salman A Jun 11 '12 at 07:47
  • Yes I do. The idea it to have patterns and once-only events together and to prio once-only over the pattern upon conflicts. – Robin Castlin Jun 11 '12 at 07:58
  • (In regard of your bounty) Your question is not very concrete. I hardly doubt the answers will be add more substance. To which kind of temporal patterns are you referring to and how should the database be queried? – hakre Dec 27 '12 at 12:28
  • You question is not very concrete. I hardly doubt the answers will be add more substance. To which kind of temporal patterns are you referring to and how should the database be queried? – hakre Dec 27 '12 at 12:29

8 Answers8

14

I have built this kind of calendar before. I found the best way to do it is to approach it the way that crons are scheduled. So in the database, make a field for minute, hour, day of month, month, and day of week.

For an event every Friday in June and August at 10:00pm your entry would look like

Minute  Hour  DayOfMonth  Month  DayOfWeek
 0       22     *          6,8       5

You could then have a field that flags it as a one time event which will ignore this information and just use the start date and duration. For events that repeat that end eventually (say every weekend for 3 months) you just need to add an end date field.

This will allow you to select it back easily and reduce the amount of data that needs to be stored. It simplifies your queries as well.

I don't think there is a need to create temporary tables. To select back the relevant events you would select them by the calendar view. If your calendar view is by the month, your select would look something like:

SELECT Events.* 
FROM Events 
WHERE (Month LIKE '%,'.$current_month.',%' OR Month = '*') 
    AND DATE(StartDate) >= "'.date('Y-m-d', $firstDayOfCurrentMonth).'" 
    AND DATE(EndDate) <= "'.date('Y-m-d', $lastDayOfCurrentMonth).'"

Obviously this should be in a prepared statement. It also assumes that you have a comma before and after the first and last value in the comma separated list of months (ie. ,2,4,6,). You could also create a Month table and a join table between the two if you would like. The rest can be parsed out by php when rendering your calendar.

If you show a weekly view of your calendar you could select in this way:

SELECT Events.* 
FROM Events 
WHERE (DayOfMonth IN ('.implode(',', $days_this_week).','*') 
    AND (Month LIKE '%,'.$current_month.',%' OR Month = '*')) 
    AND DATE(StartDate) >= "'.date('Y-m-d', $firstDayOfCurrentMonth).'" 
    AND DATE(EndDate) <= "'.date('Y-m-d', $lastDayOfCurrentMonth).'"

I haven't tested those queries so there maybe some messed up brackets or something. But that would be the general idea.

So you could either run a select for each day that you are displaying or you could select back everything for the view (month, week, etc) and loop over the events for each day.

Ragnar123
  • 5,174
  • 4
  • 24
  • 34
  • Thanks. That's half the answer I'm looking for. Personally I'd imagine it to be easier to save `start_unix`, `interval`, `format`, `amount` and `end_unix (can be NULL)`. Format 1 minute would be 60, format 1 hour would be 3600. Then you'd simply run (start_unix * (interval * format * i)) to get the unix of every occurence. Anyhow, I'm more interested in how I would fetch this data. Preferably I'd like to have everything gathered in same table to do all my fetchings etc. But I'm concerned if creating temporary table each page load may be laggy. – Robin Castlin Jun 13 '12 at 11:15
9

I like Veger's solution best .. instead of populating multiple rows you can just populate the pattern. I suggest the crontab format .. it works so well anyway.

You can query all patterns for a given customer when they load the calendar and fill in events based on the pattern. Unless you have like thousands of patterns for a single user this should not be all that slow. It should also be faster than storing a large number of row events for long periods. You will have to select all patterns at once and do some preprocessing but once again, how many patterns do you expect per user? Even 1000 or so should be pretty fast.

Explosion Pills
  • 188,624
  • 52
  • 326
  • 405
  • Would this be implemented so I can use it alongside with MySQL or would I have to calculate this along with fetched query of once-only events? – Robin Castlin Jun 11 '12 at 13:39
  • All of your events would use this system (you can store once-only events in this format as well). The pattern is stored in the DB rather than the event date itself, but the pattern may apply to a single date. – Explosion Pills Jun 11 '12 at 14:51
4

I've had this idea since I was still programming in GW Basic ;-) though, back then, I took option #3 and that was it. Looking back at it, and also some of the other responses, this would be my current solution.

table structure

start (datetime)
stop (datetime, nullable)
interval_unit ([hour, day, week, month, year?])
interval_every (1 = every <unit>, 2 every two <units>, etc.)
type ([positive (default), negative]) - will explain later

Optional fields:

title
duration

The type field determines how the event is treated:

  1. positive; normal treatment, it shows up in the calendar
  2. negative; this event cancels out another (e.g. every Monday but not on the 14th)

helper query

This query will narrow down the events to show:

SELECT * FROM `events`
WHERE `start` >= :start AND (`stop` IS NULL OR `stop` < :stop)

Assuming you query a range by dates alone (no time component), the the value of :stop should be one day ahead of your range.

Now for the various events you wish to handle.

single event

start = '2012-06-15 09:00:00'
stop = '2012-06-15 09:00:00'
type = 'positive'

Event occurs once on 2012-06-15 at 9am

bounded repeating event

start = '2012-06-15 05:00:00'
interval_unit = 'day'
interval_every = 1
stop = '2012-06-22 05:00:00'
type = 'positive'

Events occur every day at 5am, starting on 2012-06-15; last event is on the 22nd

unbounded repeating event

start = '2012-06-15 13:00:00'
interval_unit = 'week'
interval_every = 2
stop = null
type = 'positive'

Events occur every two weeks at 1pm, starting on 2012-06-15

repeating event with exceptions

start = '2012-06-15 16:00:00'
interval_unit = 'week'
interval_every = 1
type = 'positive'
stop = null

start = '2012-06-22 16:00:00'
type = 'negative'
stop = '2012-06-22 16:00:00'

Events occur every week at 4pm, starting on 2012-06-22; but not on the 22nd

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • If I understand you correctly, you mean that I should save pattern and single events in the same table and extract all occurences between `start` and `stop`? Would I then calculate in php when the actual schedule may occurs? Preferably, but maybe I'm just a bit spoiled, I'd like to fetch them as "normal" events through simple MySQL, but maybe that's just a dream that won't come true? – Robin Castlin Jun 14 '12 at 07:42
  • @RobinCastlin The SQL will help you narrow down the events, so if most of your events are "singular" the extra php code is negligible. – Ja͢ck Jun 14 '12 at 08:10
  • I'm planning to handle the events in more ways then a echo'ing a simple calendar. I want to present them in a list for instance aswell. I plan on filling maybe 70% of a week with patterns with maybe 5 - 10 patterns. Which makes it seem comprehensive to php calcuate and filter afterwards. – Robin Castlin Jun 14 '12 at 08:32
  • @RobinCastlin okay, but what kind of data sizes are you expecting though? This stuff should be pretty cacheable as well. – Ja͢ck Jun 14 '12 at 08:36
  • The data size won't be too big. How would you suggest caching? – Robin Castlin Jun 14 '12 at 08:44
  • @RobinCastlin at this point I can't give much advice on how to cache, other than by time slice perhaps? more details about the use-case would help here :) – Ja͢ck Jun 14 '12 at 08:53
  • I appreciate your help. I'll study this some and see what I come up with. You'll get the correct answer if I end up using your structure suggestion :) – Robin Castlin Jun 14 '12 at 09:04
  • @RobinCastlin no problem! let me know when you want any feedback from me :) – Ja͢ck Jun 14 '12 at 09:05
0

I would suggest something around the lines of this: Split your Events table into 2 because there are clearly 2 different types recurring events and static events and depending on the type they will have different attributes.

Then for a given Event look-up you would run 2 queries, one against each Event Type. For the static events table you would defiantly need (at least) one datetime field so the lookup for a given month would simply use that feild in the conditions (where event_date > FirstDayOfTheMonth and event_date < LastDayOfTheMonth ). Same logic for a weekly/yearly view.

This result set would be combined with a second result set from the recurring events table. Possible attributes could be similar to crontab entries, using day of week/day of month as the 2 main variables. If you're looking at a monthly view,

select * from recurring_events where DayOfWeek in (1,2,3,4,5,6,7) or (DayOfMonth > 0 and DayOfMonth < @NumberOfDaysInThisMonth )

Again similar if for a weekly/yearly view. To make this even simpler to interface, use stored procedures with all the logic for determining 'which days of the week are found between date A and date B'.

Once you have both result sets, you could aggregate them together in the client then display them together. The adavantage to this is there will be no need for "mock/empty records" nor async cronjobs which pre-fill, the queries could easily happen on the fly and if performance actually degrades, add a caching layer, especially for a system of this nature a cache makes perfect sense.

smassey
  • 5,875
  • 24
  • 37
0

I'm actually looking for something similar to this and my solution so far (on paper I didn't start to structure or code yet) stores in 2 tables:

  1. the "events" would get the date of the first occurrence, the title and description (plus the auto-increment ID).

  2. the "events_recursion" table would cross reference to the previous table (with an event_id field for instance) and could work in 2 possible ways:

    2.A: store all the occurrences by date (i.e. one entry for every occurrence so 4 if yo want to save "every friday of this month" or 12 for "the 1st of every month in 2012")

    2.B: or saving the interval (I would save it in seconds) from the date of the first event in a field + the date of the last occurrence (or end of recursion) in another field such as

ID: 2 EVENT_ID: 1 INTERVAL: 604800 (a week if I'm not mistaken) END: 1356912000 (should be the end of this year)

Then when you open the php that shows the schedule it would check for the event still active in that month with a joint between the two tables.

The reason why I would use 2 tables cross-referenced instead of saving all in one tables just comes from the facts that my projects sees very crazy events such as "every fridays AND the 3rd monday of every month" (that in this case would be 1 entry in the events tables and 2 with same "event_id" field in the second table. BTW my projects is for music teachers that here got small work on strict schedules decided 3 or 6 months at a time and are a real mess).

But as I have said i haven't started yet so I'm looking forward to seeing your solution.

PS: please forgive (and forget) my english, first isn't my language and second it is pretty late night and I'm sleepy

regrunge
  • 91
  • 1
  • 8
0

Maybe check out some great ideas from MySQL Events

and some more:
http://phpmaster.com/working-with-mysql-events/?utm_source=rss&utm_medium=rss&utm_campaign=phpmaster-working-with-mysql-events

http://dev.mysql.com/doc/refman/5.5/en/create-event.html

Pentium10
  • 204,586
  • 122
  • 423
  • 502
  • What a convenient solution that would be perfect if I wanted lets say have every static event given a year ahead. That would also help refering to every event through IDs and similiar. I'll definitely consider this approach. – Robin Castlin Jun 17 '12 at 20:51
0

the best solution depends on whether you want to favor standard compliance (RFC5545) or working exclusively within MySQL.

depend on on flexible your recurrence rule engine needs to be. If you want simple rules (every 1st of month or every January, ...) then the solutions offered above have been detailed at length.

However should you want your application to offer compatibility with existing standards (RFC5545) which involves much more complex rules you should have a look at this SO post when building a calendar app, should i store dates or recurrence rules in my database?

Community
  • 1
  • 1
Auberon Vacher
  • 4,655
  • 1
  • 24
  • 36
0

I would do it as I explained here. It will create an infinite calander:

PHP/MySQL: Model repeating events in a database but query for date ranges

The downside is that there will be some calculation during the query. If you need a high performance website, preloading the data will be the way to go. You dont even have to preload all the events in the calendar, to make it possible for easy changing the values in a single event. But it would be wise to store all dates from now till ....

Now using cached values does make it less infinite, but it will increase speed.

Copy of awnser for easy access:

I would create a tally table with just one col called id and fill that table with numbers from 0 to 500. Now we easily use that to make selections instead of using a while loop.

Id
-------------------------------------
0
1
2
etc...

Then i'd store the events in a table with Name as varchar, startdate as datetime and repeats as int

Name    | StartDate            |   Repeats
-------------------------------------
Meeting | 2012-12-10 00:00:00  |   7
Lunch   | 2012-12-10 00:00:00  |   1

Now we can use the tally table to select all dates between two dates by using:

SELECT DATE_ADD('2012-12-09 00:00:00',INTERVAL Id DAY) as showdate
FROM `tally`
WHERE (DATE_ADD('2012-12-09 00:00:00',INTERVAL Id DAY)<='2012-12-20 00:00:00')
ORDER BY Id ASC


ShowDate
-------------------------------------
2012-12-09 00:00:00
2012-12-10 00:00:00
2012-12-11 00:00:00
2012-12-12 00:00:00
2012-12-13 00:00:00
2012-12-14 00:00:00
2012-12-15 00:00:00
2012-12-16 00:00:00
2012-12-17 00:00:00
2012-12-18 00:00:00
2012-12-19 00:00:00
2012-12-20 00:00:00

Then we join this on the events table to calculate the difference between the startdate and the showdate. We devided the results of this by the repeats column and if the remainder is 0, we have match.

All combined becomes:

SELECT E.Id, E.Name, E.StartDate, E.Repeats, A.ShowDate, DATEDIFF(E.StartDate, A.ShowDate) AS diff
FROM events AS E, (
    SELECT DATE_ADD('2012-12-09 00:00:00',INTERVAL Id DAY) as showdate
    FROM `tally`
    WHERE (DATE_ADD('2012-12-09 00:00:00',INTERVAL Id DAY)<='2012-12-20 00:00:00')
    ORDER BY Id ASC
) a
WHERE MOD(DATEDIFF(E.StartDate, A.ShowDate), E.Repeats)=0
AND A.ShowDate>=E.StartDate

Which results in

Id  | Name       |StartDate             | Repeats   | ShowDate              | diff
---------------------------------------------------------------------------------
1   | Meeting    | 2012-12-10 00:00:00  | 7         | 2012-12-10 00:00:00   | 0
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-10 00:00:00   | 0
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-11 00:00:00   | -1
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-12 00:00:00   | -2
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-13 00:00:00   | -3
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-14 00:00:00   | -4
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-15 00:00:00   | -5
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-16 00:00:00   | -6
1   | Meeting    | 2012-12-10 00:00:00  | 7         | 2012-12-17 00:00:00   | -7
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-17 00:00:00   | -7
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-18 00:00:00   | -8
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-19 00:00:00   | -9
2   | Lunch      | 2012-12-10 00:00:00  | 1         | 2012-12-20 00:00:00   | -10

Now you could (and should!) speed things up. For instance by directly storing dates in a table so you can just select all dates directly instead of using a tally table with dateadd. Every thing you can cache and dont have to calculate again is good.

Community
  • 1
  • 1
Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72