From 84e8fd3f98981386bbf12e2d855724214f049d59 Mon Sep 17 00:00:00 2001 From: Patrick Kalita Date: Fri, 10 Feb 2023 16:15:01 -0800 Subject: [PATCH] Add utility methods for parsing and formatting multivalued cell values --- lib/DataHarmonizer.js | 19 ++++++------ lib/utils/fields.js | 58 +++++++++++++++++++++++++++--------- lib/utils/validation.js | 6 ++-- tests/fields.test.js | 65 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 121 insertions(+), 27 deletions(-) diff --git a/lib/DataHarmonizer.js b/lib/DataHarmonizer.js index a9526514..11c74624 100644 --- a/lib/DataHarmonizer.js +++ b/lib/DataHarmonizer.js @@ -10,6 +10,8 @@ import { dataArrayToObject, dataObjectToArray, fieldUnitBinTest, + formatMultivaluedValue, + parseMultivaluedValue, } from './utils/fields'; import { parseDatatype } from './utils/datatypes'; import { @@ -1066,14 +1068,11 @@ class DataHarmonizer { afterBeginEditing: function (row, col) { if (fields[col].flatVocabulary && fields[col].multivalued === true) { const value = this.getDataAtCell(row, col); - let selections = (value && value.split(';')) || []; - selections = selections.map((x) => x.trim()); - const selections2 = selections.filter(function (el) { - return el != ''; - }); + const selections = parseMultivaluedValue(value); + const formattedValue = formatMultivaluedValue(selections); // Cleanup of empty values that can occur with leading/trailing or double ";" - if (selections.length != selections2.length) { - this.setDataAtCell(row, col, selections2.join('; '), 'thisChange'); + if (value !== formattedValue) { + this.setDataAtCell(row, col, formattedValue, 'thisChange'); } const self = this; let content = ''; @@ -1106,9 +1105,9 @@ class DataHarmonizer { }, }) // must be rendered when html is visible .on('change', function () { - let newValCsv = $('#field-description-text .multiselect') - .val() - .join('; '); + let newValCsv = formatMultivaluedValue( + $('#field-description-text .multiselect').val() + ); self.setDataAtCell(row, col, newValCsv, 'thisChange'); }); // Saves users a click: diff --git a/lib/utils/fields.js b/lib/utils/fields.js index 04308ad6..b7b981a5 100644 --- a/lib/utils/fields.js +++ b/lib/utils/fields.js @@ -1,5 +1,7 @@ import { parseDatatype, stringifyDatatype } from './datatypes'; +const MULTIVALUED_DELIMITER = '; '; + /** * Modify a string to match specified case. * @param {String} val String to modify. @@ -39,11 +41,10 @@ export function fieldUnitBinTest(fields, col) { } const DATA_ARRAY_TO_OBJECT_OPTION_DEFAULTS = { - multivalueDelimiter: '; ', strict: true, }; export function dataArrayToObject(dataArray, fields, options = {}) { - const { multivalueDelimiter, strict } = { + const { strict } = { ...DATA_ARRAY_TO_OBJECT_OPTION_DEFAULTS, ...options, }; @@ -55,7 +56,7 @@ export function dataArrayToObject(dataArray, fields, options = {}) { const field = fields[idx]; let parsed; if (field.multivalued) { - const split = cell.split(multivalueDelimiter); + const split = parseMultivaluedValue(cell); parsed = split .map((value) => (strict ? parseDatatype(value, field.datatype) : value)) .filter((parsed) => parsed !== undefined); @@ -72,14 +73,7 @@ export function dataArrayToObject(dataArray, fields, options = {}) { return dataObject; } -const DATA_OBJECT_TO_ARRAY_DEFAULT_OPTIONS = { - multivalueDelimiter: '; ', -}; -export function dataObjectToArray(dataObject, fields, options = {}) { - const { multivalueDelimiter } = { - ...DATA_OBJECT_TO_ARRAY_DEFAULT_OPTIONS, - ...options, - }; +export function dataObjectToArray(dataObject, fields) { const dataArray = Array(fields.length).fill(''); for (const [key, value] of Object.entries(dataObject)) { const fieldIdx = fields.findIndex((f) => f.name === key); @@ -89,12 +83,48 @@ export function dataObjectToArray(dataObject, fields, options = {}) { } const field = fields[fieldIdx]; if (field.multivalued && Array.isArray(value)) { - dataArray[fieldIdx] = value - .map((v) => stringifyDatatype(v, field.datatype)) - .join(multivalueDelimiter); + dataArray[fieldIdx] = formatMultivaluedValue( + value.map((v) => stringifyDatatype(v, field.datatype)) + ); } else { dataArray[fieldIdx] = stringifyDatatype(value, field.datatype); } } return dataArray; } + +/** + * Parse a formatted string representing a multivalued value and return an + * array of the individual values. + * + * @param {String} value String-formatted multivalued value. + * @return {Array} Array of individual string values. + */ +export function parseMultivaluedValue(value) { + if (!value) { + return []; + } + // trim the delimiter and the resulting tokens to be flexible about what + // this function accepts + return value + .split(MULTIVALUED_DELIMITER.trim()) + .map((v) => v.trim()) + .filter((v) => !!v); +} + +/** + * Format a string array of multivalued values into a single string representation. + * + * @param {Array} values Array of individual values. + * @return {String} String-formatted multivalued value. + */ +export function formatMultivaluedValue(values) { + if (!values) { + return ''; + } + + return values + .filter((v) => !!v) + .map((v) => (typeof v === 'string' ? v.trim() : String(v))) + .join(MULTIVALUED_DELIMITER); +} diff --git a/lib/utils/validation.js b/lib/utils/validation.js index 2c2b4ef0..3de77d97 100644 --- a/lib/utils/validation.js +++ b/lib/utils/validation.js @@ -1,3 +1,5 @@ +import { formatMultivaluedValue, parseMultivaluedValue } from './fields'; + /** * Test cellVal against "DataHarmonizer provenance: vX.Y.Z" pattern and if it * needs an update, do so. @@ -99,7 +101,7 @@ export function validateValAgainstVocab(value, field) { */ export function validateValsAgainstVocab(delimited_string, field) { let update_flag = false; - let value_array = delimited_string.split(';'); + let value_array = parseMultivaluedValue(delimited_string); // for-loop construct ensures return is out of this function. for (let index = 0; index < value_array.length; index++) { let value = value_array[index]; @@ -110,7 +112,7 @@ export function validateValsAgainstVocab(delimited_string, field) { value_array[index] = update; } } - if (update_flag) return [true, value_array.join(';')]; + if (update_flag) return [true, formatMultivaluedValue(value_array)]; else return [true, false]; } diff --git a/tests/fields.test.js b/tests/fields.test.js index 1dcfcf23..a08d0abb 100644 --- a/tests/fields.test.js +++ b/tests/fields.test.js @@ -1,4 +1,9 @@ -import { dataArrayToObject, dataObjectToArray } from '../lib/utils/fields'; +import { + dataArrayToObject, + dataObjectToArray, + parseMultivaluedValue, + formatMultivaluedValue, +} from '../lib/utils/fields'; const fields = [ { @@ -101,3 +106,61 @@ describe('dataObjectToArray', () => { expect(dataArray).toEqual(['', '33.333', '', '', 'a; b; c', '33']); }); }); + +describe('parseMultivaluedValue', () => { + test('parses values delimited by "; "', () => { + const input = 'one two; three; four'; + const parsed = parseMultivaluedValue(input); + expect(parsed).toEqual(['one two', 'three', 'four']); + }); + + test('parses values delimited by ";"', () => { + const input = 'one two;three;four'; + const parsed = parseMultivaluedValue(input); + expect(parsed).toEqual(['one two', 'three', 'four']); + }); + + test('ignores leading and trailing spaces', () => { + const input = 'one two;three; four ;five'; + const parsed = parseMultivaluedValue(input); + expect(parsed).toEqual(['one two', 'three', 'four', 'five']); + }); + + test('discards empty entries', () => { + const input = ';one two;three; ;five;;;'; + const parsed = parseMultivaluedValue(input); + expect(parsed).toEqual(['one two', 'three', 'five']); + }); + + test('returns empty array for null or empty input', () => { + expect(parseMultivaluedValue('')).toEqual([]); + expect(parseMultivaluedValue(null)).toEqual([]); + expect(parseMultivaluedValue(undefined)).toEqual([]); + }); +}); + +describe('formatMultivaluedValue', () => { + test('formats values with correct delimiter and space', () => { + const input = ['one two', 'three', 'four']; + const formatted = formatMultivaluedValue(input); + expect(formatted).toEqual('one two; three; four'); + }); + + test('handles non-string values', () => { + const input = ['one two', 3, 'four']; + const formatted = formatMultivaluedValue(input); + expect(formatted).toEqual('one two; 3; four'); + }); + + test('discards empty entries', () => { + const input = ['one two', '', 'three', null, 'four']; + const formatted = formatMultivaluedValue(input); + expect(formatted).toEqual('one two; three; four'); + }); + + test('returns empty string for null or empty input', () => { + expect(formatMultivaluedValue([])).toEqual(''); + expect(formatMultivaluedValue(null)).toEqual(''); + expect(formatMultivaluedValue(undefined)).toEqual(''); + }); +});