Add the Broadcast Data dialog and send N1MM's score breakdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Contests;
|
||||
|
||||
/// The multiplier names N1MM keeps in the `Contest` table, such as `ZN`,
|
||||
/// `CountryPrefix` and `Sect`. The score broadcast names its multiplier types
|
||||
/// from them, and a contest's own `MultiplierNamesFor` holds the names the
|
||||
/// score window shows instead (`Zones`, `Countries`). Taken from a log N1MM
|
||||
/// wrote, without the `N/A` entries.
|
||||
public static class N1mmMultiplierNames
|
||||
{
|
||||
private static readonly Dictionary<string, IReadOnlyList<string>> Known =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["CQWWCW"] = ["ZN", "CountryPrefix"],
|
||||
["CQWWSSB"] = ["ZN", "CountryPrefix"],
|
||||
["CQWWRTTY"] = ["ZN", "CountryPrefix", "Sect"],
|
||||
["CQWPXCW"] = ["WPXPrefix"],
|
||||
["CQWPXSSB"] = ["WPXPrefix"],
|
||||
["CQWPXRTTY"] = ["WPXPrefix"],
|
||||
["ARRLDXCW"] = ["Sect"],
|
||||
["ARRLDXSSB"] = ["Sect"],
|
||||
["IARU"] = ["ZN", "HQ"],
|
||||
["SSCW"] = ["Sect"],
|
||||
["SSSSB"] = ["Sect"],
|
||||
["WAECW"] = ["CountryPrefix"],
|
||||
["WAESSB"] = ["CountryPrefix"],
|
||||
["WAERTTY"] = ["CountryPrefix"],
|
||||
["ARRLRTTY"] = ["Sect", "CountryPrefix"],
|
||||
["NAQPCW"] = ["Sect", "NA"],
|
||||
["NAQPSSB"] = ["Sect", "NA"],
|
||||
["NAQPRTTY"] = ["Sect", "NA"],
|
||||
["OKOMDX"] = ["Sect"],
|
||||
["YOTA"] = ["Exchange"],
|
||||
["IOTA"] = ["Sect"],
|
||||
["EUDXC"] = ["Sect", "CountryPrefix"],
|
||||
["CQ160CW"] = ["StateProvince", "CountryPrefix"],
|
||||
["CQ160SSB"] = ["StateProvince", "CountryPrefix"],
|
||||
["XERTTY"] = ["Countryprefix", "Sect"],
|
||||
["REFCW"] = ["Sect", "CountryPrefix"],
|
||||
["REFSSB"] = ["Sect", "CountryPrefix"],
|
||||
["UKRAINDX"] = ["CountryPrefix", "Sect"],
|
||||
["BARTGRTTYS"] = ["CountryPrefix", "Sect", "Continent"],
|
||||
["ARRL10M"] = ["StateProvince", "CountryPrefix"],
|
||||
["RUSDXRTTY"] = ["CountryPrefix", "Sect"],
|
||||
["RDAC"] = ["CountryPrefix", "Sect"],
|
||||
["YOHFDX"] = ["CountryPrefix", "Sect"],
|
||||
["OCEANIACW"] = ["WPXPrefix"],
|
||||
["OCEANIASSB"] = ["WPXPrefix"],
|
||||
["FDREG1"] = ["CountryPrefix"],
|
||||
["SARTGRTTY"] = ["CountryPrefix", "Sect"],
|
||||
["ALLASIACW"] = ["WPXPrefix"],
|
||||
["ALLASIASSB"] = ["WPXPrefix"],
|
||||
["DX"] = ["CountryPrefix"],
|
||||
};
|
||||
|
||||
/// A contest not in the table gets the names its rules give.
|
||||
public static IReadOnlyList<string> For(Contest contest, StationInfo me) =>
|
||||
Known.TryGetValue(contest.Name, out IReadOnlyList<string>? names)
|
||||
? names
|
||||
: contest.MultiplierNamesFor(me);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Globalization;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Contests;
|
||||
|
||||
/// One row of N1MM's score query: the contacts of one band and mode, or of all
|
||||
/// of them when `Band` is `ScoreBreakdown.TotalBand`. `Kind` is empty except in
|
||||
/// WAE, which gives contacts and traffic a row each.
|
||||
public sealed record ScoreBreakdownRow(
|
||||
double Band,
|
||||
string Mode,
|
||||
string Kind,
|
||||
int Qsos,
|
||||
int Points,
|
||||
IReadOnlyList<int> Multipliers);
|
||||
|
||||
/// The score the way N1MM's database query splits it: a row per band and mode
|
||||
/// of the contact, with the multiplier flags added up, and a total row last
|
||||
/// when there is any contact.
|
||||
/// `ContestLog.Rows` splits by band alone, which is what the score window shows.
|
||||
public static class ScoreBreakdown
|
||||
{
|
||||
/// The band N1MM gives the total row.
|
||||
public const double TotalBand = 900_000_000;
|
||||
|
||||
public const string TotalMode = "Both";
|
||||
|
||||
/// Ordered the way the query orders them: by the band as text, so `10.0`
|
||||
/// comes before `3.5`, then by mode. Only contacts in the claimed score
|
||||
/// are counted.
|
||||
public static IReadOnlyList<ScoreBreakdownRow> For(IEnumerable<Qso> qsos, Contest contest)
|
||||
{
|
||||
List<Qso> counted = qsos.Where(q => q.IsInScore).ToList();
|
||||
List<ScoreBreakdownRow> rows = counted
|
||||
.Where(q => q.Mode.Name.Length > 0)
|
||||
.GroupBy(q => (Band: Rounded(q), Mode: q.Mode.Name, Kind: contest.RowKindFor(q)))
|
||||
.OrderBy(g => BandText(g.Key.Band), StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Key.Mode, StringComparer.Ordinal)
|
||||
.ThenBy(g => g.Key.Kind, StringComparer.Ordinal)
|
||||
.Select(g => Row(g.Key.Band, g.Key.Mode, g.Key.Kind, g))
|
||||
.ToList();
|
||||
// the query's total is a group of the same rows, so an empty log has none
|
||||
if (counted.Count > 0)
|
||||
{
|
||||
rows.Add(Row(TotalBand, TotalMode, contest.HasQtcTraffic ? "All" : "", counted));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// The band as the database stores it: the megahertz label to one decimal.
|
||||
private static double Rounded(Qso qso) =>
|
||||
Math.Round(qso.Band?.MegahertzLabel ?? 0, 1, MidpointRounding.AwayFromZero);
|
||||
|
||||
/// What SQLite writes for a real number cast to text.
|
||||
private static string BandText(double band) => band.ToString("0.0", CultureInfo.InvariantCulture);
|
||||
|
||||
private static ScoreBreakdownRow Row(double band, string mode, string kind, IEnumerable<Qso> qsos)
|
||||
{
|
||||
List<Qso> rows = [.. qsos];
|
||||
return new ScoreBreakdownRow(
|
||||
band,
|
||||
mode,
|
||||
kind,
|
||||
rows.Count,
|
||||
rows.Sum(q => q.Points),
|
||||
[
|
||||
rows.Count(q => q.IsMultiplier1),
|
||||
rows.Count(q => q.IsMultiplier2),
|
||||
rows.Count(q => q.IsMultiplier3),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user