XSLT format-date for an attribute

1.3k views Asked by At
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
  <cd created_at="2016-12-15T15:02:55Z">
  <title created_at="2016-12-15T15:02:55Z">Empire Burlesque</title>
  <artist created_at="2016-12-15T15:02:55Z">Bob Dylan</artist>
  <cover created_at="2016-12-15T15:02:55Z"/>
  <company>Columbia</company>
  <price>10.90</price>
  <year>1985</year>
</cd>

I want to format all occurrences of created_at attribute

input format YYYY-MM-DDTHH:MM:SSZ
output format YYYY-MM-DD HH:MM:SS

I am currently using this following xslt

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>

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

<!-- Edit dates to conform to dbunit format-->
<xsl:template match="@created_at">
    <xsl:copy>
        <xsl:call-template name="formatdate">
            <xsl:with-param name="datestr" select="@created_at"/>
        </xsl:call-template>
    </xsl:copy>
</xsl:template>


<xsl:template name="formatdate">
    <xsl:param name="datestr" />
    <!-- input format YYYY-MM-DDTHH:MM:SSZ -->
    <!-- output format YYYY-MM-DD HH:MM:SS -->

    <xsl:variable name="datetext">
        <xsl:value-of select="substring-before($datestr,'T')" />
    </xsl:variable>

    <xsl:variable name="timetext">
        <xsl:value-of select="substring($datestr,12,18)" />
    </xsl:variable>

    <xsl:value-of select="concat($datetext, ' ', $timetext)" />
</xsl:template>
</xsl:stylesheet>

However as I debug through the transformation xslt it does not seem to enter the formatdate call-template. Is my xpath wrong? I found articles on modifying the node, but not the attribute. Any help would be much appreciated.

Thank you

3

There are 3 answers

0
Alexander Petrov On

Try this

<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
<xsl:output method='xml' indent='yes'/>

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

<!-- Edit dates to conform to dbunit format-->
<xsl:template match="@created_at">    
    <xsl:call-template name="formatdate">
        <xsl:with-param name="datestr" select="."/>
    </xsl:call-template>    
</xsl:template>    

<xsl:template name="formatdate">
    <xsl:param name="datestr" />
    <!-- input format YYYY-MM-DDTHH:MM:SSZ -->
    <!-- output format YYYY-MM-DD HH:MM:SS -->

    <xsl:variable name="datetext">
        <xsl:value-of select="substring-before($datestr,'T')" />
    </xsl:variable>

    <xsl:variable name="timetext">
        <xsl:value-of select="substring($datestr,12,8)" />
    </xsl:variable>

    <xsl:attribute name="created_at">
        <xsl:value-of select="concat($datetext, ' ', $timetext)" />
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>
0
michael.hor257k On

Why not simply:

<xsl:template match="@created_at">
    <xsl:attribute name="created_at">
        <xsl:value-of select="substring(translate(., 'T', ' '), 1, 19)" />
    </xsl:attribute>
</xsl:template>

Note: you cannot use xsl:copy if you want to change an attribute's value.

5
Eiríkr Útlendi On

From your post, it sounds like all you need is simple string processing.

Why your code isn't working the way you want

You're handling the @created_at attributes with this template:

<xsl:template match="@created_at">
    <xsl:copy>
        <xsl:call-template name="formatdate">
            <xsl:with-param name="datestr" select="@created_at"/>
        </xsl:call-template>
    </xsl:copy>
</xsl:template>

The kicker here is that you're using <xsl:copy>. When used with attributes, <xsl:copy> copies the entire attribute, name and value both. And since attributes can't contain any children, the children of your <xsl:copy> instruction are ignored -- so the XSLT processor never evaluates the <xsl:call-template name="formatdate"> instruction.

A different approach that works

Instead of using <xsl:copy>, you need to instead use <xsl:attribute> to create an attribute in a way where you can also specify the value. In this case, you already know the name of the attribute you want to create, so you could hard-code the name value as created_at. For a more flexible approach, you could instead give the name value as {name(.)} -- this just grabs the name of the attribute being processed, which is closer in behavior to what you probably thought <xsl:copy> would do. :)

It is also possible to produce the desired string in a single xsl:value-of expression, without relying on so many variables.

<xsl:template match="@created_at">
    <xsl:attribute name="{name(.)}">
        <xsl:value-of select="concat(substring-before(., 'T'), ' ', substring-before(substring-after(., 'T'), 'Z'))"/>
    </xsl:attribute>
</xsl:template>

Breaking down that select statement:

  • Use concat() to stitch together multiple bits of string.
  • Use substring-before(., 'T') to grab everything before the T -- that's the date portion.
  • ' ' adds the single space in the middle.
  • substring-before(substring-after(., 'T'), 'Z') --
    • The inner expression substring-after(., 'T') grabs everything after the T -- that's the time portion.
    • However, there's that pesky Z on the end, so we use substring-before as the outer expression to lop that off.

No need for variables, and it gets the job done. Confirmed to work with XSLT 1.0.