Read the zone lists in the country file
The # blocks in wl_cty.dat list CQ and ITU zones by call area and the first letter after the digit, written with the main prefix letters. A call now takes them whatever its own prefix letters are, so RA9Y and RM9I get what UA9Y gets, as in N1MM. A call whose own prefix rule gives a zone, and a whole-call entry, are left as they are. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Nonemm.Core.Country;
|
||||
|
||||
/// The country file (`cty.dat` or `wl_cty.dat`), which says which entity, zone
|
||||
/// and continent a callsign belongs to.
|
||||
public sealed class CountryFile
|
||||
public sealed partial class CountryFile
|
||||
{
|
||||
private readonly Dictionary<string, PrefixRule> exactCalls;
|
||||
private readonly Dictionary<string, PrefixRule> prefixes;
|
||||
private readonly Dictionary<DxccEntity, List<PrefixRule>> zoneRules;
|
||||
private readonly int longestPrefix;
|
||||
|
||||
private CountryFile(
|
||||
IReadOnlyList<DxccEntity> entities,
|
||||
Dictionary<string, PrefixRule> exactCalls,
|
||||
Dictionary<string, PrefixRule> prefixes,
|
||||
Dictionary<DxccEntity, List<PrefixRule>> zoneRules,
|
||||
string version)
|
||||
{
|
||||
Entities = entities;
|
||||
Version = version;
|
||||
this.exactCalls = exactCalls;
|
||||
this.prefixes = prefixes;
|
||||
this.zoneRules = zoneRules;
|
||||
longestPrefix = prefixes.Count == 0 ? 0 : prefixes.Keys.Max(k => k.Length);
|
||||
}
|
||||
|
||||
@@ -38,16 +42,37 @@ public sealed class CountryFile
|
||||
List<DxccEntity> entities = [];
|
||||
Dictionary<string, PrefixRule> exactCalls = new(StringComparer.Ordinal);
|
||||
Dictionary<string, PrefixRule> prefixes = new(StringComparer.Ordinal);
|
||||
Dictionary<DxccEntity, List<PrefixRule>> zoneRules = [];
|
||||
DxccEntity? current = null;
|
||||
string version = "";
|
||||
bool isInZoneBlock = false;
|
||||
|
||||
foreach (string rawLine in text.Split('\n'))
|
||||
{
|
||||
string line = rawLine.TrimEnd('\r', ' ', '\t');
|
||||
if (line.Length == 0 || line.StartsWith('#'))
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith('#'))
|
||||
{
|
||||
// `# UA9: UA9H(18)[31],...;` is a comment to a program that
|
||||
// ignores it and a list of zones by call area to one that
|
||||
// reads it. It can run on over the lines that follow.
|
||||
if (current is not null && ZoneBlockStart.Match(line) is { Success: true } start)
|
||||
{
|
||||
AddZoneRules(zoneRules, current, start.Groups[1].Value);
|
||||
isInZoneBlock = !line.EndsWith(';');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isInZoneBlock && char.IsWhiteSpace(rawLine[0]) && current is not null)
|
||||
{
|
||||
// the lines the block runs over are also read as aliases below,
|
||||
// which is what makes UA9X a European Russia prefix
|
||||
AddZoneRules(zoneRules, current, line);
|
||||
isInZoneBlock = !line.EndsWith(';');
|
||||
}
|
||||
if (!char.IsWhiteSpace(rawLine[0]))
|
||||
{
|
||||
current = ParseHeader(line);
|
||||
@@ -75,7 +100,7 @@ public sealed class CountryFile
|
||||
{
|
||||
throw new FormatException("country file holds no entities");
|
||||
}
|
||||
return new CountryFile(entities, exactCalls, prefixes, version);
|
||||
return new CountryFile(entities, exactCalls, prefixes, zoneRules, version);
|
||||
}
|
||||
|
||||
public CountryLookup? Find(Callsign call)
|
||||
@@ -94,9 +119,73 @@ public sealed class CountryFile
|
||||
}
|
||||
return IsGuantanamoException(call)
|
||||
? FindByPrefix("K")
|
||||
: FindByPrefix(call.EntityLookupText());
|
||||
: FindByPrefix(call.EntityLookupText()) is { } found
|
||||
? WithZoneByCallArea(found, call.EntityLookupText())
|
||||
: null;
|
||||
}
|
||||
|
||||
/// The zone block of the file (`# UA9: UA9H(18)[31],...`) lists zones by
|
||||
/// call area and the first letter after the digit, written with the
|
||||
/// prefix letters of the entity's main prefix. N1MM reads it for every
|
||||
/// call of the entity, whatever its own prefix letters are, so RA9Y and
|
||||
/// RM9I get what UA9Y gets. A call whose own prefix rule already gives a
|
||||
/// zone, such as VO2, is left as it is.
|
||||
private CountryLookup WithZoneByCallArea(CountryLookup found, string text)
|
||||
{
|
||||
if (!zoneRules.TryGetValue(found.Entity, out List<PrefixRule>? rules)
|
||||
|| found.CqZone != found.Entity.CqZone
|
||||
|| found.ItuZone != found.Entity.ItuZone)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
int digit = text.IndexOfAny(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||
if (digit < 1)
|
||||
{
|
||||
return found;
|
||||
}
|
||||
string tail = text[digit..];
|
||||
foreach (PrefixRule rule in rules)
|
||||
{
|
||||
int rulesDigit = rule.Key.IndexOfAny(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||
if (rulesDigit >= 1 && (rule.Key[..rulesDigit] + tail).StartsWith(rule.Key, StringComparison.Ordinal))
|
||||
{
|
||||
return found with
|
||||
{
|
||||
CqZone = rule.CqZoneOverride ?? found.CqZone,
|
||||
ItuZone = rule.ItuZoneOverride ?? found.ItuZone,
|
||||
};
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private static void AddZoneRules(
|
||||
Dictionary<DxccEntity, List<PrefixRule>> zoneRules,
|
||||
DxccEntity entity,
|
||||
string list)
|
||||
{
|
||||
if (!zoneRules.TryGetValue(entity, out List<PrefixRule>? rules))
|
||||
{
|
||||
rules = [];
|
||||
zoneRules[entity] = rules;
|
||||
}
|
||||
foreach (string item in list.TrimEnd(';').Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
PrefixRule rule = PrefixRule.Parse(entity, item.Trim());
|
||||
if (rule.Key.Length > 0)
|
||||
{
|
||||
rules.Add(rule);
|
||||
}
|
||||
}
|
||||
// the longest prefix first, so VE3 is read before a shorter VE
|
||||
rules.Sort((a, b) => b.Key.Length.CompareTo(a.Key.Length));
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^#\s+\w+:\s*(.*)$")]
|
||||
private static partial Regex ZoneBlockStartPattern();
|
||||
|
||||
private static readonly Regex ZoneBlockStart = ZoneBlockStartPattern();
|
||||
|
||||
/// The country file lists `KG4` as Guantanamo Bay, but only a three or five
|
||||
/// character call is Guantanamo: KG4NE is, KG4IGC is an ordinary US call.
|
||||
/// The file cannot say that, so every program that reads it carries the
|
||||
|
||||
@@ -33,6 +33,12 @@ public sealed class PrefixRule
|
||||
|
||||
public string Key { get; }
|
||||
|
||||
/// The CQ zone the alias gives, or null when it leaves the entity's own.
|
||||
public int? CqZoneOverride => cqZone;
|
||||
|
||||
/// The ITU zone the alias gives, or null when it leaves the entity's own.
|
||||
public int? ItuZoneOverride => ituZone;
|
||||
|
||||
public static PrefixRule Parse(DxccEntity entity, string alias)
|
||||
{
|
||||
string key = alias;
|
||||
|
||||
Reference in New Issue
Block a user