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?
badtz7229Asked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Gertone (Geert Bormans)Information ArchitectCommented:
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
Gertone (Geert Bormans)Information ArchitectCommented:
<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
Gertone (Geert Bormans)Information ArchitectCommented:
Just tested, the issue indeed is the substring, working on an alternative
Big Business Goals? Which KPIs Will Help You

The most successful MSPs rely on metrics – known as key performance indicators (KPIs) – for making informed decisions that help their businesses thrive, rather than just survive. This eBook provides an overview of the most important KPIs used by top MSPs.

Gertone (Geert Bormans)Information ArchitectCommented:
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

Gertone (Geert Bormans)Information ArchitectCommented:
This one groups per unique IssuedDocNumber
and then checks within each group whether there is a backreference to an earlier AssocTicketNumber
badtz7229Author Commented:
Correct I'm using xslt1
badtz7229Author Commented:
@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 ">
badtz7229Author Commented:
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."
Gertone (Geert Bormans)Information ArchitectCommented:
Yes, you can not store an Result Tree Fragment in a variable and use it as a nodeset in XSLT1.
There are extension functions that can do that, but they are XSLT processor dependent
So, you need to tell me what processor you are using, so I can show you

For microsoft xml you would do
      <xsl:for-each select="msxsl:node-set($test)/self::IssuedDocNumbers">
in order to do that you need to add this namespace declaration to the stylesheet element
xmlns:msxsl="urn:schemas-microsoft-com:xslt"

like this
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                version="1.0">

In general however, I have a feeling you are using variables too much.
Look at how I went from your earliest solution to a variable less suggestion
badtz7229Author Commented:
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.
badtz7229Author Commented:
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

Gertone (Geert Bormans)Information ArchitectCommented:
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

badtz7229Author Commented:
But will ur suggestion store into a variable. It's right now outputting into <test> node
Gertone (Geert Bormans)Information ArchitectCommented:
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
badtz7229Author Commented:
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 "/>
Gertone (Geert Bormans)Information ArchitectCommented:
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
badtz7229Author Commented:
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

Gertone (Geert Bormans)Information ArchitectCommented:
<xsl:variable name="tktNum" select="..Assoc/AssocTicketNumber"/>
badtz7229Author Commented:
that didn't work. xslt throws error. i also tried
      <xsl:variable name="tktNum" select="../Assoc/AssocTicketNumber"/>
but empty value.
Gertone (Geert Bormans)Information ArchitectCommented:
of course, I had a typo ;-)

Anyway, I see you have not included the Assoc in the node-set() constructed variable
so you just have
You have turned a result tree fragment into a nodeset, the source document context has been completely lost
If you need that value, you need to do this

<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]/parent::*"/>
      </xsl:for-each>
    </xsl:variable>
Gertone (Geert Bormans)Information ArchitectCommented:
Now the Assoc is in the variable

<xsl:for-each select="msxsl:node-set($test)/Assoc">  
      <xsl:variable name="tktNum" select="AssocTicketNumber"/>
      <xsl:variable name="cpnNum" select="IssuedDocNumber/@couponNumber"/>
    </xsl:for-each>
badtz7229Author Commented:
what does the "/parent::*" do at the end of $test variable
badtz7229Author Commented:
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.
Gertone (Geert Bormans)Information ArchitectCommented:
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
badtz7229Author Commented:
    <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]/parent::*"/>
      </xsl:for-each>
    </xsl:variable>
    
    
    <xsl:for-each select="msxsl:node-set($test)/Assoc">
      
      <xsl:variable name="docNum" >
        <xsl:copy-of select="IssuedDocNumber"/>
      </xsl:variable>

      <xsl:variable name="tktNum" select="AssocTicketNumber"/>
      <xsl:variable name="cpnNum" select="IssuedDocNumber/@couponNumber"/>

      <test>
        <xsl:copy-of select="."/>
      </test>

      

      <Issue>
        <xsl:value-of select="$docNum/ancestor::*[not(parent::*)]/A/Request/Traveler/ServiceElement/TicketNumber[1]"/>
      </Issue>

    </xsl:for-each>

Open in new window


Just out of curiousity do I have the <Issue> constructed correctly per your last statement?

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
Gertone (Geert Bormans)Information ArchitectCommented:
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
badtz7229Author Commented:
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.
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
CSS

From novice to tech pro — start learning today.