Antenna calculator: extract pure math, fix bugs, add tests.

Verify ARRL wire-antenna formulas (936/468/234/585), fix feet/inches
rollover ("N ft 12 in"), make wavelength edits update lengths, and cover
the math with 50 unit tests.
This commit is contained in:
Roland 2026-07-15 19:48:05 -06:00 • committed by lrnselfreliance
parent bad16684a4
commit 2fbf0a9b0f
2 changed files with 545 additions and 33 deletions

View file

@ -5,69 +5,231 @@ import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import {roundDigits} from "../Common";
import {ColoredInput} from "../Apps";
function convertFeetToFeetAndInches(decimalFeet) {
// Extract the integer part for feet
const feet = Math.floor(decimalFeet);
// Calculate the remainder in feet and convert it to inches (1 foot = 12 inches)
const inches = Math.round((decimalFeet - feet) * 12);
// ---------------------------------------------------------------------------
// Pure antenna math
//
// Wire antennas are shorter than free-space wavelength by ~5% (end effect).
// The ARRL / ham-radio constants are:
// full-wave length (ft) = 936 / f(MHz)
// half-wave dipole (ft) = 468 / f(MHz) = full * 0.5
// quarter-wave (ft) = 234 / f(MHz) = full * 0.25
// 5/8-wave (ft) = 585 / f(MHz) = full * 0.625
// Free-space wavelength uses the exact speed of light and is shown separately.
// ---------------------------------------------------------------------------
/** Speed of light in free space (m/s). */
export const SPEED_OF_LIGHT = 299792458;
/**
* Feet of wire for one full electrical wavelength, including end-effect.
* Free-space equivalent is ~984 / f; 936/984 ≈ 0.95 velocity factor.
*/
export const FULL_WAVE_FEET_PER_MHZ = 936;
/** Common antenna fractions shown in the calculator. */
export const ANTENNA_FRACTIONS = [
{name: '½ Antenna Length', multiplier: 0.5},
{name: '¼ Antenna Length', multiplier: 0.25},
{name: '⅝ Antenna Length', multiplier: 0.625},
];
/**
* Convert a decimal foot length into whole feet and inches.
* Rounds to the nearest inch and carries 12 inches into an extra foot
* (e.g. 3.96 ft → 4 ft 0 in, never "3 ft 12 in").
*
* @param {number} decimalFeet
* @returns {[number, number]} [feet, inches]
*/
export function convertFeetToFeetAndInches(decimalFeet) {
if (!Number.isFinite(decimalFeet) || decimalFeet < 0) {
return [0, 0];
}
const totalInches = Math.round(decimalFeet * 12);
const feet = Math.floor(totalInches / 12);
const inches = totalInches % 12;
return [feet, inches];
}
/**
* Full-wave wire-antenna length in feet for a given frequency in MHz.
* Returns null when frequency is not a positive finite number.
*
* @param {number} mhz
* @returns {number|null}
*/
export function fullWaveFeet(mhz) {
if (!Number.isFinite(mhz) || !(mhz > 0)) {
return null;
}
return FULL_WAVE_FEET_PER_MHZ / mhz;
}
/**
* Free-space wavelength in meters for a given frequency in MHz.
* λ = c / f. Returns null when frequency is not positive.
*
* @param {number} mhz
* @returns {number|null}
*/
export function freeSpaceWavelengthMeters(mhz) {
if (!Number.isFinite(mhz) || !(mhz > 0)) {
return null;
}
return SPEED_OF_LIGHT / (mhz * 1e6);
}
/**
* Frequency in MHz from a free-space wavelength in meters.
* f = c / λ. Returns null when wavelength is not positive.
*
* @param {number} meters
* @returns {number|null}
*/
export function frequencyMhzFromWavelengthMeters(meters) {
if (!Number.isFinite(meters) || !(meters > 0)) {
return null;
}
return SPEED_OF_LIGHT / meters / 1e6;
}
/**
* Convert a full-wave length in feet into the unit set used by the UI.
*
* @param {number} feetValue - full-wave length in feet
* @returns {{feet: number, inches: number, meters: number, cm: number}}
*/
export function lengthUnitsFromFeet(feetValue) {
const meters = feetValue * 0.3048;
return {
feet: feetValue,
inches: feetValue * 12,
meters,
cm: meters * 100,
};
}
/**
* Scale a length-unit set by a fraction (0.5 half-wave, 0.25 quarter, etc.).
*
* @param {{feet: number, inches: number, meters: number, cm: number}} units
* @param {number} multiplier
* @returns {{feet: number, inches: number, meters: number, cm: number}}
*/
export function scaleLengthUnits(units, multiplier) {
return {
feet: units.feet * multiplier,
inches: units.inches * multiplier,
meters: units.meters * multiplier,
cm: units.cm * multiplier,
};
}
/**
* All length units for the full-wave wire antenna at a given frequency.
* Returns null when frequency is invalid.
*
* @param {number} mhz
* @returns {{feet: number, inches: number, meters: number, cm: number}|null}
*/
export function fullWaveLengthUnits(mhz) {
const feet = fullWaveFeet(mhz);
if (feet === null) {
return null;
}
return lengthUnitsFromFeet(feet);
}
/**
* Antenna total length and per-leg length for a fraction of a full wave.
* "Leg length" is half the total (center-fed dipole legs).
*
* @param {number} mhz
* @param {number} multiplier - e.g. 0.5 for half-wave
* @returns {{
* total: {feet: number, inches: number, meters: number, cm: number, feetInches: [number, number]},
* leg: {feet: number, inches: number, meters: number, cm: number, feetInches: [number, number]},
* }|null}
*/
export function antennaFractionLengths(mhz, multiplier) {
const full = fullWaveLengthUnits(mhz);
if (full === null || !Number.isFinite(multiplier)) {
return null;
}
const total = scaleLengthUnits(full, multiplier);
const leg = scaleLengthUnits(total, 0.5);
return {
total: {
...total,
feetInches: convertFeetToFeetAndInches(total.feet),
},
leg: {
...leg,
feetInches: convertFeetToFeetAndInches(leg.feet),
},
};
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
const defaultMhzInputValue = '144.2';
export function DipoleAntennaCalculator() {
const [mhz, setMhz] = React.useState(144.2);
const [wave, setWave] = React.useState(2.08);
const [mhzInputValue, setMhzInputValue] = React.useState(defaultMhzInputValue);
const [waveInputValue, setWaveInputValue] = React.useState(0);
const [feet, setFeet] = React.useState(0);
const [inches, setInches] = React.useState(0);
const [meters, setMeters] = React.useState(0);
const [cm, setCM] = React.useState(0);
const c = 299792458;
// Reduce the full wavelength by the multiplier.
const lengths = [
{name: '½ Antenna Length', multiplier: 0.5},
{name: '¼ Antenna Length', multiplier: 0.25},
{name: '⅝ Antenna Length', multiplier: 0.625},
];
React.useEffect(() => {
// Calculate full wavelength.
const newFeet = 936 / mhz;
setFeet(newFeet);
setInches(newFeet * 12);
setMeters(newFeet * 0.3048);
setCM(newFeet * 0.3048 * 100);
setWaveInputValue(roundDigits(c / (mhz * 1e6)));
}, [mhz]);
React.useEffect(() => {
if (wave && parseFloat(wave) > 0) {
setMhzInputValue(roundDigits(c / parseFloat(wave) / 1e6));
const units = fullWaveLengthUnits(mhz);
if (!units) {
setFeet(0);
setInches(0);
setMeters(0);
setCM(0);
setWaveInputValue('');
return;
}
}, [wave])
setFeet(units.feet);
setInches(units.inches);
setMeters(units.meters);
setCM(units.cm);
setWaveInputValue(roundDigits(freeSpaceWavelengthMeters(mhz)));
}, [mhz]);
const mhzStorageKey = 'dipole_mhz';
const storageMhzValue = localStorage.getItem(mhzStorageKey);
React.useEffect(() => {
if (storageMhzValue) {
setMhz(parseFloat(storageMhzValue));
setMhzInputValue(storageMhzValue);
const parsed = parseFloat(storageMhzValue);
if (parsed > 0) {
setMhz(parsed);
setMhzInputValue(storageMhzValue);
}
}
}, []);
const handleMhzChange = (e, {value}) => {
setMhzInputValue(value);
setMhz(value && parseFloat(value) > 0 ? parseFloat(value) : 0);
const parsed = parseFloat(value);
setMhz(value && parsed > 0 ? parsed : 0);
localStorage.setItem(mhzStorageKey, value);
}
const handleWaveChange = (e, {value}) => {
setWaveInputValue(value);
setWave(value && parseFloat(value) > 0 ? parseFloat(value) : 0);
const parsed = parseFloat(value);
if (value && parsed > 0) {
const newMhz = frequencyMhzFromWavelengthMeters(parsed);
setMhz(newMhz);
setMhzInputValue(roundDigits(newMhz));
localStorage.setItem(mhzStorageKey, String(roundDigits(newMhz)));
}
}
const [mhzSelected, setMhzSelected] = React.useState(true);
@ -107,7 +269,7 @@ export function DipoleAntennaCalculator() {
</Grid.Row>
</Grid>
{lengths.map(i => {
{ANTENNA_FRACTIONS.map(i => {
const [feet_, inches_] = convertFeetToFeetAndInches(feet * i.multiplier);
const [half_feet, half_inches] = convertFeetToFeetAndInches(feet * i.multiplier / 2);
return <Table key={i.name} size='large' striped unstackable>
@ -155,4 +317,4 @@ export function DipoleAntennaCalculator() {
</Table>
})}
</>
}
}

View file

@ -0,0 +1,350 @@
import {
ANTENNA_FRACTIONS,
antennaFractionLengths,
convertFeetToFeetAndInches,
freeSpaceWavelengthMeters,
frequencyMhzFromWavelengthMeters,
FULL_WAVE_FEET_PER_MHZ,
fullWaveFeet,
fullWaveLengthUnits,
lengthUnitsFromFeet,
scaleLengthUnits,
SPEED_OF_LIGHT,
} from "./HamCalculators";
// ---------------------------------------------------------------------------
// Constants and formula relationships
// ---------------------------------------------------------------------------
describe('antenna constants', () => {
test('speed of light matches the SI definition', () => {
expect(SPEED_OF_LIGHT).toBe(299792458);
});
test('full-wave constant is the standard ARRL 936 ft·MHz value', () => {
expect(FULL_WAVE_FEET_PER_MHZ).toBe(936);
});
test('936 implies the classic half/quarter/5-8 constants', () => {
// Half-wave dipole: 468 / f
expect(FULL_WAVE_FEET_PER_MHZ * 0.5).toBe(468);
// Quarter-wave: 234 / f
expect(FULL_WAVE_FEET_PER_MHZ * 0.25).toBe(234);
// 5/8-wave: 585 / f
expect(FULL_WAVE_FEET_PER_MHZ * 0.625).toBe(585);
});
test('end-effect velocity factor is about 0.95 vs free-space 984', () => {
// Free-space full-wave feet ≈ 984 / f (c ≈ 983.57 ft/µs).
// Wire antennas use 936 / f → VF ≈ 0.951.
expect(FULL_WAVE_FEET_PER_MHZ / 984).toBeCloseTo(0.95, 2);
});
test('ANTENNA_FRACTIONS cover half, quarter, and 5/8 wave', () => {
const multipliers = ANTENNA_FRACTIONS.map(f => f.multiplier).sort();
expect(multipliers).toEqual([0.25, 0.5, 0.625]);
});
});
// ---------------------------------------------------------------------------
// convertFeetToFeetAndInches
// ---------------------------------------------------------------------------
describe('convertFeetToFeetAndInches', () => {
test('splits whole feet with zero inches', () => {
expect(convertFeetToFeetAndInches(10)).toEqual([10, 0]);
expect(convertFeetToFeetAndInches(0)).toEqual([0, 0]);
});
test('converts fractional feet to nearest inch', () => {
// 3.245 ft * 12 = 38.94 in → 3 ft 3 in
expect(convertFeetToFeetAndInches(3.245)).toEqual([3, 3]);
// 1.6227 ft * 12 = 19.47 in → 1 ft 7 in
expect(convertFeetToFeetAndInches(1.6227)).toEqual([1, 7]);
// 32.958 ft * 12 = 395.5 in → 32 ft 11 in (rounds half up via Math.round)
expect(convertFeetToFeetAndInches(32.958)).toEqual([32, 11]);
});
test('never reports 12 inches — rolls over to the next foot', () => {
// Values whose fractional part * 12 rounds to 12 (was a bug: "3 ft 12 in").
expect(convertFeetToFeetAndInches(3.9584)).toEqual([4, 0]);
expect(convertFeetToFeetAndInches(3.999)).toEqual([4, 0]);
expect(convertFeetToFeetAndInches(399.99999999999994)).toEqual([400, 0]);
// Exactly 11.5/12 feet fractional part rounds to 12 inches → +1 foot.
expect(convertFeetToFeetAndInches(5 + 11.5 / 12)).toEqual([6, 0]);
});
test('handles just-under and just-over an inch boundary', () => {
// 1/12 ft = 1 in exactly
expect(convertFeetToFeetAndInches(1 / 12)).toEqual([0, 1]);
// Slightly under half an inch rounds down
expect(convertFeetToFeetAndInches(0.04)).toEqual([0, 0]); // 0.48 in
// Slightly over half an inch rounds up
expect(convertFeetToFeetAndInches(0.05)).toEqual([0, 1]); // 0.60 in
});
test('returns [0, 0] for non-positive or non-finite input', () => {
expect(convertFeetToFeetAndInches(-1)).toEqual([0, 0]);
expect(convertFeetToFeetAndInches(NaN)).toEqual([0, 0]);
expect(convertFeetToFeetAndInches(Infinity)).toEqual([0, 0]);
expect(convertFeetToFeetAndInches(-Infinity)).toEqual([0, 0]);
});
});
// ---------------------------------------------------------------------------
// fullWaveFeet / free-space wavelength / frequency conversion
// ---------------------------------------------------------------------------
describe('fullWaveFeet', () => {
test('returns 936 / f for valid frequencies', () => {
expect(fullWaveFeet(1)).toBe(936);
expect(fullWaveFeet(144.2)).toBeCloseTo(936 / 144.2, 10);
expect(fullWaveFeet(14.2)).toBeCloseTo(936 / 14.2, 10);
});
test('returns null for invalid frequencies', () => {
expect(fullWaveFeet(0)).toBeNull();
expect(fullWaveFeet(-7)).toBeNull();
expect(fullWaveFeet(NaN)).toBeNull();
expect(fullWaveFeet(Infinity)).toBeNull();
});
});
describe('freeSpaceWavelengthMeters', () => {
test('uses λ = c / f', () => {
// 300 MHz → exactly 0.999308 m with the exact speed of light
expect(freeSpaceWavelengthMeters(300)).toBeCloseTo(SPEED_OF_LIGHT / 3e8, 10);
// 144.2 MHz 2 m band — free-space wavelength is a bit over 2 m
expect(freeSpaceWavelengthMeters(144.2)).toBeCloseTo(2.0790, 3);
// 14.2 MHz 20 m band
expect(freeSpaceWavelengthMeters(14.2)).toBeCloseTo(21.1121, 3);
});
test('matches the common 300/f approximation within 0.1%', () => {
// Amateur radio often approximates c as 300 m/µs.
for (const mhz of [7.15, 14.2, 146, 440]) {
const exact = freeSpaceWavelengthMeters(mhz);
const approx = 300 / mhz;
expect(Math.abs(exact - approx) / exact).toBeLessThan(0.001);
}
});
test('returns null for invalid frequencies', () => {
expect(freeSpaceWavelengthMeters(0)).toBeNull();
expect(freeSpaceWavelengthMeters(-1)).toBeNull();
expect(freeSpaceWavelengthMeters(NaN)).toBeNull();
});
});
describe('frequencyMhzFromWavelengthMeters', () => {
test('is the inverse of freeSpaceWavelengthMeters', () => {
for (const mhz of [1.9, 7.15, 14.2, 28.5, 50.1, 144.2, 146, 440, 446]) {
const lambda = freeSpaceWavelengthMeters(mhz);
expect(frequencyMhzFromWavelengthMeters(lambda)).toBeCloseTo(mhz, 8);
}
});
test('round-trips a known free-space wavelength', () => {
// 2 m free-space → 149.896 MHz
expect(frequencyMhzFromWavelengthMeters(2)).toBeCloseTo(SPEED_OF_LIGHT / 2e6, 8);
});
test('returns null for invalid wavelengths', () => {
expect(frequencyMhzFromWavelengthMeters(0)).toBeNull();
expect(frequencyMhzFromWavelengthMeters(-2)).toBeNull();
expect(frequencyMhzFromWavelengthMeters(NaN)).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Unit conversion helpers
// ---------------------------------------------------------------------------
describe('lengthUnitsFromFeet / scaleLengthUnits', () => {
test('converts feet to inches, meters, and cm with standard factors', () => {
const units = lengthUnitsFromFeet(10);
expect(units.feet).toBe(10);
expect(units.inches).toBe(120);
expect(units.meters).toBeCloseTo(3.048, 10);
expect(units.cm).toBeCloseTo(304.8, 10);
});
test('scaleLengthUnits multiplies every unit by the same factor', () => {
const full = lengthUnitsFromFeet(100);
const half = scaleLengthUnits(full, 0.5);
expect(half.feet).toBe(50);
expect(half.inches).toBe(600);
expect(half.meters).toBeCloseTo(15.24, 10);
expect(half.cm).toBeCloseTo(1524, 10);
});
test('scaling is linear across the standard antenna fractions', () => {
const full = lengthUnitsFromFeet(936); // 1 MHz full-wave
for (const {multiplier} of ANTENNA_FRACTIONS) {
const scaled = scaleLengthUnits(full, multiplier);
expect(scaled.feet).toBeCloseTo(936 * multiplier, 10);
expect(scaled.inches).toBeCloseTo(full.inches * multiplier, 10);
expect(scaled.meters).toBeCloseTo(full.meters * multiplier, 10);
expect(scaled.cm).toBeCloseTo(full.cm * multiplier, 10);
}
});
});
describe('fullWaveLengthUnits', () => {
test('returns consistent unit set for a valid frequency', () => {
const units = fullWaveLengthUnits(144.2);
expect(units.feet).toBeCloseTo(936 / 144.2, 10);
expect(units.inches).toBeCloseTo(units.feet * 12, 10);
expect(units.meters).toBeCloseTo(units.feet * 0.3048, 10);
expect(units.cm).toBeCloseTo(units.meters * 100, 10);
});
test('returns null for invalid frequency', () => {
expect(fullWaveLengthUnits(0)).toBeNull();
expect(fullWaveLengthUnits(-14.2)).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Half / quarter / 5/8 wave lengths (the UI tables)
// ---------------------------------------------------------------------------
describe('antennaFractionLengths — half-wave dipole (468/f)', () => {
// Published / well-known half-wave dipole lengths.
test.each([
// [MHz, expected total feet, expected leg feet]
[1.9, 468 / 1.9, 234 / 1.9], // 160 m
[3.8, 468 / 3.8, 234 / 3.8], // 80 m
[7.15, 468 / 7.15, 234 / 7.15], // 40 m
[14.2, 468 / 14.2, 234 / 14.2], // 20 m
[21.2, 468 / 21.2, 234 / 21.2], // 15 m
[28.5, 468 / 28.5, 234 / 28.5], // 10 m
[50.1, 468 / 50.1, 234 / 50.1], // 6 m
[144.2, 468 / 144.2, 234 / 144.2], // 2 m (default in UI)
[146, 468 / 146, 234 / 146], // 2 m simplex
[440, 468 / 440, 234 / 440], // 70 cm
])('half-wave at %s MHz is 468/f total and 234/f per leg', (mhz, totalFeet, legFeet) => {
const result = antennaFractionLengths(mhz, 0.5);
expect(result.total.feet).toBeCloseTo(totalFeet, 8);
expect(result.leg.feet).toBeCloseTo(legFeet, 8);
// Leg is exactly half the total.
expect(result.leg.feet).toBeCloseTo(result.total.feet / 2, 10);
});
test('default 144.2 MHz half-wave displays as 3 ft 3 in total, 1 ft 7 in per leg', () => {
const result = antennaFractionLengths(144.2, 0.5);
expect(result.total.feetInches).toEqual([3, 3]);
expect(result.leg.feetInches).toEqual([1, 7]);
});
test('14.2 MHz half-wave is about 32 ft 11 in total', () => {
const result = antennaFractionLengths(14.2, 0.5);
expect(result.total.feetInches).toEqual([32, 11]);
});
});
describe('antennaFractionLengths — quarter-wave (234/f)', () => {
test.each([
[146, 234 / 146],
[144.2, 234 / 144.2],
[14.2, 234 / 14.2],
[7.15, 234 / 7.15],
])('quarter-wave at %s MHz is 234/f', (mhz, expectedFeet) => {
const result = antennaFractionLengths(mhz, 0.25);
expect(result.total.feet).toBeCloseTo(expectedFeet, 8);
// "Leg" is still half of the shown total (center-fed construction).
expect(result.leg.feet).toBeCloseTo(expectedFeet / 2, 8);
});
});
describe('antennaFractionLengths — 5/8-wave (585/f)', () => {
test.each([
[146, 585 / 146],
[144.2, 585 / 144.2],
[50.1, 585 / 50.1],
[440, 585 / 440],
])('5/8-wave at %s MHz is 585/f', (mhz, expectedFeet) => {
const result = antennaFractionLengths(mhz, 0.625);
expect(result.total.feet).toBeCloseTo(expectedFeet, 8);
expect(result.leg.feet).toBeCloseTo(expectedFeet / 2, 8);
});
});
describe('antennaFractionLengths — metric consistency', () => {
test('inches, meters, and cm are consistent with feet for every fraction', () => {
for (const {multiplier} of ANTENNA_FRACTIONS) {
const result = antennaFractionLengths(14.2, multiplier);
for (const part of [result.total, result.leg]) {
expect(part.inches).toBeCloseTo(part.feet * 12, 8);
expect(part.meters).toBeCloseTo(part.feet * 0.3048, 8);
expect(part.cm).toBeCloseTo(part.meters * 100, 8);
// feetInches display matches the decimal feet value.
const [ft, inch] = part.feetInches;
expect(ft * 12 + inch).toBeCloseTo(Math.round(part.feet * 12), 0);
}
}
});
test('returns null for invalid frequency or multiplier', () => {
expect(antennaFractionLengths(0, 0.5)).toBeNull();
expect(antennaFractionLengths(-14, 0.5)).toBeNull();
expect(antennaFractionLengths(NaN, 0.5)).toBeNull();
expect(antennaFractionLengths(144.2, NaN)).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Cross-checks against free-space physics
// ---------------------------------------------------------------------------
describe('end-effect relationship to free space', () => {
test('wire half-wave is ~95% of free-space half-wavelength in feet', () => {
for (const mhz of [7.15, 14.2, 28.5, 144.2, 440]) {
const freeSpaceMeters = freeSpaceWavelengthMeters(mhz);
const freeSpaceHalfFeet = (freeSpaceMeters / 2) / 0.3048;
const wireHalfFeet = antennaFractionLengths(mhz, 0.5).total.feet;
// 468/f vs 492/f ≈ 0.951 (same VF as 936/984)
expect(wireHalfFeet / freeSpaceHalfFeet).toBeCloseTo(0.95, 2);
}
});
test('wire lengths are always shorter than free-space equivalents', () => {
for (const mhz of [3.8, 14.2, 146]) {
const freeSpaceFeet = freeSpaceWavelengthMeters(mhz) / 0.3048;
const wireFullFeet = fullWaveFeet(mhz);
expect(wireFullFeet).toBeLessThan(freeSpaceFeet);
expect(wireFullFeet).toBeCloseTo(freeSpaceFeet * (936 / (SPEED_OF_LIGHT * 3.280839895 / 1e6)), 2);
}
});
});
// ---------------------------------------------------------------------------
// Sanity across the HF / VHF / UHF amateur bands
// ---------------------------------------------------------------------------
describe('band sanity checks', () => {
test('higher frequency always yields shorter antennas', () => {
const bands = [1.9, 3.8, 7.15, 14.2, 21.2, 28.5, 50.1, 144.2, 440];
for (let i = 1; i < bands.length; i++) {
const lower = fullWaveFeet(bands[i - 1]);
const higher = fullWaveFeet(bands[i]);
expect(higher).toBeLessThan(lower);
}
});
test('half-wave is exactly twice quarter-wave at every frequency', () => {
for (const mhz of [7.15, 14.2, 146, 440]) {
const half = antennaFractionLengths(mhz, 0.5).total.feet;
const quarter = antennaFractionLengths(mhz, 0.25).total.feet;
expect(half).toBeCloseTo(quarter * 2, 10);
}
});
test('5/8-wave is 1.25× half-wave at every frequency', () => {
for (const mhz of [50.1, 146, 440]) {
const half = antennaFractionLengths(mhz, 0.5).total.feet;
const fiveEighths = antennaFractionLengths(mhz, 0.625).total.feet;
expect(fiveEighths).toBeCloseTo(half * 1.25, 10);
}
});
});