在Swift中按每个ID的最大值过滤/排序

nkhmeac6  于 12个月前  发布在  Swift
关注(0)|答案(4)|浏览(165)

我有一个对象数组,结构如下:

{
  id: String,
  userId: String,
  distance: Double,
  time: Double
}

字符串
在数组中,同一个userId在同一个距离上可以有多个条目。例如,数组可以是:

[
  { id: "1", userId: "1", distance: 100, time: 18.7 },
  { id: "2", userId: "1", distance: 100, time: 18.5 }, 
  { id: "3", userId: "2", distance: 100, time: 18.2 },
  { id: "4", userId: "2", distance: 200, time: 41.0 },
]


我想过滤和排序数组,使数组只包含每个用户在一定距离内的最快时间。所以如果我想使用上面的数组查找100次,它只会返回:

[
  { id: "3", userId: "2", distance: 100, time: 18.2 },
  { id: "2", userId: "1", distance: 100, time: 18.5 },
]


我知道我可以使用.filter将它减少到100次,使用.sort将时间按顺序排列,但我不知道如何将每个userId限制为1个条目。

6tdlim6h

6tdlim6h1#

您可以使用我添加到swift-algorithms(Apple托管的第一方包,就像Swift标准库的附件)的新grouped(by:)函数。
我们可以根据比赛的距离和参加比赛的用户对所有的比赛结果进行分组。在每组结果中,我们将选择一个最快的时间(该用户在该距离内)。我称之为userPersonalRecords
从那里,我们可以通过距离过滤,得到每个人最好的100米短跑。
如果我们经常这样做,可能值得按距离对比赛结果进行分组,并保留结果数组。或者,如果你有 * 很多 * 结果,你可以将所有这些转储到一个小SQLite DB中,并根据需要使用查询进行相同的简单分组/排序。

struct RaceResult {
    let id: String
    let userId: String
    let distance: Double
    let time: Double
}

let raceResults = [
    RaceResult(id: "1", userId: "1", distance: 100, time: 18.7),
    RaceResult(id: "2", userId: "1", distance: 100, time: 18.5),
    RaceResult(id: "3", userId: "2", distance: 100, time: 18.2),
    RaceResult(id: "4", userId: "2", distance: 200, time: 41.0),
]

// This simple little struct is just a workaround for tuples not being Hashable
struct UserDistance: Hashable {
    let userId: String
    let distance: Double
}

let userPersonalRecords = raceResults
    .grouped {
        UserDistance(userId: $0.userId, distance: $0.distance)
    }
    .mapValues { raceResults in raceResults.max(by: { $0.time < $1.time })! }
    .values

let fastest100MTimes = userPersonalRecords.filter { $0.distance == 100 }

for raceResult in fastest100MTimes {
    print(raceResult)
}

字符串

mrwjdhj3

mrwjdhj32#

虽然像sortfiltermap这样的操作可能很有用,并且可以简化代码,但它们都将遍历数组。
使用数组"操作",你需要:

  • filter阵列仅包括所需的距离
  • sort数组的用户ID和时间
  • 使用compactMap只返回每个用户的第一次(由于排序,您知道每个用户的第一次是最快的)
  • 按时间对"first times"数组排序

这涉及到迭代数组(或它的子集)两次,并排序两次。

struct DistanceTime {
    let id: String
    let userId: String
    let distance: Double
    let time: Double
}

let times = [DistanceTime(id: "1", userId: "1", distance: 100, time: 18.7), DistanceTime(id: "2", userId: "1", distance: 100, time : 18.5),
DistanceTime(id: "3", userId: "2", distance: 100, time: 18.2), DistanceTime(id: "4", userId: "2", distance: 200, time: 41.0)]

func fastestTimesMap(for distance: Double, from distanceTimes:[DistanceTime]) -> [DistanceTime] {
    
    let filteredTimes = distanceTimes.filter { dt in
        return dt.distance == distance
    }
    
    let sortedTimes = filteredTimes.sorted { dt1, dt2 in
        if dt1.userId != dt2.userId {
            return dt1.userId < dt2.userId
        } else {
            return dt1.time < dt2.time
        }
    }
    
    var currentUser: String? = nil
    
    let output = sortedTimes.compactMap { dt in 
        if dt.userId != currentUser {
            currentUser = dt.userId
            return dt
        } else {
            return nil
        }
    }
    
    return output.sorted { dt1, dt2 in
        return dt1.time < dt2.time
    }
}

let output = fastestTimesMap(for: 100, from: times)

字符串
另一种方法是使用for循环来将数组转换为:

  • 过滤您感兴趣的距离
  • 确定每个用户的最快时间

循环完成后,你仍然需要对减少的数组按time排序,以找到最快的时间:

func fastestTimes(for distance: Double, from distanceTimes:[DistanceTime]) -> [DistanceTime] {
    var times=[String:DistanceTime]()
    for dt in distanceTimes {
        if dt.distance == distance {
            if (times[dt.userId] == nil) {
                times[dt.userId] = dt
            }
            if dt.time < times[dt.userId]!.time {
                times[dt.userId] = dt
            }
        }
    }
    return Array(times.values).sorted { dt1, dt2 in 
        return dt1.time < dt2.time
    }
}

let output = fastestTimes(for: 100, from: times)


它不仅减少了代码,而且您现在只需遍历整个数组一次,并对减少的数组进行排序一次。
你可以使用reduce代替for循环,但我认为这会导致代码不太清晰,而且没有性能优势。

uemypmqf

uemypmqf3#

有很多方法可以解决这个问题。
我可能会将数据重构为以userID为键的结构体字典。
如果距离只是一个小的集合,(例如50 M,100 M,500 M,1000 M,10,000 M)我可能会为每个距离创建带有种族数组的结构体:

struct Race {
   let id: Int
   let time: Double
}

struct RaceUser {
    let userID: String
    let fiftyRaces: [Race]
    let hundredRaces: [Race]
    let fiveHundredRaces: [Race]
    let thousandRaces: [Race]
    let tenThousandRaces: [Race]
}

字符串
然后,如果你想让用户拥有最快的gift meter时间,你只需遍历所有的键/值对,比较fiftyRaces中该用户的第一个条目,寻找时间最短的那个。

drnojrws

drnojrws4#

下面是我的解决方案,使用reduce(into:),只迭代数组一次

let selectedDistance = 100.0

let bestResults = array.reduce(into: [String: Object]()) {
    if $1.distance == selectedDistance {
        let old = $0[$1.userId]
        if old == nil || old!.time > $1.time { $0[$1.userId] = $1 }
    }
}.values

字符串

相关问题