automap
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
### 0.7.0
|
||||
- `AutoMap<K, V>`: insertion-ordered collection with O(1) lookup by key
|
||||
**and** by index. Backed by a `List<V>` + `Map<K, int>` kept in sync by
|
||||
four mutation methods (`add`, `addAll`, `remove`, `clear`); both fields
|
||||
are private so callers cannot break the invariant. Extends `Iterable<V>`
|
||||
for `where` / `map` / `firstWhere` / etc. `remove` is O(n) to preserve
|
||||
order; every other op is O(1).
|
||||
|
||||
### 0.6.0
|
||||
- `UxFile`: new module for handing files to the OS
|
||||
- `UxFile.share(path, {title, mimeType, sourceRect})` — iOS `UIActivityViewController`,
|
||||
|
||||
90
lib/src/auto_map.dart
Normal file
90
lib/src/auto_map.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
/// Insertion-ordered collection with O(1) lookup by key **and** by index.
|
||||
///
|
||||
/// Backed by a `List<V>` + `Map<K, int>` kept consistent by the four mutation
|
||||
/// methods. Both fields are private; external callers cannot break the
|
||||
/// invariant `_indexByKey[k] == i ⇔ _keyOf(_list[i]) == k`.
|
||||
///
|
||||
/// **Contract**: the key returned by `keyOf(value)` must not change after the
|
||||
/// value is added. Mutating the derived key on a stored value silently
|
||||
/// desynchronizes the index from the data.
|
||||
///
|
||||
/// ```dart
|
||||
/// final groups = AutoMap<int, Group>((g) => g.gid);
|
||||
/// groups.add(Group(gid: 1, name: 'a'));
|
||||
/// groups[1]; // O(1) key lookup
|
||||
/// groups.elementAt(0); // O(1) indexed access
|
||||
/// ```
|
||||
class AutoMap<K, V> extends Iterable<V> {
|
||||
/// Creates an empty map. [keyOf] derives the key for each stored value;
|
||||
/// its return value must be stable for the lifetime of the entry.
|
||||
AutoMap(this._keyOf);
|
||||
|
||||
final K Function(V) _keyOf;
|
||||
final List<V> _list = [];
|
||||
final Map<K, int> _indexByKey = {};
|
||||
|
||||
@override
|
||||
Iterator<V> get iterator => _list.iterator;
|
||||
|
||||
@override
|
||||
int get length => _list.length;
|
||||
|
||||
@override
|
||||
bool get isEmpty => _list.isEmpty;
|
||||
|
||||
/// O(1). Overrides Iterable.elementAt (which would walk to index).
|
||||
@override
|
||||
V elementAt(int index) => _list[index];
|
||||
|
||||
/// O(1). Overrides Iterable.last (which would walk to end).
|
||||
@override
|
||||
V get last => _list.last;
|
||||
|
||||
/// O(1) key lookup. Returns null if absent.
|
||||
V? operator [](K key) {
|
||||
final i = _indexByKey[key];
|
||||
return i == null ? null : _list[i];
|
||||
}
|
||||
|
||||
/// True if a value is stored under [key].
|
||||
bool containsKey(K key) => _indexByKey.containsKey(key);
|
||||
|
||||
/// Keys in insertion order.
|
||||
Iterable<K> get keys => _indexByKey.keys;
|
||||
|
||||
/// O(1). Replaces in place if the key already exists (position preserved),
|
||||
/// otherwise appends.
|
||||
void add(V value) {
|
||||
final k = _keyOf(value);
|
||||
final i = _indexByKey[k];
|
||||
if (i != null) {
|
||||
_list[i] = value;
|
||||
} else {
|
||||
_indexByKey[k] = _list.length;
|
||||
_list.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// O(n). Order-preserving: entries after the removed position shift down.
|
||||
/// Returns the removed value, or null if [key] was not present.
|
||||
V? remove(K key) {
|
||||
final i = _indexByKey.remove(key);
|
||||
if (i == null) return null;
|
||||
final removed = _list.removeAt(i);
|
||||
_indexByKey.updateAll((_, v) => v > i ? v - 1 : v);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// Adds each item. Use `..clear()..addAll(xs)` for replace-all semantics.
|
||||
void addAll(Iterable<V> items) {
|
||||
for (final v in items) {
|
||||
add(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all entries.
|
||||
void clear() {
|
||||
_list.clear();
|
||||
_indexByKey.clear();
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,5 @@ export 'src/json_extension.dart';
|
||||
export 'src/bezier.dart';
|
||||
export 'src/file.dart';
|
||||
export 'src/keyboard.dart';
|
||||
export 'src/auto_map.dart';
|
||||
export 'src/sensor.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@ description: >-
|
||||
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
|
||||
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
|
||||
and layout primitives.
|
||||
version: 0.6.0
|
||||
version: 0.7.0
|
||||
homepage: https://swipelab.co/ux.html
|
||||
repository: https://github.com/swipelab/ux
|
||||
issue_tracker: https://github.com/swipelab/ux/issues
|
||||
|
||||
191
test/auto_map_test.dart
Normal file
191
test/auto_map_test.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ux/ux.dart';
|
||||
|
||||
class _Item {
|
||||
_Item(this.id, this.name);
|
||||
final int id;
|
||||
final String name;
|
||||
}
|
||||
|
||||
AutoMap<int, _Item> _map() => AutoMap<int, _Item>((i) => i.id);
|
||||
|
||||
void main() {
|
||||
group('AutoMap', () {
|
||||
test('empty on construction', () {
|
||||
final m = _map();
|
||||
expect(m, isEmpty);
|
||||
expect(m.length, 0);
|
||||
expect(m[1], isNull);
|
||||
expect(m.containsKey(1), isFalse);
|
||||
});
|
||||
|
||||
test('add appends new entries in insertion order', () {
|
||||
final m = _map()
|
||||
..add(_Item(3, 'c'))
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'));
|
||||
expect(m.map((e) => e.id).toList(), [3, 1, 2]);
|
||||
expect(m.elementAt(0).id, 3);
|
||||
expect(m.elementAt(1).id, 1);
|
||||
expect(m.elementAt(2).id, 2);
|
||||
});
|
||||
|
||||
test('add on existing key replaces in place', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'));
|
||||
m.add(_Item(2, 'B-new'));
|
||||
expect(m.map((e) => e.name).toList(), ['a', 'B-new', 'c']);
|
||||
expect(m[2]!.name, 'B-new');
|
||||
expect(m.elementAt(1).name, 'B-new');
|
||||
expect(m.length, 3);
|
||||
});
|
||||
|
||||
test('operator [] returns null for missing key', () {
|
||||
final m = _map()..add(_Item(1, 'a'));
|
||||
expect(m[1]!.name, 'a');
|
||||
expect(m[999], isNull);
|
||||
});
|
||||
|
||||
test('containsKey agrees with []', () {
|
||||
final m = _map()..add(_Item(7, 'g'));
|
||||
expect(m.containsKey(7), isTrue);
|
||||
expect(m.containsKey(8), isFalse);
|
||||
});
|
||||
|
||||
test('remove returns the removed value and shifts indices', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'))
|
||||
..add(_Item(4, 'd'));
|
||||
final removed = m.remove(2);
|
||||
expect(removed, isNotNull);
|
||||
expect(removed!.id, 2);
|
||||
expect(removed.name, 'b');
|
||||
expect(m.map((e) => e.id).toList(), [1, 3, 4]);
|
||||
expect(m.elementAt(0).id, 1);
|
||||
expect(m.elementAt(1).id, 3);
|
||||
expect(m.elementAt(2).id, 4);
|
||||
expect(m[3]!.name, 'c');
|
||||
expect(m[4]!.name, 'd');
|
||||
expect(m[2], isNull);
|
||||
});
|
||||
|
||||
test('remove on missing key returns null', () {
|
||||
final m = _map()..add(_Item(1, 'a'));
|
||||
expect(m.remove(999), isNull);
|
||||
expect(m.length, 1);
|
||||
});
|
||||
|
||||
test('add after remove assigns correct index', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'));
|
||||
m.remove(2);
|
||||
m.add(_Item(4, 'd'));
|
||||
expect(m.map((e) => e.id).toList(), [1, 3, 4]);
|
||||
expect(m.elementAt(2).id, 4);
|
||||
expect(m[4]!.name, 'd');
|
||||
});
|
||||
|
||||
test('clear + addAll replaces contents', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..clear()
|
||||
..addAll([_Item(10, 'x'), _Item(20, 'y'), _Item(30, 'z')]);
|
||||
expect(m.map((e) => e.id).toList(), [10, 20, 30]);
|
||||
expect(m[1], isNull);
|
||||
expect(m[10]!.name, 'x');
|
||||
expect(m.elementAt(2).id, 30);
|
||||
});
|
||||
|
||||
test('addAll merges, replacing entries in place on collision', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'));
|
||||
m.addAll([_Item(2, 'B-new'), _Item(4, 'd')]);
|
||||
expect(m.map((e) => '${e.id}:${e.name}').toList(),
|
||||
['1:a', '2:B-new', '3:c', '4:d']);
|
||||
expect(m.length, 4);
|
||||
});
|
||||
|
||||
test('addAll on empty behaves like bulk insert', () {
|
||||
final m = _map()..addAll([_Item(1, 'a'), _Item(2, 'b')]);
|
||||
expect(m.length, 2);
|
||||
expect(m.elementAt(0).id, 1);
|
||||
expect(m.elementAt(1).id, 2);
|
||||
});
|
||||
|
||||
test('clear drops both structures', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'));
|
||||
m.clear();
|
||||
expect(m, isEmpty);
|
||||
expect(m[1], isNull);
|
||||
expect(m.containsKey(2), isFalse);
|
||||
m.add(_Item(3, 'c'));
|
||||
expect(m.elementAt(0).id, 3);
|
||||
});
|
||||
|
||||
test('elementAt throws RangeError for out-of-range index', () {
|
||||
final m = _map()..add(_Item(1, 'a'));
|
||||
expect(() => m.elementAt(5), throwsA(isA<RangeError>()));
|
||||
});
|
||||
|
||||
test('iteration order matches insertion, even with in-place updates', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'));
|
||||
m.add(_Item(1, 'A'));
|
||||
m.add(_Item(3, 'C'));
|
||||
expect(m.map((e) => '${e.id}:${e.name}').toList(),
|
||||
['1:A', '2:b', '3:C']);
|
||||
});
|
||||
|
||||
test('elementAt is O(1) on large collections', () {
|
||||
final m = _map();
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
m.add(_Item(i, 'n$i'));
|
||||
}
|
||||
expect(m.elementAt(0).id, 0);
|
||||
expect(m.elementAt(5000).id, 5000);
|
||||
expect(m.elementAt(9999).id, 9999);
|
||||
expect(m[9999]!.name, 'n9999');
|
||||
});
|
||||
|
||||
test('keys returns keys in insertion order', () {
|
||||
final m = _map()
|
||||
..add(_Item(3, 'c'))
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'));
|
||||
expect(m.keys.toList(), [3, 1, 2]);
|
||||
m.add(_Item(3, 'C-new'));
|
||||
expect(m.keys.toList(), [3, 1, 2],
|
||||
reason: 'in-place replace must not reorder keys');
|
||||
m.remove(1);
|
||||
expect(m.keys.toList(), [3, 2]);
|
||||
});
|
||||
|
||||
test('last is O(1) and returns the last inserted value', () {
|
||||
final m = _map()
|
||||
..add(_Item(1, 'a'))
|
||||
..add(_Item(2, 'b'))
|
||||
..add(_Item(3, 'c'));
|
||||
expect(m.last.id, 3);
|
||||
m.remove(3);
|
||||
expect(m.last.id, 2);
|
||||
});
|
||||
|
||||
test('last throws StateError on empty', () {
|
||||
final m = _map();
|
||||
expect(() => m.last, throwsStateError);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user