Sortering
Beregning
Stringer
XSLT
Børre Stenseth

Sortering, stringer og beregninger

Hva

XSL som språk har ikke typer slik som vi kjenner fra de fleste andre språk. Når vi skal lagre og behandle numeriske data må vi ta utgangspunkt i disse dataene som en sekvens med tegn. Det er to oppgaver som krever litt oppmerksomhet i denne forbindelse: sortering og beregning. Begge deler krever at tegnene tolkes på en fornuftig måte.

Vi bruker et lite datasett som eksempel i denne modulen. Anta følgende resultatfil fra en konkurranse der deltagerne hver har fått notert to tider (timer,minutter,sekunder). Vi skal etterhvert interessere oss for samlet tid, rekkefølge osv.:

<?xml version="1.0" encoding="utf-8"?>
<resultater>
    <deltager>
        <navn>Syversen</navn>
        <t1>1:31:12</t1>
        <t2>1:22:09</t2>
    </deltager>
    <deltager>
        <navn>Andersen</navn>
        <t1>1:30:12</t1>
        <t2>1:12:09</t2>
    </deltager>
</resultater>

Sortering

La oss først se på de grunnleggende sorteringsmulighetene vi har i XSLT. Hvis vi ønsker å lage en HTML-fil som skriver ut deltagerlista alfabetisk, basert på navn, kan vi lage følgende transformasjon:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html"  
     doctype-system="about:legacy-compat" encoding="UTF-8"/>
   
<xsl:template match="/">
   <html>    
   <head><title>resultater</title></head>
   <body><h1>Resultater</h1>
   
   <xsl:apply-templates select="/resultater/deltager">
      <xsl:sort select="navn"/>
   </xsl:apply-templates>
   
   </body></html>
</xsl:template>
<xsl:template match="deltager">
   <h3><xsl:value-of select="navn"/></h3>
   <div>
    <xsl:value-of select="t1"/> og <xsl:value-of select="t1"/>
   </div>
</xsl:template>
 
</xsl:stylesheet>
Resultatet https://borres.hiof.no/wep/xslt/sort/trans1.html

Hva dersom vi introduserer en blank foran innholdet i navne-elementet til den ene deltageren, Syversen ? Hvis vi kjører den samme transformasjonen vil vi få Syversen foran Andersen. Det blanke tegnet inngår altså i sammenligningen av navne-elemener og forstyrrer ordningen. Vi kan korrigere for dette ved å endre sorteringselementet i transformasjonen slik at omgivende whitespace ignoreres:

<xsl:sort select="normalize-space(navn)"/>

Beregning

La oss se på beregning av samlede resultater. Vi ønsker å finne samlet tid for de to øvelsene for alle deltagerne. Vi har valgt et format for tidsangivelse som er ganske ryddig:

<t1>1:30:12</t1>

og oppgaven lar seg løse med å trekke ut delstringer, tolke dem numerisk og utføre nødvendige beregninger. XSLT gis oss verktøy for dette. En transformasjon som presenterer resultatene med angivelse av samlet tid i antall sekunder kan se slik ut:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html"  
     doctype-system="about:legacy-compat" encoding="UTF-8"/>
   
<xsl:template match="/">
   <html>   
   <head>
   <title>resultater</title>
   </head>
   <body><h1>Resultater</h1>
   
   <xsl:apply-templates select="/resultater/deltager"/>
   
   </body>
   </html>
</xsl:template>
<xsl:template match="deltager">
   <h3><xsl:value-of select="navn"/></h3>
   <xsl:variable name="sekundsum" 
    select="3600*(number(substring(t1,1,1))+number(substring(t2,1,1)))  
            +60*(number(substring(t1,3,2))+number(substring(t2,3,2)))  
            +(number(substring(t1,6,2))+number(substring(t2,6,2)))"/>
  <div><xsl:value-of select="$sekundsum"/></div>
</xsl:template>
 
</xsl:stylesheet>

Merk at tegnindekseringen begynner på 1, ikke 0 som vi er vant med i andre språk.

Resultatet https://borres.hiof.no/wep/xslt/sort/trans3.html

Koden som er foreslått er uten sikkerhetsnett. Hva hvis vi har feil i XML-formatet, eller at en deltager har brukt 10 timer? Det er ganske mye pes å skrive rutiner for datakontroll i XSLT. På den annen side har XSLT en nødutgang som ikke skaper katastrofe i form av programkrasj. Hvis vi introduserer feil format i en av tidene til Andersen, vil vi normalt få følgende resultat: trans3feil.html Vi får altså resultatet NaN (Not a Number). Hvis vi gjør en feil i transformasjonsfila og forsøker å regne på ikke-numeriske tegn eller tegn som vi prøver å hente fra en string med gal indeks får vi samme respons.

Dersom vi bare forsøker å framstille en substring som ikke finnes, f.eks. ved å skrive:

<div><xsl:value-of select="substring(t1,100,20)"/></div>

skjer det ingenting. Ingen feil og ingen string.

Vi ønsker å forbedre vår resultatframstilling noe og vil ha ut det samlede resultatet i samme form som delresultatene i XML-fila, dvs. t:mm.ss. Følgende transformasjon tar seg av dette:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html"  
     doctype-system="about:legacy-compat" encoding="UTF-8"/>
   
<xsl:template match="/">
   <html>   <head><title>resultater</title></head>
   <body><h1>Resultater</h1>
   
   <xsl:apply-templates select="/resultater/deltager"/>
   
   </body></html>
</xsl:template>
<xsl:template match="deltager">
<h3><xsl:value-of select="navn"/></h3>
   <xsl:variable name="sekundsum" 
    select=" 3600*(substring(t1,1,1)+substring(t2,1,1))  
            +60*(substring(t1,3,2)+substring(t2,3,2))  
            +(substring(t1,6,2)+substring(t2,6,2))"/>
   
   <xsl:variable name="timer" 
      select ="floor($sekundsum div 3600)"/>
   
   <xsl:variable name="minutter" 
      select="floor(($sekundsum - $timer * 3600) div 60)"/>
   
   <xsl:variable name="sekunder" 
      select="$sekundsum -($timer * 3600) -($minutter*60)"/>
   <div>
      <xsl:value-of 
        select="concat($timer,':',$minutter,':',$sekunder)"/>
   </div>
</xsl:template>
 
</xsl:stylesheet>

Vi har brukt funksjonen floor() som runder ned til nærmeste hele tall og funksjonen concat() som slår sammen stringer.

Resultatet https://borres.hiof.no/wep/xslt/sort/trans4.html

Merk at vi har forenklet uttrykket ved at vi har fjernet kallene på number()- funksjonen. Dette kan vi gjøre siden XSL-er i stand til å tolke talltegn som numeriske verdier i et numerisk uttrykk.

Beregning og sortering

Vi ønsker nå å få sortert resultatlista etter den samlede tiden. Vi må altså lage et sorteringskriterium som er basert på en beregnet verdi. Dette fungerer:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html"  
     doctype-system="about:legacy-compat" encoding="UTF-8"/>
   
<xsl:template match="/">
   <html>   <head><title>resultater</title></head>
   <body><h1>Resultater</h1>
   
   <xsl:apply-templates select="/resultater/deltager">
   <xsl:sort select=
     "3600*(substring(t1,1,1)+substring(t2,1,1))  
      +60*(substring(t1,3,2)+substring(t2,3,2))  
       +(substring(t1,6,2)+substring(t2,6,2))" 
   order="ascending" data-type="number"/>
   </xsl:apply-templates>
   
   </body></html>
</xsl:template>
<xsl:template match="deltager">
   <xsl:variable name="sumsek" select=
     "3600*(substring(t1,1,1)+substring(t2,1,1))  
      +60*(substring(t1,3,2)+substring(t2,3,2))  
      +(substring(t1,6,2)+substring(t2,6,2))"/>   
   <h3><xsl:value-of select="navn"/></h3>
   
   <xsl:variable name="timer" 
      select="floor($sumsek div 3600)"/>
   <xsl:variable name="minutter" 
      select="floor(($sumsek - $timer * 3600) div 60)"/>
   <xsl:variable name="sekunder" 
      select="$sumsek -($timer * 3600) -($minutter*60)"/>
   <div>
      <xsl:value-of 
      select="concat($timer,':',$minutter,':',$sekunder)"/>
   </div>
</xsl:template>
 
</xsl:stylesheet>
Resultatet https://borres.hiof.no/wep/xslt/sort/trans5.html

Det virker ganske klønete å regne ut sekundsummen to ganger, først som sortingskriterium og deretter i forbindelse med presentasjonen. Se om du kan finne på noe bedre og fortell meg om det.