Reputation: 2139
I am looking to transform some XML like below, in this example there are only 2 elements to combine together but it could be more:
<file>
<patient>
<Lab_No_Spec_No>12345</Lab_No_Spec_No>
<Patient_Number>ABC</Patient_Number>
<Albumin_g_L>48 </Albumin_g_L>
<Calcium_mmol_L>
<Phosphate_mmol_L>100 </Phosphate_mmol_L>
</patient>
<patient>
<Lab_No_Spec_No>12345</Lab_No_Spec_No>
<Patient_Number>ABC</Patient_Number>
<Albumin_g_L>92 </Albumin_g_L>
<Calcium_mmol_L>50 </Calcium_mmol_L>
<Phosphate_mmol_L/>
</patient>
</file>
to the following result:
<file>
<patient>
<Lab_No_Spec_No>12345</Lab_No_Spec_No>
<Patient_Number>ABC</Patient_Number>
<Albumin_g_L>48,92</Albumin_g_L>
<Calcium_mmol_L>50</Calcium_mmol_L>
<Phosphate_mmol_L>100</Phosphate_mmol_L>
</patient>
</file>
This is my transformation so far:
<?xml version="1.0" encoding="UTF-8"?>
<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="*"/>
<xsl:key name="specimen" match="patient" use="Lab_No_Spec_No" />
<xsl:template match="file">
<file>
<xsl:for-each select="patient[count(. | key('specimen', Lab_No_Spec_No)[1]) = 1]">
<patient>
<xsl:copy-of select="Lab_No_Spec_No" />
<xsl:copy-of select="Patient_Number" />
<Albumin_g_L>
<xsl:for-each select="key('specimen', Lab_No_Spec_No)">
<xsl:value-of select="normalize-space(Albumin_g_L)" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</Albumin_g_L>
<Calcium_mmol_L>
<xsl:for-each select="key('specimen', Lab_No_Spec_No)">
<xsl:value-of select="normalize-space(Calcium_mmol_L)" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</Calcium_mmol_L>
<Phosphate_mmol_L>
<xsl:for-each select="key('specimen', Lab_No_Spec_No)">
<xsl:value-of select="normalize-space(Phosphate_mmol_L)" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</Phosphate_mmol_L>
</patient>
</xsl:for-each>
</file>
</xsl:template>
</xsl:stylesheet>
There are a few issues with the above that I am looking for help on:
Empty elements are being included during the <xsl:for-each select="key('specimen', Lab_No_Spec_No)">
concatenation which I want to omit so that I don't get results like <Calcium_mmol_L>50,</Calcium_mmol_L>
. What needs changing in my for-each select so empty elements are not selected?
The real source XML file has 30+ elements that I need to do the concatenation for. I've repeated the same transformation for the 3 elements in my example but is there a shorthand way of doing this for elements after the Patient_Number
element or do I have to repeat the transformation? Something along the lines of:
<xsl:for-each select="following-sibling::Patient_Number"> <local-name(.)> <xsl:for-each select="key('specimen', Lab_No_Spec_No)"> <xsl:value-of select="normalize-space(.)" /> <xsl:if test="position() != last()"> <xsl:text>,</xsl:text> </xsl:if> </xsl:for-each> </local-name(.)> </xsl:for-each>
Upvotes: 0
Views: 765
Reputation: 167516
Tim has already provided a good answer but I think such problems benefit from starting with the identity transformation template and then adding only templates for those elements you need to transform. Also as long as in the input each patient
has the same child
elements I think you can simply group as done and then just process the child elements of the first patient
element in each group, then in the template for those elements that need to concatenate data you can use that key suggested to push them through another mode to output the list:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes" version="5"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:key name="specimen" match="patient" use="Lab_No_Spec_No" />
<xsl:key name="child" match="patient/*" use="concat(../Lab_No_Spec_No, '|', local-name())"/>
<xsl:template match="patient[count(. | key('specimen', Lab_No_Spec_No)[1]) = 1]">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="patient[not(count(. | key('specimen', Lab_No_Spec_No)[1]) = 1)]"/>
<xsl:template match="patient/*[not(self::Lab_No_Spec_No|self::Patient_Number)]">
<xsl:copy>
<xsl:apply-templates select="key('child', concat(../Lab_No_Spec_No, '|', local-name()))[normalize-space()]" mode="value"/>
</xsl:copy>
</xsl:template>
<xsl:template match="patient/*" mode="value">
<xsl:if test="position() > 1">,</xsl:if>
<xsl:value-of select="normalize-space()"/>
</xsl:template>
</xsl:stylesheet>
That doesn't free you from the problem you have raised in a comment about having to somewhere list all those elements you don't want to concatenate but you only have one match
attribute predicate where you need to do that.
https://xsltfiddle.liberty-development.net/gWmuiJX/2
Upvotes: 1
Reputation: 70618
To exclude empty elements, just add a condition in your xsl:for-each
<xsl:for-each select="key('specimen', Lab_No_Spec_No)[normalize-space(Albumin_g_L)]">
As for avoiding repetition, you can get all clever, and have a second level of grouping of the column names for each Lab_No_Spec_No.
Try this 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="*"/>
<xsl:key name="specimen" match="patient" use="Lab_No_Spec_No" />
<xsl:key name="cols" match="patient/*" use="concat(../Lab_No_Spec_No, '|', local-name())" />
<xsl:template match="file">
<file>
<xsl:for-each select="patient[count(. | key('specimen', Lab_No_Spec_No)[1]) = 1]">
<patient>
<xsl:copy-of select="Lab_No_Spec_No" />
<xsl:copy-of select="Patient_Number" />
<xsl:apply-templates select="*[not(self::Lab_No_Spec_No) and not(self::Patient_Number)]
[count(. | key('cols', concat(../Lab_No_Spec_No, '|', local-name()))[1]) = 1]" />
</patient>
</xsl:for-each>
</file>
</xsl:template>
<xsl:template match="patient/*">
<xsl:copy>
<xsl:for-each select="key('cols', concat(../Lab_No_Spec_No, '|', local-name()))[normalize-space()]">
<xsl:value-of select="normalize-space(.)" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
All this is much easier in XSLT 2.0 though....
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="file">
<file>
<xsl:for-each-group select="patient" group-by="Lab_No_Spec_No">
<patient>
<xsl:copy-of select="Lab_No_Spec_No,Patient_Number" />
<xsl:for-each-group select="current-group()/(* except (Lab_No_Spec_No, Patient_Number))" group-by="local-name()">
<xsl:copy>
<xsl:value-of select="current-group()[normalize-space()]/normalize-space()" separator="," />
</xsl:copy>
</xsl:for-each-group>
</patient>
</xsl:for-each-group>
</file>
</xsl:template>
</xsl:stylesheet>
Upvotes: 1