diff --git a/theatre/shared/src/utils/niceNumberUtils.test.ts b/theatre/shared/src/utils/niceNumberUtils.test.ts new file mode 100644 index 0000000..98dd64a --- /dev/null +++ b/theatre/shared/src/utils/niceNumberUtils.test.ts @@ -0,0 +1,158 @@ +import { + getLastMultipleOf, + numberOfDecimals, + nicestFloatBetween, + nicestIntegerBetween, + nicestNumberBetween, + toPrecision, +} from './niceNumberUtils' +import type {$IntentionalAny} from './types' + +const example = ( + fn: (...args: Args) => Return, + args: Args, + expectation: Return, + opts: Partial<{skip: boolean; debug: boolean}> = {}, +) => { + ;(opts.skip ? it.skip : it)( + `${fn.name}(${args.join(', ')}) => ${expectation}`, + () => { + // @ts-expect-error @ignore + if (opts.debug) global.dbg = true + const val = fn(...args) + // @ts-expect-error @ignore + global.dbg = false + expect(val).toEqual(expectation) + }, + ) +} + +describe(`niceNumberUtils()`, () => { + describe(`nicestNumberBetween()`, () => { + example(nicestNumberBetween, [0.1, 1.1], 1) + example(nicestNumberBetween, [0.1111111123, 0.2943439448], 0.25) + example(nicestNumberBetween, [0.19, 0.23], 0.2) + example(nicestNumberBetween, [-0.19, 0.23], 0) + example(nicestNumberBetween, [-0.19, -0.02], -0.1, {debug: false}) + example(nicestNumberBetween, [-0.19, -0.022], -0.1, {debug: false}) + example(nicestNumberBetween, [-0.19, -0.022234324], -0.1, { + debug: false, + }) + example(nicestNumberBetween, [-0.19, 0.0222222], 0) + example(nicestNumberBetween, [-0.19, 0.02], 0) + example( + nicestNumberBetween, + [22304.2398427391, 22304.2398427393], + 22304.2398427392, + ) + example(nicestNumberBetween, [22304.2398427391, 22304.4], 22304.25) + example(nicestNumberBetween, [902, 901], 902) + example(nicestNumberBetween, [-10, -5], -10) + example(nicestNumberBetween, [-5, -10], -10) + example(nicestNumberBetween, [-10, -5], -10) + example( + nicestNumberBetween, + [-0.00876370109231405, -2.909374013346118e-50], + 0, + {debug: false}, + ) + example( + nicestNumberBetween, + [0.059449443526800295, 0.06682093143783596], + 0.06, + {debug: false}, + ) + const getRandomNumber = () => { + const sign = Math.random() > 0.5 ? 1 : -1 + return ( + (Math.pow(Math.random(), Math.random()) / + Math.pow(10, Math.random() * 100)) * + sign + ) + } + test(`nicestNumberBetween() => fuzzy`, () => { + for (let i = 0; i < 2000; i++) { + const from = toPrecision(getRandomNumber()) + const to = toPrecision(getRandomNumber()) + + const result = nicestNumberBetween(from, to) + if (from < to) { + if (result < from || result > to) { + throw new Error(`Invalid: ${from} ${to} ${result}`) + } + } else { + if (result > from || result < to) { + throw new Error(`Invalid: ${to} ${from} ${result}`) + } + } + } + }) + }) + describe(`nicestIntegerBetween`, () => { + example(nicestIntegerBetween, [-1, 6], 0, {}) + example(nicestIntegerBetween, [0, 6], 0, {}) + example(nicestIntegerBetween, [-1, 0], 0, {}) + example(nicestIntegerBetween, [-1850, -1740], -1750, {}) + example(nicestIntegerBetween, [1, 6], 5, {}) + example(nicestIntegerBetween, [1, 5], 5) + example(nicestIntegerBetween, [1, 2], 2) + example(nicestIntegerBetween, [1, 10], 10) + example(nicestIntegerBetween, [1, 12], 10) + example(nicestIntegerBetween, [11, 15], 15) + example(nicestIntegerBetween, [101, 102], 102, {debug: true}) + example(nicestIntegerBetween, [11, 14, false], 12) + example(nicestIntegerBetween, [11, 14, true], 12.5) + example(nicestIntegerBetween, [11, 12], 12) + example(nicestIntegerBetween, [11, 12], 12, {}) + example(nicestIntegerBetween, [10, 90], 50) + example(nicestIntegerBetween, [10, 100], 100) + example(nicestIntegerBetween, [10, 110], 100) + example(nicestIntegerBetween, [9, 100], 10) + example(nicestIntegerBetween, [9, 1100], 10) + example(nicestIntegerBetween, [9, 699], 10) + example(nicestIntegerBetween, [9, 400], 10) + example(nicestIntegerBetween, [9, 199], 10) + example(nicestIntegerBetween, [9, 1199], 10) + example(nicestIntegerBetween, [1921, 1998], 1950) + example(nicestIntegerBetween, [1921, 2020], 2000) + example(nicestIntegerBetween, [1601, 1998], 1750) + example(nicestIntegerBetween, [1919, 1921], 1920) + example(nicestIntegerBetween, [1919, 1919], 1919) + example(nicestIntegerBetween, [3901, 3902], 3902) + example(nicestIntegerBetween, [901, 902], 902) + }) + describe(`nicestFloatBetween()`, () => { + example(nicestFloatBetween, [0.19, 0.2122], 0.2) + example(nicestFloatBetween, [0.19, 0.31], 0.25) + example(nicestFloatBetween, [0.19, 0.41], 0.25) + example(nicestFloatBetween, [0.19, 1.9], 0.5) + }) + describe(`numberOfDecimals()`, () => { + example(numberOfDecimals, [1.1], 1) + example(numberOfDecimals, [1.12], 2) + example(numberOfDecimals, [1], 0) + example(numberOfDecimals, [10], 0) + example(numberOfDecimals, [0.1 + 0.2], 1) + example(numberOfDecimals, [0.12399993], 8) + example(numberOfDecimals, [0.12399993], 8) + example(numberOfDecimals, [0.123999931], 9) + example(numberOfDecimals, [0.1239999312], 10) + example(numberOfDecimals, [0.12399993121], 10) + }) + + describe(`toPrecision()`, () => { + example(toPrecision, [1.1], 1.1) + example(toPrecision, [0.1 + 0.2], 0.3) + example(toPrecision, [0.3 - 0.3], 0) + example(toPrecision, [0.4 - 0.1], 0.3) + example(toPrecision, [1.4 - 0.1], 1.3) + }) + + describe(`getLastMultipleOf()`, () => { + example(getLastMultipleOf, [1, 10], 0) + example(getLastMultipleOf, [11, 10], 10) + example(getLastMultipleOf, [11, 5], 10) + example(getLastMultipleOf, [11, 2], 10) + example(getLastMultipleOf, [4, 2], 4) + }) +}) diff --git a/theatre/shared/src/utils/numberRoundingUtils.ts b/theatre/shared/src/utils/niceNumberUtils.ts similarity index 79% rename from theatre/shared/src/utils/numberRoundingUtils.ts rename to theatre/shared/src/utils/niceNumberUtils.ts index 3c5f978..89d00f8 100644 --- a/theatre/shared/src/utils/numberRoundingUtils.ts +++ b/theatre/shared/src/utils/niceNumberUtils.ts @@ -2,38 +2,39 @@ import padEnd from 'lodash-es/padEnd' import logger from '@theatre/shared/logger' /** - * Returns the _roundest_ number `c`, such that `a <= c <= b`. This is useful - * when a number value is beinged "nudged" by a user, and we want to avoid setting - * it to weird value like `101.1239293814314`, when we know that the user probably just meant `100`. + * Returns the _aesthetically pleasing_ (aka "nicest") number `c`, such that `a <= c <= b`. + * This is useful when a numeric value is being "nudged" by the user (e.g. dragged via mouse pointer), + * and we want to avoid setting it to weird value like `101.1239293814314`, when we know that the user + * probably just meant `100`. * * Examples * ```ts - * roundestNumberBetween(0.1111111123, 0.2943439448) // 0.25 - * roundestNumberBetween(0.19, 0.23) // 0.2 - * roundestNumberBetween(1921, 1998) // 1950 - * roundestNumberBetween(10, 110) // 100 - * // There are many more examples at `./numberRoundingUtils.test.ts` + * nicestNumberBetween(0.1111111123, 0.2943439448) // 0.25 + * nicestNumberBetween(0.19, 0.23) // 0.2 + * nicestNumberBetween(1921, 1998) // 1950 + * nicestNumberBetween(10, 110) // 100 + * // There are more examples at `./niceNumberUtils.test.ts` * ``` */ -export function roundestNumberBetween(_a: number, _b: number): number { +export function nicestNumberBetween(_a: number, _b: number): number { if (_b < _a) { - return roundestNumberBetween(_b, _a) + return nicestNumberBetween(_b, _a) } if (_a < 0 && _b < 0) { - return noMinusZero(roundestNumberBetween(-_b, -_a) * -1) + return noMinusZero(nicestNumberBetween(-_b, -_a) * -1) } if (_a <= 0 && _b >= 0) return 0 const aCeiling = Math.ceil(_a) if (aCeiling <= _b) { - return roundestIntegerBetween(aCeiling, Math.floor(_b)) + return nicestIntegerBetween(aCeiling, Math.floor(_b)) } else { const [a, b] = [_a, _b] const integer = Math.floor(a) - return integer + roundestFloat(a - integer, b - integer) + return integer + nicestFloatBetween(a - integer, b - integer) } } @@ -41,7 +42,7 @@ export function roundestNumberBetween(_a: number, _b: number): number { const halvesAndQuartiles = [5, 2.5, 7.5] const multipliersWithoutQuartiles = [5, 2, 4, 6, 8, 1, 3, 7, 9] -export function roundestIntegerBetween( +export function nicestIntegerBetween( _a: number, _b: number, decimalsAllowed: boolean = true, @@ -138,9 +139,11 @@ export const stringifyNumber = (n: number): string => { } /** + * The float-specific version of {@link nicestNumberBetween}. + * * it is expected that both args are 0 \< arg \< 1 */ -export const roundestFloat = (a: number, b: number): number => { +export const nicestFloatBetween = (a: number, b: number): number => { const inString = { a: stringifyNumber(a), b: stringifyNumber(b), @@ -173,14 +176,15 @@ export const roundestFloat = (a: number, b: number): number => { b: padEnd(withoutInteger.b, maxNumberOfDecimals, '0'), } - const roundestInt = roundestIntegerBetween( + const mostAestheticInt = nicestIntegerBetween( parseInt(withPaddedDecimals.a, 10) * Math.pow(10, maxNumberOfLeadingZeros), parseInt(withPaddedDecimals.b, 10) * Math.pow(10, maxNumberOfLeadingZeros), true, ) return toPrecision( - roundestInt / Math.pow(10, maxNumberOfLeadingZeros + maxNumberOfDecimals), + mostAestheticInt / + Math.pow(10, maxNumberOfLeadingZeros + maxNumberOfDecimals), ) } diff --git a/theatre/shared/src/utils/numberRoundingUtils.test.ts b/theatre/shared/src/utils/numberRoundingUtils.test.ts deleted file mode 100644 index 316dac5..0000000 --- a/theatre/shared/src/utils/numberRoundingUtils.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { - getLastMultipleOf, - numberOfDecimals, - roundestFloat, - roundestIntegerBetween, - roundestNumberBetween, - toPrecision, -} from './numberRoundingUtils' -import type {$IntentionalAny} from './types' - -const example = ( - fn: (...args: Args) => Return, - args: Args, - expectation: Return, - opts: Partial<{skip: boolean; debug: boolean}> = {}, -) => { - ;(opts.skip ? it.skip : it)( - `${fn.name}(${args.join(', ')}) => ${expectation}`, - () => { - // @ts-expect-error @ignore - if (opts.debug) global.dbg = true - const val = fn(...args) - // @ts-expect-error @ignore - global.dbg = false - expect(val).toEqual(expectation) - }, - ) -} - -describe(`numberRoundingUtils()`, () => { - describe(`roundestNumberBetween()`, () => { - example(roundestNumberBetween, [0.1, 1.1], 1) - example(roundestNumberBetween, [0.1111111123, 0.2943439448], 0.25) - example(roundestNumberBetween, [0.19, 0.23], 0.2) - example(roundestNumberBetween, [-0.19, 0.23], 0) - example(roundestNumberBetween, [-0.19, -0.02], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, -0.022], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, -0.022234324], -0.1, {debug: false}) - example(roundestNumberBetween, [-0.19, 0.0222222], 0) - example(roundestNumberBetween, [-0.19, 0.02], 0) - example( - roundestNumberBetween, - [22304.2398427391, 22304.2398427393], - 22304.2398427392, - ) - example(roundestNumberBetween, [22304.2398427391, 22304.4], 22304.25) - example(roundestNumberBetween, [902, 901], 902) - example(roundestNumberBetween, [-10, -5], -10) - example(roundestNumberBetween, [-5, -10], -10) - example(roundestNumberBetween, [-10, -5], -10) - example( - roundestNumberBetween, - [-0.00876370109231405, -2.909374013346118e-50], - 0, - {debug: false}, - ) - example( - roundestNumberBetween, - [0.059449443526800295, 0.06682093143783596], - 0.06, - {debug: false}, - ) - const getRandomNumber = () => { - const sign = Math.random() > 0.5 ? 1 : -1 - return ( - (Math.pow(Math.random(), Math.random()) / - Math.pow(10, Math.random() * 100)) * - sign - ) - } - test(`roundestNumberBetween() => fuzzy`, () => { - for (let i = 0; i < 2000; i++) { - const from = toPrecision(getRandomNumber()) - const to = toPrecision(getRandomNumber()) - - const result = roundestNumberBetween(from, to) - if (from < to) { - if (result < from || result > to) { - throw new Error(`Invalid: ${from} ${to} ${result}`) - } - } else { - if (result > from || result < to) { - throw new Error(`Invalid: ${to} ${from} ${result}`) - } - } - } - }) - }) - describe(`roundestIntegerBetween`, () => { - example(roundestIntegerBetween, [-1, 6], 0, {}) - example(roundestIntegerBetween, [0, 6], 0, {}) - example(roundestIntegerBetween, [-1, 0], 0, {}) - example(roundestIntegerBetween, [-1850, -1740], -1750, {}) - example(roundestIntegerBetween, [1, 6], 5, {}) - example(roundestIntegerBetween, [1, 5], 5) - example(roundestIntegerBetween, [1, 2], 2) - example(roundestIntegerBetween, [1, 10], 10) - example(roundestIntegerBetween, [1, 12], 10) - example(roundestIntegerBetween, [11, 15], 15) - example(roundestIntegerBetween, [101, 102], 102, {debug: true}) - example(roundestIntegerBetween, [11, 14, false], 12) - example(roundestIntegerBetween, [11, 14, true], 12.5) - example(roundestIntegerBetween, [11, 12], 12) - example(roundestIntegerBetween, [11, 12], 12, {}) - example(roundestIntegerBetween, [10, 90], 50) - example(roundestIntegerBetween, [10, 100], 100) - example(roundestIntegerBetween, [10, 110], 100) - example(roundestIntegerBetween, [9, 100], 10) - example(roundestIntegerBetween, [9, 1100], 10) - example(roundestIntegerBetween, [9, 699], 10) - example(roundestIntegerBetween, [9, 400], 10) - example(roundestIntegerBetween, [9, 199], 10) - example(roundestIntegerBetween, [9, 1199], 10) - example(roundestIntegerBetween, [1921, 1998], 1950) - example(roundestIntegerBetween, [1921, 2020], 2000) - example(roundestIntegerBetween, [1601, 1998], 1750) - example(roundestIntegerBetween, [1919, 1921], 1920) - example(roundestIntegerBetween, [1919, 1919], 1919) - example(roundestIntegerBetween, [3901, 3902], 3902) - example(roundestIntegerBetween, [901, 902], 902) - }) - describe(`roundestFloat()`, () => { - example(roundestFloat, [0.19, 0.2122], 0.2) - example(roundestFloat, [0.19, 0.31], 0.25) - example(roundestFloat, [0.19, 0.41], 0.25) - example(roundestFloat, [0.19, 1.9], 0.5) - }) - describe(`numberOfDecimals()`, () => { - example(numberOfDecimals, [1.1], 1) - example(numberOfDecimals, [1.12], 2) - example(numberOfDecimals, [1], 0) - example(numberOfDecimals, [10], 0) - example(numberOfDecimals, [0.1 + 0.2], 1) - example(numberOfDecimals, [0.12399993], 8) - example(numberOfDecimals, [0.12399993], 8) - example(numberOfDecimals, [0.123999931], 9) - example(numberOfDecimals, [0.1239999312], 10) - example(numberOfDecimals, [0.12399993121], 10) - }) - - describe(`toPrecision()`, () => { - example(toPrecision, [1.1], 1.1) - example(toPrecision, [0.1 + 0.2], 0.3) - example(toPrecision, [0.3 - 0.3], 0) - example(toPrecision, [0.4 - 0.1], 0.3) - example(toPrecision, [1.4 - 0.1], 1.3) - }) - - describe(`getLastMultipleOf()`, () => { - example(getLastMultipleOf, [1, 10], 0) - example(getLastMultipleOf, [11, 10], 10) - example(getLastMultipleOf, [11, 5], 10) - example(getLastMultipleOf, [11, 2], 10) - example(getLastMultipleOf, [4, 2], 4) - }) -})