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/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/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..afb695ce 100644 --- a/src/lib/kit/validators/validators.ts +++ b/src/lib/kit/validators/validators.ts @@ -13,7 +13,7 @@ import type { } from '../../core'; import {ErrorMessages} from '../validators'; -import {isFloat, isInt} from './helpers'; +import {getScaledLimit, isFloat, isInt} from './helpers'; import type {ErrorMessagesType} from './types'; interface CommonValidatorParams { @@ -186,6 +186,119 @@ export const getNumberValidator = (params: GetNumberValidatorParams = {}) => { }; }; +export interface GetNumberWithScaleValidatorParams extends GetNumberValidatorParams {} + +interface NumberWithScaleSpec extends StringSpec { + minimum?: number; + maximum?: number; + format?: 'float' | 'int64'; +} + +export const getNumberWithScaleValidator = (params: GetNumberWithScaleValidatorParams = {}) => { + const { + ignoreRequiredCheck, + ignoreSpaceStartCheck, + ignoreSpaceEndCheck, + ignoreNumberCheck, + ignoreMaximumCheck, + ignoreMinimumCheck, + ignoreIntCheck, + ignoreDotEnd, + ignoreZeroStart, + ignoreInvalidZeroFormat, + ignoreZeroEnd, + customErrorMessages, + } = params; + + // eslint-disable-next-line complexity + return (spec: StringSpec, value = '') => { + const errorMessages = {...ErrorMessages, ...customErrorMessages}; + const numericSpec = spec as NumberWithScaleSpec; + + const stringValue = String(value); + + 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 ( + !ignoreMaximumCheck && + isNumber(numericSpec.maximum) && + stringValue.length && + Number(stringValue) > numericSpec.maximum + ) { + const scaled = getScaledLimit(spec, numericSpec.maximum); + return scaled + ? errorMessages.maxNumberWithScale(scaled.count, scaled.scaleTitle) + : errorMessages.maxNumber(numericSpec.maximum); + } + + if ( + !ignoreMinimumCheck && + isNumber(numericSpec.minimum) && + stringValue.length && + numericSpec.minimum > Number(stringValue) + ) { + const scaled = getScaledLimit(spec, numericSpec.minimum); + return scaled + ? errorMessages.minNumberWithScale(scaled.count, scaled.scaleTitle) + : errorMessages.minNumber(numericSpec.minimum); + } + + if (isString(numericSpec.format) && stringValue.length) { + if (!ignoreIntCheck && numericSpec.format === 'int64' && !isInt(stringValue)) { + return errorMessages.INT; + } + } + + return false; + }; +}; + export interface GetObjectValidatorParams extends CommonValidatorParams {} export const getObjectValidator = (params: GetObjectValidatorParams = {}) => { 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: {