Link to home
Start Free TrialLog in
Avatar of tesmc
tesmcFlag for United States of America

asked on

XSL: How to use generate-id() to obtain unique values

I have the following input file.

<A>
	<Request>
		<Traveler>
			<ServiceElement>
				<TicketNumber>83876567104422</TicketNumber>
				<CouponNumber>1</CouponNumber>
			</ServiceElement>
			<ServiceElement>
				<TicketNumber>83876567104422</TicketNumber>
				<CouponNumber>2</CouponNumber>
			</ServiceElement>
			<ServiceElement>
				<TicketNumber>83876567104422</TicketNumber>
				<CouponNumber>4</CouponNumber>
			</ServiceElement>
			<ServiceElement>
				<TicketNumber>83876567104433</TicketNumber>
				<CouponNumber>1</CouponNumber>
			</ServiceElement>
			<ServiceElement>
				<TicketNumber>83876567104422</TicketNumber>
				<CouponNumber>3</CouponNumber>
			</ServiceElement>
			<TicketReference>83876567104422</TicketReference>
		</Traveler>
	</Request>
	<Envelope>
		<Body>
			<MiscRS>
				<Fees>
					<Linked>
						<Fee>
							<Assoc>
								<AssocTicketNumber couponNumber="1">8387656710442</AssocTicketNumber>
								<IssuedDocNumber couponNumber="1">8388209334265</IssuedDocNumber>
							</Assoc>
						</Fee>
						<Fee>
							<Assoc>
								<AssocTicketNumber couponNumber="2">8387656710442</AssocTicketNumber>
								<IssuedDocNumber couponNumber="2">8388209334265</IssuedDocNumber>
							</Assoc>
						</Fee>
						<Fee>
							<Assoc>
								<AssocTicketNumber couponNumber="4">8387656710442</AssocTicketNumber>
								<IssuedDocNumber couponNumber="4">8388209334265</IssuedDocNumber>
							</Assoc>
						</Fee>
						<Fee>
							<Assoc>
								<AssocTicketNumber couponNumber="5">8387656710443</AssocTicketNumber>
								<IssuedDocNumber couponNumber="1">8388209334266</IssuedDocNumber>
							</Assoc>
						</Fee>
						<Fee>
							<Assoc>
								<AssocTicketNumber couponNumber="3">8387656710442</AssocTicketNumber>
								<IssuedDocNumber couponNumber="3">8388209334265</IssuedDocNumber>
							</Assoc>
						</Fee>
					</Linked>
				</Fees>
			</MiscRS>
		</Body>
	</Envelope>
</A>

Open in new window


I want to obtain each unique TicketNumber | TicketReference.  And then use those values to obtain to unique IssuedDocNumber

Currently, I have

<xsl:key name="IssuedDocNumbers" match="IssuedDocNumber" use="." />
 
<xsl:variable name="tktNumber"       select="TicketReference|ServiceElement/TicketNumber" />

<xsl:variable name="DocumentNumbers" select="/A/Envelope/Body/MiscRS/Fees/Linked/Fee/Assoc[AssocTicketNumber=substring($tktNumber,1,13)]/IssuedDocNumber[generate-id()=generate-id(key('IssuedDocNumbers',.))][1] | /A/Envelope/Body/MiscRS/Fees/Linked/Fee/IssuedDocNumber" />

But $ DocumentNumbers  only contains 8388209334265.
I expect it to hold 2 values 8388209334265 & 8388209334266.

What am I doing wrong?
Avatar of Gertone (Geert Bormans)
Gertone (Geert Bormans)
Flag of Belgium image

As a technical detail (before I look into the code)
XSLT1 can not contain sequences in a variable, only datatypes are nodeset, string, number, boolean
For sequences in a variable you should use XSLT2

so you would either concatenate all the values or only show the first one
<xsl:variable name="tktNumber"       select="TicketReference|ServiceElement/TicketNumber" />

As a global variable this one is taking nothing, so this happens inside a particular context I believe
I am not sure based on your XSLT snippet how many TicketReference you are catching

In any case some evil is happening here
Assoc[AssocTicketNumber=substring($tktNumber,1,13)]
if you do a substring on a nodeset > 1 node in XSLT 1, you will do the substring on the 1st node of the nodeset
For me this proves you are using XSLT1, because that would be an error in XSLT2
Just tested, the issue indeed is the substring, working on an alternative
Can you check this?

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">
    <xsl:key name="IssuedDocNumbers" match="IssuedDocNumber" use="." />
     <xsl:template match="/">
        
        <xsl:for-each select="/A/Envelope/Body/MiscRS/Fees/Linked/Fee/Assoc/IssuedDocNumber[generate-id()=generate-id(key('IssuedDocNumbers',.)[1])]">
            <xsl:copy-of select="key('IssuedDocNumbers',.)[/A/Request/Traveler/ServiceElement[substring(TicketNumber, 1, 13) = current()/parent::Assoc/AssocTicketNumber]][1]"/>
        </xsl:for-each>

    </xsl:template>
</xsl:stylesheet>

Open in new window

This one groups per unique IssuedDocNumber
and then checks within each group whether there is a backreference to an earlier AssocTicketNumber
Avatar of tesmc

ASKER

Correct I'm using xslt1
Avatar of tesmc

ASKER

@Geert Bormans - ur solution is good . I get
<IssuedDocNumber couponNumber="1">8388209334265</IssuedDocNumber>
<IssuedDocNumber couponNumber="1">8388209334266</IssuedDocNumber>

But I want to store IssuedDocNumber  into a variable because I then plan to loop through each value.
i.e -
        <xsl:for-each select="$IssuedDocNumber ">
Avatar of tesmc

ASKER

i tried doing

        <xsl:variable name="test" >

        <xsl:for-each select="/A/Envelope/Body/MiscRS/Fees/Linked/Fee/Assoc/IssuedDocNumber[generate-id()=generate-id(key('IssuedDocNumbers',.)[1])]">
            <xsl:copy-of select="key('IssuedDocNumbers',.)[/A/Request/Traveler/ServiceElement[substring(TicketNumber, 1, 13) = current()/parent::Assoc/AssocTicketNumber]][1]"/>
        </xsl:for-each>
        </xsl:variable>


        <xsl:for-each select="$test">


but the for-each=$test would fail with "To use a result tree fragment in a path expression, first convert it to a node-set using the msxsl:node-set() function."



"To use a result tree fragment in a path expression, first convert it to a node-set using the msxsl:node-set() function."
SOLUTION
Avatar of Gertone (Geert Bormans)
Gertone (Geert Bormans)
Flag of Belgium image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of tesmc

ASKER

It used to work when I had

xsl:variable name="tktNumber"       select="TicketReference|ServiceElement/TicketNumber[1]" />
 
But that's bc $tktNumber was ever returning one instance. But now that I took the [1] out I have a collection that I, trying to match against.
Avatar of tesmc

ASKER

I tried the below, but $thisNumber contains both IssuedDocNumbers instead of looping through each. Am I missing something?


 
<xsl:variable name="test" >
      <xsl:for-each select="/A/Envelope/Body/MiscRS/Fees/Linked/Fee/Assoc/IssuedDocNumber[generate-id()=generate-id(key('IssuedDocNumbers',.)[1])]">
        <xsl:copy-of select="key('IssuedDocNumbers',.)[/A/Request/Traveler/ServiceElement[substring(TicketNumber, 1, 13) = current()/parent::Assoc/AssocTicketNumber]][1]"/>
      </xsl:for-each>
    </xsl:variable>

    <xsl:for-each select="msxsl:node-set($test)">
      <xsl:variable name="thisNumber" select="."/>
    </xsl:for-each>

Open in new window

node-set() wraps a document node around the elements
try this

        <xsl:for-each select="msxsl:node-set($test)/IssuedDocNumber">
            <test>
                <xsl:copy-of select="."/>
            </test>
         </xsl:for-each>   
    

Open in new window

Avatar of tesmc

ASKER

But will ur suggestion store into a variable. It's right now outputting into <test> node
Well, first things first.
Your code only created 1 iteration node, my code created two, so the select attribute in my for-each is what you need

A variable in XSLT does not vary and only exists in the context it is created
<xsl:variable name="thisNumber" select="."/>
will never exist outside the for-each and only in the one active 'branch' of the visitor

If you want to create a variable for each node in $test... you can't AND that is not XSLT common practice

Why bother about having different variables if you can do everything you wantw ith one variable
msxsl:node-set($test)/IssuedDocNumber[1]
would be the content of your first expected variable, why not use that directly?
msxsl:node-set($test)/IssuedDocNumber[2]
would be the content of your second expected variable, why not use that directly?
count(msxsl:node-set($test)/IssuedDocNumber) would tell you how much nodes there are

Overall the requirement is too vague. I am trying to guide you on a way, but I am still guessing why you need all those variables
Avatar of tesmc

ASKER

I was thinking that if I put the nodes into a variable then I could use it as parameter for another xml path.
Also, because i wanted to be able to traverse through the existing node containing that IssuedDocNumber.

i.e.
      <xsl:variable name="test" select="../../../AssocTicketNumber | ../../AssocTicketNumber "/>
Well, a variable gives you a static storage of some nodes (or 1 string, or 1 boolean, or  1 number)
That is teh decision you need to make, store a node-set (and access it with the node-set() function)
or store one single item value
Note that allthrough your XSLT you have access to the entire document tree.
Some smart XPath choice could save you a bunch of variables
Avatar of tesmc

ASKER

I did the following but $tktNum is empty. cpnNum returned expected results though. What am I doing wrong?

 
    <xsl:variable name="test" >
      <xsl:for-each select="/A/Envelope/Body/MiscRS/Fees/Linked/Fee/Assoc/IssuedDocNumber[generate-id()=generate-id(key('IssuedDocNumbers',.)[1])]">
        <xsl:copy-of select="key('IssuedDocNumbers',.)[/A/Request/Traveler/ServiceElement[substring(TicketNumber, 1, 13) = current()/parent::Assoc/AssocTicketNumber]][1]"/>
      </xsl:for-each>
    </xsl:variable>



  <xsl:for-each select="msxsl:node-set($test)/IssuedDocNumber">   
      <xsl:variable name="tktNum" select="../AssocTicketNumber"/>
      <xsl:variable name="cpnNum" select="@couponNumber"/>
    </xsl:for-each>
	
	

Open in new window

<xsl:variable name="tktNum" select="..Assoc/AssocTicketNumber"/>
Avatar of tesmc

ASKER

that didn't work. xslt throws error. i also tried
      <xsl:variable name="tktNum" select="../Assoc/AssocTicketNumber"/>
but empty value.
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of tesmc

ASKER

what does the "/parent::*" do at the end of $test variable
Avatar of tesmc

ASKER

Also, I observed that if i try to access another xpath within the for-loop it is not found.

i.e.

      <Issue>
        <xsl:value-of select="/A/Request/Traveler/ServiceElement/TicketNumber[1]"/>
      </Issue>

comes up empty.
what does the "/parent::*" do at the end of $test variable
I go back up one step, to make sure the variable has a series of Assoc elements, not IssuedDocNumber
to be able to use the Assoc Number in the variable

Also, I observed
Yes, that is correct.
An XPath always starts from the context, so even if you do "/A..." you start from the current context and jump to the document node of the current context
A for-each changes context, and basically since your for-each uses node-set, the context of the source document is lost
you are now XPathing in a artificial constructed document, no longer in the source.
If that is what you want to do you can save the entire source in a global variable and go from there
$doc/ancestor::*[not(parent::*)]/A/Request/Traveler/ServiceElement/TicketNumber[1]

but you are really stretching teh boundaries of XSLT1
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
don't think so

$docNum is a Result Tree Fragment
in order to start an XPtha from there, you need the node-set() function

You can put a select attribute on a variable
<xsl:variable name="doc" select="."/>
Then you can XPath into that variable (simply because you make a reference in the source document, you don't construct a piece of the output tree

You need all of that outside the for-each with the node-set, because you need the document context, not the node-set() context
Avatar of tesmc

ASKER

thanks for answering all my questions.
I was able to use all that you suggested except for your last comment about "in order to start an XPtha from there, you need the node-set() function". I couldn't get that to work.

I'd appreciate if you can supply an example of that. Otherwise, I'll open new ticket if needed. THanks.