XLS form- Repeat select_one question until "A" is selected

Is there any limit of those repeats or it may be infinite?

1 Like

Hi @Xiphware, @janna, @Grzesiek2010.
The choice list have 49 options.
And it may be infinite. It may repeat around 600 times per day and it take 12 days
The submitter measure products and write down results from 1 to 48.
The questions should be:
1/ What is the measure result?- Submitter should type in the result but for faster input, I prepare a list of 48 choices
2/ Answer more?- If "yes" then repeat 1st question, if "no" then end the survey.
But because most of time the answer is "yes" for whole day so I'd like to let the submitter end the survey by select the 49th choice.

It's rather not possible to do in ODK Collect but it seems like an interesting feature to me. Now we can deal with repeat groups in two ways:

  • define repeat_count and have exactly x repeats
  • add groups manually (we are asked whether a new group should be added or not)

what about adding a third option: adding a new group automatically based on a condition (eg previous answers).
What do you think @LN?

2 Likes

Define “previous answer”... :slightly_smiling_face:

The problem is, if you automatically create another repeat group instance based on a condition, the condition is (still) going to be true after you create it (!), so presumably you should create another one, and another, ...

In this particular use case, you want to replicate a repeat group if a particular question (the ‘previous’) has a particular value. So in effect the condition differs every time (!) - it’s actually dependent on an entirely different response in the form each time.

1 Like

Here's how we do this style of thing in my organization:

  1. You have a repeat group.
  2. You have an exit condition. In this case, your exit condition is "the user selects 'A'."
    3a. If the user does not select 'A', you have a note on the next screen (still within the same repeat group) that says "there is more data remaining to enter. please proceed forward to the next screen and select 'add group'."
    4b. If the user does select 'A', the next screen will be a note saying "you have finished entering data. proceed forward and select 'do not add'."
  3. If the user accidentally selects "add", you can have an "error screen" at the beginning of the next repeat which will be activated only if 'A' was selected in the previous repeat. By "error screen", I mean a group consisting of two things: (i) a note type question that is marked as required. Then you also have (ii) an acknowledge type question or a select_one with a single response. The note will instruct the user to press down on the answer option and select "delete group". I suppose you could actually implement this with just (ii) and an impossible constraint set on that question.

I hope this makes sense. Please let me know if any part of this explanation is not clear.

1 Like

Basically you have 2 options:

  1. An (infinite) repeat group. This requires the user to explicitly hit "Add Group"; there's currently no way to add a group programmatically, and for reasons described earlier I'm not even sure this feature would be feasible to add... But as described by @Joseph_E_Flack_IV there's a lot you can do with this approach to prompt and guide the user.

  2. Manually define a (fixed) number of desired groups in the form, and use relevant='...' show/hide logic to display each depending on the answer to the previous. This will be tedious to define for 600+ potential groups, and you still have a fixed maximum.

Sorry, I know neither of them ideal for your situation :slightly_frowning_face: , but I think the first approach probably best fits your actual usecase.

1 Like

Just to add a note that i think this is probably a common need among some users of ODK, essentially an infinite repeat loop until a certain "break" option is selected.
From my limited knowledge of other programming languages, infinite loops with a kind of "break" option is one of the basics I've learned.
Is there actually any feasibility of this being a feature of an ODK form? @Xiphware do you know?
@Joseph_E_Flack_IV your option is kind of what I've done in the past. I'm not sure if others also use something similar? Or if I've missed something more obvious?
For me, it was needed to do something like stock management at both a warehouse and in a rural pharmacy. When you're doing inventory counts you often can't predefine how many items you want to count. And without the infinite loop it makes the form a bit clunky for the user.
Hoping I haven't misunderstood, but happy to be educated a little further on this use case, it's really interesting!
Janna

1 Like

I can certainly understand the usecase. However, you have to bear in mind that XForms is not in actuality an (iterative) 'program' running through a series of commands - like Java, C, Basic, ... - but rather a form definition language that describes the layout and behavior of an interactive form. That Collect presents you with a sequence of questions, which you step through one by one till you reach the end of the form - is merely a consequence of how Collect goes about presenting the entire form document. Other XForm interfaces, eg Enketo, presents you with the entire form at once, wherein you can answer questions anywhere and in any order you like. So in this context, a 'repeat {...} until X=Y' doesn't really make sense anymore - I could jump in and out of this loop answering questions all over the place! :slight_smile:

@LN After a bit of digging, what might be possible, somehow (I haven't worked thru all the specifics necessary...) is leverage XForm triggers - specifically an insert action trigger - to somehow programmatically add a repeat group into a form. As defined in the W3C spec these triggers still have to be explicitly fired by the user. But maybes there's some wiggle room here to get creative and have them also activate (once) as a consequence of an XPath evaluation... Here's a concrete example - although again, this trigger has to be exlicitly fired by the user via the UI:

<repeat id="lineset" nodeset="/my:lines/my:line">
  <input ref="my:price">
    <label>Line Item</label>
  </input>
  <input ref="@name">
    <label>Name</label>
  </input>
</repeat>
        
<trigger>
  <label>Insert a new item after the current one</label>
  <action ev:event="DOMActivate">
    <insert nodeset="/my:lines/my:line" at="index('lineset')"
      position="after"/>
    <setvalue ref="/my:lines/my:line[index('lineset')]/@name"/>
    <setvalue ref="/my:lines/my:line[index('lineset')]/price">0.00</setvalue>
  </action>  
</trigger>

There's certainly sufficient expressiveness in an insert action trigger to accomplish the desired result. Its then just a matter of figuring out how to fire it (non-standardly); perhaps something like:

<trigger>
  <label>Insert a new item after the current one</label>
  <action ev:event="DOMActivate" odk:activate="/my:lines/my:line[index('lineset')-1]/@name !=''"/>
  ...
  </action>  
</trigger>

(hopefully you get my drift...)

2 Likes

To achieve something like this, you can define the repeat count in terms of your desired condition.

This indefinite-repeat XLSForm shows an example of that technique. The key part is that the count is set to

if (${count} = 0 
    or (${person}[position()=${count}]/choice != '' and ${person}[position()=${count}]/choice != 'a'), 
    ${count} + 1, 
    ${count})

edited 6/2020 to use ${} for repeat reference instead of XPath path

This says that if there are currently no repeats or your ending condition (user has selected "a") is not met, we should add one to the desired repeat count. If there are repeats already and the user has selected "a", we should keep the desired repeat count the same which means no new repeats are created. This is similar to the relevant idea @Xiphware was suggesting but it is fully dynamic. You could have any other test in there like age being greater than 50, "a" being selected AND age being greater than 50, etc.

${person}[position()=${count}]/choice uses XPath notation to identify the choice selected in the last iteration. You could also do this with indexed-repeat if you're more comfortable with it (I can never remember how it works!). The idea is that ${count} represents the number of repeats added so far and is also the index of the latest iteration.

If you think a user could go back and modify one of their answers, you'll want to use the technique described at https://github.com/opendatakit/docs/issues/726 so that if a user selects A in a previous repeat, the ones after it aren't shown.

I've also used the technique @Joseph_E_Flack_IV describes and although it may require a bit more thinking on the enumerators' part, it tends to work well.

Hopefully this addresses the use case generally enough that no specification additions are needed!

5 Likes

I stand corrected, and all the wiser for it! :blush: . I didnt realize the ODK repeat jr:count extension was in fact dynamic [I figured it was statically evaluated when the form was first rendered]. That's totally awesome!! :heart_eyes:

So yes, @dmtk41, you should be able to use @LN's repeat count trick to dynamically add additional repeat groups as needed.

[now I gotta go play with this in Enketo :thinking: ...]

2 Likes

Question: when is the repeat count re-evaluated by Collect? Every refresh cycle, eg every time you answer any question anywhere in the form?

Asking 'cause wont /indefinite-repeat/person[position()=${count}]/choice != 'a' evaluate to true for the newly created and empty repeat instance? Since person[x]/choice will be null, and null != 'a', wont it keep adding a new repeat instance every refresh? ...

1 Like

@LN 's way work perfect as my expect. Thank you all.

2 Likes

Glad it works for you, @dmtk41!

@Xiphware see https://opendatakit.github.io/xforms-spec/#creation-removal-of-repeats for the jr:count documentation. In particular, in B:

B. Using the jr:count attribute on the element. E.g. see below for the use of jr:count to automatically create 3 repeats for the above form. The value could also be a /path/to/node and clients should evaluate the number of repeats dynamically (Note: It is problematic to implement this in a truly dynamic fashion, i.e. when the value changes, to update the number of repeats).

I don't know the history of that language and it's unfortunately vague but the way it has been interpreted in JavaRosa/Collect is that jr:count is considered once after each existing repeat is filled.

This is well-suited for a UI in which the user advances through a form but I think it can also work for a UI in which all repeats are shown on the same screen. One option would be to let the user drive adding repeats with some kind of + or "add" UI element and disable that UI element if the jr:count expression evaluates to a number less than or equal to the current repeat count. It would dynamically become enabled as soon as the expression evaluated to a number greater than the current repeat count.

You are correct that if the user is not involved in adding repeats and instead the system both adds repeats and evaluates the expression when a repeat is added, there would be an infinite loop. You could probably add a null check on the node to prevent that, though, so I'm not sure there actually is an issue with a fully dynamic system.

2 Likes

Hi @LN,
In your indefinite-repeat XLSForm example, what the code should be if the END condition is "a" is selected or age <50.

It should be something like

if (${count} = 0 or 
  (/indefinite-repeat/person[position()=${count}]/choice != 'a' 
  and /indefinite-repeat/person[position()=${count}]/age >= 50), 
${count} + 1, 
${count})

We're defining a continuing condition so we need to reverse your ending condition. You want to end when "a" is selected or age < 50 so the negation is to continue as long as "a" is not selected AND age is greater than or equal to 50.

If you want to know more about why the or becomes an and, you can read up on De Morgan's laws!

2 Likes

Got it. Thank you @LN.

1 Like

Oh @LN @LN ... the face that launch'd a thousand interviews!

Thank you. This is terrific...we've just implemented it to do an end run around the ugly 'New Group?' screen.

(That screen reminds me of the irritating dog named Chester in the old Warner Bros cartoons: https://www.youtube.com/watch?v=UVNHcob3oJg "Hey, Spike...do you wanna start a new group? Do ya? Do ya? You want I should start a new group for ya?" )

We just added a question at the bottom of the repeat group to ask whether there are additional flats in the building and if yes, we go thru the loop again (without repeating the initial GPS building location capture) and if not, we drop gracefully out of the loop using Hélène's suggestion.

Many thanks!
-Dale and @marykaytrimner

2 Likes

Haha, I hear you. I always try to work around it in survey design and there's generally a way. I'm glad you were able to get to something you like!

I've filed a documentation issue at https://github.com/opendatakit/docs/issues/1042 to make these approaches easier to find.

1 Like

Wow, that works great. One remark:
When I add the full path /indefinite-repeat/..... this is not working.

However, if I set it the way you showed in the spreadsheet on google docs, it works flawless.
Now that it works, I will think if it is worth to have decrease option by the user, for exemple if he changes the previously "do you want to repeat?" question.
Thank you.

1 Like

We made some changes to the XLSForm converter a few months ago and now the XLSForm filename isn't used in the path by default anymore. Instead, /data/ is always used. Indeed, what I did in the sample form lets the form conversion fill out the group path and thus always works:

if (${count} = 0 or (${person}[position()=${count}]/choice != '' and ${person}[position()=${count}]/choice != 'a'), ${count} + 1, ${count})

Thanks for pointing that out -- I'll make an edit to the post marked as the solution to reduce confusion.