using System.Globalization; using System.Text; using Nonemm.Core; namespace Nonemm.Network; /// The checksum of one block of contacts. Two stations compare these to find /// out whether they hold the same log without sending the log. /// /// It is N1MM's: a CRC32 over the `DXLOG` columns of every contact in the /// block, in the order the columns are read out of the database, oldest first /// and by callsign within a timestamp. The columns this program works out from /// the rules — points, the three multiplier flags, the country prefix, the /// continent, the WPX prefix and `IsOriginal` — are left out, because the two /// stations are allowed to disagree about them. `ContestNR` counts only when it /// is -1. /// /// The checksum is over what a station stored, so a field that one program /// writes as empty and the other as a space is a difference. N1MM writes a /// single space into `GridSquare`, `MiscText`, `ContactType` and /// `RoverLocation` when it has nothing for them, so an empty value in those /// four is hashed as a space. /// /// A checksum that disagrees is not a fault: the block is sent again, which is /// what a resync is for. It only costs traffic. public static class LogChecksum { public static int Of(IEnumerable contacts) { StringBuilder text = new(); IEnumerable ordered = contacts .OrderBy(q => q.TimestampUtc) .ThenBy(q => q.Call.Text, StringComparer.Ordinal); foreach (Qso qso in ordered) { Append(text, qso); } return Crc32.Of(text.ToString()); } /// The contacts of one station inside one block of time. Both ends of a /// resync pick the same rows this way. public static IReadOnlyList Block( IEnumerable contacts, string computerName, DateTime fromUtc, TimeSpan length) => contacts .Where(q => string.Equals(q.StationName, computerName, StringComparison.OrdinalIgnoreCase) && q.TimestampUtc >= fromUtc && q.TimestampUtc < fromUtc + length) .OrderBy(q => q.TimestampUtc) .ThenBy(q => q.Call.Text, StringComparer.Ordinal) .ToList(); private static void Append(StringBuilder text, Qso qso) { text.Append(qso.TimestampUtc.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); text.Append(qso.Call.Text); Number(text, qso.Frequency.Kilohertz); Number(text, qso.QsxFrequency.Kilohertz); text.Append(qso.Mode.Name); text.Append(qso.ContestName); text.Append(qso.SentReport); text.Append(qso.ReceivedReport); text.Append(qso.StationPrefix); text.Append(qso.Qth); text.Append(qso.Name); text.Append(qso.Comment); Number(text, qso.ReceivedNumber); text.Append(qso.Section); text.Append(qso.Precedence); Number(text, qso.Check); Number(text, qso.Zone); Number(text, qso.SentNumber); text.Append(qso.Power); Number(text, qso.Band?.MegahertzLabel ?? 0); text.Append(qso.Exchange1); Number(text, qso.RadioNumber); text.Append(qso.Operator); text.Append(Filled(qso.GridSquare)); // the contest number counts only when it says the contact belongs to no // contest if (qso.ContestNumber == -1) { text.Append("-1"); } text.Append(Filled(qso.MiscText)); text.Append(Filled(qso.ContactType)); Number(text, qso.RunPosition); text.Append(Filled(qso.RoverLocation)); Flag(text, qso.IsRadioInterfaced); Number(text, qso.NetworkedComputerNumber); text.Append(qso.StationName); Flag(text, qso.IsRunQso); text.Append(qso.Id); Flag(text, qso.IsClaimed); } /// A column N1MM stores as a single space when it has nothing for it. private static string Filled(string value) => value.Length == 0 ? " " : value; private static void Number(StringBuilder text, double value) => text.Append(value.ToString(CultureInfo.InvariantCulture)); private static void Number(StringBuilder text, int value) => text.Append(value.ToString(CultureInfo.InvariantCulture)); private static void Flag(StringBuilder text, bool value) => text.Append(value ? '1' : '0'); }