EGF and FFG exports
This commit is contained in:
@@ -3,21 +3,27 @@ package org.jeudego.pairgoth.api
|
||||
import com.republicate.kson.Json
|
||||
import com.republicate.kson.toJsonArray
|
||||
import org.jeudego.pairgoth.model.Criterion
|
||||
import org.jeudego.pairgoth.model.Criterion.*
|
||||
import org.jeudego.pairgoth.model.Game.Result.*
|
||||
import org.jeudego.pairgoth.model.ID
|
||||
import org.jeudego.pairgoth.model.MacMahon
|
||||
import org.jeudego.pairgoth.model.Pairable
|
||||
import org.jeudego.pairgoth.model.PairingType
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.adjustedTime
|
||||
import org.jeudego.pairgoth.model.displayRank
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.historyBefore
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||
import java.io.PrintWriter
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
import org.jeudego.pairgoth.model.Criterion.*
|
||||
import org.jeudego.pairgoth.model.Game.Result.*
|
||||
import org.jeudego.pairgoth.model.ID
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
|
||||
import java.text.DecimalFormat
|
||||
|
||||
object StandingsHandler: PairgothApiHandler {
|
||||
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
@@ -83,7 +89,7 @@ object StandingsHandler: PairgothApiHandler {
|
||||
for (crit in criteria) {
|
||||
player[crit.first] = crit.second[player.getID()] ?: 0.0
|
||||
}
|
||||
player["results"] = Json.MutableArray(List(round) { "=0" })
|
||||
player["results"] = Json.MutableArray(List(round) { "0=" })
|
||||
}
|
||||
val sortedPairables = pairables.sortedWith { left, right ->
|
||||
for (crit in criteria) {
|
||||
@@ -114,38 +120,156 @@ object StandingsHandler: PairgothApiHandler {
|
||||
val blackNum = black?.getInt("num") ?: 0
|
||||
val whiteColor = if (black == null) "" else "w"
|
||||
val blackColor = if (white == null) "" else "b"
|
||||
val handicap = if (game.handicap == 0) "" else "/h${game.handicap}"
|
||||
val handicap = if (game.handicap == 0) "" else "${game.handicap}"
|
||||
assert(white != null || black != null)
|
||||
if (white != null) {
|
||||
val mark = when (game.result) {
|
||||
UNKNOWN -> "?"
|
||||
BLACK -> "-"
|
||||
WHITE -> "+"
|
||||
JIGO -> "="
|
||||
CANCELLED -> "X"
|
||||
BOTHWIN -> "++"
|
||||
BOTHLOOSE -> "--"
|
||||
BLACK, BOTHLOOSE -> "-"
|
||||
WHITE, BOTHWIN -> "+"
|
||||
JIGO, CANCELLED -> "="
|
||||
}
|
||||
val results = white.getArray("results") as Json.MutableArray
|
||||
results[r - 1] = "$whiteColor$mark$blackNum$handicap"
|
||||
results[r - 1] =
|
||||
if (blackNum == 0) "0$mark"
|
||||
else "$blackNum$mark/$whiteColor$handicap"
|
||||
}
|
||||
if (black != null) {
|
||||
val mark = when (game.result) {
|
||||
UNKNOWN -> "?"
|
||||
BLACK -> "+"
|
||||
WHITE -> "-"
|
||||
JIGO -> "="
|
||||
CANCELLED -> "X"
|
||||
BOTHWIN -> "++"
|
||||
BOTHLOOSE -> "--"
|
||||
BLACK, BOTHWIN -> "+"
|
||||
WHITE, BOTHLOOSE -> "-"
|
||||
JIGO, CANCELLED -> "="
|
||||
}
|
||||
val results = black.getArray("results") as Json.MutableArray
|
||||
results[r - 1] = "$blackColor$mark$whiteNum$handicap"
|
||||
results[r - 1] =
|
||||
if (whiteNum == 0) "0$mark"
|
||||
else "$whiteNum$mark/$blackColor$handicap"
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedPairables.toJsonArray()
|
||||
val accept = request.getHeader("Accept")?.substringBefore(";")
|
||||
return when(accept) {
|
||||
"application/json" -> sortedPairables.toJsonArray()
|
||||
"application/egf" -> {
|
||||
exportToEGFFormat(tournament, sortedPairables, neededCriteria, response.writer)
|
||||
return null
|
||||
}
|
||||
"application/ffg" -> {
|
||||
exportToFFGFormat(tournament, sortedPairables, response.writer)
|
||||
return null
|
||||
}
|
||||
else -> ApiHandler.badRequest("invalid Accept header: $accept")
|
||||
}
|
||||
}
|
||||
|
||||
val nullMap = mapOf<ID, Double>()
|
||||
|
||||
private fun exportToEGFFormat(tournament: Tournament<*>, lines: List<Json.Object>, criteria: List<Criterion>, writer: PrintWriter) {
|
||||
val mainTime = tournament.timeSystem.mainTime
|
||||
val adjustedTime = tournament.timeSystem.adjustedTime()
|
||||
val egfClass =
|
||||
if (tournament.online) {
|
||||
when (tournament.timeSystem.type) {
|
||||
FISCHER ->
|
||||
if (mainTime >= 1800 && adjustedTime >= 3000) "D"
|
||||
else "X"
|
||||
else ->
|
||||
if (mainTime >= 2400 && adjustedTime >= 3000) "D"
|
||||
else "X"
|
||||
}
|
||||
} else {
|
||||
when (tournament.timeSystem.type) {
|
||||
FISCHER ->
|
||||
if (mainTime >= 2700 && adjustedTime >= 4500) "A"
|
||||
else if (mainTime >= 1800 && adjustedTime >= 3000) "B"
|
||||
else if (mainTime >= 1200 && adjustedTime >= 1800) "C"
|
||||
else "X"
|
||||
else ->
|
||||
if (mainTime >= 3600 && adjustedTime >= 4500) "A"
|
||||
else if (mainTime >= 2400 && adjustedTime >= 3000) "B"
|
||||
else if (mainTime >= 1500 && adjustedTime >= 1800) "C"
|
||||
else "X"
|
||||
}
|
||||
}
|
||||
val ret =
|
||||
"""
|
||||
; CL[${egfClass}]
|
||||
; EV[${tournament.name}]
|
||||
; PC[${tournament.country.lowercase()},${tournament.location}]
|
||||
; DT[${tournament.startDate},${tournament.endDate}]
|
||||
; HA[${
|
||||
if (tournament.pairing.type == PairingType.MAC_MAHON) "h${tournament.pairing.pairingParams.handicap.correction}"
|
||||
else "h9"
|
||||
|
||||
}]
|
||||
; KM[${tournament.komi}]
|
||||
; TM[${tournament.timeSystem.adjustedTime() / 60}]
|
||||
; CM[Generated by Pairgoth v0.1]
|
||||
;
|
||||
; Pl Name Rk Co Club ${ criteria.map { it.name.replace(Regex("(S?)O?(SOS|DOS)[MW]?"), "$1$2").padStart(7, ' ') }.joinToString(" ") }
|
||||
${
|
||||
lines.joinToString("\n") { player ->
|
||||
"${
|
||||
player.getString("num")!!.padStart(4, ' ')
|
||||
} ${
|
||||
"${player.getString("name")} ${player.getString("firstname")}".padEnd(30, ' ').take(30)
|
||||
} ${
|
||||
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
|
||||
} ${
|
||||
player.getString("country")!!.uppercase()
|
||||
} ${
|
||||
(player.getString("club") ?: "").padStart(4).take(4)
|
||||
} ${
|
||||
criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') }
|
||||
} ${
|
||||
player.getArray("results")!!.map {
|
||||
(it as String).padStart(8, ' ')
|
||||
}.joinToString(" ")
|
||||
}"
|
||||
}
|
||||
}
|
||||
"""
|
||||
writer.println(ret)
|
||||
}
|
||||
|
||||
private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) {
|
||||
// let's try in UTF-8
|
||||
val ret =
|
||||
""";name=${tournament.shortName}
|
||||
;date=${frDate.format(tournament.startDate)}
|
||||
;vill=${tournament.location}${if (tournament.online) "(online)" else ""}
|
||||
;comm=${tournament.name}
|
||||
;prog=Pairgoth v0.1
|
||||
;time=${tournament.timeSystem.mainTime / 60}
|
||||
;ta=${tournament.timeSystem.adjustedTime() / 60}
|
||||
;size=${tournament.gobanSize}
|
||||
;komi=${tournament.komi}
|
||||
;
|
||||
;Num Nom Prenom Niv Licence Club
|
||||
${
|
||||
lines.joinToString("\n") { player ->
|
||||
"${
|
||||
player.getString("num")!!.padStart(4, ' ')
|
||||
} ${
|
||||
"${player.getString("name")} ${player.getString("firstname")}".padEnd(24, ' ').take(24)
|
||||
} ${
|
||||
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
|
||||
} ${
|
||||
player.getString("ffg") ?: " "
|
||||
} ${
|
||||
(player.getString("club") ?: "").padStart(6).take(6)
|
||||
} ${
|
||||
player.getArray("results")!!.joinToString(" ") {
|
||||
(it as String).replace("/", "").replace(Regex("(?<=[bw])$"), "0").padStart(7, ' ')
|
||||
}
|
||||
}"
|
||||
}
|
||||
}
|
||||
"""
|
||||
writer.println(ret)
|
||||
}
|
||||
|
||||
private val numFormat = DecimalFormat("###0.#")
|
||||
private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package org.jeudego.pairgoth.ext
|
||||
|
||||
import jakarta.xml.bind.JAXBContext
|
||||
import jakarta.xml.bind.JAXBElement
|
||||
import kotlinx.datetime.LocalDate
|
||||
import java.time.LocalDate
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.opengotha.TournamentType
|
||||
import org.jeudego.pairgoth.opengotha.ObjectFactory
|
||||
@@ -13,7 +13,7 @@ import javax.xml.datatype.XMLGregorianCalendar
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val MILLISECONDS_PER_DAY = 86400000
|
||||
fun XMLGregorianCalendar.toLocalDate() = LocalDate(year, month, day)
|
||||
fun XMLGregorianCalendar.toLocalDate() = LocalDate.of(year, month, day)
|
||||
|
||||
object OpenGotha {
|
||||
|
||||
@@ -114,10 +114,10 @@ object OpenGotha {
|
||||
location = genParams.location,
|
||||
online = genParams.isBInternet ?: false,
|
||||
timeSystem = when (genParams.complementaryTimeSystem) {
|
||||
"SUDDENDEATH" -> SuddenDeath(genParams.basicTime)
|
||||
"STDBYOYOMI" -> StandardByoyomi(genParams.basicTime, genParams.stdByoYomiTime, 1) // no periods?
|
||||
"CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime, genParams.canByoYomiTime, genParams.nbMovesCanTime)
|
||||
"FISCHER" -> FischerTime(genParams.basicTime, genParams.fischerTime)
|
||||
"SUDDENDEATH" -> SuddenDeath(genParams.basicTime * 60)
|
||||
"STDBYOYOMI" -> StandardByoyomi(genParams.basicTime * 60, genParams.stdByoYomiTime, 1) // no periods?
|
||||
"CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime * 60, genParams.canByoYomiTime, genParams.nbMovesCanTime)
|
||||
"FISCHER" -> FischerTime(genParams.basicTime * 60, genParams.fischerTime)
|
||||
else -> throw Error("missing byoyomi type")
|
||||
},
|
||||
pairing = when (handParams.hdCeiling) {
|
||||
@@ -269,9 +269,9 @@ object OpenGotha {
|
||||
}
|
||||
</ByePlayer>
|
||||
<TournamentParameterSet>
|
||||
<GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) {
|
||||
<GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime / 60}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) {
|
||||
TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH"
|
||||
TimeSystem.TimeSystemType.STANDARD -> "STDBYOYOMI"
|
||||
TimeSystem.TimeSystemType.JAPANESE -> "STDBYOYOMI"
|
||||
TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI"
|
||||
TimeSystem.TimeSystemType.FISCHER -> "FISCHER"
|
||||
} }" director="" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.jeudego.pairgoth.model
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
|
||||
|
||||
@@ -15,7 +14,7 @@ data class TimeSystem(
|
||||
val stones: Int
|
||||
) {
|
||||
companion object {}
|
||||
enum class TimeSystemType { CANADIAN, STANDARD, FISCHER, SUDDEN_DEATH }
|
||||
enum class TimeSystemType { CANADIAN, JAPANESE, FISCHER, SUDDEN_DEATH }
|
||||
}
|
||||
|
||||
fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
|
||||
@@ -30,7 +29,7 @@ fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
|
||||
|
||||
fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) =
|
||||
TimeSystem(
|
||||
type = STANDARD,
|
||||
type = JAPANESE,
|
||||
mainTime = mainTime,
|
||||
increment = 0,
|
||||
byoyomi = byoyomi,
|
||||
@@ -86,9 +85,16 @@ fun TimeSystem.Companion.fromJson(json: Json.Object) =
|
||||
|
||||
fun TimeSystem.toJson() = when (type) {
|
||||
TimeSystem.TimeSystemType.CANADIAN -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
|
||||
TimeSystem.TimeSystemType.STANDARD -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
|
||||
TimeSystem.TimeSystemType.JAPANESE -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
|
||||
TimeSystem.TimeSystemType.FISCHER ->
|
||||
if (maxTime == Int.MAX_VALUE) Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment)
|
||||
else Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
|
||||
TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("type" to type.name, "mainTime" to mainTime)
|
||||
}
|
||||
|
||||
fun TimeSystem.adjustedTime() = when (type) {
|
||||
TimeSystem.TimeSystemType.CANADIAN -> mainTime + 60 * byoyomi / stones
|
||||
TimeSystem.TimeSystemType.JAPANESE -> mainTime + 45 * byoyomi
|
||||
TimeSystem.TimeSystemType.FISCHER -> mainTime + 120 * increment
|
||||
TimeSystem.TimeSystemType.SUDDEN_DEATH -> mainTime
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package org.jeudego.pairgoth.model
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import com.republicate.kson.toJsonArray
|
||||
import kotlinx.datetime.LocalDate
|
||||
import java.time.LocalDate
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||
import org.jeudego.pairgoth.pairing.solver.SwissSolver
|
||||
@@ -169,8 +169,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
|
||||
type = type,
|
||||
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
|
||||
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
|
||||
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
|
||||
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
|
||||
startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"),
|
||||
endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"),
|
||||
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
|
||||
location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
|
||||
online = json.getBoolean("online") ?: default?.online ?: false,
|
||||
@@ -187,8 +187,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
|
||||
type = type,
|
||||
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
|
||||
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
|
||||
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
|
||||
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
|
||||
startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"),
|
||||
endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"),
|
||||
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
|
||||
location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
|
||||
online = json.getBoolean("online") ?: default?.online ?: false,
|
||||
|
@@ -218,8 +218,12 @@ class ApiServlet: HttpServlet() {
|
||||
"Missing 'Accept' header"
|
||||
)
|
||||
// CB TODO 1) a reference to a specific API call at this point is a code smell.
|
||||
// 2) there will e other content types: .tou, .h9, .html
|
||||
if (!isJson(accept) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+")))) throw ApiException(
|
||||
// 2) there will be other content types: .tou, .h9, .html
|
||||
if (!isJson(accept) &&
|
||||
(!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+"))) &&
|
||||
(accept != "application/ffg" && accept != "application/egf" || !request.requestURI.matches(Regex("/api/tour/\\d+/standings/\\d+")))
|
||||
|
||||
) throw ApiException(
|
||||
HttpServletResponse.SC_BAD_REQUEST,
|
||||
"Invalid 'Accept' header"
|
||||
)
|
||||
|
Reference in New Issue
Block a user