diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index 017fa03b..f0d794fb 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -191,6 +191,80 @@ if (response.status !== 404 && !response.ok) throw new Error("Logout failed"); } + // src/core/format.ts + function formatDuration(milliseconds) { + const seconds = Math.floor(milliseconds / 1e3); + const days = Math.floor(seconds / 86400); + const hours = Math.floor(seconds % 86400 / 3600); + const minutes = Math.floor(seconds % 3600 / 60); + const remainder = seconds % 60; + const parts = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0 || days > 0) parts.push(`${hours}h`); + parts.push(`${minutes}m`, `${remainder}s`); + return parts.join(" "); + } + function formatFrequency(frequencyHz) { + if (!Number.isFinite(frequencyHz)) return "--"; + if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`; + if (frequencyHz >= 1e7) return `${(frequencyHz / 1e6).toFixed(3)} MHz`; + return `${(frequencyHz / 1e3).toFixed(1)} kHz`; + } + function formatFrequencyForStep(frequencyHz, stepHz) { + if (!Number.isFinite(frequencyHz)) return "--"; + if (stepHz >= 1e6) return (frequencyHz / 1e6).toFixed(6); + if (stepHz >= 1e3) return (frequencyHz / 1e3).toFixed(3); + if (stepHz >= 1) return String(Math.round(frequencyHz)); + return formatFrequency(frequencyHz); + } + function formatFrequencyForHumans(frequencyHz) { + if (!Number.isFinite(frequencyHz)) return "--"; + if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`; + if (frequencyHz >= 1e6) return `${(frequencyHz / 1e6).toFixed(3)} MHz`; + if (frequencyHz >= 1e3) return `${(frequencyHz / 1e3).toFixed(3)} kHz`; + return `${Math.round(frequencyHz)} Hz`; + } + function formatWavelength(frequencyHz) { + if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--"; + const meters = 299792458 / frequencyHz; + return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`; + } + function parseFrequencyInput(value, defaultStepHz, mode) { + if (!value) return null; + const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/.exec(value.trim().toLowerCase()); + if (!match?.[1]) return null; + const rawNumber = match[1]; + let frequency = Number.parseFloat(rawNumber.replace(",", ".")); + const unit = match[2] ?? ""; + if (Number.isNaN(frequency)) return null; + if (unit.startsWith("gh") || unit === "g") frequency *= 1e9; + else if (unit.startsWith("mh") || unit === "m") frequency *= 1e6; + else if (unit.startsWith("kh") || unit === "k") frequency *= 1e3; + else if (!unit) { + const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(","); + if (mode.toUpperCase() === "WFM") { + if (hasDecimalSeparator && frequency >= 50 && frequency < 200) { + return Math.round(frequency * 1e6); + } + if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) { + return Math.round(frequency / 10 * 1e6); + } + } + if (defaultStepHz >= 1e6) frequency *= 1e6; + else if (defaultStepHz >= 1e3) frequency *= 1e3; + else if (defaultStepHz < 1) { + if (frequency < 1e3) frequency *= 1e6; + else if (frequency < 1e6) frequency *= 1e3; + } + } + return Math.round(frequency); + } + function formatByteSize(bytes) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1048576).toFixed(1)} MB`; + } + // src/app.js void loadDecoderRegistry(refreshOperatorLayoutCapabilities); var authRole = null; @@ -872,7 +946,7 @@ document.title = originalTitle; return; } - const parts = [formatFreq(freqHz)]; + const parts = [formatFrequency(freqHz)]; const ps = rds?.program_service; if (ps && ps.length > 0) { parts.push(ps); @@ -1416,23 +1490,10 @@ var aboutUptimeStart = null; var es; var esHeartbeat; - function formatUptime(ms) { - const s = Math.floor(ms / 1e3); - const d = Math.floor(s / 86400); - const h = Math.floor(s % 86400 / 3600); - const m = Math.floor(s % 3600 / 60); - const sec = s % 60; - const parts = []; - if (d > 0) parts.push(`${d}d`); - if (h > 0 || d > 0) parts.push(`${h}h`); - parts.push(`${m}m`); - parts.push(`${sec}s`); - return parts.join(" "); - } setInterval(() => { if (!aboutUptimeStart) return; const el = document.getElementById("about-uptime"); - if (el) el.textContent = formatUptime(Date.now() - aboutUptimeStart); + if (el) el.textContent = formatDuration(Date.now() - aboutUptimeStart); }, 1e3); var reconnectTimer = null; var overviewSignalSamples = []; @@ -1857,36 +1918,13 @@ texData[offset + 2] = _wfLut[p + 2]; texData[offset + 3] = _wfLut[p + 3]; } - function formatFreq(hz) { - if (!Number.isFinite(hz)) return "--"; - if (hz >= 1e9) { - return `${(hz / 1e9).toFixed(3)} GHz`; - } - if (hz >= 1e7) { - return `${(hz / 1e6).toFixed(3)} MHz`; - } - return `${(hz / 1e3).toFixed(1)} kHz`; - } - function formatFreqForStep(hz, step) { - if (!Number.isFinite(hz)) return "--"; - if (step >= 1e6) return (hz / 1e6).toFixed(6); - if (step >= 1e3) return (hz / 1e3).toFixed(3); - if (step >= 1) return String(Math.round(hz)); - return formatFreq(hz); - } - function formatWavelength(hz) { - if (!Number.isFinite(hz) || hz <= 0) return "--"; - const meters = 299792458 / hz; - if (meters >= 1) return `${Math.round(meters)} m`; - return `${Math.round(meters * 100)} cm`; - } function refreshWavelengthDisplay(hz) { if (!wavelengthEl) return; wavelengthEl.textContent = formatWavelength(hz); } function refreshFreqDisplay() { if (lastFreqHz == null || freqDirty) return; - freqEl.value = formatFreqForStep(lastFreqHz, jogUnit); + freqEl.value = formatFrequencyForStep(lastFreqHz, jogUnit); refreshWavelengthDisplay(lastFreqHz); } function activeRdsChannelId() { @@ -2296,7 +2334,7 @@ if (lastSpectrumData && Math.abs(nextCenterHz - Number(lastSpectrumData.center_hz)) < 1) return; await postPath(`/set_center_freq?hz=${nextCenterHz}`); if (centerFreqEl && !centerFreqDirty) { - centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit); + centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit); } } var _freqOptimisticHz = null; @@ -2535,7 +2573,7 @@ await postPath(`/set_center_freq?hz=${targetCenterHz}`); } if (centerFreqEl && !centerFreqDirty) { - centerFreqEl.value = formatFreqForStep(targetCenterHz, jogUnit); + centerFreqEl.value = formatFrequencyForStep(targetCenterHz, jogUnit); } if (Number.isFinite(originalCenterHz) && Math.abs(targetCenterHz - originalCenterHz) < 1) { showHint("Already at sweet spot", 900); @@ -2578,7 +2616,7 @@ showHint("Shifting spectrum…", 900); await postPath(`/set_center_freq?hz=${nextCenterHz}`); if (centerFreqEl && !centerFreqDirty) { - centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit); + centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit); } const nextFreqHz = tunedFrequencyForCenterCoverage(nextCenterHz); if (Number.isFinite(nextFreqHz) && Math.abs(nextFreqHz - Number(lastFreqHz)) >= 1) { @@ -2588,51 +2626,7 @@ } function refreshCenterFreqDisplay() { if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return; - centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit); - } - function parseFreqInput(val, defaultStep) { - if (!val) return null; - const trimmed = val.trim().toLowerCase(); - const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/); - if (!match) return null; - const rawNumber = match[1]; - let num = parseFloat(rawNumber.replace(",", ".")); - const unit = match[2] || ""; - if (Number.isNaN(num)) return null; - if (unit.startsWith("gh") || unit === "g") { - num *= 1e9; - } else if (unit.startsWith("mh") || unit === "m") { - num *= 1e6; - } else if (unit.startsWith("kh") || unit === "k") { - num *= 1e3; - } else if (!unit) { - const mode = (modeEl?.value || "").toUpperCase(); - const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(","); - if (mode === "WFM") { - if (hasDecimalSeparator && num >= 50 && num < 200) { - num *= 1e6; - return Math.round(num); - } - if (!hasDecimalSeparator && num >= 875 && num <= 1080) { - num = num / 10 * 1e6; - return Math.round(num); - } - } - if (defaultStep >= 1e6) { - num *= 1e6; - } else if (defaultStep >= 1e3) { - num *= 1e3; - } else if (defaultStep >= 1) { - } else { - if (num >= 1e6) { - } else if (num >= 1e3) { - num *= 1e3; - } else { - num *= 1e6; - } - } - } - return Math.round(num); + centerFreqEl.value = formatFrequencyForStep(lastSpectrumData.center_hz, jogUnit); } function normalizeMinFreqStep(cap) { const val = Number(cap && cap.min_freq_step_hz); @@ -2698,18 +2692,11 @@ } function unsupportedBandSummary() { if (supportedBands.length === 0) return "No supported frequency ranges were reported by the rig."; - const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFreqForHumans(b.low)} to ${formatFreqForHumans(b.high)}`); + const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFrequencyForHumans(b.low)} to ${formatFrequencyForHumans(b.high)}`); return `Supported ranges: ${ranges.join(", ")}`; } - function formatFreqForHumans(hz) { - if (!Number.isFinite(hz)) return "--"; - if (hz >= 1e9) return `${(hz / 1e9).toFixed(3)} GHz`; - if (hz >= 1e6) return `${(hz / 1e6).toFixed(3)} MHz`; - if (hz >= 1e3) return `${(hz / 1e3).toFixed(3)} kHz`; - return `${Math.round(hz)} Hz`; - } function showUnsupportedFreqPopup(hz) { - const message = `Unsupported frequency: ${formatFreqForHumans(hz)}. + const message = `Unsupported frequency: ${formatFrequencyForHumans(hz)}. ${unsupportedBandSummary()}`; showHint("Out of supported range", 1800); @@ -3288,7 +3275,7 @@ ${unsupportedBandSummary()}`; if (hz === null) return; const mode = entry.mode ? normalizeMode(entry.mode) : ""; const modeText = mode ? ` [${mode}]` : ""; - const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFreq(hz)}${modeText}`; + const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFrequency(hz)}${modeText}`; const btn = document.createElement("button"); btn.type = "button"; btn.textContent = label; @@ -3738,7 +3725,7 @@ ${unsupportedBandSummary()}`; } }); function applyFreqFromInput() { - const parsedRaw = parseFreqInput(freqEl.value, jogUnit); + const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || ""); const parsed = alignFreqToRigStep(parsedRaw); if (parsed === null) { showHint("Freq missing", 1500); @@ -3753,7 +3740,7 @@ ${unsupportedBandSummary()}`; } async function applyCenterFreqFromInput() { if (!centerFreqEl) return; - const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit); + const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || ""); const parsed = alignFreqToRigStep(parsedRaw); if (parsed === null) { showHint("Central freq missing", 1500); @@ -4594,11 +4581,11 @@ ${unsupportedBandSummary()}`; loadSetting, showHint, escapeMapHtml: escapeHtml, - formatFreq, - formatFreqForHumans, + formatFreq: formatFrequency, + formatFreqForHumans: formatFrequencyForHumans, formatWavelength, formatBwLabel, - formatUptime, + formatUptime: formatDuration, formatSigStrength, formatSignal, postPath, @@ -5640,11 +5627,6 @@ ${unsupportedBandSummary()}`; html += ""; el.innerHTML = html; } - function recorderFormatSize(bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB"; - return (bytes / 1048576).toFixed(1) + " MB"; - } function recFilterAndSort() { const filterEl = document.getElementById("recorder-filter"); const sortEl = document.getElementById("recorder-sort"); @@ -5711,7 +5693,7 @@ ${unsupportedBandSummary()}`; for (const f of page) { const safeName = escapeHtml(f.name); const encodedName = encodeURIComponent(f.name); - html += '' + safeName + "" + recorderFormatSize(f.size) + '
Download
'; + html += '' + safeName + "" + formatByteSize(f.size) + '
Download
'; } html += ""; el.innerHTML = html; @@ -6513,7 +6495,7 @@ ${unsupportedBandSummary()}`; let peakIndex = 0; for (let i = 1; i < bins.length; i += 1) if (bins[i] > bins[peakIndex]) peakIndex = i; const peakHz = centerHz - sampleRate / 2 + peakIndex / Math.max(1, bins.length - 1) * sampleRate; - spectrumSummary.textContent = `Spectrum centered at ${formatFreqForHumans(centerHz)}, spanning ${formatFreqForHumans(sampleRate)}. Strongest visible bin near ${formatFreqForHumans(peakHz)} at ${bins[peakIndex]} dB.`; + spectrumSummary.textContent = `Spectrum centered at ${formatFrequencyForHumans(centerHz)}, spanning ${formatFrequencyForHumans(sampleRate)}. Strongest visible bin near ${formatFrequencyForHumans(peakHz)} at ${bins[peakIndex]} dB.`; } if (spectrumCenterPendingHz !== null && Math.abs(centerHz - spectrumCenterPendingHz) < 1e3) { spectrumCenterPendingHz = null; @@ -7458,7 +7440,7 @@ ${unsupportedBandSummary()}`; return; } setRigFrequency(rounded); - showHint(`Rounded → ${formatFreq(rounded)}`, 1200); + showHint(`Rounded → ${formatFrequency(rounded)}`, 1200); } else { showHint("Already on step", 1200); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js index f317909b..e595e268 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js @@ -20,6 +20,15 @@ import { login, logout, } from "./api/auth.js"; +import { + formatByteSize as recorderFormatSize, + formatDuration as formatUptime, + formatFrequency as formatFreq, + formatFrequencyForHumans as formatFreqForHumans, + formatFrequencyForStep as formatFreqForStep, + formatWavelength, + parseFrequencyInput, +} from "./core/format.js"; // --- Decoder registry (fetched from /decoders on load) --- void loadDecoderRegistry(refreshOperatorLayoutCapabilities); @@ -1246,19 +1255,6 @@ let aboutUptimeStart = null; let es; let esHeartbeat; -function formatUptime(ms) { - const s = Math.floor(ms / 1000); - const d = Math.floor(s / 86400); - const h = Math.floor((s % 86400) / 3600); - const m = Math.floor((s % 3600) / 60); - const sec = s % 60; - const parts = []; - if (d > 0) parts.push(`${d}d`); - if (h > 0 || d > 0) parts.push(`${h}h`); - parts.push(`${m}m`); - parts.push(`${sec}s`); - return parts.join(" "); -} setInterval(() => { if (!aboutUptimeStart) return; const el = document.getElementById("about-uptime"); @@ -1738,32 +1734,6 @@ function waterfallLutWrite(texData, offset, db) { texData[offset + 3] = _wfLut[p + 3]; } -function formatFreq(hz) { - if (!Number.isFinite(hz)) return "--"; - if (hz >= 1_000_000_000) { - return `${(hz / 1_000_000_000).toFixed(3)} GHz`; - } - if (hz >= 10_000_000) { - return `${(hz / 1_000_000).toFixed(3)} MHz`; - } - return `${(hz / 1_000).toFixed(1)} kHz`; -} - -function formatFreqForStep(hz, step) { - if (!Number.isFinite(hz)) return "--"; - if (step >= 1_000_000) return (hz / 1_000_000).toFixed(6); - if (step >= 1_000) return (hz / 1_000).toFixed(3); - if (step >= 1) return String(Math.round(hz)); - return formatFreq(hz); -} - -function formatWavelength(hz) { - if (!Number.isFinite(hz) || hz <= 0) return "--"; - const meters = 299_792_458 / hz; - if (meters >= 1) return `${Math.round(meters)} m`; - return `${Math.round(meters * 100)} cm`; -} - function refreshWavelengthDisplay(hz) { if (!wavelengthEl) return; wavelengthEl.textContent = formatWavelength(hz); @@ -2602,55 +2572,6 @@ function refreshCenterFreqDisplay() { centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit); } -function parseFreqInput(val, defaultStep) { - if (!val) return null; - const trimmed = val.trim().toLowerCase(); - const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/); - if (!match) return null; - const rawNumber = match[1]; - let num = parseFloat(rawNumber.replace(",", ".")); - const unit = match[2] || ""; - if (Number.isNaN(num)) return null; - if (unit.startsWith("gh") || unit === "g") { - num *= 1_000_000_000; - } else if (unit.startsWith("mh") || unit === "m") { - num *= 1_000_000; - } else if (unit.startsWith("kh") || unit === "k") { - num *= 1_000; - } else if (!unit) { - const mode = (modeEl?.value || "").toUpperCase(); - const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(","); - if (mode === "WFM") { - if (hasDecimalSeparator && num >= 50 && num < 200) { - num *= 1_000_000; - return Math.round(num); - } - if (!hasDecimalSeparator && num >= 875 && num <= 1080) { - num = (num / 10) * 1_000_000; - return Math.round(num); - } - } - // Use currently selected input unit when user omits suffix. - if (defaultStep >= 1_000_000) { - num *= 1_000_000; - } else if (defaultStep >= 1_000) { - num *= 1_000; - } else if (defaultStep >= 1) { - // already Hz - } else { - // Fallback heuristic. - if (num >= 1_000_000) { - // Assume already Hz. - } else if (num >= 1_000) { - num *= 1_000; - } else { - num *= 1_000_000; - } - } - } - return Math.round(num); -} - function normalizeMinFreqStep(cap) { const val = Number(cap && cap.min_freq_step_hz); if (!Number.isFinite(val) || val < 1) return 1; @@ -2740,14 +2661,6 @@ function unsupportedBandSummary() { return `Supported ranges: ${ranges.join(", ")}`; } -function formatFreqForHumans(hz) { - if (!Number.isFinite(hz)) return "--"; - if (hz >= 1_000_000_000) return `${(hz / 1_000_000_000).toFixed(3)} GHz`; - if (hz >= 1_000_000) return `${(hz / 1_000_000).toFixed(3)} MHz`; - if (hz >= 1_000) return `${(hz / 1_000).toFixed(3)} kHz`; - return `${Math.round(hz)} Hz`; -} - function showUnsupportedFreqPopup(hz) { const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.\n\n${unsupportedBandSummary()}`; showHint("Out of supported range", 1800); @@ -3913,7 +3826,7 @@ pttBtn.addEventListener("click", async () => { }); function applyFreqFromInput() { - const parsedRaw = parseFreqInput(freqEl.value, jogUnit); + const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || ""); const parsed = alignFreqToRigStep(parsedRaw); if (parsed === null) { showHint("Freq missing", 1500); @@ -3930,7 +3843,7 @@ function applyFreqFromInput() { async function applyCenterFreqFromInput() { if (!centerFreqEl) return; - const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit); + const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || ""); const parsed = alignFreqToRigStep(parsedRaw); if (parsed === null) { showHint("Central freq missing", 1500); @@ -5839,12 +5752,6 @@ function renderRecorderActive(list) { el.innerHTML = html; } -function recorderFormatSize(bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB"; - return (bytes / 1048576).toFixed(1) + " MB"; -} - function recFilterAndSort() { const filterEl = document.getElementById("recorder-filter"); const sortEl = document.getElementById("recorder-sort"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts new file mode 100644 index 00000000..3e163a72 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +export function formatDuration(milliseconds: number): string { + const seconds = Math.floor(milliseconds / 1000); + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainder = seconds % 60; + const parts: string[] = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0 || days > 0) parts.push(`${hours}h`); + parts.push(`${minutes}m`, `${remainder}s`); + return parts.join(" "); +} + +export function formatFrequency(frequencyHz: number): string { + if (!Number.isFinite(frequencyHz)) return "--"; + if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`; + if (frequencyHz >= 10_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`; + return `${(frequencyHz / 1_000).toFixed(1)} kHz`; +} + +export function formatFrequencyForStep(frequencyHz: number, stepHz: number): string { + if (!Number.isFinite(frequencyHz)) return "--"; + if (stepHz >= 1_000_000) return (frequencyHz / 1_000_000).toFixed(6); + if (stepHz >= 1_000) return (frequencyHz / 1_000).toFixed(3); + if (stepHz >= 1) return String(Math.round(frequencyHz)); + return formatFrequency(frequencyHz); +} + +export function formatFrequencyForHumans(frequencyHz: number): string { + if (!Number.isFinite(frequencyHz)) return "--"; + if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`; + if (frequencyHz >= 1_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`; + if (frequencyHz >= 1_000) return `${(frequencyHz / 1_000).toFixed(3)} kHz`; + return `${Math.round(frequencyHz)} Hz`; +} + +export function formatWavelength(frequencyHz: number): string { + if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--"; + const meters = 299_792_458 / frequencyHz; + return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`; +} + +export function parseFrequencyInput( + value: string, + defaultStepHz: number, + mode: string, +): number | null { + if (!value) return null; + const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/ + .exec(value.trim().toLowerCase()); + if (!match?.[1]) return null; + const rawNumber = match[1]; + let frequency = Number.parseFloat(rawNumber.replace(",", ".")); + const unit = match[2] ?? ""; + if (Number.isNaN(frequency)) return null; + + if (unit.startsWith("gh") || unit === "g") frequency *= 1_000_000_000; + else if (unit.startsWith("mh") || unit === "m") frequency *= 1_000_000; + else if (unit.startsWith("kh") || unit === "k") frequency *= 1_000; + else if (!unit) { + const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(","); + if (mode.toUpperCase() === "WFM") { + if (hasDecimalSeparator && frequency >= 50 && frequency < 200) { + return Math.round(frequency * 1_000_000); + } + if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) { + return Math.round((frequency / 10) * 1_000_000); + } + } + if (defaultStepHz >= 1_000_000) frequency *= 1_000_000; + else if (defaultStepHz >= 1_000) frequency *= 1_000; + else if (defaultStepHz < 1) { + if (frequency < 1_000) frequency *= 1_000_000; + else if (frequency < 1_000_000) frequency *= 1_000; + } + } + return Math.round(frequency); +} + +export function formatByteSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1_048_576) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1_048_576).toFixed(1)} MB`; +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs new file mode 100644 index 00000000..c44c6c65 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import assert from "node:assert/strict"; +import test from "node:test"; +import vm from "node:vm"; +import { build } from "esbuild"; + +async function loadFormats() { + const result = await build({ + entryPoints: [new URL("../src/core/format.ts", import.meta.url).pathname], + bundle: true, + format: "cjs", + platform: "browser", + target: "es2022", + write: false, + }); + const module = { exports: {} }; + vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports }); + return module.exports; +} + +test("frequency parsing respects explicit units and WFM shorthand", async () => { + const { parseFrequencyInput } = await loadFormats(); + assert.equal(parseFrequencyInput("145.500 MHz", 1000, "FM"), 145_500_000); + assert.equal(parseFrequencyInput("99.5", 100_000, "WFM"), 99_500_000); + assert.equal(parseFrequencyInput("995", 100_000, "WFM"), 99_500_000); + assert.equal(parseFrequencyInput("not a frequency", 1000, "USB"), null); +}); + +test("frequency and byte formatters handle display boundaries", async () => { + const { formatByteSize, formatFrequencyForHumans, formatWavelength } = await loadFormats(); + assert.equal(formatFrequencyForHumans(145_500_000), "145.500 MHz"); + assert.equal(formatByteSize(1536), "1.5 KB"); + assert.equal(formatWavelength(299_792_458), "1 m"); +});