using System.Globalization; using Microsoft.Data.Sqlite; using Nonemm.Core; namespace Nonemm.Storage; /// Turns a contact into N1MM's `DXLOG` columns and back. Points and multiplier /// flags are written because N1MM reads them, and ignored on the way back /// because this program works them out from the rules. internal static class QsoColumns { internal const string TimestampFormat = "yyyy-MM-dd HH:mm:ss"; internal static readonly IReadOnlyList Names = [ "TS", "Call", "Freq", "QSXFreq", "Mode", "ContestName", "SNT", "RCV", "CountryPrefix", "StationPrefix", "QTH", "Name", "Comment", "NR", "Sect", "Prec", "CK", "ZN", "SentNr", "Points", "IsMultiplier1", "IsMultiplier2", "Power", "Band", "WPXPrefix", "Exchange1", "RadioNR", "ContestNR", "isMultiplier3", "MiscText", "IsRunQSO", "ContactType", "Run1Run2", "GridSquare", "Operator", "Continent", "RoverLocation", "RadioInterfaced", "NetworkedCompNr", "NetBiosName", "IsOriginal", "ID", "CLAIMEDQSO", ]; internal static void Bind(SqliteCommand command, Qso qso) { command.Parameters.AddWithValue("@TS", qso.TimestampUtc.ToString(TimestampFormat, CultureInfo.InvariantCulture)); command.Parameters.AddWithValue("@Call", qso.Call.Text); command.Parameters.AddWithValue("@Freq", qso.Frequency.Kilohertz); command.Parameters.AddWithValue("@QSXFreq", qso.QsxFrequency.Kilohertz); command.Parameters.AddWithValue("@Mode", qso.Mode.Name); command.Parameters.AddWithValue("@ContestName", qso.ContestName); command.Parameters.AddWithValue("@SNT", qso.SentReport); command.Parameters.AddWithValue("@RCV", qso.ReceivedReport); command.Parameters.AddWithValue("@CountryPrefix", qso.CountryPrefix); command.Parameters.AddWithValue("@StationPrefix", qso.StationPrefix); command.Parameters.AddWithValue("@QTH", qso.Qth); command.Parameters.AddWithValue("@Name", qso.Name); command.Parameters.AddWithValue("@Comment", qso.Comment); command.Parameters.AddWithValue("@NR", qso.ReceivedNumber); command.Parameters.AddWithValue("@Sect", qso.Section); command.Parameters.AddWithValue("@Prec", qso.Precedence); command.Parameters.AddWithValue("@CK", qso.Check); command.Parameters.AddWithValue("@ZN", qso.Zone); command.Parameters.AddWithValue("@SentNr", qso.SentNumber); command.Parameters.AddWithValue("@Points", qso.Points); command.Parameters.AddWithValue("@IsMultiplier1", qso.IsMultiplier1 ? 1 : 0); command.Parameters.AddWithValue("@IsMultiplier2", qso.IsMultiplier2 ? 1 : 0); command.Parameters.AddWithValue("@Power", qso.Power); command.Parameters.AddWithValue("@Band", qso.Band?.MegahertzLabel ?? 0.0); command.Parameters.AddWithValue("@WPXPrefix", qso.WpxPrefix); command.Parameters.AddWithValue("@Exchange1", qso.Exchange1); command.Parameters.AddWithValue("@RadioNR", qso.RadioNumber); command.Parameters.AddWithValue("@ContestNR", qso.ContestNumber); command.Parameters.AddWithValue("@isMultiplier3", qso.IsMultiplier3 ? 1 : 0); command.Parameters.AddWithValue("@MiscText", qso.MiscText); command.Parameters.AddWithValue("@IsRunQSO", qso.IsRunQso ? 1 : 0); command.Parameters.AddWithValue("@ContactType", qso.ContactType); command.Parameters.AddWithValue("@Run1Run2", qso.RunPosition); command.Parameters.AddWithValue("@GridSquare", qso.GridSquare); command.Parameters.AddWithValue("@Operator", qso.Operator); command.Parameters.AddWithValue("@Continent", qso.Continent); command.Parameters.AddWithValue("@RoverLocation", qso.RoverLocation); command.Parameters.AddWithValue("@RadioInterfaced", qso.IsRadioInterfaced ? 1 : 0); command.Parameters.AddWithValue("@NetworkedCompNr", qso.NetworkedComputerNumber); command.Parameters.AddWithValue("@NetBiosName", qso.StationName); command.Parameters.AddWithValue("@IsOriginal", qso.IsOriginal ? 1 : 0); command.Parameters.AddWithValue("@ID", qso.Id); command.Parameters.AddWithValue("@CLAIMEDQSO", qso.IsClaimed ? 1 : 0); } internal static Qso Read(SqliteDataReader row) => new() { Id = Text(row, "ID"), TimestampUtc = Timestamp(row, "TS"), Call = Callsign.Parse(Text(row, "Call")), Frequency = Frequency.FromKilohertz(Number(row, "Freq")), QsxFrequency = Frequency.FromKilohertz(Number(row, "QSXFreq")), Mode = Modes.Parse(Text(row, "Mode")) ?? Modes.Digital, ContestName = Text(row, "ContestName"), ContestNumber = Integer(row, "ContestNR"), SentReport = Text(row, "SNT"), ReceivedReport = Text(row, "RCV"), SentNumber = Integer(row, "SentNr"), ReceivedNumber = Integer(row, "NR"), Section = Text(row, "Sect"), Precedence = Text(row, "Prec"), Check = Integer(row, "CK"), Zone = Integer(row, "ZN"), Exchange1 = Text(row, "Exchange1"), MiscText = Text(row, "MiscText"), Comment = Text(row, "Comment"), Name = Text(row, "Name"), Qth = Text(row, "QTH"), Power = Text(row, "Power"), GridSquare = Text(row, "GridSquare"), RoverLocation = Text(row, "RoverLocation"), CountryPrefix = Text(row, "CountryPrefix"), StationPrefix = Text(row, "StationPrefix"), WpxPrefix = Text(row, "WPXPrefix"), Continent = Text(row, "Continent"), IsRunQso = Flag(row, "IsRunQSO"), ContactType = Text(row, "ContactType"), RunPosition = Integer(row, "Run1Run2"), Operator = Text(row, "Operator"), RadioNumber = Integer(row, "RadioNR"), IsRadioInterfaced = Flag(row, "RadioInterfaced"), NetworkedComputerNumber = Integer(row, "NetworkedCompNr"), StationName = Text(row, "NetBiosName"), IsOriginal = Flag(row, "IsOriginal"), IsClaimed = Flag(row, "CLAIMEDQSO"), }; private static string Text(SqliteDataReader row, string column) { int at = row.GetOrdinal(column); return row.IsDBNull(at) ? "" : row.GetString(at); } private static double Number(SqliteDataReader row, string column) { int at = row.GetOrdinal(column); return row.IsDBNull(at) ? 0 : row.GetDouble(at); } private static int Integer(SqliteDataReader row, string column) { int at = row.GetOrdinal(column); return row.IsDBNull(at) ? 0 : (int)row.GetInt64(at); } private static bool Flag(SqliteDataReader row, string column) => Integer(row, column) != 0; /// N1MM writes seconds; older rows and other writers add fractions or a /// `T` separator, so anything round-trippable is accepted. private static DateTime Timestamp(SqliteDataReader row, string column) { string text = Text(row, column); return DateTime.TryParse( text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out DateTime parsed) ? parsed : throw new FormatException($"contact timestamp '{text}' is not a date"); } }