Swift中的不区分大小写字典

10

给定一个键类型为字符串的字典Dictionary,有没有一种方式以不区分大小写的方式访问其值?例如:

let dict = [
    "name": "John",
    "location": "Chicago"
]

有没有一种方法可以调用dict["NAME"], dict["nAmE"]等,仍然得到"John"

5个回答

15

更简洁的方法,Swift 4:

extension Dictionary where Key == String {

    subscript(caseInsensitive key: Key) -> Value? {
        get {
            if let k = keys.first(where: { $0.caseInsensitiveCompare(key) == .orderedSame }) {
                return self[k]
            }
            return nil
        }
        set {
            if let k = keys.first(where: { $0.caseInsensitiveCompare(key) == .orderedSame }) {
                self[k] = newValue
            } else {
                self[key] = newValue
            }
        }
    }

}

// Usage:
var dict = ["name": "John"]
dict[caseInsensitive: "NAME"] = "David" // overwrites "name" value
print(dict[caseInsensitive: "name"]!) // outputs "David"

很棒的答案,它在不改变任何东西的情况下为字典添加了灵活性。 - Nikolay Spassov
这是一个很好的答案。值得注意的是,您可以以不区分大小写的方式添加键,因此您可以创建 var dict = ["name": "John", "NAME": "David"],而不区分大小写查找的结果将变得不可预测。 - neilgmacy
3
复杂度也从O(1)上升到了O(n)。可以使用一个键值数组来解决这个问题。下面的解决方案更好地解决了这个问题。 - Alexander Jährling

8

Swift支持多个下标,因此您可以利用这一点来定义不区分大小写的访问器:

extension Dictionary where Key : StringLiteralConvertible {
    subscript(ci key : Key) -> Value? {
        get {
            let searchKey = String(key).lowercaseString
            for k in self.keys {
                let lowerK = String(k).lowercaseString
                if searchKey == lowerK {
                    return self[k]
                }
            }
            return nil
        }
    }
}

// Usage:
let dict = [
    "name": "John",
    "location": "Chicago",
]

print(dict[ci: "NAME"])      // John
print(dict[ci: "lOcAtIoN"])  // Chicago

这个扩展仅适用于键类型为StringDictionary(因为小写形式对于其他数据类型是没有意义的)。然而,Swift会抱怨将泛型类型限制为struct。最接近String的协议是StringLiteralConvertible

请注意,如果您有两个小写形式相同的键,则不能保证您将得到哪一个:

let dict = [
    "name": "John",
    "NAME": "David",
]

print(dict[ci: "name"])   // no guarantee that you will get David or John.

就像在现实世界中一样,键值应该是相同的。你不能处理打字错误,你应该纠正它们。 - vikingosegundo
对@vikingosegundo的评论点赞。为什么需要这样做?如果大小写问题会在后面影响到你,为什么不直接将所有键存储为小写呢?看起来这是一个解决方案,可以解决一个本不应该存在的问题:D - Martin Marconcini
我最近的一个项目中需要使用它。这个字典是从JSON转换而来,我输入一个键,返回对应的值,不考虑键的大小写。我也不能将所有键都转换为小写,因为我需要将其发送回另一个Web服务。如果需要更多背景信息,请搜索“不区分大小写的字典”,你会发现几乎每种有字典的编程语言都有相关问题。 - Code Different
2
在 JSON 反序列化期间将键规范化为有意义的内容,例如小写。 - vikingosegundo

7
现有的答案很好,但使用这些策略进行查找/插入的时间复杂度会从O(1)恶化到O(N)(其中N是字典中对象的数量)。
为了保持O(1),您可以考虑以下方法:
/// Wrapper around String which uses case-insensitive implementations for Hashable
public struct CaseInsensitiveString: Hashable, LosslessStringConvertible, ExpressibleByStringLiteral {
    public typealias StringLiteralType = String

    private let value: String
    private let caseInsensitiveValue: String

    public init(stringLiteral: String) {
        self.value = stringLiteral
        self.caseInsensitiveValue = stringLiteral.lowercased()
    }

    public init?(_ description: String) {
        self.init(stringLiteral: description)
    }

    public var hashValue: Int {
        return self.caseInsensitiveValue.hashValue
    }

    public static func == (lhs: CaseInsensitiveString, rhs: CaseInsensitiveString) -> Bool {
        return lhs.caseInsensitiveValue == rhs.caseInsensitiveValue
    }

    public var description: String {
        return value
    }
}

var dict = [CaseInsensitiveString: String]()
dict["name"] = "John"
dict["NAME"] = "David" // overwrites "name" value
print(dict["name"]!) // outputs "David"

将您的密钥存储加倍似乎成本高昂。 - NetMage

4
可以使用Collection的first(where:)方法从所有键值映射中找到第一个小写匹配项,然后返回此结果对应的值。
extension Dictionary where Key == String {
    func valueForKeyInsensitive<T>(key: Key) -> T? {
        let foundKey = self.keys.first { $0.compare(key, options: .caseInsensitive) == .orderedSame } ?? key
        return self[foundKey] as? T
    }
}
first(where:)是一种更加高效的过滤或遍历大型集合的方法。
参考文献:

1
这应该可以在O(1)的时间内完成工作,同时也不允许添加相同大小写不同的字符串(例如,如果您首先插入Def,则不会被DEF替换)。如果需要,它也适用于子字符串。请注意,此解决方案更具内存效率,但每次查找字符串时都需要重新计算字符串转换和哈希的成本。如果您需要频繁查找相同的值,则可能值得实现一个缓存hashValue的实现。
struct CaseInsensitiveString<T: StringProtocol>: Hashable, Equatable, CustomStringConvertible {
    var string: T

    init(_ string: T) {
        self.string = string
    }

    var description: String { get {
        return string.description
    }}

    var hashValue: Int { get {
        string.lowercased().hashValue
    } }

    func hash(into hasher: inout Hasher) {
        hasher.combine(hashValue)
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.string.compare(rhs.string, options: .caseInsensitive) == .orderedSame
    }
}

typealias SubstringCI = CaseInsensitiveString<String>

var codeMap = [SubstringCI: Int]()
let test = "Abc Def Ghi"
let testsub = test[test.firstIndex(of: "D")!...test.lastIndex(of: "f")!]
codeMap[SubstringCI(String(testsub))] = 1
print(codeMap.keys, codeMap[SubstringCI("Def")]!, codeMap[SubstringCI("def")]!)
codeMap[SubstringCI("DEF")] = 1
print(codeMap.keys, codeMap[SubstringCI("Def")]!, codeMap[SubstringCI("def")]!)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接