DuplicateDetector.swift 68 lignes · 2539 octets
import Foundation

enum DuplicateDetector {
    private struct IdPair: Hashable {
        let lo: Int64
        let hi: Int64
    }

    static func clusters(
        contacts: [CrmContactEntity],
        exclusions: [DuplicateExclusionEntity]
    ) -> [Int64: Set<Int64>] {
        var parent: [Int64: Int64] = [:]
        for c in contacts { parent[c.id] = c.id }
        func find(_ x: Int64) -> Int64 {
            var p = x
            while parent[p] != p { p = parent[p]! }
            return p
        }
        func union(_ a: Int64, _ b: Int64) {
            let ra = find(a)
            let rb = find(b)
            if ra != rb { parent[ra] = rb }
        }
        let excluded = Set(exclusions.map { IdPair(lo: $0.idA, hi: $0.idB) })
        func excludedPair(_ a: Int64, _ b: Int64) -> Bool {
            excluded.contains(IdPair(lo: min(a, b), hi: max(a, b)))
        }
        var byEmail: [String: [Int64]] = [:]
        var byPhone: [String: [Int64]] = [:]
        // Preserve deterministic insertion order of the groups (Kotlin LinkedHashMap-like
        // ordering does not affect the resulting clusters, only union order).
        var emailKeys: [String] = []
        var phoneKeys: [String] = []
        for c in contacts {
            for e in c.emails.map(ContactNormalizer.email) where !e.isEmpty {
                if byEmail[e] == nil { emailKeys.append(e) }
                byEmail[e, default: []].append(c.id)
            }
            for p in c.phones.map(ContactNormalizer.phone) where !p.isEmpty {
                if byPhone[p] == nil { phoneKeys.append(p) }
                byPhone[p, default: []].append(c.id)
            }
        }
        func linkGroups(_ groups: [[Int64]]) {
            for g in groups where g.count > 1 {
                for i in 1..<g.count {
                    if !excludedPair(g[0], g[i]) { union(g[0], g[i]) }
                }
            }
        }
        linkGroups(emailKeys.compactMap { byEmail[$0] })
        linkGroups(phoneKeys.compactMap { byPhone[$0] })
        var rootToMembers: [Int64: Set<Int64>] = [:]
        for c in contacts {
            rootToMembers[find(c.id), default: []].insert(c.id)
        }
        var idToCluster: [Int64: Set<Int64>] = [:]
        for members in rootToMembers.values {
            for id in members { idToCluster[id] = members }
        }
        return idToCluster
    }

    static func duplicateCount(_ contactId: Int64, clusters: [Int64: Set<Int64>]) -> Int {
        max((clusters[contactId]?.count ?? 1) - 1, 0)
    }
}