List the earlier contacts with the call being typed under the log
N1MM's log window has two panes. The lower one lists every contact already logged with the call in the callsign box, on any band and in any mode, so an apparent dupe can be read off the screen rather than looked for. EarlierContacts.Matching holds the rule, so it is unit-tested without a window: three characters before anything is listed, a call that holds what is typed, `*` for any run of characters and `?` for one, the grid square instead when nothing is typed, ordered by call, mode, band and time, and fifty at most. That is N1MM's DupeSQL, except that N1MM passes `?` to SQLite as a plain character, which finds nothing. The two grids share their columns, because a value in the pane has to sit under the column it belongs to. Each column is now as wide as the longest value it can hold, measured in the font it is drawn in, and the last column takes what is left over. The values and the header are in different fonts, so each is measured on its own. This also settles the column widths, which used to change as rows scrolled into view. A DataGrid does not pass its own font down to its cells, so the cell font is set in a style. Without that the cells draw larger than the width measured for them and every column is a character short. Looked at under Xvfb with a call typed: the pane lists the two contacts that hold it, under columns that line up with the log above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// The contacts already logged with the callsign being typed. N1MM lists them
|
||||
/// under the log, so an apparent dupe can be read off the screen: which band,
|
||||
/// which mode, and how long ago.
|
||||
public static class EarlierContacts
|
||||
{
|
||||
/// N1MM's limit on the list.
|
||||
public const int Limit = 50;
|
||||
|
||||
/// Two characters match too much of any log to be worth listing.
|
||||
public const int ShortestQuery = 3;
|
||||
|
||||
/// With a call typed, the contacts whose call holds it. With nothing typed,
|
||||
/// the contacts whose grid square holds the one typed for the station being
|
||||
/// worked; with neither, nothing.
|
||||
///
|
||||
/// `*` stands for any run of characters and `?` for one, as in the check
|
||||
/// window. N1MM passes `?` to SQLite, which reads it as a plain character;
|
||||
/// that finds nothing, because no callsign holds a question mark.
|
||||
public static IReadOnlyList<Qso> Matching(
|
||||
IReadOnlyList<Qso> logged,
|
||||
string typed,
|
||||
string gridSquare = "",
|
||||
int limit = Limit)
|
||||
{
|
||||
string query = typed.Trim().ToUpperInvariant();
|
||||
string grid = gridSquare.Trim();
|
||||
IEnumerable<Qso> found;
|
||||
if (query.Length == 0)
|
||||
{
|
||||
if (grid.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
found = logged.Where(q => q.GridSquare.Contains(grid, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else if (query.Length < ShortestQuery)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
else if (query.Contains('*') || query.Contains('?'))
|
||||
{
|
||||
found = logged.Where(q => MatchesPattern(query, q.Call.Text));
|
||||
}
|
||||
else
|
||||
{
|
||||
found = logged.Where(q => q.Call.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
return
|
||||
[
|
||||
.. found
|
||||
.OrderBy(q => q.Call.Text, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(q => q.Mode.Name, StringComparer.Ordinal)
|
||||
.ThenBy(q => q.Frequency.Hertz)
|
||||
.ThenBy(q => q.TimestampUtc)
|
||||
.Take(limit),
|
||||
];
|
||||
}
|
||||
|
||||
/// A wildcard pattern covers the whole call, as N1MM's `like` does.
|
||||
private static bool MatchesPattern(string pattern, string call)
|
||||
{
|
||||
int at = 0;
|
||||
int character = 0;
|
||||
int lastStar = -1;
|
||||
int afterStar = 0;
|
||||
while (character < call.Length)
|
||||
{
|
||||
if (at < pattern.Length &&
|
||||
(pattern[at] == '?' || pattern[at] == char.ToUpperInvariant(call[character])))
|
||||
{
|
||||
at++;
|
||||
character++;
|
||||
}
|
||||
else if (at < pattern.Length && pattern[at] == '*')
|
||||
{
|
||||
lastStar = at++;
|
||||
afterStar = character;
|
||||
}
|
||||
else if (lastStar >= 0)
|
||||
{
|
||||
// the run the last `*` stood for was too short: give it one more
|
||||
at = lastStar + 1;
|
||||
character = ++afterStar;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (at < pattern.Length && pattern[at] == '*')
|
||||
{
|
||||
at++;
|
||||
}
|
||||
return at == pattern.Length;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user