Adam Lee
Adam Lee

Reputation: 43

Creating an element based on double condition with XSLT

I'm having a hard time wrapping my head around the syntax I need in order to create an element I want. Brand new to XML/XSLT and not sure if this is the right approach.

I'm trying to parse my XML data file into element-centric such that I can format the data into a readable structure in an access database.

I'm trying to pull a data value from inside an element, based on two conditions.

The data is inside an element named 'Reading' and the data is labeled 'value'. Above the 'Reading' element is the defining element named 'ConsumptionSpec'.

What I'm trying to test is what unit of measurement (UOM) the current 'ConsumptionSpec' is on, and THEN, test another attribute named 'TouBucket' that holds a value of either 'TierA', 'TierB'/C/D or Total. The UOM can hold "kWh, kW, kVAh, or kVA". I'm trying to get the first one laid out as I'm going to repeat this test for making elements for each Tier (A through D) and the Total. (Trying to give as clear of an explanation as I can)

Currently I'm trying to utilize xsl:for-each to choose the ConsumptionSpec right above Reading, and then use xsl:when to test the UOM and TouBucket seperately. After the test I create an element and I'm attempting to pull the value of the current Reading element.

Here is an excerpt from my XML so you can see what values I'm trying to step through during testing.

<MeterReadings Irn="Null" Source="Remote" SourceName="Null" SourceIrn="Null" Initiator="Schedule" Purpose="Null" CollectionTime="2017-04-01 09:00:00" >
    <Meter MeterIrn="Null" MeterName="Null" IsActive="true" SerialNumber="Null" MeterType="A3_ILN" Description="" InstallDate="2017-01-21 05:00:00" RemovalDate="" AccountIdent="Null" AccountName="" SdpIdent="" Location="Null" TimeZoneIndex="Null" Timezone="Null" TimeZoneOffset="300" ObservesDaylightSavings="false" MediaType="900 MHz" />

    <ReadingQualityIndicator Name="Tamper Alert" Value="true" />

    <ConsumptionData >

        <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="Total" MeasurementPeriod="Current" Multiplier="1" />

        <Reading TimeStamp="2017-04-01 03:08:00" Value="902" />

    </ConsumptionData>

    <ConsumptionData >

        <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="TierA" MeasurementPeriod="Current" Multiplier="1" />

        <Reading TimeStamp="2017-04-01 03:08:00" Value="0" />

    </ConsumptionData>

    <ConsumptionData >

        <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="TierB" MeasurementPeriod="Current" Multiplier="1" />

        <Reading TimeStamp="2017-04-01 03:08:00" Value="0" />

    </ConsumptionData>

    <ConsumptionData >

        <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="TierC" MeasurementPeriod="Current" Multiplier="1" />

        <Reading TimeStamp="2017-04-01 03:08:00" Value="902" />

    </ConsumptionData>

    <ConsumptionData >

        <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="TierD" MeasurementPeriod="Current" Multiplier="1" />

        <Reading TimeStamp="2017-04-01 03:08:00" Value="0" />

    </ConsumptionData>

And here is my current XSLT

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

      <!-- BY DEFAULT, elements and text nodes are copied,
           and elements' attributes and contents are transformed as child nodes
           of the output element -->
      <xsl:template match="node()">
        <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
      </xsl:template>

      <!-- By default, attributes are transformed to elements -->
      <xsl:template match="@*">
        <xsl:element name="{name()}">
          <xsl:value-of select="."/>
        </xsl:element>
      </xsl:template>


      <!-- Certain elements have only their contents transformed -->
      <xsl:template match="
            Meter | Status | ConsumptionData |
            Statuses | MaxDemandData | MaxDemandSpec |
            InstrumentationValue | IntervalData | IntervalSpec">
        <!-- no xsl:copy, and attribute children, if any, are ignored -->
        <xsl:apply-templates select="@* | node()"/>
      </xsl:template>


      <!-- 
      Applies an extra element tag to the selected match


      and pulls the value from the MeterReading ancestor it's
      tagged under.
      -->

   <xsl:template match="Reading">

        <xsl:copy>

            <xsl:element name="MeterReadingIRN">
                <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
            </xsl:element>

              <!-- 
              Trying to get into the ConsumptionSpec tag it's related to,
              then test what the unit of measurement is (UOM),
              and then test what 'TouBucket' it is a part of (TierA/B/C/D or Total),
              and THEN create a new element so that I can hold the 'value' that is inside
              the Reading element, so that it will be referenced to that specific UOM.
              -->
            <xsl:for-each select="ancestor::ConsumptionSpec">
                <xsl:choose>
                    <xsl:when test="@UOM='kWh'">
                        <xsl:when test="@TouBucket='Total'">

                            <xsl:element name="kWhTotal">
                                <xsl:value-of select="Reading/@Value"/>
                            </xsl:element>

                        </xsl:when>
                    </xsl:when>
                    <!-- Not sure how I can make my otherwise into a useful element here -->
                    <xsl:otherwise>

                        <xsl:element name="BlankTest">
                            <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
                        </xsl:element>

                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>  
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

  <xsl:template match="Channel">
  <xsl:copy>
  <xsl:element name="MeterReadingIRN">
     <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
   </xsl:element>
   <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
 </xsl:template>

</xsl:stylesheet>

Any and all suggestions are appreciated, been banging my head against this most of Easter weekend! Let me know if there's more info I can provide to make it more understandable.

Upvotes: 1

Views: 1698

Answers (2)

John Bollinger
John Bollinger

Reputation: 181664

There are good uses for XSL's iteration and conditional elements, but they are somewhat uncommon. Whenever you consider using an xsl:for-each, xsl:choose or xsl:if construct, you should pause a moment to think about whether that's really the best way to do the job.

Additionally, in developing a stylesheet it is not necessarily advantageous to try to split things into parallel cases and handle them one at a time. XSL is not a procedural programming language, and it does not lend itself well to that kind of thinking. XSLT tends to be better served by a sort of holistic, top-down approach -- one reminiscent, in fact, of XML itself.

Taking your template for <Reading> elements as an example, it seems like you're preparing to write rules for a bunch of individual cases that all fit a common pattern. Specifically, it seems like you may be trying to dynamically name an element child of the transformed <Reading> elements, based on the UOM and TouBucket of the applicable ConsumptionSpec. This is what <xsl:element> is good for; you don't really need it for output elements whose names are fixed, as a literal output element works fine for that. Thus, you might cover all the cases with a simple template such as this:

<xsl:template match="Reading">
  <xsl:copy>

    <!-- literal output element: -->
    <MeterReadingIRN>
      <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
    </MeterReadingIRN>

    <xsl:element name="{concat(../ConsumptionSpec/@UOM, ../ConsumptionSpec/@TouBucket)}">
      <xsl:value-of select="@Value"/>
    </xsl:element>

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

Even if you need more differences between the transformations for Reading elements than that provides, you should consider structuring your stylesheet so as to use more specific match expressions on your templates, more specific select expressions to select nodes to which to apply templates, and / or different template modes to express and direct the transformations.

Upvotes: 1

Tim C
Tim C

Reputation: 70648

Instead of trying to nest xsl:when statements, which is not allowed, the syntax you want is this...

<xsl:when test="@UOM='kWh' and @TouBucket='Total'">

However, you have a problem even before that point really, with your use of xsl:for-each

<xsl:for-each select="ancestor::ConsumptionSpec">

You are currently matching a Reading element at this point, and so you will be currently positioned on that element. ConsumptionSpec is not an ancestor of the current Reading element though. It is a sibling.

<ConsumptionData >
    <ConsumptionSpec UOM="kWh" Direction="Delivered" TouBucket="TierA" MeasurementPeriod="Current" Multiplier="1" />
    <Reading TimeStamp="2017-04-01 03:08:00" Value="0" />
</ConsumptionData>

It is ConsumptionData that is the parent of both ConsumptionSpec and Reading here.

Also, note if <xsl:for-each select="ancestor::ConsumptionSpec"> did select an element, it would change your position from Reading to ConsumptionSpec.

I am assuming you only have one ConsumptionSpec per Reading? In which case, it might be better to use a variable.

Try this template match instead:

<xsl:template match="Reading">
    <xsl:copy>
        <MeterReadingIRN>
            <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
        </MeterReadingIRN>
         <xsl:variable name="spec" select="../ConsumptionSpec" />
         <xsl:choose>
           <xsl:when test="$spec/@UOM='kWh' and $spec/@TouBucket='Total'">
              <kWhTotal>
                 <xsl:value-of select="@Value"/>
               </kWhTotal>
              </xsl:when>
              <xsl:otherwise>
                <BlankTest>
                  <xsl:value-of select="ancestor::MeterReadings/@Irn"/>
                </BlankTest>
              </xsl:otherwise>
            </xsl:choose>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

Note, there is no real need to use xsl:element to create a new element in this case. Just directly write out the element you want created.

The syntax .. in the XPath means select the parent, so ../ConsumptionSpec will select the ConsumptionSpec that is the child of the same parent as Reading.

Upvotes: 1

Related Questions