Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: advanced parser for numeric and boolean import values #394

Merged
merged 1 commit into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/server/src/services/Import/ImportFileDataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Service } from 'typedi';
import * as R from 'ramda';
import { isUndefined, get, chain } from 'lodash';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { parseBoolean } from '@/utils';
import { trimObject } from './_utils';
import { trimObject, parseBoolean } from './_utils';
import { multiNumberParse } from '@/utils/multi-number-parse';

@Service()
export class ImportFileDataTransformer {
/**
*
* Parses the given sheet data before passing to the service layer.
* based on the mapped fields and the each field type .
* @param {number} tenantId -
* @param {}
*/
Expand Down Expand Up @@ -76,7 +77,7 @@ export class ImportFileDataTransformer {

// Parses the boolean value.
if (fields[key].fieldType === 'boolean') {
_value = parseBoolean(value, false);
_value = parseBoolean(value);

// Parses the enumeration value.
} else if (fields[key].fieldType === 'enumeration') {
Expand All @@ -87,7 +88,7 @@ export class ImportFileDataTransformer {
_value = get(option, 'key');
// Prases the numeric value.
} else if (fields[key].fieldType === 'number') {
_value = parseFloat(value);
_value = multiNumberParse(value);
}
return _value;
};
Expand Down
27 changes: 27 additions & 0 deletions packages/server/src/services/Import/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ERRORS = {
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
IMPORTED_SHEET_EMPTY: 'IMPORTED_SHEET_EMPTY',
};

export function trimObject(obj) {
Expand Down Expand Up @@ -122,3 +123,29 @@ export const getUniqueImportableValue = (

return defaultTo(objectDTO[uniqueImportableKey], '');
};

const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];

/**
* Parses the given string value to boolean.
* @param {string} value
* @returns {string|null}
*/
export const parseBoolean = (value: string): boolean | null => {
const normalizeValue = (value: string): string =>
value.toString().trim().toLowerCase();

const normalizedValue = normalizeValue(value);
const valuesRepresentingTrue =
booleanValuesRepresentingTrue.map(normalizeValue);
const valueRepresentingFalse =
booleanValuesRepresentingFalse.map(normalizeValue);

if (valuesRepresentingTrue.includes(normalizedValue)) {
return true;
} else if (valueRepresentingFalse.includes(normalizedValue)) {
return false;
}
return null;
};
51 changes: 51 additions & 0 deletions packages/server/src/utils/multi-number-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { assert } from 'chai';
import { multiNumberParse } from './multi-number-parse';

const correctNumbers = [
{ actual: '10.5', expected: 10.5 },
{ actual: '10,5', expected: 10.5 },
{ actual: '1.235,76', expected: 1235.76 },
{ actual: '2,543.56', expected: 2543.56 },
{ actual: '10 654.1234', expected: 10654.1234 },
{ actual: '2.654$10', expected: 2654.1 },
{ actual: '5.435.123,645', expected: 5435123.645 },
{ actual: '2,566,765.234', expected: 2566765.234 },
{ actual: '2,432,123$23', expected: 2432123.23 },
{ actual: '2,45EUR', expected: 2.45 },
{ actual: '4.78€', expected: 4.78 },
{ actual: '28', expected: 28 },
{ actual: '-48', expected: -48 },
{ actual: '39USD', expected: 39 },

// Some negative numbers
{ actual: '-2,543.56', expected: -2543.56 },
{ actual: '-10 654.1234', expected: -10654.1234 },
{ actual: '-2.654$10', expected: -2654.1 },
];

const incorrectNumbers = [
'10 345,234.21', // too many different separators
'1.123.234,534,234', // impossible to detect where's the decimal separator
'10.4,2', // malformed digit groups
'1.123.2', // also malformed digit groups
];

describe('Test numbers', () => {
correctNumbers.forEach((item) => {
it(`"${item.actual}" should return ${item.expected}`, (done) => {
const parsed = multiNumberParse(item.actual);
assert.isNotNaN(parsed);
assert.equal(parsed, item.expected);

done();
});
});

incorrectNumbers.forEach((item) => {
it(`"${item}" should return NaN`, (done) => {
assert.isNaN(numberParse(item));

done();
});
});
});
130 changes: 130 additions & 0 deletions packages/server/src/utils/multi-number-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const validGrouping = (integerPart, sep) =>
integerPart.split(sep).reduce((acc, group, idx) => {
if (idx > 0) {
return acc && group.length === 3;
}

return acc && group.length;
}, true);

export const multiNumberParse = (number: number | string, standardDecSep = '.') => {
// if it's a number already, this is going to be easy...
if (typeof number === 'number') {
return number;
}

// check validity of parameters
if (!number || typeof number !== 'string') {
throw new TypeError('number must be a string');
}

if (typeof standardDecSep !== 'string' || standardDecSep.length !== 1) {
throw new TypeError('standardDecSep must be a single character string');
}

// check if negative
const negative = number[0] === '-';

// strip unnecessary chars
const stripped = number
// get rid of trailing non-numbers
.replace(/[^\d]+$/, '')
// get rid of the signal
.slice(negative ? 1 : 0);

// analyze separators
const separators = (stripped.match(/[^\d]/g) || []).reduce(
(acc, sep, idx) => {
const sepChr = `str_${sep.codePointAt(0)}`;
const cnt = ((acc[sepChr] || {}).cnt || 0) + 1;

return {
...acc,
[sepChr]: {
sep,
cnt,
lastIdx: idx,
},
};
},
{}
);

// check correctness of separators
const sepKeys = Object.keys(separators);

if (!sepKeys.length) {
// no separator, that's easy-peasy
return parseInt(stripped, 10) * (negative ? -1 : 1);
}

if (sepKeys.length > 2) {
// there's more than 2 separators, that's wrong
return Number.NaN;
}

if (sepKeys.length > 1) {
// there's two separators, that's ok by now
let sep1 = separators[sepKeys[0]];
let sep2 = separators[sepKeys[1]];

if (sep1.lastIdx > sep2.lastIdx) {
// swap
[sep1, sep2] = [sep2, sep1];
}

// if more than one separator appears more than once, that's wrong
if (sep1.cnt > 1 && sep2.cnt > 1) {
return Number.NaN;
}

// check if the last separator is the single one
if (sep2.cnt > 1) {
return Number.NaN;
}

// check the groupings
const [integerPart] = stripped.split(sep2.sep);

if (!validGrouping(integerPart, sep1.sep)) {
return Number.NaN;
}

// ok, we got here! let's handle it
return (
parseFloat(stripped.split(sep1.sep).join('').replace(sep2.sep, '.')) *
(negative ? -1 : 1)
);
}

// ok, only one separator, which is nice
const sep = separators[sepKeys[0]];

if (sep.cnt > 1) {
// there's more than one separator, which means it's integer
// let's check the groupings
if (!validGrouping(stripped, sep.sep)) {
return Number.NaN;
}

// it's valid, let's return an integer
return parseInt(stripped.split(sep.sep).join(''), 10) * (negative ? -1 : 1);
}

// just one separator, let's check last group
const groups = stripped.split(sep.sep);

if (groups[groups.length - 1].length === 3) {
// ok, we're in ambiguous territory here

if (sep.sep !== standardDecSep) {
// it's an integer
return (
parseInt(stripped.split(sep.sep).join(''), 10) * (negative ? -1 : 1)
);
}
}

// well, it looks like it's a simple float
return parseFloat(stripped.replace(sep.sep, '.')) * (negative ? -1 : 1);
};
Loading