298 lines
9.3 KiB
TypeScript
298 lines
9.3 KiB
TypeScript
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;
|
|
};
|