diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b2680..566696a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [0.2.0] - 2020-04-27 + +* Re-wrote IntervalTree based on AvlTreeSet from quiver.collection +* Made IntervalTree automatically join and split appropriate intervals at + insertion and removal, respectively +* Added IntervalTree.of() and from() factory and named constructors +* Added IntervalTree.add(), addAll(), remove(), removeAll(), and clear() + methods for managing intervals in the tree +* Added IntervalTree.union(), intersection() and difference() methods +* Made IntervalTree accept array literals like `[a,b]` in place of Interval + objects +* Made IntervalTree inherit IterableMixin to offer all standard + iterable operations +* Made Interval immutable +* Renamed Interval.stop to end +* Added Interval.length property +* Added Interval.union(), intersection() and difference() methods + ## [0.1.0] - 2020-04-23 * Initial Dart port of a minimal C++ interval tree implementation diff --git a/LICENSE b/LICENSE index 7742a93..4eeb525 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,5 @@ Copyright (c) 2020 J-P Nurmi -Based on a minimal C++ interval tree implementation: -https://github.com/ekg/intervaltree - -Copyright (c) 2011 Erik Garrison - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to diff --git a/README.md b/README.md index e3ffc78..f474c21 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,46 @@ +[![pub](https://img.shields.io/pub/v/interval_tree.svg)](https://pub.dev/packages/interval_tree) +[![license: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![coverage](https://raw.githubusercontent.com/jpnurmi/interval_tree/master/coverage_badge.svg) # interval_tree -This is a Dart port of [a minimal C++ interval tree implementation](https://github.com/ekg/intervaltree). +A [Dart][1] implementation of an [interval tree][2], with support for +calculating unions, intersections, and differences between individual +intervals, or entire iterable collections of intervals, such as other +interval trees. + +## Mutable + +IntervalTree has support for adding and removing intervals, or entire +iterable collections of intervals, such as other interval trees. + +## Non-overlapping + +IntervalTree automatically joins and splits appropriate intervals at +insertions and removals, respectively, whilst maintaining a collection +of non-overlapping intervals. + +## Iterable + +IntervalTree is an [iterable collection][3] offering all standard +iterable operations, such as easily accessing the first and last +interval. + +## History + +IntervalTree started off as a quick and dirty Dart port of Erik +Garrison's [simple C++ interval tree implementation][4], but was soon +re-written and based on [quiver.collection's][6] AVL implementation of +a self-balancing binary tree [AvlTreeSet][7]. + +## License + +IntervalTree is licensed under the [MIT][5] license. + +[1]: https://dart.dev +[2]: https://en.wikipedia.org/wiki/Interval_tree +[3]: https://dart.dev/codelabs/iterables +[4]: https://github.com/ekg/intervaltree +[5]: https://opensource.org/licenses/MIT +[6]: https://pub.dev/packages/quiver +[7]: https://pub.dev/documentation/quiver/latest/quiver.collection/AvlTreeSet-class.html diff --git a/example/example.dart b/example/example.dart index 8cbb86f..da1a7df 100644 --- a/example/example.dart +++ b/example/example.dart @@ -20,17 +20,65 @@ * SOFTWARE. */ -import 'package:interval_tree/interval_tree.dart'; +import 'package:interval_tree/interval_tree.dart' as iv; void main() { // Construct a tree: - IntervalTree tree = IntervalTree([Interval(1, 5), Interval(5, 10), Interval(10, 15)]); + final iv.IntervalTree tree = iv.IntervalTree.from([ + [1, 3], + [5, 8], + [10, 15] + ]); - // Find intervals contained within given points or intervals: - print(tree.findContained(5)); // [Interval(1, 5), Interval(5, 10)] - print(tree.findContained(5, 10)); // [Interval(5, 10)] + // Add an interval: + tree.add([2, 6]); + print(tree); // IntervalTree([1, 8], [10, 15]) - // Find intervals overlapping with given points or intervals: - print(tree.findOverlapping(5)); // [Interval(1, 5), Interval(5, 10)] - print(tree.findOverlapping(5, 10)); // [Interval(1, 5), Interval(5, 10), Interval(10, 15)] + // Remove an interval: + tree.remove([12, 16]); + print(tree); // IntervalTree([1, 8], [10, 12]) + + // Calculate union/intersection/difference: + final iv.IntervalTree other = iv.IntervalTree.from([ + [0, 2], + [5, 7] + ]); + print(tree.union(other)); // IntervalTree([0, 8], [10, 12]) + print(tree.intersection(other)); // IntervalTree([1, 2], [5, 7]) + print(tree.difference(other)); // IntervalTree([2, 5], [7, 8], [10, 12]) + + { + final a = iv.Interval(0, 3); + final b = iv.Interval(2, 5); + print(a.union(b)); // [[0, 5]] + print(a.intersection(b)); // [2,3] + print(a.difference(b)); // [[0, 2]] + } + + { + final iv.IntervalTree tree = iv.IntervalTree.from([ + [1, 3], + [5, 8], + [10, 15] + ]); + print(tree); // IntervalTree([1, 3], [5, 8], [10, 15]) + tree.add([2, 6]); + print(tree); // IntervalTree([1, 8], [10, 15]) + tree.remove([12, 16]); + print(tree); // IntervalTree([1, 8], [10, 12]) + + final iv.IntervalTree other = iv.IntervalTree.from([ + [0, 2], + [5, 7] + ]); + print(tree.union(other)); // IntervalTree([0, 8], [10, 12]) + print(tree.intersection(other)); // IntervalTree([1, 2], [5, 7]) + print(tree.difference(other)); // IntervalTree([2, 5], [7, 8], [10, 12]) + + for (final interval in tree) { + print(interval); // [1, 8] \n [10, 12] + } + print(tree.first); // [1, 8] + print(tree.last); // [10, 12] + } } diff --git a/lib/interval_tree.dart b/lib/interval_tree.dart index a7ade28..11f5ae3 100644 --- a/lib/interval_tree.dart +++ b/lib/interval_tree.dart @@ -1,11 +1,6 @@ /* * Copyright (c) 2020 J-P Nurmi * - * Based on a minimal C++ interval tree implementation: - * https://github.com/ekg/intervaltree - * - * Copyright (c) 2011 Erik Garrison - * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to @@ -25,13 +20,9 @@ * SOFTWARE. */ -/// A tree data structure to hold intervals. -/// -/// ## Overview -/// -/// An interval tree can be used to efficiently find a set of numeric intervals overlapping or containing another interval. -/// -/// This library provides a basic implementation of an interval tree, allowing the insertion of arbitrary types into the tree. +/// The library provides a non-overlapping interval tree implementation with +/// support for calculating unions, intersections, and differences between +/// individual intervals and entire trees. /// /// ## Usage /// @@ -39,226 +30,442 @@ /// /// import 'package:interval_tree/interval_tree.dart'; /// -/// /// Construct a tree: /// -/// IntervalTree tree = IntervalTree([Interval(1, 5), Interval(5, 10), Interval(10, 15)]); +/// final IntervalTree tree = IntervalTree.from([[1, 3], [5, 8], [10, 15]]); +/// +/// Add an interval: +/// +/// tree.add([2, 6]); +/// print(tree); // IntervalTree([1, 8], [10, 15]) /// -/// Find intervals contained within given points or intervals: +/// Remove an interval: /// -/// print(tree.findContained(5)); // [Interval(1, 5), Interval(5, 10)] -/// print(tree.findContained(5, 10)); // [Interval(5, 10)] +/// tree.remove([12, 16]); +/// print(tree); // IntervalTree([1, 8], [10, 12]) /// -/// Find intervals overlapping with given points or intervals: +/// Calculate union/intersection/difference: /// -/// print(tree.findOverlapping(5)); // [Interval(1, 5), Interval(5, 10)] -/// print(tree.findOverlapping(5, 10)); // [Interval(1, 5), Interval(5, 10), Interval(10, 15)] +/// final IntervalTree other = IntervalTree.from([[0, 2], [5, 7] ]); +/// +/// print(tree.union(other)); // IntervalTree([0, 8], [10, 12]) +/// print(tree.intersection(other)); // IntervalTree([1, 2], [5, 7]) +/// print(tree.difference(other)); // IntervalTree([2, 5], [7, 8], [10, 12]) /// library interval_tree; -import 'package:quiver/core.dart'; +import 'dart:collection'; -/// A visitor callback function for intervals. -typedef IntervalVisitor = void Function(Interval interval); +import 'package:meta/meta.dart'; +import 'package:quiver/core.dart'; +import 'package:quiver/collection.dart'; -/// An interval between _start_ and _stop_ points. +/// An interval between two points, _start_ and _end_. +/// +/// Interval can calculate unions, intersections, and differences between +/// individual intervals: +/// +/// final a = Interval(0, 3); +/// final b = Interval(2, 5); +/// print(a.union(b)); // [[0, 5]] +/// print(a.intersection(b)); // [2,3] +/// print(a.difference(b)); // [[0, 2]] +/// +/// See [IntervalTree] for calculating more complex unions, intersections, and +/// differences between collections of intervals. +/// +/// Notice that the Interval class name unfortunately clashes with the Interval +/// class from the Flutter animation library. However, there are two ways around +/// this problem. Either use the syntax with list literals, or import either +/// library with a name prefix, for example: +/// +/// import 'package:interval_tree/interval_tree.dart' as ivt; +/// +/// final interval = ivt.Interval(1, 2); +/// +@immutable class Interval extends Comparable { - /// Constructs an interval between [start] and [stop] points. - Interval(dynamic start, dynamic stop) - : _start = start < stop ? start : stop, - _stop = stop > start ? stop : start; + /// Creates an interval between [start] and [end] points. + Interval(dynamic start, dynamic end) + : _start = _min(start, end), + _end = _max(start, end); - /// Constructs a copy of the [other] interval. + /// Creates a copy of the [other] interval. Interval.copy(Interval other) : _start = other.start, - _stop = other.stop; + _end = other.end; - /// Returns the start of the interval. + /// Returns the start point of this interval. dynamic get start => _start; - /// Returns the stop of the interval. - dynamic get stop => _stop; + /// Returns the end point of this interval. + dynamic get end => _end; + + /// Returns the length of this interval. + dynamic get length => _end - _start; + + /// Returns `true` if this interval contains the [other] interval. + bool contains(Interval other) => other.start >= start && other.end <= end; + + /// Returns `true` if this interval intersects with the [other] interval. + bool intersects(Interval other) => other.start <= end && other.end >= start; + + /// Returns the union of this interval and the [other] interval. + /// + /// In other words, the returned interval contains the points that are in + /// either interval. + /// + /// final a = Interval(0, 3); + /// final b = Interval(2, 5); + /// print(a.union(b)); // [[0, 5]] + /// + /// Notice that `a.union(b) = b.union(a)`. + /// + /// The returned interval is the entire interval from the smaller start to the + /// larger end, including any gap in between. + /// + /// final a = Interval(0, 2); + /// final b = Interval(3, 5); + /// print(b.union(a)); // [0, 5] + /// + Interval union(Interval other) => + Interval(_min(start, other.start), _max(end, other.end)); + + /// Returns the intersection between this interval and the [other] interval, + /// or `null` if the intervals do not intersect. + /// + /// In other words, the returned interval contains the points that are also + /// in the [other] interval. + /// + /// final a = Interval(0, 3); + /// final b = Interval(2, 5); + /// print(a.intersection(b)); // [[2, 3]] + /// + /// Notice that `a.intersection(b) = b.intersection(a)`. + /// + /// The returned interval may be `null` if the intervals do not intersect. + /// + /// final a = Interval(0, 2); + /// final b = Interval(3, 5); + /// print(b.intersection(a)); // null + /// + Interval intersection(Interval other) { + if (!intersects(other)) return null; + return Interval(_max(start, other.start), _min(end, other.end)); + } + + /// Returns the difference between this interval and the [other] interval, + /// or `null` if the [other] interval contains this interval. + /// + /// In other words, the returned iterable contains the interval(s) that are + /// not in the [other] interval. + /// + /// final a = Interval(0, 3); + /// final b = Interval(2, 5); + /// print(a.difference(b)); // [[0, 2]] + /// print(b.difference(a)); // [[3, 5]] + /// + /// Notice that `a.difference(b) != b.difference(a)`. + /// + /// The returned iterable may contain multiple intervals if removing the + /// [other] interval splits the remaining interval, or `null` if there is no + /// interval left after removing the [other] interval. + /// + /// final a = Interval(1, 5); + /// final b = Interval(2, 4); + /// print(a.difference(b)); // [[1, 2], [4, 5]] + /// print(b.difference(a)); // null + /// + Iterable difference(Interval other) { + if (other.contains(this)) return null; + if (!other.intersects(this)) return [this]; + if (other.start > start && other.end >= end) + return [Interval(start, other.start)]; + if (other.start <= start && other.end < end) + return [Interval(other.end, end)]; + return [Interval(start, other.start), Interval(other.end, end)]; + } - /// Compares the interval to [other] interval. + /// Compares this interval to the [other] interval. + /// + /// Two intervals are considered _equal_ when their [start] and [end] points + /// are equal. Otherwise, the one that starts first comes first, or if the + /// start points are equal, the one that ends first. + /// + /// Similarly to [Comparator], returns: + /// - a negative integer if this interval is _less than_ the [other] interval, + /// - a positive integer if this interval is _greater than_ the [other] + /// interval, + /// - zero if this interval is _equal to_ the [other] interval. @override int compareTo(Interval other) { if (start == other.start) { - return stop - other.stop; + return end - other.end; } return start - other.start; } - /// Returns `true` if the interval starts or stops before [other] interval. + /// Returns `true` if this interval start or ends before the [other] interval. + /// + /// See [compareTo] for detailed interval comparison rules. bool operator <(Interval other) => compareTo(other) < 0; - /// Returns `true` if the interval starts or stops before or same as [other] interval. + /// Returns `true` if this interval starts or ends before or same as the + /// [other] interval. + /// + /// See [compareTo] for detailed interval comparison rules. bool operator <=(Interval other) => compareTo(other) <= 0; - /// Returns `true` if the interval starts or stops after [other] interval. + /// Returns `true` if this interval starts or ends after the [other] interval. + /// + /// See [compareTo] for detailed interval comparison rules. bool operator >(Interval other) => compareTo(other) > 0; - /// Returns `true` if the interval starts or stops after or same as [other] interval. + /// Returns `true` if this interval starts or ends after or same as the + /// [other] interval. + /// + /// See [compareTo] for detailed interval comparison rules. bool operator >=(Interval other) => compareTo(other) >= 0; - /// Returns `true` if the interval starts and stops same as [other] interval. + /// Returns `true` if this interval starts and ends same as the [other] + /// interval. + /// + /// See [compareTo] for detailed interval comparison rules. @override - bool operator ==(Object other) => other is Interval && start == other.start && stop == other.stop; + bool operator ==(Object other) { + return other is Interval && start == other.start && end == other.end; + } - /// Returns the hash code for the interval. + /// Returns the hash code for this interval. @override - int get hashCode => hash2(start, stop); + int get hashCode => hash2(start, end); - /// Returns a string representation of the interval. + /// Returns a string representation of this interval. @override - String toString() => 'Interval($start, $stop)'; + String toString() => '[$start, $end]'; - dynamic _start; - dynamic _stop; + static dynamic _min(dynamic a, dynamic b) => a < b ? a : b; + static dynamic _max(dynamic a, dynamic b) => a > b ? a : b; + + final dynamic _start; + final dynamic _end; } -/// A tree of intervals. -class IntervalTree { - /// Constructs an interval tree filled with [intervals]. - IntervalTree(Iterable intervals, - {int depth = 0, int bucketSize = 512, dynamic leftExtent, dynamic rightExtent}) - : assert(intervals != null) { - --depth; - if (intervals.isNotEmpty) { - final min = intervals.reduce((a, b) => a.start < b.start ? a : b); - final max = intervals.reduce((a, b) => a.stop > b.stop ? a : b); - _center = (min.start + max.stop) ~/ 2; - - if (leftExtent == null && rightExtent == null) { - // sort intervals by start - List list = List.of(intervals); - list.sort(); - intervals = list; - } - - if (depth == 0 || intervals.length < bucketSize) { - _intervals = List.of(intervals); - } else { - List lefts = []; - List rights = []; - for (final Interval interval in intervals) { - if (interval.stop < _center) { - lefts.add(interval); - } else if (interval.start > _center) { - rights.add(interval); - } else { - assert(interval.start <= _center); - assert(_center <= interval.stop); - _intervals.add(interval); - } - } - - if (lefts.isNotEmpty) { - _left = IntervalTree(lefts, - depth: depth, bucketSize: bucketSize, leftExtent: leftExtent ?? min, rightExtent: _center); - } - if (rights.isNotEmpty) { - _right = IntervalTree(rights, - depth: depth, bucketSize: bucketSize, leftExtent: _center, rightExtent: rightExtent ?? max); - } - } - } +/// A non-overlapping collection of intervals organized into a tree. +/// +/// IntervalTree has support for adding and removing intervals, or entire +/// iterable collections of intervals, such as other interval trees. +/// +/// final IntervalTree tree = IntervalTree.from([[1, 3], [5, 8], [10, 15]]); +/// print(tree); // IntervalTree([1, 3], [5, 8], [10, 15]) +/// +/// tree.add([2, 6]); +/// print(tree); // IntervalTree([1, 8], [10, 15]) +/// +/// tree.remove([12, 16]); +/// print(tree); // IntervalTree([1, 8], [10, 12]) +/// +/// As illustrated by the above example, IntervalTree automatically joins and +/// splits appropriate intervals at insertions and removals, respectively, +/// whilst maintaining a collection of non-overlapping intervals. +/// +/// IntervalTree can also calculate unions, intersections, and differences +/// between collections of intervals: +/// +/// final IntervalTree tree = IntervalTree.from([[1, 8], [10, 12]]); +/// final IntervalTree other = IntervalTree.from([[0, 2], [5, 7]]); +/// +/// print(tree.union(other)); // IntervalTree([0, 8], [10, 12]) +/// print(tree.intersection(other)); // IntervalTree([1, 2], [5, 7]) +/// print(tree.difference(other)); // IntervalTree([2, 5], [7, 8], [10, 12]) +/// +/// IntervalTree is an [Iterable] collection offering all standard iterable +/// operations, such as easily iterating the entire tree, or accessing the first +/// and last intervals. +/// +/// for (final interval in tree) { +/// print(interval); // [1, 8] \n [10, 12] +/// } +/// +/// print(tree.first); // [1, 8] +/// print(tree.last); // [10, 12] +/// +/// Notice that all methods that take interval arguments accept either +/// [Interval] objects or literal lists with two items. The latter is a natural +/// syntax for specifying intervals: +/// +/// tree.add([0, 5]); // vs. tree.add(Interval(0, 5)); +/// +/// Notice that the Interval class name unfortunately clashes with the Interval +/// class from the Flutter animation library. However, there are two ways around +/// this problem. Either use the syntax with list literals, or import either +/// library with a name prefix, for example: +/// +/// import 'package:interval_tree/interval_tree.dart' as ivt; +/// +/// final interval = ivt.Interval(1, 2); +/// +class IntervalTree extends IterableMixin { + /// Creates a tree, optionally with an [interval]. + IntervalTree([dynamic interval]) { + add(interval); } - /// Constructs an empty interval tree. - IntervalTree._(); - - /// Constructs a copy of the [other] interval tree. - factory IntervalTree.copy(IntervalTree other) { - if (other == null) { - return null; + /// Creates a tree from given iterable of [intervals]. + factory IntervalTree.from(Iterable intervals) { + IntervalTree tree = IntervalTree(); + for (final interval in intervals) { + tree.add(interval); } - - IntervalTree tree = IntervalTree._(); - tree._center = other._center; - tree._intervals = other._intervals; - tree._left = IntervalTree.copy(other._left); - tree._right = IntervalTree.copy(other._right); return tree; } - /// Calls [visitor] on all intervals near [start] and [stop] points. - void visitNear(dynamic start, dynamic stop, IntervalVisitor visitor) { - if (_left != null && start <= _center) { - _left.visitNear(start, stop, visitor); + /// Creates a tree from [intervals]. + factory IntervalTree.of(Iterable intervals) => + IntervalTree()..addAll(intervals); + + /// Adds an [interval] into this tree. + void add(dynamic interval) { + Interval iv = _asInterval(interval); + if (iv == null) return; + + bool joined = false; + BidirectionalIterator it = _tree.fromIterator(iv); + while (it.movePrevious()) { + final Interval union = _tryJoin(it.current, iv); + if (union == null) break; + it = _tree.fromIterator(iv = union, inclusive: false); + joined = true; } - if (_intervals.isNotEmpty && !(stop < _intervals.first.start)) { - for (final interval in _intervals) { - visitor(interval); - } + + it = _tree.fromIterator(iv, inclusive: false); + while (it.moveNext()) { + final Interval union = _tryJoin(it.current, iv); + if (union == null) break; + it = _tree.fromIterator(iv = union, inclusive: false); + joined = true; } - if (_right != null && stop >= _center) { - _right.visitNear(start, stop, visitor); + + if (!joined) { + _tree.add(iv); } } - /// Calls [visitor] on all intervals overlapping the interval between [start] and [stop] points. - void visitOverlapping(dynamic start, dynamic stop, IntervalVisitor visitor) { - IntervalVisitor filter = (final interval) { - if (start <= interval.stop && stop >= interval.start) { - visitor(interval); - } - }; - visitNear(start, stop, filter); + /// Adds all [intervals] into this tree. + void addAll(Iterable intervals) { + if (intervals == null) return; + for (final interval in intervals) { + add(interval); + } } - /// Calls [visitor] on all intervals contained within the interval between [start] and [stop] points. - void visitContained(dynamic start, dynamic stop, IntervalVisitor visitor) { - IntervalVisitor filter = (final interval) { - if (start >= interval.start && stop <= interval.stop) { - visitor(interval); - } - }; - visitNear(start, stop, filter); + /// Removes an [interval] from this tree. + void remove(dynamic interval) { + final Interval iv = _asInterval(interval); + if (iv == null) return; + + BidirectionalIterator it = _tree.fromIterator(iv); + while (it.movePrevious()) { + final Interval current = it.current; + if (!_trySplit(it.current, iv)) break; + it = _tree.fromIterator(current, inclusive: false); + } + + it = _tree.fromIterator(iv, inclusive: false); + while (it.moveNext()) { + final Interval current = it.current; + if (!_trySplit(it.current, iv)) break; + it = _tree.fromIterator(current, inclusive: false); + } } - /// Calls [visitor] on all intervals in the tree. - void visitAll(IntervalVisitor visitor) { - _left?.visitAll(visitor); - _intervals.forEach(visitor); - _right?.visitAll(visitor); + /// Removes all [intervals] from this tree. + void removeAll(Iterable intervals) { + if (intervals == null) return; + for (final interval in intervals) { + remove(interval); + } } - /// Returns all intervals overlapping the interval between [start] and [stop] points. - List findOverlapping(dynamic start, [dynamic stop]) { - List result = []; - visitOverlapping(start, stop ?? start, (final interval) { - result.add(interval); - }); - return result; + /// Clears this tree. + void clear() { + _tree.clear(); } - /// Returns all intervals contained within the interval between [start] and [stop] points. - List findContained(dynamic start, [dynamic stop]) { - List result = []; - visitContained(start, stop ?? start, (final interval) { - result.add(interval); - }); + // Returns the union of this tree and the [other] tree. + IntervalTree union(IntervalTree other) => + IntervalTree.of(this)..addAll(other); + + // Returns the difference between this tree and the [other] tree. + IntervalTree difference(IntervalTree other) => + IntervalTree.of(this)..removeAll(other); + + // Returns the intersection of this tree and the [other] tree. + IntervalTree intersection(IntervalTree other) { + IntervalTree result = IntervalTree(); + if (isEmpty || other.isEmpty) result; + for (final iv in other) { + BidirectionalIterator it = _tree.fromIterator(iv); + while (it.movePrevious() && iv.intersects(it.current)) + result.add(iv.intersection(it.current)); + it = _tree.fromIterator(iv, inclusive: false); + while (it.moveNext() && iv.intersects(it.current)) + result.add(iv.intersection(it.current)); + } return result; } - /// Returns `true` if there are no intervals in the tree. - bool get isEmpty => _intervals.isEmpty && (_left?.isEmpty ?? true) && (_right?.isEmpty ?? true); + /// Returns the number of intervals in this tree. + int get length => _tree.length; + + /// Returns `true` if there are no intervals in this tree. + bool get isEmpty => _tree.isEmpty; + + /// Returns `true` if there is at least one interval in this tree. + bool get isNotEmpty => _tree.isNotEmpty; + + /// Returns the first interval in tree, or `null` if this tree is empty. + Interval get first => _tree.first; - /// Returns `true` if there is at least one interval in the tree. - bool get isNotEmpty => !isEmpty; + /// Returns the first interval in tree, or `null` if this tree is empty. + Interval get last => _tree.last; - /// Returns the minimum interval start point. - dynamic min() => _left?.min() ?? (_intervals.isEmpty ? null : _intervals.first.start); + /// Checks that this tree has only one interval, and returns that interval. + Interval get single => _tree.single; - /// Returns the maximum interval stop point. - dynamic max() => _right?.max() ?? (_intervals.isEmpty ? null : _intervals.last.stop); + /// Returns a bidirectional iterator that allows iterating the intervals. + BidirectionalIterator get iterator => _tree.iterator; - /// Returns a string representation of the interval tree. + /// Returns a string representation of the tree. @override - String toString() => 'IntervalTree($_intervals)'; + String toString() => 'IntervalTree' + super.toString(); + + Interval _asInterval(dynamic interval) { + if (interval is List && interval.length == 2) { + return Interval(interval[0], interval[1]); + } + return interval as Interval; + } + + Interval _tryJoin(Interval a, Interval b) { + if (a == null || b == null) return null; + if (a.contains(b)) return a; + if (!a.intersects(b)) return null; + final Interval union = a.union(b); + _tree.remove(a); + _tree.remove(b); + _tree.add(union); + return union; + } + + bool _trySplit(Interval a, Interval b) { + if (a == null || b == null) return false; + if (!a.intersects(b)) return false; + _tree.remove(a); + _tree.addAll([...?a.difference(b)]); + return true; + } - dynamic _center; - IntervalTree _left; - IntervalTree _right; - List _intervals = []; + final AvlTreeSet _tree = + AvlTreeSet(comparator: Comparable.compare); } diff --git a/pubspec.yaml b/pubspec.yaml index 0a394c5..02f1808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,11 @@ name: interval_tree -version: 0.1.0 +version: 0.2.0 description: >- - A Dart port minimal C++ interval tree implementation by Erik Garrison: - https://github.com/ekg/intervaltree + A non-overlapping interval tree with support for calculating unions, + intersections, and differences between individual intervals and entire + trees. The interval tree automatically joins and splits appropriate + intervals at insertion and removal, respectively, maintaining a + collection of non-overlapping intervals. homepage: https://github.com/jpnurmi/interval_tree repository: https://github.com/jpnurmi/interval_tree issue_tracker: https://github.com/jpnurmi/interval_tree/issues @@ -10,6 +13,7 @@ issue_tracker: https://github.com/jpnurmi/interval_tree/issues environment: sdk: ">=2.7.0 <3.0.0" dependencies: + meta: ^1.1.8 quiver: ^2.1.3 dev_dependencies: test: ^1.14.2 diff --git a/test/interval_test.dart b/test/interval_test.dart index 5508de7..db52cea 100644 --- a/test/interval_test.dart +++ b/test/interval_test.dart @@ -25,21 +25,32 @@ import 'package:test/test.dart'; import 'package:interval_tree/interval_tree.dart'; void main() { - test('startstop', () { - final expect_startStop = (interval, start, stop) { - expect(interval.start, start); - expect(interval.stop, stop); - }; - expect_startStop(Interval(0, 1), 0, 1); - expect_startStop(Interval(5, 10), 5, 10); - expect_startStop(Interval(0, -10), -10, 0); + test('start', () { + expect(Interval(0, 0).start, 0); + expect(Interval(-1, 1).start, -1); + expect(Interval(1, 10).start, 1); + expect(Interval(1, -10).start, -10); + }); + + test('end', () { + expect(Interval(0, 0).end, 0); + expect(Interval(-1, 1).end, 1); + expect(Interval(1, 10).end, 10); + expect(Interval(1, -10).end, 1); + }); + + test('length', () { + expect(Interval(0, 0).length, 0); + expect(Interval(-1, 1).length, 2); + expect(Interval(1, 10).length, 9); + expect(Interval(1, -10).length, 11); }); test('copy', () { final interval = Interval(0, 10); final copy = Interval.copy(interval); expect(copy.start, interval.start); - expect(copy.stop, interval.stop); + expect(copy.end, interval.end); expect(interval == copy, isTrue); expect(copy.toString(), interval.toString()); expect(identical(interval, copy), isFalse); @@ -73,7 +84,62 @@ void main() { expect(Interval(7, 8) >= Interval(6, 7), isTrue); }); + // 1 2 3 4_5_6 7 8 9 + // ___ _|_A_|_ ___ + // |_D_|_B_|_C_|_E_| + // |_F_|___|_G_| + // |___H___| + final a = Interval(4, 6); + final b = Interval(3, 5); + final c = Interval(5, 7); + final d = Interval(1, 3); + final e = Interval(7, 9); + final f = Interval(2, 4); + final g = Interval(6, 8); + final h = Interval(3, 7); + + test('intersection', () { + expect(a.intersection(a), a); // itself + expect(a.intersection(b), Interval(a.start, b.end)); // intersect + expect(a.intersection(c), Interval(c.start, a.end)); // intersect + expect(a.intersection(d), isNull); // no intersection + expect(a.intersection(e), isNull); // no intersection + expect(a.intersection(f), Interval(f.end, a.start)); // adjacent + expect(a.intersection(g), Interval(a.end, g.start)); // adjacent + expect(a.intersection(h), a); // contains + }); + + test('difference', () { + expect(a.difference(a), isNull); // itself + expect(a.difference(b), [Interval(b.end, a.end)]); // intersect + expect(a.difference(c), [Interval(a.start, c.start)]); // intersect + expect(a.difference(d), [a]); // no intersection + expect(a.difference(e), [a]); // no intersection + expect(a.difference(f), [a]); // adjacent + expect(a.difference(g), [a]); // adjacent + expect(a.difference(h), isNull); // contains + + expect(h.difference(a), [Interval(3, 4), Interval(6, 7)]); // split + expect(h.difference(b), [Interval(b.end, h.end)]); // h > b + expect(h.difference(c), [Interval(h.start, c.start)]); // h < c + expect(h.difference(d), [h]); // adjacent + expect(h.difference(e), [h]); // adjacent + expect(h.difference(f), [Interval(f.end, h.end)]); // h > f + expect(h.difference(g), [Interval(h.start, g.start)]); // h < f + }); + + test('union', () { + expect(a.union(a), a); // itself + expect(a.union(b), Interval(b.start, a.end)); // intersect + expect(a.union(c), Interval(a.start, c.end)); // intersect + expect(a.union(d), Interval(d.start, a.end)); // no intersection + expect(a.union(e), Interval(a.start, e.end)); // no intersection + expect(a.union(f), Interval(f.start, a.end)); // adjacent + expect(a.union(g), Interval(a.start, g.end)); // adjacent + expect(a.union(h), h); // contains + }); + test('toString', () { - expect(Interval(1, 2).toString(), 'Interval(1, 2)'); + expect(Interval(1, 2).toString(), '[1, 2]'); }); } diff --git a/test/interval_tree_test.dart b/test/interval_tree_test.dart index eea65f2..0053d37 100644 --- a/test/interval_tree_test.dart +++ b/test/interval_tree_test.dart @@ -25,104 +25,246 @@ import 'package:test/test.dart'; import 'package:interval_tree/interval_tree.dart'; void main() { - final Matcher throwsAssertionError = throwsA(isA()); + final Matcher throwsStateError = throwsA(isA()); test('copy', () { - final tree = IntervalTree([Interval(0, 10)]); - final copy = IntervalTree.copy(tree); - expect(copy.toString(), tree.toString()); - expect(identical(tree, copy), isFalse); - expect(copy.hashCode == tree.hashCode, isFalse); + final tree = IntervalTree([0, 10]); + final of = IntervalTree.of(tree); + final from = IntervalTree.from(tree); + expect(of.toString(), tree.toString()); + expect(from.toString(), tree.toString()); + expect(identical(of, tree), isFalse); + expect(identical(from, tree), isFalse); + expect(of.hashCode == tree.hashCode, isFalse); + expect(from.hashCode == tree.hashCode, isFalse); }); - test('empty', () { - expect(IntervalTree([]).isEmpty, isTrue); - expect(IntervalTree([]).isNotEmpty, isFalse); - expect(IntervalTree([Interval(0, 1)]).isEmpty, isFalse); - expect(IntervalTree([Interval(0, 1)]).isNotEmpty, isTrue); - expect(() => IntervalTree(null), throwsAssertionError); + test('add', () { + final IntervalTree tree = IntervalTree(); + expect(tree, []); + + tree.add([0, 0]); + expect(tree.toList(), [Interval(0, 0)]); + + tree.add([0, 0]); + expect(tree.toList(), [Interval(0, 0)]); + + tree.add([1, 2]); + expect(tree.toList(), [Interval(0, 0), Interval(1, 2)]); + + tree.addAll([ + [4, 5], + [6, 7] + ]); + expect(tree.toList(), + [Interval(0, 0), Interval(1, 2), Interval(4, 5), Interval(6, 7)]); + + tree.add([3, 4]); + expect(tree.toList(), + [Interval(0, 0), Interval(1, 2), Interval(3, 5), Interval(6, 7)]); + + tree.addAll([ + [3, 4], + [3, 5] + ]); + expect(tree.toList(), + [Interval(0, 0), Interval(1, 2), Interval(3, 5), Interval(6, 7)]); + + tree.add([1, 6]); + expect(tree.toList(), [Interval(0, 0), Interval(1, 7)]); + }); + + test('remove', () { + IntervalTree tree = IntervalTree.from([ + [0, 0], + [1, 2], + [3, 5], + [7, 9], + [11, 15] + ]); + + tree.remove([3, 4]); + expect(tree.toList(), [ + Interval(0, 0), + Interval(1, 2), + Interval(4, 5), + Interval(7, 9), + Interval(11, 15) + ]); + + tree.remove([0, 2]); + expect(tree.toList(), [Interval(4, 5), Interval(7, 9), Interval(11, 15)]); + + tree.remove([5, 12]); + expect(tree.toList(), [Interval(4, 5), Interval(12, 15)]); }); - test('findOverlapping', () { - final expect_findOverlapping = (IntervalTree tree) { - expect(tree.findOverlapping(0), [Interval(0, 0)]); - expect(tree.findOverlapping(0, 1), [Interval(0, 0), Interval(1, 2)]); - expect(tree.findOverlapping(2, 4), [Interval(1, 2), Interval(3, 4), Interval(4, 5)]); - expect(tree.findOverlapping(5, 6), [Interval(4, 5)]); - expect(tree.findOverlapping(6), []); - expect(tree.findOverlapping(7), [Interval(7, 8)]); - expect(tree.findOverlapping(7, 8), [Interval(7, 8), Interval(8, 9)]); - expect(tree.findOverlapping(6, 10), [Interval(7, 8), Interval(8, 9)]); - expect(tree.findOverlapping(10), []); - }; - final intervals = [Interval(0, 0), Interval(1, 2), Interval(3, 4), Interval(4, 5), Interval(8, 9), Interval(7, 8)]; - expect_findOverlapping(IntervalTree(intervals)); - expect_findOverlapping(IntervalTree(intervals, bucketSize: 2)); + test('clear', () { + IntervalTree tree = IntervalTree([0, 0]); + expect(tree, isNotEmpty); + tree.clear(); + expect(tree, isEmpty); }); - test('findContained', () { - final expect_findContained = (IntervalTree tree) { - expect(tree.findContained(0), [Interval(0, 10), Interval(0, 20)]); - expect(tree.findContained(0, 10), [Interval(0, 10), Interval(0, 20)]); - expect(tree.findContained(0, 20), [Interval(0, 20)]); - expect(tree.findContained(5), [Interval(0, 10), Interval(0, 20), Interval(5, 15)]); - expect(tree.findContained(5, 10), [Interval(0, 10), Interval(0, 20), Interval(5, 15)]); - expect(tree.findContained(5, 15), [Interval(0, 20), Interval(5, 15)]); - expect(tree.findContained(10), - [Interval(0, 10), Interval(0, 20), Interval(5, 15), Interval(10, 15), Interval(10, 20)]); - expect(tree.findContained(10, 15), [Interval(0, 20), Interval(5, 15), Interval(10, 15), Interval(10, 20)]); - expect(tree.findContained(10, 20), [Interval(0, 20), Interval(10, 20)]); - expect(tree.findContained(15), - [Interval(0, 20), Interval(5, 15), Interval(10, 15), Interval(10, 20), Interval(15, 20)]); - expect(tree.findContained(20), [Interval(0, 20), Interval(10, 20), Interval(15, 20)]); - expect(tree.findContained(25), []); - }; - final intervals = [ - Interval(0, 10), - Interval(0, 20), - Interval(5, 15), - Interval(10, 15), - Interval(10, 20), - Interval(15, 20) - ]; - expect_findContained(IntervalTree(intervals)); - expect_findContained(IntervalTree(intervals, bucketSize: 2)); + test('intervals', () { + expect(IntervalTree().toList(), []); + expect(IntervalTree([0, 0]).toList(), [Interval(0, 0)]); + expect( + IntervalTree.from([ + [0, 0], + [1, 2] + ]).toList(), + [Interval(0, 0), Interval(1, 2)]); + expect( + IntervalTree.from([ + [0, 0], + [1, 2], + [3, 4], + [4, 5], + [8, 9], + [7, 8] + ]), + [Interval(0, 0), Interval(1, 2), Interval(3, 5), Interval(7, 9)]); + expect( + IntervalTree.from([ + [0, 0], + [1, 2], + [3, 4], + [-5, 5] + ]), + [Interval(-5, 5)]); }); - test('visitAll', () { - final intervals = { - Interval(10, 20), - Interval(10, 15), - Interval(5, 10), - Interval(15, 20), - Interval(5, 20), - Interval(5, 15) - }; - final tree = IntervalTree(intervals, bucketSize: 2); - tree.visitAll((Interval interval) { - expect(intervals.remove(interval), isTrue); - }); - expect(intervals, isEmpty); + final pt = IntervalTree.from([ + [0, 2], + [4, 6], + [8, 10], + [12, 14] + ]); + + final tt = IntervalTree.from([ + [1, 3], + [6, 9], + [12, 15] + ]); + + final qt = IntervalTree.from([ + [2, 6], + [8, 12], + [14, 18] + ]); + + test('union', () { + expect(pt.union(pt), pt); + expect(pt.union(tt).toList(), + [Interval(0, 3), Interval(4, 10), Interval(12, 15)]); + expect(pt.union(qt).toList(), [Interval(0, 6), Interval(8, 18)]); + + expect(tt.union(tt), tt); + expect(tt.union(pt).toList(), + [Interval(0, 3), Interval(4, 10), Interval(12, 15)]); + expect(tt.union(qt).toList(), [Interval(1, 18)]); + + expect(qt.union(qt), qt); + expect(qt.union(pt).toList(), [Interval(0, 6), Interval(8, 18)]); + expect(qt.union(tt).toList(), [Interval(1, 18)]); + }); + + test('intersection', () { + expect(pt.intersection(pt), pt); + expect(pt.intersection(tt).toList(), + [Interval(1, 2), Interval(6, 6), Interval(8, 9), Interval(12, 14)]); + expect(pt.intersection(qt).toList(), [ + Interval(2, 2), + Interval(4, 6), + Interval(8, 10), + Interval(12, 12), + Interval(14, 14) + ]); + + expect(tt.intersection(tt), tt); + expect(tt.intersection(pt).toList(), + [Interval(1, 2), Interval(6, 6), Interval(8, 9), Interval(12, 14)]); + expect(tt.intersection(qt).toList(), [ + Interval(2, 3), + Interval(6, 6), + Interval(8, 9), + Interval(12, 12), + Interval(14, 15) + ]); + + expect(qt.intersection(qt), qt); + expect(qt.intersection(pt).toList(), [ + Interval(2, 2), + Interval(4, 6), + Interval(8, 10), + Interval(12, 12), + Interval(14, 14) + ]); + expect(qt.intersection(tt).toList(), [ + Interval(2, 3), + Interval(6, 6), + Interval(8, 9), + Interval(12, 12), + Interval(14, 15) + ]); + }); + + test('difference', () { + expect(pt.difference(pt), isEmpty); + expect(pt.difference(tt).toList(), + [Interval(0, 1), Interval(4, 6), Interval(9, 10)]); + expect(pt.difference(qt).toList(), [Interval(0, 2), Interval(12, 14)]); + + expect(tt.difference(tt), isEmpty); + expect(tt.difference(pt).toList(), + [Interval(2, 3), Interval(6, 8), Interval(14, 15)]); + expect(tt.difference(qt).toList(), + [Interval(1, 2), Interval(6, 8), Interval(12, 14)]); + + expect(qt.difference(qt), isEmpty); + expect(qt.difference(pt).toList(), + [Interval(2, 4), Interval(10, 12), Interval(14, 18)]); + expect(qt.difference(tt).toList(), + [Interval(3, 6), Interval(9, 12), Interval(15, 18)]); }); - test('minmax', () { - final expect_minmax = (IntervalTree tree, dynamic min, dynamic max) { - expect(tree.min(), min); - expect(tree.max(), max); - }; + test('iterable', () { + IntervalTree empty = IntervalTree(); + expect(empty.isEmpty, isTrue); + expect(empty.isNotEmpty, isFalse); + expect(empty.first, isNull); + expect(empty.last, isNull); + expect(empty.length, 0); + expect(() => empty.single, throwsStateError); + + IntervalTree single = IntervalTree(Interval(0, 0)); + expect(single.isEmpty, isFalse); + expect(single.isNotEmpty, isTrue); + expect(single.first, isNotNull); + expect(single.last, isNotNull); + expect(single.length, 1); + expect(single.single, Interval(0, 0)); + final intervals = [ - Interval(0, 10), - Interval(0, 20), - Interval(5, 15), Interval(10, 15), - Interval(10, 20), - Interval(15, 20) + Interval(20, 25), + Interval(0, 5), ]; - expect_minmax(IntervalTree(intervals), 0, 20); - expect_minmax(IntervalTree(intervals, bucketSize: 2), 0, 20); + final sorted = List.of(intervals); + sorted.sort(); + + IntervalTree tree = IntervalTree.from(intervals); + expect(tree.toList(), [Interval(0, 5), Interval(10, 15), Interval(20, 25)]); + expect(tree.isEmpty, isFalse); + expect(tree.isNotEmpty, isTrue); + expect(tree.first, Interval(0, 5)); + expect(tree.last, Interval(20, 25)); + expect(tree.length, 3); + expect(() => tree.single, throwsStateError); }); test('toString', () { - expect(IntervalTree([Interval(1, 2)]).toString(), 'IntervalTree([Interval(1, 2)])'); + expect(IntervalTree(Interval(1, 2)).toString(), 'IntervalTree([1, 2])'); }); }