Toolbox
Toolbox

Reputation: 2493

Return only first found value from for-each

I need to find the "value/pair" that is equal to xbrl:concept": "se:Bank". XSLT finds 2 "value/pairs". Since the data extraction is for a single header I need only 1 heading answer. In below code I am trying to restrict the xsl:for-each by using [position() = 1] but still get the return of the 2 index positions.

This example is minimized to fit a proper question. In the real scenario I am producing about 100-130 headings.

Can this be solved with an for-each or should I move to another XSLT element?

The code in xsltfiddle

Below you find same code:

JSON

<data>
{
  "report": {
    "facts": [
      {
        "xbrl:concept": "se:Bank",
        "numericValue": 1000,
        "heading": "Bank balance"
      },
      {
        "xbrl:concept": "se:CompanyName",
        "value": "Great Company Ltd"
      },
      {
        "xbrl:concept": "se:Bank",
        "numericValue": 3000,
        "heading": "Bank balance"
      }
    ]
  }
}
</data>

XSL:

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">

  
  <xsl:output method="xhtml" indent="yes" html-version="5"/>
  <xsl:mode on-no-match="shallow-skip"/>

    <!-- Parse JSON to XML -->
    
    <xsl:template match="data">
        <xsl:apply-templates select="json-to-xml(.)/*"/>
    </xsl:template>
    
    <!-- Printout heading -->
    
    <xsl:template match="//*[@key='facts']">
        
    <xsl:for-each select="//*[@key='xbrl:concept'][. = 'se:Bank'][position() = 1]">
          <xsl:value-of select="../*[@key='heading']"/>
        </xsl:for-each>
        
    </xsl:template>
  
</xsl:stylesheet>

Result

Bank balanceBank balance

Wanted result

Bank balance

Upvotes: 0

Views: 428

Answers (3)

Michael Kay
Michael Kay

Reputation: 163458

The construct //*[1] often causes problems: it parses as /descendant-or-self::node()/child::*[1] which selects every descendant that is the first child of its parent. What people usually want here is (//*)[1] which selects the first descendant in the entire document.

The problem is obscured here because it appears in the form of the complex expression //*[@key='xbrl:concept'][. = 'se:Bank'][position() = 1] but the issue is the same; what you want is (//*[@key='xbrl:concept'][. = 'se:Bank'])[position() = 1], or more concisely (//*[@key='xbrl:concept'][. = 'se:Bank'])[1].

Upvotes: 3

Caleth
Caleth

Reputation: 63019

I'd put the collection of nodes into a variable, then select the first of them

<xsl:template match="//*[@key='facts']">
    <xsl:variable name="headings"  select="//*[@key='xbrl:concept'][. = 'se:Bank']/../*[@key='heading']"/>
    <xsl:value-of select="$headings[1]"/>
</xsl:template>

Fiddle

Or even simpler, pass those nodes to head

<xsl:template match="//*[@key='facts']">
    <xsl:value-of select="head(//*[@key='xbrl:concept'][. = 'se:Bank']/../*[@key='heading'])"/>
</xsl:template>

Fiddle

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167706

Don't use for-each, simply select the first element of your sequence with e.g. the head function:

<xsl:template match="//*[@key='facts']">
    
  <xsl:value-of select="head(fn:map[fn:string[@key = 'xbrl:concept'][. = 'se:Bank']]/fn:string[@key = 'heading'])"/>

Declare xmlns:fn="http://www.w3.org/2005/xpath-functions", I don't think it makes sense to ignore the names of elements completely and simply select based on the key attribute.

Upvotes: 2

Related Questions