28 Calculated Attributes
A path tail can calculate an output attribute from values reached in the current path context. The following expression calculates the duration of each session:
u.course.session.time.(endMinute-startMinute as durationMinutes)
The calculation uses two path tails, endMinute and startMinute, both evaluated
from the same time slot. The as clause names the calculated output.
Calculations are path tails, so they can appear beside ordinary navigation in tuple construction:
u.course.session.(room, time.(day, endMinute-startMinute as durationMinutes))
This selects the room, the day, and the calculated duration for each session.
Calculations over assessment fields
The assessment variant attributes have numeric fields that can be used in calculations. This expression calculates how many minutes of exam time are assigned to each mark:
u.course.assessment.exam.(durationMinutes/maxMark as minutesPerMark)
The same idea works for assignments:
u.course.assessment.assignment.(wordLimit/maxMark as wordsPerMark)
Products are ordinary calculations too:
u.course.assessment.project.(estimatedHours*weight as weightedHours)
Calculated path tails inside filters
Calculated path tails can also appear inside filters. The following expression reaches long sessions:
u.course.session.time.[endMinute-startMinute > 120]
The filter condition calculates the duration from the current time slot, then
compares it with 120.
Gotcha: Cartesian products with repeated use of attributes
Consider the following schema:
value X
{
Real v*;
}
Multiple occurrences of an attribute in a path expression can lead to Cartesian products,
which can be unexpected. For example, given a program variable x of type X the
path expression:
x.(v*v as product)
is equivalent to:
x.(v as v1, v as v2).(v1*v2 as product)
Each occurrence of v is a distinct path tail, so the two occurrences are evaluated
independently. The calculation therefore gives all combinations of V1*V2 where
x.v(V1) and x.v(V2).
This follows from the uniform rule that each path tail contributes its own frame;
treating two identical-looking tails as the same reached value would be an extra
special case.
One way to ask for all values of V*V where x.v(V) is to navigate to v first
and then use this:
x.v.(this*this as product)
Another approach is to give the calculation a name by defining a function:
func Real square(Real x) = x*x;
The path expression can then call the function on the current reached value:
x.(square(v) as product)
For larger calculations, named functions make path expressions easier to read and avoid repeating the same multivalued path tail inside the calculation.
When several path tails need values below the same prefix, navigate to the shared prefix first. Repeating a multivalued prefix in separate tails can create Cartesian products between independently reached values.