]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(utils): swap getRandomFloat(Rounded) params to (min, max) for consistency...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 12 Jul 2026 12:42:29 +0000 (14:42 +0200)
committerGitHub <noreply@github.com>
Sun, 12 Jul 2026 12:42:29 +0000 (14:42 +0200)
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

src/charging-station/ocpp/OCPPServiceUtils.ts
src/utils/Utils.ts
tests/utils/Utils.test.ts

index e9cd3b440259804503cd9177253b891a25111298..65acfb2b6df5bd43d811bbc29d3cc7be918cf0ee 100644 (file)
@@ -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 ${
index 144a97a4006e620ad46c78b4521b01d9cf7123a2..376144470fcea91a9c8ae5760dcdffb2fa7e4ac6 100644 (file)
@@ -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<TimestampedData>): number[] => {
index 86a1b1d909cffeb773db550069ab2ab65ae00927..d92d3910fdbbe8bf6fee672bf85861a7ed1187af 100644 (file)
@@ -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', () => {