From dc6345c0b317ad630101cfb84b09b06a02770e63 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 7 Jul 2025 22:31:46 +0800 Subject: [PATCH] core&ui: fix contest with more than 26 problems have wrong alphabetic id (#1008) Co-authored-by: undefined --- packages/hydrooj/src/handler/contest.ts | 6 +++--- packages/hydrooj/src/model/contest.ts | 10 ++++++---- packages/ui-default/backendlib/template.ts | 4 ++-- packages/ui-default/pages/contest_balloon.page.tsx | 5 +++-- packages/ui-default/templates/components/contest.html | 2 +- packages/ui-default/templates/components/problem.html | 2 +- packages/ui-default/templates/contest_manage.html | 2 +- packages/ui-default/templates/contest_problemlist.html | 2 +- .../ui-default/templates/partials/contest_balloon.html | 2 +- packages/ui-default/templates/problem_detail.html | 4 ++-- packages/utils/lib/common.ts | 9 +++++++++ 11 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index e7afaa1b4..1d7075747 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -5,7 +5,7 @@ import { escapeRegExp, pick } from 'lodash'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; import { - Counter, diffArray, randomstring, sortFiles, Time, yaml, + Counter, diffArray, getAlphabeticId, randomstring, sortFiles, Time, yaml, } from '@hydrooj/utils/lib/utils'; import { Context, Service } from '../context'; import { @@ -132,7 +132,7 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'problem_detail', - displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, + displayName: `${getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, checker: () => 'pdoc' in this, }, @@ -791,7 +791,7 @@ export async function apply(ctx: Context) { const teamIds: Record = {}; for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); - const pid = (i: number) => String.fromCharCode(65 + i); + const pid = (i: number) => getAlphabeticId(i); const escape = (i: string) => i.replace(/[",]/g, ''); const unknownSchool = this.translate('Unknown School'); const statusMap = { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 2ee3f7dcf..b86f95b66 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,6 +1,8 @@ import { sumBy } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; -import { Counter, formatSeconds, Time } from '@hydrooj/utils/lib/utils'; +import { + Counter, formatSeconds, getAlphabeticId, Time, +} from '@hydrooj/utils/lib/utils'; import { ContestAlreadyAttendedError, ContestNotFoundError, ContestScoreboardHiddenError, ValidationError, @@ -158,7 +160,7 @@ const acm = buildContestRule({ } else { columns.push({ type: 'problem', - value: String.fromCharCode(65 + i - 1), + value: getAlphabeticId(i - 1), raw: pid, }); } @@ -324,7 +326,7 @@ const oi = buildContestRule({ } else { columns.push({ type: 'problem', - value: String.fromCharCode(65 + i - 1), + value: getAlphabeticId(i - 1), raw: tdoc.pids[i - 1], }); } @@ -699,7 +701,7 @@ const homework = buildContestRule({ } else { columns.push({ type: 'problem', - value: String.fromCharCode(65 + i - 1), + value: getAlphabeticId(i - 1), raw: pid, }); } diff --git a/packages/ui-default/backendlib/template.ts b/packages/ui-default/backendlib/template.ts index 6813830de..e4bd8850c 100644 --- a/packages/ui-default/backendlib/template.ts +++ b/packages/ui-default/backendlib/template.ts @@ -1,6 +1,6 @@ import path from 'path'; import * as status from '@hydrooj/common/status'; -import { findFileSync } from '@hydrooj/utils/lib/utils'; +import { findFileSync, getAlphabeticId } from '@hydrooj/utils/lib/utils'; import { avatar, Context, difficultyAlgorithm, fs, PERM, PRIV, Service, STATUS, yaml, } from 'hydrooj'; @@ -130,7 +130,7 @@ class Nunjucks extends nunjucks.Environment { this.addGlobal('instanceof', (a, b) => a instanceof b); this.addGlobal('paginate', misc.paginate); this.addGlobal('size', misc.size); - this.addGlobal('utils', { status }); + this.addGlobal('utils', { status, getAlphabeticId }); this.addGlobal('avatarUrl', avatar); this.addGlobal('formatSeconds', misc.formatSeconds); this.addGlobal('model', global.Hydro.model); diff --git a/packages/ui-default/pages/contest_balloon.page.tsx b/packages/ui-default/pages/contest_balloon.page.tsx index 0ab3b351c..29c4aca2a 100644 --- a/packages/ui-default/pages/contest_balloon.page.tsx +++ b/packages/ui-default/pages/contest_balloon.page.tsx @@ -1,4 +1,5 @@ /* eslint-disable react-refresh/only-export-components */ +import { getAlphabeticId } from '@hydrooj/utils/lib/common'; import yaml from 'js-yaml'; import React from 'react'; import { HexColorInput, HexColorPicker } from 'react-colorful'; @@ -31,8 +32,8 @@ function Balloon({ tdoc, val }) { {now === pid - ? ({String.fromCharCode(65 + tdoc.pids.indexOf(+pid))}) - : ({String.fromCharCode(65 + tdoc.pids.indexOf(+pid))})} + ? ({getAlphabeticId(tdoc.pids.indexOf(+pid))}) + : ({getAlphabeticId(tdoc.pids.indexOf(+pid))})} {%- endif -%} {% if show_pid %} -{%- if tdoc and alphabetic -%}{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} +{%- if tdoc and alphabetic -%}{{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }} {%- elif pdoc.pid and pdoc.pid.includes('-') -%}{{ pdoc.pid.split('-').join('#') }} {%- else -%}{{ pdoc.pid|default(pdoc.docId) }}{%- endif -%} {% endif %} diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index a7af2a9a9..4753a4184 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -127,7 +127,7 @@ {% for pid in tdoc.pids %} - + {% endfor %} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index cf90d0ece..8f5b8e90f 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -185,7 +185,7 @@ {% for pid in tdoc.pids %} - + {% endfor %} diff --git a/packages/ui-default/templates/partials/contest_balloon.html b/packages/ui-default/templates/partials/contest_balloon.html index 23e44b43c..d39462f4b 100644 --- a/packages/ui-default/templates/partials/contest_balloon.html +++ b/packages/ui-default/templates/partials/contest_balloon.html @@ -25,7 +25,7 @@ {% endif %} - {{ String.fromCharCode(65+tdoc.pids.indexOf(bdoc.pid)) }}  ({{ tdoc.balloon[bdoc.pid].name }}) + {{ utils.getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)) }}  ({{ tdoc.balloon[bdoc.pid].name }}) {{ user.render_inline(udict[bdoc.uid], badge=false) }} diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 670e71b0a..81429dd4a 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -64,7 +64,7 @@ {% endif %} {%- if tdoc -%} - {{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} + {{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }} {%- elif pdoc.pid and pdoc.pid.includes('-') -%} {{ pdoc.pid.split('-').join('#') }} {%- else -%} @@ -80,7 +80,7 @@ - {{ String.fromCharCode(65+loop.index0) }} + {{ utils.getAlphabeticId(loop.index0) }} {% if status %}{% endif %} {%- endfor %} diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 3c6397093..99f1bd23c 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -236,3 +236,12 @@ export function sortFiles(files: Record[] | string[], key = '_id') }); return result.map((x) => (isString ? x.name : (delete x._weights && x))); } + +export const getAlphabeticId = (() => { + // A...Z, AA...AZ, BA...BZ, ... + const f = (a: number) => (a < 0 ? '' : f(a / 26 - 1) + String.fromCharCode((a % 26) + 65)) as string; + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const cache = alphabet.split(''); + for (const ch of alphabet) cache.push(...alphabet.split('').map((c) => ch + c)); + return (i: number) => cache[i] || (i < 0 ? '?' : f(i)); +})();