Thursday, March 31, 2011

How to emulate C enumeration in XSLT with optional values

I'm trying to make a XSLT conversion that generates C code, the following XML should be converted:

<enum name="anenum">
  <enumValue name="a"/>
  <enumValue name="b"/>
  <enumValue name="c" data="10"/>
  <enumValue name="d" />
  <enumValue name="e" />
</enum>

It should convert to some C code as following:

enum anenum {
   a = 0,
   b = 1,
   c = 10,
   d = 11,
   e = 12
}

or alternatively (as the C preprocessor will handle the summation):

   enum anenum {
       a = 0,
       b = 1,
       c = 10,
       d = c+1,
       e = c+2
    }

The core of my XSLT looks like:

<xsl:for-each select="enumValue">
  <xsl:value-of select="name"/>
  <xsl:text> = </xsl:text>
  <xsl:choose>
    <xsl:when test="string-length(@data)&gt;0">
      <xsl:value-of select="@data"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="position()-1"/>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:text>,

(for simplicity I skip some of the 'no comma at the last element' code)

This example will not generate the correct values for d and e

I've been trying to get it working for the variable d and e, but so far I'm unsuccessful.

Using constructions like:

<xsl:when test="string-length(preceding-sibling::enumValue[1]/@datavalue)&gt;0">
    <xsl:value-of select="preceding-sibling::enumValue/@data + 1"/>
</xsl:when>

...only work for the first one after the specified value (in this case d).

Who can help me? I'm probably thinking too much in a procedural way...

From stackoverflow
  • You're quite close, I think, but you need the data value of the first preceding enumValue sibling with a data attribute, not the data value of the first preceding enumValue. Then add the number of preceding enumValue siblings of the current node, and subtract the number of preceding enumValue siblings of the node from which you took the data value.

  • You can't change "variables" in xsl but you can use recursion. Don't use preceding-sibling predicates unless absolutely urgent as they will kill your performance.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
        <xsl:template match="/" >
         <xsl:call-template name="printEnum">
          <xsl:with-param name="value" select="0"/>
          <xsl:with-param name="position" select="1"/>
         </xsl:call-template>
        </xsl:template>
    
        <xsl:template name="printEnum">
         <xsl:param name="position"/> 
         <xsl:param name="value" select="0"/>
         <xsl:variable name="node" select="/enum/enumValue[$position]"/>
         <xsl:variable name="enumValue">
          <xsl:choose>
           <xsl:when test="$node/@data">
            <xsl:value-of select="$node/@data"/>
           </xsl:when>
           <xsl:otherwise>
            <xsl:value-of select="$value + 1"/>
           </xsl:otherwise>
          </xsl:choose>  
         </xsl:variable>  
         <xsl:value-of select="concat($node/@name, ' = ', $enumValue, ' , ')"/>
         <xsl:if test="/enum/enumValue[$position + 1]">
          <xsl:call-template name="printEnum">
           <xsl:with-param name="value" select="$enumValue"/>
           <xsl:with-param name="position" select="$position + 1"/>
          </xsl:call-template>
         </xsl:if>
        </xsl:template>
    </xsl:stylesheet>
    
    Roalt : Thank you! However, you have to change in the last call-template, the line into in order to let it work.
    Goran : Fixed :) - glad it works
  • A nonrecursive solution, using keys:

    <xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:strip-space elements="*"/>
     <xsl:output method="text"/>
    
     <xsl:key name="koffsetEnums" match="enumValue[@data]"
      use="generate-id()"/>
    
        <xsl:template match="enum">
          enum <xsl:value-of select="@name"/> {
          <xsl:apply-templates select="enumValue"/>
          }
        </xsl:template>
    
        <xsl:template match="enumValue">
          <xsl:value-of select="concat(@name, ' = ')"/>
    
          <xsl:variable name="voffsetValueId" select=
           "generate-id((. | preceding-sibling::enumValue)
                                                [@data][last()]
                      )"/>
    
          <xsl:choose>
            <xsl:when test="not($voffsetValueId)">
              <xsl:value-of select="concat(position(),'&#xA;      ')"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:variable name="vinitOffset" select=
               "key('koffsetEnums', $voffsetValueId)/@data"
               />
    
               <xsl:value-of select=
                "$vinitOffset
                +
                   count(preceding-sibling::enumValue)
                 -
                   count(key('koffsetEnums', $voffsetValueId)/preceding-sibling::enumValue)
                "
                />
               <xsl:text>&#xA;      </xsl:text>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:template>
    
    </xsl:stylesheet>
    

    When the above transformation is applied on the originally provided XML document:

    <enum name="anenum">
        <enumValue name="a"/>
        <enumValue name="b"/>
        <enumValue name="c" data="10"/>
        <enumValue name="d" />
        <enumValue name="e" />
    </enum>
    

    the required result is produced:

    enum anenum {
          a = 1
          b = 2
          c = 10
          d = 11
          e = 12
    
          }
    
    Andrew Cowenhoven : Really like this one!
    Roalt : Wow, this one is also nice! I've also considered keys but did not quite know how to manage them!
    Dimitre Novatchev : @Roalt I have improved the solution and now it seems really good, see my new answer. :)
  • A better solution with keys, avoiding most use of the preceding-sibling axis:

    This transformation:

    <xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:strip-space elements="*"/>
        <xsl:output method="text"/>
    <!--                                              -->   
        <xsl:key name="ksimpleEnValues" match="enumValue[not(@data)]"
         use="generate-id(preceding-sibling::enumValue[@data][1])"/>
    <!--                                              -->       
        <xsl:template match="enum">
        enum <xsl:value-of select="@name"/>
          {      
           <xsl:apply-templates select=
            "key('ksimpleEnValues', '')
            "/>
            <xsl:apply-templates select="enumValue[@data]"/>
          }
        </xsl:template>
    <!--                                              -->
        <xsl:template match="enumValue">
          <xsl:param name="pOffset" select="0"/>
         <xsl:value-of select=
          "concat(@name, ' = ', position()+$pOffset,'&#xA;      ')"/>
        </xsl:template>
    <!--                                              -->
        <xsl:template match="enumValue[@data]">
         <xsl:value-of select=
          "concat(@name, ' = ', @data,'&#xA;      ')"/>
    <!--                                              -->
          <xsl:apply-templates select=
               "key('ksimpleEnValues', generate-id())">
           <xsl:with-param name="pOffset" select="@data"/>
          </xsl:apply-templates>
      </xsl:template>    
    </xsl:stylesheet>
    

    when applied on the originally-provided XML document:

    <enum name="anenum">
        <enumValue name="a"/>
        <enumValue name="b"/>
        <enumValue name="c" data="10"/>
        <enumValue name="d" />
        <enumValue name="e" />
    </enum>
    

    Produces the wanted result:

    enum anenum
     {      
      a = 1
      b = 2
      c = 10
      d = 11
      e = 12
     }
    

    Explanation:

    1. The key named ksimpleEnValues indexes all enumValue elements that do not have the data attribute. The indexing is done by the generate-id() value of the first preceding enumValue element that has a data attribute.

    2. Thus key('ksimpleEnValues', someId) is the nodeset containing all enumValue elements following the enumValue that has its generate-id() equal to someId, and all these enumValue elements are preceding the next enumValue with a data attribute, if such exists.

    3. key('ksimpleEnValues', '') is the node-set of all enumValue elements that do not have a preceding enumValue element with a data attribute.

    4. The template that matches enumValue takes an optional parameter $pOffset, in which the value of the data attribute from the immediate preceding enumValue element with this attribute, will be passed, otherwise the default value for $pOffset is 0.

    5. The template matching enumValue elements that have a data attribute produces its enum-value (@name = @data) and then applies templates to all enumValue elements between itself and the next (if such exists) enumValue with a data attribute. The value of the data attribute is passed as the $pOffset parameter and it will be added to the relative position of each selected enumValue element when producing the output from its processing.

    Dimitre Novatchev : If anybody can explain why this answer has become "community wiki", I would greatly appreciate it. ?!?
    Roalt : I put the flag there when I accepted the first/initial answer.
    Roalt : I think this improvement is probably better than your previous answer, but I use your previous answer because (1) it's implemented and it works and (2) the performance isn't an issue.
    Dimitre Novatchev : @Roalt But if something is marked "community wiki" this prevents it for receiving further votes -- not a good thing.
    Dimitre Novatchev : @Roalt But the last answer is also implemented. And it works. Or what do you mean by "but I use your previous answer because (1) it's implemented and it works "
    Roalt : Ok, thanks for telling me on the community wiki, I won't do it again.
    Roalt : @Dimitre The accepted answer was put in some other more complex XSLT code with some slightly different tag-names including namespaces. I don't need the extra performance of your improved solution.

0 comments:

Post a Comment