Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import akka.http.scaladsl.model.headers.`Access-Control-Allow-Origin`
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.kenkoooo.atcoder.db.SqlClient
import com.kenkoooo.atcoder.model.ApiJsonSupport
import com.kenkoooo.atcoder.model.{ApiJsonSupport, UserInfo}
import com.kenkoooo.atcoder.scraper.AtCoder.UserNameRegex

/**
Expand Down Expand Up @@ -48,6 +48,27 @@ class JsonApi(sqlClient: SqlClient) extends ApiJsonSupport {

complete(sqlClient.loadUserSubmissions(users: _*).toList)
}
} ~ pathPrefix("v2") {
path("results") {
parameters('users ? "") { users =>
val userList =
users.split(",").map(_.trim).filter(_.nonEmpty).filter(_.matches(UserNameRegex))
complete(sqlClient.loadUserSubmissions(userList: _*).toList)
}
} ~ path("user_info") {
parameters('user ? "") { userId =>
val (point, pointRank) = sqlClient.pointAndRankOf(userId)
val (count, countRank) = sqlClient.countAndRankOf(userId)
val userInfo = UserInfo(
userId = userId,
acceptedCount = count,
acceptedCountRank = countRank,
ratedPointSum = point,
ratedPointSumRank = pointRank
)
complete(userInfo)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.kenkoooo.atcoder.common

object ReducedRanker {
implicit class ReducedRankerImplicit[T](val self: List[T]) {
def reduceToMap: Map[T, Int] = {
var map = Map[T, Int]()
for ((point, index) <- self.zipWithIndex) {
if (index == 0 || point != self(index - 1)) {
map += (point -> index)
}
}
map
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kenkoooo.atcoder.db
import com.kenkoooo.atcoder.common.TypeAnnotations.UserId
import com.kenkoooo.atcoder.common.ReducedRanker._
import com.kenkoooo.atcoder.model.AcceptedCount

class AcceptedCountInfo(val list: List[AcceptedCount]) {
private val countMap: Map[UserId, Int] =
list.map(count => count.userId -> count.problemCount).toMap
private val countRank = list.map(_.problemCount).sorted.reverse.reduceToMap

def countAndRankOf(userId: UserId): (Int, Int) = {
val count = countMap.getOrElse(userId, AcceptedCountInfo.MINIMUM_COUNT)
val rank = countRank.getOrElse(count, list.size)
(count, rank)
}
}

object AcceptedCountInfo {
private val MINIMUM_COUNT = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.kenkoooo.atcoder.db
import com.kenkoooo.atcoder.common.TypeAnnotations.UserId
import com.kenkoooo.atcoder.common.ReducedRanker._
import com.kenkoooo.atcoder.model.RatedPointSum

class RatedPointSumInfo(val list: List[RatedPointSum]) {
private val pointMap: Map[UserId, Double] = list.map(sum => sum.userId -> sum.pointSum).toMap
private val pointRank = list.map(_.pointSum).sorted.reverse.reduceToMap

def pointAndRankOf(userId: UserId): (Double, Int) = {
val point = pointMap.getOrElse(userId, RatedPointSumInfo.MINIMUM_POINT)
val rank = pointRank.getOrElse(point, list.size)
(point, rank)
}
}

object RatedPointSumInfo {
private val MINIMUM_POINT = 0.0
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,22 @@ class SqlClient(url: String, user: String, password: String) extends Logging {

private var _contests: Map[ContestId, Contest] = Map()
private var _problems: Map[ProblemId, Problem] = Map()
private var _acceptedCounts: List[AcceptedCount] = List()
private var _firstSubmissionCounts: List[FirstSubmissionCount] = List()
private var _fastestSubmissionCounts: List[FastestSubmissionCount] = List()
private var _shortestSubmissionCounts: List[ShortestSubmissionCount] = List()
private var _mergedProblems: List[MergedProblem] = List()
private var _ratedPointSums: List[RatedPointSum] = List()
private var _languageCounts: List[LanguageCount] = List()
private var _predictedRatings: List[PredictedRating] = List()
private var _lastReloaded: Long = 0

private var ratedPointSumInfo = new RatedPointSumInfo(List())
private var acceptedCountInfo = new AcceptedCountInfo(List())

def contests: Map[String, Contest] = _contests

def problems: Map[String, Problem] = _problems

def acceptedCounts: List[AcceptedCount] = _acceptedCounts
def acceptedCounts: List[AcceptedCount] = acceptedCountInfo.list

def firstSubmissionCounts: List[FirstSubmissionCount] = _firstSubmissionCounts

Expand All @@ -49,14 +50,17 @@ class SqlClient(url: String, user: String, password: String) extends Logging {

def mergedProblems: List[MergedProblem] = _mergedProblems

def ratedPointSums: List[RatedPointSum] = _ratedPointSums
def ratedPointSums: List[RatedPointSum] = ratedPointSumInfo.list

def languageCounts: List[LanguageCount] = _languageCounts

def predictedRatings: List[PredictedRating] = _predictedRatings

def lastReloadedTimeMillis: Long = _lastReloaded

def pointAndRankOf(userId: UserId): (Double, Int) = ratedPointSumInfo.pointAndRankOf(userId)
def countAndRankOf(userId: UserId): (Int, Int) = acceptedCountInfo.countAndRankOf(userId)

private[db] def executeAndLoadSubmission(builder: SQLBuilder[_]): List[Submission] =
this.synchronized {
DB.readOnly { implicit session =>
Expand Down Expand Up @@ -330,12 +334,12 @@ class SqlClient(url: String, user: String, password: String) extends Logging {
_contests = loadRecords(Contest).map(s => s.id -> s).toMap
_problems = loadRecords(Problem).map(s => s.id -> s).toMap

_acceptedCounts = loadRecords(AcceptedCount).toList
acceptedCountInfo = new AcceptedCountInfo(loadRecords(AcceptedCount).toList)
_firstSubmissionCounts = loadRecords(FirstSubmissionCount).toList
_shortestSubmissionCounts = loadRecords(ShortestSubmissionCount).toList
_fastestSubmissionCounts = loadRecords(FastestSubmissionCount).toList
_mergedProblems = loadMergedProblems().toList
_ratedPointSums = loadRecords(RatedPointSum).toList
ratedPointSumInfo = new RatedPointSumInfo(loadRecords(RatedPointSum).toList)
_languageCounts = loadRecords(LanguageCount).toList
_predictedRatings = loadRecords(PredictedRating).toList

Expand Down Expand Up @@ -444,6 +448,16 @@ private object SqlClient {
private val SubmissionSyntax = Submission.syntax("s")

def concat(columns: SQLSyntax*): SQLSyntax = sqls"concat(${sqls.csv(columns: _*)})"

def reduce[T](sortedList: List[T]): Map[T, Int] = {
var map = Map[T, Int]()
for ((point, index) <- sortedList.zipWithIndex) {
if (index == 0 || point != sortedList(index - 1)) {
map += (point -> index)
}
}
map
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,12 @@ trait ApiJsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
jsonFormat(LanguageCount.apply, "user_id", "language", "count")
implicit val predictedRatingFormat: RootJsonFormat[PredictedRating] =
jsonFormat(PredictedRating.apply, "user_id", "rating")
implicit val userInfoFormat: RootJsonFormat[UserInfo] = jsonFormat(
UserInfo.apply,
"user_id",
"accepted_count",
"accepted_count_rank",
"rated_point_sum",
"rated_point_sum_rank"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kenkoooo.atcoder.model
import com.kenkoooo.atcoder.common.TypeAnnotations.UserId

case class UserInfo(userId: UserId,
acceptedCount: Int,
acceptedCountRank: Int,
ratedPointSum: Double,
ratedPointSumRank: Int)
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class JsonApiTest
when(sql.languageCounts)
.thenReturn(List(LanguageCount("user1", "Rust", 100), LanguageCount("user2", "Java", 200)))
when(sql.predictedRatings).thenReturn(List(PredictedRating("kenkoooo", 3.14)))
when(sql.pointAndRankOf("kenkoooo")).thenReturn((1.0, 2))
when(sql.countAndRankOf("kenkoooo")).thenReturn((3, 4))
}

test("return 200 to new request") {
Expand Down Expand Up @@ -227,4 +229,31 @@ class JsonApiTest
responseAs[String] shouldBe """[{"user_id":"kenkoooo","rating":3.14}]"""
}
}

test("submission api v2 with a user") {
val api = new JsonApi(sql)

Get("/v2/results?users=kenkoooo") ~> api.routes ~> check {
status shouldBe OK
verify(sql, times(1)).loadUserSubmissions("kenkoooo")
}
}

test("submission api v2 with multiple users") {
val api = new JsonApi(sql)

Get("/v2/results?users=kenkoooo,kenkoooo1,kenkoooo2") ~> api.routes ~> check {
status shouldBe OK
verify(sql, times(1)).loadUserSubmissions("kenkoooo", "kenkoooo1", "kenkoooo2")
}
}

test("user info API") {
val api = new JsonApi(sql)

Get("/v2/user_info?user=kenkoooo") ~> api.routes ~> check {
status shouldBe OK
responseAs[String] shouldBe """{"accepted_count_rank":4,"rated_point_sum_rank":2,"rated_point_sum":1.0,"user_id":"kenkoooo","accepted_count":3}"""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.kenkoooo.atcoder.common
import com.kenkoooo.atcoder.common.ReducedRanker._
import org.scalatest.{FunSuite, Matchers}

class ReducedRankerTest extends FunSuite with Matchers {
test("reduce") {
val list = List(1.0, 1.0, 2.0, 1.0, 3.0, 1.0)
val map = list.sorted.reverse.reduceToMap
map shouldBe Map(1.0 -> 2, 2.0 -> 1, 3.0 -> 0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kenkoooo.atcoder.db
import com.kenkoooo.atcoder.model.AcceptedCount
import org.scalatest.{FunSuite, Matchers}

class AcceptedCountInfoTest extends FunSuite with Matchers {
test("construct") {
val info = new AcceptedCountInfo(
List(
AcceptedCount("user1", 5),
AcceptedCount("user2", 4),
AcceptedCount("user3", 4),
AcceptedCount("user4", 9)
)
)
info.countAndRankOf("user1") shouldBe (5, 1)
info.countAndRankOf("user2") shouldBe (4, 2)
info.countAndRankOf("user3") shouldBe (4, 2)
info.countAndRankOf("user4") shouldBe (9, 0)
info.countAndRankOf("unknown_user") shouldBe (0, 4)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kenkoooo.atcoder.db
import com.kenkoooo.atcoder.model.RatedPointSum
import org.scalatest.{FunSuite, Matchers}

class RatedPointSumInfoTest extends FunSuite with Matchers {
test("construct") {
val list =
List(
RatedPointSum("user1", 100),
RatedPointSum("user2", 50),
RatedPointSum("user3", 50),
RatedPointSum("user4", 20)
)
val info = new RatedPointSumInfo(list)
info.pointAndRankOf("user1") shouldBe (100.0, 0)
info.pointAndRankOf("user2") shouldBe (50.0, 1)
info.pointAndRankOf("user3") shouldBe (50.0, 1)
info.pointAndRankOf("user4") shouldBe (20.0, 3)
info.pointAndRankOf("unknown_user") shouldBe (0.0, 4)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ class ApiJsonSupportTest extends FunSuite with Matchers with ApiJsonSupport {
| "title": "title",
| "source_code_length": 5
|}""".stripMargin
}

test("convert user info to json") {
UserInfo("kenkoooo", 114, 514, 810.0, 893).toJson.prettyPrint shouldBe
"""{
| "accepted_count_rank": 514,
| "rated_point_sum_rank": 893,
| "rated_point_sum": 810.0,
| "user_id": "kenkoooo",
| "accepted_count": 114
|}""".stripMargin
}
}