4
\$\begingroup\$

I've been playing with some genealogical files in XML lately and wanted to create a transform that would list, for each person, the person's name, the count of ancestors, and the list of ancestors. Here's what I came up with, but I'd like to see if there's a better way to do this.

<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
>
<xsl:output method="text"/>

<xsl:template match="/people">
    <xsl:apply-templates select="person"/>
</xsl:template>

<!-- for each person, print the name and ancestor count -->
<xsl:template match="person">
    <xsl:value-of select="@name"/>
    <xsl:text>(</xsl:text>
    <xsl:apply-templates select="." mode="count-kin"/>
    <xsl:text>): </xsl:text>
    <!-- now name all of the ancestors -->
    <xsl:apply-templates select="." mode="name-kin"/>
    <xsl:text>
</xsl:text>
</xsl:template>

<!-- recursively name the ancestors -->
<xsl:template match="parent" mode="name-kin">
    <xsl:value-of select="concat(@name,' ')"/>
    <xsl:variable name="myname" select="@name"/>
    <xsl:apply-templates select="/people/person[@name=$myname]" mode="name-kin"/>
</xsl:template>

<!-- recursively name the ancestors -->
<xsl:template match="person" mode="name-kin">
    <xsl:apply-templates select="parent" mode="name-kin"/>
</xsl:template>


<!-- recursively count the number of ancestors -->
<xsl:template match="parent" mode="count-kin">
    <xsl:variable name="myname" select="@name"/>
    <xsl:variable name="ancestor-count">
    <xsl:apply-templates select="/people/person[@name=$myname]" mode="count-kin"/>
    </xsl:variable>
    <xsl:value-of select="$ancestor-count"/>
</xsl:template>

<!-- recursively count the number of ancestors -->
<xsl:template match="person" mode="count-kin">
    <xsl:choose>
    <!-- only count ancestors if there are any -->
    <xsl:when test="count(parent)">
        <xsl:variable name="ancestor-count">
        <xsl:apply-templates select="parent" mode="count-kin"/>
        </xsl:variable>
        <xsl:value-of select="number($ancestor-count)+count(parent)"/>
    </xsl:when>
    <!-- no parents -->
    <xsl:otherwise>
        <xsl:value-of select="0"/>
    </xsl:otherwise>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

Here's a sample input file:

<?xml version="1.0" encoding="iso-8859-1"?>
<people>
<person name="adam"/>
<person name="eve"/>
<person name="cain">
    <parent name="adam"/>
    <parent name="eve"/>
</person>
<person name="henoch">
    <parent name="cain"/>
</person>
<person name="gladys"/>
<person name="frank">
    <parent name="henoch"/>
</person>
<person name="jose">
    <parent name="gladys"/>
    <parent name="frank"/>
</person>
</people>

Here's sample output from xsltproc on Linux:

adam(0): 
eve(0): 
cain(2): adam eve 
henoch(3): cain adam eve 
gladys(0): 
frank(4): henoch cain adam eve 
jose(6): gladys frank henoch cain adam eve 
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Fun question!

Usually when I find myself doing something like this: select="/people/person[@name=$myname]", I'll use an xsl:key instead.

You could use that key to collect all of the parents based on the parent person's @name in a variable.

If you could treat that variable as a node-set (either XSLT 2.0+ or EXSLT's node-set() function) getting the count of ancestors would simply be a matter of counting the number of items in the node-set (count()).

Another option, which feels a little hacky in my opinion, is to output a separator along with the ancestor name.

To get the ancestor count you can remove all characters other than the separator character (nested translate()) and then count the number of separators (string-length()).

You'd also need to replace all of the separators with a space when you finally output the ancestor names (another translate() call).

Example...

XML Input

<people>
    <person name="adam"/>
    <person name="eve"/>
    <person name="cain">
        <parent name="adam"/>
        <parent name="eve"/>
    </person>
    <person name="henoch">
        <parent name="cain"/>
    </person>
    <person name="gladys"/>
    <person name="frank">
        <parent name="henoch"/>
    </person>
    <person name="jose">
        <parent name="gladys"/>
        <parent name="frank"/>
    </person>
</people>

XSLT 1.0

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>

  <xsl:key name="parent" match="parent" use="../@name"/>

  <xsl:template match="person">
    <xsl:variable name="ancestors">
      <xsl:apply-templates select="key('parent',@name)"/>
    </xsl:variable>
    <xsl:variable name="ancestor_count" 
      select="string-length(translate($ancestors,translate($ancestors,'|',''),''))"/>      
    <xsl:value-of select="concat(@name,'(',$ancestor_count,'): ',translate($ancestors,'|',' '))"/>
    <xsl:text>&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="parent">
    <xsl:value-of select="concat(@name,'|')"/>
    <xsl:apply-templates select="key('parent',@name)"/>
  </xsl:template>
  
</xsl:stylesheet>

Output

adam(0): 
eve(0): 
cain(2): adam eve 
henoch(3): cain adam eve 
gladys(0): 
frank(4): henoch cain adam eve 
jose(6): gladys frank henoch cain adam eve 

Fiddle: http://xsltfiddle.liberty-development.net/3MEdvhm

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.