Portfolio Talks About Blog

Scott Wittrock

Optimizing my calendar by automatically scheduling daily tasks with One

May 25, 2020

I’m building an app called One, which I use to complete tasks and create good habits. One automatically schedules the tasks I do habitually based on my calendar events for that day. For example, I’m creating a habit to workout after work. If I schedule a dinner out one evening, the app reschedules my workout session for the morning. If I start faltering on the habits, the app notifies me of which habits are slipping and suggests how I can do a better job.

In this article, I’ll describe my approach to building One to automatically schedule a set of habits onto my calendar every day.

Defining the data model

To keep track of my habits and the number of times I completed one, I needed to create two related models. The first is the base instance of the habit, which contains the data for how often I should do it.

                      
                        
                          interface
                         
                        
                          Habit
                         
                        
                          {
                        
                        title
                        
                          :
                         
                        
                          string
                        
                        
                          ;
                        
                        length
                        
                          :
                         
                        
                          number
                        
                        
                          ;
                        
                        days
                        
                          :
                         
                        
                          Array
                        
                        
                          <
                        
                        
                          boolean
                        
                        
                          >
                        
                        
                          ;
                        
                        habit_target_minute
                        
                          :
                         
                        
                          number
                        
                        
                          ;
                        
                        habit_target_hour
                        
                          :
                         
                        
                          number
                        
                        
                          ;
                        

                        
                          }
                        
                      
                    

The app currently schedules habits to occur once daily at the right time, day, and the number of occurrences week.

Each habit has a sub-collection that shows how often it repeats.

                      
                        
                          export
                         
                        
                          enum
                        
                        Habit_Occurence
                        
                          {
                        
                        complete
                        
                          :
                         
                        
                          boolean
                        
                        
                          ,
                        
                        completed_date
                        
                          :
                        
                        Date
                        
                          ,
                        
                        title
                        
                          :
                         
                        
                          string
                        
                        
                          ,
                        
                        reference_id
                        
                          :
                         
                        
                          string
                        
                        
                          ,
                        
                        start
                        
                          :
                        
                        Date
                        
                          ,
                        
                        end
                        
                          :
                        
                        Date
                        
                          }
                        
                      
                    

Each hour a serverless function runs to create or update the occurrence for the current day. Users can also trigger this update in the app manually.

Determining the best time

Finding time to get your habits done each day is a task itself. The app automates this process by rescheduling habits to a better time for you. If a habit conflicts with an event, the app reschedules the habit.

At the start of the scheduling process, we have an array of events with start and end times, and an array of habit occurrences with start and end dates. We want to update the start and end times of any habit which overlaps an event or another habit. We also want to group tasks into blocks to give you more extended periods of uninterrupted time for focused work or family time.

Defining a successful optimization

To optimize the scheduler, I had to come up with a list of hard constraints and a list of soft constraints. Hard constraints are things that must always be true; for example, One does not reschedule an event like a work meeting. The soft constraints would measure how successful the algorithm is.

Hard constraints:

  • Calendar events cannot move.
  • Habits cannot overlap with events.
  • Habits cannot overlap with other habits.

Soft constraints:

  • Habits are scheduled as close to their target time as possible.
  • Minimize the number of fewer than 30 minutes throughout the day.

To optimize for the soft constraints, I need to figure out the scheduling combination that provides the lowest difference between the targeted start and scheduled start of the habit. This optimization is how I measure the success of the various algorithms I create.

Creating the scheduling algorithm

To schedule my day, I needed to create a virtual day to hold all the events and habits. This process allows me to quickly find times throughout the day and reschedule events by accessing the value in the array direction. The array had a slot for every minute during the day.

The first step was to fill this array with all the events from the day. To do this, I had to convert the start and end date into a local time index, which I mapped to the array. I then created a loop to loop from the starting minute through the end minute and set the array index equal to that event id. This id denoted there was an event scheduled for that time.

                      
                        
                          function
                         
                        
                          addTimesToDay
                        
                        
                          (
                        
                        
                          original
                          
                            :
                           
                          
                            ORIGINAL_EVENT
                          
                          
                            ,
                          
                          currentDay
                          
                            :
                           
                          
                            string
                          
                          
                            [
                          
                          
                            ]
                          
                        
                        
                          )
                        
                        
                          :
                         
                        
                          string
                        
                        
                          [
                        
                        
                          ]
                        
                        
                          {
                        
  
                        
                          const
                        
                        newDay
                        
                          =
                         
                        
                          [
                        
                        
                          ...
                        
                        currentDay
                        
                          ]
                        
                        
                          ;
                        
  
                        
                          let
                        
                        start
                        
                          :
                         
                        
                          number
                         
                        
                          =
                         
                        
                          getMinutes
                        
                        
                          (
                        
                        original
                        
                          .
                        
                        start
                        
                          )
                        
                        
                          ;
                        
  
                        
                          const
                        
                        end
                        
                          :
                         
                        
                          number
                         
                        
                          =
                         
                        
                          getMinutes
                        
                        
                          (
                        
                        original
                        
                          .
                        
                        end
                        
                          )
                        
                        
                          ;
                        
  
                        
                          for
                         
                        
                          (
                        
                        start
                        
                          ;
                        
                        start
                        
                          <
                        
                        end
                        
                          ;
                        
                        start
                        
                          ++
                        
                        
                          )
                         
                        
                          {
                        
    
                        
                          if
                         
                        
                          (
                        
                        newDay
                        
                          [
                        
                        start
                        
                          ]
                        
                        
                          )
                         
                        
                          {
                        
      
                        
                          throw
                         
                        
                          new
                         
                        
                          Error
                        
                        
                          (
                        
                        
                          
                            `
                          
                          
                            Event with id of
                          
                          
                            
                              ${
                            
                            newDay
                            
                              [
                            
                            start
                            
                              ]
                            
                            
                              }
                            
                          
                          
                            is already scheduled at minute
                          
                          
                            
                              ${
                            
                            start
                            
                              }
                            
                          
                          
                            `
                          
                        
                        
                          )
                        
                        
                          ;
                        
    
                        
                          }
                         
                        
                          else
                         
                        
                          {
                        
                        newDay
                        
                          [
                        
                        start
                        
                          ]
                         
                        
                          =
                        
                        original
                        
                          .
                        
                        id
                        
                          ;
                        
    
                        
                          }
                        
  
                        
                          }
                        
  
                        
                          return
                        
                        newDay
                        
                          ;
                        

                        
                          }
                        
                      
                    

The next step was to schedule individual habits. For each habit, I would find the optimal time then append it’s the id to the array to ensure future I didn’t double book myself. To find the optimal time for the habit, I took two approaches.

Block off time for events that cannot be moved first

The first approach was deciding what order to schedule the habits. Once a habit was scheduled, all following habits would shift around the time that was blocked off. I scheduled the habits with low success rates first to ensure they are closest to their scheduled time. I was able to achieve this by sorting habits based on the percentage that the habit had completed over the last 15 attempts.

Calculate all possible times for flexible tasks

The next approach was to find the most optimal time. To achieve this, I took a brute force approach to optimization. Since the number of habits was relatively low (10-20), I calculated all possible times the habit could be scheduled during the day, then took the time with the lowest delta from the target time. This approach ensured the habit was scheduled as close to the target time as possible without double booking the time.

                      
                        
                          function
                         
                        
                          findStartAndEndTime
                        
                        
                          (
                        
                        
                          day
                          
                            :
                           
                          
                            string
                          
                          
                            [
                          
                          
                            ]
                          
                          
                            ,
                          
                          startDate
                          
                            :
                          
                          Date
                          
                            ,
                          
                          endDate
                          
                            :
                          
                          Date
                          
                            ,
                          
                          date
                          
                            :
                           
                          
                            DATE
                          
                        
                        
                          )
                        
                        
                          :
                         
                        
                          {
                        
                        start
                        
                          :
                        
                        Date
                        
                          ,
                        
                        end
                        
                          :
                        
                        Date
                        
                          ,
                        
                        delta
                        
                          :
                         
                        
                          number
                        
                        
                          }
                         
                        
                          {
                        
 
                        
                          // Brute forcing by finding all possiblitites then sorting for the one with the lowest possible delta.
                        
 
                        
                          const
                        
                        possibleTimes
                        
                          :
                         
                        
                          Array
                        
                        
                          <
                        
                        
                          {
                        
                        start
                        
                          :
                        
                        Date
                        
                          ,
                        
                        end
                        
                          :
                        
                        Date
                        
                          ,
                        
                        delta
                        
                          :
                         
                        
                          number
                        
                        
                          }
                        
                        
                          >
                         
                        
                          =
                         
                        
                          [
                        
                        
                          ]
                        
                        
                          ;
                        
 
                        
                          const
                        
                        length
                        
                          :
                         
                        
                          number
                         
                        
                          =
                         
                        
                          (
                        
                        endDate
                        
                          .
                        
                        
                          valueOf
                        
                        
                          (
                        
                        
                          )
                         
                        
                          -
                        
                        startDate
                        
                          .
                        
                        
                          valueOf
                        
                        
                          (
                        
                        
                          )
                        
                        
                          )
                         
                        
                          /
                         
                        
                          1000
                         
                        
                          /
                         
                        
                          60
                        
                        
                          ;
                        
                        day
                        
                          .
                        
                        
                          forEach
                        
                        
                          (
                        
                        
                          (
                        
                        
                          minute
                          
                            :
                           
                          
                            string
                          
                          
                            ,
                          
                          index
                        
                        
                          )
                         
                        
                          =>
                         
                        
                          {
                        
   
                        
                          if
                         
                        
                          (
                        
                        
                          !
                        
                        minute
                        
                          )
                         
                        
                          {
                        
     
                        
                          let
                        
                        open
                        
                          =
                         
                        
                          true
                        
                        
                          ;
                        
     
                        
                          const
                        
                        endIndex
                        
                          =
                        
                        index
                        
                          +
                        
                        length
                        
                          ;
                        
     
                        
                          for
                         
                        
                          (
                        
                        
                          let
                        
                        i
                        
                          =
                        
                        index
                        
                          ;
                        
                        i
                        
                          <
                        
                        endIndex
                        
                          ;
                        
                        i
                        
                          ++
                        
                        
                          )
                         
                        
                          {
                        
       
                        
                          if
                        
                        
                          (
                        
                        day
                        
                          [
                        
                        i
                        
                          ]
                        
                        
                          )
                        
                        
                          {
                        
                        open
                        
                          =
                         
                        
                          false
                        
                        
                          ;
                        
       
                        
                          }
                        
     
                        
                          }
                        
     
                        
                          if
                         
                        
                          (
                        
                        open
                        
                          )
                         
                        
                          {
                        
                        possibleTimes
                        
                          .
                        
                        
                          push
                        
                        
                          (
                        
                        
                          {
                        
                        start
                        
                          :
                         
                        
                          getDate
                        
                        
                          (
                        
                        index
                        
                          ,
                        
                        date
                        
                          .
                        
                        localDate
                        
                          )
                        
                        
                          ,
                        
                        end
                        
                          :
                         
                        
                          getDate
                        
                        
                          (
                        
                        index
                        
                          +
                        
                        length
                        
                          ,
                        
                        date
                        
                          .
                        
                        localDate
                        
                          )
                        
                        
                          ,
                        
                        delta
                        
                          :
                        
                        Math
                        
                          .
                        
                        
                          abs
                        
                        
                          (
                        
                        
                          (
                        
                        startDate
                        
                          .
                        
                        
                          valueOf
                        
                        
                          (
                        
                        
                          )
                         
                        
                          -
                         
                        
                          getDate
                        
                        
                          (
                        
                        index
                        
                          ,
                        
                        date
                        
                          .
                        
                        localDate
                        
                          )
                        
                        
                          .
                        
                        
                          valueOf
                        
                        
                          (
                        
                        
                          )
                        
                        
                          )
                         
                        
                          /
                         
                        
                          1000
                         
                        
                          /
                         
                        
                          60
                        
                        
                          )
                        
       
                        
                          }
                        
                        
                          )
                        
                        
                          ;
                        
     
                        
                          }
                        
   
                        
                          }
                        
 
                        
                          }
                        
                        
                          )
                        
                        
                          ;
                        
                        possibleTimes
                        
                          .
                        
                        
                          sort
                        
                        
                          (
                        
                        
                          function
                         
                        
                          compare
                        
                        
                          (
                        
                        
                          a
                          
                            ,
                          
                          b
                        
                        
                          )
                         
                        
                          {
                        
   
                        
                          if
                         
                        
                          (
                        
                        a
                        
                          .
                        
                        delta
                        
                          >
                        
                        b
                        
                          .
                        
                        delta
                        
                          )
                         
                        
                          return
                         
                        
                          1
                        
                        
                          ;
                        
   
                        
                          if
                         
                        
                          (
                        
                        b
                        
                          .
                        
                        delta
                        
                          >
                        
                        a
                        
                          .
                        
                        delta
                        
                          )
                         
                        
                          return
                         
                        
                          -
                        
                        
                          1
                        
                        
                          ;
                        
   
                        
                          return
                         
                        
                          0
                        
                        
                          ;
                        
 
                        
                          }
                        
                        
                          )
                        
                        
                          ;
                        
  
                        
                          return
                        
                        possibleTimes
                        
                          [
                        
                        
                          0
                        
                        
                          ]
                        
                        
                          ;
                        

                        
                          }
                        
                      
                    

Validating the results

To compare these results to future modifications, I created a validator to measure how well the algorithm met the soft constraints. The validator averages the delta between the target and the scheduled start of all habits. It also measures the number of empty blocks less than 30 minutes. The number of empty blocks is a metric I want to optimize next to avoid having short blocks of empty time throughout the day.

Machine learning for the win

By setting hard and soft constraints, I was able to define a successful algorithm or machine learning process. While my initial approach uses a brute force technique to schedule new habits, I created a framework to measure future machine learning models. As I refine the scheduling algorithm, I can utilize this validator to determine if they achieve a better result.

References : I used a thesis paper by Siddharth Dahiya titled Course Scheduling with Preference Optimization helpful in determining the best approach to this problem.


Written by Scott Wittrock a developer and product manager. I lead teams who create APIs, SDKs, Design Systems, and Cross-Platform Apps. You should follow me on Twitter