mirror of
https://github.com/jakobkordez/call-tester.git
synced 2025-07-17 11:27:40 +00:00
Changed to CTY database
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"vite.autoStart": false,
|
"vite.autoStart": false,
|
||||||
"cSpell.words": ["callsign", "dxcc", "adif", "graphviz", "clublog"]
|
"cSpell.words": ["adif", "callsign", "clublog", "dxcc", "graphviz", "ituz", "timez"]
|
||||||
}
|
}
|
||||||
|
18
README.md
18
README.md
@ -2,10 +2,26 @@
|
|||||||
|
|
||||||
An app for checking the format of a callsign and finding the country it belongs to.
|
An app for checking the format of a callsign and finding the country it belongs to.
|
||||||
|
|
||||||
## Obtaining the Clublog prefix database
|
## Downloading the country database
|
||||||
|
|
||||||
|
Data about countries can be downloaded from either [Amateur Radio Country Files by AD1C](https://www.country-files.com/) or [Clublog](https://clublog.freshdesk.com/support/solutions/articles/54902-downloading-the-prefixes-and-exceptions-as-xml).
|
||||||
|
|
||||||
|
### Obtaining the AD1C country database
|
||||||
|
|
||||||
|
Download any `cty.dat` file from the [Country Files](https://www.country-files.com/) website. I recommend using the [Big CTY](https://www.country-files.com/big-cty/) file.
|
||||||
|
|
||||||
|
Parsing can be done with the script [`cty-dat-parser.ts`](./scripts/cty-dat-parser.ts).
|
||||||
|
|
||||||
|
Instead of `cty.dat` you can also use the `cty.csv` file. Parsing can be done with the script [`cty-csv-parser.ts`](./scripts/cty-csv-parser.ts).
|
||||||
|
|
||||||
|
`Note:` The `cty.dat` does not contain the `adif` DXCC number.
|
||||||
|
|
||||||
|
### Obtaining the Clublog prefix database `OBSOLETE`
|
||||||
|
|
||||||
Read how to obtain the Clublog prefix file [here](https://clublog.freshdesk.com/support/solutions/articles/54902-downloading-the-prefixes-and-exceptions-as-xml)
|
Read how to obtain the Clublog prefix file [here](https://clublog.freshdesk.com/support/solutions/articles/54902-downloading-the-prefixes-and-exceptions-as-xml)
|
||||||
|
|
||||||
|
Parsing can be done with the script [`clublog-parser.ts`](./scripts/clublog-parser.ts).
|
||||||
|
|
||||||
## Deploying the app
|
## Deploying the app
|
||||||
|
|
||||||
The app can be deployed using the following command:
|
The app can be deployed using the following command:
|
||||||
|
@ -102,7 +102,7 @@ for (const entity of doc.clublog.entities[0].entity) {
|
|||||||
const end = entity.end?.[0];
|
const end = entity.end?.[0];
|
||||||
if (end && new Date(end) < now) continue;
|
if (end && new Date(end) < now) continue;
|
||||||
entities.push({
|
entities.push({
|
||||||
entity: id,
|
id,
|
||||||
name,
|
name,
|
||||||
cqz: cqz ? parseInt(cqz) : undefined,
|
cqz: cqz ? parseInt(cqz) : undefined,
|
||||||
cont: cont ? cont : undefined
|
cont: cont ? cont : undefined
|
||||||
|
150
scripts/cty-csv-parser.ts
Normal file
150
scripts/cty-csv-parser.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const filePath = process.argv[2];
|
||||||
|
if (!filePath) {
|
||||||
|
console.error('Please provide a file path as the first argument');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OUT_DIR = '../src/assets/';
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outDir = path.resolve(__dirname, OUT_DIR);
|
||||||
|
console.log('Outputting to', outDir);
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import { DxccEntity } from '../src/lib/models/dxcc-entity';
|
||||||
|
|
||||||
|
// Parse the cty.dat file
|
||||||
|
const file = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
const entities: DxccEntity[] = [];
|
||||||
|
const prefixes: { call: string; entity: number }[] = [];
|
||||||
|
|
||||||
|
console.log('Parsing', filePath);
|
||||||
|
|
||||||
|
for (const entity of file.split(';')) {
|
||||||
|
if (!entity.trim()) continue;
|
||||||
|
const [primaryPrefixRaw, name, dxcc, cont, cqz, ituz, lat, long, timez, otherPrefixesRaw] = entity
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim());
|
||||||
|
|
||||||
|
const hasStar = primaryPrefixRaw.startsWith('*');
|
||||||
|
const primaryPrefix = primaryPrefixRaw.replace('*', '');
|
||||||
|
const otherPrefixes = otherPrefixesRaw.split(' ').map((s) => s.trim());
|
||||||
|
const entityId = entities.length + 1;
|
||||||
|
|
||||||
|
entities.push({
|
||||||
|
id: entityId,
|
||||||
|
dxcc: parseInt(dxcc),
|
||||||
|
primaryPrefix,
|
||||||
|
name,
|
||||||
|
cqz: parseInt(cqz),
|
||||||
|
ituz: parseInt(ituz),
|
||||||
|
cont,
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
long: parseFloat(long),
|
||||||
|
timezone: parseFloat(timez)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const prefix of otherPrefixes) {
|
||||||
|
const find = prefixes.find((p) => p.call === prefix);
|
||||||
|
if (find) {
|
||||||
|
// console.error('Duplicate prefix', prefix, hasStar ? 'Overwriting' : 'Skipping');
|
||||||
|
if (hasStar) find.entity = entityId;
|
||||||
|
} else {
|
||||||
|
prefixes.push({ call: prefix, entity: entityId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Parsed', prefixes.length, 'prefixes');
|
||||||
|
|
||||||
|
// Build the initial trie
|
||||||
|
import { TrieNode } from '../src/lib/models/trie';
|
||||||
|
|
||||||
|
const root = new TrieNode();
|
||||||
|
for (const { call: callRaw, entity } of prefixes) {
|
||||||
|
const [, call] = callRaw.match(/^=?((?:[A-Z\d/])+)(.*)/)!;
|
||||||
|
const isExact = callRaw.startsWith('=');
|
||||||
|
root.insert(call, entity, isExact);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Built trie with', root.getAllNodes().size, 'nodes');
|
||||||
|
|
||||||
|
// Collapse nodes that do not cause changes
|
||||||
|
root.collapseNodes();
|
||||||
|
|
||||||
|
console.log('Collapsed trie with', root.getAllNodes().size, 'nodes');
|
||||||
|
|
||||||
|
// Merge as many nodes as possible
|
||||||
|
const nodes = new Map([...root.getAllNodes()].map((node) => [node.id, node]));
|
||||||
|
|
||||||
|
const parents: Map<number, TrieNode[]> = new Map();
|
||||||
|
for (const node of nodes.values()) {
|
||||||
|
for (const child of node.children.values()) {
|
||||||
|
const list = parents.get(child.id) ?? [];
|
||||||
|
list.push(node);
|
||||||
|
parents.set(child.id, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let anyChanged = true;
|
||||||
|
while (anyChanged) {
|
||||||
|
anyChanged = false;
|
||||||
|
|
||||||
|
const hashed = new Map<string, TrieNode>();
|
||||||
|
for (const node of nodes.values()) {
|
||||||
|
const hash = node.hash();
|
||||||
|
const existing = hashed.get(hash);
|
||||||
|
if (!existing) {
|
||||||
|
hashed.set(hash, node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.canMerge(node)) {
|
||||||
|
throw new Error('Merge conflict false positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parent of parents.get(node.id) ?? []) {
|
||||||
|
for (const [k, v] of parent.children) {
|
||||||
|
if (v === node) {
|
||||||
|
parent.children.set(k, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parents.delete(node.id);
|
||||||
|
nodes.delete(node.id);
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Finished merge with', nodes.size, 'nodes');
|
||||||
|
|
||||||
|
// Validate the trie
|
||||||
|
for (const { call: callRaw, entity } of prefixes) {
|
||||||
|
const [, call] = callRaw.match(/^=?((?:[A-Z\d/])+)(.*)/)!;
|
||||||
|
let node: TrieNode | null = root;
|
||||||
|
let currentEntity: number | null = null;
|
||||||
|
for (const c of call) {
|
||||||
|
node = node.children.get(c) ?? null;
|
||||||
|
if (!node) break;
|
||||||
|
currentEntity = node.entity ?? currentEntity;
|
||||||
|
}
|
||||||
|
if (currentEntity !== entity && node?.exactEntity !== entity) {
|
||||||
|
console.error('Failed to find', call, entity);
|
||||||
|
console.log('Found', node?.entity, node?.exactEntity, currentEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimize node IDs
|
||||||
|
let i = 0;
|
||||||
|
for (const node of root.getAllNodes()) {
|
||||||
|
node.id = i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the trie
|
||||||
|
const out = root.encodeToString();
|
||||||
|
fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
|
||||||
|
|
||||||
|
// Output the entities
|
||||||
|
entities.sort((a, b) => a.id - b.id);
|
||||||
|
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));
|
148
scripts/cty-dat-parser.ts
Normal file
148
scripts/cty-dat-parser.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const filePath = process.argv[2];
|
||||||
|
if (!filePath) {
|
||||||
|
console.error('Please provide a file path as the first argument');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OUT_DIR = '../src/assets/';
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outDir = path.resolve(__dirname, OUT_DIR);
|
||||||
|
console.log('Outputting to', outDir);
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import { DxccEntity } from '../src/lib/models/dxcc-entity';
|
||||||
|
|
||||||
|
// Parse the cty.dat file
|
||||||
|
const file = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
const entities: DxccEntity[] = [];
|
||||||
|
const prefixes: { call: string; entity: number }[] = [];
|
||||||
|
|
||||||
|
console.log('Parsing', filePath);
|
||||||
|
|
||||||
|
for (const entity of file.split(';')) {
|
||||||
|
if (!entity.trim()) continue;
|
||||||
|
const [name, cqz, ituz, cont, lat, long, timez, primaryPrefixRaw, otherPrefixesRaw] = entity
|
||||||
|
.split(':')
|
||||||
|
.map((s) => s.trim());
|
||||||
|
|
||||||
|
const hasStar = primaryPrefixRaw.startsWith('*');
|
||||||
|
const primaryPrefix = primaryPrefixRaw.replace('*', '');
|
||||||
|
const otherPrefixes = otherPrefixesRaw.split(',').map((s) => s.trim());
|
||||||
|
const entityId = entities.length + 1;
|
||||||
|
|
||||||
|
entities.push({
|
||||||
|
id: entityId,
|
||||||
|
primaryPrefix,
|
||||||
|
name,
|
||||||
|
cqz: parseInt(cqz),
|
||||||
|
ituz: parseInt(ituz),
|
||||||
|
cont,
|
||||||
|
lat: parseFloat(lat),
|
||||||
|
long: parseFloat(long),
|
||||||
|
timezone: parseFloat(timez)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const prefix of otherPrefixes) {
|
||||||
|
const find = prefixes.find((p) => p.call === prefix);
|
||||||
|
if (find) {
|
||||||
|
// console.error('Duplicate prefix', prefix, hasStar ? 'Overwriting' : 'Skipping');
|
||||||
|
if (hasStar) find.entity = entityId;
|
||||||
|
} else {
|
||||||
|
prefixes.push({ call: prefix, entity: entityId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Parsed', prefixes.length, 'prefixes');
|
||||||
|
|
||||||
|
// Build the initial trie
|
||||||
|
import { TrieNode } from '../src/lib/models/trie';
|
||||||
|
|
||||||
|
const root = new TrieNode();
|
||||||
|
for (const { call: callRaw, entity } of prefixes) {
|
||||||
|
const [, call] = callRaw.match(/^=?((?:[A-Z\d/])+)(.*)/)!;
|
||||||
|
const isExact = callRaw.startsWith('=');
|
||||||
|
root.insert(call, entity, isExact);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Built trie with', root.getAllNodes().size, 'nodes');
|
||||||
|
|
||||||
|
// Collapse nodes that do not cause changes
|
||||||
|
root.collapseNodes();
|
||||||
|
|
||||||
|
console.log('Collapsed trie with', root.getAllNodes().size, 'nodes');
|
||||||
|
|
||||||
|
// Merge as many nodes as possible
|
||||||
|
const nodes = new Map([...root.getAllNodes()].map((node) => [node.id, node]));
|
||||||
|
|
||||||
|
const parents: Map<number, TrieNode[]> = new Map();
|
||||||
|
for (const node of nodes.values()) {
|
||||||
|
for (const child of node.children.values()) {
|
||||||
|
const list = parents.get(child.id) ?? [];
|
||||||
|
list.push(node);
|
||||||
|
parents.set(child.id, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let anyChanged = true;
|
||||||
|
while (anyChanged) {
|
||||||
|
anyChanged = false;
|
||||||
|
|
||||||
|
const hashed = new Map<string, TrieNode>();
|
||||||
|
for (const node of nodes.values()) {
|
||||||
|
const hash = node.hash();
|
||||||
|
const existing = hashed.get(hash);
|
||||||
|
if (!existing) {
|
||||||
|
hashed.set(hash, node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.canMerge(node)) {
|
||||||
|
throw new Error('Merge conflict false positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parent of parents.get(node.id) ?? []) {
|
||||||
|
for (const [k, v] of parent.children) {
|
||||||
|
if (v === node) {
|
||||||
|
parent.children.set(k, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parents.delete(node.id);
|
||||||
|
nodes.delete(node.id);
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Finished merge with', nodes.size, 'nodes');
|
||||||
|
|
||||||
|
// Validate the trie
|
||||||
|
for (const { call: callRaw, entity } of prefixes) {
|
||||||
|
const [, call] = callRaw.match(/^=?((?:[A-Z\d/])+)(.*)/)!;
|
||||||
|
let node: TrieNode | null = root;
|
||||||
|
let currentEntity: number | null = null;
|
||||||
|
for (const c of call) {
|
||||||
|
node = node.children.get(c) ?? null;
|
||||||
|
if (!node) break;
|
||||||
|
currentEntity = node.entity ?? currentEntity;
|
||||||
|
}
|
||||||
|
if (currentEntity !== entity && node?.exactEntity !== entity) {
|
||||||
|
console.error('Failed to find', call, entity);
|
||||||
|
console.log('Found', node?.entity, node?.exactEntity, currentEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimize node IDs
|
||||||
|
let i = 0;
|
||||||
|
for (const node of root.getAllNodes()) {
|
||||||
|
node.id = i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the trie
|
||||||
|
const out = root.encodeToString();
|
||||||
|
fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
|
||||||
|
|
||||||
|
// Output the entities
|
||||||
|
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));
|
File diff suppressed because it is too large
Load Diff
29902
src/assets/dxcc-tree.txt
29902
src/assets/dxcc-tree.txt
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import { parseCallsign } from './callsign';
|
import { parseCallsign } from './callsign';
|
||||||
|
import { dxccEntities } from './dxcc-util';
|
||||||
|
|
||||||
|
const s5ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5')?.id;
|
||||||
|
const svID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV')?.id;
|
||||||
|
const svaID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a')?.id;
|
||||||
|
|
||||||
describe('parseCallsign', () => {
|
describe('parseCallsign', () => {
|
||||||
test('S52KJ', () => {
|
test('S52KJ', () => {
|
||||||
@ -10,8 +15,7 @@ describe('parseCallsign', () => {
|
|||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe(null);
|
expect(data?.secondarySuffix).toBe(null);
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
|
||||||
expect(data?.prefixDxcc).toBe(null);
|
expect(data?.prefixDxcc).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,8 +27,7 @@ describe('parseCallsign', () => {
|
|||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe(null);
|
expect(data?.secondarySuffix).toBe(null);
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
|
||||||
expect(data?.prefixDxcc).toBe(null);
|
expect(data?.prefixDxcc).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -36,35 +39,32 @@ describe('parseCallsign', () => {
|
|||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe('P');
|
expect(data?.secondarySuffix).toBe('P');
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
|
||||||
expect(data?.prefixDxcc).toBe(null);
|
expect(data?.prefixDxcc).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('9A/S52KJ', () => {
|
test('SV/S52KJ', () => {
|
||||||
const data = parseCallsign('9A/S52KJ');
|
const data = parseCallsign('SV/S52KJ');
|
||||||
expect(data).not.toBe(null);
|
expect(data).not.toBe(null);
|
||||||
expect(data?.secondaryPrefix).toBe('9A');
|
expect(data?.secondaryPrefix).toBe('SV');
|
||||||
expect(data?.basePrefix).toBe('S5');
|
expect(data?.basePrefix).toBe('S5');
|
||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe(null);
|
expect(data?.secondarySuffix).toBe(null);
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
expect(data?.prefixDxcc).toBe(svID);
|
||||||
expect(data?.prefixDxcc).toBe(497);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('9A/S52KJ/P', () => {
|
test('SV/S52KJ/P', () => {
|
||||||
const data = parseCallsign('9A/S52KJ/P');
|
const data = parseCallsign('SV/S52KJ/P');
|
||||||
expect(data).not.toBe(null);
|
expect(data).not.toBe(null);
|
||||||
expect(data?.secondaryPrefix).toBe('9A');
|
expect(data?.secondaryPrefix).toBe('SV');
|
||||||
expect(data?.basePrefix).toBe('S5');
|
expect(data?.basePrefix).toBe('S5');
|
||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe('P');
|
expect(data?.secondarySuffix).toBe('P');
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
expect(data?.prefixDxcc).toBe(svID);
|
||||||
expect(data?.prefixDxcc).toBe(497);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('S52KJ/A', () => {
|
test('S52KJ/A', () => {
|
||||||
@ -75,21 +75,19 @@ describe('parseCallsign', () => {
|
|||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe('A');
|
expect(data?.secondarySuffix).toBe('A');
|
||||||
expect(data?.suffixPartOf).toBe(null);
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
|
||||||
expect(data?.prefixDxcc).toBe(null);
|
expect(data?.prefixDxcc).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV1KJ/A', () => {
|
test('SV2RSG/A', () => {
|
||||||
const data = parseCallsign('SV1KJ/A');
|
const data = parseCallsign('SV2RSG/A');
|
||||||
expect(data).not.toBe(null);
|
expect(data).not.toBe(null);
|
||||||
expect(data?.secondaryPrefix).toBe(null);
|
expect(data?.secondaryPrefix).toBe(null);
|
||||||
expect(data?.basePrefix).toBe('SV');
|
expect(data?.basePrefix).toBe('SV2RSG/A');
|
||||||
expect(data?.baseSuffix).toBe('1KJ');
|
expect(data?.baseSuffix).toBe('');
|
||||||
expect(data?.base).toBe('SV1KJ');
|
expect(data?.base).toBe('SV2RSG');
|
||||||
expect(data?.secondarySuffix).toBe('A');
|
expect(data?.secondarySuffix).toBe('A');
|
||||||
expect(data?.suffixPartOf).toBe('base');
|
expect(data?.baseDxcc).toBe(svaID);
|
||||||
expect(data?.baseDxcc).toBe(180);
|
|
||||||
expect(data?.prefixDxcc).toBe(null);
|
expect(data?.prefixDxcc).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,8 +99,7 @@ describe('parseCallsign', () => {
|
|||||||
expect(data?.baseSuffix).toBe('2KJ');
|
expect(data?.baseSuffix).toBe('2KJ');
|
||||||
expect(data?.base).toBe('S52KJ');
|
expect(data?.base).toBe('S52KJ');
|
||||||
expect(data?.secondarySuffix).toBe('A');
|
expect(data?.secondarySuffix).toBe('A');
|
||||||
expect(data?.suffixPartOf).toBe('prefix');
|
expect(data?.baseDxcc).toBe(s5ID);
|
||||||
expect(data?.baseDxcc).toBe(499);
|
expect(data?.prefixDxcc).toBe(svID);
|
||||||
expect(data?.prefixDxcc).toBe(180);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,8 +8,6 @@ type CallsignData = {
|
|||||||
baseSuffix: string | null;
|
baseSuffix: string | null;
|
||||||
base: string;
|
base: string;
|
||||||
secondarySuffix: string | null;
|
secondarySuffix: string | null;
|
||||||
suffixPartOf: 'base' | 'prefix' | null;
|
|
||||||
suffixDescription?: string;
|
|
||||||
baseDxcc: number | null;
|
baseDxcc: number | null;
|
||||||
prefixDxcc: number | null;
|
prefixDxcc: number | null;
|
||||||
};
|
};
|
||||||
@ -24,17 +22,13 @@ export function parseCallsign(callsign: string): CallsignData | null {
|
|||||||
const secondaryPrefix = match[1]?.slice(0, -1) ?? null;
|
const secondaryPrefix = match[1]?.slice(0, -1) ?? null;
|
||||||
const base = match[2];
|
const base = match[2];
|
||||||
const secondarySuffix = match[3]?.slice(1) ?? null;
|
const secondarySuffix = match[3]?.slice(1) ?? null;
|
||||||
const baseDxcc = findDxcc(base + '/' + secondarySuffix);
|
|
||||||
|
const baseWithSuffix = base + (secondarySuffix ? '/' + secondarySuffix : '');
|
||||||
|
const baseDxcc = findDxcc(baseWithSuffix);
|
||||||
const prefixDxcc = secondaryPrefix ? findDxcc(callsign) : null;
|
const prefixDxcc = secondaryPrefix ? findDxcc(callsign) : null;
|
||||||
|
|
||||||
const basePrefix = baseDxcc ? base.slice(0, baseDxcc.prefixLength) : null;
|
const basePrefix = baseDxcc ? baseWithSuffix.slice(0, baseDxcc.matchLength) : null;
|
||||||
const baseSuffix = baseDxcc ? base.slice(baseDxcc.prefixLength) : null;
|
const baseSuffix = baseDxcc ? baseWithSuffix.slice(baseDxcc.matchLength).split('/')[0] : null;
|
||||||
|
|
||||||
const suffixPartOf = prefixDxcc?.withSuffix
|
|
||||||
? 'prefix'
|
|
||||||
: !prefixDxcc && baseDxcc?.withSuffix
|
|
||||||
? 'base'
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secondaryPrefix,
|
secondaryPrefix,
|
||||||
@ -42,26 +36,13 @@ export function parseCallsign(callsign: string): CallsignData | null {
|
|||||||
baseSuffix,
|
baseSuffix,
|
||||||
base,
|
base,
|
||||||
secondarySuffix,
|
secondarySuffix,
|
||||||
suffixPartOf,
|
baseDxcc: baseDxcc?.entityId || null,
|
||||||
baseDxcc: baseDxcc?.entity || null,
|
prefixDxcc: prefixDxcc?.entityId || null
|
||||||
prefixDxcc: prefixDxcc?.entity || null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSecondarySuffixDescription(callsign: CallsignData): string | null {
|
export function getSecondarySuffixDescription(suffix: string): string | null {
|
||||||
if (!callsign.secondarySuffix) {
|
switch (suffix) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callsign.suffixPartOf === 'base') {
|
|
||||||
return 'Part of prefix';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callsign.suffixPartOf === 'prefix') {
|
|
||||||
return 'Part of secondary prefix';
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (callsign.secondarySuffix) {
|
|
||||||
case 'P':
|
case 'P':
|
||||||
return 'Portable';
|
return 'Portable';
|
||||||
case 'M':
|
case 'M':
|
||||||
@ -70,6 +51,7 @@ export function getSecondarySuffixDescription(callsign: CallsignData): string |
|
|||||||
return 'Aeronautical mobile';
|
return 'Aeronautical mobile';
|
||||||
case 'MM':
|
case 'MM':
|
||||||
return 'Maritime mobile';
|
return 'Maritime mobile';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
@ -8,81 +8,86 @@ describe('dxccTree', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findDxcc', () => {
|
describe('findDxcc', () => {
|
||||||
|
const s5ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5')?.id;
|
||||||
|
const svID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV')?.id;
|
||||||
|
const svaID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a')?.id;
|
||||||
|
const sv9ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV9')?.id;
|
||||||
|
|
||||||
test('S52KJ', () => {
|
test('S52KJ', () => {
|
||||||
const result = findDxcc('S52KJ');
|
const result = findDxcc('S52KJ');
|
||||||
expect(result?.entity).toBe(499);
|
expect(result?.entityId).toBe(s5ID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('s52kj', () => {
|
test('s52kj', () => {
|
||||||
const result = findDxcc('s52kj');
|
const result = findDxcc('s52kj');
|
||||||
expect(result?.entity).toBe(499);
|
expect(result?.entityId).toBe(s5ID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('S52KJ/P', () => {
|
test('S52KJ/P', () => {
|
||||||
const result = findDxcc('S52KJ/P');
|
const result = findDxcc('S52KJ/P');
|
||||||
expect(result?.entity).toBe(499);
|
expect(result?.entityId).toBe(s5ID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV2AAA', () => {
|
test('SV2AAA', () => {
|
||||||
const result = findDxcc('SV2AAA');
|
const result = findDxcc('SV2AAA');
|
||||||
expect(result?.entity).toBe(236);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV2AAA/A', () => {
|
test('SV2RSG/A', () => {
|
||||||
const result = findDxcc('SV2AAA/A');
|
const result = findDxcc('SV2RSG/A');
|
||||||
expect(result?.entity).toBe(180);
|
expect(result?.entityId).toBe(svaID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(8);
|
||||||
expect(result?.withSuffix).toBe(true);
|
expect(result?.isExact).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV2AAA/AP', () => {
|
test('SV2AAA/AP', () => {
|
||||||
const result = findDxcc('SV2AAA/AP');
|
const result = findDxcc('SV2AAA/AP');
|
||||||
expect(result?.entity).toBe(236);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV2AAA/P', () => {
|
test('SV2AAA/P', () => {
|
||||||
const result = findDxcc('SV2AAA/P');
|
const result = findDxcc('SV2AAA/P');
|
||||||
expect(result?.entity).toBe(236);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV/S52KJ', () => {
|
test('SV/S52KJ', () => {
|
||||||
const result = findDxcc('SV/S52KJ');
|
const result = findDxcc('SV/S52KJ');
|
||||||
expect(result?.entity).toBe(236);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV/S52KJ/A', () => {
|
test('SV/S52KJ/A', () => {
|
||||||
const result = findDxcc('SV/S52KJ/A');
|
const result = findDxcc('SV/S52KJ/A');
|
||||||
expect(result?.entity).toBe(180);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(true);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV/S52KJ/P', () => {
|
test('SV/S52KJ/P', () => {
|
||||||
const result = findDxcc('SV/S52KJ/P');
|
const result = findDxcc('SV/S52KJ/P');
|
||||||
expect(result?.entity).toBe(236);
|
expect(result?.entityId).toBe(svID);
|
||||||
expect(result?.prefixLength).toBe(2);
|
expect(result?.matchLength).toBe(2);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SV9/S52KJ/A', () => {
|
test('SV9/S52KJ/A', () => {
|
||||||
const result = findDxcc('SV9/S52KJ/A');
|
const result = findDxcc('SV9/S52KJ/A');
|
||||||
expect(result?.entity).toBe(40);
|
expect(result?.entityId).toBe(sv9ID);
|
||||||
expect(result?.prefixLength).toBe(3);
|
expect(result?.matchLength).toBe(3);
|
||||||
expect(result?.withSuffix).toBe(false);
|
expect(result?.isExact).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('empty string', () => {
|
test('empty string', () => {
|
||||||
@ -106,8 +111,10 @@ describe('dxccEntities', () => {
|
|||||||
expect(dxccEntities).not.toBe(null);
|
expect(dxccEntities).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('499', () => {
|
test('S5', () => {
|
||||||
const entity = dxccEntities.get(499);
|
const s5Id = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5')!.id;
|
||||||
|
|
||||||
|
const entity = dxccEntities.get(s5Id);
|
||||||
expect(entity).not.toBe(undefined);
|
expect(entity).not.toBe(undefined);
|
||||||
expect(entity?.name).toBe('Slovenia');
|
expect(entity?.name).toBe('Slovenia');
|
||||||
expect(entity?.cont).toBe('EU');
|
expect(entity?.cont).toBe('EU');
|
||||||
|
@ -1,43 +1,50 @@
|
|||||||
import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
|
import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
|
||||||
import dxccEntitiesFile from '../assets/dxcc-entities.json';
|
import dxccEntitiesFile from '../assets/dxcc-entities.json';
|
||||||
import { TrieNode } from './models/trie';
|
import { TrieNode } from './models/trie';
|
||||||
|
import type { DxccEntity } from './models/dxcc-entity';
|
||||||
|
|
||||||
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
|
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
|
||||||
|
|
||||||
interface DxccResult {
|
interface DxccResult {
|
||||||
entity: number;
|
entityId: number;
|
||||||
prefixLength: number;
|
matchLength: number;
|
||||||
withSuffix: boolean;
|
isExact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): DxccResult | null {
|
export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): DxccResult | null {
|
||||||
prefix = prefix.toUpperCase();
|
prefix = prefix.toUpperCase();
|
||||||
let node = startingNode;
|
let node = startingNode;
|
||||||
let entity: number | null = null;
|
let entityId: number | null = null;
|
||||||
let prefixLength = 0;
|
let tempPrefixLength = 0;
|
||||||
|
let matchLength = 0;
|
||||||
|
|
||||||
while (prefix) {
|
while (prefix) {
|
||||||
const next = node.children.get(prefix[0]);
|
const next = node.children.get(prefix[0]);
|
||||||
|
if (!next) {
|
||||||
if (prefix[0] === '/' || !next) {
|
|
||||||
const slashPos = prefix.lastIndexOf('/');
|
|
||||||
if (node.children.has('/') && slashPos > 0) {
|
|
||||||
const suffix = prefix.slice(slashPos + 1);
|
|
||||||
const res = findDxcc(suffix, node.children.get('/'));
|
|
||||||
if (res && res.prefixLength == suffix.length)
|
|
||||||
return { entity: res.entity, prefixLength, withSuffix: true };
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
node = next;
|
node = next;
|
||||||
if (node.entity) entity = node.entity;
|
|
||||||
prefix = prefix.slice(1);
|
prefix = prefix.slice(1);
|
||||||
prefixLength++;
|
tempPrefixLength++;
|
||||||
|
if (node.entity) {
|
||||||
|
entityId = node.entity;
|
||||||
|
matchLength = tempPrefixLength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entity) return null;
|
if (node.exactEntity && !prefix) {
|
||||||
return { entity, withSuffix: false, prefixLength };
|
return {
|
||||||
|
entityId: node.exactEntity,
|
||||||
|
matchLength: tempPrefixLength,
|
||||||
|
isExact: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entityId) return null;
|
||||||
|
return { entityId, matchLength, isExact: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dxccEntities = new Map([...dxccEntitiesFile].map((e) => [e.entity, e]));
|
export const dxccEntities: Map<number, DxccEntity> = new Map(
|
||||||
|
[...dxccEntitiesFile].map((e) => [e.id, e])
|
||||||
|
);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
export type DxccEntity = {
|
export type DxccEntity = {
|
||||||
entity: number;
|
id: number;
|
||||||
|
dxcc?: number;
|
||||||
|
primaryPrefix?: string;
|
||||||
name: string;
|
name: string;
|
||||||
cqz?: number;
|
cqz?: number;
|
||||||
|
ituz?: number;
|
||||||
cont?: string;
|
cont?: string;
|
||||||
long?: number;
|
long?: number;
|
||||||
lat?: number;
|
lat?: number;
|
||||||
|
timezone?: number;
|
||||||
start?: string;
|
start?: string;
|
||||||
end?: string;
|
end?: string;
|
||||||
};
|
};
|
||||||
|
@ -4,10 +4,14 @@ import { TrieNode } from './trie';
|
|||||||
describe('parseString', () => {
|
describe('parseString', () => {
|
||||||
test('Basic test', () => {
|
test('Basic test', () => {
|
||||||
const encoded = `
|
const encoded = `
|
||||||
31-YAP-4
|
31
|
||||||
3=401
|
-YAP-4
|
||||||
31-X-3
|
-X-3
|
||||||
4=400
|
3
|
||||||
|
=401
|
||||||
|
4
|
||||||
|
=400
|
||||||
|
!404
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const root = TrieNode.decodeFromString(encoded);
|
const root = TrieNode.decodeFromString(encoded);
|
||||||
@ -29,6 +33,7 @@ describe('parseString', () => {
|
|||||||
|
|
||||||
expect(y!.id).toBe(4);
|
expect(y!.id).toBe(4);
|
||||||
expect(y!.entity).toBe(400);
|
expect(y!.entity).toBe(400);
|
||||||
|
expect(y!.exactEntity).toBe(404);
|
||||||
expect(y!.children.size).toBe(0);
|
expect(y!.children.size).toBe(0);
|
||||||
|
|
||||||
const x = root.children.get('X');
|
const x = root.children.get('X');
|
||||||
|
@ -2,24 +2,42 @@ let nodeCounter = 0;
|
|||||||
|
|
||||||
export class TrieNode {
|
export class TrieNode {
|
||||||
public id: number;
|
public id: number;
|
||||||
|
public entity: number | null;
|
||||||
|
public exactEntity: number | null;
|
||||||
|
public children: Map<string, TrieNode>;
|
||||||
|
|
||||||
constructor(
|
constructor({
|
||||||
public children: Map<string, TrieNode> = new Map(),
|
id,
|
||||||
public entity: number | null = null,
|
entity,
|
||||||
id: number | null = null
|
exactEntity,
|
||||||
) {
|
children
|
||||||
|
}: {
|
||||||
|
id?: number | null;
|
||||||
|
entity?: number | null;
|
||||||
|
exactEntity?: number | null;
|
||||||
|
children?: Map<string, TrieNode>;
|
||||||
|
} = {}) {
|
||||||
if (id) this.id = id;
|
if (id) this.id = id;
|
||||||
else this.id = ++nodeCounter;
|
else this.id = ++nodeCounter;
|
||||||
|
this.entity = entity ?? null;
|
||||||
|
this.exactEntity = exactEntity ?? null;
|
||||||
|
this.children = children ?? new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a prefix into the trie.
|
* Insert a prefix into the trie.
|
||||||
*/
|
*/
|
||||||
insert(prefix: string, entity: number): void {
|
insert(prefix: string, entity: number, isExact: boolean = false): void {
|
||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
if (this.entity && this.entity !== entity)
|
if (isExact) {
|
||||||
throw new Error(`Prefix conflict: ${this.entity} vs ${entity}`);
|
if (this.exactEntity && this.exactEntity !== entity)
|
||||||
this.entity = entity;
|
throw new Error(`Exact prefix conflict: ${this.exactEntity} vs ${entity}`);
|
||||||
|
this.exactEntity = entity;
|
||||||
|
} else {
|
||||||
|
if (this.entity && this.entity !== entity)
|
||||||
|
throw new Error(`Prefix conflict: ${this.entity} vs ${entity}`);
|
||||||
|
this.entity = entity;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let next = this.children.get(prefix[0]);
|
let next = this.children.get(prefix[0]);
|
||||||
@ -27,7 +45,7 @@ export class TrieNode {
|
|||||||
next = new TrieNode();
|
next = new TrieNode();
|
||||||
this.children.set(prefix[0], next);
|
this.children.set(prefix[0], next);
|
||||||
}
|
}
|
||||||
next.insert(prefix.slice(1), entity);
|
next.insert(prefix.slice(1), entity, isExact);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,11 +68,40 @@ export class TrieNode {
|
|||||||
return new Set(nodes);
|
return new Set(nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse nodes that do not cause changes.
|
||||||
|
* Returns true if node can be deleted, false otherwise.
|
||||||
|
*/
|
||||||
|
collapseNodes(currentEntity: number | null = null): boolean {
|
||||||
|
for (const [k, child] of this.children.entries()) {
|
||||||
|
if (child.collapseNodes(this.entity ?? currentEntity)) {
|
||||||
|
this.children.delete(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.children.size == 0 &&
|
||||||
|
(!this.entity || this.entity === currentEntity) &&
|
||||||
|
(!this.exactEntity || this.exactEntity === currentEntity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a hash for merging nodes.
|
||||||
|
*/
|
||||||
|
hash(): string {
|
||||||
|
const children = [...this.children.entries()]
|
||||||
|
.map(([k, v]) => `${k}:${v.id}`)
|
||||||
|
.sort()
|
||||||
|
.join(',');
|
||||||
|
return `${this.entity ?? ''}_${this.exactEntity ?? ''}_${children}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if this node can be merged with another node.
|
* Checks if this node can be merged with another node.
|
||||||
*/
|
*/
|
||||||
canMerge(other: TrieNode): boolean {
|
canMerge(other: TrieNode): boolean {
|
||||||
if (this === other) return false;
|
if (this === other) return false;
|
||||||
|
if (this.exactEntity !== other.exactEntity) return false;
|
||||||
if (this.entity !== other.entity) return false;
|
if (this.entity !== other.entity) return false;
|
||||||
// Union set of all children keys
|
// Union set of all children keys
|
||||||
const l = new Set([...this.children.keys(), ...other.children.keys()]);
|
const l = new Set([...this.children.keys(), ...other.children.keys()]);
|
||||||
@ -74,9 +121,12 @@ export class TrieNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_encodeToString(): string {
|
_encodeToString(): string {
|
||||||
const s = [];
|
const s = [`${this.id}`];
|
||||||
if (this.entity) {
|
if (this.entity) {
|
||||||
s.push(`${this.id}=${this.entity}`);
|
s.push(`=${this.entity}`);
|
||||||
|
}
|
||||||
|
if (this.exactEntity) {
|
||||||
|
s.push(`!${this.exactEntity}`);
|
||||||
}
|
}
|
||||||
for (const c of new Set(this.children.values())) {
|
for (const c of new Set(this.children.values())) {
|
||||||
const chars = [];
|
const chars = [];
|
||||||
@ -84,7 +134,7 @@ export class TrieNode {
|
|||||||
if (v === c) chars.push(k);
|
if (v === c) chars.push(k);
|
||||||
}
|
}
|
||||||
chars.sort();
|
chars.sort();
|
||||||
s.push(`${this.id}-${chars.join('')}-${c.id}`);
|
s.push(`-${chars.join('')}-${c.id}`);
|
||||||
}
|
}
|
||||||
return s.join('\n');
|
return s.join('\n');
|
||||||
}
|
}
|
||||||
@ -98,7 +148,7 @@ export class TrieNode {
|
|||||||
function getNode(id: number): TrieNode {
|
function getNode(id: number): TrieNode {
|
||||||
let node = nodes.get(id);
|
let node = nodes.get(id);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
node = new TrieNode(undefined, undefined, id);
|
node = new TrieNode({ id });
|
||||||
nodes.set(id, node);
|
nodes.set(id, node);
|
||||||
// Assert root is the first node
|
// Assert root is the first node
|
||||||
root ??= node;
|
root ??= node;
|
||||||
@ -106,21 +156,24 @@ export class TrieNode {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentNode: TrieNode | null = null;
|
||||||
for (let line of s.trim().split('\n')) {
|
for (let line of s.trim().split('\n')) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
const parts = line.split('=');
|
if (line.startsWith('=')) {
|
||||||
if (parts.length === 2) {
|
const entity = line.slice(1);
|
||||||
const [id, entity] = parts;
|
currentNode!.entity = parseInt(entity);
|
||||||
const node = getNode(parseInt(id));
|
} else if (line.includes('!')) {
|
||||||
node.entity = parseInt(entity);
|
const entity = line.slice(1);
|
||||||
} else {
|
currentNode!.exactEntity = parseInt(entity);
|
||||||
const [id, chars, child] = line.split('-');
|
} else if (line.startsWith('-')) {
|
||||||
const parent = getNode(parseInt(id));
|
const [, chars, child] = line.split('-');
|
||||||
const childNode = getNode(parseInt(child));
|
const childNode = getNode(parseInt(child));
|
||||||
for (const char of chars) {
|
for (const char of chars) {
|
||||||
parent.children.set(char, childNode);
|
currentNode!.children.set(char, childNode);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
currentNode = getNode(parseInt(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
<footer class="bg-[#444] py-3">
|
<footer class="bg-[#444] py-3">
|
||||||
<div class="c flex justify-between gap-4">
|
<div class="c flex justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
By <a href="https://jkob.cc">S52KJ</a>
|
Website by <a href="https://jkob.cc/">S52KJ</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Data from <a href="https://clublog.org">ClubLog</a>
|
Data by <a href="https://www.country-files.com/">AD1C</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Source code on <a href="https://github.com/jakobkordez/call-tester">GitHub</a>
|
Source code on <a href="https://github.com/jakobkordez/call-tester">GitHub</a>
|
||||||
|
@ -1,29 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { getSecondarySuffixDescription, parseCallsign } from '$lib/callsign';
|
import { getSecondarySuffixDescription, parseCallsign } from '$lib/callsign';
|
||||||
import { dxccEntities, findDxcc } from '$lib/dxcc-util';
|
import { dxccEntities, findDxcc } from '$lib/dxcc-util';
|
||||||
import CallsignInput from '../components/callsign-input.svelte';
|
import CallsignInput from '../components/callsign-input.svelte';
|
||||||
|
|
||||||
let callsign = '9a/s52kj/p';
|
let query = new URLSearchParams($page.url.searchParams.toString());
|
||||||
|
|
||||||
|
let callsign = query.get('c') ?? '';
|
||||||
|
|
||||||
$: callsignData = parseCallsign(callsign);
|
$: callsignData = parseCallsign(callsign);
|
||||||
$: rawDxcc = findDxcc(callsign);
|
$: rawDxcc = findDxcc(callsign);
|
||||||
|
|
||||||
$: suffixPartOf = [null, 'base', 'prefix'].indexOf(callsignData?.suffixPartOf ?? null);
|
$: updateUrl(callsign);
|
||||||
|
|
||||||
|
function updateUrl(callsign: string) {
|
||||||
|
if (!callsign) goto('.', { keepFocus: true });
|
||||||
|
else goto(`?c=${callsign}`, { keepFocus: true });
|
||||||
|
}
|
||||||
|
|
||||||
function styleText(): string {
|
function styleText(): string {
|
||||||
const baseClass = 'text-red-400';
|
const baseClass = 'text-cyan-300';
|
||||||
const prefixClass = 'text-blue-400';
|
const prefixClass = 'text-blue-400';
|
||||||
|
const suffixClass = 'text-green-400';
|
||||||
|
|
||||||
if (!callsignData) {
|
if (!callsignData) {
|
||||||
const dxcc = findDxcc(callsign);
|
const dxcc = findDxcc(callsign);
|
||||||
if (!dxcc) return callsign;
|
if (!dxcc) return callsign;
|
||||||
return `<span class="${baseClass}">${callsign.slice(0, dxcc.prefixLength)}</span>${callsign.slice(dxcc.prefixLength)}`;
|
return `<span class="text-cyan-300">${callsign.slice(0, dxcc.matchLength)}</span>${callsign.slice(dxcc.matchLength)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawDxcc?.isExact) {
|
||||||
|
return `<span class="text-amber-400">${callsign}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffix } = callsignData;
|
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffix } = callsignData;
|
||||||
|
|
||||||
// TODO Check if base and prefix same dxcc
|
// TODO Check if base and prefix same dxcc
|
||||||
const suffixClass = ['text-green-400', baseClass, prefixClass][suffixPartOf];
|
|
||||||
return [
|
return [
|
||||||
secondaryPrefix ? `<span class="${prefixClass}">${secondaryPrefix}/</span>` : '',
|
secondaryPrefix ? `<span class="${prefixClass}">${secondaryPrefix}/</span>` : '',
|
||||||
basePrefix ? `<span class="${baseClass}">${basePrefix}</span>${baseSuffix}` : base,
|
basePrefix ? `<span class="${baseClass}">${basePrefix}</span>${baseSuffix}` : base,
|
||||||
@ -40,7 +53,13 @@
|
|||||||
<CallsignInput bind:inputText={callsign} generateStyledText={styleText} />
|
<CallsignInput bind:inputText={callsign} generateStyledText={styleText} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if callsignData}
|
{#if rawDxcc?.isExact}
|
||||||
|
<div class="data-box full">
|
||||||
|
<h2>Full match</h2>
|
||||||
|
<div class="font-mono text-2xl font-medium">{callsign}</div>
|
||||||
|
<div>{dxccEntities.get(rawDxcc.entityId)?.name ?? '?'}</div>
|
||||||
|
</div>
|
||||||
|
{:else if callsignData}
|
||||||
<div class="flex flex-col gap-4 md:flex-row">
|
<div class="flex flex-col gap-4 md:flex-row">
|
||||||
{#if callsignData.prefixDxcc}
|
{#if callsignData.prefixDxcc}
|
||||||
<div class="data-box prefix">
|
<div class="data-box prefix">
|
||||||
@ -57,34 +76,37 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if callsignData.secondarySuffix}
|
{#if callsignData.secondarySuffix}
|
||||||
<div class={`data-box ${['suffix', 'base', 'prefix'][suffixPartOf]}`}>
|
<div class="data-box suffix">
|
||||||
<h2>Secondary suffix</h2>
|
<h2>Secondary suffix</h2>
|
||||||
<div class="font-mono text-2xl font-medium">{callsignData.secondarySuffix}</div>
|
<div class="font-mono text-2xl font-medium">{callsignData.secondarySuffix}</div>
|
||||||
<div>{getSecondarySuffixDescription(callsignData) ?? ''}</div>
|
<div>{getSecondarySuffixDescription(callsignData.secondarySuffix) ?? ''}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if rawDxcc}
|
{:else if rawDxcc}
|
||||||
<div class="data-box base">
|
<div class="data-box base">
|
||||||
<h2>Prefix</h2>
|
<h2>Prefix</h2>
|
||||||
<div class="font-mono text-2xl font-medium">{callsign.slice(0, rawDxcc.prefixLength)}</div>
|
<div class="font-mono text-2xl font-medium">{callsign.slice(0, rawDxcc.matchLength)}</div>
|
||||||
<div>{dxccEntities.get(rawDxcc.entity)?.name ?? '?'}</div>
|
<div>{dxccEntities.get(rawDxcc.entityId)?.name ?? '?'}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.data-box {
|
.data-box {
|
||||||
@apply flex-1 rounded-xl p-4 text-center;
|
@apply mx-auto w-full max-w-[50%] flex-1 rounded-xl p-4 text-center;
|
||||||
}
|
}
|
||||||
.data-box > h2 {
|
.data-box > h2 {
|
||||||
@apply text-sm;
|
@apply text-sm;
|
||||||
}
|
}
|
||||||
|
.data-box.full {
|
||||||
|
@apply bg-amber-600/40;
|
||||||
|
}
|
||||||
.data-box.prefix {
|
.data-box.prefix {
|
||||||
@apply bg-blue-600/40;
|
@apply bg-blue-600/40;
|
||||||
}
|
}
|
||||||
.data-box.base {
|
.data-box.base {
|
||||||
@apply bg-red-600/40;
|
@apply bg-cyan-600/40;
|
||||||
}
|
}
|
||||||
.data-box.suffix {
|
.data-box.suffix {
|
||||||
@apply bg-green-600/40;
|
@apply bg-green-600/40;
|
||||||
|
Reference in New Issue
Block a user