diff --git a/package-lock.json b/package-lock.json index c8ba9bc83..3f892498f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,19 +78,6 @@ "@flatfile/listener": "^1.0.5" } }, - "flatfilers/playground": { - "name": "@private/playground", - "version": "0.0.0", - "license": "ISC", - "dependencies": { - "@flatfile/api": "^1.9.19", - "@flatfile/listener": "^1.1.0", - "modern-async": "^2.0.0" - }, - "devDependencies": { - "flatfile": "3.8.0" - } - }, "flatfilers/sandbox": { "name": "@private/sandbox", "version": "0.0.0", @@ -2915,8 +2902,6 @@ }, "node_modules/@flatfile/api": { "version": "1.9.19", - "resolved": "https://registry.npmjs.org/@flatfile/api/-/api-1.9.19.tgz", - "integrity": "sha512-2vblUl7YtiR14RtF/nPQwyWrhavtm6Zzjj5/gMHIvgoA7Wj6jEHxJQUoPCDh+gh5eDh4QcvrrCWO6tw5WzAW2g==", "dependencies": { "@flatfile/cross-env-config": "0.0.4", "@types/pako": "2.0.1", @@ -3068,8 +3053,7 @@ }, "node_modules/@flatfile/listener": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@flatfile/listener/-/listener-1.1.0.tgz", - "integrity": "sha512-LEzq0ucXmDy/tCM23dg9MTjtZtd8eGSzHLdaKwzeQWrwjN+XnZE/5wrpU+0vXxfDxTpuKFv8TGA/Bvxh/e+4yg==", + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.3", "cross-fetch": "^4.0.0", @@ -3196,6 +3180,10 @@ "resolved": "validate/isbn", "link": true }, + "node_modules/@flatfile/plugin-validate-number": { + "resolved": "validate/number", + "link": true + }, "node_modules/@flatfile/plugin-validate-phone": { "resolved": "validate/phone", "link": true @@ -6142,10 +6130,6 @@ "node": ">=14" } }, - "node_modules/@private/playground": { - "resolved": "flatfilers/playground", - "link": true - }, "node_modules/@private/sandbox": { "resolved": "flatfilers/sandbox", "link": true @@ -7663,11 +7647,10 @@ }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7770,9 +7753,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -8001,8 +7984,7 @@ }, "node_modules/@types/estree": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "license": "MIT" }, "node_modules/@types/flat": { "version": "5.0.5", @@ -8045,9 +8027,8 @@ }, "node_modules/@types/jest": { "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -8176,8 +8157,6 @@ }, "node_modules/@what3words/api": { "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@what3words/api/-/api-5.4.0.tgz", - "integrity": "sha512-e8BIyUtpvL460q2qcNivFNIPOOb16UN/tGZQge3BagE0QRGI2kNxO6dGappRnvRYdEJXzGG3AdbO8sBC2hj+yQ==", "engines": { "node": ">=18" }, @@ -11137,9 +11116,7 @@ } }, "node_modules/flatfile": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/flatfile/-/flatfile-3.8.0.tgz", - "integrity": "sha512-7QrCUlIaywKIbMMKDmxKZC3EG8iF/iY45gqrjGfQkgKL56GjK/Zq7ib/sFKc4N1oDZLIai9Jpuhzd14PKmVpGg==", + "version": "3.6.8", "dev": true, "dependencies": { "@flatfile/cross-env-config": "^0.0.6", @@ -12606,8 +12583,7 @@ }, "node_modules/isbn3": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isbn3/-/isbn3-1.2.0.tgz", - "integrity": "sha512-ackTSv/bE4MxrMIoqwl9FihRFHKdoM+E3hxYeRoYozoaCFuhxjtlS/27kkdUD75bG+CwO6Z1pYxxVYY+66rqAA==", + "license": "MIT", "bin": { "isbn": "bin/isbn", "isbn-audit": "bin/isbn-audit", @@ -12828,9 +12804,8 @@ }, "node_modules/jest": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14580,9 +14555,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.10.tgz", - "integrity": "sha512-yHEtzDlG2VbhuxMIo/sAUY+lyL5YThqD+gHT3YyelaRWRQv1VeyEk94jDvviRT4PYmLJR9cHxvtrx9h2f4OAIQ==" + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.11.tgz", + "integrity": "sha512-mF3KaORjJQR6JBNcOkluDcJKhtoQT4VTLRMrX1v/wlBayL4M8ybwEDeryyPcrSEJmD0rVwHUbBarpZwN5NfPFQ==" }, "node_modules/lighthouse-logger": { "version": "1.4.2", @@ -17798,8 +17773,7 @@ }, "node_modules/rollup": { "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "license": "MIT", "dependencies": { "@types/estree": "1.0.6" }, @@ -17873,6 +17847,18 @@ "rollup": "*" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", + "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-async": { "version": "3.0.0", "dev": true, @@ -19229,9 +19215,8 @@ }, "node_modules/ts-jest": { "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", @@ -20247,7 +20232,7 @@ }, "plugins/automap": { "name": "@flatfile/plugin-automap", - "version": "0.5.1", + "version": "0.5.2", "license": "ISC", "dependencies": { "@flatfile/common-plugin-utils": "^1.0.2", @@ -20812,8 +20797,7 @@ }, "utils/common/node_modules/@flatfile/cross-env-config": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@flatfile/cross-env-config/-/cross-env-config-0.0.6.tgz", - "integrity": "sha512-/8GpBVxjzTlAmFni08ELQfIrfuwZ+yIsAsTcYOzazbIAMp1ShKNB/1RfaIzRGhOF5OTWbj37mAL/z/tu6+rvpQ==" + "license": "ISC" }, "utils/extractor": { "name": "@flatfile/util-extractor", @@ -20919,35 +20903,34 @@ "@flatfile/listener": "^1.1.0" } }, - "validate/phone": { - "name": "@flatfile/plugin-validate-phone", - "version": "0.0.0", - "license": "ISC", + "validate/number": { + "name": "@flatfile/plugin-validate-number", + "version": "1.0.0", + "license": "MIT", "dependencies": { - "@flatfile/plugin-record-hook": "^1.7.0", - "libphonenumber-js": "^1.11.10" + "@flatfile/plugin-record-hook": "^1.7.0" }, "devDependencies": { "@flatfile/rollup-config": "^0.1.1" }, - "engines": { - "node": ">= 16" - }, "peerDependencies": { "@flatfile/listener": "^1.0.5" } }, - "validators/isbn": { - "name": "@flatfile/plugin-isbn-validator", - "version": "1.0.0", - "extraneous": true, - "license": "MIT", + "validate/phone": { + "name": "@flatfile/plugin-validate-phone", + "version": "0.0.0", + "license": "ISC", "dependencies": { - "@flatfile/plugin-record-hook": "^1.7.0" + "@flatfile/plugin-record-hook": "^1.7.0", + "libphonenumber-js": "^1.11.10" }, "devDependencies": { "@flatfile/rollup-config": "^0.1.1" }, + "engines": { + "node": ">= 16" + }, "peerDependencies": { "@flatfile/listener": "^1.0.5" } diff --git a/validate/number/README.MD b/validate/number/README.MD new file mode 100644 index 000000000..4cdb11c7a --- /dev/null +++ b/validate/number/README.MD @@ -0,0 +1,81 @@ + +# @flatfile/plugin-validate-number + +The `@flatfile/plugin-validate-number` plugin provides comprehensive number validation capabilities for your data import processes. It offers a wide range of validation options to ensure that numeric data meets specific criteria before being accepted. + +**Event Type:** +`listener.on('commit:created')` + + + +## Features + +- Min/Max value validation +- Integer-only validation +- Precision and scale validation +- Currency formatting validation +- Step (increment) validation +- Special number type validation (prime, even, odd) +- Rounding and truncation options +- Customizable thousands separator and decimal point +- Inclusive/exclusive range validation + +## Installation + +To install the plugin, use npm: + +```bash +npm install @flatfile/plugin-validate-number +``` + +## Example Usage + +```javascript +import { FlatfileListener } from '@flatfile/listener'; +import { validateNumber } from '@flatfile/plugin-validate-number'; + +const listener = new FlatfileListener(); + +listener.use( + validateNumber({ + min: 0, + max: 1000, + inclusive: true, + integerOnly: true, + precision: 10, + scale: 2, + currency: true, + step: 5, + thousandsSeparator: ',', + decimalPoint: '.', + specialTypes: ['even'], + round: true + }) +); +``` + +## Configuration + +The `validateNumber` accepts a configuration object with the following options: + +- `min`: Minimum allowed value +- `max`: Maximum allowed value +- `inclusive`: Whether the min/max range is inclusive +- `integerOnly`: Allow only integer values +- `precision`: Total number of digits allowed +- `scale`: Number of decimal places allowed +- `currency`: Validate as a currency value +- `step`: Increment value (e.g., multiples of 5) +- `thousandsSeparator`: Character used as thousands separator +- `decimalPoint`: Character used as decimal point +- `specialTypes`: Array of special number types to validate ('prime', 'even', 'odd') +- `round`: Round the number to the nearest integer +- `truncate`: Truncate the decimal part of the number + +## Behavior + +The plugin listens for the 'commit:created' event and performs validation on a specified number field (default field name is 'numberField'). It applies the configured validations and throws an error if any validation fails. + +The plugin also handles number parsing, considering the specified thousands separator and decimal point. It can optionally round or truncate the number before applying validations. + +If all validations pass, the plugin logs a success message. If any validation fails, it logs an error message and throws an error with a descriptive message about the validation failure. diff --git a/validate/number/jest.config.js b/validate/number/jest.config.js new file mode 100644 index 000000000..e6d7ca40b --- /dev/null +++ b/validate/number/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'node', + + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + setupFiles: ['../../test/dotenv-config.js'], + setupFilesAfterEnv: [ + '../../test/betterConsoleLog.js', + '../../test/unit.cleanup.js', + ], + testTimeout: 60_000, + globalSetup: '../../test/setup-global.js', + forceExit: true, + passWithNoTests: true, +} diff --git a/validate/number/package.json b/validate/number/package.json new file mode 100644 index 000000000..7e820e53e --- /dev/null +++ b/validate/number/package.json @@ -0,0 +1,66 @@ +{ + "name": "@flatfile/plugin-validate-number", + "version": "0.0.0", + "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/validate/number", + "description": "A Flatfile Listener plugin for number validation", + "engines": { + "node": ">= 16" + }, + "browser": { + "./dist/index.cjs": "./dist/index.browser.cjs", + "./dist/index.mjs": "./dist/index.browser.mjs" + }, + "exports": { + "types": "./dist/index.d.ts", + "node": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "browser": { + "require": "./dist/index.browser.cjs", + "import": "./dist/index.browser.mjs" + }, + "default": "./dist/index.mjs" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "rollup -c", + "build:watch": "rollup -c --watch", + "build:prod": "NODE_ENV=production rollup -c", + "check": "tsc ./**/*.ts --noEmit --esModuleInterop", + "test": "jest src/*.spec.ts --detectOpenHandles", + "test:unit": "jest src/*.spec.ts --testPathIgnorePatterns=.*\\.e2e\\.spec\\.ts$ --detectOpenHandles", + "test:e2e": "jest src/*.e2e.spec.ts --detectOpenHandles" + }, + "keywords": [ + "flatfile-plugins", + "category-transform" + ], + "author": "Flatfile, Inc", + "license": "ISC", + "dependencies": { + "@flatfile/plugin-record-hook": "^1.7.0" + }, + "peerDependencies": { + "@flatfile/listener": "^1.0.5" + }, + "devDependencies": { + "@flatfile/rollup-config": "^0.1.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/FlatFilers/flatfile-plugins.git", + "directory": "validate/number" + }, + "browserslist": [ + "> 0.5%", + "last 2 versions", + "not dead" + ] +} diff --git a/validate/number/rollup.config.mjs b/validate/number/rollup.config.mjs new file mode 100644 index 000000000..fafa813c6 --- /dev/null +++ b/validate/number/rollup.config.mjs @@ -0,0 +1,5 @@ +import { buildConfig } from '@flatfile/rollup-config' + +const config = buildConfig({}) + +export default config diff --git a/validate/number/src/index.ts b/validate/number/src/index.ts new file mode 100644 index 000000000..4c5f44f21 --- /dev/null +++ b/validate/number/src/index.ts @@ -0,0 +1 @@ +export * from './validate.number.plugin' diff --git a/validate/number/src/validate.number.plugin.spec.ts b/validate/number/src/validate.number.plugin.spec.ts new file mode 100644 index 000000000..50613f5ca --- /dev/null +++ b/validate/number/src/validate.number.plugin.spec.ts @@ -0,0 +1,155 @@ +import { + validateNumberField, + NumberValidationConfig, +} from './validate.number.plugin' + +describe('validateNumberField', () => { + const defaultConfig: NumberValidationConfig = {} + + it('should validate a basic number', () => { + const result = validateNumberField('123', defaultConfig) + expect(result).toEqual({ + value: 123, + errors: [], + warnings: [], + }) + }) + + it('should handle non-numeric input', () => { + const result = validateNumberField('abc', defaultConfig) + expect(result).toEqual({ + value: null, + errors: ['Must be a number'], + warnings: [], + }) + }) + + it('should validate min and max constraints', () => { + const config: NumberValidationConfig = { min: 0, max: 100 } + expect(validateNumberField('50', config)).toEqual({ + value: 50, + errors: [], + warnings: [], + }) + expect(validateNumberField('-1', config)).toEqual({ + value: -1, + errors: [], + warnings: ['Must be greater than 0'], + }) + expect(validateNumberField('101', config)).toEqual({ + value: 101, + errors: [], + warnings: ['Must be less than 100'], + }) + }) + + it('should validate integer constraint', () => { + const config: NumberValidationConfig = { integerOnly: true } + expect(validateNumberField('10', config)).toEqual({ + value: 10, + errors: [], + warnings: [], + }) + expect(validateNumberField('10.5', config)).toEqual({ + value: 10.5, + errors: [], + warnings: ['Must be an integer'], + }) + }) + + it('should validate precision and scale', () => { + const config: NumberValidationConfig = { precision: 5, scale: 2 } + expect(validateNumberField('123.45', config)).toEqual({ + value: 123.45, + errors: [], + warnings: [], + }) + expect(validateNumberField('1234.567', config)).toEqual({ + value: 1234.567, + errors: [], + warnings: [ + 'Must have at most 3 digits before the decimal point', + 'Must have at most 2 digits after the decimal point', + ], + }) + }) + + it('should validate currency format', () => { + const config: NumberValidationConfig = { currency: true } + expect(validateNumberField('100.00', config)).toEqual({ + value: 100, + errors: [], + warnings: [], + }) + expect(validateNumberField('100.123', config)).toEqual({ + value: 100.123, + errors: [], + warnings: [ + 'Must be a valid currency value with at most two decimal places', + ], + }) + }) + + it('should validate step constraint', () => { + const config: NumberValidationConfig = { step: 0.5 } + expect(validateNumberField('2.5', config)).toEqual({ + value: 2.5, + errors: [], + warnings: [], + }) + expect(validateNumberField('2.7', config)).toEqual({ + value: 2.7, + errors: [], + warnings: ['Must be a multiple of 0.5'], + }) + }) + + it('should handle thousands separator and decimal point', () => { + const config: NumberValidationConfig = { + thousandsSeparator: ',', + decimalPoint: '.', + } + expect(validateNumberField('1,234.56', config)).toEqual({ + value: 1234.56, + errors: [], + warnings: [], + }) + }) + + it('should validate special types', () => { + const config: NumberValidationConfig = { specialTypes: ['prime', 'odd'] } + expect(validateNumberField('7', config)).toEqual({ + value: 7, + errors: [], + warnings: [], + }) + expect(validateNumberField('9', config)).toEqual({ + value: 9, + errors: [], + warnings: ['Must be a prime number'], + }) + expect(validateNumberField('2', config)).toEqual({ + value: 2, + errors: [], + warnings: ['Must be an odd number'], + }) + }) + + it('should round numbers when configured', () => { + const config: NumberValidationConfig = { round: true } + expect(validateNumberField('3.7', config)).toEqual({ + value: 4, + errors: [], + warnings: [], + }) + }) + + it('should truncate numbers when configured', () => { + const config: NumberValidationConfig = { truncate: true } + expect(validateNumberField('3.7', config)).toEqual({ + value: 3, + errors: [], + warnings: [], + }) + }) +}) diff --git a/validate/number/src/validate.number.plugin.ts b/validate/number/src/validate.number.plugin.ts new file mode 100644 index 000000000..2ee8bf7ad --- /dev/null +++ b/validate/number/src/validate.number.plugin.ts @@ -0,0 +1,155 @@ +import { FlatfileListener } from '@flatfile/listener' +import { FlatfileRecord, recordHook } from '@flatfile/plugin-record-hook' + +export interface NumberValidationConfig { + min?: number + max?: number + inclusive?: boolean + integerOnly?: boolean + precision?: number + scale?: number + currency?: boolean + step?: number + thousandsSeparator?: string + decimalPoint?: string + specialTypes?: string[] + round?: boolean + truncate?: boolean +} + +export interface NumberValidationResult { + value: number | null + errors: string[] + warnings: string[] +} + +export function validateNumberField( + value: string | number, + config: NumberValidationConfig +): NumberValidationResult { + const result: NumberValidationResult = { + value: null, + errors: [], + warnings: [], + } + + try { + let numberValue: string | number = String(value) + .replace(config.thousandsSeparator || ',', '') + .replace(config.decimalPoint || '.', '.') + numberValue = parseFloat(numberValue) + + if (isNaN(numberValue)) { + result.errors.push('Must be a number') + return result + } + + if (config.round) { + numberValue = Math.round(numberValue) + } + + if (config.truncate) { + numberValue = Math.trunc(numberValue) + } + + if (config.integerOnly && !Number.isInteger(numberValue)) { + result.warnings.push('Must be an integer') + } + + if (config.min !== undefined) { + if ( + config.inclusive ? numberValue < config.min : numberValue <= config.min + ) { + result.warnings.push( + `Must be greater than ${config.inclusive ? 'or equal to ' : ''}${config.min}` + ) + } + } + + if (config.max !== undefined) { + if ( + config.inclusive ? numberValue > config.max : numberValue >= config.max + ) { + result.warnings.push( + `Must be less than ${config.inclusive ? 'or equal to ' : ''}${config.max}` + ) + } + } + + if (config.precision !== undefined && config.scale !== undefined) { + const [integerPart, decimalPart] = numberValue.toString().split('.') + if (integerPart.length > config.precision - config.scale) { + result.warnings.push( + `Must have at most ${config.precision - config.scale} digits before the decimal point` + ) + } + if (decimalPart && decimalPart.length > config.scale) { + result.warnings.push( + `Must have at most ${config.scale} digits after the decimal point` + ) + } + } + + if (config.currency && !/^\d+(\.\d{1,2})?$/.test(numberValue.toString())) { + result.warnings.push( + 'Must be a valid currency value with at most two decimal places' + ) + } + + if (config.step !== undefined && numberValue % config.step !== 0) { + result.warnings.push(`Must be a multiple of ${config.step}`) + } + + if (config.specialTypes) { + if (config.specialTypes.includes('prime') && !isPrime(numberValue)) { + result.warnings.push('Must be a prime number') + } + if (config.specialTypes.includes('even') && numberValue % 2 !== 0) { + result.warnings.push('Must be an even number') + } + if (config.specialTypes.includes('odd') && numberValue % 2 === 0) { + result.warnings.push('Must be an odd number') + } + } + + result.value = numberValue + } catch (error) { + result.errors.push(`Error processing value: ${error.message}`) + } + + return result +} + +export function validateNumber( + config: NumberValidationConfig & { fields: string[]; sheetSlug?: string } +) { + return (listener: FlatfileListener) => { + listener.use( + recordHook(config.sheetSlug || '**', async (record: FlatfileRecord) => { + for (const field of config.fields) { + const value = record.get(field) + if (typeof value !== 'string' && typeof value !== 'number') { + return record + } + const result = validateNumberField(value, config) + if (result.value !== null) { + record.set(field, result.value) + } + + result.errors.forEach((error) => record.addError(field, error)) + result.warnings.forEach((warning) => + record.addWarning(field, warning) + ) + } + return record + }) + ) + } +} + +export function isPrime(num: number): boolean { + for (let i = 2, sqrt = Math.sqrt(num); i <= sqrt; i++) { + if (num % i === 0) return false + } + return num > 1 +}