Off-by-one error is one of the most common bugs. We can reduce this category of bugs with how we choose to write code. The more math present in our code, the more opportunities for errors.
This blog post will run through several scenarios that can be solved with typical math heavier patterns and alternative reduced math solutions. By doing less math in our code, we are allowing computers to do what they are meant for.
The code samples are written in JavaScript and may include lodash (imported as _
).
Access every element in an array
// BEFORE
for (let i = 0; i < array.length; i++) {
const element = array[i];
..
}
// AFTER
for (const element of array) {
..
}
Common errors
- using
var
instead of let
(causes i
to be visible outside of for loop)
- using
const
instead of let
(loop fails on i++
)
- typo
,
instead of ;
(comma surprisingly is an operator)
- typo
i <= array.length
- typo
i < array.length — 1
- typo
j
instead of i
(hard to detect if j
has been defined before the loop)
- typo
array(i)
instead of array[i]
(especially hard to detect if array
is typeof
function which happens to be used in an array-like fashion)
Access every element in an array with an index
// BEFORE
for (let i = 0; i < array.length; i++) {
const element = array[i];
..
}
// AFTER
array.forEach((element, i) => {
..
});
Access every element in an array in reverse
// BEFORE
for (let i = array.length - 1; i >= 0; i--) {
const element = array[i];
..
}
// AFTER
for (const element of [...array].reverse()) {
..
}
Common errors (omits one similar to “Access every element in an array”)
- typo
let i = array.length
- typo
i > 0
Transform every element into a new array
// BEFORE
const result = [];
for (let i = 0; i < array.length; i++) {
result[i] = someFunction(array[i]);
}
// AFTER
const result = array.map((element) => someFunction(element));
Alternatively, if someFunction
ignores parameters after the first, shorten to array.map(someFunction)
.
Concatenate two arrays
// BEFORE
const result = [];
for (let i = 0; i < array1.length; i++) {
result[i] = array1[i];
}
for (let i = 0; i < array2.length; i++) {
result[array1.length + i] = array2[i];
}
// AFTER
const result = [...array1, ...array2];
Combine two arrays pair-wise with some function
// BEFORE
const result = [];
for (let i = 0; i < Math.max(array1.length, array2.length); i++) {
result[i] = someFunction(array1[i], array2[i]);
}
// AFTER
const result = _.zip(array1, array2).map(([element1, element2]) => someFunction(element1, element2));
Note someFunction
will receive undefined
items from the shorter array.
Take first n elements from an array
// BEFORE
const result = [];
for (let i = 0; i < Math.min(n, array.length); i++) {
result[i] = array[i];
}
// AFTER
const result = array.slice(0, n);
Drop first n elements from an array
// BEFORE
const result = [];
for (let i = n; i < array.length; i++) {
result[i - n] = array[i];
}
// AFTER
const result = array.slice(n);
Take last n elements from an array
// BEFORE
const result = [];
const init = Math.max(0, array.length - n);
for (let i = init; i < array.length; i++) {
result[i - init] = array[i];
}
// AFTER
const result = array.slice(-n);
Drop last n elements from an array
// BEFORE
const result = [];
for (let i = 0; i < Math.max(0, array.length - n); i++) {
result[i] = array[i];
}
// AFTER
const result = array.slice(0, -n);
Take subarray from start to end index
// BEFORE
const subarray = [];
for (let i = start; i < Math.min(end, array.length); i++) {
subarray[i - start] = array[i];
}
// AFTER
const subarray = array.slice(start, end);
Drop subarray from start to end index
// BEFORE
const subarray = [];
for (let i = 0; i < Math.min(start, array.length); i++) {
subarray[i] = array[i];
}
for (let i = end; i < array.length; i++) {
subarray[start + i] = array[i];
}
// AFTER
const subarray = [...array.slice(0, start), ...array.slice(end)];
Take subarray from start index to length n
// BEFORE
const subarray = [];
for (let i = start; i < Math.min(start + n, array.length); i++) {
subarray[i - start] = array[i];
}
// AFTER
const subarray = array.slice(start);
subarray.length = n;
Drop subarray from start index to length n
// BEFORE
const subarray = [];
for (let i = 0; i < Math.min(start, array.length); i++) {
subarray[i] = array[i];
}
for (let i = start + n; i < array.length; i++) {
subarray[start + i] = array[i];
}
// AFTER
const subarray = [...array];
subarray.splice(start, n);
Note the splice
, not to be confused with slice
.
Create array of length n with fill value
// BEFORE
const result = [];
for (let i = 0; i < n; i++) {
result[i] = value;
}
// AFTER
const result = Array(n).fill(value);
If someFunction
needs the index, use Array.from({length: n}, (_, i) => someFunction(i))
.
Pad end of array to length n with fill value
// BEFORE
const result = [];
const paddedLength = Math.max(0, n - array.length);
for (let i = 0; i < array.length; i++) {
result[i] = array[i];
}
for (let i = 0; i < paddedLength; i++) {
result[array.length + i] = value;
}
// AFTER
const result = Object.assign(Array(n).fill(value), ...array);
Pad start of array to length n with fill value
// BEFORE
const result = [];
const paddedLength = Math.max(0, n - array.length);
for (let i = 0; i < paddedLength; i++) {
result[i] = value;
}
for (let i = 0; i < array.length; i++) {
result[paddedLength + i] = array[i];
}
// AFTER
const result = Object.assign(Array(n).fill(value), [...array].reverse()).reverse();
Oof, that’s not great. But neither is [...Array(n).fill(value), ...array].slice(-(Math.max(n, array.length))
. Leave a response if you have a better solution.
Extract head and tail from an array
// BEFORE
const head = array[0];
const tail = [];
for (let i = 1; i < array.length; i++) {
tail[i - 1] = array[i];
}
// AFTER
const [head, ...tail] = array;
Extract initial and last from an array
// BEFORE
const initial = [];
for (let i = 0; i < array.length - 1; i++) {
initial[i] = array[i];
}
const last = array[array.length - 1];
// AFTER
const initial = array.slice(0, -1);
const last = array.at(-1);
Chunk by size n
// BEFORE
const result = [];
for (let i = 0; i < array.length; i += n) {
const chunk = [];
for (let j = 0; j < n && i + j < array.length; j++) {
chunk[j] = array[i + j];
}
result.push(chunk);
}
// AFTER
const result = [];
while (array.length) {
result.push(array.splice(0, n)); // modifies array
}
If one cannot modify the existing array, use lodash’s chunk.
Access matrix by columns
// BEFORE
for (let i= 0; i < matrix[0].length; i++) {
const column = [];
for(let j = 0; j < matrix.length; j++) {
column[i] = matrix[j][i];
}
// use column
..
}
// AFTER
for (const column of _.unzip(matrix)) { // _.unzip is R.transpose
// use column
..
}
This assumed matrix
is rectangular.
Be careful with mathless code in production
While the examples show how to write less math, some are definitely harder to read and understand. Do not use these mathless forms it made your production code worse.
Do you want to write less math? You’re in luck, Battlefy is hiring.