typia
Version:
Superfast runtime validators with only one line
246 lines (224 loc) • 8.54 kB
text/typescript
import {
_IDecimal,
_IDecimalRatio,
_decimalDecompose,
_decimalDivide,
_decimalGcd,
_decimalIntegerStep,
_decimalPower,
_decimalToNumber,
} from "./_decimal";
import { _isMultipleOf } from "./_isMultipleOf";
export const _randomMultiple = (props: {
minimum: number;
maximum: number;
multipleOf: number;
exclusiveMinimum: boolean;
exclusiveMaximum: boolean;
integer: boolean;
}): number => {
const step: _IDecimal | null = props.integer
? _decimalIntegerStep(props.multipleOf)
: _decimalDecompose(props.multipleOf);
if (step === null || step.coefficient <= BigInt(0))
throw new Error("The multipleOf value must be a positive finite number.");
const lower: _IDecimalRatio | null = _decimalDivide(props.minimum, step);
const upper: _IDecimalRatio | null = _decimalDivide(props.maximum, step);
if (lower === null || upper === null)
throw new Error("The random number range must be finite.");
const minimum: bigint = lowerBound(lower, props.exclusiveMinimum);
const maximum: bigint = upperBound(upper, props.exclusiveMaximum);
if (minimum > maximum)
throw new Error("The range does not contain a multipleOf value.");
const selected: bigint = randomBigint(minimum, maximum);
const candidates: bigint[] = unique([
selected,
minimum,
maximum,
clamp(BigInt(0), minimum, maximum),
clamp(BigInt(1), minimum, maximum),
clamp(BigInt(-1), minimum, maximum),
...nearby(selected, minimum, maximum),
]);
for (const coefficient of candidates) {
const value: number = _decimalToNumber({
coefficient: step.coefficient * coefficient,
exponent: step.exponent,
});
if (isValid(props, value)) return value;
}
const aligned: number | null = findRepresentableIntegerMultiple(props);
if (aligned !== null) return aligned;
const decimalAligned: number | null = findRepresentableDecimalMultiple(
props,
step,
);
if (decimalAligned !== null) return decimalAligned;
throw new Error(
"The range does not contain a representable multipleOf value.",
);
};
const isValid = (
props: Parameters<typeof _randomMultiple>[0],
value: number,
): boolean =>
Number.isFinite(value) &&
(props.integer === false || Number.isInteger(value)) &&
(props.exclusiveMinimum ? value > props.minimum : value >= props.minimum) &&
(props.exclusiveMaximum ? value < props.maximum : value <= props.maximum) &&
_isMultipleOf(value, props.multipleOf);
const findRepresentableDecimalMultiple = (
props: Parameters<typeof _randomMultiple>[0],
step: _IDecimal,
): number | null => {
const limit: bigint = BigInt("999999999999999");
for (let exponent = -324; exponent <= 308; ++exponent) {
const unit: _IDecimal = { coefficient: BigInt(1), exponent };
const lower: _IDecimalRatio | null = _decimalDivide(props.minimum, unit);
const upper: _IDecimalRatio | null = _decimalDivide(props.maximum, unit);
if (lower === null || upper === null) return null;
const coefficientMinimum: bigint = max(
-limit,
lowerBound(lower, props.exclusiveMinimum),
);
const coefficientMaximum: bigint = min(
limit,
upperBound(upper, props.exclusiveMaximum),
);
if (coefficientMinimum > coefficientMaximum) continue;
const coefficientStep: bigint = decimalCoefficientStep(step, exponent);
const minimum: bigint = lowerBound(
{ numerator: coefficientMinimum, denominator: coefficientStep },
false,
);
const maximum: bigint = upperBound(
{ numerator: coefficientMaximum, denominator: coefficientStep },
false,
);
if (minimum > maximum) continue;
const selected: bigint = randomBigint(minimum, maximum);
for (const quotient of unique([
selected,
minimum,
maximum,
clamp(BigInt(0), minimum, maximum),
...nearby(selected, minimum, maximum),
])) {
const value: number = _decimalToNumber({
coefficient: coefficientStep * quotient,
exponent,
});
if (isValid(props, value)) return value;
}
}
return null;
};
const decimalCoefficientStep = (step: _IDecimal, exponent: number): bigint => {
const difference: number = exponent - step.exponent;
if (difference >= 0) {
const power: bigint = _decimalPower(difference);
return step.coefficient / _decimalGcd(step.coefficient, power);
}
return step.coefficient * _decimalPower(-difference);
};
const findRepresentableIntegerMultiple = (
props: Parameters<typeof _randomMultiple>[0],
): number | null => {
const step: _IDecimal | null = _decimalIntegerStep(props.multipleOf);
if (step === null) return null;
const unit: _IDecimal = { coefficient: BigInt(1), exponent: 0 };
const lower: _IDecimalRatio | null = _decimalDivide(props.minimum, unit);
const upper: _IDecimalRatio | null = _decimalDivide(props.maximum, unit);
if (lower === null || upper === null) return null;
const minimum: bigint = lowerBound(lower, props.exclusiveMinimum);
const maximum: bigint = upperBound(upper, props.exclusiveMaximum);
if (minimum > maximum) return null;
if (minimum <= BigInt(0) && maximum >= BigInt(0)) return 0;
const candidate: bigint | null =
minimum > BigInt(0)
? findPositiveAligned(minimum, maximum, step.coefficient)
: (() => {
const magnitude: bigint | null = findPositiveAligned(
-maximum,
-minimum,
step.coefficient,
);
return magnitude === null ? null : -magnitude;
})();
if (candidate === null) return null;
const value: number = Number(candidate);
return isValid(props, value) ? value : null;
};
const findPositiveAligned = (
minimum: bigint,
maximum: bigint,
integerStep: bigint,
): bigint | null => {
const first: number = bitLength(minimum) - 1;
const last: number = bitLength(maximum) - 1;
for (let exponent = first; exponent <= last; ++exponent) {
const bandMinimum: bigint = max(minimum, BigInt(1) << BigInt(exponent));
const bandMaximum: bigint = min(
maximum,
(BigInt(1) << BigInt(exponent + 1)) - BigInt(1),
);
const quantum: bigint =
exponent <= 52 ? BigInt(1) : BigInt(1) << BigInt(exponent - 52);
const alignedStep: bigint =
(integerStep / _decimalGcd(integerStep, quantum)) * quantum;
const lower: bigint = lowerBound(
{ numerator: bandMinimum, denominator: alignedStep },
false,
);
const upper: bigint = upperBound(
{ numerator: bandMaximum, denominator: alignedStep },
false,
);
if (lower <= upper) return randomBigint(lower, upper) * alignedStep;
}
return null;
};
const bitLength = (value: bigint): number => value.toString(2).length;
const min = (x: bigint, y: bigint): bigint => (x < y ? x : y);
const max = (x: bigint, y: bigint): bigint => (x > y ? x : y);
const lowerBound = (ratio: _IDecimalRatio, exclusive: boolean): bigint => {
const quotient: bigint = ratio.numerator / ratio.denominator;
const remainder: bigint = ratio.numerator % ratio.denominator;
const ceiling: bigint =
quotient + (remainder > BigInt(0) ? BigInt(1) : BigInt(0));
return (
ceiling + (exclusive && remainder === BigInt(0) ? BigInt(1) : BigInt(0))
);
};
const upperBound = (ratio: _IDecimalRatio, exclusive: boolean): bigint => {
const quotient: bigint = ratio.numerator / ratio.denominator;
const remainder: bigint = ratio.numerator % ratio.denominator;
const floor: bigint =
quotient - (remainder < BigInt(0) ? BigInt(1) : BigInt(0));
return floor - (exclusive && remainder === BigInt(0) ? BigInt(1) : BigInt(0));
};
const randomBigint = (minimum: bigint, maximum: bigint): bigint => {
const scale: bigint = BigInt(1) << BigInt(53);
const sample: bigint = BigInt(
Math.min(
Number(scale - BigInt(1)),
Math.floor(Math.max(0, Math.random()) * Number(scale)),
),
);
return minimum + ((maximum - minimum + BigInt(1)) * sample) / scale;
};
const clamp = (value: bigint, minimum: bigint, maximum: bigint): bigint =>
value < minimum ? minimum : value > maximum ? maximum : value;
const nearby = (
selected: bigint,
minimum: bigint,
maximum: bigint,
): bigint[] => {
const output: bigint[] = [];
for (let distance = BigInt(1); distance <= BigInt(32); ++distance) {
if (selected - distance >= minimum) output.push(selected - distance);
if (selected + distance <= maximum) output.push(selected + distance);
}
return output;
};
const unique = (values: bigint[]): bigint[] => [...new Set(values)];