Archiv für März, 2009

Die einfachste XSL-Transformation

Meine ersten Gehversuche mit XSLT hatten das Ziel, kleine Änderungen an XML-Dokumenten vorzunehmen: einzelne Elemente löschen, Werte neu berechnen und ähnliches. Die Lösung dafür ist die sogenannte »Identity Transformation«:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Dieses Stylesheet kopiert das Eingabedokument vollständig in das Ausgabedokument. Das einzige Template matcht alle Attribute (@*) und alle Knoten (node(), das sind Elemente, Text, Kommentare und sogenannte Processing Instructions).

Wie kann man nun Änderungen am Dokument vornehmen? Ganz einfach: Es werden zusätzliche Templates eingefügt, die nur die zu ändernden Knoten beeinflussen. Sollen zum Beispiel aus einem XHTML-Dokument alle <code/>-Elemente gelöscht werden, hilft folgendes Stylesheet:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="xhtml:code"/>
	<!--  -->
</xsl:stylesheet>

Leider wird jetzt der enthaltene Text ebenfalls gelöscht. Wenn die Kind-Knoten (z.B. text()-Knoten) erhalten werden sollen, muss für diese die allgemeine Kopierregel angewendet werden, am einfachsten durch ein <xsl:apply-templates/>:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="xhtml:code">
		<xsl:apply-templates/>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Voilà! Alle <code/>-Tags sind entfernt, der Inhalt ist noch da, und alles ohne Suchen&Ersetzen mit regulären Ausdrücken.

Letztes Beispiel: die <code/>-Tags sollen durch <span class="code"/>-Tags ersetzt werden. Dazu werden hier die neuen Elemente einfach als sogenannte literale Elemente (englisch Literal Result Elements) in das Template geschrieben:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns="http://www.w3.org/1999/xhtml"
	xpath-default-namespace="http://www.w3.org/1999/xhtml"
	exclude-result-prefixes="#default"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="code">
		<span class="code"><xsl:apply-templates/></span>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Man beachte die Verwendung von xpath-default-namespace und exclude-result-prefixes. Dies sorgt dafür, dass das Namespace-Präfix nicht mehr explizit z.B. in das match-Attribut geschrieben werden muss und keine unnötigen Namespace-Angaben in die Ausgabedatei geschrieben werden.

Das letzte Stylesheet gibt es zum Herunterladen, ein passendes Transformations-Szenario für OxygenXML in der Projekt-Datei example.xpr

Tipp: Ich habe die Identity Transformation als Dokumentenvorlage in OxygenXML hinterlegt, weil ich sie sehr oft brauche. Dann sind Mini-Stylesheets sehr schnell geschrieben.

Nachtrag: Die oben angebotene Identity-Transformation hat einige Nebeneffekte: Standard-Attribute aus der DTD werden ergänzt, Zeilenumbrüche werden nicht genau rekonstruiert, Numeric Character References werden in Zeichen umgewandelt. Eine kompliziertere Alternative wird unter http://www.xmlplease.com/identity-template (englisch) vorgestellt.

Keine Kommentare

Römische Zahlen in Integer konvertieren

Für ein Projekt stand ich vor der Aufgabe, a) einen String darauf zu testen, ob er eine römische Zahl ist und b) und diesen String dann in einen Integer zu konvertieren. Ich kannte mich mit römischen Zahlen nicht wirklich aus, also erst einmal in der Wikipedia nachschlagen. Dabei lernte ich gleich, dass „römische Zahlen“ auf englisch „roman numerals“ heißen – gut, dass konnte ich in der anschließenden Google-Suche gebrauchen.

Im ersten Anlauf fand ich in Sal Manganos XSLT Cookbook eine angestaubte XSLT 1.0-Lösung, die mir überhaupt nicht gefiel. Der Test auf römische Zahlen ist ein Test auf gültige Zeichen, und anschließend wird mangels Funktionen eine Menge mit rekursiven Templates gemacht. Mit XSLT 2.0 muss das doch eleganter gehen. Die freche Lösung – allerdings ohne Gültigkeitstest – liefert Mukul Gandhi in der segensreichen xsl-Mailing-Liste ab. Er zählt solange von 1 bis 10000, bis das Ergebnis von <xsl:number/> gleich dem Input-String ist:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xs="http://www.w3.org/2001/XMLSchema"
	xmlns:num="http://whatever">
	<!--  -->
	<xsl:output method="text"/>
	<!--  -->
	<xsl:variable name="max" select="10000"/>
	<!--  -->
	<xsl:template match="/">
		<xsl:call-template name="RomanToInteger">
			<xsl:with-param name="roman_number" select="'IIX'"/>
		</xsl:call-template>
	</xsl:template>
	<!--  -->
	<xsl:template name="RomanToInteger">
		<xsl:param name="roman_number"/>
		<xsl:for-each select="1 to $max">
			<xsl:if test="num:toRoman(.) = $roman_number">
				<xsl:value-of select="."/>
			</xsl:if>
		</xsl:for-each>
	</xsl:template>
	<!--  -->
	<xsl:function name="num:toRoman" as="xs:string">
		<xsl:param name="value" as="xs:integer"/>
		<xsl:number value="$value" format="I"/>
	</xsl:function>
	<!--  -->
</xsl:stylesheet>

Nachteil 1: Bei großen Zahlen kann dieser Algorithmus nicht effektiv sein. Nachteil 2: Die Beschränkung auf 10000 ist ein Schutz vor ungültigen Eingaben und der daraus resultierenden Endlosschleife, größere Zahlen sind aber vorstellbar. Also selbst entwickeln.

a) einen String darauf testen, ob er eine gültige römische Zahl ist

Der von Mangano angebotene Test auf die gültigen Zeichen „I“, „V“, „X“, „L“, „C“, „D“, „M“, „i“, „v“, „x“, „l“, „c“, „d“, „m“ ist nicht ausreichend, weil damit auch ungültige Kombinationen wie „IIX“ möglich sind (die unter anderem Mukul Gandhis Algorithmus bis zum Abbruch laufen lassen). Besser ist ein regulärer Ausdruck, dank Google gefunden bei regexlib.com:

<xsl:function name="misc:IsRomanNumeral" as="xs:boolean">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="normalize-space(upper-case($Input))"/>
	<xsl:value-of select="not(matches($temp,'(([IXCM])2{3,})|[^IVXLCDM]|([IL][LCDM])|([XD][DM])|(V[VXLCDM])|(IX[VXLC])|(VI[VX])|(XC[LCDM])|(LX[LC])|((CM|DC)[DM])|(I[VX]I)|(X[CL]X)|(C[DM]C)|(I{2,}[VX])|(X{2,}[CL])|(C{2,}[DM])'))"/>
</xsl:function>

In der Variablen $temp wird der eingegebene String vorbereitet. Da der reguläre Ausdruck auf ungültige Kombinationen testet, war noch ein zusätzliches not() notwendig.

b) einen String mit einer römischen Zahl in einen Integer umwandeln

In der Wikipedia ist der Algorithmus beschrieben: Die Buchstaben werden durch ihre Integer-Werte ersetzt (dafür nehme ich eine Hilfsfunktion) und dann aufaddiert. Eine Ausnahme ist die Subtraktionsschreibweise. Vereinfacht: Steht genau eine kleinere Ziffer vor einer größeren, wird deren Wert von der Summe abgezogen. Ich setze das geradlinig um:

<xsl:function name="misc:GetIntegerFromRomanNumeral" as="xs:integer">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="normalize-space(upper-case($Input))"/>
	<xsl:choose>
		<xsl:when test="misc:IsRomanNumeral($temp)">
			<xsl:variable name="Values" as="xs:integer*">
				<xsl:for-each select="for $i in 1 to string-length($temp) return $i">
					<xsl:variable name="CharValue" as="xs:integer"
						select="misc:GetIntegerFromRomanNumberChar(substring($temp, position(), 1))"/>
					<xsl:variable name="NextCharValue" as="xs:integer"
						select="misc:GetIntegerFromRomanNumberChar(substring($temp, position() + 1, 1) )"/>
					<xsl:choose>
						<xsl:when test="$CharValue lt $NextCharValue">
							<xsl:value-of select="- $CharValue"/>
						</xsl:when>
						<xsl:otherwise>
							<xsl:value-of select="$CharValue"/>
						</xsl:otherwise>
					</xsl:choose>
				</xsl:for-each>
			</xsl:variable>
			<xsl:value-of select="sum($Values)"/>
		</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:function>
<!--  -->
<xsl:function name="misc:GetIntegerFromRomanNumberChar" as="xs:integer">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="upper-case(normalize-space($Input))"/>
	<xsl:choose>
		<xsl:when test="$temp = 'I' ">1</xsl:when>
		<xsl:when test="$temp = 'V' ">5</xsl:when>
		<xsl:when test="$temp = 'X' ">10</xsl:when>
		<xsl:when test="$temp = 'L' ">50</xsl:when>
		<xsl:when test="$temp = 'C' ">100</xsl:when>
		<xsl:when test="$temp = 'D' ">500</xsl:when>
		<xsl:when test="$temp = 'M' ">1000</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:function>

In der Variablen $Values wird für jedes Zeichen des Strings ein Wert erzeugt, am Ende enthält die Variable eine Sequenz von Werten. Diese Sequenz wird mit sum() einfach aufaddiert. Ein Test, ob bei $NextCharValue das nächste Zeichen hinter dem letzen Zeichen liegt, ist hier übrigens nicht notwendig, weil in diesem Fall substring() einen Leerstring zurückgibt, dessen Wert in misc:GetIntegerFromRomanNumberChar() als 0 definiert ist.

Nachtrag: Diese Funktionen sind nun in der Beispielsammlung abgelegt.

Keine Kommentare