Search code examples
dartsetmobxsplay-tree

SplayTreeSet contains duplicate values


I have a SplayTreeSet of the objects ChatRoomListModel. I'd like to order my set based on the objects DateTime createTime value.

I'm not sure where I'm going wrong because there's duplicate items being added item. I later down the line perform a _latestMessages.add(newMessage) and it's not actually calling the overloads and there's duplicates being added.

I tested by using _latestMessage.contains(newMessageButSameChatRoomId), it returns false.

When I perform _latestMessage.toSet() every duplicate goes away.

How can I get SplayTreeSet to use my overloading equals? Thanks!

  ObservableSet<ChatRoomListModel> _latestMessages = ObservableSet.splayTreeSetFrom(
    ObservableSet(),
    compare: (a, b) => b.compareTo(a),
  );

The ChatRoomListModel model has the following methods and overloads:

  int compareTo(ChatRoomListModel other){
    return messagesModel.createTime.compareTo(other.messagesModel.createTime);
  }

  ChatRoomListModel copyWith({
    String? firstName,
    String? otherUserId,
    MessagesModel? messagesModel,
  }) {
    return ChatRoomListModel(
      firstName: firstName ?? this.firstName,
      otherUserId: otherUserId ?? this.otherUserId,
      messagesModel: messagesModel ?? this.messagesModel,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is ChatRoomListModel &&
          runtimeType == other.runtimeType &&
          messagesModel.chatRoomId == other.messagesModel.chatRoomId;

  @override
  int get hashCode => messagesModel.chatRoomId.hashCode;

Solution

  • Your issue is that you have two completely different notions of what it means for two ChatRoomListModel objects to be "equal". You provide both compareTo and operator == implementations, but they consider different sets of properties, so compareTo can return 0 when operator == returns false, which is confusing at best. SplayTreeMap considers only compareTo, not operator ==. From the SplayTreeSet documentation:

    Elements of the set are compared using the compare function passed in the constructor, both for ordering and for equality. If the set contains only an object a, then set.contains(b) will return true if and only if compare(a, b) == 0, and the value of a == b is not even checked.

    I'm presuming what you call "duplicates" are elements that have equal chatRoomIds but that have different creation times, and creation times are the only things that your SplayTreeSet cares about.

    If your goal is to maintain only the latest message per chatRoomId, you need to maintain a data structure that uses the chatRoomId (a String) as a key. The natural collection for that would be a Map<String, ChatRoomListModel>. (Since the ChatRoomListModel knows its own chatRoomId, it also could just be a HashSet with explicit equals and hashCode callbacks.)

    If you additionally want to keep messages in chronological order, you either will need to explicitly sort them afterward or maintain a separate data structure that keeps them in chronological order. You could continue using a SplayTreeSet for that. Basically before you add any entry to the SplayTreeSet, check the Map first to see if an existing entry for that chatRoomId.

    I don't fully understand your data structures, but here's an example that you presumably can adapt:

    import 'dart:collection';
    
    class Message {
      Message(this.creationTime, {required this.chatRoomId, required this.text});
    
      final DateTime creationTime;
      final String chatRoomId;
      final String text;
    
      @override
      String toString() => '$creationTime: [$chatRoomId] $text';
    }
    
    class Messages {
      final _latestMessages = <String, Message>{};
      final _orderedMessages = SplayTreeSet<Message>((message1, message2) {
        var result = message1.creationTime.compareTo(message2.creationTime);
        if (result != 0) {
          return result;
        }
        result = message1.chatRoomId.compareTo(message2.chatRoomId);
        if (result != 0) {
          return result;
        }
        return message1.text.compareTo(message2.text);
      });
    
      void add(Message message) {
        var existingMessage = _latestMessages[message.chatRoomId];
        if (existingMessage != null &&
            message.creationTime.compareTo(existingMessage.creationTime) < 0) {
          // An existing message exists with a later creation date.  Ignore the
          // incoming message.
          return;
        }
    
        _latestMessages[message.chatRoomId] = message;
        _orderedMessages.remove(existingMessage);
        _orderedMessages.add(message);
      }
    
      void printAll() => _orderedMessages.forEach(print);
    }
    
    void main() {
      var messages = Messages();
    
      messages.add(Message(
        DateTime(2023, 1, 1),
        chatRoomId: 'foo',
        text: 'Hello foo!',
      ));
    
      messages.add(Message(
        DateTime(2023, 1, 2),
        chatRoomId: 'bar',
        text: 'Goodbye bar!',
      ));
    
      messages.add(Message(
        DateTime(2023, 1, 2),
        chatRoomId: 'foo',
        text: 'Goodbye foo!',
      ));
    
      messages.add(Message(
        DateTime(2023, 1, 1),
        chatRoomId: 'bar',
        text: 'Hello bar!',
      ));
    
      messages.printAll();
    }