An URI relativization in XQuery - xpath

How to relativize an URI to another URI?
uri1
file:/folder1/file2.txt
uri2
file:/folder1/folder2/file1.txt
needed result
relativize-method($uri1, $uri2) == '../file2.txt'

Something like this (will rewrite it in XQuery):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<xsl:sequence select="my:RelativeUrl(url[1], url[2])"/>
</xsl:template>
<xsl:function name="my:RelativeUrl" as="xs:string">
<xsl:param name="pUrl" as="xs:string"/>
<xsl:param name="pBase" as="xs:string"/>
<xsl:variable name="vurlSegments" select="tokenize($pUrl, '/')"/>
<xsl:variable name="vbaseSegments" select="tokenize($pBase, '/')"/>
<xsl:variable name="vCommonPrefixLength" select=
"(for $i in 1 to count($vbaseSegments)
return
if($vbaseSegments[$i] ne $vurlSegments[$i])
then $i -1
else ()
)[1]
"/>
<xsl:variable name="vUpSteps" select=
"count($vbaseSegments) -$vCommonPrefixLength "/>
<xsl:sequence select=
"string-join
(
(
(for $i in 1 to $vUpSteps
return
'..'
),
(for $k in 1 to count($vurlSegments) - $vCommonPrefixLength
return
$vurlSegments[$vCommonPrefixLength + $k]
)
),
'/'
)
"/>
</xsl:function>
</xsl:stylesheet>
when applied on this XML document:
<t>
<url>file:/folder1/file2.txt</url>
<url>file:/folder1/folder2/file1.txt</url>
</t>
the wanted, correct result is produced:
../../file2.txt
Update:
Below is a pure XPath 3.0 solution:
let $pUrl := "file:/folder1/file2.txt",
$pBase := "file:/folder1/folder2/folder3/file1.txt",
$urlSegments := tokenize($pUrl, '/'),
$baseSegments := tokenize($pBase, '/'),
$idiff := (for $ind in 1 to max((count($urlSegments), count($baseSegments)))
return $ind[$urlSegments[$ind] ne $baseSegments[$ind]]
) [1]
return
string-join(
((1 to count($baseSegments) - count($urlSegments)) ! '..',
$urlSegments[position() ge $idiff])
, '/')

You could tokenize to get the directories, and then use a recursive function to compute the desired result. Something like the following (tested on try.zorba-xquery.com):
declare function local:compute-relative-uri($absolute as xs:string,
$current as xs:string)
{
local:compute-relative-uri-aux(tokenize($absolute, "/"),
tokenize($current, "/"))
};
declare function local:compute-relative-uri-aux($absolute as xs:string*,
$current as xs:string*)
{
if (head($absolute) eq head($current))
then
local:compute-relative-uri-aux(tail($absolute), tail($current))
else
let $steps := (for $dir in 1 to count($current) - 1 return "..", $absolute)
return string-join($steps, "/")
};
let $absolute := "file:/folder1/file2.txt"
let $current := "file:/folder1/folder2/file1.txt"
return
local:compute-relative-uri($absolute, $current)

Related

XSLT how to display/output duplicate values based on different element node and attributes

I am trying to output duplicate values across different nodes and value by using XSLT. I want the node element to be dynamic so it can track different value after the namespace prefix, for example: car:ID, car:Name, car:Location_name, or more. I know i can use the function Local-Name(.) but I am not sure how to apply to my XSLT logic. please help
the sample XML as follow:
<car:root xmlns:car="com.sample">
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_001</car:ID>
<car:Name>Fastmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Automatic</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_002</car:ID>
<car:Name>Slowmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Manual</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
<Car_Input_Request>
<car:Car_Details>
<car:ID>Car_001</car:ID>
<car:Name>Fastmobile</car:Name>
<car:Local_Name>New York</car:Local_Name>
<car:Transmission_Reference_Type>
<car:ID car:type="Transmission_Reference_Type">Automatic</car:ID>
</car:Transmission_Reference_Type>
</car:Car_Details>
</Car_Input_Request>
</car:root>
The XSLT used:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:car="com.sample"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="3.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:value-of select="//car:ID[ let $v:=string(.),$t:=#car:type return not( preceding::car:ID[string(.) = $v and #car:type=$t]) ]/(let $v:=string(.), $t:=#car:type,$c:=1+count(following::car:ID[string(.)=$v and $t=#car:type]) ,$c:=1+count(following::car:*[string(.)=$v]) return if ($c > 1) then concat( string(.), ' occurs ', $c, ' times for type ', $t, '
') else () )"/>
</xsl:template>
</xsl:stylesheet>
output shown from xslt:
Car_001 occurs 2 times for type
Automatic occurs 2 times for type Transmission_Reference_Type
But I want it to show
Car_001 occurs 2 times for type ID
Fastmobile occurs 2 times for type Name
Automatic occurs 2 times for type Transmission_Reference_Type
New York occurs 3 times for type Local_Name
If you are looking for an XSLT solution (rather than a single line XPath expression), you could make use of xsl:for-each-group with a composite key:
<xsl:stylesheet
xmlns:car="com.sample"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
expand-text="yes"
version="3.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each-group select="//car:Car_Details/*" group-by="local-name(), normalize-space()" composite="yes">
<xsl:if test="current-group()[2]">
<xsl:text>{normalize-space()} occurs {count(current-group())} times for {local-name()}
</xsl:text>
</xsl:if>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>

Passing parameters from script to XSL

Using XSLT2 with the latest Saxon HE.
I'm trying to pass multiple coordinate parameters from a script to XSL in order to filter results based on a location boundary box
Script:
java -jar saxon9he.jar -s:litter_bins.xml -o:"bins.xml" -xsl:"Split xml coords.xsl" Coord_2=51.3725 Coord_4=51.3751 Coord_1=-2.3615 Coord_3=-2.3572
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="Coord_2" select="Coord_2"/>
<xsl:param name="Coord_4" select="Coord_4"/>
<xsl:param name="Coord_1" select="Coord_1"/>
<xsl:param name="Coord_3" select="Coord_3"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="node[#lat[ . < $Coord_2 or . > $Coord_4 ] or #lon[ . < $Coord_1 or . > $Coord_3]]"/>
</xsl:stylesheet>
The above returns:
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="JOSM"/>
However if I hard code the coordinates into the match xpath, it returns the expected results.
Xpath:
<xsl:template match="node[#lat[ . < 51.3725 or . > 51.3751 ] or #lon[ . < -2.3615 or . > -2.3572]]"/>
Results:
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="JOSM">
<node id="-102973" visible="true" lat="51.37283499216" lon="-2.359890029">
<tag k="date_creat" v="17/07/2014 07:59:04 AM UTC"/>
<tag k="form_recor" v="888"/>
</node>
<snip...>
</osm>
What am I misunderstanding?
Try to declare a numeric type for the parameters e.g. <xsl:param name="Coord_2" as="xs:double"/> or <xsl:param name="Coord_2" as="xs:decimal"/>. Of course for that your stylesheet needs to declare xmlns:xs="http://www.w3.org/2001/XMLSchema" as a namespace declaration on the root element.
Without a numeric type I think the comparison will be of two xs:untypedAtomic values and then https://www.w3.org/TR/xpath-31/#id-general-comparisons demands
If both atomic values are instances of xs:untypedAtomic, then the
values are cast to the type xs:string
and then the string comparison of negative numbers fails to give you the wanted result.

how can I get a list of indexes of nodes that have a value using xpath

using the following;
<a>
<b>false</b>
<b>true</b>
<b>false</b>
<b>false</b>
<b>true</b>
</a>
I want to get the following result using something like
/a/b[.='true'].position()
for a result like
2,5 (as in a collection of the 2 positions)
I. XPath 1.0 solution:
Use:
count(/*/*[.='true'][1]/preceding-sibling::*)+1
This produces the position of the first b element whose string value is "true":
2
Repeat the evaluation of a similar expression, where [1] is replaced by [2] ,..., etc, up to count(/*/*[.='true'])
XSLT - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="/*/*[.='true']">
<xsl:variable name="vPos" select="position()"/>
<xsl:value-of select=
"count(/*/*[.='true'][$vPos]
/preceding-sibling::*) +1"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<a>
<b>false</b>
<b>true</b>
<b>false</b>
<b>false</b>
<b>true</b>
</a>
The XPath expression is constructed and evaluated for everyb, whose string value is"true". The results of these evaluations are copied to the output:
2
5
II. XPath 2.0 solution:
Use:
index-of(/*/*, 'true')
XSLT 2.0 - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:sequence select="index-of(/*/*, 'true')"/>
</xsl:template>
</xsl:stylesheet>
When this XSLT 2.0 transformation is applied on the same XML document (above), the XPath 2.0 expression is evaluated and the result of this evaluation is copied to the output:
2 5
A basic (& working) approach in python language :
from lxml import etree
root = etree.XML("""
<a>
<b>false</b>
<b>true</b>
<b>false</b>
<b>false</b>
<b>true</b>
</a>
""")
c = 0
lst = []
for i in root.xpath('/a/b/text()'):
c+=1
if i == 'true':
lst.append(str(c))
print ",".join(lst)

Longer node in XPath

I'd like to use XPath to retrieve the longer of two nodes.
E.g., if my XML is
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
And I do document.SelectSingleNode(your XPath here)
I would expect to get back the url1 node. If url2 is longer, or there is no url1 node, I'd expect to get back the url2 node.
Seems simple but I'm having trouble figuring it out. Any ideas?
This works for me, but it is ugly. Cannot you do the comparison outside XPath?
record/*[starts-with(name(),'url')
and string-length(.) > string-length(preceding-sibling::*[1])
and string-length(.) > string-length(following-sibling::*[1])]/text()
<xsl:for-each select="*">
<xsl:sort select="string-length(.)" data-type="number"/>
<xsl:if test="position() = last()">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
Even works in XSLT 1.0!
Use this single XPath expression:
/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]
XSLT - 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:strip-space elements="*"/>
<xsl:template match="/">
<xsl:copy-of select=
"/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
the Xpath expression is evaluated and the result of this evaluation (the selected element) is copied to the output:
<url1>http://www.google.com</url1>

applying the XPath function 'substring-after'

from the given html :
<span class="flag_16 left_16 armenia_16_left"> First League</span>
how i can get the (armenia) string only or at least (armenia_16_left).
thanks in advance.
Use this XPath 1.0 expression:
substring-before(substring-after(substring-after(/span /#class, ' '), ' '), '_')
In XPath 2.0 one can simply use:
tokenize(tokenize(/span /#class, ' ')[last()], '_')[1]
XSLT-based verification:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
"<xsl:value-of select=
"substring-before(substring-after(substring-after(/span /#class, ' '), ' '), '_')
"/>"
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the provided XML document:
<span class="flag_16 left_16 armenia_16_left"> First League</span>
the Xpath expression is evaluated and the result is copied to the output:
"armenia"
When this XSLT 2.0 transformation is applied on the same XML document (above):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
"<xsl:sequence select=
"tokenize(tokenize(/span /#class, ' ')[last()], '_')[1]"/>"
</xsl:template>
</xsl:stylesheet>
again the same correct result is produced:
"armenia"

Resources