From: Jérôme Benoit Date: Sun, 12 Jul 2026 12:42:29 +0000 (+0200) Subject: refactor(utils): swap getRandomFloat(Rounded) params to (min, max) for consistency... X-Git-Tag: cli@v4.11.0~33 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=bc003ccca94105bb04f45f8f044e07ec8e4fb351;p=e-mobility-charging-stations-simulator.git refactor(utils): swap getRandomFloat(Rounded) params to (min, max) for consistency (#1998) Swap getRandomFloat and getRandomFloatRounded params from (max, min[, scale]) to (min, max[, scale]) for consistency with the sibling bounds helpers randomInt (node:crypto) and isValidRandomIntBounds; migrate all 11 production + 5 test call sites; harmonize JSDoc across the random-float family. Bundles a correctness fix: getRandomFloat now rejects a non-finite interval width (max - min), closing a latent overflow where finite endpoints such as (-MAX_VALUE, MAX_VALUE) returned Infinity/NaN, silently violating the documented [min, max] contract. Internal API refactor: package.json exports only ./dist/start.js, so getRandomFloat is app-internal and this is not a semver-major public break. Closes #1967 --- diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index e9cd3b44..65acfb2b 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -459,7 +459,7 @@ const buildEnergyMeasurandValue = ( ), energyTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded) + : getRandomFloatRounded(connectorMinimumEnergyRounded, connectorMaximumEnergyRounded) return { template: energyTemplate, @@ -616,22 +616,22 @@ const buildPowerMeasurandValue = ( phase1Value ?? defaultFluctuatedPowerPerPhase ?? getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider + connectorMinimumPowerPerPhase / unitDivider, + connectorMaximumPowerPerPhase / unitDivider ) powerValues.L2 = phase2Value ?? defaultFluctuatedPowerPerPhase ?? getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider + connectorMinimumPowerPerPhase / unitDivider, + connectorMaximumPowerPerPhase / unitDivider ) powerValues.L3 = phase3Value ?? defaultFluctuatedPowerPerPhase ?? getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider + connectorMinimumPowerPerPhase / unitDivider, + connectorMaximumPowerPerPhase / unitDivider ) } else { powerValues.L1 = isNotEmptyString(powerTemplate.value) @@ -648,8 +648,8 @@ const buildPowerMeasurandValue = ( powerTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) : getRandomFloatRounded( - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider + connectorMinimumPower / unitDivider, + connectorMaximumPower / unitDivider ) powerValues.L2 = 0 powerValues.L3 = 0 @@ -671,8 +671,8 @@ const buildPowerMeasurandValue = ( powerTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) : getRandomFloatRounded( - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider + connectorMinimumPower / unitDivider, + connectorMaximumPower / unitDivider ) break default: { @@ -823,15 +823,15 @@ const buildCurrentMeasurandValue = ( currentValues.L1 = phase1Value ?? defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + getRandomFloatRounded(connectorMinimumAmperage, connectorMaximumAmperage) currentValues.L2 = phase2Value ?? defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + getRandomFloatRounded(connectorMinimumAmperage, connectorMaximumAmperage) currentValues.L3 = phase3Value ?? defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + getRandomFloatRounded(connectorMinimumAmperage, connectorMaximumAmperage) } else { currentValues.L1 = isNotEmptyString(currentTemplate.value) ? getRandomFloatFluctuatedRounded( @@ -846,7 +846,7 @@ const buildCurrentMeasurandValue = ( ), currentTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + : getRandomFloatRounded(connectorMinimumAmperage, connectorMaximumAmperage) currentValues.L2 = 0 currentValues.L3 = 0 } @@ -874,7 +874,7 @@ const buildCurrentMeasurandValue = ( ), currentTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + : getRandomFloatRounded(connectorMinimumAmperage, connectorMaximumAmperage) break default: { const errorMsg = `MeterValues measurand ${ diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 144a97a4..37614447 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -356,16 +356,17 @@ export const convertToBoolean = (value: unknown): boolean => { } /** - * Generates a cryptographically secure random float in the [min, max] range - * @param max - The maximum value (inclusive). Defaults to `Number.MAX_VALUE` - * @param min - The minimum value (inclusive). Defaults to `0` - * @returns A float in the [min, max] range + * Generates a cryptographically secure random float in the [min, max] range. + * @param min - The minimum value (inclusive). Defaults to `0`. + * @param max - The maximum value (inclusive). Defaults to `Number.MAX_VALUE`. + * @returns A float in the [min, max] range. + * @throws {RangeError} If `max < min` or the interval width (`max - min`) is not finite. */ -export const getRandomFloat = (max = Number.MAX_VALUE, min = 0): number => { +export const getRandomFloat = (min = 0, max = Number.MAX_VALUE): number => { if (max < min) { throw new RangeError('Invalid interval') } - if (!Number.isFinite(max) || !Number.isFinite(min)) { + if (!Number.isFinite(max - min)) { throw new RangeError('Invalid interval') } return (randomBytes(4).readUInt32LE() / 0xffffffff) * (max - min) + min @@ -395,10 +396,27 @@ export const roundTo = (numberValue: number, scale: number): number => { return roundedScaled / factor } -export const getRandomFloatRounded = (max = Number.MAX_VALUE, min = 0, scale = 2): number => { - return roundTo(getRandomFloat(max, min), scale) +/** + * Generates a cryptographically secure random float in the [min, max] range, rounded to the given scale. + * @param min - The minimum value (inclusive). Defaults to `0`. + * @param max - The maximum value (inclusive). Defaults to `Number.MAX_VALUE`. + * @param scale - The number of decimal places to round to. Defaults to `2`. + * @returns A float in the [min, max] range, rounded to `scale` decimal places. + * @throws {RangeError} If `max < min` or the interval width (`max - min`) is not finite. + */ +export const getRandomFloatRounded = (min = 0, max = Number.MAX_VALUE, scale = 2): number => { + return roundTo(getRandomFloat(min, max), scale) } +/** + * Generates a cryptographically secure random float fluctuating around `staticValue` by up to `fluctuationPercent`, rounded to the given scale. + * @param staticValue - The center value to fluctuate around. + * @param fluctuationPercent - The maximum fluctuation as a percentage in the [0, 100] range. + * @param scale - The number of decimal places to round to. Defaults to `2`. + * @returns A float within `fluctuationPercent` of `staticValue`, rounded to `scale` decimal places. + * @throws {RangeError} If `fluctuationPercent` is outside the [0, 100] range. + * @throws {RangeError} If a non-zero `fluctuationPercent` derives a non-finite interval width (e.g. `staticValue` near `Number.MAX_VALUE`). + */ export const getRandomFloatFluctuatedRounded = ( staticValue: number, fluctuationPercent: number, @@ -417,7 +435,7 @@ export const getRandomFloatFluctuatedRounded = ( const lowerValue = staticValue * (1 - fluctuationRatio) const max = Math.max(upperValue, lowerValue) const min = Math.min(upperValue, lowerValue) - return getRandomFloatRounded(max, min, scale) + return getRandomFloatRounded(min, max, scale) } export const extractTimeSeriesValues = (timeSeries: CircularBuffer): number[] => { diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index 86a1b1d9..d92d3910 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -261,19 +261,26 @@ await describe('Utils', async () => { assert.notDeepStrictEqual(randomFloat, getRandomFloat()) assert.throws( () => { - getRandomFloat(0, 1) + getRandomFloat(1, 0) }, { message: /Invalid interval/ } ) assert.throws( () => { - getRandomFloat(Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY) + getRandomFloat(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY) }, { message: /Invalid interval/ } ) - randomFloat = getRandomFloat(0, -Number.MAX_VALUE) + assert.throws( + () => { + getRandomFloat(-Number.MAX_VALUE, Number.MAX_VALUE) + }, + { message: /Invalid interval/ } + ) + randomFloat = getRandomFloat(-Number.MAX_VALUE, 0) assert.strictEqual(randomFloat >= -Number.MAX_VALUE, true) assert.strictEqual(randomFloat <= 0, true) + assert.strictEqual(getRandomFloat(5, 5), 5) }) await it('should extract numeric values from timestamped circular buffer', () => { @@ -806,7 +813,7 @@ await describe('Utils', async () => { }) await it('should generate random float rounded to specified scale', () => { - const result = getRandomFloatRounded(10, 0, 2) + const result = getRandomFloatRounded(0, 10, 2) assert.strictEqual(result >= 0, true) assert.strictEqual(result <= 10, true) // Check rounding to 2 decimal places @@ -815,7 +822,7 @@ await describe('Utils', async () => { assert.strictEqual(decimalStr.split('.')[1].length <= 2, true) } // Default scale - const defaultScale = getRandomFloatRounded(10, 0) + const defaultScale = getRandomFloatRounded(0, 10) assert.strictEqual(defaultScale >= 0, true) assert.strictEqual(defaultScale <= 10, true) }) @@ -838,6 +845,17 @@ await describe('Utils', async () => { const negResult = getRandomFloatFluctuatedRounded(-100, 10) assert.strictEqual(negResult >= -110, true) assert.strictEqual(negResult <= -90, true) + // Non-finite derived interval width throws via delegation to getRandomFloat + assert.throws( + () => { + getRandomFloatFluctuatedRounded(Number.MAX_VALUE, 10) + }, + { message: /Invalid interval/ } + ) + // Zero fluctuation returns early without delegating, so it never reaches the width guard + assert.doesNotThrow(() => { + getRandomFloatFluctuatedRounded(Number.MAX_VALUE, 0) + }) }) await it('should detect Cloud Foundry environment from VCAP_APPLICATION', () => {