using System.Globalization; using Nonemm.Core; namespace Nonemm.Spotting; /// Reads the `DX de` lines a cluster node sends. public static class SpotLine { private const string Marker = "DX de "; /// Null for any other traffic from the node, which the packet window shows /// as it arrived. public static Spot? Parse(string line, DateTime nowUtc) { int start = line.IndexOf(Marker, StringComparison.OrdinalIgnoreCase); if (start < 0) { return null; } string body = line[(start + Marker.Length)..]; int colon = body.IndexOf(':'); string[] parts = (colon < 0 ? body : body[(colon + 1)..]) .Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) { return null; } // without a colon the spotter is the first word and the frequency follows string spotter = colon >= 0 ? body[..colon].Trim() : parts[0]; string[] fields = colon >= 0 ? parts : parts[1..]; if (fields.Length < 2 || !double.TryParse(fields[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double kilohertz)) { return null; } string[] tail = fields[2..]; int stamp = LastTimeStamp(tail); Frequency where = Frequency.FromKilohertz(kilohertz); string comment = string.Join(' ', stamp < 0 ? tail : tail[..stamp]).Trim(); return new Spot( Callsign.Parse(fields[1]), where, stamp < 0 ? nowUtc : TimeOf(tail[stamp], nowUtc), SpotSource.Cluster, spotter, comment) { Qsx = Qsx.Parse(comment, where), }; } /// The node writes the spot's time as `1234Z`. It is usually the last word, /// but DXSpider puts the spotter's grid or country after it. private static int LastTimeStamp(IReadOnlyList words) { for (int at = words.Count - 1; at >= 0; at--) { if (IsTimeStamp(words[at])) { return at; } } return -1; } private static bool IsTimeStamp(string word) => word.Length == 5 && char.ToUpperInvariant(word[4]) == 'Z' && word[..4].All(char.IsAsciiDigit); private static DateTime TimeOf(string stamp, DateTime nowUtc) { int hour = int.Parse(stamp[..2], CultureInfo.InvariantCulture); int minute = int.Parse(stamp[2..4], CultureInfo.InvariantCulture); if (hour > 23 || minute > 59) { return nowUtc; } DateTime at = new(nowUtc.Year, nowUtc.Month, nowUtc.Day, hour, minute, 0, DateTimeKind.Utc); // a spot timed later than now came in just before midnight return at > nowUtc.AddMinutes(5) ? at.AddDays(-1) : at; } }