exupero's blog
RSSApps

Relative schedules

In a complex schedule, events often depend on the start or finish of other events. In Clojure, we might define such a schedule with relative offsets:

(def events
  '[[plan :starts 0]
    [plan :ends [plan :starts + 3]]
    [design :starts [plan :starts + 2]]
    [design :ends [design :starts + 5]]
    [coding :starts [design :starts + 3]]
    [coding :ends [coding :starts + 7]]
    [testing :starts [coding :starts + 1]]
    [testing :ends [coding :ends + 1]]
    [user-testing :starts [design :starts + 2]]
    [user-testing :ends [coding :ends + 2]]])

To convert these relative offsets to absolute times, we can use a function that recursively searches the list of events until it finds one with an absolute time, then applies offsets:

(defn absolute-time [[entity attribute] events]
  (some (fn [[entity2 attribute2 definition]]
          (when (= [entity attribute] [entity2 attribute2])
            (if (number? definition)
              definition
              (let [[entity3 attribute3 f d] definition
                    f (eval f)]
                (f (absolute-time [entity3 attribute3] events) d)))))
        events))

Because I've quoted the data, we have to eval the function symbol to get the actual function. If we used strings for the entity names it wouldn't be necessary to quote the data, but having + as a symbol rather than a value will make some outputs easier to read later.

Now we can get the absolute times for each event:

(map (fn [[entity attribute]]
       [entity attribute (absolute-time [entity attribute] events)])
     events)
([plan :starts 0]
 [plan :ends 3]
 [design :starts 2]
 [design :ends 7]
 [coding :starts 5]
 [coding :ends 12]
 [testing :starts 6]
 [testing :ends 13]
 [user-testing :starts 4]
 [user-testing :ends 14])

If we group events by entity, we can turn this schedule into a simple Gantt chart:

(def data
  (->> events
    (map (fn [[entity attribute]]
           [entity attribute (absolute-time [entity attribute] events)]))
    (group-by first)
    (map (fn [[entity events]]
           [entity (into {} (map #(vec (drop 1 %))) events)]))))
([plan {:starts 0, :ends 3}]
 [design {:starts 2, :ends 7}]
 [coding {:starts 5, :ends 12}]
 [testing {:starts 6, :ends 13}]
 [user-testing {:starts 4, :ends 14}])
012345678910111213plandesigncodingtestinguser-testing

This setup will be the basis for a more complex Gantt chart in the next post.