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) } }
GitRust