ソースを参照

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 -

sergei 16 年 前
コミット
b02aededf8
2 ファイル変更482 行追加135 行削除
  1. 188 110
      packages/fcl-xml/src/xpath.pp
  2. 294 25
      packages/fcl-xml/tests/xpathts.pp

+ 188 - 110
packages/fcl-xml/src/xpath.pp

@@ -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;

+ 294 - 25
packages/fcl-xml/tests/xpathts.pp

@@ -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);