Skip to content

Commit 09d9cfb

Browse files
authored
Merge pull request #376 from cidgoh/issue-375
Add utility methods for parsing and formatting multivalued cell values
2 parents 398f83d + 84e8fd3 commit 09d9cfb

File tree

4 files changed

+121
-27
lines changed

4 files changed

+121
-27
lines changed

lib/DataHarmonizer.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
dataArrayToObject,
1111
dataObjectToArray,
1212
fieldUnitBinTest,
13+
formatMultivaluedValue,
14+
parseMultivaluedValue,
1315
} from './utils/fields';
1416
import { parseDatatype } from './utils/datatypes';
1517
import {
@@ -1066,14 +1068,11 @@ class DataHarmonizer {
10661068
afterBeginEditing: function (row, col) {
10671069
if (fields[col].flatVocabulary && fields[col].multivalued === true) {
10681070
const value = this.getDataAtCell(row, col);
1069-
let selections = (value && value.split(';')) || [];
1070-
selections = selections.map((x) => x.trim());
1071-
const selections2 = selections.filter(function (el) {
1072-
return el != '';
1073-
});
1071+
const selections = parseMultivaluedValue(value);
1072+
const formattedValue = formatMultivaluedValue(selections);
10741073
// Cleanup of empty values that can occur with leading/trailing or double ";"
1075-
if (selections.length != selections2.length) {
1076-
this.setDataAtCell(row, col, selections2.join('; '), 'thisChange');
1074+
if (value !== formattedValue) {
1075+
this.setDataAtCell(row, col, formattedValue, 'thisChange');
10771076
}
10781077
const self = this;
10791078
let content = '';
@@ -1106,9 +1105,9 @@ class DataHarmonizer {
11061105
},
11071106
}) // must be rendered when html is visible
11081107
.on('change', function () {
1109-
let newValCsv = $('#field-description-text .multiselect')
1110-
.val()
1111-
.join('; ');
1108+
let newValCsv = formatMultivaluedValue(
1109+
$('#field-description-text .multiselect').val()
1110+
);
11121111
self.setDataAtCell(row, col, newValCsv, 'thisChange');
11131112
});
11141113
// Saves users a click:

lib/utils/fields.js

+44-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { parseDatatype, stringifyDatatype } from './datatypes';
22

3+
const MULTIVALUED_DELIMITER = '; ';
4+
35
/**
46
* Modify a string to match specified case.
57
* @param {String} val String to modify.
@@ -39,11 +41,10 @@ export function fieldUnitBinTest(fields, col) {
3941
}
4042

4143
const DATA_ARRAY_TO_OBJECT_OPTION_DEFAULTS = {
42-
multivalueDelimiter: '; ',
4344
strict: true,
4445
};
4546
export function dataArrayToObject(dataArray, fields, options = {}) {
46-
const { multivalueDelimiter, strict } = {
47+
const { strict } = {
4748
...DATA_ARRAY_TO_OBJECT_OPTION_DEFAULTS,
4849
...options,
4950
};
@@ -55,7 +56,7 @@ export function dataArrayToObject(dataArray, fields, options = {}) {
5556
const field = fields[idx];
5657
let parsed;
5758
if (field.multivalued) {
58-
const split = cell.split(multivalueDelimiter);
59+
const split = parseMultivaluedValue(cell);
5960
parsed = split
6061
.map((value) => (strict ? parseDatatype(value, field.datatype) : value))
6162
.filter((parsed) => parsed !== undefined);
@@ -72,14 +73,7 @@ export function dataArrayToObject(dataArray, fields, options = {}) {
7273
return dataObject;
7374
}
7475

75-
const DATA_OBJECT_TO_ARRAY_DEFAULT_OPTIONS = {
76-
multivalueDelimiter: '; ',
77-
};
78-
export function dataObjectToArray(dataObject, fields, options = {}) {
79-
const { multivalueDelimiter } = {
80-
...DATA_OBJECT_TO_ARRAY_DEFAULT_OPTIONS,
81-
...options,
82-
};
76+
export function dataObjectToArray(dataObject, fields) {
8377
const dataArray = Array(fields.length).fill('');
8478
for (const [key, value] of Object.entries(dataObject)) {
8579
const fieldIdx = fields.findIndex((f) => f.name === key);
@@ -89,12 +83,48 @@ export function dataObjectToArray(dataObject, fields, options = {}) {
8983
}
9084
const field = fields[fieldIdx];
9185
if (field.multivalued && Array.isArray(value)) {
92-
dataArray[fieldIdx] = value
93-
.map((v) => stringifyDatatype(v, field.datatype))
94-
.join(multivalueDelimiter);
86+
dataArray[fieldIdx] = formatMultivaluedValue(
87+
value.map((v) => stringifyDatatype(v, field.datatype))
88+
);
9589
} else {
9690
dataArray[fieldIdx] = stringifyDatatype(value, field.datatype);
9791
}
9892
}
9993
return dataArray;
10094
}
95+
96+
/**
97+
* Parse a formatted string representing a multivalued value and return an
98+
* array of the individual values.
99+
*
100+
* @param {String} value String-formatted multivalued value.
101+
* @return {Array<String>} Array of individual string values.
102+
*/
103+
export function parseMultivaluedValue(value) {
104+
if (!value) {
105+
return [];
106+
}
107+
// trim the delimiter and the resulting tokens to be flexible about what
108+
// this function accepts
109+
return value
110+
.split(MULTIVALUED_DELIMITER.trim())
111+
.map((v) => v.trim())
112+
.filter((v) => !!v);
113+
}
114+
115+
/**
116+
* Format a string array of multivalued values into a single string representation.
117+
*
118+
* @param {Array<Any>} values Array of individual values.
119+
* @return {String} String-formatted multivalued value.
120+
*/
121+
export function formatMultivaluedValue(values) {
122+
if (!values) {
123+
return '';
124+
}
125+
126+
return values
127+
.filter((v) => !!v)
128+
.map((v) => (typeof v === 'string' ? v.trim() : String(v)))
129+
.join(MULTIVALUED_DELIMITER);
130+
}

lib/utils/validation.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { formatMultivaluedValue, parseMultivaluedValue } from './fields';
2+
13
/**
24
* Test cellVal against "DataHarmonizer provenance: vX.Y.Z" pattern and if it
35
* needs an update, do so.
@@ -99,7 +101,7 @@ export function validateValAgainstVocab(value, field) {
99101
*/
100102
export function validateValsAgainstVocab(delimited_string, field) {
101103
let update_flag = false;
102-
let value_array = delimited_string.split(';');
104+
let value_array = parseMultivaluedValue(delimited_string);
103105
// for-loop construct ensures return is out of this function.
104106
for (let index = 0; index < value_array.length; index++) {
105107
let value = value_array[index];
@@ -110,7 +112,7 @@ export function validateValsAgainstVocab(delimited_string, field) {
110112
value_array[index] = update;
111113
}
112114
}
113-
if (update_flag) return [true, value_array.join(';')];
115+
if (update_flag) return [true, formatMultivaluedValue(value_array)];
114116
else return [true, false];
115117
}
116118

tests/fields.test.js

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { dataArrayToObject, dataObjectToArray } from '../lib/utils/fields';
1+
import {
2+
dataArrayToObject,
3+
dataObjectToArray,
4+
parseMultivaluedValue,
5+
formatMultivaluedValue,
6+
} from '../lib/utils/fields';
27

38
const fields = [
49
{
@@ -101,3 +106,61 @@ describe('dataObjectToArray', () => {
101106
expect(dataArray).toEqual(['', '33.333', '', '', 'a; b; c', '33']);
102107
});
103108
});
109+
110+
describe('parseMultivaluedValue', () => {
111+
test('parses values delimited by "; "', () => {
112+
const input = 'one two; three; four';
113+
const parsed = parseMultivaluedValue(input);
114+
expect(parsed).toEqual(['one two', 'three', 'four']);
115+
});
116+
117+
test('parses values delimited by ";"', () => {
118+
const input = 'one two;three;four';
119+
const parsed = parseMultivaluedValue(input);
120+
expect(parsed).toEqual(['one two', 'three', 'four']);
121+
});
122+
123+
test('ignores leading and trailing spaces', () => {
124+
const input = 'one two;three; four ;five';
125+
const parsed = parseMultivaluedValue(input);
126+
expect(parsed).toEqual(['one two', 'three', 'four', 'five']);
127+
});
128+
129+
test('discards empty entries', () => {
130+
const input = ';one two;three; ;five;;;';
131+
const parsed = parseMultivaluedValue(input);
132+
expect(parsed).toEqual(['one two', 'three', 'five']);
133+
});
134+
135+
test('returns empty array for null or empty input', () => {
136+
expect(parseMultivaluedValue('')).toEqual([]);
137+
expect(parseMultivaluedValue(null)).toEqual([]);
138+
expect(parseMultivaluedValue(undefined)).toEqual([]);
139+
});
140+
});
141+
142+
describe('formatMultivaluedValue', () => {
143+
test('formats values with correct delimiter and space', () => {
144+
const input = ['one two', 'three', 'four'];
145+
const formatted = formatMultivaluedValue(input);
146+
expect(formatted).toEqual('one two; three; four');
147+
});
148+
149+
test('handles non-string values', () => {
150+
const input = ['one two', 3, 'four'];
151+
const formatted = formatMultivaluedValue(input);
152+
expect(formatted).toEqual('one two; 3; four');
153+
});
154+
155+
test('discards empty entries', () => {
156+
const input = ['one two', '', 'three', null, 'four'];
157+
const formatted = formatMultivaluedValue(input);
158+
expect(formatted).toEqual('one two; three; four');
159+
});
160+
161+
test('returns empty string for null or empty input', () => {
162+
expect(formatMultivaluedValue([])).toEqual('');
163+
expect(formatMultivaluedValue(null)).toEqual('');
164+
expect(formatMultivaluedValue(undefined)).toEqual('');
165+
});
166+
});

0 commit comments

Comments
 (0)