/** * ADIF (Amateur Data Interchange Format) Parser * Handles standard ADIF format from LoTW, DCL, and other sources * * ADIF format: value * Example: DK0MU80m20250621 */ /** * Parse ADIF data into array of QSO records * @param {string} adifData - Raw ADIF data string * @returns {Array} Array of parsed QSO records */ export function parseADIF(adifData) { const qsos = []; // Split by (case-insensitive to handle , , , etc.) const regex = new RegExp('', 'gi'); const records = adifData.split(regex); for (const record of records) { if (!record.trim()) continue; // Skip header records const trimmed = record.trim(); if (trimmed.startsWith('<') && !trimmed.includes('/gi); for (const match of matches) { const [fullMatch, fieldName, lengthStr] = match; const length = parseInt(lengthStr, 10); const valueStart = match.index + fullMatch.length; // Extract exactly 'length' characters from the string const value = record.substring(valueStart, valueStart + length); qso[fieldName.toLowerCase()] = value.trim(); } // Only add if we have at least a callsign if (Object.keys(qso).length > 0 && (qso.call || qso.callsign)) { qsos.push(qso); } } return qsos; } /** * Parse DCL API response * DCL returns JSON with an "adif" field containing ADIF data * @param {Object} response - DCL API response * @returns {Array} Array of parsed QSO records */ export function parseDCLResponse(response) { if (!response || !response.adif) { return []; } const adifData = response.adif; const qsos = parseADIF(adifData); // Map DCL-specific fields to standard names return qsos.map(qso => ({ ...qso, dcl_qsl_rcvd: qso.dcl_qsl_rcvd, dcl_qslrdate: qso.dcl_qslrdate, darc_dok: qso.darc_dok, my_darc_dok: qso.my_darc_dok, })); } /** * Normalize band name to standard format * @param {string} band - Band name * @returns {string|null} Normalized band name */ export function normalizeBand(band) { if (!band) return null; const bandMap = { '160m': '160m', '1800': '160m', '80m': '80m', '3500': '80m', '3.5mhz': '80m', '60m': '60m', '5mhz': '60m', '40m': '40m', '7000': '40m', '7mhz': '40m', '30m': '30m', '10100': '30m', '10mhz': '30m', '20m': '20m', '14000': '20m', '14mhz': '20m', '17m': '17m', '18100': '17m', '18mhz': '17m', '15m': '15m', '21000': '15m', '21mhz': '15m', '12m': '12m', '24890': '12m', '24mhz': '12m', '10m': '10m', '28000': '10m', '28mhz': '10m', '6m': '6m', '50000': '6m', '50mhz': '6m', '4m': '4m', '70000': '4m', '70mhz': '4m', '2m': '2m', '144000': '2m', '144mhz': '2m', '1.25m': '1.25m', '222000': '1.25m', '222mhz': '1.25m', '70cm': '70cm', '432000': '70cm', '432mhz': '70cm', '33cm': '33cm', '902000': '33cm', '902mhz': '33cm', '23cm': '23cm', '1296000': '23cm', '1296mhz': '23cm', }; const normalized = bandMap[band.toLowerCase()]; return normalized || band; } /** * Normalize mode name to standard format * @param {string} mode - Mode name * @returns {string} Normalized mode name */ export function normalizeMode(mode) { if (!mode) return ''; const modeMap = { 'cw': 'CW', 'ssb': 'SSB', 'lsb': 'SSB', 'usb': 'SSB', 'am': 'AM', 'fm': 'FM', 'rtty': 'RTTY', 'psk31': 'PSK31', 'psk63': 'PSK63', 'ft8': 'FT8', 'ft4': 'FT4', 'jt65': 'JT65', 'jt9': 'JT9', 'js8': 'JS8', 'mfsk': 'MFSK', 'olivia': 'OLIVIA', 'sstv': 'SSTV', 'packet': 'PACKET', 'pactor': 'PACTOR', 'winlink': 'WINLINK', 'fax': 'FAX', 'hell': 'HELL', 'tor': 'TOR', }; const normalized = modeMap[mode.toLowerCase()]; return normalized || mode.toUpperCase(); }