More progress with XPath:

src/xpath.pp:

+ Implemented sum() and normalize-space() core functions.
* Rewrote comparison code, nodesets are now handled correctly (to the
  extent of the tests which I could find).
* starts-with() and contains() return True when second argument is an
  empty string - that differs from Pos() behavior.
* NaNs are propagated through mod operator, floor() and ceiling().
* Fixed memory leak caused by not releasing arguments after function
  calls.
* string-value of a nodeset is the value of its first node, not all
  nodes glued together.

tests/xpathts.pp:

+ Added 120 tests, most coming from OASIS Prototype XSLT/XPath 1.0
  conformance test suite.
+ Tests can now take an input xml data as a string.

git-svn-id: trunk@13046 -
This commit is contained in:
sergei 2009-04-26 16:06:15 +00:00
parent 58f4f7829c
commit b02aededf8
2 changed files with 482 additions and 135 deletions

View File

@ -483,7 +483,7 @@ function EvaluateXPathExpression(const AExpressionString: DOMString;
implementation
uses Math;
uses Math, xmlutils;
{ Helper functions }
@ -635,6 +635,8 @@ begin
for i := 0 to FArgs.Count - 1 do
Args.Add(TXPathExprNode(FArgs[i]).Evaluate(AContext, AEnvironment));
Result := Fn(AContext, Args);
for i := 0 to FArgs.Count - 1 do
TXPathVariable(Args[i]).Release;
finally
Args.Free;
end;
@ -703,7 +705,9 @@ begin
NumberResult := Op1 * Op2;
opDivide:
NumberResult := Op1 / Op2;
opMod:
opMod: if IsNan(Op1) or IsNan(Op2) then
NumberResult := NaN
else
NumberResult := Trunc(Op1) mod Trunc(Op2);
end;
finally
@ -715,6 +719,92 @@ begin
Result := TXPathNumberVariable.Create(NumberResult);
end;
const
reverse: array[TXPathCompareOp] of TXPathCompareOp = (
opEqual, opNotEqual,
opGreaterEqual, //opLess
opGreater, //opLessEqual
opLessEqual, //opGreater
opLess //opGreaterEqual
);
function CmpNumbers(const n1, n2: Extended; op: TXPathCompareOp): Boolean;
begin
result := (op = opNotEqual);
if IsNan(n1) or IsNan(n2) then
Exit; // NaNs are not equal
case op of
// TODO: should CompareValue() be used here?
opLess: result := n1 < n2;
opLessEqual: result := n1 <= n2;
opGreater: result := n1 > n2;
opGreaterEqual: result := n1 >= n2;
else
if IsInfinite(n1) or IsInfinite(n2) then
result := n1 = n2
else
result := SameValue(n1, n2);
result := result xor (op = opNotEqual);
end;
end;
function CmpStrings(const s1, s2: DOMString; op: TXPathCompareOp): Boolean;
begin
case op of
opEqual: result := s1 = s2;
opNotEqual: result := s1 <> s2;
else
result := CmpNumbers(StrToNumber(s1), StrToNumber(s2), op);
end;
end;
function CmpNodesetWithString(ns: TNodeSet; const s: DOMString; op: TXPathCompareOp): Boolean;
var
i: Integer;
begin
Result := True;
for i := 0 to ns.Count - 1 do
begin
if CmpStrings(NodeToText(TDOMNode(ns[i])), s, op) then
exit;
end;
Result := False;
end;
function CmpNodesetWithNumber(ns: TNodeSet; const n: Extended; op: TXPathCompareOp): Boolean;
var
i: Integer;
begin
Result := True;
for i := 0 to ns.Count - 1 do
begin
if CmpNumbers(StrToNumber(NodeToText(TDOMNode(ns[i]))), n, op) then
exit;
end;
Result := False;
end;
function CmpNodesetWithBoolean(ns: TNodeSet; b: Boolean; op: TXPathCompareOp): Boolean;
begin
// TODO: handles only equality
result := ((ns.Count <> 0) = b) xor (op = opNotEqual);
end;
function CmpNodesets(ns1, ns2: TNodeSet; op: TXPathCompareOp): Boolean;
var
i, j: Integer;
s: DOMString;
begin
Result := True;
for i := 0 to ns1.Count - 1 do
begin
s := NodeToText(TDOMNode(ns1[i]));
for j := 0 to ns2.Count - 1 do
if CmpStrings(s, NodeToText(TDOMNode(ns2[j])), op) then
exit;
end;
Result := False;
end;
constructor TXPathCompareNode.Create(AOperator: TXPathCompareOp;
AOperand1, AOperand2: TXPathExprNode);
@ -729,102 +819,55 @@ function TXPathCompareNode.Evaluate(AContext: TXPathContext;
AEnvironment: TXPathEnvironment): TXPathVariable;
var
Op1, Op2: TXPathVariable;
e1, e2: Extended;
BoolResult: Boolean;
function EvalEqual: Boolean;
var
i, j: Integer;
NodeSet1, NodeSet2: TNodeSet;
s: DOMString;
e1, e2: Extended;
begin
// !!!: Doesn't handle nodesets yet!
if Op1.InheritsFrom(TXPathNodeSetVariable) then
begin
NodeSet1 := Op1.AsNodeSet;
if Op2.InheritsFrom(TXPathNodeSetVariable) then
begin
NodeSet2 := Op2.AsNodeSet;
for i := 0 to NodeSet1.Count - 1 do
begin
s := NodeToText(TDOMNode(NodeSet1[i]));
for j := 0 to NodeSet2.Count - 1 do
if s = NodeToText(TDOMNode(NodeSet2[j])) then
begin
Result := True;
exit;
end;
end;
end else
begin
s := Op2.AsText;
for i := 0 to NodeSet1.Count - 1 do
begin
if NodeToText(TDOMNode(NodeSet1[i])) = s then
begin
Result := True;
exit;
end;
end;
end;
Result := False;
end else if Op2.InheritsFrom(TXPathNodeSetVariable) then
begin
s := Op1.AsText;
for i := 0 to NodeSet2.Count - 1 do
if s = NodeToText(TDOMNode(NodeSet2[i])) then
begin
Result := True;
exit;
end;
Result := False;
end else if Op1.InheritsFrom(TXPathBooleanVariable) or
Op2.InheritsFrom(TXPathBooleanVariable) then
Result := Op1.AsBoolean = Op2.AsBoolean
else if Op1.InheritsFrom(TXPathNumberVariable) or
Op2.InheritsFrom(TXPathNumberVariable) then
begin
e1 := Op1.AsNumber;
e2 := Op2.AsNumber;
if IsNan(e1) or IsNan(e2) then
Result := False
else if IsInfinite(e1) or IsInfinite(e2) then
Result := e1 = e2
else
Result := SameValue(e1, e2);
end
else
Result := Op1.AsText = Op2.AsText; // !!!: Attention with Unicode!
end;
nsnum: Integer;
begin
Op1 := FOperand1.Evaluate(AContext, AEnvironment);
try
Op2 := FOperand2.Evaluate(AContext, AEnvironment);
try
case FOperator of
opEqual:
BoolResult := EvalEqual;
opNotEqual:
BoolResult := not EvalEqual;
else
e1 := Op1.AsNumber;
e2 := Op2.AsNumber;
if IsNan(e1) or IsNan(e2) then
BoolResult := False
else
case FOperator of
opLess:
BoolResult := e1 < e2;
opLessEqual:
BoolResult := e1 <= e2;
opGreater:
BoolResult := e1 > e2;
opGreaterEqual:
BoolResult := e1 >= e2;
nsnum := 0;
if Op1 is TXPathNodeSetVariable then
Inc(nsnum);
if Op2 is TXPathNodeSetVariable then
Inc(nsnum);
case nsnum of
0: begin // neither op is a nodeset
if (FOperator in [opEqual, opNotEqual]) then
begin
if ((Op1 is TXPathBooleanVariable) or (Op2 is TXPathBooleanVariable)) then
BoolResult := (Op1.AsBoolean = Op2.AsBoolean) xor (FOperator = opNotEqual)
else if (Op1 is TXPathNumberVariable) or (Op2 is TXPathNumberVariable) then
BoolResult := CmpNumbers(Op1.AsNumber, Op2.AsNumber, FOperator)
else
BoolResult := (Op1.AsText = Op2.AsText) xor (FOperator = opNotEqual);
end
else
BoolResult := CmpNumbers(Op1.AsNumber, Op2.AsNumber, FOperator);
end;
1: begin
if Op1 is TXPathNodeSetVariable then
begin
if Op2 is TXPathNumberVariable then
BoolResult := CmpNodesetWithNumber(Op1.AsNodeSet, Op2.AsNumber, FOperator)
else if Op2 is TXPathStringVariable then
BoolResult := CmpNodesetWithString(Op1.AsNodeSet, Op2.AsText, FOperator)
else
BoolResult := CmpNodesetWithBoolean(Op1.AsNodeSet, Op2.AsBoolean, FOperator);
end
else // Op2 is nodeset
begin
if Op1 is TXPathNumberVariable then
BoolResult := CmpNodesetWithNumber(Op2.AsNodeSet, Op1.AsNumber, reverse[FOperator])
else if Op1 is TXPathStringVariable then
BoolResult := CmpNodesetWithString(Op2.AsNodeSet, Op1.AsText, reverse[FOperator])
else
BoolResult := CmpNodesetWithBoolean(Op2.AsNodeSet, Op1.AsBoolean, reverse[FOperator]);
end;
end;
else // both ops are nodesets
BoolResult := CmpNodesets(Op1.AsNodeSet, Op2.AsNodeSet, FOperator);
end;
finally
Op2.Release;
@ -1357,21 +1400,11 @@ begin
end;
function TXPathNodeSetVariable.AsText: DOMString;
var
i: Integer;
begin
if FValue.Count = 0 then
SetLength(Result, 0)
Result := ''
else
begin
Result := '';
for i := 0 to FValue.Count - 1 do
begin
if i > 0 then
Result := Result + LineEnding;
Result := Result + NodeToText(TDOMNode(FValue[i]));
end;
end;
Result := NodeToText(TDOMNode(FValue.First));
end;
function TXPathNodeSetVariable.AsBoolean: Boolean;
@ -1803,6 +1836,7 @@ begin
if NextToken = tkString then
begin
// TODO: Handle processing-instruction('name') constructs
CurStep.NodeTestString := CurTokenString;
NextToken;
end;
if CurToken <> tkRightBracket then
@ -2434,23 +2468,33 @@ end;
function TXPathEnvironment.xpStartsWith(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
s1, s2: DOMString;
res: Boolean;
begin
if Args.Count <> 2 then
EvaluationError(SEvalInvalidArgCount);
s1 := TXPathVariable(Args[0]).AsText;
s2 := TXPathVariable(Args[1]).AsText;
Result := TXPathBooleanVariable.Create(Pos(s2, s1) = 1);
if s2 = '' then
res := True
else
res := Pos(s2, s1) = 1;
Result := TXPathBooleanVariable.Create(res);
end;
function TXPathEnvironment.xpContains(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
s1, s2: DOMString;
res: Boolean;
begin
if Args.Count <> 2 then
EvaluationError(SEvalInvalidArgCount);
s1 := TXPathVariable(Args[0]).AsText;
s2 := TXPathVariable(Args[1]).AsText;
Result := TXPathBooleanVariable.Create(Pos(s2, s1) <> 0);
if s2 = '' then
res := True
else
res := Pos(s2, s1) <> 0;
Result := TXPathBooleanVariable.Create(res);
end;
function TXPathEnvironment.xpSubstringBefore(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
@ -2525,11 +2569,27 @@ begin
end;
function TXPathEnvironment.xpNormalizeSpace(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
s: DOMString;
p: DOMPChar;
i: Integer;
begin
if Args.Count > 1 then
EvaluationError(SEvalInvalidArgCount);
// TODO: xmlutils.NormalizeSpace is not appropriate because it handles only #32 chars
EvaluationError(SEvalFunctionNotImplementedYet, ['normalize-space']); // !!!
if Args.Count = 0 then
s := NodeToText(Context.ContextNode)
else
s := TXPathVariable(Args[0]).AsText;
UniqueString(s);
p := DOMPChar(s);
for i := 1 to Length(s) do
begin
if (p^ = #10) or (p^ = #13) or (p^ = #9) then
p^ := #32;
Inc(p);
end;
NormalizeSpaces(s);
Result := TXPathStringVariable.Create(s);
end;
function TXPathEnvironment.xpTranslate(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
@ -2589,24 +2649,42 @@ begin
end;
function TXPathEnvironment.xpSum(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
i: Integer;
ns: TNodeSet;
sum: Extended;
begin
if Args.Count <> 1 then
EvaluationError(SEvalInvalidArgCount);
EvaluationError(SEvalFunctionNotImplementedYet, ['sum']); // !!!
ns := TXPathVariable(Args[0]).AsNodeSet;
sum := 0.0;
for i := 0 to ns.Count-1 do
sum := sum + StrToNumber(NodeToText(TDOMNode(ns[i])));
Result := TXPathNumberVariable.Create(sum);
end;
function TXPathEnvironment.xpFloor(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
n: Extended;
begin
if Args.Count <> 1 then
EvaluationError(SEvalInvalidArgCount);
Result := TXPathNumberVariable.Create(Floor(TXPathVariable(Args[0]).AsNumber));
n := TXPathVariable(Args[0]).AsNumber;
if not IsNan(n) then
n := floor(n);
Result := TXPathNumberVariable.Create(n);
end;
function TXPathEnvironment.xpCeiling(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;
var
n: Extended;
begin
if Args.Count <> 1 then
EvaluationError(SEvalInvalidArgCount);
Result := TXPathNumberVariable.Create(Ceil(TXPathVariable(Args[0]).AsNumber));
n := TXPathVariable(Args[0]).AsNumber;
if not IsNan(n) then
n := ceil(n);
Result := TXPathNumberVariable.Create(n);
end;
function TXPathEnvironment.xpRound(Context: TXPathContext; Args: TXPathVarList): TXPathVariable;

View File

@ -15,7 +15,7 @@
**********************************************************************}
program xpathts;
{$mode objfpc}{$h+}
{$mode delphi}{$h+}
uses
Classes, SysUtils, Math,
@ -25,6 +25,7 @@ type
TResultType = (rtString, rtNumber, rtBool, rtNodeset);
TTestRec = record
data: string; // UTF-8 encoded
expr: DOMString;
case rt: TResultType of
rtString: (s: DOMPChar); // cannot use DOMString here
@ -32,6 +33,7 @@ type
rtBool: (b: Boolean);
end;
{$warnings off}
const
BaseTests: array[0..4] of TTestRec = (
(expr: '1'; rt: rtNumber; n: 1),
@ -41,7 +43,7 @@ const
(expr: '(1+2)*(3+4)'; rt: rtNumber; n: 21)
);
CompareTests: array[0..45] of TTestRec = (
CompareTests: array[0..46] of TTestRec = (
(expr: '0<0'; rt: rtBool; b: False),
(expr: '0<=0'; rt: rtBool; b: True),
(expr: '0>0'; rt: rtBool; b: False),
@ -58,6 +60,7 @@ const
(expr: '1<=1'; rt: rtBool; b: True),
(expr: '1>1'; rt: rtBool; b: False),
(expr: '1>=1'; rt: rtBool; b: True),
(expr: '0>-0'; rt: rtBool; b: False),
(expr: '"0"<1'; rt: rtBool; b: True),
(expr: '"0"<=1'; rt: rtBool; b: True),
(expr: '"0">1'; rt: rtBool; b: False),
@ -90,6 +93,141 @@ const
(expr: '"a" < "0.0"'; rt: rtBool; b: False)
);
nscmp = '<doc>'#10+
'<j l="12" w="33">first</j>'#10+
'<j l="17" w="45">second</j>'#10+
'<j l="16" w="78">third</j>'#10+
'<j l="12" w="33">fourth</j>'#10+
'</doc>';
nscmp2 = '<doc>'#10+
'<j l="12" w="45">first</j>'#10+
'<j l="17" w="45">second</j>'#10+
'<j l="16" w="78">third</j>'#10+
'<j l="12" w="33">fourth</j>'#10+
'</doc>';
simple = '<doc>test</doc>';
bool58 = '<doc>'#10+
'<av>'#10+
' <a>'#10+
' <b>b</b>'#10+
' <c>c</c>'#10+
' <d>d</d>'#10+
' <e>e</e>'#10+
' </a>'#10+
' <v>'#10+
' <w>w</w>'#10+
' <x>x</x>'#10+
' <y>y</y>'#10+
' <z>z</z>'#10+
' </v>'#10+
' <a>'#10+
' <b>fe</b>'#10+
' <c>fi</c>'#10+
' <d>fo</d>'#10+
' <e>fu</e>'#10+
' </a>'#10+
' <v>'#10+
' <w>fee</w>'#10+
' <x>fii</x>'#10+
' <y>foo</y>'#10+
' <z>fom</z>'#10+
' </v>'#10+
' <j>foo</j>'#10+
' <j>foo</j>'#10+
' <j>foo</j>'#10+
' <j>foo</j>'#10+
'</av>'#10+
'</doc>';
bool84='<doc>'#10+
'<avj>'#10+
' <good>'#10+
' <b>12</b>'#10+
' <c>34</c>'#10+
' <d>56</d>'#10+
' <e>78</e>'#10+
' </good>'#10+
'</avj>'#10+
'</doc>';
bool85='<doc>'#10+
'<avj>'#10+
' <bool>'#10+
' <b>true</b>'#10+
' <c></c>'#10+
' <d>false?</d>'#10+
' <e>1</e>'#10+
' <f>0</f>'#10+
' </bool>'#10+
'</avj>'#10+
'</doc>';
str04='<doc>'#10+
'<a>Testing this</a>'#10+
'<b>and this too</b>'#10+
'</doc>';
NodesetCompareTests: array[0..38] of TTestRec = (
{ same nodeset }
(data: nscmp; expr: 'j[@l="12"] = j[@w="33"]'; rt: rtBool; b: True), // #70
{ disjoint nodesets }
(data: nscmp; expr: 'j[@l="12"] = j[@l="17"]'; rt: rtBool; b: False), // #71
{ both have one common node }
(data: nscmp2; expr: 'j[@l="12"] = j[@w="45"]'; rt: rtBool; b: True), // #72
{ same nodeset - unequal }
(data: nscmp; expr: 'j[@l="12"] != j[@w="33"]'; rt: rtBool; b: True), // #73
{ disjoint - unequal }
(data: nscmp; expr: 'j[@l="12"] != j[@l="17"]'; rt: rtBool; b: True), // #74
{ one common node - unequal }
(data: nscmp2; expr: 'j[@l="12"] != j[@w="45"]'; rt: rtBool; b: True), // #75
{ single common node - unequal }
(data: nscmp2; expr: 'j[@l="16"] != j[@w="78"]'; rt: rtBool; b: False),// #76
{ nodeset vs. string }
(data: bool58; expr: '/doc/av//*="foo"'; rt: rtBool; b: True), // #58.1
(data: bool58; expr: 'not(/doc/av//*!="foo")'; rt: rtBool; b: False), // #58.2
(data: bool58; expr: '/doc/av//j="foo"'; rt: rtBool; b: True), // #58.3
(data: bool58; expr: 'not(/doc/av//j!="foo")'; rt: rtBool; b: True), // #58.4
{ empty nodeset vs. string. Data differs, but that doesn't matter }
(data: bool58; expr: '/doc/avj//k="foo"'; rt: rtBool; b: False), // #59.1
(data: bool58; expr: 'not(/doc/avj//k="foo")'; rt: rtBool; b: True), // #59.2
(data: bool58; expr: '/doc/avj//k!="foo"'; rt: rtBool; b: False), // #59.3
(data: bool58; expr: 'not(/doc/avj//k!="foo")'; rt: rtBool; b: True), // #59.4
{ nodeset vs. number }
(data: bool84; expr: '/doc/avj/good/*=34'; rt: rtBool; b: True), // #84.1
(data: bool84; expr: 'not(/doc/avj/good/*=34)'; rt: rtBool; b: False), // #84.2
(data: bool84; expr: '/doc/avj/good/*!=34'; rt: rtBool; b: True), // #84.3
(data: bool84; expr: 'not(/doc/avj/good/*!=34)'; rt: rtBool; b: False),// #84.4
{ same with reversed order of operands }
(data: bool84; expr: '34=/doc/avj/good/*'; rt: rtBool; b: True), // #84.5
(data: bool84; expr: 'not(34=/doc/avj/good/*)'; rt: rtBool; b: False), // #84.6
(data: bool84; expr: '34!=/doc/avj/good/*'; rt: rtBool; b: True), // #84.7
(data: bool84; expr: 'not(34!=/doc/avj/good/*)'; rt: rtBool; b: False),// #84.8
{ nodeset vs. boolean }
(data: bool85; expr: '/doc/avj/bool/*=true()'; rt: rtBool; b: True), // #85.1
(data: bool85; expr: 'not(/doc/avj/bool/*=true())'; rt: rtBool; b: False), // #85.2
(data: bool85; expr: '/doc/avj/bool/*!=true()'; rt: rtBool; b: False), // #85.3
(data: bool85; expr: 'not(/doc/avj/bool/*!=true())'; rt: rtBool; b: True), // #85.4
{ same with reversed order of operands }
(data: bool85; expr: 'true()=/doc/avj/bool/*'; rt: rtBool; b: True), // #85.5
(data: bool85; expr: 'not(true()=/doc/avj/bool/*)'; rt: rtBool; b: False), // #85.6
(data: bool85; expr: 'true()!=/doc/avj/bool/*'; rt: rtBool; b: False), // #85.7
(data: bool85; expr: 'not(true()!=/doc/avj/bool/*)'; rt: rtBool; b: True), // #85.8
{ empty nodeset vs. boolean }
(data: bool85; expr: '/doc/avj/none/*=true()'; rt: rtBool; b: False), // #86.1
(data: bool85; expr: 'not(/doc/avj/none/*=true())'; rt: rtBool; b: True), // #86.2
(data: bool85; expr: '/doc/avj/none/*!=true()'; rt: rtBool; b: True), // #86.3
(data: bool85; expr: 'not(/doc/avj/none/*!=true())'; rt: rtBool; b: False),// #86.4
{ same with reversed order of operands }
(data: bool85; expr: 'true()=/doc/avj/none/*'; rt: rtBool; b: False), // #86.5
(data: bool85; expr: 'not(true()=/doc/avj/none/*)'; rt: rtBool; b: True), // #86.6
(data: bool85; expr: 'true()!=/doc/avj/none/*'; rt: rtBool; b: True), // #86.7
(data: bool85; expr: 'not(true()!=/doc/avj/none/*)'; rt: rtBool; b: False) // #86.8
);
EqualityTests: array[0..25] of TTestRec = (
(expr: '1=1'; rt: rtBool; b: True),
(expr: '1!=1'; rt: rtBool; b: False),
@ -125,7 +263,42 @@ const
(expr: '"a"!=0.0'; rt: rtBool; b: True)
);
FloatTests: array[0..60] of TTestRec = (
math88='<doc>'+
'<n0>0</n0>'+
'<n1>1</n1>'+
'<n2>2</n2>'+
'<n3>3</n3>'+
'<n4>4</n4>'+
'<n5>5</n5>'+
'<n6>6</n6>'+
'<n7>2</n7>'+
'<n8>6</n8>'+
'<n9>10</n9>'+
'<n10>3</n10>'+
'</doc>';
math85='<doc>'+
'<n0>0</n0>'+
'<n1>1</n1>'+
'<n2>2</n2>'+
'<n3>3</n3>'+
'<n4>4</n4>'+
'<e>five</e>'+
'</doc>';
math80='<doc>'+
'<n1 attrib="10">5</n1>'+
'<n2 attrib="4">2</n2>'+
'<div attrib="-5">-5</div>'+
'<mod attrib="-2">2</mod>'+
'</doc>';
math69='<doc>'+
'<n-1 attrib="9">3</n-1>'+
'<n-2 attrib="1">7</n-2>'+
'</doc>';
FloatTests: array[0..70] of TTestRec = (
(expr: '1'; rt: rtNumber; n: 1),
(expr: '123'; rt: rtNumber; n: 123),
(expr: '1.23'; rt: rtNumber; n: 1.23),
@ -189,13 +362,39 @@ const
(expr: '5 mod -2'; rt: rtNumber; n: 1),
(expr: '-5 mod 2'; rt: rtNumber; n: -1),
(expr: '-5 mod -2'; rt: rtNumber; n: -1),
(expr: '2 mod number("xxx")'; rt: rtNumber; n: NaN),
(expr: 'number("xxx") mod 3'; rt: rtNumber; n: NaN),
(expr: '8 mod 3 = 2'; rt: rtBool; b: True),
(data: math88; expr: '(n1*n2*n3*n4*n5*n6)div n7 div n8 div n9 div n10'; rt: rtNumber; n: 2),
(data: math85; expr: '((((((n3+5)*(3)+(((n2)+2)*(n1 - 6)))-(n4 - n2))+(-(4-6)))))'; rt: rtNumber; n: 4),
(data: math80; expr: 'div mod mod'; rt: rtNumber; n: -1),
(data: math69; expr: '-(n-2/@attrib) - -(n-1/@attrib)'; rt: rtNumber; n: 8),
(data: math69; expr: '-n-2/@attrib --n-1/@attrib'; rt: rtNumber; n: 8),
(data: math69; expr: '-n-2 --n-1'; rt: rtNumber; n: -4),
// test boolean operator short-circuting; "count(5)" acts as an error
(expr: '10+30*20 or count(5)'; rt: rtBool; b: True),
(expr: '75-50-25 and count(5)'; rt: rtBool; b: False)
(expr: '75-50-25 and count(5)'; rt: rtBool; b: False),
(expr: '"1" and "0"'; rt: rtBool; b: True),
(expr: '0 or ""'; rt: rtBool; b: False)
);
FunctionTests: array[0..36] of TTestRec = (
math95='<doc>'+
'<e>1</e>'+
'<e>2</e>'+
'<e>3</e>'+
'<e>4</e>'+
'<e>five</e>'+
'</doc>';
math96='<doc>'+
'<e>17</e>'+
'<e>-5</e>'+
'<e>8</e>'+
'<e>-37</e>'+
'</doc>';
FunctionTests: array[0..45] of TTestRec = (
// last()
// position()
// count()
@ -211,13 +410,16 @@ const
(expr: 'boolean(0 div 0)'; rt: rtBool; b: False),
(expr: 'boolean("")'; rt: rtBool; b: False),
(expr: 'boolean("abc")'; rt: rtBool; b: True),
{
boolean(node-set) -- TODO
}
(data: simple; expr: 'boolean(/doc)'; rt: rtBool; b: True), // #40
(data: simple; expr: 'boolean(foo)'; rt: rtBool; b: False), // #41
(expr: 'true()'; rt: rtBool; b: True),
(expr: 'false()'; rt: rtBool; b: False),
(expr: 'not(true())'; rt: rtBool; b: False),
(expr: 'not(false())'; rt: rtBool; b: True),
(expr: 'not("")'; rt: rtBool; b: True),
{
not()
lang() -- involves nodes
}
@ -226,9 +428,9 @@ const
(expr: '-number("abc")'; rt: rtNumber; n: NaN),
(expr: 'number(true())'; rt: rtNumber; n: 1.0),
(expr: 'number(false())'; rt: rtNumber; n: 0),
{
sum() -- involves nodes
}
(data: math95; expr: 'sum(e)'; rt: rtNumber; n: NaN),
(data: math96; expr: 'sum(e)'; rt: rtNumber; n: -17),
(expr: 'floor(0.1)'; rt: rtNumber; n: 0),
(expr: 'floor(-0.1)'; rt: rtNumber; n: -1),
@ -236,6 +438,7 @@ const
(expr: 'floor(0)'; rt: rtNumber; n: 0),
(expr: 'floor(5.2)'; rt: rtNumber; n: 5),
(expr: 'floor(-5.2)'; rt: rtNumber; n: -6),
(expr: 'floor("NaN")'; rt: rtNumber; n: NaN),
(expr: 'ceiling(0.1)'; rt: rtNumber; n: 1),
(expr: 'ceiling(-0.1)'; rt: rtNumber; n: 0),
@ -243,6 +446,7 @@ const
(expr: 'ceiling(0)'; rt: rtNumber; n: 0),
(expr: 'ceiling(5.2)'; rt: rtNumber; n: 6),
(expr: 'ceiling(-5.2)'; rt: rtNumber; n: -5),
(expr: 'ceiling("NaN")'; rt: rtNumber; n: NaN),
(expr: 'round(0.1)'; rt: rtNumber; n: 0),
(expr: 'round(5.2)'; rt: rtNumber; n: 5),
@ -257,7 +461,31 @@ const
(expr: 'round(-1 div 0)'; rt: rtNumber; n: -Infinity)
);
StringTests: array[0..43] of TTestRec = (
str14 ='<doc>'#10+
' <av>'#10+
' <a>'#10+
' <b>b</b>'#10+
' <c>c</c>'#10+
' <d>d</d>'#10+
' <e>e</e>'#10+
' </a>'#10+
' <v>'#10+
' <w>w</w>'#10+
' <x>x</x>'#10+
' <y>y</y>'#10+
' <z>z</z>'#10+
' </v>'#10+
' </av>'#10+
'</doc>';
out14 =#10+
' b'#10+
' c'#10+
' d'#10+
' e'#10+
' ';
StringTests: array[0..59] of TTestRec = (
(expr: 'string(5)'; rt: rtString; s: '5'),
(expr: 'string(0.5)'; rt: rtString; s: '0.5'),
(expr: 'string(-0.5)'; rt: rtString; s: '-0.5'),
@ -267,19 +495,32 @@ const
(expr: 'string(1 div 0)'; rt: rtString; s: 'Infinity'),
(expr: 'string(-1 div 0)'; rt: rtString; s: '-Infinity'),
// maybe other checks for correct numeric formats
(data: str14; expr: 'string(av//*)'; rt: rtString; s: out14),
(expr: 'concat("titi","toto")'; rt: rtString; s: 'tititoto'),
(expr: 'concat("titi","toto","tata")'; rt: rtString; s: 'tititototata'),
(expr: 'concat("titi",''toto'')'; rt: rtString; s: 'tititoto'),
(expr: 'concat("titi",''toto'',"tata","last")'; rt: rtString; s: 'tititototatalast'),
(expr: 'concat("cd", 34)'; rt: rtString; s: 'cd34'), // #101
(expr: 'concat(false(), "ly")'; rt: rtString; s: 'falsely'), // #104
(expr: 'starts-with("tititoto","titi")'; rt: rtBool; b: True),
(expr: 'starts-with("tititoto","to")'; rt: rtBool; b: False),
(expr: 'starts-with("ab", "abc")'; rt: rtBool; b: False),
(expr: 'starts-with("abc", "")'; rt: rtBool; b: True), // xalan/string/string48
(expr: 'starts-with("", "")'; rt: rtBool; b: True), // #49
(expr: 'contains("tititototata","titi")'; rt: rtBool; b: True),
(expr: 'contains("tititototata","toto")'; rt: rtBool; b: True),
(expr: 'contains("tititototata","tata")'; rt: rtBool; b: True),
(expr: 'contains("tititototata","tita")'; rt: rtBool; b: False),
(expr: 'contains("ab", "abc")'; rt: rtBool; b: False), // #59
(expr: 'contains("abc", "bcd")'; rt: rtBool; b: False), // #60
(expr: 'contains("abc", "")'; rt: rtBool; b: True), // #61
(expr: 'contains("", "")'; rt: rtBool; b: True), // #62
// 'contains(concat(.,'BC'),concat('A','B','C'))' == true
(expr: 'substring("12345",2,3)'; rt: rtString; s: '234'),
(expr: 'substring("12345",2)'; rt: rtString; s: '2345'),
@ -310,12 +551,17 @@ const
(expr: 'string-length("")'; rt: rtNumber; n: 0),
(expr: 'string-length("titi")'; rt: rtNumber; n: 4),
{
normalize-space()
}
(data: simple; expr: 'string-length(.)'; rt: rtNumber; n: 4), // #02 modified
(data: str04; expr: 'string-length(/)'; rt: rtNumber; n:27), // #04.1 modified
(data: str04; expr: 'string-length(/doc/a)'; rt: rtNumber; n: 12), // #04.2
(data: str04; expr: 'string-length()'; rt: rtNumber; n: 27),
(expr: 'normalize-space("'#9#10#13' ab cd'#10#13#9'ef'#9#10#13' ")'; rt: rtString; s: 'ab cd ef'),
(expr: 'translate("bar", "abc", "ABC")'; rt: rtString; s: 'BAr'),
(expr: 'translate("--aaa--","abc-","ABC")'; rt: rtString; s: 'AAA')
(expr: 'translate("--aaa--","abc-","ABC")'; rt: rtString; s: 'AAA'),
(expr: 'translate("ddaaadddd","abcd","ABCxy")'; rt: rtString; s: 'xxAAAxxxx') // #96
);
{$warnings on}
var
FailCount: Integer = 0;
@ -360,18 +606,40 @@ begin
Inc(FailCount);
end;
function ParseString(const data: string): TXMLDocument;
var
parser: TDOMParser;
src: TXMLInputSource;
begin
parser := TDOMParser.Create;
try
parser.Options.PreserveWhitespace := True;
src := TXMLInputSource.Create(data);
try
parser.Parse(src, Result);
finally
src.Free;
end;
finally
parser.Free;
end;
end;
procedure DoSuite(const tests: array of TTestRec);
var
i: Integer;
doc: TXMLDocument;
rslt: TXPathVariable;
begin
doc := TXMLDocument.Create;
try
for i := 0 to High(tests) do
begin
for i := 0 to High(tests) do
begin
if tests[i].data <> '' then
doc := ParseString(tests[i].data)
else
doc := TXMLDocument.Create;
try
try
rslt := EvaluateXPathExpression(tests[i].expr, doc);
rslt := EvaluateXPathExpression(tests[i].expr, doc.DocumentElement);
try
CheckResult(tests[i], rslt);
finally
@ -383,16 +651,17 @@ begin
SysUtils.ShowException(ExceptObject, ExceptAddr);
Inc(FailCount);
end;
finally
doc.Free;
end;
finally
doc.Free;
end;
end;
end;
begin
DecimalSeparator := '.';
DoSuite(BaseTests);
DoSuite(CompareTests);
DoSuite(NodesetCompareTests);
DoSuite(EqualityTests);
DoSuite(FloatTests);
DoSuite(FunctionTests);