import Fluent import Vapor struct CODDate { let month:Int let year:Int let day: Int let hour:Int let minute:Int } struct StatsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let statsRoute = routes.grouped("cod-tracker","api", "stats") statsRoute.get("allMatches", use: index) statsRoute.get("totalWins", use: totalWins) statsRoute.get("totalLosses", use: totalLosses) statsRoute.get("overall", use: overall) statsRoute.get("all", use: test) statsRoute.get("allDaily", use: allDaily) statsRoute.get("test", use: test) statsRoute.post("logMatch", use: logMatch) statsRoute.get("history","page",":page", use: history) statsRoute.get("history", use: history) } func history(req: Request) throws -> EventLoopFuture { if let page = req.parameters.get("page", as: Int.self) { return Match.query(on: req.db).count().flatMap { (totalMatches) -> EventLoopFuture in let startRecord = min (page * 20, totalMatches) let lastRecord = min (startRecord + 20, totalMatches) return Match.query(on: req.db).sort(\.$date, .descending).range(startRecord.. (MatchHistory) in return MatchHistory(total:totalMatches, matches: matches, hasMorePages: lastRecord < totalMatches) } } } else { return Match.query(on: req.db).count().flatMap { (totalMatches) -> EventLoopFuture in return Match.query(on: req.db).sort(\.$date, .descending).limit(20).all().map { (matches) -> (MatchHistory) in return MatchHistory(total:totalMatches, matches: matches, hasMorePages: totalMatches > 20) } } } } func logMatch(req: Request) throws -> EventLoopFuture { let newMatch = try req.content.decode(Match.self) return newMatch.save(on: req.db).map { newMatch} } func getStats(matches:[Match]) -> Stats{ let countedMatches = matches.filter { return self.shouldCountMatch(match: $0) } let totals = countedMatches.reduce([0,0]) { (totals, match) -> [Int] in if match.win == true { return [totals[0] + 1, totals[1]] } else { return [totals[0], totals[1] + 1] } } let winCount = totals[0] let lossCount = totals[1] let ratio = self.getRatio(num: Double(winCount), den: Double(lossCount)) return Stats(winLoss: ratio, totalWins: Int(winCount), totalLosses: Int(lossCount)) } func getStatsWithMostRecentDailyRecord(matches:[Match], game:String? = nil) -> StatsWithMostRecentDailyRecord { let stats = getStats(matches: matches) let mostRecentDailyStats = self.mostRecentDailyStats(matches: matches, game: game) 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(matches: matches) let lastDayPlayed = daysPlayed.last return getStats(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) if let game = game { // if game == "mw" { // shouldInclude = shouldInclude && match.codGame == "mw" // } // else if game == "bocw" { // shouldInclude = shouldInclude && match.codGame == "bocw" // // }else { // // } shouldInclude = shouldInclude && match.codGame == game } return shouldInclude })) } private func shouldCountMatch (match:Match) -> Bool { let isColdWar = match.codGame == "bocw" let numberOfPlayers = self.numberOfPlayers(match: match) return !isColdWar || (isColdWar && (numberOfPlayers == 0 || numberOfPlayers > 4 )) } private func numberOfPlayers(match:Match) -> Int { return match.players?.components(separatedBy: ",").count ?? 0 } private func getDaysPlayed(matches:[Match]) -> [CODDate] { var sortedMatches = matches sortedMatches.sort { (match1, match2) -> Bool in return match1.date < match2.date } return sortedMatches.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) }.reduce([CODDate]()) { (datesPlayed, codDate) -> [CODDate] in if datesPlayed.contains(where: { (existingDate) -> Bool in if codDate.month == existingDate.month && codDate.year == existingDate.year && existingDate.day == codDate.day{ return true } return false }){ return datesPlayed }else { return datesPlayed + [codDate] } } } func getCumulativeWinLossRatios(matches:[Match]) -> [DataPoint] { let daysPlayed = getDaysPlayed(matches: matches) var cumulativeRatios : [DataPoint] = [] var cumulativeWins:Int = 0 var cumulativeLosses:Int = 0 var dayMatches:[[Match]] = [] var currentDay = daysPlayed.first?.day ?? 0 var currentMonth = daysPlayed.first?.month ?? 0 var currentYear = daysPlayed.first?.year ?? 0 let sortedMatches = matches.sorted { (m1, m2) -> Bool in return m1.date < m2.date } var currentMatches:[Match] = [] for match in sortedMatches { if match.date.year == currentYear && match.date.month == currentMonth && match.date.day == currentDay { currentMatches.append(match) } else { dayMatches.append(currentMatches) currentMatches = [match] currentDay = match.date.day currentYear = match.date.year currentMonth = match.date.month } } for (i, matchGroup) in dayMatches.enumerated() { let stats = self.getStats(matches: matchGroup) cumulativeWins = cumulativeWins + stats.totalWins; cumulativeLosses = cumulativeLosses + stats.totalLosses; cumulativeRatios.append( DataPoint(x: Double(i), y: (Double(cumulativeWins) / (Double(cumulativeLosses) )), label: ("\(Utilities.monthToString(month: matchGroup.first!.date.month)) \( matchGroup.first!.date.day)"))) } return cumulativeRatios } func test(req: Request) throws -> EventLoopFuture { let startTime = Date() var date = getStartDate() var previousMonths:[CODDate] = [] repeat { //let stats = getStatsByMonth(year: date.year, month: date.month, req: req) //returns eventloopfuture previousMonths.append(CODDate(month: date.month, year: date.year, day: 15, hour:6, minute: 0)) date = Calendar.current.date(byAdding: .month, value: 1, to: date)! } while ( date < Calendar.current.date(byAdding: .month, value: 1, to: Date())!) // print (date - Date().timeIntervalSince(startTime)) return Match.query(on: req.db).all().map { (matches) -> AllStats in let overallStats = self.getStatsWithMostRecentDailyRecord(matches: matches) // print ( Date().timeIntervalSince(startTime)) let mwStats = self.getStatsWithMostRecentDailyRecord(matches: matches.filter({ (match) -> Bool in return match.codGame == "mw" })) // print ( Date().timeIntervalSince(startTime)) let bocwStats = self.getStatsWithMostRecentDailyRecord(matches: matches.filter({ (match) -> Bool in return match.codGame == "bocw" })) // print ( Date().timeIntervalSince(startTime)) let mostRecentDailyStats = self.mostRecentDailyStats(matches: matches) let monthlyStats = previousMonths.reversed().map { (codDate) -> MonthStats in let relevantMatches = matches.filter { (match) -> Bool in return match.date.month == codDate.month } return MonthStats(month: codDate.month, year: codDate.year, stats: self.getStats(matches: relevantMatches )) } // print ( Date().timeIntervalSince(startTime)) // let cumulativeWinLossRatios = self.getCumulativeWinLossRatios(matches: matches) // print ( Date().timeIntervalSince(startTime)) // let highestWinLossRatio = cumulativeWinLossRatios.reduce("0") { (highestRatio, dataPoint) -> String in // if dataPoint.y > Double(highestRatio)!{ // return String(dataPoint.y) // } // return highestRatio // } // print ( Date().timeIntervalSince(startTime)) return AllStats.init(overall: overallStats,mwStats: mwStats, bocwStats: bocwStats, byMonth: monthlyStats, mostRecentRecord: "\(mostRecentDailyStats.totalWins) - \(mostRecentDailyStats.totalLosses)") } // func getMonthStats (_ remaining: ArraySlice, allMonthlyStats: inout [MonthStats], eventLoop: EventLoop) -> EventLoopFuture<[MonthStats]> { // var remaining = remaining // if let first = remaining.popLast() { // // return getstatsForMonth(year: first.year, month: first.month, req: req).flatMap { [remaining, allMonthlyStats] (stats) -> EventLoopFuture<[MonthStats]> in // var allMonthlyStats = allMonthlyStats // allMonthlyStats.append(MonthStats(month: first.month, year: first.year, stats:stats )) // return getMonthStats(remaining, allMonthlyStats:&allMonthlyStats, eventLoop: eventLoop) // } // // } else { // return req.eventLoop.makeSucceededFuture(allMonthlyStats) // } // } // // var stats:[MonthStats] = [] // let monthstats = getMonthStats(previousMonths[0.. AllStats in // // let (((overall, monthlyStats), cumulativeRatios), mostRecentDayStats) = arg // let highestWinLossRatio = cumulativeRatios.reduce("0") { (highestRatio, dataPoint) -> String in // if dataPoint.y > Double(highestRatio)!{ // return String(dataPoint.y) // } // return highestRatio // } // // return AllStats.init(overall: overall,mwStats: overall, bocwStats: overall, byMonth: monthlyStats, highestWinLossRatio: highestWinLossRatio, dataPoints: cumulativeRatios, mostRecentRecord: "\(mostRecentDayStats.totalWins) - \(mostRecentDayStats.totalLosses)") // } } func index(req: Request) throws -> EventLoopFuture<[Match]> { return Match.query(on: req.db).sort(\.$date).all() } func totalWins(req: Request) throws -> EventLoopFuture { return Match.query(on: req.db) .filter(\.$win == true) .count() } func totalLosses(req: Request) throws -> EventLoopFuture { return Match.query(on: req.db) .filter(\.$win == false) .count() } func getMarchStats(req:Request) throws -> EventLoopFuture { return getstatsForMonth(year: 2020, month: 03, req: req) } func getstatsForMonth(year:Int, month:Int, req: Request) -> EventLoopFuture{ let winCount = Match.query(on: req.db) .filter(\.$date >= getStartOfMonth(month: month, year: year)) .filter(\.$date <= getEndOfMonth(month: month, year: year)) .filter(\.$win == true ) .count() let lossCount = Match.query(on: req.db) .filter(\.$date >= getStartOfMonth(month: month, year: year)) .filter(\.$date <= getEndOfMonth(month: month, year: year)) .filter(\.$win == false ) .count() let combined = winCount.and(lossCount) return combined.map { (winCount, lossCount) -> (Stats) in let ratio:Double = (Double(winCount) / Double(lossCount)).truncate(places: 2) return Stats.init(winLoss: String(ratio), totalWins: winCount, totalLosses: lossCount) } } func getStatsForDay(year:Int, month:Int, day:Int, req: Request) -> EventLoopFuture{ let winCount = Match.query(on: req.db) .filter(\.$date >= getStartOfDay(day:day, month: month, year: year)) .filter(\.$date <= getEndOfDay(day: day, month: month, year: year)) .filter(\.$win == true ) .count() let lossCount = Match.query(on: req.db) .filter(\.$date >= getStartOfDay(day:day, month: month, year: year)) .filter(\.$date <= getEndOfDay(day: day, month: month, year: year)) .filter(\.$win == false ) .count() let combined = winCount.and(lossCount) return combined.map { (winCount, lossCount) -> (Stats) in return Stats.init(winLoss: self.getRatio(num: Double(winCount), den: Double(lossCount)), totalWins: winCount, totalLosses: lossCount) } } func statsForRecent(numberGames:Int, req:Request) -> EventLoopFuture { let winCount = Match.query(on: req.db) .sort(\.$date) .range(lower: 0, upper: numberGames) .filter(\.$win == true ) .count() let lossCount = Match.query(on: req.db) .sort(\.$date) .range(lower: 0, upper: numberGames) .filter(\.$win == false ) .count() let combined = winCount.and(lossCount) return combined.map { (winCount, lossCount) -> (Stats) in let ratio:Double = (Double(winCount) / Double(lossCount)).truncate(places: 2) return Stats.init(winLoss: String(ratio), totalWins: winCount, totalLosses: lossCount) } } private func getStartOfMonth(month:Int, year:Int) -> Date { let calendar = Calendar.current var components = DateComponents() components.timeZone = TimeZone(identifier: "GMT") components.day = 1 components.month = month components.year = year components.hour = 0 components.minute = 0 return calendar.date(from: components)! } private func getEndOfMonth(month:Int, year:Int) -> Date { let calendar = Calendar.current var components = DateComponents() components.day = -0 components.timeZone = TimeZone(identifier: "GMT") components.month = month + 1 components.year = year components.hour = 23 components.minute = 59 return calendar.date(from: components)! } private func getStartOfDay(day:Int, month:Int, year:Int) -> Date { let calendar = Calendar.current var components = DateComponents() components.timeZone = TimeZone(identifier: "GMT") components.day = day components.month = month components.year = year components.hour = 0 components.minute = 0 return calendar.date(from: components)! } private func getEndOfDay(day:Int, month:Int, year:Int) -> Date { let calendar = Calendar.current var components = DateComponents() components.timeZone = TimeZone(identifier: "GMT") components.day = day components.month = month components.year = year components.hour = 23 components.minute = 59 return calendar.date(from: components)! } private func getStartDate() -> Date { let calendar = Calendar.current var components = DateComponents() components.timeZone = TimeZone(identifier: "GMT") components.day = 10 components.month = 03 components.year = 2020 components.hour = 4 components.minute = 0 return calendar.date(from: components)! } private func createDate(day:Int, month:Int, year:Int, hour:Int, minute:Int) -> Date { let calendar = Calendar.current var components = DateComponents() components.timeZone = TimeZone(identifier: "GMT") components.day = day components.month = month components.year = year components.hour = hour components.minute = minute return calendar.date(from: components)! } func mostRecentDailyStats (req:Request) -> EventLoopFuture{ return getDaysPlayed(req: req).flatMap { (days) -> (EventLoopFuture) in return self.getStatsForDay(year: days.first?.year ?? 0, month: days.first?.month ?? 0, day: days.first?.day ?? 0, req: req) } } // func all(req: Request) throws -> EventLoopFuture { // // var date = getStartDate() // var previousMonths:[CODDate] = [] // // repeat { // //let stats = getStatsByMonth(year: date.year, month: date.month, req: req) //returns eventloopfuture // previousMonths.append(CODDate(month: date.month, year: date.year, day: 15, hour:6, minute: 0)) // date = Calendar.current.date(byAdding: .month, value: 1, to: date)! // }while ( date < Calendar.current.date(byAdding: .month, value: 1, to: Date())!) // // // func getMonthStats (_ remaining: ArraySlice, allMonthlyStats: inout [MonthStats], eventLoop: EventLoop) -> EventLoopFuture<[MonthStats]> { // var remaining = remaining // if let first = remaining.popLast() { // // return getstatsForMonth(year: first.year, month: first.month, req: req).flatMap { [remaining, allMonthlyStats] (stats) -> EventLoopFuture<[MonthStats]> in // var allMonthlyStats = allMonthlyStats // allMonthlyStats.append(MonthStats(month: first.month, year: first.year, stats:stats )) // return getMonthStats(remaining, allMonthlyStats:&allMonthlyStats, eventLoop: eventLoop) // } // // } else { // return req.eventLoop.makeSucceededFuture(allMonthlyStats) // } // } // // var stats:[MonthStats] = [] // let monthstats = getMonthStats(previousMonths[0.. AllStats in // // let (((overall, monthlyStats), cumulativeRatios), mostRecentDayStats) = arg // let highestWinLossRatio = cumulativeRatios.reduce("0") { (highestRatio, dataPoint) -> String in // if dataPoint.y > Double(highestRatio)!{ // return String(dataPoint.y) // } // return highestRatio // } // // // return AllStats.init(overall: overall,mwStats: overall, bocwStats: overall, byMonth: monthlyStats, highestWinLossRatio: highestWinLossRatio, dataPoints: cumulativeRatios, mostRecentRecord: "\(mostRecentDayStats.totalWins) - \(mostRecentDayStats.totalLosses)") // } // } // func overall(req: Request) throws -> EventLoopFuture { return Match.query(on: req.db).all().map { (matches) -> OverallStats in let overallStats = self.getStatsWithMostRecentDailyRecord(matches: matches) // print ( Date().timeIntervalSince(startTime)) let mwStats = self.getStatsWithMostRecentDailyRecord(matches: matches.filter({ (match) -> Bool in return match.codGame == "mw" && self.shouldCountMatch(match: match ) })) let bocwStats = self.getStatsWithMostRecentDailyRecord(matches: matches.filter({ (match) -> Bool in return match.codGame == "bocw" && self.shouldCountMatch(match: match ) })) let mostRecentDailyStats = self.mostRecentDailyStats(matches: matches) return OverallStats(overall: overallStats, mwStats: mwStats, bocwStats: bocwStats, mostRecentRecord: "\(mostRecentDailyStats.totalWins) - \(mostRecentDailyStats.totalLosses)") } } private func getRatio( num:Double, den:Double) -> String { var returnString = "" let deno = (den != 0) ? den : 1 returnString = String((Double(num) / Double(deno)).truncate(places: 2)) if den == 0 { returnString = returnString + "+" } return returnString } private func getDaysPlayed(req:Request) -> EventLoopFuture<[CODDate]> { return Match.query(on: req.db).sort(\.$date, .descending).all().map { (matches) -> ([CODDate]) in return matches.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) }.reduce([CODDate]()) { (datesPlayed, codDate) -> [CODDate] in if datesPlayed.contains(where: { (existingDate) -> Bool in if codDate.month == existingDate.month && codDate.year == existingDate.year && existingDate.day == codDate.day{ return true } return false }){ return datesPlayed }else { return datesPlayed + [codDate] } } } } func allDaily(req:Request) -> EventLoopFuture{ return getDaysPlayed(req: req).flatMap { (previousDays) -> (EventLoopFuture) in func getDailyStats (_ remaining: ArraySlice, allDailyStats: inout [DailyStats], eventLoop: EventLoop) -> EventLoopFuture<[DailyStats]> { var remaining = remaining if let first = remaining.popLast() { return self.getStatsForDay(year: first.year, month: first.month, day:first.day, req: req).flatMap { [remaining, allDailyStats] (stats) -> EventLoopFuture<[DailyStats]> in var allDailyStats = allDailyStats let totalWins = allDailyStats.reduce(Double(stats.totalWins)) { (total, dailyStats) -> Double in return total + Double(dailyStats.stats.totalWins) } let totalLosses = allDailyStats.reduce(Double(stats.totalLosses)) { (total, dailyStats) -> Double in return total + Double(dailyStats.stats.totalLosses) } allDailyStats.append(DailyStats(day: first.day, month: first.month, year: first.year, stats: stats, cumulativeRatio: self.getRatio(num: totalWins, den: totalLosses))) return getDailyStats(remaining, allDailyStats:&allDailyStats, eventLoop: eventLoop) } } else { return req.eventLoop.makeSucceededFuture(allDailyStats) } } var stats:[DailyStats] = [] let dailyStats = getDailyStats(Array(previousDays)[0.. AllDailyStats in return AllDailyStats(dailyStats: dailyStats.filter({ (dailyStats) -> Bool in if dailyStats.stats.totalWins == 0 && dailyStats.stats.totalLosses == 0 { return false } return true }).reversed() ) } } } func getCumulativeWinLossRatios(req:Request) -> EventLoopFuture<[DataPoint]> { // let previousDays = getDaysPlayed().reversed() 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() { return self.getStatsForDay(year: first.year, month: first.month, day:first.day, req: req).flatMap { [remaining, allDailyStats, cumulativeWinLossRatios] (stats) -> EventLoopFuture<[DataPoint]> in var allDailyStats = allDailyStats let totalWins = allDailyStats.reduce(Double(stats.totalWins)) { (total, dailyStats) -> Double in return total + Double(dailyStats.stats.totalWins) } let totalLosses = allDailyStats.reduce(Double(stats.totalLosses)) { (total, dailyStats) -> Double in return total + Double(dailyStats.stats.totalLosses) } var cumulativeWinLossRatios = cumulativeWinLossRatios if !(stats.totalWins == 0 && stats.totalLosses == 0) { let date = self.createDate(day: first.day, month: first.month, year: first.year, hour: first.hour + 6, minute:first.minute) // 6 hours to make sure we pick a time that isnt on borders of us time zones // print ("p \(date.timeIntervalSince1970)") let x = Double(cumulativeWinLossRatios.count) let d = Date(timeIntervalSince1970: date.timeIntervalSince1970) cumulativeWinLossRatios.append(DataPoint(x:x , y: (totalWins/totalLosses).truncate(places: 2), label:("\(Utilities.monthToString(month: d.month)) \(d.day)"))) } allDailyStats.append(DailyStats(day: first.day, month: first.month, year: first.year, stats: stats, cumulativeRatio: self.getRatio(num: totalWins, den: totalLosses))) return getRatios(remaining, allDailyStats:&allDailyStats, cumulativeWinLossRatios:&cumulativeWinLossRatios, eventLoop: eventLoop) } } else { return req.eventLoop.makeSucceededFuture(cumulativeWinLossRatios) } } var stats:[DailyStats] = [] var cumulativeWinLossRatios:[DataPoint] = [DataPoint]() return getRatios(Array(previousDays)[0.. Double { return Double(floor(pow(10.0, Double(places)) * self)/pow(10.0, Double(places))) } } extension Date { var month:Int { var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "GMT")! let calanderDate = calendar.dateComponents([.minute, .month, .year, .hour, .day], from: self) return calanderDate.month ?? 1 } var year:Int { var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "GMT")! let calanderDate = calendar.dateComponents([.minute, .month, .year, .hour, .day], from: self) return calanderDate.year ?? 1 } var day:Int { var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "GMT")! let calanderDate = calendar.dateComponents([.minute, .month, .year, .hour, .day], from: self) return calanderDate.day ?? 1 } var hour:Int { var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "GMT")! let calanderDate = calendar.dateComponents([.minute, .month, .year, .hour, .day], from: self) return calanderDate.hour ?? 1 } var minute:Int { var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "GMT")! let calanderDate = calendar.dateComponents([.minute, .month, .year, .hour, .day], from: self) return calanderDate.minute ?? 1 } }