Skip to content

Commit

Permalink
Merge pull request #376 from cidgoh/issue-375
Browse files Browse the repository at this point in the history
Add utility methods for parsing and formatting multivalued cell values
  • Loading branch information
ddooley authored Feb 11, 2023
2 parents 398f83d + 84e8fd3 commit 09d9cfb
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 27 deletions.
19 changes: 9 additions & 10 deletions lib/DataHarmonizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
dataArrayToObject,
dataObjectToArray,
fieldUnitBinTest,
formatMultivaluedValue,
parseMultivaluedValue,
} from './utils/fields';
import { parseDatatype } from './utils/datatypes';
import {
Expand Down Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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:
Expand Down
58 changes: 44 additions & 14 deletions lib/utils/fields.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
};
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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<String>} 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<Any>} 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);
}
6 changes: 4 additions & 2 deletions lib/utils/validation.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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];
Expand All @@ -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];
}

Expand Down
65 changes: 64 additions & 1 deletion tests/fields.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { dataArrayToObject, dataObjectToArray } from '../lib/utils/fields';
import {
dataArrayToObject,
dataObjectToArray,
parseMultivaluedValue,
formatMultivaluedValue,
} from '../lib/utils/fields';

const fields = [
{
Expand Down Expand Up @@ -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('');
});
});

0 comments on commit 09d9cfb

Please sign in to comment.