Mark a dupe in the contact type column, as N1MM does

N1MM writes a contact type on every contact it logs: `D` for a station
already worked, `N` for one the entry may not work at all, and empty for an
ordinary contact. Nothing here wrote the column, so a dupe counted as a
contact worth no points instead of dropping out of the score, and the two
places that read the column did nothing: the QTC pile offered dupes that
N1MM leaves out, and the statistics window could not leave dupes out either.

The marking is in one place, `ContestLog.ApplyVerdict`, and every contest is
marked the same way. What changes between contests is what counts as a dupe
and which stations are workable, and both come from the contest, as they do
in N1MM's `Contact.IsDupe`. No contest module in N1MM marks differently. Our
own callsign is not workable either, which is what N1MM adds on top of the
contest rule.

Two details follow N1MM as well: QTC traffic keeps an empty column, because
its `IsDupe` only marks a row while the program is logging contacts; and a
mark that came with the contact stands, because N1MM marks a contact when it
logs or edits one and never clears marks in a pass over the log. Editing a
contact clears the column so it is worked out again, which is what N1MM does
at the same point: an edit can move a contact to a band where it is no
longer a dupe.

A dupe is now out of the score altogether — contacts, points and multipliers
— which is N1MM's `ScoreClass.AddContact`. The QSO count still counts WAE
QTC rows as contacts; that is the other half of the scoring audit's item 14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
2026-09-10 19:38:57 +00:00
co-authored by Claude Opus 5
parent 0d9b4a713e
commit e0f307968e
9 changed files with 175 additions and 7 deletions
+35 -2
View File
@@ -170,8 +170,41 @@ public sealed class ContestLog
History = History,
};
private static Qso ApplyVerdict(Qso qso, Verdict verdict) =>
ApplyMultiplierFlags(qso, verdict.NewMultipliers) with { Points = verdict.Points };
private Qso ApplyVerdict(Qso qso, Verdict verdict) =>
ApplyMultiplierFlags(qso, verdict.NewMultipliers) with
{
Points = verdict.Points,
ContactType = ContactTypeOf(qso),
};
/// N1MM's contact type column, which it writes in `Contact.IsDupe`: `D`
/// for a station already worked, `N` for one this entry may not work at
/// all, and empty for an ordinary contact. Every contest is marked the
/// same way; what changes between them is what counts as a dupe and which
/// stations are workable, and both of those come from the contest here as
/// they do there.
///
/// Traffic keeps an empty column, which is what N1MM's QTC window writes:
/// its `IsDupe` only marks a row while the program is logging contacts.
private string ContactTypeOf(Qso qso)
{
if (!contest.IsContact(qso))
{
return "";
}
// a mark that came with the contact stands: N1MM marks a contact when
// it logs or edits it and never clears the mark in a pass over the
// log, so a row that arrived from another station or from an N1MM
// database keeps what it was given
if (IsWorked(qso) || qso.ContactType.Equals("D", StringComparison.OrdinalIgnoreCase))
{
return "D";
}
// N1MM adds our own callsign to whatever the contest says
bool workable = contest.IsWorkable(ContextFor(qso))
&& !qso.Call.Text.Equals(me.Callsign, StringComparison.OrdinalIgnoreCase);
return workable ? "" : "N";
}
private static Qso ApplyMultiplierFlags(Qso qso, IReadOnlyList<Multiplier> claimed) => qso with
{