Another approach would be where you don't flatten the array, but provide utility functions for those parts of the logic that expects it to be flat.
Here are some ideas:
const flatAt = (arr, i) => arr[i >> 1][i & 1];
const flatPutAt = (arr, i, val) => arr[i >> 1][i & 1] = val;
const flatLength = (arr) => arr.length * 2;
function* flatValues(arr) {
for (const pair of arr) yield* pair;
}
// Demo
let arr = [[1,2], [3,4]];
console.log("get");
for (let i = 0, size = flatLength(arr); i < size; i++) {
console.log(flatAt(arr, i));
}
console.log("double");
for (let i = 0, size = flatLength(arr); i < size; i++) {
console.log(flatPutAt(arr, i, flatAt(arr, i) * 2));
}
console.log("iter", ...flatValues(arr));
You could take it a step further and create a class for it:
class ArrayAsFlat extends Array {
flatAt(i) {
return this[i >> 1][i & 1];
}
flatPutAt(i, val) {
return this[i >> 1][i & 1] = val;
}
*flatValues() {
for (const pair of this) yield* pair;
}
get flatLength() {
return this.length * 2;
}
}
// Demo
const arr = new ArrayAsFlat([1,2], [3,4]);
console.log("get");
for (let i = 0, size = arr.flatLength; i < size; i++) {
console.log(arr.flatAt(i));
}
console.log("double");
for (let i = 0, size = arr.flatLength; i < size; i++) {
console.log(arr.flatPutAt(i, arr.flatAt(i) * 2));
}
console.log("iter", ...arr.flatValues());
Whether or not this turns out to be more efficient depends on how many actions you have to perform using the additional methods.
If most of your code needs the flat type of access, then you could do the inverse, and store the array as flat from the start and provide methods that will return (or set) values as if the array were nested.
The best solution would be to convert your code so it uses one type of access only, so no conversion is needed.