Hello everyone!
I'm in the process of designing a quiz feature, and I'm struggling with designing an algorithm that will choose questions for a quiz. I believe my issue would easily be solved with a while loop, but since this is unavailable in Appian, I need some help designing an alternative solution. I have no code written, so let me describe my issue from a high level.
I have a table containing quiz questions - around 400 rows at the moment. When loading a quiz form, I need to pull randomly chosen 40 questions from this table.
First, I determine the first available question ID. Then, I determine the last available question ID. I save both as integers, 7 and 403.
Then, I get a random integer number, picked from a range between 7 and 403.
And here's where I get stuck: I need to make sure these numbers are unique. Normally I would have an array that I would append with these randomly generated integers on the condition that they are not already in the array, and I would do this until the array item count equals 40. But since I need to specify, somehow, the number of iterations in the for loop, the whole idea breaks apart.
Furthermore, I also need to make sure that these numbers are actually valid primary keys... You might have noticed that the range starts at 7, and this is because the first 6 rows were test values I have filtered out of the Record Type via Source Filter.
Querying the Record Type for all question IDs, and then choosing a random ID from this array is also an option, but it seems like a pretty heavy query. And then I would have to re-query.
I have another idea which might work, but I am not sure if this is the most optimal approach. The interface is using a wizard pattern, so after each question the user will have to click a button to navigate to the next question. I think I could, upon button click, append the ID of the current question to a variable, then get a random ID following the steps above, query for this ID to populate the next step. The hole in this one is that I still need to make sure that the randomly picked integer is actually a valid identifier.
Please, help a brother out!
Discussion posts and replies are publicly visible
https://appian.rocks/2022/08/29/complex-algorithms-in-appian/
Is it going to stay in the ballpark of 400 rows? Then you could query all IDs (only) without too much worry about load. You could probably do it safely with 10 times as many as that (but not much more).
Then you could sort them into a randomized order using a variation of the following:
a!localVariables( local!primaryKeyIds: [your query goes here, make sure to have it only pull back the needed column], local!dict: a!forEach( items: local!primaryKeyIds, expression: a!map( index: fv!index, pkId: fv!item, sort: rand() ) ), local!sorted: todatasubset( local!dict, a!pagingInfo( startIndex: 1, batchSize: local!targetSize, sort: a!sortInfo(field: "sort", ascending: true()) ) ), local!sorted.data.pkId /* apparently randomized list of IDs */ )
Then you could take the left(40) of that array and query the questions with those IDs.
if( ri!isRecursion, a!localVariables( local!index: ceiling(rand() * count(ri!data.chars)), a!map( chars: remove(ri!data.chars, local!index), output: append(ri!data.output, ri!data.chars[local!index]) ) ), reduce( rule!SSH_RandomPicker( isRecursion: true, data:_, num:_ ), a!map( chars: char(65 + enumerate(26)), output: {} ), enumerate(10) ) )
Yup, Expressions aren't always that friendly for some things. Some options:
fn!index( fn!union( a!flatten( a!forEach( items: fn!enumerate(403) + 7, expression: a!localVariables( local!outerValue: fv!item, a!forEach( items: fn!enumerate(10), expression: fn!tointeger( local!outerValue + fn!round(fn!rand() * 10, 0) ) ) ) ) ), fn!tointeger({}) ), 10 * fn!enumerate(40) + 1 )
Basically the Expression is operating a loop within a loop. The inner loop is generating the random numbers as integers in batches of 10, each of which is added to the value of the outer loop (which is incrementing from your lower to upper bounds - i.e. 7 to 403).
The result is then flattened (so the lists of lists is made into one list) which is the deduplicated (using the union function).
Lastly, we index into this list every 10th item.
It's crude but seems to work. Others may have better ideas.
I'm expecting the set to grow up to 1000 questions, probably not many more (per quiz topic). Do you think it's feasible?
Yes - as long as your initial query restricts the column being pulled back to just the IDs available, it shouldn't be bad. To test this, you can just arbitrarily run a query entity on any other tables you have in your system that have many more rows, and check the runtime.
It does seem crude but doable. I considered using a process, since you can actually perform a while loop there, but I was not entirely sure how to access the output in the expression, since I am using a Start Form. Thanks for your help!
Thank you so much!