Reputation: 13242
How do you trim contiguous falsey members from only the left and right of an array?
I would like to write a method that does something like this
getLookupKey('prefix', 'prefix', 'middle', 'suffix', 'suffix');
// => 'prefix.prefix.middle.suffix.suffix'
getLookupKey(null, 'prefix', 'middle', 'suffix', null);
// => 'prefix.middle.suffix'
getLookupKey('name');
// => 'name'
getLookupKey();
// => ''
getLookupKey('prefix', null, 'suffix');
// => throws error
Where getLookupKey
takes a variable number of arguments and produces a lookup key.
I would like this method to ignore nulls on the left and right of the array but throw if there are any nulls in the middle.
Here are some behavior rules:
Upvotes: 1
Views: 911
Reputation: 23482
Not using any libraries, just pure Javascript, so has more lines of code than the sugared syntax of a library. Should have better performance than using multiple loops as it is just a single loop, will be more noticeable with a larger number of arguments.
Works by looking at each end of the array-like arguments
object, marking the start
and end
positions where elements do not evaluate to false
. Once marked further elements are concatenated to the result
string. If we have content then we concatenate the start
and end
element (if they exist) to give the full content, finally return
ing the result
.
function getLookupKey() {
var length = arguments.length,
result = '',
index,
start,
end,
stop,
value;
for (index = 0; index < length; index += 1) {
if (arguments[index]) {
start = index;
break;
}
}
for (index = length; index > start; index -= 1) {
if (arguments[index]) {
end = index;
break;
}
}
stop = end - 1;
for (index = start + 1; index < end; index += 1) {
value = arguments[index];
if (!value) {
throw new SyntaxError('middle evaluates false');
}
result += value;
if (index !== stop) {
result += '.';
}
}
if (typeof start === 'number') {
if (result) {
result = '.' + result;
}
result = arguments[start] + result;
}
if (typeof end === 'number') {
result += '.' + arguments[end];
}
return result;
}
function log(str) {
document.getElementById('out').textContent += str + '\n';
}
var tests = [
[
[null, 'middle'], 'middle'
],
[
[null, null, null, 'middle', null, null], 'middle'
],
[
[null, null, 'middle', null, null, null], 'middle'
],
[
['prefix1', 'prefix2', 'middle', 'suffix1', 'suffix2'], 'prefix1.prefix2.middle.suffix1.suffix2'
],
[
[null, 'prefix', 'middle', 'suffix', null], 'prefix.middle.suffix'
],
[
['name'], 'name'
],
[
[null], ''
],
[
[null, null, null, null], ''
],
[
['a', null, null, null], 'a'
],
[
[null, null, null, 'b'], 'b'
],
[
[], ''
],
[
['prefix', null, 'suffix'], Error
],
[
[null, null, 'a', 'b', 'c', null, null], 'a.b.c'
],
[
['a', null, 'b'], Error
],
[
[null, 'middle', null], 'middle'
],
[
[null, 'middle1', null, 'middle2', 'middle3', null], Error
],
[
[null, 'middle1', 'middle2', 'middle3', null, 'middle4', 'middle5', null], Error
],
[
[null, 'middle1', null, null, null, null, 'middle2', null], Error
]
];
function test(fn) {
var length = tests.length,
index,
expected,
args,
actual;
for (index = 0; index < length; index += 1) {
args = tests[index][0];
expected = tests[index][1];
if (typeof expected === 'string') {
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else if (expected === Error) {
expected = 'middle evaluates false';
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else {
log('Test ' + index + ': coder error');
}
}
}
test(getLookupKey);
<pre id="out"></pre>
Simple but less efficient than the above, in ES5
function getLookupKey() {
var arr = Array.prototype.reduce.call(arguments, function (acc, arg) {
if (arg || acc.length) {
acc.push(arg);
}
return acc;
}, []).reduceRight(function (acc, arg) {
if (arg || acc.length) {
acc.unshift(arg);
}
return acc;
}, []);
if (!arr.every(Boolean)) {
throw new SyntaxError('middle evaluates false');
}
return arr.join('.');
}
function log(str) {
document.getElementById('out').textContent += str + '\n';
}
var tests = [
[
[null, 'middle'], 'middle'
],
[
[null, null, null, 'middle', null, null], 'middle'
],
[
[null, null, 'middle', null, null, null], 'middle'
],
[
['prefix1', 'prefix2', 'middle', 'suffix1', 'suffix2'], 'prefix1.prefix2.middle.suffix1.suffix2'
],
[
[null, 'prefix', 'middle', 'suffix', null], 'prefix.middle.suffix'
],
[
['name'], 'name'
],
[
[null], ''
],
[
[null, null, null, null], ''
],
[
['a', null, null, null], 'a'
],
[
[null, null, null, 'b'], 'b'
],
[
[], ''
],
[
['prefix', null, 'suffix'], Error
],
[
[null, null, 'a', 'b', 'c', null, null], 'a.b.c'
],
[
['a', null, 'b'], Error
],
[
[null, 'middle', null], 'middle'
],
[
[null, 'middle1', null, 'middle2', 'middle3', null], Error
],
[
[null, 'middle1', 'middle2', 'middle3', null, 'middle4', 'middle5', null], Error
],
[
[null, 'middle1', null, null, null, null, 'middle2', null], Error
]
];
function test(fn) {
var length = tests.length,
index,
expected,
args,
actual;
for (index = 0; index < length; index += 1) {
args = tests[index][0];
expected = tests[index][1];
if (typeof expected === 'string') {
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else if (expected === Error) {
expected = 'middle evaluates false';
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else {
log('Test ' + index + ': coder error');
}
}
}
test(getLookupKey);
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.7/es5-shim.min.js"></script>
<pre id="out"></pre>
Simple but less efficient again, in ES6
function fVal(test, alt) {
return test !== -1 ? test : alt;
}
function getLookupKey() {
var arr = Array.prototype.slice.call(arguments),
length = arr.length,
begin = fVal(arr.findIndex(Boolean), length),
end = length - fVal(arr.slice().reverse().findIndex(Boolean), 0);
arr = arr.slice(begin, end);
if (!arr.every(Boolean)) {
throw new SyntaxError('middle evaluates false');
}
return arr.join('.');
}
function log(str) {
document.getElementById('out').textContent += str + '\n';
}
var tests = [
[
[null, 'middle'], 'middle'],
[
[null, null, null, 'middle', null, null], 'middle'],
[
[null, null, 'middle', null, null, null], 'middle'],
[
['prefix1', 'prefix2', 'middle', 'suffix1', 'suffix2'], 'prefix1.prefix2.middle.suffix1.suffix2'],
[
[null, 'prefix', 'middle', 'suffix', null], 'prefix.middle.suffix'],
[
['name'], 'name'],
[
[null], ''],
[
[null, null, null, null], ''],
[
['a', null, null, null], 'a'],
[
[null, null, null, 'b'], 'b'],
[
[], ''],
[
['prefix', null, 'suffix'], Error],
[
[null, null, 'a', 'b', 'c', null, null], 'a.b.c'],
[
['a', null, 'b'], Error],
[
[null, 'middle', null], 'middle'],
[
[null, 'middle1', null, 'middle2', 'middle3', null], Error],
[
[null, 'middle1', 'middle2', 'middle3', null, 'middle4', 'middle5', null], Error],
[
[null, 'middle1', null, null, null, null, 'middle2', null], Error]
];
function test(fn) {
var length = tests.length,
index,
expected,
args,
actual;
for (index = 0; index < length; index += 1) {
args = tests[index][0];
expected = tests[index][1];
if (typeof expected === 'string') {
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else if (expected === Error) {
expected = 'middle evaluates false';
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else {
log('Test ' + index + ': coder error');
}
}
}
test(getLookupKey);
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.32.2/es6-shim.min.js"></script>
<pre id="out"></pre>
Simple but less efficient again, in lodash (may be more efficient than ES5 and most likely better than ES6, no jsPerf available to test assumptions at present).
function getLookupKey() {
var slice =_(arguments).dropWhile(_.isEmpty).dropRightWhile(_.isEmpty);
if (!slice.all(Boolean)) {
throw new SyntaxError('middle evaluates false');
}
return slice.join('.');
}
function log(str) {
document.getElementById('out').textContent += str + '\n';
}
var tests = [
[
[null, 'middle'], 'middle'
],
[
[null, null, null, 'middle', null, null], 'middle'
],
[
[null, null, 'middle', null, null, null], 'middle'
],
[
['prefix1', 'prefix2', 'middle', 'suffix1', 'suffix2'], 'prefix1.prefix2.middle.suffix1.suffix2'
],
[
[null, 'prefix', 'middle', 'suffix', null], 'prefix.middle.suffix'
],
[
['name'], 'name'
],
[
[null], ''
],
[
[null, null, null, null], ''
],
[
['a', null, null, null], 'a'
],
[
[null, null, null, 'b'], 'b'
],
[
[], ''
],
[
['prefix', null, 'suffix'], Error
],
[
[null, null, 'a', 'b', 'c', null, null], 'a.b.c'
],
[
['a', null, 'b'], Error
],
[
[null, 'middle', null], 'middle'
],
[
[null, 'middle1', null, 'middle2', 'middle3', null], Error
],
[
[null, 'middle1', 'middle2', 'middle3', null, 'middle4', 'middle5', null], Error
],
[
[null, 'middle1', null, null, null, null, 'middle2', null], Error
]
];
function test(fn) {
var length = tests.length,
index,
expected,
args,
actual;
for (index = 0; index < length; index += 1) {
args = tests[index][0];
expected = tests[index][1];
if (typeof expected === 'string') {
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else if (expected === Error) {
expected = 'middle evaluates false';
try {
actual = fn.apply(null, args);
} catch (e) {
actual = e.message;
}
log('Test ' + index + ': Expected: "' + expected + '" Actual: "' + actual + '"');
} else {
log('Test ' + index + ': coder error');
}
}
}
test(getLookupKey);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.0/lodash.min.js"></script>
<pre id="out"></pre>
Upvotes: 1
Reputation: 13242
Implementation:
function getLookupKey(){
var keyParts = trimArrayFalsey(_.toArray(arguments));
if (! _.isArray(keyParts) || ! _.all(keyParts)) {
throw new Error('Center null');
}
return keyParts.join('.');
}
Dependent methods:
var _ = require("lodash");
/**
* getArrayRightFalsyCount(array, [predicate=_.identity])
* Get a count of the contiguous falsey members on the right of the array.
*/
function getArrayRightFalsyCount(arr, predicate){
if (! _.isArray(arr)) return 0;
predicate = predicate || _.identity;
return _.reduce(arr, function(result, value, index){
return predicate(value) ? 0 : result + 1;
}, 0);
}
/**
* getArrayLeftFalsyCount(array, [predicate=_.identity])
* Get a count of the contiguous falsey members on the left of the array.
*/
function getArrayLeftFalsyCount(arr, predicate){
if (! _.isArray(arr)) return 0;
return _.reduceRight(arr, function(result, value, index){
return value ? 0 : result + 1;
}, 0);
}
/**
* trimArrayFalsey(array, [predicate=_.identity])
* Trims the contiguous falsey members from the left and
* right edge of the array, but not from the middle.
*/
function trimArrayFalsey(arr, predicate){
if (! _.isArray(arr)) return null;
return _.slice(
arr,
getArrayLeftFalsyCount(arr, predicate),
arr.length - getArrayRightFalsyCount(arr, predicate)
);
}
Testing it:
getArrayRightFalsyCount(["a", false, "b", false, null, "", 0]);
// => 4
getArrayLeftFalsyCount([0, "", null, false, "b", false, "a"]);
// => 4
trimArrayFalsey([]);
// => []
trimArrayFalsey(["a", "b", "c"]);
// => ["a", "b", "c"]
trimArrayFalsey([null, null, false, "a", null, "b", "c", 0, ""]);
// => ["a", null, "b", "c"]
getLookupKey(null, null, "a", "b", "c", null, null);
// => "a.b.c"
getLookupKey("a", null, "b");
// => Uncaught Error: Center null
Upvotes: 0
Reputation: 11211
dropWhile() and dropRightWhile() are your friends:
function getLookupKey() {
var falsey = _.negate(_.identity);
var result = _(arguments)
.dropWhile(falsey)
.dropRightWhile(falsey)
.join('.');
if (_.includes(result, '..')) {
throw new Error('invalid');
} else if (_.isUndefined(result)) {
result = '';
}
return result;
}
Upvotes: 1
Reputation: 3254
Interesting question, here's my take on the solution. I used dropWhile
and dropRightWhile
to do most of the heavy lifting.
dropWhile
will drop all values that are false from the left until it finds a true value. dropRightWhile
will drop all values that are false from the right until it finds a true value. The any
method is used to test if any false values exist within members.
The isFalse
method is just a method to check for falsey values. It's just a js
trick to convert any value to a boolean value.
function log(value) {
document.getElementById("output").innerHTML += JSON.stringify(value, null, 2) + "\n"
}
function getLookupKey() {
var keyParts = trimArrayFalsey(_.toArray(arguments));
if (_.any(keyParts, isFalse)) {
throw new Error('Center null');
}
return keyParts.join('.');
}
function trimArrayFalsey(arr) {
return _.chain(arr)
.dropWhile(isFalse)
.dropRightWhile(isFalse)
.value();
}
function isFalse(value) {
return !!!value;
}
log(getLookupKey('prefix', 'prefix', 'middle', 'suffix', 'suffix'));
// => 'prefix.prefix.middle.suffix.suffix'
log(getLookupKey(null, 'prefix', 'middle', 'suffix', null));
// => 'prefix.middle.suffix'
log(getLookupKey('name'));
// => 'name'
log(getLookupKey());
// => ''
log(getLookupKey('prefix', null, 'suffix'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<pre id="output"></pre>
Upvotes: 1