mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-12 12:15:33 +02:00
Fix compact QRG parsing and initial worked-state restore - parse digit-only full frequencies across supported bands - restore persisted Worked state before publishing initial user lists - add regression tests and update documentation Fixes #85
This commit is contained in:
@@ -1848,7 +1848,7 @@ private ObservableList<String>
|
||||
* assumption which one is the current run frequency. Therefore the legacy
|
||||
* frequency property is initialized only when exactly one explicit QRG exists.</p>
|
||||
*/
|
||||
private void initializeFrequencyFromStationNameIfUnambiguous(
|
||||
/* package */ void initializeFrequencyFromStationNameIfUnambiguous(
|
||||
ChatMember member
|
||||
) {
|
||||
if (member == null) {
|
||||
@@ -1942,6 +1942,8 @@ private ObservableList<String>
|
||||
int categoryNumber = category.getCategoryNumber();
|
||||
List<ChatMember> safeMembers = completeMembers == null
|
||||
? List.of() : new ArrayList<>(completeMembers);
|
||||
Map<String, ChatMember> workedDataFromDatabase =
|
||||
loadWorkedStateForInitialUserList(safeMembers);
|
||||
for (ChatMember member : safeMembers) {
|
||||
initializeFrequencyFromStationNameIfUnambiguous(member);
|
||||
}
|
||||
@@ -1966,10 +1968,48 @@ private ObservableList<String>
|
||||
&& member.getChatCategory() != null
|
||||
&& member.getChatCategory().getCategoryNumber() == categoryNumber);
|
||||
lst_chatMemberList.addAll(safeMembers);
|
||||
if (workedDataFromDatabase != null) {
|
||||
getLst_DBBasedWkdCallSignList().setAll(
|
||||
workedDataFromDatabase.values());
|
||||
}
|
||||
fireUserListUpdate("Complete ON4KST user list received");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads one database snapshot for a completed initial ON4KST user list and
|
||||
* applies it before those members are published to the active model or UI.
|
||||
* Every callsign variant receives the state stored for its normalized base
|
||||
* callsign.
|
||||
*
|
||||
* @param initialMembers completed members of one chat category
|
||||
* @return loaded snapshot, or {@code null} when the database read failed
|
||||
*/
|
||||
/* package */ Map<String, ChatMember> loadWorkedStateForInitialUserList(
|
||||
Collection<ChatMember> initialMembers
|
||||
) {
|
||||
if (dbHandler == null) {
|
||||
LOGGER.warning(
|
||||
"Cannot load Worked state for initial ON4KST user list: "
|
||||
+ "database is not initialized");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, ChatMember> workedDataFromDatabase =
|
||||
dbHandler.fetchChatMemberWkdDataFromDB();
|
||||
applyWorkedAndQrvStateFromDatabase(
|
||||
initialMembers, workedDataFromDatabase);
|
||||
return workedDataFromDatabase;
|
||||
} catch (SQLException | RuntimeException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Could not load Worked state for completed initial ON4KST user list",
|
||||
exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a member from the thread-safe active model. This avoids reading the
|
||||
* TableView backing list from MessageBusManagementThread.
|
||||
@@ -3854,15 +3894,6 @@ private ObservableList<String>
|
||||
|
||||
}
|
||||
|
||||
new Timer().schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("fetchWorkedFromDBTimer");
|
||||
refreshWorkedStateAndDatabaseListFromDatabase();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// new Timer().schedule(new TimerTask() {
|
||||
// HashMap<String, ChatMember> getWorkedDataFromDb;
|
||||
//
|
||||
@@ -3966,23 +3997,34 @@ private ObservableList<String>
|
||||
HashMap<String, ChatMember> finalWorkedDataFromDatabase = workedDataFromDatabase;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
helper_applyWorkedAndQrvStateFromDatabase(finalWorkedDataFromDatabase);
|
||||
applyWorkedAndQrvStateFromDatabase(
|
||||
activeChatMembersByCallAndCategory.values(),
|
||||
finalWorkedDataFromDatabase);
|
||||
getLst_DBBasedWkdCallSignList().setAll(finalWorkedDataFromDatabase.values());
|
||||
fireUserListUpdate("Worked database state refreshed");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the worked and not-QRV state from the database snapshot to all active
|
||||
* chatmember objects that are currently visible in the live chat list.
|
||||
* Applies the worked and not-QRV state from a database snapshot to chat members.
|
||||
* Database rows are keyed by normalized base callsign, so all active category
|
||||
* and suffix variants receive the same persisted state.
|
||||
*
|
||||
* @param chatMembers members that should receive persisted state
|
||||
* @param workedDataFromDatabase map keyed by normalized raw callsign
|
||||
*/
|
||||
private void helper_applyWorkedAndQrvStateFromDatabase(HashMap<String, ChatMember> workedDataFromDatabase) {
|
||||
/* package */ static void applyWorkedAndQrvStateFromDatabase(
|
||||
Collection<ChatMember> chatMembers,
|
||||
Map<String, ChatMember> workedDataFromDatabase
|
||||
) {
|
||||
if (chatMembers == null || workedDataFromDatabase == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Iterator iterator = getLst_chatMemberList().iterator(); iterator.hasNext();) {
|
||||
|
||||
ChatMember activeChatMember = (ChatMember) iterator.next();
|
||||
for (ChatMember activeChatMember : chatMembers) {
|
||||
if (activeChatMember == null) {
|
||||
continue;
|
||||
}
|
||||
ChatMember storedChatMemberState = workedDataFromDatabase.get(activeChatMember.getCallSignRaw());
|
||||
|
||||
if (storedChatMemberState == null) {
|
||||
|
||||
@@ -57,7 +57,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
/*
|
||||
* Frequency formats handled by the smart parser:
|
||||
*
|
||||
* Group 1: full frequencies, for example 144.210 or 10368.100
|
||||
* Group 1: full frequencies, for example 144.210, 144210 or 10368100
|
||||
* Group 2: relative frequencies with a separator, for example .210 or ,210
|
||||
* Group 3: bare three-digit values, for example 210
|
||||
*
|
||||
@@ -66,7 +66,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* would be converted into plausible but incorrect frequencies.
|
||||
*/
|
||||
private static final Pattern SMART_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
"(?<![A-Z0-9])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})(?![A-Z0-9])"
|
||||
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
|
||||
);
|
||||
@@ -279,7 +279,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* @param message message whose text is inspected
|
||||
* @param prefs preferences containing the global fallback band
|
||||
*/
|
||||
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
|
||||
/* package */ void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
|
||||
if (message == null || message.getMessageText() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import java.util.regex.Pattern;
|
||||
* Common parser for explicit amateur-radio frequencies embedded in text.
|
||||
*
|
||||
* <p>This parser deliberately handles only complete frequencies such as
|
||||
* 144.300, 432.357 or 10368.100. Relative forms such as ".210" or ambiguous
|
||||
* bare values such as "210" require additional message context and remain the
|
||||
* responsibility of the chat-message parser.</p>
|
||||
* 144.300, 432.357, 10368.100 or their compact digit-only forms. Relative
|
||||
* forms such as ".210" or ambiguous bare values such as "210" require
|
||||
* additional message context and remain the responsibility of the
|
||||
* chat-message parser.</p>
|
||||
*/
|
||||
public final class FrequencyTextParser {
|
||||
|
||||
@@ -26,14 +27,18 @@ public final class FrequencyTextParser {
|
||||
* 432,357
|
||||
* 10368.100
|
||||
* 144.300.03
|
||||
* 144300
|
||||
* 10368100
|
||||
*
|
||||
* At least two digits are required before the decimal separator. This
|
||||
* intentionally prevents "1.2" from being interpreted as a frequency.
|
||||
* At least two digits are required before a decimal separator. Compact
|
||||
* values need at least five digits because their final three digits form
|
||||
* the kHz part. This intentionally prevents "1.2" and bare values such as
|
||||
* "210" from being interpreted as complete frequencies.
|
||||
*/
|
||||
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![A-Z0-9])"
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
|
||||
+ "(?!\\d)",
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})"
|
||||
+ "(?![A-Z0-9])",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
@@ -93,12 +98,12 @@ public final class FrequencyTextParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized =
|
||||
normalizeFrequencyString(
|
||||
rawFrequency
|
||||
.trim()
|
||||
.replace(',', '.')
|
||||
);
|
||||
final String trimmedFrequency = rawFrequency.trim();
|
||||
final String normalized = trimmedFrequency.matches("\\d{5,8}")
|
||||
? trimmedFrequency.substring(0, trimmedFrequency.length() - 3)
|
||||
+ "."
|
||||
+ trimmedFrequency.substring(trimmedFrequency.length() - 3)
|
||||
: normalizeFrequencyString(trimmedFrequency.replace(',', '.'));
|
||||
|
||||
try {
|
||||
double frequencyMHz =
|
||||
@@ -188,4 +193,4 @@ public final class FrequencyTextParser {
|
||||
return sourceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ChatControllerInitialWorkedStateTest {
|
||||
|
||||
@Test
|
||||
void loadsEachCompletedInitialListOnceAndAppliesStateToEveryVariant()
|
||||
throws SQLException {
|
||||
DBController database = mock(DBController.class);
|
||||
ChatMember stored = member("9A0BB", 2);
|
||||
stored.setWorked(true);
|
||||
stored.setWorked144(true);
|
||||
stored.setWorked10G(true);
|
||||
stored.setQrv432(false);
|
||||
|
||||
HashMap<String, ChatMember> databaseSnapshot = new HashMap<>();
|
||||
databaseSnapshot.put(stored.getCallSignRaw(), stored);
|
||||
when(database.fetchChatMemberWkdDataFromDB())
|
||||
.thenReturn(databaseSnapshot);
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setDbHandler(database);
|
||||
|
||||
ChatMember mainVariant = member("9A0BB-2", 2);
|
||||
ChatMember secondVariant = member("9A0BB-70", 3);
|
||||
ChatMember reconnectMainVariant = member("9A0BB-144", 2);
|
||||
ChatMember reconnectSecondVariant = member("9A0BB-432", 3);
|
||||
|
||||
controller.loadWorkedStateForInitialUserList(List.of(mainVariant));
|
||||
controller.loadWorkedStateForInitialUserList(List.of(secondVariant));
|
||||
controller.loadWorkedStateForInitialUserList(
|
||||
List.of(reconnectMainVariant));
|
||||
controller.loadWorkedStateForInitialUserList(
|
||||
List.of(reconnectSecondVariant));
|
||||
|
||||
verify(database, times(4)).fetchChatMemberWkdDataFromDB();
|
||||
for (ChatMember variant : List.of(
|
||||
mainVariant,
|
||||
secondVariant,
|
||||
reconnectMainVariant,
|
||||
reconnectSecondVariant
|
||||
)) {
|
||||
assertTrue(variant.isWorked());
|
||||
assertTrue(variant.isWorked144());
|
||||
assertTrue(variant.isWorked10G());
|
||||
assertFalse(variant.isQrv432());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsAmbiguousStationNameFromReplacingCompatibilityFrequency() {
|
||||
ChatController controller = new ChatController();
|
||||
ChatMember member = member("DL1ABC", 2);
|
||||
member.setName("144307 and 432100");
|
||||
|
||||
controller.initializeFrequencyFromStationNameIfUnambiguous(member);
|
||||
|
||||
assertTrue(
|
||||
member.getFrequency() == null
|
||||
|| member.getFrequency().get() == null
|
||||
|| member.getFrequency().get().isBlank()
|
||||
);
|
||||
}
|
||||
|
||||
private static ChatMember member(String callSign, int categoryNumber) {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setCallSign(callSign);
|
||||
member.setChatCategory(new ChatCategory(categoryNumber));
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatMessage;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class MessageBusManagementThreadFrequencyTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void detectsCompactMicrowaveFrequencyInPublicAndDirectedMessages(
|
||||
boolean directedMessage
|
||||
) {
|
||||
ChatController controller = mock(ChatController.class);
|
||||
ThreadStatusCallback callback = mock(ThreadStatusCallback.class);
|
||||
MessageBusManagementThread messageBus =
|
||||
new MessageBusManagementThread(
|
||||
controller,
|
||||
callback,
|
||||
1L,
|
||||
new LinkedBlockingQueue<>(),
|
||||
ignored -> true
|
||||
);
|
||||
|
||||
ChatMember sender = member("DL1ABC");
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setSender(sender);
|
||||
message.setReceiver(member(directedMessage ? "DL2XYZ" : "ALL"));
|
||||
message.setMessageText("pse try 10368100");
|
||||
|
||||
messageBus.smartFrequencyExtraction(message, null);
|
||||
|
||||
verify(controller).applyDetectedFrequencyToActiveMembers(
|
||||
sender,
|
||||
Band.B_10G,
|
||||
10368.100
|
||||
);
|
||||
}
|
||||
|
||||
private static ChatMember member(String callSign) {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setCallSign(callSign);
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import kst4contest.model.Band;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FrequencyTextParserRegressionTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("compactFrequenciesAcrossSupportedBands")
|
||||
void detectsCompactFrequenciesAcrossSupportedBands(
|
||||
String compactFrequency,
|
||||
Band expectedBand,
|
||||
double expectedFrequencyMHz
|
||||
) {
|
||||
FrequencyTextParser.DetectedFrequency detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"QRV " + compactFrequency
|
||||
).get(0);
|
||||
|
||||
assertEquals(expectedBand, detected.getBand());
|
||||
assertEquals(
|
||||
expectedFrequencyMHz,
|
||||
detected.getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsReferenceFrequencyInStationName() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"Operator 144307"
|
||||
);
|
||||
|
||||
assertEquals(1, detected.size());
|
||||
assertEquals(Band.B_144, detected.get(0).getBand());
|
||||
assertEquals(
|
||||
144.307,
|
||||
detected.get(0).getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCompactValuesOutsideSupportedBandRanges() {
|
||||
assertTrue(
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"146100 434100 99999"
|
||||
).isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsBareThreeDigitValuesOutOfCompleteFrequencyDetection() {
|
||||
assertTrue(
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"210 599 144"
|
||||
).isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> compactFrequenciesAcrossSupportedBands() {
|
||||
return Stream.of(
|
||||
Arguments.of("50278", Band.B_50, 50.278),
|
||||
Arguments.of("70200", Band.B_70, 70.200),
|
||||
Arguments.of("145500", Band.B_144, 145.500),
|
||||
Arguments.of("432100", Band.B_432, 432.100),
|
||||
Arguments.of("1296100", Band.B_1296, 1296.100),
|
||||
Arguments.of("2320100", Band.B_2320, 2320.100),
|
||||
Arguments.of("3400100", Band.B_3400, 3400.100),
|
||||
Arguments.of("5760100", Band.B_5760, 5760.100),
|
||||
Arguments.of("10368100", Band.B_10G, 10368.100),
|
||||
Arguments.of("24048100", Band.B_24G, 24048.100)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user