The Working Time Problem

When you assign a task, you want to make sure that task gets done. In Appian, we use the built-in escalation and exception mechanism to manage task completion.

If there is a lot of time available, such as several days or even weeks, we can easily define those times and ignore that an exception might occur during non-working time. However, when the available time is limited, this becomes a problem because you want to give employees the opportunity to respond during normal business hours.

Appian provides some capabilities we can use to define working time, and check whether a certain time is within that time.

For an application I am working on, I needed a more flexible way to define working time and an algorithm to automatically shift a time into working time. I will try my best to explain the method and also share the code with you.

The Idea

I define working time as a daily period with a start and an end, adding a way to configure the working days. I store this in three values:

  • workingDays: A list of 7 booleans defines the working days.
  • workingHoursStart: Start of working activities.
  • workingHoursEnd: End of working activities.

Then I developed an algorithm that checks whether a time is within a valid period and shifts it as necessary.

Working Hours

Ignoring the date, I just check whether the time is before the beginning of the working period. If yes, I modify the full timestamp to the start of the working hours.

In case the time is later than the working hours, I modify the full timestamp to the beginning of the working hours at the next day.

This ignores the working day definition, which is taken care of in the next step.

Working Days

To check whether the now modified time is on a working day, I just determine the day of the week, and check the boolean at that index in the list of working days.

Modifying the date is a bit more tricky. Looking at my list of booleans, I need to find the next TRUE value starting at the day of the week as an index. When this is on a Saturday or Sunday, then there is not much left.

I solve this by appending the list of workdays to the remaining days of the current week. The index of the first TRUE value is the number of days I need to add. Study the code snippet below before you continue.

local!daysOffset: lookup( /* Find index of first TRUE value */
  append(
    ldrop(local!workingDays, local!dayOfWeek), /* Remaining days of current week */
    local!workingDays /* Next week */
  ),
  true
),

I added some comments in the full code to make this clear.

Full Working Code

a!localVariables(
  local!timeStamp: userdatetime(2023, 4, 2, 5, 30, 0),
  
  /*
    The working days is a list of 7 booleans, one for each day of the week.
  */
  local!workingDays: {true, true, true, true, true, false, false},
  local!workingHoursStart: 9,
  local!workingHoursEnd: 17,

  /*
    Move the timestamp into working hours. This ignores working days, which is
    fixed in the second step
  */
  local!inWorkingHours: a!match(
    value: totime(local(local!timestamp)),

    /* Timestamp is before working hours, move it to the beginning of the working period */
    whenTrue: fv!value < time(local!workingHoursStart, 0, 0),
    then: userdatetime( /* Make sure to use the users timezone */
      year(local!timestamp),
      month(local!timestamp),
      day(local!timestamp),
      local!workingHoursStart,
      0,
      0
    ),

    /* Timestamp is after working hours, move it to the beginning of the next day's working period */
    whenTrue: fv!value > time(local!workingHoursEnd, 0, 0),
    then: userdatetime( /* Make sure to use the users timezone */
      year(local!timestamp + 1),
      month(local!timestamp + 1),
      day(local!timestamp + 1),
      local!workingHoursStart,
      0,
      0
    ),

    default: local!timestamp
  ),

  /* This is a number between 1-7 */
  local!dayOfWeek: weekday(local!inWorkingHours, 2), 

  if(
    /* Check whether the modified timestamp is in a working day*/
    local!workingDays[local!dayOfWeek],

    /* YES -> Good. Return the timestamp in working hours */
    local!inWorkingHours,

    /*
      NO -> Bad! Move the timestamp to a working day
    */
    a!localVariables(
      /*
        To find the next working day, which could be next week, we us the list of
        booleans defined aboe. We take the remaining
        days of the current week and append the whole list for the next week to it.

        Example: Our timestamp (T) is on a Saturday

                current week            next week
           <               T   >  <                   >
          {(1, 1, 1, 1, 1, 0, 0), (1, 1, 1, 1, 1, 0, 0)}
                             <1    2  3  4  5  6  7  8>
                                     final list
                                   ^

         Then we find the index of the first true value in the final list
         and add this number of days to the timestamp. In the example we add two days
         to get from Saturday to Monday
      */
      local!daysOffset: lookup( /* Find index of first TRUE value */
        append(
          ldrop(local!workingDays, local!dayOfWeek), /* Remaining days of current week */
          local!workingDays /* Next week */
        ),
        true
      ),

      /* Calculate the new timestamp and move it into working time */
      local!newTimestamp: local!inWorkingHours + local!daysOffset,        
      userdatetime( /* Make sure to use the users timezone */
        year(local!newTimestamp),
        month(local!newTimestamp),
        day(local!newTimestamp),
        local!workingHoursStart,
        0,
        0
      )
    )
  )
)

Summary

I use this algorithm in an application where I need to dynamically calculate due dates for a five-step process where each step has multiple levels of exceptions and escalations. And the overall due dates might become as short as 3–4 hours.

I hope this little coding challenge helps you to build better applications.

Leave a Reply