Nate
Nate

Reputation: 13242

Trim the left and right of an array

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

Answers (4)

Xotic750
Xotic750

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 returning 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

Nate
Nate

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

Adam Boduch
Adam Boduch

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

Pete
Pete

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

Related Questions