Assume the position

XPath is a slippery beast. I use it infrequently when crafting XSLT - and not often enough to really grok how axes work. It doesn't help that the syntax is unlike anything else that I work with and is especially arcane.

Consider a recent scenario - the following (badly structured) XML:

<CustomersData>
  <Customer1Data>
    <Customer1Forename>Dave</Customer1Forename>
    <Customer1Surname>Davison</Customer1Surname>
    <Customer1Reference>123456</Customer1Reference>
  </Customer1Data>
  <Customer2Data>
    <Customer2Forename>Edith</Customer2Forename>
    <Customer2Surname>Ericson</Customer2Surname>
    <Customer2Reference>ABCDEF</Customer2Reference>
  </Customer2Data>
  <Customer3Data>
    <Customer3Forename>Freddy</Customer3Forename>
    <Customer3Surname>Farquar</Customer3Surname>
    <Customer3Reference>987-65</Customer3Reference>
  </Customer3Data>
</CustomersData>

I need to transform this into the following (better structured) XML:

<Customers>
  <Customer>
    <Reference>123456</Reference>
    <FirstName>Dave</FirstName>
    <LastName>Davison</LastName>
  </Customer>
  <Customer>
    <Reference>ABCDEF</Reference>
    <FirstName>Edith</FirstName>
    <LastName>Ericson</LastName>
  </Customer>
  <Customer>
    <Reference>987-65</Reference>
    <FirstName>Freddy</FirstName>
    <LastName>Farquar</LastName>
  </Customer>
</Customers>

It would be great to have a single template to create my target Customer elements and their children - but what about the input? Semantically <Customer1Data> is entirely different to <Customer2Data> so I can't use a single template can I?

Well, although they're named differently the data in the CustomerData sections is in the same order, Forename, then Surname, then Reference. So perhaps it would work if I could select the data by position...

And this is where XPath's wilfully arcane syntax starts to baffle. Do I need to use descendant... or perhaps child...

Actually neither. The answer is surprisingly simple:

  <xsl:template match="/CustomersData">
    <Customers>
      <xsl:apply-templates select="Customer1Data" mode="AddCustomer" />
      <xsl:apply-templates select="Customer2Data" mode="AddCustomer" />
      <xsl:apply-templates select="Customer3Data" mode="AddCustomer" />
    </Customers>
  </xsl:template>
 
  <xsl:template match="*" mode="AddCustomer">
    <Customer>
      <Reference>
        <xsl:value-of select="*[3]"/>
      </Reference>
      <FirstName>
        <xsl:value-of select="*[1]"/>
      </FirstName>
      <LastName>
        <xsl:value-of select="*[2]"/>
      </LastName>
    </Customer>
  </xsl:template>

Yes, I'm calling the AddCustomer template 3 times (and in this trivial example you could work around that) - but I'm using a single template that selects child values according to position.

Neat!

Comments