From e6d0b58305d5395927c35955b80a2ad173b0e759 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 27 Aug 2026 23:01:39 +0000 Subject: [PATCH] Keep the band when the frequency is past the edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contest operating goes outside the band edges. Someone answers a CQ at 14352, or a radio reports a frequency a few hundred hertz off, and Bands.ForFrequency returned null: the contact scored nothing, missed the per-band dupe check and exported with no band. N1MM buckets a frequency by its whole megahertz — its BandFor divides the kilohertz by 1000 and switches on the result, with 1 and 2 both 160M, 3 and 4 both 80M, 28 and 29 both 10M — and never looks at a band edge. Same rule here, except that the real edges are checked first: 2190M and 630M are both under 1 MHz and share the bucket, so nothing else can tell them apart. A frequency under 1 MHz that misses both edge ranges still has no band, as it does in N1MM. The bandmap clamped its slice to the band edges, so a radio past the edge left the receiver bar off the top of the scale. The slice now stretches to wherever the radio is. Co-Authored-By: Claude Opus 5 --- README.md | 13 +++++++++- src/Nonemm.App/Windows/BandmapWindow.axaml.cs | 12 ++++++---- src/Nonemm.Core/Bands.cs | 22 ++++++++++++++++- tests/Nonemm.Core.Tests/BandsTests.cs | 24 +++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 986f8bf..844941f 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,8 @@ to where it really is. That is what N1MM does, and the stacking follows its rule: place the label centred on its frequency, and push it below the one above if it would cover it. -The slice shown follows the receiver — 10, 20, 40 or 100 kHz, or the whole band. +The slice shown follows the receiver — 10, 20, 40 or 100 kHz, or the whole band +— and stretches past the band edge when the radio is past it. Callsigns are coloured by the same scorer as the entry window, so a dupe reads as a dupe here too. Clicking a callsign puts the radio there with the call already in the entry window; clicking anywhere else just moves the radio. @@ -231,6 +232,16 @@ other radio of a two-radio station. Band-plan colouring of the scale is not there. The segments differ by ITU region and the program has no band-plan table, so it would be guesswork. +### Band edges + +A contact just outside a band edge keeps its band. `Bands.ForFrequency` first +checks the real edges, and for anything outside them falls back to the whole +megahertz: 14400 kHz is 20M, 7305 kHz is 40M. This is N1MM's rule, and it is +there because contest operating goes past the edges — someone answers a CQ at +14352, or a radio reports a frequency a few hundred hertz off — and a contact +with no band scores nothing and exports wrong. Below 1 MHz the megahertz is +always 0, so 2190M and 630M are told apart by their edges alone. + ### The DX cluster **Config → Cluster** takes the node's address, a password for the few nodes that diff --git a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs index 6480fa2..da6c739 100644 --- a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs +++ b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs @@ -94,16 +94,20 @@ public sealed partial class BandmapWindow : RefreshableWindow return markers; } - /// The slice to show, centred on the receiver and kept inside the band. + /// The slice to show, centred on the receiver. Contest operating goes + /// outside the band edges, so the slice stretches to wherever the receiver + /// is rather than leaving it off the top of the scale. private (Frequency Low, Frequency High) Window(Band band, Frequency centre) { + long lowEdge = Math.Min(band.Low.Hertz, centre.Hertz); + long highEdge = Math.Max(band.High.Hertz, centre.Hertz); Frequency span = Spans[Math.Max(0, SpanBox.SelectedIndex)].Span; - if (span.Hertz == 0 || span.Hertz >= band.High.Hertz - band.Low.Hertz) + if (span.Hertz == 0 || span.Hertz >= highEdge - lowEdge) { - return (band.Low, band.High); + return (Frequency.FromHertz(lowEdge), Frequency.FromHertz(highEdge)); } long half = span.Hertz / 2; - long low = Math.Clamp(centre.Hertz - half, band.Low.Hertz, band.High.Hertz - span.Hertz); + long low = Math.Clamp(centre.Hertz - half, lowEdge, highEdge - span.Hertz); return (Frequency.FromHertz(low), Frequency.FromHertz(low + span.Hertz)); } diff --git a/src/Nonemm.Core/Bands.cs b/src/Nonemm.Core/Bands.cs index 4d6b759..74abff5 100644 --- a/src/Nonemm.Core/Bands.cs +++ b/src/Nonemm.Core/Bands.cs @@ -51,7 +51,15 @@ public static class Bands private static Band Make(string name, double label, double lowKhz, double highKhz) => new(name, label, Frequency.FromKilohertz(lowKhz), Frequency.FromKilohertz(highKhz)); - /// Null when the frequency is outside every amateur allocation. + /// The band a frequency counts as, or null when no band covers that + /// megahertz. + /// + /// Contest operating goes outside the band edges: an operator answering a + /// CQ near the edge, or a radio reporting a frequency a few hundred hertz + /// off. N1MM buckets a frequency by its whole megahertz rather than by the + /// edges, so 14400 kHz is still 20M and the contact keeps its band. We do + /// the same. Exact edges are checked first, because 2190M and 630M are both + /// under 1 MHz and the bucket cannot tell them apart. public static Band? ForFrequency(Frequency f) { foreach (Band band in All) @@ -61,6 +69,18 @@ public static class Bands return band; } } + long megahertz = f.Hertz / 1_000_000; + if (megahertz == 0) + { + return null; + } + foreach (Band band in All) + { + if (megahertz >= band.Low.Hertz / 1_000_000 && megahertz <= band.High.Hertz / 1_000_000) + { + return band; + } + } return null; } diff --git a/tests/Nonemm.Core.Tests/BandsTests.cs b/tests/Nonemm.Core.Tests/BandsTests.cs index 9a441f9..858fcee 100644 --- a/tests/Nonemm.Core.Tests/BandsTests.cs +++ b/tests/Nonemm.Core.Tests/BandsTests.cs @@ -18,6 +18,30 @@ public class BandsTests public void FrequencyOutsideEveryAllocationHasNoBand() => Assert.Null(Bands.ForFrequency(Frequency.FromKilohertz(12000))); + /// N1MM buckets by whole megahertz, so a contact made past the edge keeps + /// its band instead of losing it. + [Theory] + [InlineData(14400, "20M")] + [InlineData(7305, "40M")] + [InlineData(1999.9, "160M")] + [InlineData(4100, "80M")] + [InlineData(29800, "10M")] + [InlineData(21500, "15M")] + public void FrequencyPastTheBandEdgeStillCountsAsThatBand(double kilohertz, string expected) => + Assert.Equal(expected, Bands.ForFrequency(Frequency.FromKilohertz(kilohertz))?.Name); + + /// Below 1 MHz every band shares the same megahertz, so only the real edges + /// decide. + [Theory] + [InlineData(137, "2190M")] + [InlineData(475, "630M")] + public void TheLowFrequencyBandsGoByTheirEdges(double kilohertz, string expected) => + Assert.Equal(expected, Bands.ForFrequency(Frequency.FromKilohertz(kilohertz))?.Name); + + [Fact] + public void BroadcastFrequenciesBelowOneMegahertzHaveNoBand() => + Assert.Null(Bands.ForFrequency(Frequency.FromKilohertz(900))); + [Fact] public void BandEdgesAreIncluded() {