views:

48

answers:

3

Hi... I have an XML file that looks like the following...

    <states>
 <state>
  <name>North Carolina</name>
  <city>Charlotte</city>
 </state>
 <state>
  <name>Alaska</name>
  <city>Fairbanks</city>
 </state>
 <state>
  <name>Virginia</name>
  <city>Leesburg</city>
 </state>
 <state>
  <name>Alaska</name>
  <city>Coldfoot</city>
 </state>
 <state>
  <name>North Carolina</name>
  <city>Harrisburg</city>
 </state>
 <state>
  <name>Virginia</name>
  <city>Ashburn</city>
 </state>  
</states>

I need to produce a report that lists each state, is alphabetical order with each city following.... such as ..

Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn

(the cities do not have to be in alpha order, just the states)

I tried to solve this by doing a for-each on states/state, sorting it by name and processing it. Like this....

    <xsl:for-each select="states/state">
       <xsl:sort select="name" data-type="text" order="ascending"/>
       <xsl:value-of select="name"/>-<xsl:value-of select="city"/>
    </xsl:for-each>   

This gave me....

 Alaska - Fairbanks
 Alaska - Coldfoot
 North Carolina - Charlotte
 North Carolina - Harrisburg
 Virginia - Leesburg
 Virginia - Ashburn

The sorting worked, now I want to group. The only thing I could think to do was to compare to the previous state, since it is sorted, it should recognize if the state value has not changed. Like this...

<xsl:for-each select="states/state">
             <xsl:sort select="name" data-type="text" order="ascending"/>
  <xsl:variable name="name"><xsl:value-of select="name">
  <xsl:variable name="previous-name"><xsl:value-of select="(preceding-sibling::state)/name">
  <xsl:if test="$name != $previous-name">
   <br/><xsl:value-of select="name"/>-
  </xsl:if>
  <xsl:value-of select="city"/>
 </xsl:for-each>

Sadly, it appears that the preceding-sibling feature does not work well with the sort, so, the first time through (on the first Alaska) it saw the first North Carolina as a preceding sibling. This causes some weird results, which were not at all to my liking.

So, I am using XSLT1.0... Any thoughts/suggestions?

Thanks

+1  A: 

For grouping in XSLT 1.0 you are probably going to have to use the Muenchian Method. It can be hard to understand but once you get it working you should be good to go.

Abe Miessler
starting reading about it.... got lost quickly. Will try again.
Doug
Yeah I won't lie, you've got your work cut out for you. I've only implemented it a few times but I always struggle...
Abe Miessler
+2  A: 

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:key name="kStateByName" match="state" use="name"/>
    <xsl:output method="text"/>
    <xsl:template match="/">
        <xsl:apply-templates
                   select="/*/state[count(.|key('kStateByName',name)[1])=1]">
            <xsl:sort select="name"/>
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="state">
        <xsl:value-of select="concat(name,' - ')"/>
        <xsl:apply-templates select="key('kStateByName',name)/city"/>
    </xsl:template>
    <xsl:template match="city">
        <xsl:value-of select="concat(.,substring(', ', 
                                                 1 div (position()!=last())),
                                       substring('&#xA;',
                                                 1 div (position()=last())))"/>
    </xsl:template>
</xsl:stylesheet>

Output:

Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn

Note: Grouping by State's name. Separator substring expression only works with a pull style (applying templates to city)

An XSLT 2.0 solution:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="text"/>
    <xsl:template match="states">
        <xsl:for-each-group select="state" group-by="name">
            <xsl:sort select="name"/>
            <xsl:value-of select="concat(name,
                                         ' - ',
                                         string-join(current-group()/city,', '),
                                         '&#xA;')"/>
        </xsl:for-each-group>
    </xsl:template>
</xsl:stylesheet>

Just for fun, this XPath 2.0 expression:

string-join(for $state in distinct-values(/*/*/name)
            return concat($state,
                          ' - ',
                          string-join(/*/*[name=$state]/city,
                                      ', ')),
            '&#xA;')
Alejandro
+1, your the man Alejandro. The 1.0 solution looks like the Muenchian Method, is that correct?
Abe Miessler
@Abe Miessler: You are right. It's the Muenchian Method. It's the standar grouping for XSLT 1.0
Alejandro
+1 for a correct and short answer.
Dimitre Novatchev
A: 

This will return a distinct list of states:

  <xsl:for-each select="states/state">
     <xsl:sort select="name" />
     <xsl:if test="not(name = preceding-sibling::state/name)" >
         <xsl:value-of select="name" />
     </xsl:if>
  </xsl:for-each>

I used your example XML, built a little style sheet with the above, ran it through Xalan-j, and it returns:

Alaska North Carolina Virginia

So from there you should be able to apply a template or another for-each loop to pull the list of cities for each distinct state.

Chris

Chris Houseknecht
@Chris Houseknecht: This is the way to go when you have no access to `key` (Long, long ago). But if you take comparison as complexity unit, you will have 1 + (N * N) / 2 complexity. Then you need to process the group. That will be N' * N complexity with node set comparison. Also, that test is not necesary. You could have that test as a predicate added to for-each select expression.
Alejandro