Jeff Meden
Jeff Meden

Reputation: 160

XQuery FLWOR return section code content with multiple if statements

I am trying to clean up some XQuery code and ran into another problem (last question was this one: Error "undefined variable at noteLine" in FLWOR expression when returning multiple nodes). I am further cleaning up the following working code, to minimize reuse:

    for $noteLine in $noteLineArr
    where $noteLine != ''

    return (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 1, 80)}</Descr>
        </NTE>, 
        if (fn:string-length(fn:normalize-space($noteLine)) > 80 and fn:string-length(fn:normalize-space($noteLine)) <= 160) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 81, 80)}</Descr>
        </NTE>
        ) else if (fn:string-length(fn:normalize-space($noteLine)) > 160) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 81, 80)}</Descr>
        </NTE>,
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 161, 80)}</Descr>
        </NTE>
        ) else ()
    )   

Now that I moved the first NTE out, I want to basically do the same with the rest (run an IF and only output another using exactly the code that's needed) so I pictured a solution like this:

    for $noteLine in $noteLineArr
    where $noteLine != ''

    return (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 1, 80)}</Descr>
        </NTE>, 
        if (fn:string-length(fn:normalize-space($noteLine)) > 80) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 81, 80)}</Descr>
        </NTE>
        ) else()
        if (fn:string-length(fn:normalize-space($noteLine)) > 160) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 161, 80)}</Descr>
        </NTE>
        ) else ()
    )   

This fails in the parser with the error "Unexpected Token: ' (fn:string-len...'" pointing at the second if statement. I have concluded that XQuery does not like recurring if sections unless they are within a node, like this:

    for $noteLine in $noteLineArr
    where $noteLine != ''

    return (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 1, 80)}</Descr>
        </NTE>, 
        <NTE>
        if (fn:string-length(fn:normalize-space($noteLine)) > 80) then (
            <NoteRefCd>BOL</NoteRefCd>,
            <Descr>{fn:substring(fn:normalize-space($noteLine), 81, 80)}</Descr>
        ) else()
        </NTE>,
        <NTE>
        if (fn:string-length(fn:normalize-space($noteLine)) > 160) then (
            <NoteRefCd>BOL</NoteRefCd>,
            <Descr>{fn:substring(fn:normalize-space($noteLine), 161, 80)}</Descr>
        ) else ()
        </NTE>
    )   

This is not workable, because I can't let any of those extra nodes get in the output if they are empty. Is there a way in XQuery to achieve the desired output a similar way (I worked out a solution by putting some of the code into a local function but it is not as readable imo)? And possibly as a follow up, why is XQuery so sensitive to multiple consecutive if statements?

Solution (again thanks to Jens Erat)

    for $noteLine in $noteLineArr
    where $noteLine != ''

    return (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 1, 80)}</Descr>
        </NTE>, 
        if (fn:string-length(fn:normalize-space($noteLine)) > 80) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 81, 80)}</Descr>
        </NTE>
        ) else(),
        if (fn:string-length(fn:normalize-space($noteLine)) > 160) then (
        <NTE>
            <NoteRefCd>BOL</NoteRefCd>
            <Descr>{fn:substring(fn:normalize-space($noteLine), 161, 80)}</Descr>
        </NTE>
        ) else()
    )   

(Note: comma placement is appropriate after the first two "sections" of the return, the first NTE node and then after the if statement, not within the if statement).

Upvotes: 2

Views: 4081

Answers (1)

Jens Erat
Jens Erat

Reputation: 38682

XQuery is a functional programming language. Among other things, this means everything has an expression with a single, distinct return value. You can also have multiple statements returning something, but then you always have to wrap them in a sequence, which is an expression wrapping an arbitrary number of input expressions into one return value (the sequence).

Explaining Expressions

To explain the issue with less boilerplate code, consider a small example like

for $i in 1 to 10
return $i

This obviously returns all numbers from one to ten. What if we only want to return the even numbers, using an if statement? The if statement is of the form if (expression) then expression else expression. Remember: every expression has to have a return value, this also applies to the else clause. In this case, we just have it return the empty sequence (), which in XQuery means "no value".

for $i in 1 to 10
return
  if ($i mod 2 eq 0)
  then $i
  else ()

Having Multiple if Expressions

This works fine and returns all the even values. Now, lets consider we also want to print a string whenever a number can be divided by three. We cannot simply put this into the else clause, as for example 6 is both even and can be divided by three. We need to separate if clauses. This is also what you tried in your second code example.

for $i in 1 to 10
return
  if ($i mod 2 eq 0)
  then $i
  else (),
  if ($i mod 3 eq 0)
  then "three"
  else ()

This fails because $i is not defined in the second if clause. But why? Remember: Every expression is expected to have a single return value. Also the expression for the return statement, which is the if then else expression. So similar to your first question, the XQuery compiler considers this example to be

for $i in 1 to 10
return
  if ($i mod 2 eq 0)
  then $i
  else (),
if ($i mod 3 eq 0)
then "three"
else ()

which should make the problem more obvious. In the end, it is the same problem like in your original question, but with more complicated expressions. Both are of the form

for $i in 1 to 10
return
  expression1,
  expression2

having to be enclosed in a sequence if they should both belong to the return statement:

for $i in 1 to 10
return (
  expression1,
  expression2
)

With the expressions being elements/element constructors in your old question, and if then else expressions over here.

The Solution for Multiple if Expressions

Again, you have to wrap them in a sequence:

for $i in 1 to 10
return (
  if ($i mod 2 eq 0)
  then $i
  else (),
  if ($i mod 3 eq 0)
  then "three"
  else ()
)

Now, the sequence is the single expression for the return statement, and has again two subexpressions containing the if then else code. This is also required when you're mixing different kinds of expressions, like in this example:

for $i in 1 to 10
return (
  if ($i mod 2 eq 0)
  then $i
  else (),
  <foo/>
)

Removing the sequence parenthesis will have the XQuery engine output the even numbers and then a single <foo/> element, instead of ten <foo/> elements -- one for each $i.

Upvotes: 4

Related Questions