@@ -25,14 +25,14 @@ type FloatGenConfig = Readonly<{
2525 origin : number | null ;
2626 minPrecision : number | null ;
2727 maxPrecision : number | null ;
28- scale : ScaleMode | null ;
28+ noBias : boolean ;
2929} > ;
3030
3131// TODO: Origin validation (defer to genFactory.integer())
3232// TODO: Handle decimal min/max by destructuring into min/max integer component and min/max fractional component
3333// TODO: Handle a min/max range of less than 2
3434// TODO: Negative ranges do not shrink to the origin e.g. Gen.float().between(-10, -1) does not minimise to -1 (it minimises to -2, off-by-one)
35- // TODO: Memoize powers of 10
35+ // TODO: Add "unsafe" filter, a filter that does not produce discards. Use internally in float gen
3636
3737export const FloatGen = {
3838 create : ( ) : FloatGen => {
@@ -72,7 +72,7 @@ export const FloatGen = {
7272
7373 /* istanbul ignore next */
7474 noBias ( ) : FloatGen {
75- return this . withConfig ( { scale : 'constant' } ) ;
75+ return this . withConfig ( { noBias : true } ) ;
7676 }
7777
7878 private withConfig ( config : Partial < FloatGenConfig > ) : FloatGen {
@@ -89,7 +89,7 @@ export const FloatGen = {
8989 origin : null ,
9090 minPrecision : null ,
9191 maxPrecision : null ,
92- scale : null ,
92+ noBias : false ,
9393 } ) ;
9494 } ,
9595} ;
@@ -138,7 +138,7 @@ const genFloat = (args: FloatGenConfig): GenRunnable<number> => {
138138 * precisions will be generated, producing "less complex" fractions.
139139 */
140140 const genFractionalComponent = ( precision : number ) : Gen < number > => {
141- const maxFractionalComponentAsInteger = Math . pow ( 10 , precision ) - 1 ;
141+ const maxFractionalComponentAsInteger = tenPowX ( precision ) - 1 ;
142142 return Gen . integer ( ) . between ( 0 , maxFractionalComponentAsInteger ) . noBias ( ) ;
143143 } ;
144144
@@ -150,7 +150,7 @@ const genFloat = (args: FloatGenConfig): GenRunnable<number> => {
150150} ;
151151
152152const makeDecimal = ( integerComponent : number , fractionalComponentAsInteger : number , precision : number ) : number => {
153- const integerToFractionRatio = Math . pow ( 10 , precision ) ;
153+ const integerToFractionRatio = tenPowX ( precision ) ;
154154 const fractionalComponent = fractionalComponentAsInteger / integerToFractionRatio ;
155155 switch ( Math . sign ( integerComponent ) ) {
156156 /* istanbul ignore next */
@@ -207,66 +207,90 @@ const tryDeriveMaxPrecision = (maxPrecision: number | null): number | string =>
207207} ;
208208
209209const tryDeriveMin = ( min : number | null , minPrecision : number , maxPrecision : number ) : number | string => {
210- if ( min === null ) return - MAX_INT_32 ; // TODO: Ensure this fits into minPrecision
210+ if ( min !== null ) {
211+ const precisionOfMin = measurePrecision ( min ) ;
212+ if ( precisionOfMin > maxPrecision ) {
213+ return `Bound violates maximum precision constraint, minPrecision = ${ minPrecision } , maxPrecision = ${ maxPrecision } , min = ${ min } ` ;
214+ }
211215
212- const precisionOfMin = measurePrecision ( min ) ;
213- if ( precisionOfMin > maxPrecision ) {
214- return `Bound must be within precision range, minPrecision = ${ minPrecision } , maxPrecision = ${ maxPrecision } , min = ${ min } ` ;
216+ const precisionMagnitude = magnitudeOfPrecision ( minPrecision ) ;
217+ if ( min < - precisionMagnitude ) {
218+ return `Bound violates minimum precision constraint, minPrecision = ${ minPrecision } , minMin = -${ precisionMagnitude } , receivedMin = ${ min } ` ;
219+ }
215220 }
216221
217- const precisionMagnitude = magnitudeOfPrecision ( minPrecision ) ;
218- if ( min < - precisionMagnitude ) {
219- return `Bound violates minimum precision constraint, minPrecision = ${ minPrecision } , minMin = -${ precisionMagnitude } , receivedMin = ${ min } ` ;
222+ if ( min === null && minPrecision > 0 ) {
223+ return - tenPowX ( FLOAT_BITS - minPrecision ) ;
220224 }
221225
222- return min ;
226+ return min === null ? - MAX_INT_32 : roundToInteger ( min ) ;
223227} ;
224228
225229const tryDeriveMax = ( max : number | null , minPrecision : number , maxPrecision : number ) : number | string => {
226- if ( max === null ) return MAX_INT_32 ; // TODO: Ensure this fits into minPrecision
230+ if ( max !== null ) {
231+ const precisionOfMax = measurePrecision ( max ) ;
232+ if ( precisionOfMax > maxPrecision ) {
233+ return `Bound violates maximum precision constraint, minPrecision = ${ minPrecision } , maxPrecision = ${ maxPrecision } , max = ${ max } ` ;
234+ }
227235
228- const precisionOfMax = measurePrecision ( max ) ;
229- if ( precisionOfMax > maxPrecision ) {
230- return `Bound must be within precision range, minPrecision = ${ minPrecision } , maxPrecision = ${ maxPrecision } , max = ${ max } ` ;
236+ const precisionMagnitude = magnitudeOfPrecision ( minPrecision ) ;
237+ if ( max > precisionMagnitude ) {
238+ return `Bound violates minimum precision constraint, minPrecision = ${ minPrecision } , maxMax = ${ precisionMagnitude } , receivedMax = ${ max } ` ;
239+ }
231240 }
232241
233- const precisionMagnitude = magnitudeOfPrecision ( minPrecision ) ;
234- if ( max > precisionMagnitude ) {
235- return `Bound violates minimum precision constraint, minPrecision = ${ minPrecision } , maxMax = ${ precisionMagnitude } , receivedMax = ${ max } ` ;
242+ if ( max === null && minPrecision > 0 ) {
243+ return tenPowX ( FLOAT_BITS - minPrecision ) ;
236244 }
237245
238- return max ;
246+ return max === null ? MAX_INT_32 : roundToInteger ( max ) ;
239247} ;
240248
241249const measurePrecision = ( x : number ) : number => {
242- const xStr = x . toPrecision ( 1 ) ;
250+ const xStr = x . toPrecision ( ) ;
243251 const match = xStr . match ( / e - ( \d + ) / ) ;
244252
245253 /*istanbul ignore next */
246254 if ( match !== null ) {
247255 return Number ( match [ 1 ] ) ;
248256 }
249257
250- let count = 0 ;
251- x = Math . abs ( x ) ;
252-
253- while ( x % 1 > 0 ) {
254- count ++ ;
255- x = x * 10 ;
256-
257- /*istanbul ignore next */
258- if ( count > 100 ) {
259- throw new Error ( `Fatal: Exceeded calculation limit whilst measuring precision, x = ${ x } ` ) ;
260- }
261- }
262-
263- return count ;
258+ const fractionalComponentStr = xStr . split ( '.' ) [ 1 ] ;
259+ return fractionalComponentStr === undefined ? 0 : fractionalComponentStr . length ;
264260} ;
265261
266- const unitOfPrecision = ( precision : number ) : number => Math . pow ( 10 , - precision ) ;
262+ const unitOfPrecision = ( precision : number ) : number => tenPowX ( - precision ) ;
267263
268264const magnitudeOfPrecision = ( precision : number ) : number => {
269265 if ( precision === 0 ) return Infinity ;
270- const max = Math . pow ( 10 , FLOAT_BITS - precision ) - unitOfPrecision ( precision ) ;
271- return max ;
266+ return tenPowX ( FLOAT_BITS - precision ) - unitOfPrecision ( precision ) ;
267+ } ;
268+
269+ const tenPowX = ( ( ) => {
270+ const memo = new Map < number , number > ( ) ;
271+
272+ return ( x : number ) : number => {
273+ let result = memo . get ( x ) ;
274+
275+ if ( ! result ) {
276+ result = Math . pow ( 10 , x ) ;
277+ memo . set ( x , result ) ;
278+ }
279+
280+ return result ;
281+ } ;
282+ } ) ( ) ;
283+
284+ const roundToInteger = ( x : number ) : number => {
285+ switch ( Math . sign ( x ) ) {
286+ case 0 :
287+ return 0 ;
288+ case 1 :
289+ return Math . round ( x ) ;
290+ case - 1 :
291+ return - Math . round ( - x ) ;
292+ /* istanbul ignore next */
293+ default :
294+ throw new Error ( 'Fatal: Unhandled result from Math.sign' ) ;
295+ }
272296} ;
0 commit comments