Dart distinctBy оператор для коллекций

Необходимо выбрать из коллекции только уникальные элементы. Обычно в Dart используется следующий подход (не используя сторонние библиотеки):

final collection = [1, 3, 5, 1, 5];
final unique = collection.toSet().toList();

Этот подход хорошо подходит для простых типов, но не работает если нужно отфильтровать по определенным полям класса, поскольку Set вычисляет дубликаты по equals и, к тому же, не гарантирует порядок элементов в коллекции.

Например:

class KeyValuePair {
  final String key;
  final int value;

  const KeyValuePair(this.key, this.value);

  @override
  String toString() => "[$key, $value]";

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is KeyValuePair &&
          runtimeType == other.runtimeType &&
          key == other.key &&
          value == other.value;

  @override
  int get hashCode => key.hashCode ^ value.hashCode;
}

void main() {
  final collection = [
    KeyValuePair("A", 1),
    KeyValuePair("B", 3),
    KeyValuePair("C", 5),
    KeyValuePair("D", 1),
    KeyValuePair("E", 5),
  ];

  // [A, 1], [B, 3], [C, 5], [D, 1], [E, 5]
  print(collection.toSet().toList().join(", "));
}

Ответы (1 шт):

Автор решения: Ziens

Решением может быть функция-расширение для Iterable:

extension IteratorExt<T> on Iterable<T> {
  /// Возвращает [Iterable] который фильтрует повторяющиеся элементы.
  /// Элементы сравниваются по [keySelector].
  /// Если какой-то из элементов повторяется, то вызывается [onDuplicate] с помощью которого можно проигнорировать дубликат
  /// (если [onDuplicate] параметр опущен или возвращает `null`)
  /// или заменить на новое значение.
  /// Если элемент возвращаемый [onDuplicate] эквивалентен оригиналу (по equals) или
  /// соответствующий ему ключ уже существует, то он будет проигнорирован.
  Iterable<T> distinctBy(Object Function(T) keySelector,
          {T Function(T old, T current) onDuplicate}) =>
      _DistinctIterable(this, keySelector, onDuplicate);
}

class _DistinctIterable<T> extends Iterable<T> {
  final Iterable<T> _iterable;
  final Object Function(T) _keySelector;
  final T Function(T old, T current) _onDuplicate;
  final Map<Object, T> uniqueElements = {};

  _DistinctIterable(
    this._iterable,
    this._keySelector,
    T Function(T, T) onDuplicate,
  ) : _onDuplicate = onDuplicate ?? ((old, curr) => null);

  Iterator<T> get iterator {
    _iterable.forEach((element) {
      final key = _keySelector(element);

      final old = uniqueElements[key];
      if (old != null) {
        final value = _onDuplicate(old, element);
        if (value != null) uniqueElements[_keySelector(value)] = value;
      } else
        uniqueElements[key] = element;
    });

    return uniqueElements.values.iterator;
  }
}

Примеры использования:

void main() {
  // 1, 3, 5
  print([1, 3, 5, 1, 5].distinctBy((i) => i).join(", "));

  // 1, 3, 5, 2, 10
  print(
      [1, 3, 5, 1, 5].distinctBy((i) => i, onDuplicate: (old, current ) => old + current).join(", "));

  final collection = [
    KeyValuePair("A", 1),
    KeyValuePair("B", 3),
    KeyValuePair("C", 5),
    KeyValuePair("D", 1),
    KeyValuePair("E", 5),
  ];

  // [A, 1], [B, 3], [C, 5]
  print(collection.distinctBy((obj) => obj.value).join(", "));

  // [A, 1], [B, 3], [C, 5], [D (duplicated from A), 2], [E (duplicated from C), 10]
  print(collection
      .distinctBy((obj) => obj.value,
      onDuplicate: (old, current) => KeyValuePair(
        "${current.key} (duplicated from ${old.key})",
        old.value + current.value,
      ))
      .join(", "));
}

class KeyValuePair {
  final String key;
  final int value;

  const KeyValuePair(this.key, this.value);

  @override
  String toString() => "[$key, $value]";

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is KeyValuePair &&
          runtimeType == other.runtimeType &&
          key == other.key &&
          value == other.value;

  @override
  int get hashCode => key.hashCode ^ value.hashCode;
}
→ Ссылка