RE: [xsl] Getting a NodeList from a NodeList II (Full Problem Shown with Code)

Subject: RE: [xsl] Getting a NodeList from a NodeList II (Full Problem Shown with Code)
From: "Allistair Crossley" <Allistair.Crossley@xxxxxxx>
Date: Wed, 2 Jul 2003 11:14:20 +0100
Thanks loads for looking through that for me Jeni. I will look at using the keys and XSL way of filtering department as you suggest.

The one point I would make about using <= or &lt;= is that it does not matter which one I use, both throw errors. The only way I got it to work was with using > and then notting it!!

Cheers!

-----Original Message-----
From: Jeni Tennison [mailto:jeni@xxxxxxxxxxxxxxxx]
Sent: 02 July 2003 11:05
To: Allistair Crossley
Cc: xsl-list@xxxxxxxxxxxxxxxxxxxxxx
Subject: Re: [xsl] Getting a NodeList from a NodeList II (Full Problem
Shown with Code)


Hi Allistair,

> The report I am trying to generate is based on department, so I loop
> over unique departments and for each one I get the employees in it
> and print some information. I use a msxml function getDepartments
> which is my way of getting a different set of departments depending
> on the filterDepartment node value (which was a Query String
> parameter).
[snip]
> <xsl:for-each select="comp:getDepartments(/)">
>   <xsl:sort select="." order="ascending" />
>   <xsl:variable name="deptName" select="." />
>   <xsl:for-each select="//employee[./@department = $deptName]">
>     <xsl:sort select="@fullname" order="ascending" />
>     <xsl:value-of select="./@fullname" />
>     .. other code
>   </xsl:for-each>
> </xsl:for-each>
[snip]
> <msxsl:script language="JScript" implements-prefix="comp">
>   function getDepartments(nodeList) {
>     var dept = (nodeList.item(0).selectSingleNode("//filterDepartment")).getAttribute("value");
>     if(dept == "All Staff") {
>       return nodeList.item(0).selectNodes("//employee/@department[not(. = preceding::employee/@department)]");
>     } else if(dept == "Some Staff") {
>       return nodeList.item(0).selectNodes("//employee/@department[not(. = preceding::employee/@department) and (" +
>           "(. = 'Sales') " +
>           "or (. = 'I.T'))]");
>     }
>     return nodeList.item(0).selectNodes("//employee/@department[not(. = preceding::employee/@department) and (. = '" + dept + "')]");
>   }
> </msxml:script>

I can see why you're having trouble here, but you can do this in XSLT.
Obviously you can select the department that you're filtering by
easily enough:

  <xsl:variable name="dept"
                select="/employees/filterDepartment/@value" />

And you can create a selection of the first employee in each
department, used as a basis for the future filtering, in the way you
are (or you could use keys to get this set, but one thing at a time):

  <xsl:variable name="employees"
    select="/employees/employee
              [not(@department =
                   preceding-sibling::employee/@department)]" />

Now comes the part where you need to filter $employees based on $dept.
You can do this with a predicate, with a test that returns true if you
want a particular employee and false otherwise. If $dept is 'All
Staff', you don't need any additional filtering, you want the
employee:

  $employees[$dept = 'All Staff' ...]

If $dept is 'Some Staff' then you want the employee if their
@department is 'Sales' or 'I.T':

  $employees[$dept = 'All Staff' or
             ($dept = 'Some Staff' and
              (@department = 'Sales' or @department = 'I.T')) ...]

Otherwise, you want the employee if their department is the same as
$dept:

  $employees[$dept = 'All Staff' or
             ($dept = 'Some Staff' and
              (@department = 'Sales' or @department = 'I.T')) or
             @department = $dept]

(This is very fiddly, I know. In XPath 2.0, there are conditional
expressions, so you can do:

  if ($dept = 'All Staff') then
    $employees
  else if ($dept = 'Some Staff') then
    $employees[@department = 'Sales' or @department = 'I.T']
  else
    $employees[@department = $dept]

which is a lot more intuitive.)

So you can use:

  <xsl:variable name="dept"
                select="/employees/filterDepartment/@value" />
  <xsl:variable name="employees"
    select="/employees/employee
              [not(@department =
                   preceding-sibling::employee/@department)]" />
  <xsl:for-each
    select="$employees[$dept = 'All Staff' or
                       ($dept = 'Some Staff' and
                        (@department = 'Sales' or
                         @department = 'I.T')) or
                       @department = $dept]">
    ...
  </xsl:for-each>

instead of your current MSXML-specific function.

By the way, the above is selecting <employee> elements rather than
department attributes, so you need to change the code inside the
<xsl:for-each> a little. I'd really recommend using keys for selecting
the <employee> elements that belong to the same department as the
employee you're processing. Use a key that indexes all the <employee>
elements by their department:

<xsl:key name="employees" match="employee" use="@department" />

and then select the employees using the key() function:

  key('employees', $deptName)

In other words, you <xsl:for-each> should look like:

  <xsl:for-each
    select="$employees[$dept = 'All Staff' or
                       ($dept = 'Some Staff' and
                        (@department = 'Sales' or
                         @department = 'I.T')) or
                       @department = $dept]">
    <xsl:sort select="@department" order="ascending" />
    <xsl:variable name="deptName" select="@department" />
    <xsl:for-each select="key('employees', $deptName)">
      <xsl:sort select="@fullname" order="ascending" />
      <xsl:value-of select="@fullname" />
      ...
    </xsl:for-each>
  </xsl:for-each>

> I could not think of another way to make my XSL "aware" of Query
> String parameters other than to add them as top level "filter" nodes
> with value attributes and then use a function like this.

That's fine, and it works, but you might also consider using XSLT
parameters to pass in these kinds of filters.

> Well, what I want to do is filter the employees brought back
> depending on some time filters I have which define a date range to
> search within.
[snip]
> I thought about it differently after posting the message and decided
> to put the whole condition that I would have done in the function
> into the select so that I selected all emps in the current dept.
> that had 1 or more approvals whose timestamp falls between the 2
> filters.
>
> <xsl:for-each select="//employee[(./@department = $deptName) and
> (./approvals/approval [(@timestamp >= $filterFDate) and not
> (@timestamp > $filterTDate)])]">
>     <xsl:sort select="@fullname" order="ascending" />
>     <xsl:value-of select="./@fullname" />
>   </xsl:for-each>
>
> This seems to work fine in actual fact...but, as this is my first
> outing with XSL I would really appreciate some constructive
> criticism about my problem and how I have attempted to solve it and
> if I could have done things better.

That looks great. It's how I would have done it. Note that with the
key suggestion above, it should look like:

  <xsl:for-each
    select="key('employees', $deptName)
              [approvals/approval[@timestamp >= $filterFDate and
                                  @timestamp &lt;= $filterTDate]]">
    ...
  </xsl:for-each>

I've used <= rather than not(... > ...) in the above just because it
makes more sense to me.

> And shortly, I shall be having to change the report so that you can
> order by not only department but employee name too. At the moment
> the only way I can think of doing that is to create a whole separate
> XSL sheet for it??

There's rarely a requirement to use a whole separate stylesheet. At
the very least you can do something along the lines of:

  <xsl:choose>
    <xsl:when test="$orderBy = 'department'">
      ... code that does ordering by department ...
    </xsl:when>
    <xsl:when test="$orderBy = 'name'">
      ... code that does ordering by name ...
    </xsl:when>
  </xsl:choose>

Let us know if you have any problems/questions about the above. If you
want to know about how to use keys to select the employees with unique
departments, have a look at
http://www.jenitennison.com/xslt/grouping/muenchian.html.
  
Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/



<FONT SIZE=1 FACE="VERDANA,ARIAL" COLOR=BLUE> 
-------------------------------------------------------
QAS Ltd.
Developers of QuickAddress Software
<a href="http://www.qas.com";>www.qas.com</a>
Registered in England: No 2582055
Registered in Australia: No 082 851 474
-------------------------------------------------------
</FONT>


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread