Compare commits

..

No commits in common. "a942798a6c1103b0deb770cdcc535d542a94e85d" and "876545630a13ac3413ac6f7dd9f1c33b8da29c3d" have entirely different histories.

7 changed files with 81 additions and 95 deletions

View File

@ -174,25 +174,46 @@ public class ResultService {
.distinct() .distinct()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(comb -> { .map(comb -> {
CombStat stat = makeStat(matchEntities, comb); AtomicInteger w = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger(0);
AtomicInteger pointTake = new AtomicInteger(0);
matchEntities.stream()
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> {
int win = matchModel.win();
if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0)
w.getAndIncrement();
for (ScoreEmbeddable score : matchModel.getScores()) {
if (score.getS1() <= -900 || score.getS2() <= -900)
continue;
if (matchModel.isC1(comb)) {
pointMake.addAndGet(score.getS1());
pointTake.addAndGet(score.getS2());
} else {
pointMake.addAndGet(score.getS2());
pointTake.addAndGet(score.getS1());
}
}
});
float pointRate = (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get();
return new ResultCategoryData.RankArray(0, return new ResultCategoryData.RankArray(0,
comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), stat.score, stat.w, comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY_NO_DETAILS), w.get(),
stat.pointMake, stat.pointTake, stat.getPointRate()); pointMake.get(), pointTake.get(), pointRate);
}) })
.sorted(Comparator .sorted(Comparator
.comparing(ResultCategoryData.RankArray::getScore) .comparing(ResultCategoryData.RankArray::getWin)
.thenComparing(ResultCategoryData.RankArray::getWin)
.thenComparing(ResultCategoryData.RankArray::getPointRate).reversed()) .thenComparing(ResultCategoryData.RankArray::getPointRate).reversed())
.toList(); .toList();
out.getMatchs().put(c, matchs); out.getMatchs().put(c, matchs);
int lastScore = -1;
int lastWin = -1; int lastWin = -1;
float pointRate = 0; float pointRate = 0;
int rank = 0; int rank = 0;
for (ResultCategoryData.RankArray rankArray1 : rankArray) { for (ResultCategoryData.RankArray rankArray1 : rankArray) {
if (rankArray1.getScore() != lastScore || rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) { if (rankArray1.getWin() != lastWin || pointRate != rankArray1.getPointRate()) {
lastScore = rankArray1.getScore();
lastWin = rankArray1.getWin(); lastWin = rankArray1.getWin();
pointRate = rankArray1.getPointRate(); pointRate = rankArray1.getPointRate();
rank++; rank++;
@ -254,7 +275,12 @@ public class ResultService {
.distinct() .distinct()
.map(comb -> { .map(comb -> {
var builder2 = CombsArrayData.CombsData.builder(); var builder2 = CombsArrayData.CombsData.builder();
CombStat stat = makeStat(matchModels, comb); AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
makeStat(matchModels, comb, w, l, pointMake, pointTake);
Categorie categorie = null; Categorie categorie = null;
String clubName = null; String clubName = null;
@ -276,13 +302,14 @@ public class ResultService {
builder2.cat((categorie == null) ? "---" : categorie.getName(trad)); builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY)); builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(stat.w); builder2.w(w.get());
builder2.l(stat.l); builder2.l(l.get());
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l); builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.club(clubName); builder2.club(clubName);
builder2.pointMake(stat.pointMake); builder2.pointMake(pointMake.get());
builder2.pointTake(stat.pointTake); builder2.pointTake(pointTake.get());
builder2.ratioPoint(stat.getPointRate()); builder2.ratioPoint(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
return builder2.build(); return builder2.build();
}) })
@ -407,7 +434,7 @@ public class ResultService {
} else { } else {
builder2.score(new ArrayList<>()); builder2.score(new ArrayList<>());
} }
builder2.win(matchModel.isEnd() && matchModel.win() > 0); builder2.win(matchModel.win() > 0);
} else { } else {
builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest())); builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest()));
if (matchModel.isEnd()) { if (matchModel.isEnd()) {
@ -422,9 +449,9 @@ public class ResultService {
} else { } else {
builder2.score(new ArrayList<>()); builder2.score(new ArrayList<>());
} }
builder2.win(matchModel.isEnd() && matchModel.win() < 0); builder2.win(matchModel.win() < 0);
} }
builder2.eq(matchModel.isEnd() && matchModel.win() == 0);
builder2.ratio( builder2.ratio(
(pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
@ -468,7 +495,7 @@ public class ResultService {
@Builder @Builder
@RegisterForReflection @RegisterForReflection
public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio, public static record MatchsData(Date date, String poule, String adv, List<Integer[]> score, float ratio,
boolean win, boolean eq) { boolean win) {
} }
} }
@ -539,7 +566,12 @@ public class ResultService {
List<ClubArrayData.CombData> combData = combs.stream().map(comb -> { List<ClubArrayData.CombData> combData = combs.stream().map(comb -> {
var builder2 = ClubArrayData.CombData.builder(); var builder2 = ClubArrayData.CombData.builder();
CombStat stat = makeStat(matchModels, comb); AtomicInteger w = new AtomicInteger(0);
AtomicInteger l = new AtomicInteger(0);
AtomicInteger pointMake = new AtomicInteger();
AtomicInteger pointTake = new AtomicInteger();
makeStat(matchModels, comb, w, l, pointMake, pointTake);
Categorie categorie = null; Categorie categorie = null;
@ -555,15 +587,15 @@ public class ResultService {
builder2.cat((categorie == null) ? "---" : categorie.getName(trad)); builder2.cat((categorie == null) ? "---" : categorie.getName(trad));
builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY)); builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY));
builder2.w(stat.w); builder2.w(w.get());
builder2.l(stat.l); builder2.l(l.get());
builder2.ratioVictoire((stat.l == 0) ? stat.w : (float) stat.w / stat.l); builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get());
builder2.pointMake(stat.pointMake); builder2.pointMake(pointMake.get());
builder2.pointTake(stat.pointTake); builder2.pointTake(pointTake.get());
builder2.ratioPoint(stat.getPointRate()); builder2.ratioPoint((pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get());
tt_win.addAndGet(stat.w); tt_win.addAndGet(w.get());
tt_match.addAndGet(stat.w + stat.l); tt_match.addAndGet(w.get() + l.get());
return builder2.build(); return builder2.build();
}) })
@ -583,34 +615,30 @@ public class ResultService {
return builder.build(); return builder.build();
} }
private static CombStat makeStat(List<MatchModel> matchModels, CombModel comb) { private static void makeStat(List<MatchModel> matchModels, CombModel comb, AtomicInteger w, AtomicInteger l,
CombStat stat = new CombStat(); AtomicInteger pointMake, AtomicInteger pointTake) {
matchModels.stream() matchModels.stream()
.filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb))) .filter(m -> m.isEnd() && (m.isC1(comb) || m.isC2(comb)))
.forEach(matchModel -> { .forEach(matchModel -> {
int win = matchModel.win(); int win = matchModel.win();
if (win == 0) { if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
stat.score += 1; w.getAndIncrement();
} else if ((matchModel.isC1(comb) && win > 0) || matchModel.isC2(comb) && win < 0) {
stat.w++;
stat.score += 3;
} else { } else {
stat.l++; l.getAndIncrement();
} }
matchModel.getScores().stream() matchModel.getScores().stream()
.filter(s -> s.getS1() > -900 && s.getS2() > -900) .filter(s -> s.getS1() > -900 && s.getS2() > -900)
.forEach(score -> { .forEach(score -> {
if (matchModel.isC1(comb)) { if (matchModel.isC1(comb)) {
stat.pointMake += score.getS1(); pointMake.addAndGet(score.getS1());
stat.pointTake += score.getS2(); pointTake.addAndGet(score.getS2());
} else { } else {
stat.pointMake += score.getS2(); pointMake.addAndGet(score.getS2());
stat.pointTake += score.getS1(); pointTake.addAndGet(score.getS1());
} }
}); });
}); });
return stat;
} }
@Builder @Builder
@ -624,27 +652,6 @@ public class ResultService {
} }
} }
@RegisterForReflection
public static class CombStat {
public int w;
public int l;
public int score;
public int pointMake;
public int pointTake;
public CombStat() {
this.w = 0;
this.l = 0;
this.score = 0;
this.pointMake = 0;
this.pointTake = 0;
}
public float getPointRate() {
return (pointTake == 0) ? pointMake : (float) pointMake / pointTake;
}
}
private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) { private Uni<MembreModel> hasAccess(String uuid, SecurityCtx securityCtx) {
return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid) return registerRepository.find("membre.userId = ?1 AND competition.uuid = ?2", securityCtx.getSubject(), uuid)
.firstResult() .firstResult()
@ -671,8 +678,7 @@ public class ResultService {
securityCtx.getSubject()) securityCtx.getSubject())
.chain(c2 -> { .chain(c2 -> {
if (c2 > 0) return Uni.createFrom().item(m); if (c2 > 0) return Uni.createFrom().item(m);
return Uni.createFrom() return Uni.createFrom().failure(new DForbiddenException(trad.t("access.denied")));
.failure(new DForbiddenException(trad.t("access.denied")));
}); });
} }
}); });

View File

@ -11,7 +11,6 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -33,7 +32,6 @@ public class ResultCategoryData {
public static class RankArray { public static class RankArray {
int rank; int rank;
String name; String name;
int score;
int win; int win;
int pointMake; int pointMake;
int pointTake; int pointTake;
@ -41,8 +39,7 @@ public class ResultCategoryData {
} }
@RegisterForReflection @RegisterForReflection
public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue, public record PouleArrayData(String red, boolean red_w, List<Integer[]> score, boolean blue_w, String blue, boolean end) {
boolean eq, boolean end, Date date) {
public static PouleArrayData fromModel(MatchModel matchModel, MembreModel membreModel, ResultPrivacy privacy) { public static PouleArrayData fromModel(MatchModel matchModel, MembreModel membreModel, ResultPrivacy privacy) {
return new PouleArrayData( return new PouleArrayData(
matchModel.getC1Name(membreModel, privacy), matchModel.getC1Name(membreModel, privacy),
@ -52,9 +49,7 @@ public class ResultCategoryData {
: new ArrayList<>(), : new ArrayList<>(),
matchModel.isEnd() && matchModel.win() < 0, matchModel.isEnd() && matchModel.win() < 0,
matchModel.getC2Name(membreModel, privacy), matchModel.getC2Name(membreModel, privacy),
matchModel.isEnd() && matchModel.win() == 0, matchModel.isEnd());
matchModel.isEnd(),
matchModel.getDate());
} }
} }
@ -62,8 +57,7 @@ public class ResultCategoryData {
public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores, public static record TreeData(long id, String c1FullName, String c2FullName, List<ScoreEmbeddable> scores,
boolean end) { boolean end) {
public static TreeData from(MatchModel match, MembreModel membreModel, ResultPrivacy privacy) { public static TreeData from(MatchModel match, MembreModel membreModel, ResultPrivacy privacy) {
return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), return new TreeData(match.getId(), match.getC1Name(membreModel, privacy), match.getC2Name(membreModel, privacy), match.getScores(), match.isEnd());
match.getC2Name(membreModel, privacy), match.getScores(), match.isEnd());
} }
} }
} }

View File

@ -7,9 +7,6 @@ const rootDiv = document.getElementById("safca_api_data");
const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635" const cupImg = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png" style="width: 16px;" src="https://intra.ffsaf.fr/img/171891.png"
alt="">` alt="">`
const cupImg2 = `<img decoding="async" loading="lazy" width="16" height="16" class="wp-image-1635"
style="width: 16px;" src="https://intra.ffsaf.fr/img/171892.png"
alt="">`
const voidFunction = () => { const voidFunction = () => {
@ -18,7 +15,7 @@ let lastRf = 0;
let rfFonction = voidFunction; let rfFonction = voidFunction;
setInterval(() => { setInterval(() => {
// rfFonction(); rfFonction();
}, 15000); }, 15000);
function setSubPage(name) { function setSubPage(name) {
@ -217,11 +214,11 @@ function buildMatchArray(matchs) {
arrayContent += ` arrayContent += `
<tr> <tr>
<td class="has-text-align-right" data-align="right">${match.red}</td> <td class="has-text-align-right" data-align="right">${match.red}</td>
<td class="has-text-align-center" data-align="center">${match.red_w ? cupImg : (match.eq ? cupImg2 : "")}</td> <td class="has-text-align-center" data-align="center">${match.red_w ? cupImg : ""}</td>
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td> <td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
<td class="has-text-align-center" data-align="center">${match.blue_w ? cupImg : (match.eq ? cupImg2 : "")}</td> <td class="has-text-align-center" data-align="center">${match.blue_w ? cupImg : ""}</td>
<td class="has-text-align-left" data-align="left">${match.blue}</td> <td class="has-text-align-left" data-align="left">${match.blue}</td>
<td class="has-text-align-center" data-align="center">${dateToString((match.end) ? match.date : null)}</td> <td class="has-text-align-center" data-align="center">${dateToString((match.red_w || match.blue_w) ? match.date : null)}</td>
</tr>` </tr>`
} }
arrayContent += `</tbody></table></figure>` arrayContent += `</tbody></table></figure>`
@ -237,7 +234,6 @@ function buildRankArray(rankArray) {
<tr> <tr>
<th class="has-text-align-center" data-align="center">${i18next.t('place')}</th> <th class="has-text-align-center" data-align="center">${i18next.t('place')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th> <th class="has-text-align-center" data-align="center">${i18next.t('nom')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('score')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('victoire')}</th> <th class="has-text-align-center" data-align="center">${i18next.t('victoire')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th> <th class="has-text-align-center" data-align="center">${i18next.t('ratio')}</th>
<th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th> <th class="has-text-align-center" data-align="center">${i18next.t('pointsMarqués')}</th>
@ -249,7 +245,6 @@ function buildRankArray(rankArray) {
<tr> <tr>
<td class="has-text-align-center" data-align="center">${row.rank}</td> <td class="has-text-align-center" data-align="center">${row.rank}</td>
<td class="has-text-align-left" data-align="left">${row.name}</td> <td class="has-text-align-left" data-align="left">${row.name}</td>
<td class="has-text-align-center" data-align="center">${row.score}</td>
<td class="has-text-align-center" data-align="center">${row.win}</td> <td class="has-text-align-center" data-align="center">${row.win}</td>
<td class="has-text-align-center" data-align="center">${row.pointRate.toFixed(3)}</td> <td class="has-text-align-center" data-align="center">${row.pointRate.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${row.pointMake}</td> <td class="has-text-align-center" data-align="center">${row.pointMake}</td>
@ -424,7 +419,7 @@ function buildCombView(comb) {
<td class="has-text-align-center" data-align="center">${match.adv}</td> <td class="has-text-align-center" data-align="center">${match.adv}</td>
<td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td> <td class="has-text-align-center" data-align="center">${scoreToString(match.score)}</td>
<td class="has-text-align-center" data-align="center">${match.ratio.toFixed(3)}</td> <td class="has-text-align-center" data-align="center">${match.ratio.toFixed(3)}</td>
<td class="has-text-align-center" data-align="center">${match.win ? cupImg : (match.eq ? cupImg2 : "")}</td> <td class="has-text-align-center" data-align="center">${match.win ? cupImg : ""}</td>
</tr>` </tr>`
} }
arrayContent += `</tbody></table></figure>` arrayContent += `</tbody></table></figure>`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -57,7 +57,6 @@
"rechercheParCombattant": "Search by fighter", "rechercheParCombattant": "Search by fighter",
"rouge": "Red", "rouge": "Red",
"résultatDeLaCompétition": "Competition result", "résultatDeLaCompétition": "Competition result",
"score": "Score",
"scores": "Scores", "scores": "Scores",
"statistique": "Statistics", "statistique": "Statistics",
"tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})", "tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})",

View File

@ -57,7 +57,6 @@
"rechercheParCombattant": "Recherche par combattant", "rechercheParCombattant": "Recherche par combattant",
"rouge": "Rouge", "rouge": "Rouge",
"résultatDeLaCompétition": "Résultat de la compétition", "résultatDeLaCompétition": "Résultat de la compétition",
"score": "Score",
"scores": "Scores", "scores": "Scores",
"statistique": "Statistique", "statistique": "Statistique",
"tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})", "tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})",

View File

@ -14,11 +14,6 @@ function CupImg() {
style={{width: "16px"}} src="/img/171891.png" style={{width: "16px"}} src="/img/171891.png"
alt=""/> alt=""/>
} }
function CupImg2() {
return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
alt=""/>
}
export function ResultView() { export function ResultView() {
const {uuid} = useParams() const {uuid} = useParams()
@ -92,9 +87,9 @@ function BuildMatchArray({matchs}) {
<tbody> <tbody>
{matchs.map((match, idx) => <tr key={idx}> {matchs.map((match, idx) => <tr key={idx}>
<td style={{textAlign: "right"}}>{match.red}</td> <td style={{textAlign: "right"}}>{match.red}</td>
<td style={{textAlign: "center"}}>{match.red_w ? <CupImg/> : (match.eq ? <CupImg2/> : "")}</td> <td style={{textAlign: "center"}}>{match.red_w ? <CupImg/> : ""}</td>
<td style={{textAlign: "center"}}>{scoreToString(match.score)}</td> <td style={{textAlign: "center"}}>{scoreToString(match.score)}</td>
<td style={{textAlign: "center"}}>{match.blue_w ? <CupImg/> : (match.eq ? <CupImg2/> : "")}</td> <td style={{textAlign: "center"}}>{match.blue_w ? <CupImg/> : ""}</td>
<td style={{textAlign: "left"}}>{match.blue}</td> <td style={{textAlign: "left"}}>{match.blue}</td>
</tr>)} </tr>)}
</tbody> </tbody>
@ -110,7 +105,6 @@ function BuildRankArray({rankArray}) {
<tr> <tr>
<th scope="col" style={{textAlign: "center"}}>{t('place')}</th> <th scope="col" style={{textAlign: "center"}}>{t('place')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('nom')}</th> <th scope="col" style={{textAlign: "center"}}>{t('nom')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('score')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('victoire')}</th> <th scope="col" style={{textAlign: "center"}}>{t('victoire')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('ratio')}</th> <th scope="col" style={{textAlign: "center"}}>{t('ratio')}</th>
<th scope="col" style={{textAlign: "center"}}>{t('pointsMarqués')}</th> <th scope="col" style={{textAlign: "center"}}>{t('pointsMarqués')}</th>
@ -121,7 +115,6 @@ function BuildRankArray({rankArray}) {
{rankArray.map((row, idx) => <tr key={idx}> {rankArray.map((row, idx) => <tr key={idx}>
<td style={{textAlign: "center"}}>{row.rank}</td> <td style={{textAlign: "center"}}>{row.rank}</td>
<td style={{textAlign: "left"}}>{row.name}</td> <td style={{textAlign: "left"}}>{row.name}</td>
<td style={{textAlign: "center"}}>{row.score}</td>
<td style={{textAlign: "center"}}>{row.win}</td> <td style={{textAlign: "center"}}>{row.win}</td>
<td style={{textAlign: "center"}}>{row.pointRate.toFixed(3)}</td> <td style={{textAlign: "center"}}>{row.pointRate.toFixed(3)}</td>
<td style={{textAlign: "center"}}>{row.pointMake}</td> <td style={{textAlign: "center"}}>{row.pointMake}</td>
@ -402,7 +395,7 @@ function CombResult({uuid, combId}) {
<td style={{textAlign: "center"}}>{match.adv}</td> <td style={{textAlign: "center"}}>{match.adv}</td>
<td style={{textAlign: "center"}}>{scoreToString(match.score)}</td> <td style={{textAlign: "center"}}>{scoreToString(match.score)}</td>
<td style={{textAlign: "center"}}>{match.ratio.toFixed(3)}</td> <td style={{textAlign: "center"}}>{match.ratio.toFixed(3)}</td>
<td style={{textAlign: "left"}}>{match.win ? <CupImg/> : (match.eq ? <CupImg2/> : "")}</td> <td style={{textAlign: "left"}}>{match.win ? <CupImg/> : ""}</td>
</tr>)} </tr>)}
</tbody> </tbody>
</table> </table>