@lucaspaganini/value-objects
Version:
TypeScript first validation and class creation library
158 lines (144 loc) • 5.29 kB
text/typescript
import { isDefined, isNotNumber } from '../utils'
import { LogicError, MaxSizeError, MinSizeError, NotInSetError, NotIntegerError, RawTypeError } from './errors'
export interface VOFloatOptions {
/**
* Minimum inclusive acceptable value.
* Can't be bigger than `max`.
*/
min?: number
/**
* Maximum inclusive acceptable value.
* Can't be smaller than `min`.
*/
max?: number
/**
* Floating point precision.
* Can't be less than zero.
*/
precision?: number
/**
* Trimming strategy for numbers with more precision than `precision`.
* @default "round"
*/
precisionTrim?: 'floor' | 'ceil' | 'round'
}
export interface VOFloatInstance {
valueOf(): number
}
export interface VOFloatConstructor {
new (r: number): VOFloatInstance
}
const makeIsInSet = <T extends string>(values: Array<T>) => {
const set = new Set(values)
return (v: string): v is T => set.has(<T>v)
}
const PRECISION_TRIM_SET: Array<NonNullable<VOFloatOptions['precisionTrim']>> = ['floor', 'ceil', 'round']
const isPrecisionTrim = makeIsInSet(PRECISION_TRIM_SET)
/**
* Function to create a floating point number value object constructor.
*
* @param options Customizations for the returned class constructor
* @return Class constructor that accepts a number for instantiation
* and returns that number when {@link VOFloatInstance.valueOf} is called.
*
* @example
* ```typescript
* class MyFloat extends VOFloat() {}
*
* const float1 = new MyFloat(5); // OK
* float1.valueOf(); // 5
*
* const float2 = new MyFloat(5.0); // OK
* float2.valueOf(); // 5
*
* const float3 = new MyFloat(5.5); // OK
* float3.valueOf(); // 5.5
*
* const float4 = new MyFloat('5.5'); // Compilation error: Not a number
* ```
*
* @example
* ```typescript
* class PositiveNumber extends VOFloat({ min: 0 }) {} // OK
* new PositiveNumber(0); // OK
* new PositiveNumber(1000000); // Ok
* new PositiveNumber(-1); // Runtime error: Too small
* new PositiveNumber(1.5); // OK
* ```
*
* @example
* ```typescript
* class FloatWithValidRange extends VOFloat({ min: -100.5, max: 100.5 }) {} // OK
* new FloatWithValidRange(-100); // OK
* new FloatWithValidRange(100); // Ok
* new FloatWithValidRange(-100.5); // OK
* new FloatWithValidRange(-101); // Runtime error: Too small
* new FloatWithValidRange(101); // Runtime error: Too big
* ```
*
* @example
* ```typescript
* class FloatWithInvalidRange extends VOFloat({ min: 100, max: -100 }) {} // Runtime error: Invalid logic (options.min should not be bigger than options.max)
* ```
*
* @example
* ```typescript
* class LimitedPrecisionFloat extends VOFloat({
* precision: 5,
* precisionTrim: 'round'
* }) {} // OK
* const limited1 = new LimitedPrecisionFloat(0.123456789);
* limited1.valueOf(); // 0.12346 => Only 5 precision digits and it's rounded
* ```
*
* @example
* ```typescript
* class LimitedPrecisionFloatWithRange extends VOFloat({
* min: 1,
* max: 999.999,
* precision: 2,
* precisionTrim: 'ceil'
* }) {} // OK
* new LimitedPrecisionFloatWithRange(-100); // Runtime error: Too small
* new LimitedPrecisionFloatWithRange(100); // Ok
* new LimitedPrecisionFloatWithRange(0.9999); // OK (rounds to 1 and passes the minimum)
* new LimitedPrecisionFloatWithRange(999.999); // Runtime error: Too big (rounds to 1000 and doesn't pass the maximum)
* const limited2 = new LimitedPrecisionFloatWithRange(0.123456789);
* limited2.valueOf(); // 0.13 => Only 2 precision digits and it's rounded up because we're using "ceil"
* ```
*/
export const VOFloat = (options: VOFloatOptions = {}): VOFloatConstructor => {
if (isDefined(options.min)) {
if (isNotNumber(options.min)) throw new RawTypeError('number', typeof options.min, 'options.min')
}
if (isDefined(options.max)) {
if (isNotNumber(options.max)) throw new RawTypeError('number', typeof options.max, 'options.max')
}
if (isDefined(options.min) && isDefined(options.max)) {
if (options.min > options.max) throw new LogicError('options.min should not be bigger than options.max')
}
if (isDefined(options.precision)) {
if (isNotNumber(options.precision)) throw new RawTypeError('number', typeof options.precision, 'options.precision')
if (!Number.isInteger(options.precision)) throw new NotIntegerError(options.precision, 'options.precision')
if (options.precision < 0) throw new MinSizeError(options.precision, 0)
}
if (isDefined(options.precisionTrim)) {
if (!isPrecisionTrim(options.precisionTrim))
throw new NotInSetError(PRECISION_TRIM_SET, options.precisionTrim, 'options.precisionTrim')
}
const precisionPower = 10 ** (options.precision ?? 0)
const precisionTrim = options.precisionTrim ?? 'round'
return class {
protected _value: number
constructor(raw: number) {
if (isNotNumber(raw)) throw new RawTypeError('number', typeof raw, 'raw')
if (isDefined(options.precision)) raw = Math[precisionTrim](raw * precisionPower) / precisionPower
if (isDefined(options.min) && raw < options.min) throw new MinSizeError(options.min, raw)
if (isDefined(options.max) && raw > options.max) throw new MaxSizeError(options.max, raw)
this._value = raw
}
valueOf(): number {
return this._value
}
}
}