Reputation: 43
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
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
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