xquery optional where statements - xpath

In Xquery 3.1 I am processing the variable parameters from a form to search for matching XML documents. The XML documents look like this:
<listBibl xml:id="TC0001" type="collection">
<bibl>
<title type="collection">Bonum universale de apibus</title>
<affiliation corresp="dominican"/>
<author nymRef="thomas_cantipratensis"/>
<location corresp="flanders"/>
<othercontent>....</othercontent>
</bibl>
</listBibl>
The user can submit optional parameters against xml:id, affiliation, author, and location, and they can be parameters with multiple values (sequences).
If the user were to submit all parameters, the query might look like:
for $c in $mycollection//listBibl[#xml:id=($params_id)]
where $c/affiliation[#corresp=($params_affil)]
and $c/author[#nymRef=($params_author)]
and $c/location[#corresp=($params_location)]
return $c
But the user may leave certain parameters empty, effectively making each where statement optional.
The only solution I can currently put together is to have a series of if...then...else statements which account for each permutation of parameters.
Is there any way in Xpath or Xquery to account for the parameters being empty with a wildcard of some sort? In pseudo code, where * represents a wished-for wildcard:
where $c/affiliation[if ($params_affil)
then #corresp=($params_affil)
else #corresp=* ]
Many thanks.

Use predicates of the form
[$params_affil=("", #corresp)]
which matches if $params_affil is either a zero-length string or equal to #corresp. And make zero-length-string (rather than empty sequence) the default if the parameter is not supplied.
Alternatively if the default for an absent parameter is (), use
[empty($params_affil) or $params_affil=#corresp)]
If that gets too repetitive, put the logic in a user-declared function.

I think you can always declare and use your own function as a predicate expression e.g.
declare function local:check-item($item as node(), $values as item()*) as xs:boolean
{
if (exists($values))
then $item = $values
else true()
};
....
where $c/affiliation[local:check-item(#corresp, $params_affil)]

Related

How to convert string to Xpath in Xquery function (BaseX)

I am writing Xquery function for BaseX which gets one of arguments as name of the element node. This name is then used in Xpath, but in general I cannot convert string to element.
This is how the method looks like
declare function prefix:getElementWithValue($root as document-node()?, $elem as xs:string?, $minVal as xs:float?, $maxVal as xs:float?)
as element()*
{
let $e := element {$elem} {""}
for $x in $root//SUBELEM
return if ($x//$e/#ATTRIB>=$minVal and $x//$e/#ATTRIB<=$maxVal) then ($x)
};
and the call
return prefix:getElementWithValue($db, "SomeElem", 10.0, 10.0)
and I am getting empty response from that. If I replace the $x//$e with $x//SomeElem it returns proper response. From the QueryPlan I see that the $e is treated as literal value. XPATH is not $x//SomeElem/#ATTRIB but $x//$e/#ATTRIB
So my question is how to covert string to type that can be used in XPATH?
XQuery does not have a standard function to evaluate a dynamically-constructed XPath expression.
Many XQuery processors offer some kind of extension function that does this, however. For example, BaseX offers query:eval():
https://docs.basex.org/wiki/XQuery_Module#xquery:eval
Note that variables in XQuery represent values, not fragments of expression text. Your expression $x//$e/#ATTRIB is equivalent to $x//"SomeElem"/#ATTRIB, which is quite different from $x//SomeElem/#ATTRIB.
If you know that $elem will always be an element name, then you can write $x//*[name()=$e]/#ATTRIB. But take care over namespaces.

Need XPath and XQuery query

I'm working on Xpath/Xquery to return values of multiple child nodes based on a sibling node value in a single query. My XML looks like this
<FilterResults>
<FilterResult>
<ID>535</ID>
<Analysis>
<Name>ZZZZ</Name>
<Identifier>asdfg</Identifier>
<Result>High</Result>
<Score>0</Score>
</Analysis>
<Analysis>
<Name>XXXX</Name>
<Identifier>qwerty</Identifier>
<Result>Medium</Result>
<Score>0</Score>
</Analysis>
</FilterResult>
<FilterResult>
<ID>745</ID>
<Analysis>
<Name>XXXX</Name>
<Identifier>xyz</Identifier>
<Result>Critical</Result>
<Score>0</Score>
</Analysis>
<Analysis>
<Name>YYYY</Name>
<Identifier>qwerty</Identifier>
<Result>Medium</Result>
<Score>0</Score>
</Analysis>
</FilterResult>
</FilterResults>
I need to get values of Score and Identifier based on Name value. I'm currently trying with below query but not working as desired
fn:string-join((
for $Identifier in fn:distinct-values(FilterResults/FilterResult/Analysis[Name="XXXX"])
return fn:string-join((//Identifier,//Score),'-')),',')
The output i'm looking for is this
qwerty-0,xyz-0
Your question suggests some fundamental misunderstandings about XQuery, generally. It's hard to explain everything in a single answer, but 1) that is not how distinct-values works (it returns string values, not nodes), and 2) the double slash selections in your return statement are returning everything because they are not constrained by anything. The XPath you use inside the distinct-values call is very close, however.
Instead of calling distinct-values, you can assign the Analysis results of that XPath to a variable, iterate over them, and generate concatenated strings. Then use string-join to comma separate the full sequence. Note that in the return statement, the variable $a is used to concat only one pair of values at a time.
string-join(
let $analyses := FilterResults/FilterResult/Analysis[Name="XXXX"]
for $a in $analyses
return $a/concat(Identifier, '-', Score),
',')
=> qwerty-0,xyz-0

How can I compare strings case sensitive in xforms:select1 ref attribute?

I have to provide the ref attribute value in an xf:select1. I need to select names of properties only if they are present in the supportedProperties instance which can be done with the following:
<xf:select1
ref="
instance('properties')/property[
name = instance('supportedProperties')/property/name
]/name">
However, the problem is that supportedProperties can contain names which are in capital letters. Assuming we cannot change the instance, is there a way we can do a case sensitive comparison?
Tried to use the lower-case() XPath function as follows but it didn't work:
<xf:select1
ref="
instance('properties')/property[
name = instance('supportedProperties')/property/name
]/lower-case(name)">
Assuming you are using XPath 2, you can write:
<xf:select1
ref="
instance('properties')/property[
name = instance('supportedProperties')/property/name/lower-case(.)
]/name">
What this does is that the lower-case(.) function applies to all elements in the sequence returned by instance('supportedProperties')/property/name.
You can also write it:
<xf:select1
ref="
instance('properties')/property[
name = (
for $name in instance('supportedProperties')/property/name
return lower-case($name)
)
]/name">

How to use like in XPath?

I have a page that searches with filters. I have this code for example,
xmlTempResultSearch = xmlResidentListDisplay.selectNodes("//PeopleList/Row[#LastName != '"+txtSearch.value+"']");
xmlTempResultSearch.removeAll();
This selects the data that is not equal to the LastName inputted on the txtSearch textbox and then removes them from the result set so that its filtered to equal the last name on the txtSearch textbox.
My problem with this code is that it should be equal (=) to the txtSearch.value, what I want is that I want the result set LIKE the txtSearch.value. What happens on my page is that when I type 'santos' on the txtSearch textbox, its result set is all those last names with 'santos'. But when I type 'sant', nothing appears. I want the same result set with 'santos' because it all contains 'sant'
You can use all of the XPath (1.0) string functions. If you have XPath 2.0 available, then you can even use RegEx.
contains()
starts-with()
substring()
substring-before()
substring-after()
concat()
translate()
string-length()
There is no **ends-with() in XPath 1.0, but it can easily be expressed with this XPath 1.0 expression**:
substring($s, string-length($s) - string-length($t) +1) = $t
is true() exactly when the string $s ends with the string $t.
You can use start-with function and not function. Reference:
http://www.w3schools.com/xpath/xpath_functions.asp
xmlTempResultSearch = xmlResidentListDisplay.selectNodes("//PeopleList/Row[not(starts-with(#LastName,'"+ txtSearch.value +"'))]");
you can use contains() function of XPath:
xmlTempResultSearch = xmlResidentListDisplay.selectNodes("//PeopleList/Row[not(contains(#LastName,'"+txtSearch.value+"'))]");

Is it possible to exclude some of the string used to match from Ruby regexp data?

I have a bunch of strings that look, for example, like this:
<option value="Spain">Spain</option>
And I want to extract the name of the country from inside.
The easiest way I could think of to do this in Ruby was to use a regular expression of this form:
country = line.match(/>(.+)</)
However, this returns >Spain<. So I did this:
line.match(/>(.+)</).to_s.gsub!(/<|>/,"")
Works well enough, but I'd be surprised if there's not a more elegant way to do this? It seems like using a regular expression to declare how to find the thing you want, without actually wanting the enclosing strings that were used to match it to be part of the data that gets returned.
Is there a conventional approach to this problem?
The right way to deal with that string is to use an HTML parser, for example:
country = Nokogiri::HTML('<option value="Spain">Spain</option>').at('option').text
And if you have several such strings, paste them together and use search:
html = '<option value="Spain">Spain</option><option value="Canada">Canada</option>'
countries = Nokogiri::HTML(html).search('option').map(&:text)
# ["Spain", "Canada"]
But if you must use a regex, then:
country = '<option value="Spain">Spain</option>'.match('>([^<]+)<')[1]
Keep in mind that match actually returns a MatchData object and MatchData#to_s:
Returns the entire matched string.
But you can access the captured groups using MatchData#[]. And if you don't like counting, you could use a named capture group as well:
country = '<option value="Spain">Spain</option>'.match('>(?<name>[^<]+)<')['name']

Resources