Skip to content

Set method that returns all areas of a set intersection #184

Open

Description

@Lasering

When computing the set differences its very common to need access to all "areas" of a set intersection:

val existingLocationIds: Set[String] = ???
val newLocationIds: Set[String] = ???
val (removed, unchanged, added) = existingLocationIds.fullIntersection(newLocationIds)

This can be easily implemented with:

val unchanged = existingLocationsIds.intersect(newLocationIds)
val removed = existingLocationsIds.diff(unchanged)
val added = newLocationIds.diff(unchanged)

However this is not very performant, as we are iterating each set multiple times. Thus a performant method could be implemented. The name fullIntersection is just a placeholder, I'm sure there is a better name for this.

As an added bonus we could even take this one step further and have a fullIntersectionBy variant. In most cases this would be much more useful:

val existingLocations: Seq[Location] = ???
val newLocations: Seq[Location] = ???
val (removed, unchanged, added) = existingLocations.toSet.fullIntersectionBy(newLocations.toSet)(_.id)

Since List also has the intersect method we could also add it directly there:

existingLocations.fullIntersectionBy(newLocations)(_.id)

Activity

  1. simaomartins-visma commented on Mar 30, 2026

    @simaomartins-visma

    Ran into this need again. Here are possible implementations and an improved version:

      extension [T](existingItems: Set[T])
    
        // A \ B, A ∩ B, and B \ A
        def vennAreas(newItems: Set[T]): (Set[T], Set[T], Set[T]) =
          var removed = Set.empty[T]
          var unchanged = Set.empty[T]
          var added = newItems
    
          for item <- existingItems do
            if added.contains(item) then
              unchanged += item
              added -= item
            else removed += item
    
          (removed, unchanged, added)
    
        def vennAreasBy[K](newItems: Set[T])(f: T => K): (Set[T], Set[(T, T)], Set[T]) =
          var removed = Set.empty[T]
          var modified = Set.empty[(T, T)]
          var addedByKey = newItems.map(a => f(a) -> a).toMap
    
          for item <- existingItems do
            val key = f(item)
            addedByKey.get(key) match
              case Some(newItem) =>
                modified += (item -> newItem)
                addedByKey -= key
              case None =>
                removed += item
    
          (removed, modified, addedByKey.values.toSet)
    
      extension [K, V](existingItems: Map[K, V])
        /** Returns the venn areas of the existing and new items, where the key is used to determine the intersection. The
          * values are returned as follows:
          *   - First element (A \ B): items present in existingItems but not in newItems
          *   - Second element (A ∩ B): items present in both existingItems and newItems, with their values (old value, new
          *     value) as a tuple
          *   - Third element (B \ A): items present in newItems but not in existingItems
          */
        def vennAreas(newItems: Map[K, V]): (Map[K, V], Map[K, (V, V)], Map[K, V]) =
          var removed = Map.empty[K, V]
          var modified = Map.empty[K, (V, V)]
          var added = newItems
    
          for (key, value) <- existingItems do
            added.get(key) match
              case Some(newValue) =>
                modified += (key -> (value, newValue))
                added -= key
              case None =>
                removed += (key -> value)
    
          (removed, modified, added)

    The implementation is reasonable, but can probably be improved.

  2. bishabosha commented on Jun 22, 2026

    @bishabosha
    Member

    i would like to ask when this would be useful to general programmers rather than a toy example for drawing venn diagrams.

    is it just for auditing purposes?

  3. Lasering commented on Jun 30, 2026

    @Lasering
    Author

    The name vennAreas is not the best.

    This is a general solution to comparing two sets (or lists, or maps) of things. You get to know what is only in set1, what is in set1 and set2 (the intersection), and what is only in set2. This pattern is also helpful to compare a collection of things that gets changed over time and you want to figure out what was added, what was removed, and what stayed the same (according to some key, hence f). Its also useful to determine what will change if you apply some changes to the collection.

    Let me give some concrete examples:

    • You have a list of applications deployed to the acceptance environment. Using these functions you can compare acceptance with production. Thus it will be easy to see which new applications will be deployed, which applications will be removed, and which applications just got a version upgrade the tuple (T, T) or (V, V) (for the map) will make it easy to figure out the old vs new version.
    • The D3 Enter, Exit and Update is another good example.
    • Its like the comm command in linux

    The base implementation is just a combination of diff+intersect. But the variant *By receiving f or the Map variant is where this pattern really shines.

  4. bishabosha commented on Jun 30, 2026

    @bishabosha
    Member

    the animation example does seem a great use case (show different things to user for "transition states")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions