How Contexts Simplify DMN Logic
Blog: Method & Style (Bruce Silver)
I have made the point on multiple occasions (for example, here) that DMN is both more business-friendly and more powerful than better-known Low-Code languages such as Microsoft’s Power FX. One reason for that is DMN’s context boxed expression, which breaks down a complex bit of business logic (or decision logic, if you prefer) into multiple simple pieces, without cluttering up the DRD with dozens of little decisions. A context is a two-column table, the first column naming a new variable local to the context, and the second column a FEEL expression for its value. This expression can reference both inputs to the context as a whole and prior context entries. Anyone with a basic knowledge of FEEL can understand the logic of each of these expressions. Thus, in addition to simplifying the DRD and making it more manageable, contexts also promote logic transparency. Even if you could not figure out how to implement some complex logic yourself, if you know FEEL you can understand it as decomposed in a context.
How Many Vacation Days?
To illustrate, here is an example from the Low-Code Business Automation training, part of a Vacation Request process. The process data input Vacation Request includes components vacation start and vacation return, both FEEL type Date. The decision Requested Days returns the count of vacation days requested by the employee. Sounds simple, right? Subtracting the start date from the return date gives a duration, and dividing that by the duration of one day gives a number of days. Yes, but that’s just a raw count. Raw Days also includes weekends and possibly holidays, which should not be included in the count of Requested Days. Hmmm, that’s a little more complicated. Let’s look at a calendar and see if you can figure out how to do that.
Let’s say vacation start is October 5, and note that October 10 is a holiday. Validation rules ensure than neither vacation start nor vacation return may be weekends or holidays. To discover the logic, let’s look at 4 possibilities for vacation return (the first day back at work):
- if vacation return is October 11, Requested Days is 3.
- if vacation return is October 13, Requested Days is 5.
- if vacation return is October 17, Requested Days is 7.
- if vacation return is October 19, Requested Days is 9.
So the question is, what is the generalized logic given any vacation start and vacation end? Try to derive it yourself, and then read on.
Newcomers to DMN generally find it easier to use a separate decision for each bit of the logic and not worry about cluttering up the DRD. So let’s do that.
We start with Raw Days, a number computed by subtracting vacation start from vacation return and dividing by the duration of one day, as mentioned earlier.
Raw Days = (Vacation Request.vacation return - Vacation Request.vacation start)/duration("P1D")
In order to adjust Raw Days to account for weekends, we need to consider the number of weekends included in the interval. Each subtracts 2 days from the count of Requested Days. To get the number of included weekends, we will need the number of Whole weeks, which we get by dividing Raw Days by 7 and discarding any fractional values using the FEEL floor() function.
Whole weeks = floor(Raw Days/7)
To discover the general formula, let’s look for the pattern:
- vacation return is October 11, Whole weeks = 0, Requested Days = 3 = 6 Raw Days – 2 weekend days – 1 holiday
- vacation return is October 13, Whole weeks = 1, Requested Days = 5 = 8 Raw Days – 2 weekend days – 1 holiday
- vacation return is October 17, Whole weeks = 1, Requested Days = 7 = 12 Raw Days – 4 weekend days – 1 holiday
- vacation return is October 19, Whole weeks = 2, Requested Days is 9 = 14 Raw Days – 4 weekend days – 1 holiday
Notice that the number of included weekends depends on more than just Whole weeks. What else is needed? It’s related to the day of the week of vacation return. If the vacation return day of the week is earlier than the vacation start day of the week, Whole weeks undercounts weekends by 1. In that case, the number of weekend days subtracted from Raw Days should be 2*(Whole weeks + 1), and otherwise it is 2*Whole weeks. Fortunately, dates in FEEL have a weekday property, a number from 1 to 7 indicating the day of the week, Monday to Sunday. So we can define the decision Weekend adjustment days as:
Weekend adjustment days = if Vacation Request.vacation return.weekday < Vacation Request.vacation start.weekday then 2*(Whole weeks + 1) else 2*Whole weeks
We also need a table of holidays to be excluded, such as one like this:
We apply a filter to Federal Holidays to list any Included holidays in the interval:
Included holidays = Federal Holidays[Date in [Vacation Request.vacation start..Vacation Request.vacation return)]
This says select from the table Federal Holidays all rows where the Date component value is in the requested vacation interval. Then we just count them:
Holiday count = count(Included holidays)
Finally, we subtract Weekend adjustment days and Holiday count from Raw Days:
Requested Days = Raw Days - Weekend adjustment days - Holiday count
We can model all this in DMN with the DRD below:
The expressions for each decision in this model are simple. Anyone with a bit of FEEL knowledge would understand them. But that was never the hard part. The hard part was discovering the logic. I think when people say FEEL is hard for business users, they really mean discovering the logic can be hard. Are you good at puzzles? If so, you will be good at this.
Simplifying with a Context
Now, notice that here we were just trying to get the count of requested days from the Vacation Request, and this required a DRD with 7 decisions. In practice, getting this count is just one of many things we’re trying to do with Vacation Request, and if we need 7 decisions for each of them we’re going to have a DRD with dozens of nodes, making it difficult to maintain. This is where a context comes in. A context lets us decompose the logic into small steps without cluttering up the DRD. With a context, our DRD looks like this, a single decision.
The context looks like this:
Now we have 4 context entries, each defining a variable local to the decision. The logic of each one is as we derived previously. If I showed this to you at the beginning of the post, your eyes would have glazed over. It looks complicated when you haven’t gone through the logic. But now you’ve already seen each piece of it. All we’re doing is contracting that DRD with 7 decisions to something equivalent but much simpler. Note that in a context, each value expression can reference both the decision input Vacation Request and any preceding context entry. So, for example, Whole weeks references the prior entry Raw days. The final result box at the bottom here references only context entries, simplifying the expression.
Effectively, then, DMN provides two levels of logic decomposition: the DRD and within each decision, potentially also a context. Modelers have the opportunity to balance how much decomposition goes into each piece. If you get into DMN seriously, you will use this all the time.
Comparison with Power FX
Now that you know how contexts work, let’s revisit the comparison of DMN with Power FX. Microsoft marketing published an article touting the capabilities of Power FX by showing how it extracts the last word in a string. The Power FX expression is
A2 = RIGHT(A1, LEN(A1) - FIND("|", SUBSTITUTE(A1, " ", "|", LEN(A1) - LEN(SUBSTITUTE(A1, " ", "")))))
where A1 is the original string and A2 is the last word in it. Compare that with the equivalent FEEL:
A2 = split(A1, " ")[-1]
The FEEL is obviously simpler because of the built-in split() function, which tokenizes the string into a list of words, and the filter operator [-1], which selects the last item in that list. Power FX has neither of those features. But let’s imagine FEEL did not have them either, and use FEEL equivalent to Microsoft’s logic, which is based on finding the character position of the last space in A1. Microsoft’s expression is virtually impenetrable, but it works as follows:
LEN(A1)-LEN(SUBSTITUTE(A1," ","")) – Count of spaces in the original string, call it N
SUBSTITUTE(A1," ","|", ... ) – Replaces just the Nth (final) space with a
FIND("|", ... ) – Finds the absolute position of that replaced
| (i.e., the final space)
Right(A1,LEN(A1) - ... )) – Returns all characters after that
We cannot do it exactly that way in FEEL because its replace() function – roughly equivalent to SUBSTITUTE – is based on regular expressions. While it is possible to replace the Nth occurrence of a character using a regular expression, the syntax is very complicated, so we need to find the position of the last space in a different way. We can iterate substring() to turn A1 into a list of characters and then use the list function index of() to return the positions of all space characters. Applying the max() function selects the last one, and then we can use substring() again to return the portion of A1 following that position. Writing this as a single literal expression – as it would be done in Power FX – makes the logic similarly opaque:
substring(A1, max(index of(for n in 1..string length(A1) return substring(A1, n, 1))) + 1)
But while it’s more complicated than our original FEEL expression, the logic expressed as a context becomes transparent:
The Value of Contexts
While contexts are still not supported by many so-called DMN tools, they clearly provide two important benefits to modelers:
- They allow decomposition of complex value expressions without adding clutter to the DRD. In real-world decision models, this is an important consideration.
- They provide logic transparency, especially in comparison to the equivalent logic expressed as a single literal expression, as we have with Power FX. Even if you cannot figure out the logic yourself, basic knowledge of FEEL makes the logic transparent when modeled as a context.
Learn to Do This Yourself
Our DMN Method and Style course provides comprehensive coverage of FEEL, contexts, and all the other boxed expression types. And our follow-on Low-Code Business Automation course shows you how to use FEEL and boxed expressions, along with all the REST services at your disposal, to create cloud-based Business Automation services and pageflow apps without programming. Each course includes 60-day use of the Trisotech platform and post-class certification. You should check them out!