Critical bug fix: ADIF parser was using case-sensitive split on '<EOR>', but LoTW returns lowercase '<eor>' tags. This caused all 242,239 QSOs to be parsed as a single giant record with fields overwriting each other, resulting in only 1 QSO being imported. Changes: - Changed EOR split from case-sensitive to case-insensitive regex - Removes all debug logging - Restored normal incremental/first-sync LoTW logic Before: 6.8MB LoTW report → 1 QSO (bug) After: 6.8MB LoTW report → All 242K+ QSOs (fixed) Also includes: - Previous fix: Added missing timeOn to LoTW duplicate detection - Previous fix: Replaced regex.exec() while loop with matchAll() for-of Tested with limited date range (2025-10-01) and confirmed 420 QSOs imported successfully. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
146 lines
4.0 KiB
JavaScript
146 lines
4.0 KiB
JavaScript
/**
|
|
* ADIF (Amateur Data Interchange Format) Parser
|
|
* Handles standard ADIF format from LoTW, DCL, and other sources
|
|
*
|
|
* ADIF format: <FIELD_NAME:length>value
|
|
* Example: <CALL:5>DK0MU<BAND:3>80m<QSO_DATE:8>20250621
|
|
*/
|
|
|
|
/**
|
|
* Parse ADIF data into array of QSO records
|
|
* @param {string} adifData - Raw ADIF data string
|
|
* @returns {Array<Object>} Array of parsed QSO records
|
|
*/
|
|
export function parseADIF(adifData) {
|
|
const qsos = [];
|
|
|
|
// Split by <EOR> (case-insensitive to handle <EOR>, <eor>, <Eor>, etc.)
|
|
const regex = new RegExp('<eor>', '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('<CALL:') && !trimmed.includes('<call:')) {
|
|
continue;
|
|
}
|
|
|
|
const qso = {};
|
|
|
|
// Use matchAll for cleaner parsing (creates new iterator for each record)
|
|
const matches = record.matchAll(/<([A-Z0-9_]+):(\d+)(?::[A-Z]+)?>/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<Object>} 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();
|
|
}
|