From 5e4502d663291c9af7755f6eea280509e6e33cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B2?= <19825793+iuccio@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:41:56 +0000 Subject: [PATCH] fix: preserve numeric precision and leading zeros - Fix issue with large numbers losing precision - Preserve leading zeros in numeric strings - Refactor StringUtils for better maintainability - Add comprehensive test coverage - Update documentation with clear examples No breaking changes - maintains backward compatibility while fixing precision issues. close #74 --- README.md | 72 ++++++--- src/util/stringUtils.js | 111 ++++++++++--- test/stringUtils.spec.js | 330 +++++++++++++++++++++++++-------------- 3 files changed, 349 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 5383fdf..becf644 100644 --- a/README.md +++ b/README.md @@ -221,13 +221,49 @@ If the header is not on the first line you can define the header index like: Empty rows are ignored and not parsed. #### Format property value by type -If you want that a number will be printed as a Number type, and values *true* or *false* is printed as a boolean Type, use: +The `formatValueByType()` function intelligently converts string values to their appropriate types while preserving data integrity. To enable automatic type conversion: + ```js - csvToJson.formatValueByType() - .getJsonFromCsv(fileInputName); +csvToJson.formatValueByType() + .getJsonFromCsv(fileInputName); +``` + +This conversion follows these rules: + +##### Numbers +- Regular integers and decimals are converted to Number type +- Numbers with leading zeros are preserved as strings (e.g., "0012" stays "0012") +- Large integers outside JavaScript's safe range are preserved as strings +- Valid decimal numbers are converted to Number type + +For example: +```json +{ + "normalInteger": 42, // Converted to number + "decimal": 3.14, // Converted to number + "leadingZeros": "0012345", // Kept as string to preserve leading zeros + "largeNumber": "9007199254740992" // Kept as string to preserve precision +} +``` + +##### Boolean +Case-insensitive "true" or "false" strings are converted to boolean values: +```json +{ + "registered": true, // From "true" or "TRUE" or "True" + "active": false // From "false" or "FALSE" or "False" +} +``` + +##### Complete Example +Input CSV: +```csv +first_name;last_name;email;gender;age;id;zip;registered +Constantin;Langsdon;clangsdon0@hc360.com;Male;96;00123;123;true +Norah;Raison;nraison1@wired.com;Female;32;987;00456;FALSE ``` -For example: +Output JSON: ```json [ { @@ -236,8 +272,9 @@ For example: "email": "clangsdon0@hc360.com", "gender": "Male", "age": 96, - "zip": 123, - "registered": true + "id": "00123", // Preserved leading zeros + "zip": 123, // Converted to number + "registered": true // Converted to boolean }, { "first_name": "Norah", @@ -245,29 +282,12 @@ For example: "email": "nraison1@wired.com", "gender": "Female", "age": 32, - "zip": "", - "registered": false + "id": "987", + "zip": "00456", // Preserved leading zeros + "registered": false // Case-insensitive boolean conversion } ] ``` -##### Number -The property **age** is printed as -```json - "age": 32 -``` -instead of -```json - "age": "32" - ``` -##### Boolean -The property **registered** is printed as -```json - "registered": true -``` -instead of -```json - "registered": "true" - ``` #### Encoding You can read and decode files with the following encoding: diff --git a/src/util/stringUtils.js b/src/util/stringUtils.js index c40ae23..625f146 100644 --- a/src/util/stringUtils.js +++ b/src/util/stringUtils.js @@ -1,40 +1,109 @@ 'use strict'; class StringUtils { + // Regular expressions as constants for better maintainability + static PATTERNS = { + INTEGER: /^-?\d+$/, + FLOAT: /^-?\d*\.\d+$/, + WHITESPACE: /\s/g + }; - trimPropertyName(isTrimHeaderFieldWhiteSpace,value) { - if(isTrimHeaderFieldWhiteSpace) { - return value.replace(/\s/g, ''); + static BOOLEAN_VALUES = { + TRUE: 'true', + FALSE: 'false' + }; + + /** + * Removes whitespace from property names based on configuration + * @param {boolean} shouldTrimAll - If true, removes all whitespace, otherwise only trims edges + * @param {string} propertyName - The property name to process + * @returns {string} The processed property name + */ + trimPropertyName(shouldTrimAll, propertyName) { + if (!propertyName) { + return ''; } - return value.trim(); - + return shouldTrimAll ? + propertyName.replace(StringUtils.PATTERNS.WHITESPACE, '') : + propertyName.trim(); } + /** + * Converts a string value to its appropriate type while preserving data integrity + * @param {string} value - The input value to convert + * @returns {string|number|boolean} The converted value + */ getValueFormatByType(value) { - if(value === undefined || value === ''){ + if (this.isEmpty(value)) { return String(); } - //is Number - let isNumber = !isNaN(value); - if (isNumber) { - return Number(value); + + if (this.isBoolean(value)) { + return this.convertToBoolean(value); + } + + if (this.isInteger(value)) { + return this.convertInteger(value); } - // is Boolean - if(value === "true" || value === "false"){ - return JSON.parse(value.toLowerCase()); + + if (this.isFloat(value)) { + return this.convertFloat(value); } + return String(value); } - hasContent(values) { - if (values.length > 0) { - for (let i = 0; i < values.length; i++) { - if (values[i]) { - return true; - } - } + /** + * Checks if a value array contains any non-empty values + * @param {Array} values - Array to check for content + * @returns {boolean} True if array has any non-empty values + */ + hasContent(values = []) { + return Array.isArray(values) && + values.some(value => Boolean(value)); + } + + // Private helper methods for type checking and conversion + isEmpty(value) { + return value === undefined || value === ''; + } + + isBoolean(value) { + const normalizedValue = value.toLowerCase(); + return normalizedValue === StringUtils.BOOLEAN_VALUES.TRUE || + normalizedValue === StringUtils.BOOLEAN_VALUES.FALSE; + } + + isInteger(value) { + return StringUtils.PATTERNS.INTEGER.test(value); + } + + isFloat(value) { + return StringUtils.PATTERNS.FLOAT.test(value); + } + + hasLeadingZero(value) { + const isPositiveWithLeadingZero = value.length > 1 && value[0] === '0'; + const isNegativeWithLeadingZero = value.length > 2 && value[0] === '-' && value[1] === '0'; + return isPositiveWithLeadingZero || isNegativeWithLeadingZero; + } + + convertToBoolean(value) { + return JSON.parse(value.toLowerCase()); + } + + convertInteger(value) { + if (this.hasLeadingZero(value)) { + return String(value); } - return false; + + const num = Number(value); + return Number.isSafeInteger(num) ? num : String(value); + } + + convertFloat(value) { + const num = Number(value); + return Number.isFinite(num) ? num : String(value); } } diff --git a/test/stringUtils.spec.js b/test/stringUtils.spec.js index 025476f..26feed0 100644 --- a/test/stringUtils.spec.js +++ b/test/stringUtils.spec.js @@ -1,131 +1,227 @@ 'use strict'; -let stringUtils = require('../src/util/stringUtils'); - -describe('StringUtils class testing', function () { - - describe('trimPropertyName()', function () { - - it('Should trim input value with empty spaces', function () { - //given - let value = ' value '; - - //when - let result = stringUtils.trimPropertyName(true,value); - - //then - expect(result).toEqual('value'); - }); - - it('Should trim input value without empty spaces', function () { - //given - let value = ' val ue '; - - //when - let result = stringUtils.trimPropertyName(false,value); - - //then - expect(result).toEqual('val ue'); +const stringUtils = require('../src/util/stringUtils'); + +describe('StringUtils', () => { + describe('trimPropertyName', () => { + const testCases = [ + { + name: 'should remove all whitespace when shouldTrimAll is true', + input: ' value with spaces ', + shouldTrimAll: true, + expected: 'valuewithspaces' + }, + { + name: 'should only trim edges when shouldTrimAll is false', + input: ' val ue ', + shouldTrimAll: false, + expected: 'val ue' + }, + { + name: 'should handle empty input', + input: '', + shouldTrimAll: true, + expected: '' + }, + { + name: 'should handle undefined input', + input: undefined, + shouldTrimAll: true, + expected: '' + }, + { + name: 'should handle input with only spaces', + input: ' ', + shouldTrimAll: false, + expected: '' + } + ]; + + testCases.forEach(({ name, input, shouldTrimAll, expected }) => { + it(name, () => { + expect(stringUtils.trimPropertyName(shouldTrimAll, input)).toBe(expected); + }); }); }); - describe('getValueFormatByType()', function () { - it('should return type of Number for integers', function () { - //given - let value = '23'; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('number'); - expect(result).toEqual(23); - }); - - it('should return type of Number for non-integers', function () { - //given - let value = '0.23'; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('number'); - expect(result).toEqual(0.23); - }); - - it('should return type of String when value contains only words', function () { - //given - let value = 'value'; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('string'); - expect(result).toEqual('value'); - }); - - it('should return type of String when value contains words and digits', function () { - //given - let value = '11value'; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('string'); - expect(result).toEqual('11value'); - }); - - it('should return empty value when input value is not defined', function () { - //given - let value; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('string'); - expect(result).toEqual(''); + describe('getValueFormatByType', () => { + describe('Number handling', () => { + const numberTestCases = [ + { + name: 'should convert simple integer to number', + input: '42', + expectedType: 'number', + expectedValue: 42 + }, + { + name: 'should convert negative integer to number', + input: '-42', + expectedType: 'number', + expectedValue: -42 + }, + { + name: 'should convert decimal to number', + input: '3.14', + expectedType: 'number', + expectedValue: 3.14 + }, + { + name: 'should convert negative decimal to number', + input: '-3.14', + expectedType: 'number', + expectedValue: -3.14 + }, + { + name: 'should preserve leading zeros as string', + input: '00340434621911190873', + expectedType: 'string', + expectedValue: '00340434621911190873' + }, + { + name: 'should preserve negative numbers with leading zeros as string', + input: '-0012345', + expectedType: 'string', + expectedValue: '-0012345' + }, + { + name: 'should preserve integers above MAX_SAFE_INTEGER as string', + input: '9007199254740992', // MAX_SAFE_INTEGER + 1 + expectedType: 'string', + expectedValue: '9007199254740992' + }, + { + name: 'should preserve integers below MIN_SAFE_INTEGER as string', + input: '-9007199254740992', // MIN_SAFE_INTEGER - 1 + expectedType: 'string', + expectedValue: '-9007199254740992' + }, + { + name: 'should handle MAX_SAFE_INTEGER as number', + input: '9007199254740991', + expectedType: 'number', + expectedValue: Number.MAX_SAFE_INTEGER + } + ]; + + numberTestCases.forEach(({ name, input, expectedType, expectedValue }) => { + it(name, () => { + const result = stringUtils.getValueFormatByType(input); + expect(typeof result).toBe(expectedType); + expect(result).toBe(expectedValue); + }); + }); }); - it('should return empty value when input value is empty string', function () { - //given - let value = ''; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('string'); - expect(result).toEqual(''); + describe('Boolean handling', () => { + const booleanTestCases = [ + { + name: 'should convert lowercase "true" to boolean true', + input: 'true', + expected: true + }, + { + name: 'should convert lowercase "false" to boolean false', + input: 'false', + expected: false + }, + { + name: 'should handle uppercase "TRUE"', + input: 'TRUE', + expected: true + }, + { + name: 'should handle uppercase "FALSE"', + input: 'FALSE', + expected: false + }, + { + name: 'should handle mixed case "TrUe"', + input: 'TrUe', + expected: true + } + ]; + + booleanTestCases.forEach(({ name, input, expected }) => { + it(name, () => { + const result = stringUtils.getValueFormatByType(input); + expect(typeof result).toBe('boolean'); + expect(result).toBe(expected); + }); + }); }); - it('should return Boolean value when input value is "true"', function () { - //given - let value = "true"; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('boolean'); - expect(result).toEqual(true); + describe('String handling', () => { + const stringTestCases = [ + { + name: 'should keep pure text as string', + input: 'hello world', + expected: 'hello world' + }, + { + name: 'should keep alphanumeric text as string', + input: 'abc123', + expected: 'abc123' + }, + { + name: 'should handle undefined input', + input: undefined, + expected: '' + }, + { + name: 'should handle empty string', + input: '', + expected: '' + }, + { + name: 'should handle string with special characters', + input: '123-abc!@#', + expected: '123-abc!@#' + } + ]; + + stringTestCases.forEach(({ name, input, expected }) => { + it(name, () => { + const result = stringUtils.getValueFormatByType(input); + expect(typeof result).toBe('string'); + expect(result).toBe(expected); + }); + }); }); + }); - it('should return Boolean value when input value is "false"', function () { - //given - let value = "false"; - - //when - let result = stringUtils.getValueFormatByType(value); - - //then - expect(typeof result).toEqual('boolean'); - expect(result).toEqual(false); + describe('hasContent', () => { + const contentTestCases = [ + { + name: 'should return true for array with some non-empty values', + input: ['value', '', null, undefined], + expected: true + }, + { + name: 'should return false for empty array', + input: [], + expected: false + }, + { + name: 'should return false for array with only empty values', + input: ['', null, undefined], + expected: false + }, + { + name: 'should handle undefined input', + input: undefined, + expected: false + }, + { + name: 'should handle null input', + input: null, + expected: false + } + ]; + + contentTestCases.forEach(({ name, input, expected }) => { + it(name, () => { + expect(stringUtils.hasContent(input)).toBe(expected); + }); }); - }); - });