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
@@ -111,6 +111,17 @@ public class QsoEditorTests
Assert.Contains("callsign", edit.Error);
}
/// An edit can move a contact to a band where it is no longer a dupe, so
/// the mark comes off and the log works it out again. N1MM works it out
/// again at the same point.
[Fact]
public void EditingAContactClearsTheContactType()
{
QsoEdit edit = CqWw().Apply(Contact() with { ContactType = "D" }, QsoField.Frequency, "21005");
Assert.Equal("", edit.Result!.ContactType);
}
[Fact]
public void ARefusedEditReturnsNoContact() =>
Assert.Null(CqWw().Apply(Contact(), QsoField.Zone, "41").Result);
@@ -187,6 +187,21 @@ public class QtcTrafficTests
Assert.Equal(87, traffic.NextSeries());
}
/// A contact that is a dupe is not worth reporting, and it is what N1MM
/// leaves out of the pile with `Trim(ContactType) = ''`.
[Fact]
public void ADupeContactIsNotReported()
{
(OperatingPosition position, QtcTraffic traffic) = Station(ModeCategory.Digital);
Contact(position, "DL1AAA", receivedNumber: 16, sentNumber: 1, minute: 46);
Contact(position, "DL1AAA", receivedNumber: 16, sentNumber: 2, minute: 47);
IReadOnlyList<WaeQtc> lines = traffic.ToSend(Callsign.Parse("JA1XYZ"));
Assert.Equal(["DL1AAA"], lines.Select(l => l.Call.Text).ToList());
Assert.Equal(1, traffic.Available(Callsign.Parse("JA1XYZ")));
}
/// Europe on CW and SSB only takes traffic down, so there is no pile of
/// contacts to report and the line says how many of the ten are left.
[Fact]