From 11f7e54c4203cf051b82f238898adc2095b716ac Mon Sep 17 00:00:00 2001 From: Nikita Skvortsov Date: Tue, 26 May 2026 10:14:42 +0300 Subject: [PATCH 1/2] feat: number with scale error messages --- src/lib/kit/constants/config.tsx | 2 + src/lib/kit/i18n/en.json | 2 + src/lib/kit/i18n/ru.json | 2 + src/lib/kit/validators/messages.ts | 6 + src/lib/kit/validators/types.ts | 2 + src/lib/kit/validators/validators.ts | 289 ++++++++++++++---- src/stories/StringNumberWithScale.stories.tsx | 2 +- .../components/InputPreview/constants.ts | 2 +- 8 files changed, 253 insertions(+), 54 deletions(-) diff --git a/src/lib/kit/constants/config.tsx b/src/lib/kit/constants/config.tsx index 9108289b..41aae3ea 100644 --- a/src/lib/kit/constants/config.tsx +++ b/src/lib/kit/constants/config.tsx @@ -78,6 +78,7 @@ import { getArrayValidator, getBooleanValidator, getNumberValidator, + getNumberWithScaleValidator, getObjectValidator, getStringValidator, } from '../validators'; @@ -200,6 +201,7 @@ export const dynamicConfig: DynamicFormConfig = { validators: { base: getStringValidator(), number: getNumberValidator() as unknown as ValidatorType, + number_with_scale: getNumberWithScaleValidator(), }, }, }; diff --git a/src/lib/kit/i18n/en.json b/src/lib/kit/i18n/en.json index 1629b47f..5b573d2a 100644 --- a/src/lib/kit/i18n/en.json +++ b/src/lib/kit/i18n/en.json @@ -37,6 +37,8 @@ ], "label_error-min-number": "Value must be an integer no less than {{count}}", "label_error-max-number": "Value must not be greater than {{count}}", + "label_error-min-number-with-scale": "Value must be no less than {{count}} {{scale}}", + "label_error-max-number-with-scale": "Value must not be greater than {{count}} {{scale}}", "label_error-space-end": "Value must not end with a space", "label_error-space-start": "Value must not start with a space", "label_error-zero-start": "Value must not start with a zero", diff --git a/src/lib/kit/i18n/ru.json b/src/lib/kit/i18n/ru.json index 0738f3d2..490bcca8 100644 --- a/src/lib/kit/i18n/ru.json +++ b/src/lib/kit/i18n/ru.json @@ -37,6 +37,8 @@ ], "label_error-min-number": "Значение должно быть числом не меньше {{count}}", "label_error-max-number": "Значение должно быть числом не больше {{count}}", + "label_error-min-number-with-scale": "Значение должно быть не меньше {{count}} {{scale}}", + "label_error-max-number-with-scale": "Значение должно быть не больше {{count}} {{scale}}", "label_error-space-end": "Значение не должно заканчиваться пробелом", "label_error-space-start": "Значение не должно начинаться с пробела", "label_error-zero-start": "Значение не должно начинаться с нуля", diff --git a/src/lib/kit/validators/messages.ts b/src/lib/kit/validators/messages.ts index 50be056a..322a7622 100644 --- a/src/lib/kit/validators/messages.ts +++ b/src/lib/kit/validators/messages.ts @@ -26,6 +26,12 @@ const getErrorMessages = (): ErrorMessagesType => ({ maxNumber(count: number | bigint) { return i18n('label_error-max-number', {count}); }, + minNumberWithScale(count: number | bigint | string, scale: string) { + return i18n('label_error-min-number-with-scale', {count, scale}); + }, + maxNumberWithScale(count: number | bigint | string, scale: string) { + return i18n('label_error-max-number-with-scale', {count, scale}); + }, SPACE_START: i18n('label_error-space-start'), SPACE_END: i18n('label_error-space-end'), DOT_END: i18n('label_error-dot-end'), diff --git a/src/lib/kit/validators/types.ts b/src/lib/kit/validators/types.ts index f84b051f..768a415e 100644 --- a/src/lib/kit/validators/types.ts +++ b/src/lib/kit/validators/types.ts @@ -9,6 +9,8 @@ export interface ErrorMessagesType { maxLengthArr: (count: number | bigint) => string; minNumber: (count: number | bigint) => string; maxNumber: (count: number | bigint) => string; + minNumberWithScale: (count: number | bigint | string, scale: string) => string; + maxNumberWithScale: (count: number | bigint | string, scale: string) => string; SPACE_START: string; SPACE_END: string; DOT_END: string; diff --git a/src/lib/kit/validators/validators.ts b/src/lib/kit/validators/validators.ts index 5754e526..96a3d885 100644 --- a/src/lib/kit/validators/validators.ts +++ b/src/lib/kit/validators/validators.ts @@ -11,6 +11,7 @@ import type { ObjectValue, StringSpec, } from '../../core'; +import {divide, multiply} from '../utils'; import {ErrorMessages} from '../validators'; import {isFloat, isInt} from './helpers'; @@ -88,96 +89,280 @@ export interface GetNumberValidatorParams extends CommonValidatorParams { ignoreZeroEnd?: boolean; } +type NumberSyntaxFlags = Pick< + GetNumberValidatorParams, + | 'ignoreRequiredCheck' + | 'ignoreSpaceStartCheck' + | 'ignoreSpaceEndCheck' + | 'ignoreNumberCheck' + | 'ignoreDotEnd' + | 'ignoreZeroStart' + | 'ignoreInvalidZeroFormat' + | 'ignoreZeroEnd' +>; + +// eslint-disable-next-line complexity +const validateNumberSyntax = ( + stringValue: string, + required: boolean | undefined, + flags: NumberSyntaxFlags, + errorMessages: ErrorMessagesType, +): string | false => { + if (!flags.ignoreRequiredCheck && required && !stringValue.length) { + return errorMessages.REQUIRED; + } + + if (!stringValue.length) { + return false; + } + + if (!flags.ignoreSpaceStartCheck && !stringValue[0].trim()) { + return errorMessages.SPACE_START; + } + + if (!flags.ignoreSpaceEndCheck && !stringValue[stringValue.length - 1].trim()) { + return errorMessages.SPACE_END; + } + + if (!flags.ignoreDotEnd && stringValue[stringValue.length - 1] === '.') { + return errorMessages.DOT_END; + } + + if (!flags.ignoreNumberCheck && !isFloat(stringValue)) { + return errorMessages.NUMBER; + } + + if ( + !flags.ignoreZeroStart && + ((stringValue.length > 1 && stringValue[0] === '0' && stringValue[1] !== '.') || + (stringValue.length > 2 && + stringValue.substring(0, 2) === '-0' && + stringValue[2] !== '.')) + ) { + return errorMessages.ZERO_START; + } + + if ( + !flags.ignoreInvalidZeroFormat && + stringValue.trim().length > 1 && + Number(stringValue.trim()) === 0 + ) { + return errorMessages.INVALID_ZERO_FORMAT; + } + + if ( + !flags.ignoreZeroEnd && + !isInt(stringValue) && + stringValue[stringValue.length - 1] === '0' + ) { + return errorMessages.ZERO_END; + } + + return false; +}; + export const getNumberValidator = (params: GetNumberValidatorParams = {}) => { const { - ignoreRequiredCheck, - ignoreSpaceStartCheck, - ignoreSpaceEndCheck, - ignoreNumberCheck, ignoreMaximumCheck, ignoreMinimumCheck, ignoreIntCheck, - ignoreDotEnd, - ignoreZeroStart, - ignoreInvalidZeroFormat, - ignoreZeroEnd, customErrorMessages, + ...syntaxFlags } = params; - // eslint-disable-next-line complexity return (spec: NumberSpec, value: string | number = '') => { const errorMessages = {...ErrorMessages, ...customErrorMessages}; - const stringValue = String(value); - if (!ignoreRequiredCheck && spec.required && !stringValue.length) { - return errorMessages.REQUIRED; + const syntaxError = validateNumberSyntax( + stringValue, + spec.required, + syntaxFlags, + errorMessages, + ); + if (syntaxError) { + return syntaxError; } - if (stringValue.length) { - if (!ignoreSpaceStartCheck && !stringValue[0].trim()) { - return errorMessages.SPACE_START; - } + if ( + !ignoreMaximumCheck && + isNumber(spec.maximum) && + stringValue.length && + Number(stringValue) > spec.maximum + ) { + return errorMessages.maxNumber(spec.maximum); + } - if (!ignoreSpaceEndCheck && !stringValue[stringValue.length - 1].trim()) { - return errorMessages.SPACE_END; - } + if ( + !ignoreMinimumCheck && + isNumber(spec.minimum) && + stringValue.length && + spec.minimum > Number(stringValue) + ) { + return errorMessages.minNumber(spec.minimum); + } - if (!ignoreDotEnd && stringValue[stringValue.length - 1] === '.') { - return errorMessages.DOT_END; + if (isString(spec.format) && stringValue.length) { + if (!ignoreIntCheck && spec.format === 'int64' && !isInt(stringValue)) { + return errorMessages.INT; } + } - if (!ignoreNumberCheck && !isFloat(stringValue)) { - return errorMessages.NUMBER; - } + return false; + }; +}; + +export interface GetNumberWithScaleValidatorParams extends GetNumberValidatorParams {} + +const scaleLimit = (limit: number, defaultFactor: string, scaleFactor: string): string | null => { + if (defaultFactor === scaleFactor) { + return String(limit); + } + + const limitStr = String(limit); + + try { + if (BigInt(scaleFactor) > BigInt(defaultFactor)) { + const ratio = divide(scaleFactor, defaultFactor); + return ratio ? divide(limitStr, ratio, 6) : null; + } + + const ratio = divide(defaultFactor, scaleFactor); + return ratio ? multiply(limitStr, ratio, 6) : null; + } catch { + return null; + } +}; + +const isReadable = (numStr: string | null): boolean => { + if (numStr === null) { + return false; + } + + // Math.abs does not support BigInt + const intPart = numStr.split('.')[0].replace('-', ''); + + try { + return BigInt(intPart) >= BigInt(1); + } catch { + return false; + } +}; + +const getMostAppropriateScale = ( + sizeParams: NonNullable, + value: string, + limit: number, +): string => { + const {defaultType, scale} = sizeParams; + + const limitStr = String(limit); + const hasValidValue = Boolean(value) && divide(value, value) !== null; + + const defaultFactor = scale[defaultType].factor; + let bestType = defaultType; + + Object.keys(scale).forEach((key) => { + if (BigInt(scale[key].factor) > BigInt(scale[bestType].factor)) { + const ratio = divide(scale[key].factor, defaultFactor); - if ( - !ignoreZeroStart && - ((stringValue.length > 1 && stringValue[0] === '0' && stringValue[1] !== '.') || - (stringValue.length > 2 && - stringValue.substring(0, 2) === '-0' && - stringValue[2] !== '.')) - ) { - return errorMessages.ZERO_START; + if (!ratio) { + return; } - if ( - !ignoreInvalidZeroFormat && - stringValue.trim().length > 1 && - Number(stringValue.trim()) === 0 - ) { - return errorMessages.INVALID_ZERO_FORMAT; + if (!isReadable(divide(limitStr, ratio, 2))) { + return; } - if ( - !ignoreZeroEnd && - !isInt(stringValue) && - stringValue[stringValue.length - 1] === '0' - ) { - return errorMessages.ZERO_END; + if (hasValidValue && !isReadable(divide(value, ratio, 2))) { + return; } + + bestType = key; + } + }); + + return bestType; +}; + +const getScaledLimit = ( + spec: StringSpec, + limit: number, + value: string, +): {count: string; scaleTitle: string} | undefined => { + const sizeParams = spec.viewSpec?.sizeParams; + + if (!sizeParams) { + return undefined; + } + + const selectedScale = getMostAppropriateScale(sizeParams, value, limit); + const scaleEntry = sizeParams.scale[selectedScale]; + const defaultEntry = sizeParams.scale[sizeParams.defaultType]; + + if (!scaleEntry || !defaultEntry) { + return undefined; + } + + const count = scaleLimit(limit, defaultEntry.factor, scaleEntry.factor); + + if (count === null) { + return undefined; + } + + return {count, scaleTitle: scaleEntry.title}; +}; + +export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorParams = {}) => { + const { + ignoreMaximumCheck, + ignoreMinimumCheck, + ignoreIntCheck, + customErrorMessages, + ...syntaxFlags + } = params; + + return (spec: StringSpec, value = '') => { + const errorMessages = {...ErrorMessages, ...customErrorMessages}; + const numericSpec = spec as unknown as NumberSpec; + const stringValue = String(value); + + const syntaxError = validateNumberSyntax( + stringValue, + spec.required, + syntaxFlags, + errorMessages, + ); + if (syntaxError) { + return syntaxError; } if ( !ignoreMaximumCheck && - isNumber(spec.maximum) && + isNumber(numericSpec.maximum) && stringValue.length && - Number(stringValue) > spec.maximum + Number(stringValue) > numericSpec.maximum ) { - return errorMessages.maxNumber(spec.maximum); + const scaled = getScaledLimit(spec, numericSpec.maximum, stringValue); + return scaled + ? errorMessages.maxNumberWithScale(scaled.count, scaled.scaleTitle) + : errorMessages.maxNumber(numericSpec.maximum); } if ( !ignoreMinimumCheck && - isNumber(spec.minimum) && + isNumber(numericSpec.minimum) && stringValue.length && - spec.minimum > Number(stringValue) + numericSpec.minimum > Number(stringValue) ) { - return errorMessages.minNumber(spec.minimum); + const scaled = getScaledLimit(spec, numericSpec.minimum, stringValue); + return scaled + ? errorMessages.minNumberWithScale(scaled.count, scaled.scaleTitle) + : errorMessages.minNumber(numericSpec.minimum); } - if (isString(spec.format) && stringValue.length) { - if (!ignoreIntCheck && spec.format === 'int64' && !isInt(stringValue)) { + if (isString(numericSpec.format) && stringValue.length) { + if (!ignoreIntCheck && numericSpec.format === 'int64' && !isInt(stringValue)) { return errorMessages.INT; } } diff --git a/src/stories/StringNumberWithScale.stories.tsx b/src/stories/StringNumberWithScale.stories.tsx index 6e9d23ae..c68e5ee4 100644 --- a/src/stories/StringNumberWithScale.stories.tsx +++ b/src/stories/StringNumberWithScale.stories.tsx @@ -14,7 +14,7 @@ export default { const baseSpec: StringSpec = { type: SpecTypes.String, - validator: 'number', + validator: 'number_with_scale', viewSpec: { type: 'number_with_scale', layout: 'row', diff --git a/src/stories/components/InputPreview/constants.ts b/src/stories/components/InputPreview/constants.ts index 5c9c3ffd..99d0aff3 100644 --- a/src/stories/components/InputPreview/constants.ts +++ b/src/stories/components/InputPreview/constants.ts @@ -452,7 +452,7 @@ const sizeParams: ObjectSpec = { viewSpec: {type: 'base', layout: 'table_item'}, }, factor: { - type: SpecTypes.Number, + type: SpecTypes.String, viewSpec: {type: 'base', layout: 'table_item'}, }, title: { From f452df8a032494a6b9cacda1daba15c9c9dfff7f Mon Sep 17 00:00:00 2001 From: Nikita Skvortsov Date: Wed, 27 May 2026 11:34:20 +0300 Subject: [PATCH 2/2] fix: de-deduplicated code in numberValidator and numberWithScaleValidator --- src/lib/kit/validators/helpers.ts | 114 ++++++++++ src/lib/kit/validators/validators.ts | 312 +++++++++++---------------- 2 files changed, 234 insertions(+), 192 deletions(-) diff --git a/src/lib/kit/validators/helpers.ts b/src/lib/kit/validators/helpers.ts index 4594196d..7c06d5ce 100644 --- a/src/lib/kit/validators/helpers.ts +++ b/src/lib/kit/validators/helpers.ts @@ -1,3 +1,7 @@ +import type {StringSpec} from 'src/lib/core'; + +import {divide, multiply} from '../utils'; + export const isInt = (value: string) => { const regex = /^(?:[-+]?(?:0|[1-9]\d*))$/; @@ -9,3 +13,113 @@ export const isFloat = (value: string) => { return regex.test(value); }; + +/** + * Scales value given in defaultFactor to scaleFactor + * @param value + * @param scaleFactor + * @param defaultFactor + * @returns value in scaleFactor + */ +const scaleValue = (value: number, scaleFactor: string, defaultFactor: string): string | null => { + if (defaultFactor === scaleFactor) { + return String(value); + } + + const valueStr = String(value); + + try { + if (BigInt(scaleFactor) > BigInt(defaultFactor)) { + const ratio = divide(scaleFactor, defaultFactor); + return ratio ? divide(valueStr, ratio, 6) : null; + } + + const ratio = divide(defaultFactor, scaleFactor); + return ratio ? multiply(valueStr, ratio, 6) : null; + } catch { + return null; + } +}; + +/** + * Checks if the number is > 1 + * @param numStr - number in a string form + * @returns true if the number is > 1, false otherwise + */ +const isReadable = (numStr: string | null): boolean => { + if (numStr === null) { + return false; + } + + // Math.abs does not support BigInt + const intPart = numStr.split('.')[0].replace('-', ''); + + try { + return BigInt(intPart) >= BigInt(1); + } catch { + return false; + } +}; + +/** + * Returns the biggest readable scale for the given limit + * @param sizeParams - sizeParams of NumberWithScale + * @param value - value in defaultFactor scale, which is referenced to deduce appropriate scale + * @returns the biggest readable scale for the given limit + */ +const getMostAppropriateScale = ( + sizeParams: NonNullable, + value: number, +): string => { + const {defaultType, scale} = sizeParams; + + const valueStr = String(value); + + const defaultFactor = scale[defaultType].factor; + let bestType = defaultType; + + Object.keys(scale).forEach((key) => { + if (BigInt(scale[key].factor) > BigInt(scale[bestType].factor)) { + const ratio = divide(scale[key].factor, defaultFactor); + + if (!ratio) { + return; + } + + if (!isReadable(divide(valueStr, ratio, 2))) { + return; + } + + bestType = key; + } + }); + + return bestType; +}; + +export const getScaledLimit = ( + spec: StringSpec, + limit: number, +): {count: string; scaleTitle: string} | undefined => { + const sizeParams = spec.viewSpec?.sizeParams; + + if (!sizeParams) { + return undefined; + } + + const mostAppropriateScale = getMostAppropriateScale(sizeParams, limit); + const scaleEntry = sizeParams.scale[mostAppropriateScale]; + const defaultEntry = sizeParams.scale[sizeParams.defaultType]; + + if (!scaleEntry || !defaultEntry) { + return undefined; + } + + const count = scaleValue(limit, scaleEntry.factor, defaultEntry.factor); + + if (count === null) { + return undefined; + } + + return {count, scaleTitle: scaleEntry.title}; +}; diff --git a/src/lib/kit/validators/validators.ts b/src/lib/kit/validators/validators.ts index 96a3d885..afb695ce 100644 --- a/src/lib/kit/validators/validators.ts +++ b/src/lib/kit/validators/validators.ts @@ -11,10 +11,9 @@ import type { ObjectValue, StringSpec, } from '../../core'; -import {divide, multiply} from '../utils'; import {ErrorMessages} from '../validators'; -import {isFloat, isInt} from './helpers'; +import {getScaledLimit, isFloat, isInt} from './helpers'; import type {ErrorMessagesType} from './types'; interface CommonValidatorParams { @@ -89,99 +88,74 @@ export interface GetNumberValidatorParams extends CommonValidatorParams { ignoreZeroEnd?: boolean; } -type NumberSyntaxFlags = Pick< - GetNumberValidatorParams, - | 'ignoreRequiredCheck' - | 'ignoreSpaceStartCheck' - | 'ignoreSpaceEndCheck' - | 'ignoreNumberCheck' - | 'ignoreDotEnd' - | 'ignoreZeroStart' - | 'ignoreInvalidZeroFormat' - | 'ignoreZeroEnd' ->; - -// eslint-disable-next-line complexity -const validateNumberSyntax = ( - stringValue: string, - required: boolean | undefined, - flags: NumberSyntaxFlags, - errorMessages: ErrorMessagesType, -): string | false => { - if (!flags.ignoreRequiredCheck && required && !stringValue.length) { - return errorMessages.REQUIRED; - } - - if (!stringValue.length) { - return false; - } - - if (!flags.ignoreSpaceStartCheck && !stringValue[0].trim()) { - return errorMessages.SPACE_START; - } - - if (!flags.ignoreSpaceEndCheck && !stringValue[stringValue.length - 1].trim()) { - return errorMessages.SPACE_END; - } - - if (!flags.ignoreDotEnd && stringValue[stringValue.length - 1] === '.') { - return errorMessages.DOT_END; - } - - if (!flags.ignoreNumberCheck && !isFloat(stringValue)) { - return errorMessages.NUMBER; - } - - if ( - !flags.ignoreZeroStart && - ((stringValue.length > 1 && stringValue[0] === '0' && stringValue[1] !== '.') || - (stringValue.length > 2 && - stringValue.substring(0, 2) === '-0' && - stringValue[2] !== '.')) - ) { - return errorMessages.ZERO_START; - } - - if ( - !flags.ignoreInvalidZeroFormat && - stringValue.trim().length > 1 && - Number(stringValue.trim()) === 0 - ) { - return errorMessages.INVALID_ZERO_FORMAT; - } - - if ( - !flags.ignoreZeroEnd && - !isInt(stringValue) && - stringValue[stringValue.length - 1] === '0' - ) { - return errorMessages.ZERO_END; - } - - return false; -}; - export const getNumberValidator = (params: GetNumberValidatorParams = {}) => { const { + ignoreRequiredCheck, + ignoreSpaceStartCheck, + ignoreSpaceEndCheck, + ignoreNumberCheck, ignoreMaximumCheck, ignoreMinimumCheck, ignoreIntCheck, + ignoreDotEnd, + ignoreZeroStart, + ignoreInvalidZeroFormat, + ignoreZeroEnd, customErrorMessages, - ...syntaxFlags } = params; + // eslint-disable-next-line complexity return (spec: NumberSpec, value: string | number = '') => { const errorMessages = {...ErrorMessages, ...customErrorMessages}; + const stringValue = String(value); - const syntaxError = validateNumberSyntax( - stringValue, - spec.required, - syntaxFlags, - errorMessages, - ); - if (syntaxError) { - return syntaxError; + if (!ignoreRequiredCheck && spec.required && !stringValue.length) { + return errorMessages.REQUIRED; + } + + if (stringValue.length) { + if (!ignoreSpaceStartCheck && !stringValue[0].trim()) { + return errorMessages.SPACE_START; + } + + if (!ignoreSpaceEndCheck && !stringValue[stringValue.length - 1].trim()) { + return errorMessages.SPACE_END; + } + + if (!ignoreDotEnd && stringValue[stringValue.length - 1] === '.') { + return errorMessages.DOT_END; + } + + if (!ignoreNumberCheck && !isFloat(stringValue)) { + return errorMessages.NUMBER; + } + + if ( + !ignoreZeroStart && + ((stringValue.length > 1 && stringValue[0] === '0' && stringValue[1] !== '.') || + (stringValue.length > 2 && + stringValue.substring(0, 2) === '-0' && + stringValue[2] !== '.')) + ) { + return errorMessages.ZERO_START; + } + + if ( + !ignoreInvalidZeroFormat && + stringValue.trim().length > 1 && + Number(stringValue.trim()) === 0 + ) { + return errorMessages.INVALID_ZERO_FORMAT; + } + + if ( + !ignoreZeroEnd && + !isInt(stringValue) && + stringValue[stringValue.length - 1] === '0' + ) { + return errorMessages.ZERO_END; + } } if ( @@ -214,127 +188,81 @@ export const getNumberValidator = (params: GetNumberValidatorParams = {}) => { export interface GetNumberWithScaleValidatorParams extends GetNumberValidatorParams {} -const scaleLimit = (limit: number, defaultFactor: string, scaleFactor: string): string | null => { - if (defaultFactor === scaleFactor) { - return String(limit); - } - - const limitStr = String(limit); - - try { - if (BigInt(scaleFactor) > BigInt(defaultFactor)) { - const ratio = divide(scaleFactor, defaultFactor); - return ratio ? divide(limitStr, ratio, 6) : null; - } - - const ratio = divide(defaultFactor, scaleFactor); - return ratio ? multiply(limitStr, ratio, 6) : null; - } catch { - return null; - } -}; - -const isReadable = (numStr: string | null): boolean => { - if (numStr === null) { - return false; - } - - // Math.abs does not support BigInt - const intPart = numStr.split('.')[0].replace('-', ''); - - try { - return BigInt(intPart) >= BigInt(1); - } catch { - return false; - } -}; +interface NumberWithScaleSpec extends StringSpec { + minimum?: number; + maximum?: number; + format?: 'float' | 'int64'; +} -const getMostAppropriateScale = ( - sizeParams: NonNullable, - value: string, - limit: number, -): string => { - const {defaultType, scale} = sizeParams; +export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorParams = {}) => { + const { + ignoreRequiredCheck, + ignoreSpaceStartCheck, + ignoreSpaceEndCheck, + ignoreNumberCheck, + ignoreMaximumCheck, + ignoreMinimumCheck, + ignoreIntCheck, + ignoreDotEnd, + ignoreZeroStart, + ignoreInvalidZeroFormat, + ignoreZeroEnd, + customErrorMessages, + } = params; - const limitStr = String(limit); - const hasValidValue = Boolean(value) && divide(value, value) !== null; + // eslint-disable-next-line complexity + return (spec: StringSpec, value = '') => { + const errorMessages = {...ErrorMessages, ...customErrorMessages}; + const numericSpec = spec as NumberWithScaleSpec; - const defaultFactor = scale[defaultType].factor; - let bestType = defaultType; + const stringValue = String(value); - Object.keys(scale).forEach((key) => { - if (BigInt(scale[key].factor) > BigInt(scale[bestType].factor)) { - const ratio = divide(scale[key].factor, defaultFactor); + if (!ignoreRequiredCheck && spec.required && !stringValue.length) { + return errorMessages.REQUIRED; + } - if (!ratio) { - return; + if (stringValue.length) { + if (!ignoreSpaceStartCheck && !stringValue[0].trim()) { + return errorMessages.SPACE_START; } - if (!isReadable(divide(limitStr, ratio, 2))) { - return; + if (!ignoreSpaceEndCheck && !stringValue[stringValue.length - 1].trim()) { + return errorMessages.SPACE_END; } - if (hasValidValue && !isReadable(divide(value, ratio, 2))) { - return; + if (!ignoreDotEnd && stringValue[stringValue.length - 1] === '.') { + return errorMessages.DOT_END; } - bestType = key; - } - }); - - return bestType; -}; - -const getScaledLimit = ( - spec: StringSpec, - limit: number, - value: string, -): {count: string; scaleTitle: string} | undefined => { - const sizeParams = spec.viewSpec?.sizeParams; - - if (!sizeParams) { - return undefined; - } - - const selectedScale = getMostAppropriateScale(sizeParams, value, limit); - const scaleEntry = sizeParams.scale[selectedScale]; - const defaultEntry = sizeParams.scale[sizeParams.defaultType]; - - if (!scaleEntry || !defaultEntry) { - return undefined; - } - - const count = scaleLimit(limit, defaultEntry.factor, scaleEntry.factor); - - if (count === null) { - return undefined; - } - - return {count, scaleTitle: scaleEntry.title}; -}; + if (!ignoreNumberCheck && !isFloat(stringValue)) { + return errorMessages.NUMBER; + } -export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorParams = {}) => { - const { - ignoreMaximumCheck, - ignoreMinimumCheck, - ignoreIntCheck, - customErrorMessages, - ...syntaxFlags - } = params; + if ( + !ignoreZeroStart && + ((stringValue.length > 1 && stringValue[0] === '0' && stringValue[1] !== '.') || + (stringValue.length > 2 && + stringValue.substring(0, 2) === '-0' && + stringValue[2] !== '.')) + ) { + return errorMessages.ZERO_START; + } - return (spec: StringSpec, value = '') => { - const errorMessages = {...ErrorMessages, ...customErrorMessages}; - const numericSpec = spec as unknown as NumberSpec; - const stringValue = String(value); + if ( + !ignoreInvalidZeroFormat && + stringValue.trim().length > 1 && + Number(stringValue.trim()) === 0 + ) { + return errorMessages.INVALID_ZERO_FORMAT; + } - const syntaxError = validateNumberSyntax( - stringValue, - spec.required, - syntaxFlags, - errorMessages, - ); - if (syntaxError) { - return syntaxError; + if ( + !ignoreZeroEnd && + !isInt(stringValue) && + stringValue[stringValue.length - 1] === '0' + ) { + return errorMessages.ZERO_END; + } } if ( @@ -343,7 +271,7 @@ export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorP stringValue.length && Number(stringValue) > numericSpec.maximum ) { - const scaled = getScaledLimit(spec, numericSpec.maximum, stringValue); + const scaled = getScaledLimit(spec, numericSpec.maximum); return scaled ? errorMessages.maxNumberWithScale(scaled.count, scaled.scaleTitle) : errorMessages.maxNumber(numericSpec.maximum); @@ -355,7 +283,7 @@ export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorP stringValue.length && numericSpec.minimum > Number(stringValue) ) { - const scaled = getScaledLimit(spec, numericSpec.minimum, stringValue); + const scaled = getScaledLimit(spec, numericSpec.minimum); return scaled ? errorMessages.minNumberWithScale(scaled.count, scaled.scaleTitle) : errorMessages.minNumber(numericSpec.minimum);