codesforcoffee
codesforcoffee

Reputation: 179

With XSLT, how can I use this if-test with an array, when search element is returned by a template call inside the for loop?

I think this simple example might ask the question a lot more clearly.

I have an input file with multiple products. There are different types of product (2 types with 2 product IDs is fine enough for this example), but the input will have many more.

I only want to output the info for the first product of each type that I encounter in the for each Product loop. (I'm outputting info for the lowest priced product, so the first one will be the lowest price because I sort by Rate first.)

So I want to read in each product, but only output the product's info if I haven't already output a product with that same ID. I have a variable for the IDArray, and I want to check if each product inside the for each product loop has an ID that is already in that IDArray - if not, continue, if it is already in the array, skip everything and just loop to the next.

I couldn't figure out how to get the child element to be a node from IDArray with the value of each CurrentID. It keeps adding that value as a node to CurrentID which is only in the scope of each product, not the whole product group. I know the following code does not work, but it illustrates the idea and gives me a place to start:

<?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" encoding="UTF-8" indent="yes" cdata-section-elements="prod_name adv_notes"/>
  <xsl:template match="/">
  <List>
     <xsl:for-each select="ProductGroup">
     <xsl:sort select="Product/Rate"/>
     <xsl:variable name="IDarray">
     <xsl:for-each select="Product">
        <xsl:variable name="CurrentID">
           <xsl:call-template name="processID">
              <xsl:with-param name="ProductCode" select="ProductCode" />
            </xsl:call-template>
        </xsl:variable>
        <xsl:if test="not(contains($IDarray, $CurrentID))">
           <child elem="{@elem}">
             <xsl:select value-of="$CurrentID" />
           </child>
          <Product>
           <xsl:attribute name="ID">
             <xsl:select value-of="$CurrentID" />
           </xsl:attribute>
        <prod_name>
           <xsl:value-of select="ProductName"/>
        </prod_name>
        <rate>
           <xsl:value-of select="Rate"/>
        </rate>
    </Product>
    </xsl:if>
    </xsl:for-each>
    </xsl:variable>
    </xsl:for-each>
  </List>
</xsl:template>


<xsl:template name="processID">
 <xsl:param name="ProductCode"/>
 <xsl:choose>
   <xsl:when test="starts-with($ProductCode, '515')">5</xsl:when>
   <xsl:when test="starts-with($ProductCode, '205')">2</xsl:when>
 </xsl:choose>
</xsl:template>

Thanks so much in advance, I know some of the awesome programmers here can help! :) -Holly

An input would look like this:

 <ProductGroup>
  <Product>
    <ProductCode>
       5155
    </ProductCode>
    <ProductName>
       House
    </ProductName>
    <Rate>
       3.99
    </Rate>
    </Product>
    <Product>
    <ProductCode>
       5158
    </ProductCode>
    <ProductName>
       House
    </ProductName>
    <Rate>
       4.99
    </Rate>
    </Product>
   </ProductGroup>
        <ProductGroup>
  <Product>
    <ProductCode>
       2058
    </ProductCode>
    <ProductName>
       House
    </ProductName>
    <Rate>
       2.99
    </Rate>
    </Product>
    <Product>
    <ProductCode>
       2055
    </ProductCode>
    <ProductName>
       House
    </ProductName>
    <Rate>
       7.99
    </Rate>
    </Product>
   </ProductGroup>

Output would be this for only that simple input file:

enter image description here

Implementing a Muenchian grouping is tough here because my actual dataset is huge. I made it simple to ask a question. If I can just get that array method working, the rest of my huge project will work.

Upvotes: 1

Views: 3362

Answers (1)

Serge Belov
Serge Belov

Reputation: 5803

how can I use this if-test with an array, when search element is returned by a template call inside the for loop?

UPDATE: You're using XSLT 1.0 so your only option to create an array is to use extensions and that might or might not be possible depending on your environment (see this question for more information). My guess this would be complicated and not very efficient in comparison with keys anyway.

A better question is

I have an input file with multiple products. There are 10 types of product (10 product IDs), but the input will have 200 products, and I only want to output the info for the first product of each type.

The following is an example of how to achieve that. I'd rather use your input and desired output but unfortunately they were not provided.

Input

<?xml version="1.0" encoding="utf-8"?>
<ProductGroup>
  <Product ID="Product 1" Type="A"/>
  <Product ID="Product 2" Type="B"/>
  <Product ID="Product 3" Type="C"/>
  <Product ID="Product 4" Type="A"/>
  <Product ID="Product 5" Type="B"/>
  <Product ID="Product 6" Type="C"/>
  <Product ID="Product 7" Type="A"/>
  <Product ID="Product 8" Type="B"/>
  <Product ID="Product 9" Type="C"/>
</ProductGroup>

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:key name="Product-by-Type" match="Product" use="@Type" />
  <xsl:template match="/ProductGroup">
    <Products>
      <xsl:for-each select="Product[count(. | key('Product-by-Type', @Type)[1]) = 1]">
        <xsl:sort select="@Type" />
        <xsl:copy-of select="key('Product-by-Type', @Type)[1]" />
      </xsl:for-each>
    </Products>
  </xsl:template>
</xsl:stylesheet>

Output

<?xml version="1.0" encoding="utf-8"?>
<Products>
  <Product ID="Product 1" Type="A" />
  <Product ID="Product 2" Type="B" />
  <Product ID="Product 3" Type="C" />
</Products>

This post provides a very good explanation of how Muenchian grouping works.

UPDATE

Your help with more information is really appreciated. Here's how I'd get from your input to your output:

<?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:key name="Product-by-ProductCode" match="Product" use="concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3))" />
  <xsl:template match="/List">
    <List>
      <xsl:for-each select="ProductGroup/ActiveProducts/Product[count(. | key('Product-by-ProductCode', concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3)))[1]) = 1]">
        <xsl:sort select="Rate" />
        <xsl:element name="Product">
          <xsl:attribute name="ID">
            <xsl:choose>
              <xsl:when test="concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3)) = 'Purchase515'">5</xsl:when>
              <xsl:when test="concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3)) = 'Rent205'">2</xsl:when>
              <xsl:otherwise>0</xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
          <xsl:element name="prod_name">
            <xsl:value-of select="key('Product-by-ProductCode', concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3)))[1]/ProductName"/>
          </xsl:element>
          <xsl:element name="rate">
            <xsl:value-of select="key('Product-by-ProductCode', concat(normalize-space(../../preceding-sibling::Purpose[1]), substring(normalize-space(ProductCode), 1, 3)))[1]/Rate"/>
          </xsl:element>
        </xsl:element>
      </xsl:for-each>
    </List>
  </xsl:template>
</xsl:stylesheet>

The approach is the same as in my earlier example, except the key is a concatenation of Purpose and ProductCode. There might be an issue with your input, I thing <Purpose>Rent</Purpose> is missing before the second product group.

Upvotes: 2

Related Questions