diff --git a/Sources/App/Controllers/StatsController.swift b/Sources/App/Controllers/StatsController.swift index 3333994..a034c09 100644 --- a/Sources/App/Controllers/StatsController.swift +++ b/Sources/App/Controllers/StatsController.swift @@ -37,12 +37,19 @@ struct StatsController: RouteCollection { let adamAffectedMatches = Match.query(on: db).filter(\.$finalKillRuinedPlayerId == 6).count() let totalMWGames = Match.query(on: db).filter(\.$codGame == "mw").count() - return (statistics.and(adamAffectedMatches).and(totalMWGames)).map { arg -> (DashboardStats) in + + let colourSchemeConfigs = ColourScheme.query(on: db).all().map { colourScheme in + return colourScheme.map { cs in + return cs.colorSchemeConfig + } + } + + return (statistics.and(adamAffectedMatches).and(totalMWGames).and(colourSchemeConfigs)).map { arg -> (DashboardStats) in - let (((statistics, adamAffectedMatches), totalMWGames)) = arg + let ((((statistics, adamAffectedMatches), totalMWGames),colors)) = arg - // return statistics.map { statistics in + // return statistics.map { statistics in return DashboardStats(dashboardItems: (statistics.map({ statisticItem in @@ -51,12 +58,14 @@ struct StatsController: RouteCollection { var content1 = "" var content2 = "" var sortOrder = 0 + var backgroundColor = "484848" if statisticItem.codTrackerId == "no_hyder_overall"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 100 + } else if statisticItem.codTrackerId == "mw_overall"{ @@ -64,29 +73,33 @@ struct StatsController: RouteCollection { content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 20 - + backgroundColor = colors.first(where: {$0.gameId == "mw"})?.tileColour ?? backgroundColor + + } else if statisticItem.codTrackerId == "mw_six_players"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 60 + backgroundColor = colors.first(where: {$0.gameId == "mw"})?.tileColour ?? backgroundColor + - } else if statisticItem.codTrackerId == "2021_overall"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 50 - + } else if statisticItem.codTrackerId == "bocw_overall"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 30 - + backgroundColor = colors.first(where: {$0.gameId == "bocw"})?.tileColour ?? backgroundColor + } else if statisticItem.codTrackerId == "with_hyder_overall"{ title2 = "Ratio" @@ -99,15 +112,16 @@ struct StatsController: RouteCollection { content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 70 - + backgroundColor = colors.first(where: {$0.gameId == "mw"})?.tileColour ?? backgroundColor + } else if statisticItem.codTrackerId == "casual_overall"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 40 - - + + } else if statisticItem.codTrackerId == "overall_four_players"{ title2 = "Ratio" @@ -128,35 +142,54 @@ struct StatsController: RouteCollection { sortOrder = 90 } - + else if statisticItem.codTrackerId == "bocw_nuketown_halloween"{ title2 = "Ratio" content1 = "\(statisticItem.wins)-\(statisticItem.losses)" content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) sortOrder = 41 + backgroundColor = "CC5500" } + else if statisticItem.codTrackerId == "vg_casual"{ + title2 = "Ratio" + content1 = "\(statisticItem.wins)-\(statisticItem.losses)" + content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) + sortOrder = -80 + backgroundColor = colors.first(where: {$0.gameId == "vg"})?.tileColour ?? backgroundColor + + } + + else if statisticItem.codTrackerId == "bocw_casual"{ + title2 = "Ratio" + content1 = "\(statisticItem.wins)-\(statisticItem.losses)" + content2 = Utils.getRatio(wins: statisticItem.wins, losses: statisticItem.losses) + sortOrder = -79 + backgroundColor = colors.first(where: {$0.gameId == "bocw"})?.tileColour ?? backgroundColor + } + else { } - return - - DashboardItem(codTrackerId:statisticItem.codTrackerId, title: title, content: content1, title2: title2, content2: content2, sortOrder: sortOrder, backgroundColor: statisticItem.codTrackerId == "bocw_nuketown_halloween" ? "FFA500" : "004999") + return DashboardItem(codTrackerId:statisticItem.codTrackerId, title: title, content: content1, title2: title2, content2: content2, sortOrder: sortOrder, backgroundColor:backgroundColor) } ) + - [ - DashboardItem(codTrackerId: "adam_ruined_final_kills", title: "Final Kills Ruined by Adam", content: "\(adamAffectedMatches + 7)", title2: "", content2: "",sortOrder: 1000), - DashboardItem(codTrackerId:"total_mw_games", title: "Total MW Games", content: "\(totalMWGames)", title2: "", content2: "", sortOrder: -10), - ] - ).sorted{$0.sortOrder < $1.sortOrder} + [ + DashboardItem(codTrackerId: "adam_ruined_final_kills", title: "Final Kills Ruined by Adam", content: "\(adamAffectedMatches + 7)", title2: "", content2: "",sortOrder: 1000, backgroundColor: "484848"), + DashboardItem(codTrackerId:"total_mw_games", title: "Total MW Games", content: "\(totalMWGames)", title2: "", content2: "", sortOrder: -10, backgroundColor: colors.first(where: {$0.gameId == "mw"})?.tileColour ?? "484848" + + + ), + ] + ).sorted{$0.sortOrder < $1.sortOrder} ) } } - + func dashboard(req: Request) throws -> EventLoopFuture { - + return try dashboard(db: req.db) } @@ -217,7 +250,7 @@ struct StatsController: RouteCollection { } func getAllMatches(matches:[Match]) -> Stats{ - + let totals = matches.reduce([0,0]) { (totals, match) -> [Int] in if match.win == true { return [totals[0] + 1, totals[1]] @@ -235,35 +268,35 @@ struct StatsController: RouteCollection { } func getStatsWithMostRecentDailyRecord(sortedMatches:[Match], game:String? = nil) -> StatsWithMostRecentDailyRecord { - + let stats = getCountedMatches(matches: sortedMatches) //print ("MRR STATS \(Date().timeIntervalSince(startTime))") - + let mostRecentDailyStats = self.mostRecentDailyStats(matches: sortedMatches, game: game) //print ("MRR DAILY \(Date().timeIntervalSince(startTime))") - + let ret = StatsWithMostRecentDailyRecord(winLoss: stats.winLossRatio, totalWins: stats.totalWins, totalLosses: stats.totalLosses, mostRecentRecord:"\(mostRecentDailyStats.totalWins)-\(mostRecentDailyStats.totalLosses)") return ret } - + func mostRecentDailyStats (matches:[Match], game:String? = nil) -> Stats{ - + let daysPlayed = getDaysPlayed(sortedMatches: matches) let lastDayPlayed = daysPlayed.last //print ("MDD days played \(Date().timeIntervalSince(startTime))") - + return getCountedMatches(matches: matches.filter({ (match) -> Bool in var shouldInclude = - match.date.day == lastDayPlayed?.day && - match.date.month == lastDayPlayed?.month && - match.date.year == lastDayPlayed?.year && - self.shouldCountMatch(match: match) + match.date.day == lastDayPlayed?.day && + match.date.month == lastDayPlayed?.month && + match.date.year == lastDayPlayed?.year && + self.shouldCountMatch(match: match) if let game = game { shouldInclude = shouldInclude && match.codGame == game @@ -278,7 +311,7 @@ struct StatsController: RouteCollection { let isColdWar = match.codGame == "bocw" let numberOfPlayers = self.numberOfPlayers(match: match) - + if match.competitive == false { return false } @@ -295,7 +328,7 @@ struct StatsController: RouteCollection { } } else { - + if numberOfPlayers > 4 || numberOfPlayers == 0 { return true } @@ -308,7 +341,7 @@ struct StatsController: RouteCollection { } } - } + } func getStatsForYear(year:Int, db: Database) -> EventLoopFuture{ @@ -321,7 +354,7 @@ struct StatsController: RouteCollection { dateComponents.timeZone = TimeZone(abbreviation: "EST") // Japan Standard Time dateComponents.hour = 8 dateComponents.minute = 0 - + // Create date from components let userCalendar = Calendar(identifier: .gregorian) // since the components above (like year 1980) are for Gregorian let startDate = userCalendar.date(from: dateComponents) @@ -335,10 +368,10 @@ struct StatsController: RouteCollection { endDateComponenents.timeZone = TimeZone(abbreviation: "EST") // Japan Standard Time endDateComponenents.hour = 8 endDateComponenents.minute = 0 - + let endDate = userCalendar.date(from: endDateComponenents) - + return Match.query(on: db).filter(\.$date > startDate!).filter(\.$date < endDate!).all().map { (matches) -> (Stats) in return self.getCountedMatches(matches: matches) @@ -352,7 +385,7 @@ struct StatsController: RouteCollection { private func getDaysPlayed(sortedMatches:[Match]) -> [CODDate] { - + let dates = sortedMatches.suffix(30).map { (match) -> CODDate in return CODDate(month: match.date.month, year: match.date.year, day: match.date.day, hour: match.date.hour, minute: match.date.minute) } @@ -441,7 +474,7 @@ struct StatsController: RouteCollection { return getstatsForMonth(year: 2020, month: 03, req: req) } - + func getstatsForMonth(year:Int, month:Int, req: Request) -> EventLoopFuture{ @@ -467,14 +500,19 @@ struct StatsController: RouteCollection { func getCasualStats( db: Database) -> EventLoopFuture{ - + return Match.query(on: db).filter(\.$competitive == false).all().map { matches in return Stats(totalWins: matches.filter{$0.win == true}.count, totalLosses: matches.filter{$0.win == false}.count) } } - + func getCasualStatsForGame( db: Database, gameId:String, afterDate:Date = Date(timeIntervalSince1970: 0)) -> EventLoopFuture{ + return Match.query(on: db).filter(\.$competitive == false).filter(\.$codGame == gameId).filter(\.$date > afterDate ).all().map { matches in + return Stats(totalWins: matches.filter{$0.win == true}.count, totalLosses: matches.filter{$0.win == false}.count) + } + } + @@ -608,18 +646,18 @@ struct StatsController: RouteCollection { return self.getStatsForDay(year: days.first?.year ?? 0, month: days.first?.month ?? 0, day: days.first?.day ?? 0, req: req) } } - + func recalc(req:Request) -> EventLoopFuture<[String:Stats]> { return forceCalculatedStats(db: req.db) } func forceCalculatedStats(db: Database) -> EventLoopFuture<[String:Stats]> { - + let statsWithHyder = statsWithPlayer(db: db, playerId: 5) let statsWithoutHyder = statsWithoutPlayer(db: db, playerId: 5) let statsFor2020 = getStatsForYear(year: 2020, db: db) let statsFor2021 = getStatsForYear(year: 2021, db: db) - + let hyderFuture = statsWithHyder.and(statsWithoutHyder) @@ -629,32 +667,37 @@ struct StatsController: RouteCollection { let casualOverall = getCasualStats(db: db) + let bocwCasual = getCasualStatsForGame(db: db, gameId: "bocw") + let vgCasual = getCasualStatsForGame(db: db, gameId: "vg", afterDate: Date(timeIntervalSince1970: 1635823473)) //nov 21 to exlude beta time in sept + let matches = Match.query(on: db).sort( \.$date).all() - return matches.and(hyderStats).and(statsFor2020).and(statsFor2021).and(casualOverall).map { arg -> ([String:Stats]) in + return matches.and(hyderStats).and(statsFor2020).and(statsFor2021).and(casualOverall).and(bocwCasual).and(vgCasual).map { arg -> ([String:Stats]) in - let ((((matches, hyderStats), statsFor2020), statsFor2021), casualOverall) = arg + let ((((((matches, hyderStats), statsFor2020), statsFor2021), casualOverall), bocwCasual), vgCasual) = arg //print ("got matches \(Date().timeIntervalSince(startTime))") - + let queue = DispatchQueue(label: "com.sledsoft.cod-tracker.queue", attributes: .concurrent) let group = DispatchGroup() var overallStats:StatsWithMostRecentDailyRecord? var mwStats:StatsWithMostRecentDailyRecord? - + var bocwStats:StatsWithMostRecentDailyRecord? var mostRecentStats:Stats? var mwSixPlayers:Stats? var mwFivePlayers:Stats? var overallFourPlayers:Stats? var blackOpsColdWarNuketownHalloween:Stats? + -// var mapStats:[Int:Stats]? -// var worstMap:Int? -// var bestMap:Int? -// + + // var mapStats:[Int:Stats]? + // var worstMap:Int? + // var bestMap:Int? + // group.enter() queue.async { overallStats = self.getStatsWithMostRecentDailyRecord(sortedMatches: matches) @@ -678,7 +721,7 @@ struct StatsController: RouteCollection { return match.codGame == "bocw" && self.shouldCountMatch(match: match ) })) - + group.leave() } @@ -694,7 +737,7 @@ struct StatsController: RouteCollection { mwFivePlayers = self.getCountedMatchesByPlayerCount(matches: matches.filter{$0.codGame == "mw"}, playerCount: 5) group.leave() } - + group.enter() queue.async { @@ -707,10 +750,10 @@ struct StatsController: RouteCollection { blackOpsColdWarNuketownHalloween = self.getRecordForMapId(matches: matches.filter{$0.codGame == "bocw"}, mapId: "69") group.leave() } - - + + group.wait() - + return [ "bocw_nuketown_halloween":Stats(totalWins: blackOpsColdWarNuketownHalloween!.totalWins, totalLosses: blackOpsColdWarNuketownHalloween!.totalLosses), "mw_overall":Stats(totalWins: mwStats!.totalWins, totalLosses: mwStats!.totalLosses), @@ -724,30 +767,29 @@ struct StatsController: RouteCollection { "mw_five_players":Stats(totalWins: mwFivePlayers!.totalWins, totalLosses: mwFivePlayers!.totalLosses), "overall":Stats(totalWins: overallStats!.totalWins, totalLosses: overallStats!.totalLosses), "casual_overall":Stats(totalWins: casualOverall.totalWins, totalLosses: casualOverall.totalLosses), - - - + "bocw_casual":Stats(totalWins: bocwCasual.totalWins, totalLosses: bocwCasual.totalLosses), + "vg_casual":Stats(totalWins: vgCasual.totalWins, totalLosses: vgCasual.totalLosses), ] } } - + func overall(db: Database) throws -> EventLoopFuture { - - + + let dashboardStats = try dashboard(db:db) - + let matches = Match.query(on: db).sort( \.$date).all() - let mapConfigs = getMapConfigs(db: db) + let mapConfigs = getMapConfigs(db: db) return matches.and(dashboardStats).and(mapConfigs).map { arg -> (OverallStats) in let ((matches, dashboardStats),mapConfigs) = arg - + let queue = DispatchQueue(label: "com.sledsoft.cod-tracker.queue", attributes: .concurrent) let group = DispatchGroup() @@ -755,43 +797,43 @@ struct StatsController: RouteCollection { var worstMap:Int? var bestMap:Int? -// + // group.enter() queue.async { - mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) - //print ("maps done \(Date().timeIntervalSince(startTime))") + mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) + //print ("maps done \(Date().timeIntervalSince(startTime))") group.leave() - - } -// - group.enter() - queue.async { - let mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) - - bestMap = self.getBestMap(records: mapStats) - group.leave() - } - - group.enter() - queue.async { - let mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) - - worstMap = self.getWorstMap(records: mapStats) - group.leave() - } - - group.wait() - - let dashboardItems:[DashboardItem] = - (dashboardStats.dashboardItems + - [ - DashboardItem(codTrackerId:"best_map_overall", title: "Best Map", content: mapConfigs.first{$0.mapId == bestMap}?.name ?? "error", title2: "Ratio", content2: "\(mapStats![bestMap!]!.winLossRatio) \(mapStats![bestMap!]!.record)", sortOrder: 12), + } + // + group.enter() + queue.async { + let mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) + + bestMap = self.getBestMap(records: mapStats) + group.leave() + } + + group.enter() + queue.async { + let mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) + + worstMap = self.getWorstMap(records: mapStats) + group.leave() + } + + group.wait() + + let dashboardItems:[DashboardItem] = + + (dashboardStats.dashboardItems + + [ + DashboardItem(codTrackerId:"best_map_overall", title: "Best Map", content: mapConfigs.first{$0.mapId == bestMap}?.name ?? "error", title2: "Ratio", content2: "\(mapStats![bestMap!]!.winLossRatio) \(mapStats![bestMap!]!.record)", sortOrder: 12), DashboardItem(codTrackerId:"worst_map_overall", title: "Worst Map", content: mapConfigs.first{$0.mapId == worstMap}?.name ?? "error", title2: "Ratio", content2: "\(mapStats![worstMap!]!.winLossRatio) \(mapStats![worstMap!]!.record)",sortOrder: 13), - - ]).sorted{ - $0.sortOrder < $1.sortOrder - } + + ]).sorted{ + $0.sortOrder < $1.sortOrder + } return OverallStats(dashboardItems: dashboardItems) } } @@ -801,7 +843,7 @@ struct StatsController: RouteCollection { return try overall(db: req.db) - + } func getMapConfigs(db: Database) -> EventLoopFuture<[MapConfig]> { @@ -817,26 +859,26 @@ struct StatsController: RouteCollection { func mapRecords(req: Request) throws -> EventLoopFuture<[MapRecord]> { - + let matches = Match.query(on: req.db).all() let mapConfigs = getMapConfigs(db: req.db) return matches.and(mapConfigs).map { matches, mapConfigs in let mapStats:[Int:Stats] - + if let game = req.parameters.get("game", as: String.self), let competitive = req.parameters.get("competitive", as:Bool.self) { - - let gameMode = req.parameters.get("gamemode", as:Int.self) ?? -2 - mapStats = self.getMapStats(matches: matches,game: game, competitive: competitive, gameMode: gameMode) - + + let gameMode = req.parameters.get("gamemode", as:Int.self) ?? -2 + mapStats = self.getMapStats(matches: matches,game: game, competitive: competitive, gameMode: gameMode) + } else { - mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) + mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) } - + let sortedMaps = self.mapsSortedByBest(records: mapStats) let records = sortedMaps.map { (mapId) -> MapRecord in @@ -850,51 +892,51 @@ struct StatsController: RouteCollection { //print("\(record.map.name) \(record.stats.record) \(record.ratio)") wins = wins + Double(record.stats.totalWins) loss = loss + Double(record.stats.totalLosses) - + } - return records - + return records + } - -// -// return Match.query(on: req.db).all().map { (matches) -> [MapRecord] in -// -// -// -// let mapStats:[Int:Stats] -// -// if let game = req.parameters.get("game", as: String.self), -// let competitive = req.parameters.get("competitive", as:Bool.self) { -// -// let gameMode = req.parameters.get("gamemode", as:Int.self) ?? -2 -// mapStats = self.getMapStats(matches: matches,game: game, competitive: competitive, gameMode: gameMode) -// -// } -// else { -// mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) -// } -// -// -// let sortedMaps = self.mapsSortedByBest(records: mapStats) -// -// let records = sortedMaps.map { (mapId) -> MapRecord in -// return MapRecord(map: MapData.allMaps[mapId]!, stats: mapStats[mapId]!, ratio:mapStats[mapId]!.winLossRatio) -// } -// -// var wins:Double = 0 -// var loss:Double = 0 -// -// for record in records { -// //print("\(record.map.name) \(record.stats.record) \(record.ratio)") -// wins = wins + Double(record.stats.totalWins) -// loss = loss + Double(record.stats.totalLosses) -// -// } -// return records -// } + + // + // return Match.query(on: req.db).all().map { (matches) -> [MapRecord] in + // + // + // + // let mapStats:[Int:Stats] + // + // if let game = req.parameters.get("game", as: String.self), + // let competitive = req.parameters.get("competitive", as:Bool.self) { + // + // let gameMode = req.parameters.get("gamemode", as:Int.self) ?? -2 + // mapStats = self.getMapStats(matches: matches,game: game, competitive: competitive, gameMode: gameMode) + // + // } + // else { + // mapStats = self.getMapStats(matches: matches,game: "mw", competitive: true) + // } + // + // + // let sortedMaps = self.mapsSortedByBest(records: mapStats) + // + // let records = sortedMaps.map { (mapId) -> MapRecord in + // return MapRecord(map: MapData.allMaps[mapId]!, stats: mapStats[mapId]!, ratio:mapStats[mapId]!.winLossRatio) + // } + // + // var wins:Double = 0 + // var loss:Double = 0 + // + // for record in records { + // //print("\(record.map.name) \(record.stats.record) \(record.ratio)") + // wins = wins + Double(record.stats.totalWins) + // loss = loss + Double(record.stats.totalLosses) + // + // } + // return records + // } } func mapsSortedByBest (records :[ Int:Stats] ) -> [ Int ] { @@ -904,16 +946,16 @@ struct StatsController: RouteCollection { } func getCountedMatchesByPlayerCount(matches:[Match], playerCount:Int) -> Stats { - return getCountedMatches(matches: matches.filter{$0.playerList.count == playerCount}) + return getCountedMatches(matches: matches.filter{$0.playerList.count == playerCount}) } func getRecordForMapId(matches:[Match], mapId:String) -> Stats { - return getAllMatches(matches: matches.filter{$0.map == mapId}) + return getAllMatches(matches: matches.filter{$0.map == mapId}) } func getAllMatchesByPlayerCount(matches:[Match], playerCount:Int) -> Stats { - return getAllMatches(matches: matches.filter{$0.playerList.count == playerCount}) + return getAllMatches(matches: matches.filter{$0.playerList.count == playerCount}) } func getBestMap (records :[ Int:Stats] ) -> Int { @@ -952,7 +994,7 @@ struct StatsController: RouteCollection { return true } } - + for match in filteredMatches { @@ -966,18 +1008,18 @@ struct StatsController: RouteCollection { if let map = match.map, let mapInt = Int(map) { if mapStats[mapInt] == nil { - mapStats[mapInt] = Stats(totalWins: 0, totalLosses: 0) + mapStats[mapInt] = Stats(totalWins: 0, totalLosses: 0) } if match.win { - - mapStats[mapInt]?.totalWins += 1 + + mapStats[mapInt]?.totalWins += 1 } else{ mapStats[mapInt]?.totalLosses += 1 } } - + } return mapStats } @@ -1064,7 +1106,7 @@ struct StatsController: RouteCollection { } return true }).reversed() - + ) } } @@ -1086,9 +1128,9 @@ struct StatsController: RouteCollection { func getCumulativeWinLossRatios(req:Request) -> EventLoopFuture<[DataPoint]> { - - return getDaysPlayed(req: req).flatMap { (previousDays) -> (EventLoopFuture<[DataPoint]>) in + return getDaysPlayed(req: req).flatMap { (previousDays) -> (EventLoopFuture<[DataPoint]>) in + func getRatios (_ remaining: ArraySlice, allDailyStats: inout [DailyStats], cumulativeWinLossRatios: inout [DataPoint], eventLoop: EventLoop) -> EventLoopFuture<[DataPoint]> { var remaining = remaining if let first = remaining.popLast() {