Initial commit
This commit is contained in:
297
resources/js/utils/ediFileValidation.ts
Normal file
297
resources/js/utils/ediFileValidation.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
PWWLO_REGEX,
|
||||
normalizeContent,
|
||||
validateCallsign,
|
||||
validateCallsignList,
|
||||
isMultiOp,
|
||||
isSingleOp,
|
||||
} from "@/utils/ediValidation";
|
||||
import i18n from "@/i18n";
|
||||
import { type EdiHeaderForm } from "@/types/edi";
|
||||
|
||||
const translate = (key: string, options?: Record<string, unknown>, fallback?: string) => {
|
||||
const value = i18n.t(key, options);
|
||||
if (fallback && (value === key || value === "")) return fallback;
|
||||
return value as string;
|
||||
};
|
||||
|
||||
export const validateEdi = (content: string, name: string): string[] => {
|
||||
const errors: string[] = [];
|
||||
const lines = normalizeContent(content).split("\n");
|
||||
const nonEmpty = lines.map((l) => l.trim()).filter((l) => l.length > 0);
|
||||
let tDateStart: number | null = null;
|
||||
let tDateEnd: number | null = null;
|
||||
let tDateStartYear: number | null = null;
|
||||
let tDateEndYear: number | null = null;
|
||||
let tDateValid = false;
|
||||
if (nonEmpty.length === 0) {
|
||||
errors.push(
|
||||
translate("edi_error_file_empty", { name }, `${name}: soubor je prázdný.`)
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!nonEmpty[0].startsWith("[REG1TEST")) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_missing_header",
|
||||
{ name },
|
||||
`${name}: chybí hlavička [REG1TEST;1].`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const headerMap: Record<string, string> = {};
|
||||
let remarksStart = -1;
|
||||
let qsoStart = -1;
|
||||
|
||||
nonEmpty.forEach((line, idx) => {
|
||||
if (line.startsWith("[Remarks")) remarksStart = idx;
|
||||
if (line.startsWith("[QSORecords")) qsoStart = idx;
|
||||
const m = line.match(/^([A-Za-z0-9]+)=(.*)$/);
|
||||
if (m) {
|
||||
headerMap[m[1]] = m[2].trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Required header fields
|
||||
const requiredFields: Array<[keyof EdiHeaderForm, string]> = [
|
||||
["TName", translate("edi_field_tname", undefined, "název soutěže (TName)")],
|
||||
["TDate", translate("edi_field_tdate", undefined, "datum závodu (TDate=YYYYMMDD;YYYYMMDD)")],
|
||||
["PCall", translate("edi_field_pcall", undefined, "volací znak (PCall)")],
|
||||
["PWWLo", translate("edi_field_pwwlo", undefined, "lokátor (PWWLo)")],
|
||||
["PSect", translate("edi_field_psect", undefined, "sekce/kategorie (PSect)")],
|
||||
["PBand", translate("edi_field_pband", undefined, "použité pásmo (PBand)")],
|
||||
["SPowe", translate("edi_field_spowe", undefined, "výkon (SPowe)")],
|
||||
["SAnte", translate("edi_field_sante", undefined, "anténa (SAnte)")],
|
||||
["CQSOP", translate("edi_field_cqsop", undefined, "QSO body (CQSOP)")],
|
||||
["CToSc", translate("edi_field_ctosc", undefined, "celkové body (CToSc)")],
|
||||
];
|
||||
requiredFields.forEach(([key, label]) => {
|
||||
if (!headerMap[key] || headerMap[key].trim() === "") {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_missing_field",
|
||||
{ name, field: label },
|
||||
`${name}: chybí ${label}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (headerMap["TDate"]) {
|
||||
const parts = headerMap["TDate"].split(";");
|
||||
const invalidCount = parts.length !== 2;
|
||||
const invalidFormat = parts.some((p) => !/^\d{8}$/.test(p));
|
||||
const invertedRange = !invalidCount && !invalidFormat && parseInt(parts[0], 10) > parseInt(parts[1], 10);
|
||||
if (invalidCount || invalidFormat || invertedRange) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_tdate_format",
|
||||
{ name },
|
||||
`${name}: TDate musí být ve formátu YYYYMMDD;YYYYMMDD a první datum nesmí být větší než druhé.`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
tDateStart = parseInt(parts[0], 10);
|
||||
tDateEnd = parseInt(parts[1], 10);
|
||||
tDateStartYear = parseInt(parts[0].slice(0, 4), 10);
|
||||
tDateEndYear = parseInt(parts[1].slice(0, 4), 10);
|
||||
tDateValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerMap["PWWLo"]) {
|
||||
const normalizedPwwlo = headerMap["PWWLo"].trim().toUpperCase();
|
||||
if (!PWWLO_REGEX.test(normalizedPwwlo)) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_pwwlo_format",
|
||||
{ name },
|
||||
`${name}: PWWLo musí mít 6 znaků ve formátu lokátoru (AA00AA).`
|
||||
)
|
||||
);
|
||||
}
|
||||
headerMap["PWWLo"] = normalizedPwwlo;
|
||||
}
|
||||
|
||||
if (headerMap["RHBBS"] && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(headerMap["RHBBS"])) {
|
||||
// měkká validace, řeší se upozorněním ve formuláři
|
||||
}
|
||||
|
||||
const pcallValidation = validateCallsign(headerMap["PCall"] ?? "");
|
||||
if (!pcallValidation.isValid) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_pcall_format",
|
||||
{ name },
|
||||
`${name}: PCall musí být validní volací znak.`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
headerMap["PCall"] = pcallValidation.normalized;
|
||||
}
|
||||
|
||||
const section = headerMap["PSect"] ?? "";
|
||||
const requiresSingle = isSingleOp(section);
|
||||
const requiresMulti = isMultiOp(section);
|
||||
if (requiresSingle || requiresMulti) {
|
||||
const rcallValidation = validateCallsign(headerMap["RCall"] ?? "");
|
||||
if (!rcallValidation.isValid) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_rcall_for_psect",
|
||||
{ name, psect: section },
|
||||
`${name}: chybí nebo je neplatné RCall pro PSect ${section}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (requiresMulti) {
|
||||
const mope1Val = validateCallsignList(headerMap["MOpe1"] ?? "");
|
||||
const mope2Val = validateCallsignList(headerMap["MOpe2"] ?? "");
|
||||
const hasAnyValid = mope1Val.hasValid || mope2Val.hasValid;
|
||||
const invalidTokens = [...mope1Val.invalidTokens, ...mope2Val.invalidTokens];
|
||||
if (!hasAnyValid) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_mope_missing",
|
||||
{ name },
|
||||
`${name}: chybí MOpe1/MOpe2 pro multi operátory.`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (invalidTokens.length > 0) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_mope_invalid",
|
||||
{ name, invalid: invalidTokens.join(", ") },
|
||||
`${name}: neplatné značky v MOpe1/MOpe2 (${invalidTokens.join(", ")}).`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (qsoStart === -1) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_missing_qso_records",
|
||||
{ name },
|
||||
`${name}: chybí sekce [QSORecords;N].`
|
||||
)
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
|
||||
const qsoHeader = nonEmpty[qsoStart];
|
||||
const m = qsoHeader.match(/^\[QSORecords;(\d+)\]/);
|
||||
const expectedCount = m ? parseInt(m[1], 10) : null;
|
||||
|
||||
const qsoLines: string[] = [];
|
||||
for (let i = qsoStart + 1; i < nonEmpty.length; i++) {
|
||||
const line = nonEmpty[i];
|
||||
if (line.startsWith("[END")) break;
|
||||
if (line.startsWith("[") && !line.startsWith("[QSORecords")) break;
|
||||
qsoLines.push(line);
|
||||
}
|
||||
if (expectedCount !== null && qsoLines.length !== expectedCount) {
|
||||
// upozornění řeší computeQsoCountWarning, tady neblokujeme upload
|
||||
}
|
||||
|
||||
let hasQsoValidationError = false;
|
||||
qsoLines.forEach((line, idx) => {
|
||||
const parts = line.split(";");
|
||||
if (parts.length < 10) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_fields_min",
|
||||
{ name, index: idx + 1, min: 10 },
|
||||
`${name}: QSO #${idx + 1} má málo polí (minimálně 10 očekáváno).`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
return;
|
||||
}
|
||||
const date = parts[0] ?? "";
|
||||
const time = parts[1] ?? "";
|
||||
const call = parts[2] ?? "";
|
||||
if (!/^\d{6}$/.test(date)) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_date_format",
|
||||
{ name, index: idx + 1 },
|
||||
`${name}: QSO #${idx + 1} má neplatné datum (YYMMDD).`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
} else if (tDateValid && tDateStart !== null && tDateEnd !== null) {
|
||||
const yy = parseInt(date.slice(0, 2), 10);
|
||||
const mmdd = date.slice(2);
|
||||
let fullYear: number | null = null;
|
||||
if (tDateStartYear !== null && tDateEndYear !== null) {
|
||||
if (tDateStartYear === tDateEndYear) {
|
||||
fullYear = tDateStartYear;
|
||||
} else if (yy === tDateStartYear % 100) {
|
||||
fullYear = tDateStartYear;
|
||||
} else if (yy === tDateEndYear % 100) {
|
||||
fullYear = tDateEndYear;
|
||||
}
|
||||
}
|
||||
if (fullYear === null) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_year_out_of_range",
|
||||
{ name, index: idx + 1 },
|
||||
`${name}: QSO #${idx + 1} má rok mimo rozsah TDate.`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
} else {
|
||||
const fullDate = parseInt(`${fullYear}${mmdd}`, 10);
|
||||
if (fullDate < tDateStart || fullDate > tDateEnd) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_date_out_of_range",
|
||||
{ name, index: idx + 1 },
|
||||
`${name}: QSO #${idx + 1} má datum mimo rozsah TDate.`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!/^\d{4}$/.test(time)) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_time_format",
|
||||
{ name, index: idx + 1 },
|
||||
`${name}: QSO #${idx + 1} má neplatný čas (HHMM UTC).`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
}
|
||||
const qsoCallValidation = validateCallsign(call);
|
||||
if (!qsoCallValidation.isValid) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_callsign",
|
||||
{ name, index: idx + 1 },
|
||||
`${name}: QSO #${idx + 1} má neplatný volací znak.`
|
||||
)
|
||||
);
|
||||
hasQsoValidationError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasQsoValidationError) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_qso_edit_in_file",
|
||||
{ name },
|
||||
`${name}: Záznamy QSO nelze upravit ve formuláři, oprav je v souboru a zkus ho nahrát znovu.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
355
resources/js/utils/ediValidation.ts
Normal file
355
resources/js/utils/ediValidation.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import { EdiHeaderForm, PSectResult } from "@/types/edi";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
const translate = (key: string, options?: Record<string, unknown>, fallback?: string) => {
|
||||
const value = i18n.t(key, options);
|
||||
if (fallback && (value === key || value === "")) return fallback;
|
||||
return value as string;
|
||||
};
|
||||
|
||||
export const PWWLO_REGEX = /^[A-R]{2}[0-9]{2}[A-X]{2}$/;
|
||||
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
export const CALLSIGN_REGEX =
|
||||
/^(?:[A-Z0-9]{1,3}\/[A-Z0-9]{1,3}[0-9][A-Z0-9]{1,4}|[A-Z0-9]{1,3}[0-9][A-Z0-9]{1,4}(?:\/[A-Z0-9]{1,4})?)$/;
|
||||
|
||||
export const normalizeContent = (content: string) => content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
|
||||
export const hasEdiHeader = (content: string) => {
|
||||
const lines = normalizeContent(content)
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0);
|
||||
return lines.length > 0 && lines[0].toUpperCase().startsWith("[REG1TEST");
|
||||
};
|
||||
|
||||
export const extractHeaderMap = (content: string) => {
|
||||
const lines = normalizeContent(content).split("\n");
|
||||
const headerMap: Record<string, string> = {};
|
||||
lines.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
const m = trimmed.match(/^([A-Za-z0-9]+)=(.*)$/);
|
||||
if (m) {
|
||||
headerMap[m[1]] = m[2].trim();
|
||||
}
|
||||
});
|
||||
return headerMap;
|
||||
};
|
||||
|
||||
export const normalizeCallsign = (raw: string) => {
|
||||
let value = raw.trim().toUpperCase().replace(/\s+/g, "");
|
||||
if (!value) return "";
|
||||
const firstSlash = value.indexOf("/");
|
||||
if (firstSlash !== -1) {
|
||||
const before = value.slice(0, firstSlash + 1);
|
||||
const after = value.slice(firstSlash + 1).replace(/\//g, "");
|
||||
value = before + after;
|
||||
} else {
|
||||
value = value.replace(/\//g, "");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const validateCallsign = (raw: string) => {
|
||||
const normalized = normalizeCallsign(raw);
|
||||
const isValid = normalized !== "" && CALLSIGN_REGEX.test(normalized);
|
||||
return { normalized, isValid };
|
||||
};
|
||||
|
||||
export const validateCallsignList = (raw: string) => {
|
||||
const tokens = raw.split(/[\s;,]+/).map((t) => t.trim()).filter((t) => t.length > 0);
|
||||
const results = tokens.map((t) => validateCallsign(t));
|
||||
const invalidTokens = tokens.filter((_, idx) => !results[idx].isValid);
|
||||
const normalizedTokens = results.map((r) => r.normalized).filter((n) => n.length > 0);
|
||||
const hasValid = results.some((r) => r.isValid);
|
||||
return { tokens, invalidTokens, normalizedTokens, hasValid };
|
||||
};
|
||||
|
||||
export type SpoweParse = { normalized: string; changed: boolean; valid: boolean };
|
||||
|
||||
export const normalizeSpowe = (raw: string): SpoweParse => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return { normalized: "", changed: false, valid: false };
|
||||
|
||||
// remove unit suffixes and prepare numeric string
|
||||
const lower = trimmed.toLowerCase();
|
||||
const compact = lower.replace(/\s+/g, "");
|
||||
let cleaned = lower;
|
||||
let factor = 1;
|
||||
if (compact.endsWith("kw")) {
|
||||
cleaned = compact.slice(0, -2);
|
||||
factor = 1000;
|
||||
} else if (compact.endsWith("mw")) {
|
||||
cleaned = compact.slice(0, -2);
|
||||
factor = 0.001;
|
||||
} else if (compact.endsWith("w")) {
|
||||
cleaned = compact.slice(0, -1);
|
||||
}
|
||||
cleaned = cleaned.trim();
|
||||
|
||||
// allow comma or dot as decimal separator
|
||||
cleaned = cleaned.replace(/,/g, ".");
|
||||
|
||||
// if nothing but digits/dot remains, parse float
|
||||
if (!/^[0-9]*\.?[0-9]+$/.test(cleaned)) {
|
||||
return { normalized: cleaned || trimmed, changed: cleaned !== trimmed, valid: false };
|
||||
}
|
||||
|
||||
const numeric = parseFloat(cleaned) * factor;
|
||||
if (Number.isNaN(numeric)) {
|
||||
return { normalized: cleaned || trimmed, changed: cleaned !== trimmed, valid: false };
|
||||
}
|
||||
|
||||
const normalized = factor === 1000 ? String(Math.round(numeric)) : String(numeric);
|
||||
const changed = normalized !== trimmed;
|
||||
return { normalized, changed, valid: true };
|
||||
};
|
||||
|
||||
export const isSingleOp = (psect: string) => {
|
||||
const lower = psect.toLowerCase();
|
||||
const patterns = ["single", "single-op", "single operator", "so", "so-lp", "sop", "sohp"];
|
||||
return patterns.some((p) => lower.includes(p));
|
||||
};
|
||||
|
||||
export const isMultiOp = (psect: string) => {
|
||||
const lower = psect.toLowerCase();
|
||||
const patterns = ["multi", "multi-op", "multi operator", "mo", "mo-lp", "mohp"];
|
||||
return patterns.some((p) => lower.includes(p));
|
||||
};
|
||||
|
||||
export const hasPowerKeyword = (psect: string) => {
|
||||
const lower = psect.toLowerCase();
|
||||
return /\b(qrp|lp)\b/.test(lower) || lower.includes("low power");
|
||||
};
|
||||
|
||||
export const computeQsoCountWarning = (content: string, name: string): string | null => {
|
||||
const lines = normalizeContent(content).split("\n");
|
||||
const nonEmpty = lines.map((l) => l.trim()).filter((l) => l.length > 0);
|
||||
let qsoStart = -1;
|
||||
nonEmpty.forEach((line, idx) => {
|
||||
if (line.startsWith("[QSORecords")) qsoStart = idx;
|
||||
});
|
||||
if (qsoStart === -1) return null;
|
||||
|
||||
const qsoHeader = nonEmpty[qsoStart];
|
||||
const m = qsoHeader.match(/^\[QSORecords;(\d+)\]/);
|
||||
const expectedCount = m ? parseInt(m[1], 10) : null;
|
||||
|
||||
const qsoLines: string[] = [];
|
||||
for (let i = qsoStart + 1; i < nonEmpty.length; i++) {
|
||||
const line = nonEmpty[i];
|
||||
if (line.startsWith("[END")) break;
|
||||
if (line.startsWith("[") && !line.startsWith("[QSORecords")) break;
|
||||
qsoLines.push(line);
|
||||
}
|
||||
|
||||
if (expectedCount !== null && qsoLines.length !== expectedCount) {
|
||||
return translate(
|
||||
"edi_warning_qso_count_mismatch",
|
||||
{ name, expected: expectedCount, actual: qsoLines.length },
|
||||
`${name}: Deklarovaný počet QSO (${expectedCount}) nesedí se skutečným (${qsoLines.length}).`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateContentWithHeaders = (content: string, updates: Partial<EdiHeaderForm>): string => {
|
||||
const normalized = normalizeContent(content);
|
||||
const lines = normalized.split("\n");
|
||||
const updatedLines = [...lines];
|
||||
const regIndex = lines.findIndex((line) => line.trim().toUpperCase().startsWith("[REG1TEST"));
|
||||
const insertionIndex = regIndex >= 0 ? regIndex + 1 : lines.length;
|
||||
const handledKeys = new Set<string>();
|
||||
|
||||
updatedLines.forEach((line, idx) => {
|
||||
const m = line.trim().match(/^([A-Za-z0-9]+)=(.*)$/);
|
||||
if (!m) return;
|
||||
const key = m[1];
|
||||
if (key in updates) {
|
||||
updatedLines[idx] = `${key}=${updates[key as keyof EdiHeaderForm] ?? ""}`;
|
||||
handledKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (handledKeys.has(key) || value === undefined) return;
|
||||
updatedLines.splice(insertionIndex, 0, `${key}=${value}`);
|
||||
});
|
||||
|
||||
return updatedLines.join("\n");
|
||||
};
|
||||
|
||||
export const parsePSect = (raw: string, opts: { strict?: boolean; allow6h?: boolean } = {}): PSectResult => {
|
||||
const strict = opts.strict ?? false;
|
||||
const allow6h = opts.allow6h ?? false;
|
||||
const normalizedBase = raw
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/\b6\s*HOURS?\b/g, "6H")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[\/,;._]+/g, " ");
|
||||
const tokensRaw = normalizedBase
|
||||
.split(" ")
|
||||
.flatMap((t) => t.split("-"))
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0)
|
||||
.map((t) =>
|
||||
t
|
||||
.replace(/^SINGLE$/, "SO")
|
||||
.replace(/^MULTI$/, "MO")
|
||||
.replace(/^LOWPOWER$/, "LP")
|
||||
.replace(/^HIGHPOWER$/, "HP")
|
||||
.replace(/^UNLIMITED$/, "A")
|
||||
.replace(/^CHECKLOG$/, "CHECK")
|
||||
);
|
||||
|
||||
let operatorClass: "SO" | "MO" | null = null;
|
||||
let powerClass: "QRP" | "N" | "LP" | "A" | null = null;
|
||||
let timeClass: "6H" | null = null;
|
||||
let isCheckLog = false;
|
||||
const unknownTokens: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const powerTokens = new Set(["QRP", "N", "LP", "A", "HP"]);
|
||||
const timeTokens = new Set(["6H"]);
|
||||
const operatorTokens = new Set(["SO", "MO", "SOLO", "MULTI"]);
|
||||
const optionalTokens = new Set(["CHECK", "SWL", "MGM", "CW", "SSB", "FM", "ALL", "FULL", "UNLIMITEDTIME", "12H", "24H", "OP"]);
|
||||
|
||||
tokensRaw.forEach((token) => {
|
||||
if (operatorTokens.has(token)) {
|
||||
if (token === "SOLO") {
|
||||
operatorClass = operatorClass ?? "SO";
|
||||
} else if (token === "MULTI") {
|
||||
operatorClass = operatorClass ?? "MO";
|
||||
} else {
|
||||
operatorClass = operatorClass ?? (token as "SO" | "MO");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (powerTokens.has(token)) {
|
||||
const mapped = token === "HP" ? "A" : token;
|
||||
if (powerClass && powerClass !== mapped) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_psect_multiple_power",
|
||||
undefined,
|
||||
"PSect obsahuje více výkonových kategorií."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
powerClass = mapped as typeof powerClass;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeTokens.has(token)) {
|
||||
if (!allow6h) {
|
||||
warnings.push(
|
||||
translate(
|
||||
"edi_warning_psect_time_not_allowed",
|
||||
undefined,
|
||||
"PSect obsahuje časovou kategorii 6H, která není pro toto kolo povolena."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (timeClass && timeClass !== token) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_psect_multiple_time",
|
||||
undefined,
|
||||
"PSect obsahuje více časových kategorií."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
timeClass = token as "6H";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (token === "CHECK") {
|
||||
isCheckLog = true;
|
||||
return;
|
||||
}
|
||||
if (optionalTokens.has(token)) {
|
||||
return;
|
||||
}
|
||||
unknownTokens.push(token);
|
||||
});
|
||||
|
||||
if (!operatorClass && !isCheckLog) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_psect_missing_operator",
|
||||
undefined,
|
||||
"PSect musí obsahovat SO nebo MO."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isCheckLog) {
|
||||
const hasAnythingElse =
|
||||
operatorClass !== null ||
|
||||
powerClass !== null ||
|
||||
timeClass !== null ||
|
||||
unknownTokens.length > 0;
|
||||
if (hasAnythingElse) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_psect_check_extra",
|
||||
undefined,
|
||||
"PSect CHECK nesmí obsahovat žádné další tokeny."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (strict && unknownTokens.length > 0) {
|
||||
errors.push(
|
||||
translate(
|
||||
"edi_error_psect_unknown_tokens",
|
||||
{ tokens: unknownTokens.join(", ") },
|
||||
`PSect obsahuje neznámé tokeny: ${unknownTokens.join(", ")}.`
|
||||
)
|
||||
);
|
||||
} else if (unknownTokens.length > 0) {
|
||||
warnings.push(
|
||||
translate(
|
||||
"edi_warning_psect_unknown_tokens",
|
||||
{ tokens: unknownTokens.join(", ") },
|
||||
`Neznámé tokeny v PSect: ${unknownTokens.join(", ")}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedParts = [
|
||||
operatorClass ?? "",
|
||||
powerClass ?? "",
|
||||
timeClass ?? "",
|
||||
isCheckLog ? "CHECK" : "",
|
||||
...unknownTokens,
|
||||
].filter((t) => t);
|
||||
|
||||
return {
|
||||
operatorClass,
|
||||
powerClass,
|
||||
timeClass,
|
||||
isCheckLog,
|
||||
unknownTokens,
|
||||
normalized: normalizedParts.join(" "),
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildIaruPsect = (res: PSectResult): string | null => {
|
||||
if (res.isCheckLog) return "CHECK";
|
||||
if (!res.operatorClass) return null;
|
||||
const parts: string[] = [res.operatorClass];
|
||||
if (res.powerClass && res.powerClass !== "A") {
|
||||
parts[0] = `${parts[0]}-${res.powerClass}`;
|
||||
}
|
||||
let base = parts.join(" ");
|
||||
if (res.timeClass === "6H") {
|
||||
base = `${base} 6H`;
|
||||
}
|
||||
return base.trim();
|
||||
};
|
||||
Reference in New Issue
Block a user