Skip to content

Commit

Permalink
Implement union, intersection, and except operators.
Browse files Browse the repository at this point in the history
  • Loading branch information
renggli committed Sep 12, 2023
1 parent aaa4af7 commit d74a61b
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 17 deletions.
22 changes: 18 additions & 4 deletions lib/src/xpath/evaluation/values.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:meta/meta.dart';

import '../../xml/extensions/comparison.dart';
import '../../xml/nodes/document.dart';
import '../../xml/nodes/element.dart';
import '../../xml/nodes/node.dart';
Expand Down Expand Up @@ -28,16 +29,29 @@ sealed class XPathValue implements XPathExpression {

/// Wrapper around an [Iterable] of [XmlNode]s in XPath.
class XPathNodeSet implements XPathValue {
const XPathNodeSet(this.value);
/// Constructs a new node-set from [nodes]. By default we assume that the
/// input require sorting (`isSorted = false`) and deduplication (`isUnique
/// = false`).
factory XPathNodeSet(Iterable<XmlNode> nodes,
{bool isSorted = false, bool isUnique = false}) {
if (!isUnique) nodes = nodes.toSet();
final list = nodes.toList(growable: false);
if (!isUnique || !isUnique) {
list.sort((a, b) => a.compareNodePosition(b));
}
return XPathNodeSet._(list);
}

const XPathNodeSet._(this.value);

/// The empty node-set as a reusable object
static const empty = XPathNodeSet([]);
static const empty = XPathNodeSet._([]);

@override
final Iterable<XmlNode> value;
final List<XmlNode> value;

@override
Iterable<XmlNode> get nodes => value;
List<XmlNode> get nodes => value;

@override
String get string {
Expand Down
3 changes: 2 additions & 1 deletion lib/src/xpath/expressions/axis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import '../evaluation/values.dart';

abstract class AxisExpression implements XPathExpression {
@override
XPathValue call(XPathContext context) => XPathNodeSet(find(context.node));
XPathValue call(XPathContext context) =>
XPathNodeSet(find(context.node), isUnique: true, isSorted: true);

Iterable<XmlNode> find(XmlNode node);
}
Expand Down
22 changes: 17 additions & 5 deletions lib/src/xpath/functions/nodes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ XPathValue id(XPathContext context, List<XPathExpression> arguments) {
: object.string.split(' ').toSet();
if (ids.isEmpty) return XPathNodeSet.empty;
// This should likely consult the DTD about the ID attribute ...
return XPathNodeSet(context.node.root.descendantElements
.where((element) => ids.contains(element.getAttribute('id')))
.toList());
return XPathNodeSet(
context.node.root.descendantElements
.where((element) => ids.contains(element.getAttribute('id')))
.toList(),
isSorted: true,
isUnique: true);
}

// string local-name(node-set?)
Expand Down Expand Up @@ -69,11 +72,20 @@ XPathValue name(XPathContext context, List<XPathExpression> arguments) {
// node-set intersect(node-set, node-set)
XPathValue intersect(XPathContext context, List<XPathExpression> arguments) {
XPathEvaluationException.checkArgumentCount('intersect', arguments, 2);
throw UnimplementedError('intersect');
final a = arguments[0](context).nodes, b = arguments[1](context).nodes;
return XPathNodeSet(a.toSet().intersection(b.toSet()), isUnique: true);
}

// node-set except(node-set, node-set)
XPathValue except(XPathContext context, List<XPathExpression> arguments) {
XPathEvaluationException.checkArgumentCount('except', arguments, 2);
final a = arguments[0](context).nodes, b = arguments[1](context).nodes;
return XPathNodeSet(a.toSet()..removeAll(b), isUnique: true);
}

// node-set union(node-set, node-set)
XPathValue union(XPathContext context, List<XPathExpression> arguments) {
XPathEvaluationException.checkArgumentCount('union', arguments, 2);
throw UnimplementedError('union');
final a = arguments[0](context).nodes, b = arguments[1](context).nodes;
return XPathNodeSet(a.followedBy(b));
}
2 changes: 1 addition & 1 deletion lib/src/xpath/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class XPathParser {
..prefix(_t('+'), (t, a) => a);
builder.group()
..left(_t('intersect'), (a, t, b) => _SFE(t, nodes.intersect, [a, b]))
..left(_t('except'), (a, t, b) => _SFE(t, nodes.intersect, [a, b]));
..left(_t('except'), (a, t, b) => _SFE(t, nodes.except, [a, b]));
builder.group()
..left(_t('union'), (a, t, b) => _SFE(t, nodes.union, [a, b]))
..left(_t('|'), (a, t, b) => _SFE(t, nodes.union, [a, b]));
Expand Down
56 changes: 50 additions & 6 deletions test/xpath_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ void main() {
expect(value.toString(), '[123]');
});
test('elements', () {
final nodes = [
XmlDocument.parse('<a>1</a>').rootElement,
XmlDocument.parse('<b>2</b>').rootElement,
];
final nodes =
XmlDocument.parse('<r><a>1</a><b>2</b></r>').rootElement.children;
final value = XPathNodeSet(nodes);
expect(value.nodes, nodes);
expect(value.string, '1');
Expand Down Expand Up @@ -121,6 +119,19 @@ void main() {
expect(value.boolean, isTrue);
expect(value.toString(), '[First, Second, Third, ...]');
});
test('sorting', () {
final nodes =
XmlDocument.parse('<r><a/><b/><c/></r>').rootElement.children;
final value = XPathNodeSet([nodes[2], nodes[1], nodes[0]]);
expect(value.nodes, nodes);
});
test('deduplication', () {
final nodes =
XmlDocument.parse('<r><a/><b/><c/></r>').rootElement.children;
final value =
XPathNodeSet([nodes[2], nodes[2], nodes[0], nodes[0], nodes[1]]);
expect(value.nodes, nodes);
});
});
group('string', () {
test('empty', () {
Expand Down Expand Up @@ -296,10 +307,43 @@ void main() {
);
});
test('intersect(node-set, node-set)', () {
// TODO
final xml = XmlDocument.parse('<r><a/><b/><c/></r>');
final children = xml.rootElement.children;
expectEvaluate(xml, '(r/*) intersect (r/*)', isNodeSet(children));
expectEvaluate(xml, '(r/*) intersect (r/b)', isNodeSet([children[1]]));
expectEvaluate(xml, '(r/b) intersect (r/*)', isNodeSet([children[1]]));
expectEvaluate(xml, '(r/b) intersect (r/b)', isNodeSet([children[1]]));
expectEvaluate(xml, '(r/a) intersect (r/c)', isNodeSet(isEmpty));
});
test('except(node-set, node-set)', () {
final xml = XmlDocument.parse('<r><a/><b/><c/></r>');
final children = xml.rootElement.children;
expectEvaluate(xml, '(r/*) except (r/*)', isNodeSet(isEmpty));
expectEvaluate(
xml, '(r/*) except (r/b)', isNodeSet([children[0], children[2]]));
expectEvaluate(xml, '(r/b) except (r/*)', isNodeSet(isEmpty));
expectEvaluate(xml, '(r/b) except (r/b)', isNodeSet(isEmpty));
expectEvaluate(xml, '(r/a) except (r/c)', isNodeSet([children[0]]));
});
test('union(node-set, node-set)', () {
// TODO
final xml = XmlDocument.parse('<r><a/><b/><c/></r>');
final children = xml.rootElement.children;
expectEvaluate(xml, '(r/*) union (r/*)', isNodeSet(children));
expectEvaluate(xml, '(r/a) union (r/a)', isNodeSet([children[0]]));
expectEvaluate(
xml, '(r/a) union (r/c)', isNodeSet([children[0], children[2]]));
expectEvaluate(
xml, '(r/c) union (r/a)', isNodeSet([children[0], children[2]]));
});
test('|(node-set, node-set)', () {
final xml = XmlDocument.parse('<r><a/><b/><c/></r>');
final children = xml.rootElement.children;
expectEvaluate(xml, '(r/*) | (r/*)', isNodeSet(children));
expectEvaluate(xml, '(r/a) | (r/a)', isNodeSet([children[0]]));
expectEvaluate(
xml, '(r/a) | (r/c)', isNodeSet([children[0], children[2]]));
expectEvaluate(
xml, '(r/c) | (r/a)', isNodeSet([children[0], children[2]]));
});
});
group('string', () {
Expand Down

0 comments on commit d74a61b

Please sign in to comment.