AlphabetIndex.swift 68 lignes · 2960 octets
import Foundation

enum AlphabetIndex {
    /// Kotlin returned a `LinkedHashMap<Char, List<CrmContactEntity>>`;
    /// Swift has no ordered dictionary, so sections are an ordered array of pairs.
    typealias Section = (letter: Character, contacts: [CrmContactEntity])

    static func sortKey(_ c: CrmContactEntity, sort: ContactSort) -> String {
        switch sort {
        case .lastName:
            return c.lastName ?? c.fullName ?? ""
        case .firstName:
            return c.firstName ?? c.fullName ?? ""
        case .company:
            return c.company ?? ""
        case .createdAt:
            let s = String(c.createdAt)
            return s.count >= 20 ? s : String(repeating: "0", count: 20 - s.count) + s
        }
    }

    static func sectionLetter(_ key: String) -> Character {
        let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let first = trimmed.first else { return "#" }
        let up = String(first).uppercased()
        // Compare on the scalar value like Kotlin's `c in 'A'..'Z'` (so 'É' → '#').
        guard up.unicodeScalars.count == 1,
              let scalar = up.unicodeScalars.first,
              (65...90).contains(scalar.value) else { return "#" }
        return Character(up)
    }

    /// Alphabetical sorts: ordered A–Z then `#`, contacts sorted within each section.
    /// `ContactSort.createdAt`: single flat section (chrono desc), no A–Z rail sections.
    static func group(contacts: [CrmContactEntity], sort: ContactSort) -> [Section] {
        if sort == .createdAt {
            // Stable descending sort by createdAt.
            let sorted = contacts.enumerated().sorted { l, r in
                if l.element.createdAt != r.element.createdAt {
                    return l.element.createdAt > r.element.createdAt
                }
                return l.offset < r.offset
            }.map { $0.element }
            return [(letter: "#", contacts: sorted)]
        }
        // Stable case-insensitive sort (Kotlin: compareBy(String.CASE_INSENSITIVE_ORDER)).
        let sorted = contacts.enumerated().sorted { l, r in
            let cmp = sortKey(l.element, sort: sort).caseInsensitiveCompare(sortKey(r.element, sort: sort))
            if cmp != .orderedSame { return cmp == .orderedAscending }
            return l.offset < r.offset
        }.map { $0.element }
        var order: [Character] = []
        var buckets: [Character: [CrmContactEntity]] = [:]
        for c in sorted {
            let letter = sectionLetter(sortKey(c, sort: sort))
            if buckets[letter] == nil { order.append(letter) }
            buckets[letter, default: []].append(c)
        }
        var out: [Section] = []
        for k in order where k != "#" {
            out.append((letter: k, contacts: buckets[k] ?? []))
        }
        if let hash = buckets["#"] {
            out.append((letter: "#", contacts: hash))
        }
        return out
    }
}