using Nonemm.Contests; namespace Nonemm.Session; /// What a word clicked in the receive pane is worth. N1MM drops a clicked word /// into whichever exchange box has the cursor. That box still comes first here /// when it takes the word, but a word the cursor's box does not take goes to /// the box of the contest's exchange that holds that kind of value, so a serial /// number and a state land where they belong. public static class DigitalElement { private const string Precedences = "QABUMS"; /// Strips what RTTY carries around a value: the report, a colon, and the Z /// on a four-figure time. N1MM cuts the same three before it takes an /// element from a click. public static string Trim(string word) { string text = DigitalReceiver.Scrub(word).Replace(":", ""); if (text.Length > 3 && text.Contains("599")) { text = text.Replace("599", ""); } if (text.Length == 5 && text[^1] == 'Z' && text[..4].All(char.IsAsciiDigit)) { text = text[..4]; } return text; } /// True when the box holds this kind of value. A report box is never a /// target: nobody clicks 599 in, and the program fills it in itself. public static bool Fits(ExchangeField field, string word) => field.Kind switch { ExchangeFieldKind.Report => false, ExchangeFieldKind.Number => IsNumber(word, 1, 6), ExchangeFieldKind.CqZone => IsNumber(word, 1, 2) && InRange(word, 1, 40), ExchangeFieldKind.ItuZone => IsNumber(word, 1, 2) && InRange(word, 1, 90), ExchangeFieldKind.Check => IsNumber(word, 2, 2), ExchangeFieldKind.Power => IsPower(word), ExchangeFieldKind.Precedence => word.Length == 1 && Precedences.Contains(word[0]), ExchangeFieldKind.Grid => IsGrid(word), ExchangeFieldKind.Text => word.Length > 0, _ => ExchangeValues.For(field.Kind).Contains(word, StringComparer.OrdinalIgnoreCase), }; /// The exchange box the word belongs in, counted the way `EntryFields` /// counts: 0 is the callsign box, 1 and up are the exchange boxes. Null /// when the contest has no box that takes it. public static int? FieldFor(EntryFields fields, string word) { if (word.Length == 0) { return null; } // the box the cursor is in wins when it takes the word: that is where // N1MM puts it, and the operator has already said which box it is if (fields.Focus > 0 && Fits(fields.Exchange[fields.Focus - 1], word)) { return fields.Focus; } // then the box that only holds this kind of value, then a number box, // then plain text, so a check does not land in the serial number box // and a section does not land in the name box return Pick(fields, word, ListedOrShaped) ?? Pick(fields, word, kind => kind == ExchangeFieldKind.Number) ?? Pick(fields, word, kind => kind == ExchangeFieldKind.Text); } private static bool ListedOrShaped(ExchangeFieldKind kind) => kind is not (ExchangeFieldKind.Number or ExchangeFieldKind.Text or ExchangeFieldKind.Report); private static int? Pick(EntryFields fields, string word, Func wanted) { int? filled = null; for (int at = 0; at < fields.Exchange.Count; at++) { ExchangeField field = fields.Exchange[at]; if (!wanted(field.Kind) || !Fits(field, word)) { continue; } if (fields[at + 1].Trim().Length == 0) { return at + 1; } // every box that takes it is full, so the first one is written over filled ??= at + 1; } return filled; } private static bool IsNumber(string word, int shortest, int longest) => word.Length >= shortest && word.Length <= longest && word.All(char.IsAsciiDigit); private static bool InRange(string word, int lowest, int highest) => int.TryParse(word, out int value) && value >= lowest && value <= highest; /// A power is the figures, with or without the W or KW after them. private static bool IsPower(string word) { string figures = word.EndsWith("KW", StringComparison.Ordinal) ? word[..^2] : word.EndsWith('W') ? word[..^1] : word; return IsNumber(figures, 1, 4); } /// Four or six characters: two letters, two figures, and a letter pair on a /// six-character locator. private static bool IsGrid(string word) => word.Length is 4 or 6 && char.IsAsciiLetter(word[0]) && char.IsAsciiLetter(word[1]) && char.IsAsciiDigit(word[2]) && char.IsAsciiDigit(word[3]) && (word.Length == 4 || (char.IsAsciiLetter(word[4]) && char.IsAsciiLetter(word[5]))); }