refactor: Improve remark decoding robustness with data-driven parsing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
+135
-123
@@ -257,7 +257,8 @@
|
||||
'SC': 'Stratocumulus',
|
||||
'ST': 'Stratus',
|
||||
'CU': 'Cumulus',
|
||||
'CB': 'Cumulonimbus'
|
||||
'CB': 'Cumulonimbus',
|
||||
'TCU': 'Towering cumulus'
|
||||
};
|
||||
const specialRemarks = {
|
||||
'FROIN': 'Frontal passage in vicinity',
|
||||
@@ -270,130 +271,141 @@
|
||||
'S': 'South', 'SW': 'Southwest', 'W': 'West', 'NW': 'Northwest'
|
||||
};
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part.startsWith('SLP')) {
|
||||
decoded.push(` - ${decodeSlp(part)}`);
|
||||
} else if (obscurationRegex.test(part)) {
|
||||
decoded.push(` - ${decodeObscurationRemark(part)}`);
|
||||
} else if (part.match(/^([A-Z]{2}\d)+$/)) {
|
||||
decoded.push(` - ${decodeCloudTypesRemark(part, metarString)}`);
|
||||
} else if (part === 'VIA' && i + 1 < parts.length) {
|
||||
const location = parts[i+1];
|
||||
decoded.push(` - VIA ${location}: relayed via ${location}`);
|
||||
i++; // Consume location
|
||||
} else if (part === 'VIS' && i + 2 < parts.length) {
|
||||
const direction = parts[i+1];
|
||||
const distance = parts[i+2];
|
||||
decoded.push(` - VIS ${direction} ${distance}: Sector visibility to the ${direction} of ${distance} statute miles`);
|
||||
i += 2;
|
||||
} else if (part === 'VIRGA' && i + 1 < parts.length && parts[i+1] === 'ALQDS') {
|
||||
decoded.push(` - VIRGA ALQDS: Virga in all quadrants`);
|
||||
i++; // Consume ALQDS
|
||||
} else if (part === 'VIRGA' && i + 1 < parts.length && directions[parts[i+1]]) {
|
||||
const direction = directions[parts[i+1]];
|
||||
decoded.push(` - VIRGA ${parts[i+1]}: Virga to the ${direction.toLowerCase()}`);
|
||||
i++; // Consume direction part
|
||||
} else if (part === 'CVCTV' && i + 2 < parts.length && parts[i+1] === 'CLD' && parts[i+2] === 'EMBD') {
|
||||
decoded.push(` - CVCTV CLD EMBD: Convective cloud embedded in stratiform layer`);
|
||||
i += 2; // consume CLD and EMBD
|
||||
} else if (part === 'TCU' && i + 1 < parts.length && parts[i+1] === 'ASOCTD') {
|
||||
let remark = ' - TCU ASOCTD: Towering cumulus associated';
|
||||
let consumed = 1;
|
||||
if (i + 2 < parts.length && directions[parts[i+2]]) {
|
||||
const direction = directions[parts[i+2]];
|
||||
remark += ` to the ${direction.toLowerCase()}`;
|
||||
consumed = 2;
|
||||
}
|
||||
decoded.push(remark);
|
||||
i += consumed;
|
||||
} else if (part === 'TCU' && i + 1 < parts.length && parts[i+1] === 'ALQDS') {
|
||||
decoded.push(` - TCU ALQDS: Towering cumulus in all quadrants`);
|
||||
i++; // Consume ALQDS
|
||||
} else if (part === 'TCU' && i + 2 < parts.length && parts[i+1] === 'DSNT' && directions[parts[i+2]]) {
|
||||
const direction = directions[parts[i+2]];
|
||||
decoded.push(` - TCU DSNT ${parts[i+2]}: Towering cumulus distant ${direction.toLowerCase()}`);
|
||||
i += 2; // Consume DSNT and direction
|
||||
} else if (part === 'DENSITY' && i + 2 < parts.length && parts[i+1] === 'ALT' && parts[i+2].endsWith('FT')) {
|
||||
const altFeet = parts[i+2];
|
||||
const altitude = parseInt(altFeet.replace('FT', '')).toLocaleString();
|
||||
decoded.push(` - DENSITY ALT ${altFeet}: Density altitude ${altitude} feet`);
|
||||
i += 2; // consume ALT and altFeet
|
||||
} else if (cloudTypes[part] && i + 1 < parts.length && parts[i+1] === 'LENT') {
|
||||
const cloudName = cloudTypes[part];
|
||||
let remark = ` - ${part} LENT: ${cloudName} lenticularis`;
|
||||
let consumed = 1;
|
||||
if (i + 2 < parts.length && directions[parts[i+2]]) {
|
||||
const direction = directions[parts[i+2]];
|
||||
remark = ` - ${part} LENT ${parts[i+2]}: ${cloudName} lenticularis to the ${direction.toLowerCase()}`;
|
||||
consumed = 2;
|
||||
}
|
||||
decoded.push(remark);
|
||||
i += consumed;
|
||||
} else if (cloudTypes[part] && i + 1 < parts.length && parts[i+1] === 'ASOCTD') {
|
||||
const cloudName = cloudTypes[part];
|
||||
let remark = ` - ${part} ASOCTD: ${cloudName} associated`;
|
||||
let consumed = 1;
|
||||
if (i + 3 < parts.length && parts[i+2] === '/' && parts[i+3] === 'HALO') {
|
||||
remark = ` - ${part} ASOCTD / HALO: ${cloudName} associated with Halo phenomenon`;
|
||||
consumed = 3;
|
||||
} else if (i + 2 < parts.length && directions[parts[i+2]]) {
|
||||
const direction = directions[parts[i+2]];
|
||||
remark += ` to the ${direction.toLowerCase()}`;
|
||||
consumed = 2;
|
||||
}
|
||||
decoded.push(remark);
|
||||
i += consumed;
|
||||
} else if (cloudTypes[part] && i + 1 < parts.length && parts[i+1] === 'ALQDS') {
|
||||
const cloudName = cloudTypes[part];
|
||||
decoded.push(` - ${part} ALQDS: ${cloudName} in all quadrants`);
|
||||
i++; // Consume ALQDS
|
||||
} else if (cloudTypes[part] && i + 1 < parts.length && directions[parts[i+1]]) {
|
||||
const cloudName = cloudTypes[part];
|
||||
let remark = ` - ${part} ${parts[i+1]}: ${cloudName} to the ${directions[parts[i+1]].toLowerCase()}`;
|
||||
let consumed = 1;
|
||||
if (i + 2 < parts.length && parts[i+2] === 'MOV' && i + 3 < parts.length && directions[parts[i+3]]) {
|
||||
remark += `, moving ${directions[parts[i+3]].toLowerCase()}`;
|
||||
consumed = 3;
|
||||
}
|
||||
decoded.push(remark);
|
||||
i += consumed;
|
||||
} else if (specialRemarks[part]) {
|
||||
decoded.push(` - ${part}: ${specialRemarks[part]}`);
|
||||
} else if (cloudTypes[part] && i + 1 < parts.length && parts[i + 1] === 'TR') {
|
||||
const cloudName = cloudTypes[part];
|
||||
decoded.push(` - ${part} ${parts[i+1]}: ${cloudName} clouds are translucent (thin)`);
|
||||
i++; // Consume 'TR' part
|
||||
} else if (weatherRegex.test(part)) {
|
||||
decoded.push(` - ${decodeWeather(part, metarString)}`);
|
||||
} else if (part.match(/^(AC|CI|CC)/)) {
|
||||
// This is a cloud-like remark, e.g. ACC for Altocumulus Castellanus
|
||||
// It can have modifiers like other clouds.
|
||||
const cloudName = cloudTypes[part] || part; // part will be ACC
|
||||
let remark = ` - ${cloudName}: Cloud types and coverage details`;
|
||||
let consumed = 0;
|
||||
|
||||
if (i + 1 < parts.length && parts[i+1] === 'TR') {
|
||||
remark = ` - ${cloudName} TR: ${cloudName} clouds are translucent (thin)`;
|
||||
consumed = 1;
|
||||
} else if (i + 1 < parts.length && parts[i+1] === 'ASOCTD') {
|
||||
remark = ` - ${cloudName} ASOCTD: ${cloudName} associated`;
|
||||
consumed = 1;
|
||||
if (i + 2 < parts.length && directions[parts[i+2]]) {
|
||||
remark += ` to the ${directions[parts[i+2]].toLowerCase()}`;
|
||||
consumed = 2;
|
||||
const decoders = [
|
||||
// SLP
|
||||
{
|
||||
match: p => p[0].startsWith('SLP'),
|
||||
decode: p => ({ consumed: 1, text: ` - ${decodeSlp(p[0])}` })
|
||||
},
|
||||
// Obscuration
|
||||
{
|
||||
match: p => obscurationRegex.test(p[0]),
|
||||
decode: p => ({ consumed: 1, text: ` - ${decodeObscurationRemark(p[0])}` })
|
||||
},
|
||||
// Cloud types summary
|
||||
{
|
||||
match: p => p[0].match(/^([A-Z]{2}\d)+$/),
|
||||
decode: (p, ms) => ({ consumed: 1, text: ` - ${decodeCloudTypesRemark(p[0], ms)}` })
|
||||
},
|
||||
// VIA
|
||||
{
|
||||
match: p => p[0] === 'VIA' && p.length > 1,
|
||||
decode: p => ({ consumed: 2, text: ` - VIA ${p[1]}: relayed via ${p[1]}` })
|
||||
},
|
||||
// VIS
|
||||
{
|
||||
match: p => p[0] === 'VIS' && p.length > 2,
|
||||
decode: p => ({ consumed: 3, text: ` - VIS ${p[1]} ${p[2]}: Sector visibility to the ${p[1]} of ${p[2]} statute miles` })
|
||||
},
|
||||
// VIRGA
|
||||
{
|
||||
match: p => p[0] === 'VIRGA',
|
||||
decode: p => {
|
||||
if (p.length > 1 && p[1] === 'ALQDS') {
|
||||
return { consumed: 2, text: ` - VIRGA ALQDS: Virga in all quadrants` };
|
||||
}
|
||||
} else if (i + 1 < parts.length && directions[parts[i+1]]) {
|
||||
remark = ` - ${cloudName} ${parts[i+1]}: ${cloudName} to the ${directions[parts[i+1]].toLowerCase()}`;
|
||||
consumed = 1;
|
||||
}
|
||||
decoded.push(remark);
|
||||
i += consumed;
|
||||
} else if (part === '/') {
|
||||
// separator, ignore
|
||||
if (p.length > 1 && directions[p[1]]) {
|
||||
return { consumed: 2, text: ` - VIRGA ${p[1]}: Virga to the ${directions[p[1]].toLowerCase()}` };
|
||||
}
|
||||
return { consumed: 1, text: ` - ${p[0]}: ${specialRemarks[p[0]]}` };
|
||||
}
|
||||
},
|
||||
// CVCTV CLD EMBD
|
||||
{
|
||||
match: p => p[0] === 'CVCTV' && p.length > 2 && p[1] === 'CLD' && p[2] === 'EMBD',
|
||||
decode: p => ({ consumed: 3, text: ` - CVCTV CLD EMBD: Convective cloud embedded in stratiform layer` })
|
||||
},
|
||||
// DENSITY ALT
|
||||
{
|
||||
match: p => p[0] === 'DENSITY' && p.length > 2 && p[1] === 'ALT' && p[2].endsWith('FT'),
|
||||
decode: p => {
|
||||
const altFeet = p[2];
|
||||
const altitude = parseInt(altFeet.replace('FT', '')).toLocaleString();
|
||||
return { consumed: 3, text: ` - DENSITY ALT ${altFeet}: Density altitude ${altitude} feet` };
|
||||
}
|
||||
},
|
||||
// Cloud remarks
|
||||
{
|
||||
match: p => cloudTypes[p[0]] || p[0] === 'ACC',
|
||||
decode: p => {
|
||||
const cloudCode = p[0];
|
||||
const cloudName = cloudTypes[cloudCode] || (cloudCode === 'ACC' ? 'Altocumulus Castellanus' : cloudCode);
|
||||
let consumed = 1;
|
||||
let text = ` - ${cloudCode}: ${cloudName}`;
|
||||
|
||||
if (p.length > 1) {
|
||||
const mod = p[1];
|
||||
if (mod === 'LENT') {
|
||||
text = ` - ${cloudCode} LENT: ${cloudName} lenticularis`;
|
||||
consumed = 2;
|
||||
if (p.length > 2 && directions[p[2]]) {
|
||||
text += ` to the ${directions[p[2]].toLowerCase()}`;
|
||||
consumed = 3;
|
||||
}
|
||||
} else if (mod === 'ASOCTD') {
|
||||
text = ` - ${cloudCode} ASOCTD: ${cloudName} associated`;
|
||||
consumed = 2;
|
||||
if (p.length > 3 && p[2] === '/' && p[3] === 'HALO') {
|
||||
text = ` - ${cloudCode} ASOCTD / HALO: ${cloudName} associated with Halo phenomenon`;
|
||||
consumed = 4;
|
||||
} else if (p.length > 2 && directions[p[2]]) {
|
||||
text += ` to the ${directions[p[2]].toLowerCase()}`;
|
||||
consumed = 3;
|
||||
}
|
||||
} else if (mod === 'ALQDS') {
|
||||
text = ` - ${cloudCode} ALQDS: ${cloudName} in all quadrants`;
|
||||
consumed = 2;
|
||||
} else if (mod === 'TR') {
|
||||
text = ` - ${cloudCode} TR: ${cloudName} clouds are translucent (thin)`;
|
||||
consumed = 2;
|
||||
} else if (mod === 'DSNT' && p.length > 2 && directions[p[2]]) {
|
||||
text = ` - ${cloudCode} DSNT ${p[2]}: ${cloudName} distant ${directions[p[2]].toLowerCase()}`;
|
||||
consumed = 3;
|
||||
} else if (directions[mod]) {
|
||||
text = ` - ${cloudCode} ${mod}: ${cloudName} to the ${directions[mod].toLowerCase()}`;
|
||||
consumed = 2;
|
||||
if (p.length > 3 && p[2] === 'MOV' && directions[p[3]]) {
|
||||
text += `, moving ${directions[p[3]].toLowerCase()}`;
|
||||
consumed = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { consumed, text };
|
||||
}
|
||||
},
|
||||
// Simple remarks
|
||||
{
|
||||
match: p => specialRemarks[p[0]],
|
||||
decode: p => ({ consumed: 1, text: ` - ${p[0]}: ${specialRemarks[p[0]]}` })
|
||||
},
|
||||
// Weather
|
||||
{
|
||||
match: p => weatherRegex.test(p[0]),
|
||||
decode: (p, ms) => ({ consumed: 1, text: ` - ${decodeWeather(p[0], ms)}` })
|
||||
},
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
while (i < parts.length) {
|
||||
const remainingParts = parts.slice(i);
|
||||
let result = null;
|
||||
|
||||
for (const decoder of decoders) {
|
||||
if (decoder.match(remainingParts)) {
|
||||
result = decoder.decode(remainingParts, metarString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (result.text) decoded.push(result.text);
|
||||
i += result.consumed;
|
||||
} else if (parts[i] === '/') {
|
||||
i++; // separator, ignore
|
||||
} else {
|
||||
console.log(`Other remark: ${part} in METAR: ${metarString}`);
|
||||
decoded.push(` - ${part}: Other remark`);
|
||||
console.log(`Other remark: ${parts[i]} in METAR: ${metarString}`);
|
||||
decoded.push(` - ${parts[i]}: Other remark`);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return decoded.join('\n');
|
||||
|
||||
Reference in New Issue
Block a user