0

I am working on an XSLT version 1.0 (apologies, I know 2.0 is much better!) and thanks to this brilliant community actually got somewhere. I am now stuck again. I am trying to convert this raw and less than attractive XML to a .csv:

<transmissions>
  <ES_TRANSMISSION>
    <tx_txblocks>
      <ES_TXBLOCK>
        <txb_announcedtime>
          <ESP_TIMEDURATION time="25:00:00.000" tvdayhours="25" hours="1" minutes="00" />
        </txb_announcedtime>
        <duration>
          <ESP_TIMEDURATION duration="50:00.000" />
        </duration>
        <product>
          <ES_PRODUCT p_product_calculatedbroadcasttitle="4" />
        </product>
      </ES_TXBLOCK>
    </tx_txblocks>
    <tx_date>
      <ESP_DATE date="2019-10-12" dateindays="43383" day="12" dayname="Saturday" month="10" monthname="October" productionweeknumber="41" weekNumberYear="2019" weekdaynumber="6" weeknumber="41" year="2019" />
    </tx_date>
    <tx_duration>
      <ESP_TIMEDURATION duration="50:00.000" hours="0" minutes="50" />
    </tx_duration>
    <tx_channel>
      <ESP_CHANNEL name="Channel A">
        <popupLookups />
      </ESP_CHANNEL>
    </tx_channel>
  </ES_TRANSMISSION>
  <ES_TRANSMISSION>
    <tx_txblocks>
      <ES_TXBLOCK>
        <txb_announcedtime>
          <ESP_TIMEDURATION time="25:50:00.000" tvdayhours="25" hours="1" minutes="50" />
        </txb_announcedtime>
        <duration>
          <ESP_TIMEDURATION duration="45:00.000" />
        </duration>
        <product>
          <ES_PRODUCT p_product_calculatedbroadcasttitle="5" />
        </product>
      </ES_TXBLOCK>
    </tx_txblocks>
    <tx_date>
      <ESP_DATE date="2019-10-12" dateindays="43383" day="12" dayname="Saturday" month="10" monthname="October" productionweeknumber="41" weekNumberYear="2019" weekdaynumber="6" weeknumber="41" year="2019" />
    </tx_date>
    <tx_duration>
      <ESP_TIMEDURATION duration="45:00.000" hours="0" minutes="45" />
    </tx_duration>
    <tx_channel>
      <ESP_CHANNEL name="Channel A">
        <popupLookups />
      </ESP_CHANNEL>
    </tx_channel>
  </ES_TRANSMISSION>
</transmissions>

The third party has some requirements on the .csv, such as always needing two numbers (sorted) and loads of commas because....why not!

But every time I try a for-each to get it to report on more than the first item on the list it comes back with NaN. Below is the XSLT I have built (apologies for it being so basic) that works, but only takes the first item into consideration. I have tried various <xsl:for-each> for it, but every time it breaks it.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  <xsl:output encoding="utf-8" method="text" omit-xml-declaration="yes"/>

  <xsl:template match="/">
    <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@day, '00')"/> 
    <xsl:text>/</xsl:text>
    <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@month, '00')"/>
    <xsl:text>/</xsl:text>
    <xsl:value-of select="transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@year"/>
    <xsl:text>,</xsl:text>
    <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_txblocks/ES_TXBLOCK/txb_announcedtime/ESP_TIMEDURATION/@hours, '00')"/>
    <xsl:text>:</xsl:text><xsl:value-of select="transmissions/ES_TRANSMISSION/tx_txblocks/ES_TXBLOCK/txb_announcedtime/ESP_TIMEDURATION/@minutes"/>
    <xsl:text>,</xsl:text>
    <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_duration/ESP_TIMEDURATION/@hours, '00')"/><xsl:text>:</xsl:text> 
    <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_duration/ESP_TIMEDURATION/@minutes, '00')"/>
    <xsl:text>,</xsl:text>
    <xsl:value-of select="translate(transmissions/ES_TRANSMISSION/tx_txblocks/ES_TXBLOCK/product/ES_PRODUCT/@p_product_calculatedbroadcasttitle, '.,', '')"/>
    <xsl:text>,,,,,,,,,,,,,,,</xsl:text>
    <xsl:text>TEST</xsl:text>
    <xsl:text>&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="ESP_DATE" mode="full">
    <xsl:value-of select="@day"/>
    <xsl:text>00</xsl:text>
  </xsl:template>

  <xsl:template match="ESP_DATE" mode="full">
    <xsl:text>00</xsl:text>
    <xsl:value-of select="@month"/>
  </xsl:template>

  <xsl:template match="ESP_TIMEDURATION" mode="full">
    <xsl:text>00</xsl:text>
    <xsl:value-of select="@hour"/>
  </xsl:template>

  <xsl:template match ="ES_TRANSMISSION"/>
  <xsl:template match="ES_PRODUCT" mode="full"/>
</xsl:stylesheet>

Any hints or guidance would be greatly appreciated. And then I will leave you all alone for a bit while I try to talk people I work with to use 2.0 because I have this massive reference book that I know would be extremely handy!

Tomalak
  • 332,285
  • 67
  • 532
  • 628
Anne S
  • 5
  • 2
  • Thanks @Tomalak, I will remember that for next time to ensure it looks the part. – Anne S Nov 19 '19 at 17:27
  • 1
    Here is a handy XML formatter that can bring messy XML (and other stuff) into shape: https://www.freeformatter.com/xsl-transformer.html. For testing XSLT you can use [XSLT Fiddle](https://xsltfiddle.liberty-development.net/). – Tomalak Nov 19 '19 at 17:28
  • 1
    Also I strongly recommend using `yyyy-mm-dd` as the only date pattern. Your `dd/mm/yyyy` is an ambiguous format and should not be used to store dates. The `` element already has the right format, you could use this straight-away instead of putting effort into creating an inferior date representation. – Tomalak Nov 19 '19 at 17:38
  • Hi Tomalak, Thank you for the link, that is being saved as a new favourite. On the second comment, unfortunately it is a requirement for a 3rd party to have it in the dd/mm/yyyy format, although like you I feel that the proper way of having the date pattern is better. – Anne S Nov 20 '19 at 09:23

2 Answers2

1

You are looking for something like this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="utf-8" />

  <xsl:template match="/">
    <xsl:apply-templates select="transmissions/ES_TRANSMISSION" />
  </xsl:template>

  <xsl:template match="ES_TRANSMISSION">
    <xsl:apply-templates select="tx_date/ESP_DATE" mode="csv" />
    <xsl:text>,</xsl:text>
    <xsl:apply-templates select="tx_txblocks/ES_TXBLOCK/txb_announcedtime/ESP_TIMEDURATION" mode="csv" />
    <xsl:text>,</xsl:text>
    <xsl:apply-templates select="tx_duration/ESP_TIMEDURATION" mode="csv" />
    <xsl:text>,</xsl:text>
    <xsl:value-of select="translate(tx_txblocks/ES_TXBLOCK/product/ES_PRODUCT/@p_product_calculatedbroadcasttitle, '.,', '')"/>
    <xsl:text>&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="ESP_DATE" mode="csv">
    <xsl:value-of select="format-number(@day, '00')"/> 
    <xsl:text>/</xsl:text>
    <xsl:value-of select="format-number(@month, '00')"/>
    <xsl:text>/</xsl:text>
    <xsl:value-of select="@year"/>
  </xsl:template>

  <xsl:template match="ESP_TIMEDURATION" mode="csv">
    <xsl:choose>
      <xsl:when test="@tvdayhours">
        <xsl:value-of select="format-number(@tvdayhours, '00')" />
      </xsl:when>
      <xsl:otherwise>00</xsl:otherwise>
    </xsl:choose>
    <xsl:text>:</xsl:text>
    <xsl:value-of select="format-number(@hours, '00')" />
    <xsl:text>:</xsl:text>
    <xsl:value-of select="format-number(@minutes, '00')"/>
  </xsl:template>

</xsl:stylesheet>

which outputs this CSV from your sample data:

12/10/2019,25:01:00,00:00:50,4
12/10/2019,25:01:50,00:00:45,5

Note that it starts with an <xsl:apply-templates> (see how <xsl:apply-templates> works) and then it re-uses the ESP_TIMEDURATION template twice to output the same format for the two duration values.

Note that I keep using <xsl:apply-templates>, I can only recommend to get into this habit.

You can add an <xsl:text>...</xsl:text> with a header row in the / template.


Your attempt fails because it does not contain any iteration:

<xsl:template match="/">
  <xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@day, '00')"/> 
  <!-- ... --->
</xsl:template>
  • The / template matches exactly once, at the root of the input XML.
  • <xsl:value-of select="..." /> outputs the value of the expression.
  • The XPath transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@day selects all matching @day attributes from all transmissions.
  • format-number(...) can only format one item, but in this situation it gets more than one. It takes the first of those items and formats it.
  • This happens to all the other outputs in that template.
  • The / template ends after processing effectively only the data from the first transmission.

Using a separate template and <xsl:apply-templates> solves this situation (plus all XPaths become shorter).

Tomalak
  • 332,285
  • 67
  • 532
  • 628
1

In order to create a .csv, you need a template that will generate a row for every element in the XSLT that represents a record (I'll assume it's ES_TRANSMISSION in your example).

This template can be a stand-alone xsl:template instruction (as shown in the answer given by Tomalak) or it can be contained in an xsl:for each instruction.

every time I try a for-each to get it to report on more than the first item on the list it comes back with NaN.

The important thing to remember is that both xsl:template and xsl:for each establish the context for the instructions and expressions contained in them.

An instruction like:

<xsl:value-of select="format-number(transmissions/ES_TRANSMISSION/tx_date/ESP_DATE/@day, '00')"/> 

that works from the context of:

<xsl:template match="/">

must be changed to:

<xsl:value-of select="format-number(tx_date/ESP_DATE/@day, '00')"/> 

in order to work from the context of:

<xsl:for-each select="transmissions/ES_TRANSMISSION">
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51