Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LruSet implementation #280

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ false.

`listsEqual`, `mapsEqual` and `setsEqual` check collections for equality.

`LruMap` is a map that removes the least recently used item when a threshold
`LruSet` and `LruMap` are collections that remove the least recently used item when a threshold
length is exceeded.

`Multimap` is an associative collection that maps keys to collections of
Expand Down
1 change: 1 addition & 0 deletions lib/collection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'package:quiver/iterables.dart';

part 'src/collection/bimap.dart';
part 'src/collection/lru_map.dart';
part 'src/collection/lru_set.dart';
part 'src/collection/multimap.dart';
part 'src/collection/treeset.dart';
part 'src/collection/delegates/iterable.dart';
Expand Down
28 changes: 14 additions & 14 deletions lib/src/collection/lru_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ abstract class LruMap<K, V> implements Map<K, V> {
* Simple implementation of a linked-list entry that contains a [key] and
* [value].
*/
class _LinkedEntry<K, V> {
class _LinkedMapEntry<K, V> {
K key;
V value;

_LinkedEntry<K, V> next;
_LinkedEntry<K, V> previous;
_LinkedMapEntry<K, V> next;
_LinkedMapEntry<K, V> previous;

_LinkedEntry([this.key, this.value]);
_LinkedMapEntry([this.key, this.value]);
}

/**
Expand All @@ -59,18 +59,18 @@ class _LinkedEntry<K, V> {
class LinkedLruHashMap<K, V> implements LruMap<K, V> {
static const _DEFAULT_MAXIMUM_SIZE = 100;

final Map<K, _LinkedEntry<K, V>> _entries;
final Map<K, _LinkedMapEntry<K, V>> _entries;

int _maximumSize;

_LinkedEntry<K, V> _head;
_LinkedEntry<K, V> _tail;
_LinkedMapEntry<K, V> _head;
_LinkedMapEntry<K, V> _tail;

/**
* Create a new LinkedLruHashMap with a [maximumSize].
*/
factory LinkedLruHashMap({int maximumSize}) =>
new LinkedLruHashMap._fromMap(new HashMap<K, _LinkedEntry<K, V>>(),
new LinkedLruHashMap._fromMap(new HashMap<K, _LinkedMapEntry<K, V>>(),
maximumSize: maximumSize);

LinkedLruHashMap._fromMap(
Expand Down Expand Up @@ -133,8 +133,8 @@ class LinkedLruHashMap<K, V> implements LruMap<K, V> {
/**
* Creates an [Iterable] around the entries of the map.
*/
Iterable<_LinkedEntry<K, V>> _iterable() {
return new GeneratingIterable<_LinkedEntry<K, V>>(
Iterable<_LinkedMapEntry<K, V>> _iterable() {
return new GeneratingIterable<_LinkedMapEntry<K, V>>(
() => _head, (n) => n.next);
}

Expand Down Expand Up @@ -244,7 +244,7 @@ class LinkedLruHashMap<K, V> implements LruMap<K, V> {
/**
* Moves [entry] to the MRU position, shifting the linked list if necessary.
*/
void _promoteEntry(_LinkedEntry<K, V> entry) {
void _promoteEntry(_LinkedMapEntry<K, V> entry) {
if (entry.previous != null) {
// If already existed in the map, link previous to next.
entry.previous.next = entry.next;
Expand Down Expand Up @@ -273,16 +273,16 @@ class LinkedLruHashMap<K, V> implements LruMap<K, V> {
/**
* Creates and returns an entry from [key] and [value].
*/
_LinkedEntry<K, V> _createEntry(K key, V value) {
return new _LinkedEntry<K, V>(key, value);
_LinkedMapEntry<K, V> _createEntry(K key, V value) {
return new _LinkedMapEntry<K, V>(key, value);
}

/**
* If [entry] does not exist, inserts it into the backing map.
* If it does, replaces the existing [_LinkedEntry.value] with [entry.value].
* Then, in either case, promotes [entry] to the MRU position.
*/
void _insertMru(_LinkedEntry<K, V> entry) {
void _insertMru(_LinkedMapEntry<K, V> entry) {
// Insert a new entry if necessary (only 1 hash lookup in entire function).
// Otherwise, just updates the existing value.
final value = entry.value;
Expand Down
288 changes: 288 additions & 0 deletions lib/src/collection/lru_set.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

part of quiver.collection;

/**
* An implementation of a [Set] which has a maximum size and uses a (Least
* Recently Used)[http://en.wikipedia.org/wiki/Cache_algorithms#LRU] algorithm
* to remove items from the [Set] when the [maximumSize] is reached and new
* items are added.
*
* It is safe to access the iterator and contains method without affecting
* the "used" ordering - as well as using [forEach]. Other types of access,
* including lookup, promotes the key-value pair to the MRU position.
*/
abstract class LruSet<E> implements Set<E> {
/**
* Creates a [LruMap] instance with the default implementation.
*/
factory LruSet({int maximumSize}) = LinkedLruHashSet;

/**
* Maximum size of the [Map]. If [length] exceeds this value at any time,
* n entries accessed the earliest are removed, where n is [length] -
* [maximumSize].
*/
int maximumSize;
}

/**
* Simple implementation of a linked-list entry that contains a [key] and
* [element].
*/
class _LinkedSetEntry<E> {
E element;

_LinkedSetEntry<E> next;
_LinkedSetEntry<E> previous;

_LinkedSetEntry([this.element]);
}

/**
* A linked hash-table based implementation of [LruSet].
*/
class LinkedLruHashSet<E> extends SetBase<E> implements LruSet<E> {
static const _DEFAULT_MAXIMUM_SIZE = 100;

final HashMap<E, _LinkedSetEntry<E>> _entries;

int _maximumSize;

_LinkedSetEntry<E> _head;
_LinkedSetEntry<E> _tail;

/**
* Create a new LinkedLruHashMap with a [maximumSize].
*/
factory LinkedLruHashSet({int maximumSize}) =>
new LinkedLruHashSet._fromSet(new HashMap<E, _LinkedSetEntry<E>>(),
maximumSize: maximumSize);

LinkedLruHashSet._fromSet(
this._entries, {
int maximumSize})
// This pattern is used instead of a default value because we want to
// be able to respect null values coming in from MapCache.lru.
: _maximumSize = firstNonNull(maximumSize, _DEFAULT_MAXIMUM_SIZE);

/**
* If [element] already exists, promotes it to the MRU position.
*
* Otherwise, adds [element] to the MRU position.
* If [length] exceeds [maximumSize] while adding, removes the LRU position.
*/
@override
bool add(E element) {
bool wasPresent = _entries.containsKey(element);
_insertMru(_createEntry(element));

// Remove the LRU item if the size would be exceeded by adding this item.
if (length > maximumSize) {
assert(length == maximumSize + 1);
_removeLru();
}
return !wasPresent;
}

/**
* Adds all key-value pairs of [other] to this set.
*
* The operation is equivalent to doing this[key] = value for each key and
* associated value in other. It iterates over other, which must therefore not
* change during the iteration.
*
* If the number of unique keys is greater than [maximumSize] then the least
* recently use keys are evicted. For items added by [other], the least
* recently user order is determined by [other]'s iteration order.
*/
@override
void addAll(Set<E> other) => other.forEach((v) => this.add(v));

@override
void clear() {
_entries.clear();
_head = _tail = null;
}

/**
* If an object equal to object is in the set, return it.
*
* Checks if there is an object in the set that is equal to object.
* If so, that object is returned, otherwise returns null.
*
* The [element] will be promoted to the 'Most Recently Used' position.
*/
@override lookup(E element) {
final entry = _entries[element];
if (entry != null) {
_promoteEntry(entry);
return entry.element;
} else {
return null;
}
}

@override
bool contains(E element) => _entries.containsKey(element);

@override
E get first {
if(isEmpty) throw new StateError("Set is empty");
return _head.element;
}

/**
* Returns the last element.
*
* This operation is performed in constant time.
*/
@override
E get last {
if(isEmpty) throw new StateError("Set is empty");
return _tail.element;
}

/**
* Applies [action] to each key-value pair of the map in order of MRU to LRU.
*
* Calling `action` must not add or remove keys from the map.
*/
@override
void forEach(void action(E element)) {
var head = _head;
while (head != null) {
action(head.element);
head = head.next;
}
}

@override
int get length => _entries.length;

@override
bool get isEmpty => _entries.isEmpty;

@override
bool get isNotEmpty => _entries.isNotEmpty;

@override
Iterator<E> get iterator => _iterable().map((e) => e.element).iterator;

/**
* Creates an [Iterable] around the entries of the map.
*/
Iterable<_LinkedSetEntry<E>> _iterable() {
return new GeneratingIterable<_LinkedSetEntry<E>>(
() => _head, (n) => n.next);
}

@override
int get maximumSize => _maximumSize;

@override
void set maximumSize(int maximumSize) {
if (maximumSize == null) throw new ArgumentError.notNull('maximumSize');
while (length > maximumSize) {
_removeLru();
}
_maximumSize = maximumSize;
}

@override
bool remove(E element) {
final _LinkedSetEntry entry = _entries.remove(element);
if (entry != null) {
if (entry == _head) {
_head = _head.next;
} else if (entry == _tail) {
_tail.previous.next = null;
_tail = _tail.previous;
} else {
entry.previous.next = entry.next;
}
return true;
}
return false;
}

@override
Set<E> toSet() => _entries.keys.toSet();



@override
String toString() => _entries.keys.toString();

/**
* Moves [entry] to the MRU position, shifting the linked list if necessary.
*/
void _promoteEntry(_LinkedSetEntry<E> entry) {
if (entry.previous != null) {
// If already existed in the map, link previous to next.
entry.previous.next = entry.next;

// If this was the tail element, assign a new tail.
if (_tail == entry) {
_tail = entry.previous;
}
}

// Replace head with this element.
if (_head != null) {
_head.previous = entry;
}
entry.previous = null;
entry.next = _head;
_head = entry;

// Add a tail if this is the first element.
if (_tail == null) {
assert(length == 1);
_tail = _head;
}
}

/**
* Creates and returns an entry from [key] and [value].
*/
_LinkedSetEntry<E> _createEntry(E value) {
return new _LinkedSetEntry<E>(value);
}

/**
* If [entry] does not exist, inserts it into the backing map.
* If it does, replaces the existing [_LinkedEntry.value] with [entry.value].
* Then, in either case, promotes [entry] to the MRU position.
*/
void _insertMru(_LinkedSetEntry<E> entry) {
// Insert a new entry if necessary (only 1 hash lookup in entire function).
// Otherwise, just updates the existing value.
final value = entry.element;
_promoteEntry(_entries.putIfAbsent(entry.element, () => entry)..element = value);
}

/**
* Removes the LRU position, shifting the linked list if necessary.
*/
void _removeLru() {
// Remove the tail from the internal map.
_entries.remove(_tail.element);

// Remove the tail element itself.
_tail = _tail.previous;
_tail.next = null;
}
}
Loading