I have some XML data with an inheritance-like semantics, and I'd like to make a query that takes the inheritance into consideration. I know it is not possible in XPath 1.0, but I believe it is possible in XPath 3.0, but I'm not familiar with 3.0.
So I have a structure which is like this:
<elems>
<elem id="n">
<property name="xxx" value="yyy"/>
...
</elem>
</elems>
Not, the property with name inherits points to the #id of another <elem>. So, basically, I want to query the #id of the <elem> which have (or do not have) a property Z, whether that property is on itself or on any of the elements chained through the inherits property. For example:
<elems>
<elem id="1">
<property name="a" value="alpha"/>
</elem>
<elem id="2">
<property name="inherits" value="1"/>
<property name="b" value="bravo"/>
</elem>
<elem id="3">
<property name="inherits" value="2"/>
<property name="c" value="charlie"/>
</elem>
</elems>
So a query for elements with property c would return 3, and its reverse would return 1 and 2. A query for elements with property b would return 2 and 3 and its reverse would return 1. Finally, a call for elements with property a would return 1, 2 and 3, and it's reverse would not return anything.
How do I do that?
What you are looking for is essentially a transitive closure, which is the most common type of recursive query; and basically XPath cannot do recursive queries, except in the special case of the ancestor and descendant axes which are built in.
XPath 3.0 allows you to define functions, but because they are anonymous, they cannot (easily) call themselves.
The "(easily)" is because there is an escape clause: apparently Y-combinators allow you to overcome this limitation. See for example What is a Y-combinator?. But I've never really got my head around them and would never attempt this in real life, because there's a much easier solution: use named functions in XQuery or XSLT, which make recursion very straightforward. In fact in XSLT 3.0 you don't even need recursion, you can use xsl:iterate.
Here is a pure XPath 3.1 solution:
The function $allProps() below, returns a sequence of strings that are the names of all the properties of an element whose id is equal to the $id parameter passed to the function.
In this sample expression the function $allProps() is called 3 times -- once for each "elem" element and the returned properties are delimited by a NL character:
let $root := /,
$allProps-inner := function($id as xs:integer, $self as function(*)) as xs:string*
{
let $elem := $root/*/elem[xs:integer(#id )eq $id],
$ownProperties := $elem/property/#name[not(. eq 'inherits')]/string(),
$ParentId := $elem/property[#name eq 'inherits']/#value
return
(
$ownProperties,
if(empty($ParentId)) then ()
else
$self($ParentId, $self)
)
},
$allProps := function($id as xs:integer) as xs:string*
{ $allProps-inner($id, $allProps-inner ) }
return
(
$allProps(1), '
',
$allProps(2), '
',
$allProps(3), '
'
)
XSLT 3.0 - based verification:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:value-of select=
"let $root := /,
$allProps-inner := function($id as xs:integer, $self as function(*)) as xs:string*
{
let $elem := $root/*/elem[xs:integer(#id )eq $id],
$ownProperties := $elem/property/#name[not(. eq 'inherits')]/string(),
$ParentId := $elem/property[#name eq 'inherits']/#value
return
(
$ownProperties,
if(empty($ParentId)) then ()
else
$self($ParentId, $self)
)
},
$allProps := function($id as xs:integer) as xs:string*
{ $allProps-inner($id, $allProps-inner ) }
return
(
$allProps(1), '
',
$allProps(2), '
',
$allProps(3), '
'
)
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<elems>
<elem id="1">
<property name="a" value="alpha"/>
</elem>
<elem id="2">
<property name="inherits" value="1"/>
<property name="b" value="bravo"/>
</elem>
<elem id="3">
<property name="inherits" value="2"/>
<property name="c" value="charlie"/>
</elem>
</elems>
the wanted, correct result is produced:
a
b a
c b a
Finally, we reach naturally the solution of the original question:
So a query for elements with property c would return 3, and its
reverse would return 1 and 2. A query for elements with property b
would return 2 and 3 and its reverse would return 1. Finally, a call
for elements with property a would return 1, 2 and 3, and it's reverse
would not return anything.
How do I do that?
let $root := /,
$allProps-inner := function($id as xs:integer, $self as function(*)) as xs:string*
{
let $elem := $root/*/elem[xs:integer(#id )eq $id],
$ownProperties := $elem/property/#name[not(. eq 'inherits')]/string(),
$ParentId := $elem/property[#name eq 'inherits']/#value
return
(
$ownProperties,
if(empty($ParentId)) then ()
else
$self($ParentId, $self)
)
},
$allProps := function($id as xs:integer) as xs:string*
{ $allProps-inner($id, $allProps-inner ) }
return
(
for $name in ('a', 'b', 'c')
return
( $root/*/elem[$name = $allProps(#id) ]/#id, '
' )
)
When this XPath expression is evaluated (just replace the XPath expression in the transformation with this one), then the result when output is the wanted, correct one:
1 2 3
2 3
3
Related
I have XML file there is <a> and <b> for each element
I want to write a query using XQuery to return True or False
there is an element called <element>.
each <element> has 2 element in it <a>and<b>.
Return False :
if there is any <a> has the same value as another <a> in another element && there <b>'s value are different
otherwise True :
<a> values are differnt in each element
or there is similarity but there <b> values are different
for example
<root>
<element>
<a>ttt</a>
<b>tttsame</b>
</element>
<element>
<a>ttt</a>
<b>tttsame</b>
</element>
<element>
<a/>
<b>value</b>
</element>
<element>
<a>rrr</a>
<b>rrrvalue</b>
</element>
<element>
<a>mmm</a>
<b>rrrvalue</b>
</element>
<element>
<a>mmm</a>
<b>rrrvalue</b>
</element>
</root>
This one should be okay
should return true
<root>
<element>
<a>ttt</a>
<b>ttt value</b>
</element>
<element>
<a>ttt</a>
<b>ttrdiff</b>
</element>
<element>
<a/>
<b>value</b>
</element>
<element>
<a>mmm</a>
<b>rrrvalue</b>
</element>
</root>
shoudn't be accepted because ttt has two different values
should return false
Simple XPath 2.0:
empty(
(for $parentA-Dubled in /*/*[a = following-sibling::*/a]
return
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
)
[not(.)]
)
XSLT 2.0 - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:value-of select=
"empty(
(for $parentA-Dubled in /*/*[a = following-sibling::*/a]
return
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
)
[not(.)]
)
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on any XML document, it evaluates the XPath expression and outputs the result of this evaluation.
When applied on the first provided XML document, the wanted, correct result is produced:
true
When applied on the second provided XML document, again the wanted, correct result is produced:
false
Explanation:
This sub-expression:
(for $parentA-Dubled in /*/*[a = following-sibling::*/a]
return
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
evaluates to a sequence of boolean values: true() / false()
true() is returned when this is true:
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
This means that true() is returned for every occasion when there is an $parentA-Dubled/a that has no other a (a child of a following sibling of $parentA-Dubled with the same value as $parentA-Dubled/a but the value of its b sibling is different than the value of $parentA-Dubled/b.
To summarize: true() is returned when for all a elements with the same value, their b siblings also have (all b s) the same value
Then when is the case when false() is returned?
Returning false() means that empty() returned false() -- that is, there exists at least one occasion of two a elements that have the same value, but their b siblings have different values.
Thus, the sub-expression above returns a sequence such as:
true(), true(), true(), ..., true() -- all values are true()
or
true(), true(), true(), ..., false), ..., true() -- at least one of the values is false()
The original problem requires us to return true() in the first case and to return false() in the second case.
This is easy to express as:
empty($booleanSequence[. eq false()]) -- and this is equivalent to the shorter:
empty($booleanSequence[not(.)])
Now, we just need to substitute in the above expression $booleanSequence with the first sub-expression that we analyzed above:
(for $parentA-Dubled in /*/*[a = following-sibling::*/a]
return
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
Thus we obtain the complete XPath expression that solves the original problem:
empty(
(for $parentA-Dubled in /*/*[a = following-sibling::*/a]
return
empty($parentA-Dubled/following-sibling::*
[$parentA-Dubled/a eq a and $parentA-Dubled/b ne b])
)
[not(.)]
)
You could group on a and then check if there is more than one distinct b in any group, for instance with
not
(
for $a-group in root/element
group by $a := $a-group/a
where tail(distinct-values($a-group/b))
return $a-group
)
https://xqueryfiddle.liberty-development.net/6qM2e2r/0 and https://xqueryfiddle.liberty-development.net/6qM2e2r/1 has your two input samples.
As for how it works, the question asks to return false "if there is any <a> has the same value as another <a> in another element && there <b>'s value are different".
To find element elements with the same a child element we can group by $a := $a-group/a in a for $a-group in root/element expression. The distinct or different b values in each group of as with the same value are computed by distinct-values($a-group/b), if there are at least two different b values then tail(distinct-values($a-group/b)) contains at least one value, otherwise it is an empty sequence. This works as through XQuery 3's group by clause "In the post-grouping tuple generated for a given group, each non-grouping variable is bound to a sequence containing the concatenated values of that variable in all the pre-grouping tuples that were assigned to that group" (https://www.w3.org/TR/xquery-31/#id-group-by) so that after the group by $a := $a-group/a clause the variable $a-group is bound to a sequence of element elements with the same grouping key based on the a child element.
So the complete for .. group by .. where .. return selects the groups of element elements with the same a value where there are at least two different/distinct b values.
As the requirement is to "return false" if any such groups exist the not() function is applied to implement that condition as the boolean value of a non-empty sequence is true and the not(..) then gives false if there are any elements meeting the condition expressed in the for selection.
Try this XQuery code to get only one distinct item of <a> (The corresponding <b> value is not specified; here, the first element is chosen):
let $file := doc("input.xml")/root,
$vals := distinct-values($file/element/a) return
<root>
{for $i in $vals return $file/element[a=$i][1]}
</root>
Its result is:
<root>
<element>
<a>ttt</a>
<b>ttt value</b>
</element>
<element>
<a/>
<b>value</b>
</element>
<element>
<a>rrr</a>
<b>rrrvalue</b>
</element>
<element>
<a>mmm</a>
<b>rrrvalue</b>
</element>
</root>
I have the following XML file:
<Body>
<ELEMENT Property1="A" Property2="B" Property3="C"/>
<ELEMENT Property1="D" Property2="E" Property3="F"/>
<ELEMENT Property1="G" Property2="H" Property3="I"/>
</Body>
And I'm trying to have a one string object all of the properties values ordered like this:
A,B,C;D,E,F;G,H,I
I basically need to have each ELEMENT properties seperated by a semicolon.
How to do that?
You can try below expression
string-join((string-join(//ELEMENT/#Property1, ','), string-join(//ELEMENT/#Property2, ','), string-join(//ELEMENT/#Property3, ',')), ';')
Output:
A,D,G;B,E,H;C,F,I
or
string-join((string-join(//ELEMENT[1]/#*, ','), string-join(//ELEMENT[2]/#*, ','), string-join(//ELEMENT[3]/#*, ',')), ';')
output:
A,B,C;D,E,F;G,H,I
Since I wanted , between the properties and ; between the ELEMENTs, here's the required solution for an undefined number of ELEMENTs :
string-join(for $x in //ELEMENT return string-join($x/#*,','), ';')
The result is
A,B,C;D,E,F;G,H,I
I need to filter tag value from the following sample XML.
<ClinicalDocument xmlns="urn:hl7-org:v3">
<id root="3930E379-5C54-477D-8DB2-F6C92BC08C691" />
<component>
<structuredBody>
<component>
<section>
<templateId root="1.3.6.1.4.1.19376.1.5.3.1.3.4"/>
<code code="10164-2" codeSystem="2.16.840.1.113883.6.1"
codeSystemName="LOINC" displayName="HISTORY OF PRESENT ILLNESS"/>
<title>HISTORY OF PRESENT ILLNESS</title>
<text>Patient slipped and fell on ice, twisting her ankle as she fell.
</text>
</section>
</component>
<component>
<section>
<templateId root="1.3.6.1.4.1.19376.1.5.3.1.3.5"/>
<code code="10164-3" codeSystem="2.16.840.1.113883.6.12"
codeSystemName="LOINC1" displayName="DEMO"/>
<title>DEMO HISTORY OF PRESENT ILLNESS</title>
<text>DEMO Patient slipped and fell on ice, twisting her ankle as she fell.
</text>
</section>
</component>
</structuredBody>
</component>
</ClinicalDocument>
there are many file like this in my collection(i am using eXits-db), and i need to filter based on 'root' attribute in <id> tag and 'root' attribute in <templateId> tag. and the result i need is only the <title> text value.
Following is the query i tried.But is shows all the title values(not the one which match my condition).
xquery version "3.0";
declare namespace d = "urn:hl7-org:v3";
(
for $prod in collection("/db/netspectivedb/")/d:ClinicalDocument
where $prod/d:id/#root/string()='3930E379-5C54-477D-8DB2-F6C92BC08C691'
and $prod/d:component/d:structuredBody/d:component/d:section/d:templateId/#root/string()='1.3.6.1.4.1.19376.1.5.3.1.3.4'
return $prod/d:component/d:structuredBody/d:component/d:section/d:title/text()
)
The problem was, that $prod in your XQuery references ClinicalDocument, which isn't specific enough for your purpose. You want to loop through component or section inside structuredBody instead to start with, for example :
declare namespace d = "urn:hl7-org:v3";
(
for $section in collection("/db/netspectivedb/")/d:ClinicalDocument[d:id/#root eq '3930E379-5C54-477D-8DB2-F6C92BC08C691']/d:component/d:structuredBody/d:component/d:section
where $section/d:templateId/#root eq '1.3.6.1.4.1.19376.1.5.3.1.3.4'
return $section/d:title/text()
)
or using nested for as you specifically asked. Nested for also turns out to be more readable in this case :
declare namespace d = "urn:hl7-org:v3";
(
for $prod in collection("/db/netspectivedb/")/d:ClinicalDocument
for $section in $prod/d:component/d:structuredBody/d:component/d:section
where $prod/d:id/#root eq '3930E379-5C54-477D-8DB2-F6C92BC08C691'
and $section/d:templateId/#root eq '1.3.6.1.4.1.19376.1.5.3.1.3.4'
return $section/d:title/text()
)
I am using eq instead of = above since we mean to do value comparison (read more: https://developer.marklogic.com/blog/comparison-operators-whats-the-difference)
You could achieve the same thing with a single XPath expression:
declare namespace d = "urn:hl7-org:v3";
collection("/db/netspectivedb/")/
d:ClinicalDocument[d:id/#root eq '3930E379-5C54-477D-8DB2-F6C92BC08C691']/
d:component/d:structuredBody/d:component/
d:section[d:templateId/#root eq '1.3.6.1.4.1.19376.1.5.3.1.3.4']/d:title/text()
<results>
{
for $p in
(
for $o in doc("mondial-3.0.xml") /mondial/organization
where fn:count($o/members)
order by fn:count($o/members) descending
return <organization>
<name> {$o/#name/string()} </name>
<abbreviation> {$o/#abbrev/string()} </abbreviation>
<num_members> {fn:count($o/members)} </num_members>
<members> {for $m in doc("mondial-3.0.xml") $o/members
return <country> {mondial/country[#id=$m/#country]/#name/string()} </country>} </members>
</organization>
)[position() < 10]
return $p
}
</results>
I am unable to access the ancestor node in this problem as I have got an id stored of a parameter and now I want to match the id of that parameter and get a name of the parameter.
I am not getting any output for this. I am not sure where I am going wrong.
XML FILE :-
The link for the xml file is https://raw.githubusercontent.com/kroell/hsrm-mi-2semester-markuplanguage/master/Abgabe2/Aufgabe2/mondial-3.0.xml
In the link you posted, the root element of the XML is mondial not users, so I'd use mondial in this answer. Notice that member of organization and country are linked by country id, so you can do as follow to get country name of every member element :
<f>
{
for $m in $o/members
return <g> {mondial/country[#id=$m/#country]/#name/string()} </g>
}
</f>
Here is the complete working query. Tested in http://www.xpathtester.com/xquery using XML from the link posted in question as input :
<a>
{
for $p in
(
for $o in /mondial/organization
where fn:count($o/members)
order by fn:count($o/members) descending
return <b>
<c> {$o/#name/string()} </c>
<d> {$o/#abbrev/string()} </d>
<e> {fn:count($o/members)} </e>
<f> {for $m in $o/members
return <g> {/mondial/country[#id=$m/#country]/#name/string()} </g>} </f>
</b>
)[position() < 10]
return $p
}
</a>
You don't define the desired output of your query so it's hard to tell you why your query isn't giving the desired output; but there are several things about the query that suggest it's completely wrong.
Firstly, $o is always a single <mem> element, so fn:count($o) is always 1, so the use of this expression in the where and order by clauses cannot have any useful effect.
Secondly, the expression used to produce the content of the <f> element looks all wrong because it doesn't depend in any way on the value of $o.
I have this XML:
<property id="1011">
<leasehold>No</leasehold>
<freehold>Yes</freehold>
<propertyTypes>
<propertyType>RESIDENTIAL</propertyType>
</propertyTypes>
</property>
and I want to create an xpath statement that is same as the following nested if-else pseudocode block.
if( propertyTypes/propertyType == 'RESIDENTIAL') {
if( leasehold == 'Yes' ){
return 'Rent'
} else
return 'Buy'
}
} else {
if( leasehold == 'Yes' ){
return 'Leasehold'
} else
return 'Freehold'
}
}
I've seen something about Becker's method but I couldn't really follow it. XPath isn't my strong point really.
I. In XPath 2.0 one simply translates this to:
if(/*/propertyTypes/propertyType = 'RESIDENTIAL')
then
(if(/*/leasehold='Yes')
then 'Rent'
else 'Buy'
)
else
if(/*/leasehold='Yes')
then 'Leasehold'
else 'Freehold'
XSLT 2.0 - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:sequence select=
"if(/*/propertyTypes/propertyType = 'RESIDENTIAL')
then
(if(/*/leasehold='Yes')
then 'Rent'
else 'Buy'
)
else
if(/*/leasehold='Yes')
then 'Leasehold'
else 'Freehold'
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<property id="1011">
<leasehold>No</leasehold>
<freehold>Yes</freehold>
<propertyTypes>
<propertyType>RESIDENTIAL</propertyType>
</propertyTypes>
</property>
the XPath expression is evaluated and the result of this evaluation is copied to the output:
Buy
II. XPath 1.0 solution
In XPath 1.0 there isn't an if operator.
A conditional statement can still be implemented with a single XPath 1.0 expression, but this is more tricky and the expression may not be too readable and understandable.
Here is a generic way (first proposed by Jeni Tennison) to produce $stringA when a condition $cond is true() and otherwise produce $stringB:
concat(substring($stringA, 1 div $cond), substring($stringB, 1 div not($cond)))
One of the main achivements of this formula is that it works for strings of any length and no lengths need to be specified.
Explanation:
Here we use the fact that by definition:
number(true()) = 1
and
number(false()) = 0
and that
1 div 0 = Infinity
So, if $cond is false, the first argument of concat() above is:
substring($stringA, Infinity)
and this is the empty string, because $stringA has a finite length.
On the other side, if $cond is true() then the first argument of concat() above is:
sibstring($stringA, 1)
that is just $stringA.
So, depending on the value of $cond only one of the two arguments of concat() above is a nonempty string (respectively $stringA or $stringB).
Applying this generic formula to the specific question, we can translate the first half of the big conditional expression into:
concat(
substring('rent',
1 div boolean(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
),
substring('buy',
1 div not(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
)
)
This should give you an idea how to translate the whole conditional expression into a single XPath 1.0 expression.
XSLT 1.0 - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:copy-of select=
"concat(
substring('rent',
1 div boolean(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
),
substring('buy',
1 div not(/*[leasehold='Yes'
and
propertyTypes/propertyType = 'RESIDENTIAL'
]
)
)
)
"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (above), the XPath expression is evaluated and the result of this evaluation is copied to the output:
buy
Do note:
If you decide to replace the specific strings with other strings that have different lengths than the original, you simply replace these strings in the above XPath 1.0 expression and you don't have to worry about specifying any lengths.
Becker's method for your data is the following:
concat(substring('Rent', 1 div boolean(propertyTypes/propertyType ="RESIDENTIAL" and leasehold="Yes")),
substring('Buy', 1 div boolean(propertyTypes/propertyType ="RESIDENTIAL" and leasehold="No")),
substring('Leasehold', 1 div boolean(propertyTypes/propertyType!="RESIDENTIAL" and leasehold="Yes")),
substring('Freehold', 1 div boolean(propertyTypes/propertyType!="RESIDENTIAL" and leasehold="No")))
Spent all day today, but works for me this is for Xpath 1.0:
concat(
substring(properties/property[#name="Headline"], 1, string-length(properties/property[#name="Headline"]) * 1),
substring(properties/property[#name="Name"], 1, not(number(string-length(properties/property[#name="Headline"]))) * string-length(properties/property[#name="Name"]))
)
Try this
if (condition)
then
if (condition) stmnt
else stmnt
else
if (condition) stmnt
else stmnt