LC_123
LC_123

Reputation: 63

Return Distinct Records of XML

I am completely new to XML and XLST. I have a report that spits out in XML. It has several thousand records but it's the same two rows of data being repeated over and over. I need a way to limit the output XML file to only those two unique rows of data.

Here is an example of what my XML file currently looks like:

<zd:Report_Data xmlns:zd="urn:com.xxxx.report/xxxx-Employee_Status-Outbound">
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>1</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>1</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>0</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>0</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   </zd:Report_Data>

This is what I would like it to look like:

<zd:Report_Data xmlns:zd="urn:com.xxxx.report/xxxx-Employee_Status-Outbound">
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>1</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   <zd:Report_Entry>
<zd:empStat.emplStatusCode>A</zd:empStat.emplStatusCode>
<zd:empStat.name>Active Employee</zd:empStat.name>
<zd:worker>
<zd:empStat.lastUpdateDate>1/1/1968</zd:empStat.lastUpdateDate>
<zd:empStat.actvInd>0</zd:empStat.actvInd>
</zd:worker>
   </zd:Report_Entry>
   </zd:Report_Data>

^^updated


I saw something that I thought would work on this site (http://stackoverflow.com/questions/3016929/selecting-unique-records-in-xslt-xpath) but am having trouble applying it to my situation. Any help would be greatly appreciated!

This is what I have so far based on another post I read here. Unfortunately it's not returning any data:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:key name="kItemBy3Children" match="Report_Entry"
     use="concat(empStat.emplStatusCode, '+', empStat.name, '+', empStat.lastUpdateDate, '+', empStat.actvInd)"/>

 <xsl:template match="/">
       <xsl:copy-of select=
        "*/item[generate-id()
              = generate-id(key('kItemBy4Children',
                                concat(empStat.emplStatusCode,
                                       '+', empStat.name,
                       '+', empStat.lastUpdateDate,     
                                       '+', empStat.actvInd)
                               )
                            )
               ]
        "/>
 </xsl:template>
</xsl:stylesheet>

Upvotes: 2

Views: 179

Answers (4)

O. R. Mapper
O. R. Mapper

Reputation: 20750

This is a different solution than your attempt, because I am not using any keys. I think it is easier like this, because you do not have to know how keys in XSLT work (in fact, I don't know how they work because I never needed them so far).

Note that I've declared some URI for your namespace prefix ZD; you'll need to insert yours.

First, I have applied the identity template to basically copy all the contents.

Then, there's a template for <ZD:Report_Entry> elements. In that template, the selected element is omitted iff it has a preceding sibling element with the same contents.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ZD="http://xyz.abc">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="/ZD:Report_Data/ZD:Report_Entry">
        <xsl:choose>
            <xsl:when test="preceding-sibling::ZD:Report_Entry[(ZD:empStat.emplStatusCode = current()/ZD:empStat.emplStatusCode) and (ZD:empStat.name = current()/ZD:empStat.name) and (ZD:worker/ZD:empStat.lastUpdateDate = current()/ZD:worker/ZD:empStat.lastUpdateDate) and (ZD:worker/ZD:empStat.actvInd = current()/ZD:worker/ZD:empStat.actvInd)]"/>
            <xsl:otherwise>
                <xsl:copy-of select="."/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

Checking for the preceding sibling element is done by selecting a preceding sibling <ZD:Report_Entry> element. A condition for that element is that its four content element values match those of the respective content element values of the selected node (current()).

If you need to compare the records for equality based on different criteria, modify the part in the square brackets.

Test case

This example document:

<ZD:Report_Data xmlns:ZD="http://xyz.abc">

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Terminated Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>0</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>2</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Terminated Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>0</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

</ZD:Report_Data>

gets transformed into this output:

<ZD:Report_Data xmlns:ZD="http://xyz.abc">
    <ZD:Report_Entry>
        <ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
        <ZD:empStat.name>Active Employee</ZD:empStat.name>
        <ZD:worker>
            <ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
            <ZD:empStat.actvInd>1</ZD:empStat.actvInd>
        </ZD:worker>
    </ZD:Report_Entry>
    <ZD:Report_Entry>
        <ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
        <ZD:empStat.name>Terminated Employee</ZD:empStat.name>
        <ZD:worker>
            <ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
            <ZD:empStat.actvInd>0</ZD:empStat.actvInd>
        </ZD:worker>
    </ZD:Report_Entry>
    <ZD:Report_Entry>
        <ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
        <ZD:empStat.name>Active Employee</ZD:empStat.name>
        <ZD:worker>
            <ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
            <ZD:empStat.actvInd>2</ZD:empStat.actvInd>
        </ZD:worker>
    </ZD:Report_Entry>
</ZD:Report_Data>

Update: Correctly copied the <ZD:Record_Entry> element instead of just its contents.

Upvotes: 0

Sean B. Durkin
Sean B. Durkin

Reputation: 12729

This style-sheet ...

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:ZD="urn:com.xxxx.report/xxxx-Employee_Status-Outbound">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

<xsl:key name="kItemBy4Children" match="ZD:Report_Entry"
     use="concat(ZD:empStat.emplStatusCode, '+', ZD:empStat.name, '+', ZD:worker/ZD:empStat.lastUpdateDate, '+', ZD:worker/ZD:empStat.actvInd)"/>

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
  </xsl:copy>
</xsl:template>

 <xsl:template match="ZD:Report_Data">
  <xsl:copy>
    <xsl:apply-templates select="@*" />
    <xsl:copy-of select=
      "ZD:Report_Entry[ generate-id() = generate-id(key('kItemBy4Children',
       concat(ZD:empStat.emplStatusCode, '+', ZD:empStat.name, '+', ZD:worker/ZD:empStat.lastUpdateDate, '+', ZD:worker/ZD:empStat.actvInd))[1])]"/>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

... will transform this input document ...

<ZD:Report_Data xmlns:ZD="urn:com.xxxx.report/xxxx-Employee_Status-Outbound">

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Terminated Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>0</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

<ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Terminated Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>0</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>

</ZD:Report_Data>

... into this output document ...

<ZD:Report_Data xmlns:ZD="urn:com.xxxx.report/xxxx-Employee_Status-Outbound">
  <ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Active Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>1</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>
  <ZD:Report_Entry>
<ZD:empStat.emplStatusCode>A</ZD:empStat.emplStatusCode>
<ZD:empStat.name>Terminated Employee</ZD:empStat.name>
<ZD:worker>
<ZD:empStat.lastUpdateDate>1/1/1968</ZD:empStat.lastUpdateDate>
<ZD:empStat.actvInd>0</ZD:empStat.actvInd>
</ZD:worker>
</ZD:Report_Entry>
</ZD:Report_Data>

Explanation

The Report_Data nodes are grouped into groups with the same content as measured by the 4 child members. In other words there is exactly one group per distinct Report_Data, and only the first member of each group is output. The technique is called Muenchian grouping. I could give a long explanation of Muenchian grouping or you could just search for the several hundred StackOverflow questions which already explain this. I would only be repeating what some-one else has written.

Starting points for search:

UPDATE

I made a correction the empStat.emplStatusCode field and the empStat.name field should be prefaced by ZD:worker/

Upvotes: 1

Mitya
Mitya

Reputation: 34576

I tried to put something together that would achieve what you want whilst working generally, not specifically for this XML schema.

I'm tired and it's very fudgey and hackey (take note before hating, people), so there's probably better ways, but it seems to work.

    <!-- root and static content -->
    <xsl:template match="/">
        <root>
            <xsl:apply-templates select='*/*' />
        </root>
    </xsl:template>


    <!-- children - output only unique -->
    <xsl:template match='*'>
        <xsl:variable name='node' select='.' />

        <!-- does this node have identical siblings up ahead? If so, skip it, and we'll output a sibling later -->
        <xsl:variable name='has_identical_siblings'>
            <xsl:for-each select='following-sibling::*[name() = name($node)]'>
                <xsl:call-template name='check_identical'>
                    <xsl:with-param name='this_node_profile'>
                        <xsl:copy-of select='$node' />
                    </xsl:with-param>
                    <xsl:with-param name='check_against'>
                        <xsl:copy-of select='.' />
                    </xsl:with-param>
                </xsl:call-template>
            </xsl:for-each>
        </xsl:variable>

        <!-- output? -->
        <xsl:if test='not(normalize-space($has_identical_siblings))'>
            <xsl:copy-of select='.' />
        </xsl:if>
    </xsl:template>

    <!-- util: two nodes are identical? -->
    <xsl:template name='check_identical'>
        <xsl:param name='this_node_profile' />
        <xsl:param name='check_against' />
        <xsl:if test='$this_node_profile = $check_against'>true</xsl:if>
    </xsl:template>

You can run it here (see output source - one active employee, one teminated).

Upvotes: 0

Michael Kay
Michael Kay

Reputation: 163458

If you just need to select the first two rows, that's very easy:

<xsl:template match="/*">
  <xsl:copy>
    <xsl:copy-of select="*[1]|*[2]"/>
  </xsl:copy>
</xsl:template>

But perhaps I have misunderstood the problem.

Upvotes: 0

Related Questions