From d74a61b70c0aa619339559bf15087aa6fc3eb27b Mon Sep 17 00:00:00 2001 From: Lukas Renggli Date: Tue, 12 Sep 2023 10:33:39 +0300 Subject: [PATCH] Implement `union`, `intersection`, and `except` operators. --- lib/src/xpath/evaluation/values.dart | 22 +++++++++-- lib/src/xpath/expressions/axis.dart | 3 +- lib/src/xpath/functions/nodes.dart | 22 ++++++++--- lib/src/xpath/parser.dart | 2 +- test/xpath_test.dart | 56 +++++++++++++++++++++++++--- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/lib/src/xpath/evaluation/values.dart b/lib/src/xpath/evaluation/values.dart index d766f55..0110527 100644 --- a/lib/src/xpath/evaluation/values.dart +++ b/lib/src/xpath/evaluation/values.dart @@ -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'; @@ -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 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 value; + final List value; @override - Iterable get nodes => value; + List get nodes => value; @override String get string { diff --git a/lib/src/xpath/expressions/axis.dart b/lib/src/xpath/expressions/axis.dart index bc9fd02..c84b2b7 100644 --- a/lib/src/xpath/expressions/axis.dart +++ b/lib/src/xpath/expressions/axis.dart @@ -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 find(XmlNode node); } diff --git a/lib/src/xpath/functions/nodes.dart b/lib/src/xpath/functions/nodes.dart index bf9bdb8..4fcc263 100644 --- a/lib/src/xpath/functions/nodes.dart +++ b/lib/src/xpath/functions/nodes.dart @@ -31,9 +31,12 @@ XPathValue id(XPathContext context, List 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?) @@ -69,11 +72,20 @@ XPathValue name(XPathContext context, List arguments) { // node-set intersect(node-set, node-set) XPathValue intersect(XPathContext context, List 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 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 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)); } diff --git a/lib/src/xpath/parser.dart b/lib/src/xpath/parser.dart index 88511a9..cae96d4 100644 --- a/lib/src/xpath/parser.dart +++ b/lib/src/xpath/parser.dart @@ -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])); diff --git a/test/xpath_test.dart b/test/xpath_test.dart index a241ce3..4b7203e 100644 --- a/test/xpath_test.dart +++ b/test/xpath_test.dart @@ -70,10 +70,8 @@ void main() { expect(value.toString(), '[123]'); }); test('elements', () { - final nodes = [ - XmlDocument.parse('1').rootElement, - XmlDocument.parse('2').rootElement, - ]; + final nodes = + XmlDocument.parse('12').rootElement.children; final value = XPathNodeSet(nodes); expect(value.nodes, nodes); expect(value.string, '1'); @@ -121,6 +119,19 @@ void main() { expect(value.boolean, isTrue); expect(value.toString(), '[First, Second, Third, ...]'); }); + test('sorting', () { + final nodes = + XmlDocument.parse('').rootElement.children; + final value = XPathNodeSet([nodes[2], nodes[1], nodes[0]]); + expect(value.nodes, nodes); + }); + test('deduplication', () { + final nodes = + XmlDocument.parse('').rootElement.children; + final value = + XPathNodeSet([nodes[2], nodes[2], nodes[0], nodes[0], nodes[1]]); + expect(value.nodes, nodes); + }); }); group('string', () { test('empty', () { @@ -296,10 +307,43 @@ void main() { ); }); test('intersect(node-set, node-set)', () { - // TODO + final xml = XmlDocument.parse(''); + 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(''); + 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(''); + 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(''); + 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', () {