2

I have a number of arrays that I need to pull values from depending on the page that is being requested. Each page needs to be assigned a unique combination of data, so for example:

let numbers = ['One', 'Two']
let fruits  = ['Apples', 'Oranges']
let colours = ['Red', 'Green']
let names   = ['John', 'Jane']

None of the arrays would ever be empty, and so the number of available pages would be the length of all arrays multiplied by each other (so in this case, totalPages would be 16

let totalPages = numbers.length * fruits.length * colours.length * names.length

For every page requested, I need to return a unique set of indices so that every page shows a different group of values, and never more than one from each group.

I currently have nested for-loops (example below) but I was wondering if there's a neater way using the mod operator or something, so that I don't need to rely on the for-loops because at some point I may need to introduce extra arrays and I hate how the nested loops look...

let page = 4 // The requested page (not zero-based index)

let nPage = 1
for(let numberIndex = 0; numberIndex < numbers.length; numberIndex ++) {
    for(let fruitIndex = 0; fruitIndex < fruits.length; fruitIndex ++) {
        for(let colourIndex = 0; colourIndex < colours.length; colourIndex ++) {
            for(let nameIndex = 0; nameIndex < names.length; nameIndex ++) {
                // If the loop iteration matches the requested page,
                // return the combination of indexes as array
                if(page === nPage) {
                    return [numberIndex, fruitIndex, colourIndex, nameIndex]
                }
                nPage ++
            }
        }
    }
}

Thanks in advance for any ideas/suggestions :o)

Tom Dyer
  • 457
  • 2
  • 10
  • please add the wanted result of all. – Nina Scholz Jan 27 '22 at 18:26
  • 1
    It's rare and nice to see emoticons with noses. :o) – jsejcksn Jan 27 '22 at 18:27
  • @PrisonerZERO shouldn't matter if the arrays have different lengths, the product will always give the total value – skara9 Jan 27 '22 at 18:31
  • 1
    Use [this function](https://stackoverflow.com/a/44338759/) with `let combinations = cartesian(numbers, fruits, colors, names)` and then you can find the n-th combination wtih something like `for (let i = 0; i < page; i++) combinations.next(); return combinations.next();` – skara9 Jan 27 '22 at 18:38
  • is your page mandatorily an int? I mean 1,2,2,1 can be associated the unique couple (one, oranges, green, john) which is a lot simpler than doing modulo and such – grodzi Jan 27 '22 at 18:54

2 Answers2

3

This is really a math/algorithm question (which are always fun!). So a good approach is to see if you can figure out the pattern. E.g., given 3 arrays, [1,2,3,4], [x,y,z], and [11,12], you expect

1 x 11
2 x 11
3 x 11
4 x 11
 
1 y 11
2 y 11
3 y 11
4 y 11
 
1 z 11
2 z 11
3 z 11
4 z 11
 
1 x 12
2 x 12
3 x 12
4 x 12
 
1 y 12
2 y 12
3 y 12
4 y 12
 
1 z 12
2 z 12
3 z 12
4 z 12

The pattern should be clear now, it's just counting, like you count 1,2,3,...9,10,11...99,100,101.... But in traditional counting, all digits start at 0 and wrap at 10. Whereas in our example, the first digit starts at 1 and wraps around at 4; the 2nd digit starts at 'x' and wraps around at 'z', and the 3rd starts at '11' and wraps at 12.

If someone asks you to find the n-th digit (from the right) of the m-th decimal number 4 digits long (e.g. the 3rd digit of 1234 is 2), you could truncate with temp = floor(m / 10 ** (n-1)), which would give you 1234, 123, 12, 1. Then temp % 10 would give the last digit of each: 4, 3, 2, 1.

In our case, our digits aren't base 10, so we replace the 10's with the appropriate calculations:

let numbers = ['1', '2', '3'];
let fruits = ['a', 'b', 'c'];
let colours = ['x', 'y', 'z'];
let names = ['11', '12', '13'];

let getPage = i =>
    [
        numbers[i % numbers.length],
        fruits[Math.floor(i / numbers.length) % numbers.length],
        colours[Math.floor(i / numbers.length / fruits.length) % colours.length],
    ];
  
// print only the first 40 pages, otherwise stackoverflow will truncate the console
for (let i = 0; i < 40; i++)
    console.log(getPage(i).join(' '));

And if you have a dynamic set of arrays (e.g. you don't know ahead of time which of these arrays you will use):

let numbers = ['1', '2', '3'];
let fruits = ['a', 'b', 'c'];
let colours = ['x', 'y', 'z'];
let names = ['11', '12', '13'];

let getPage = (pageI, arrays) => {
    // `pageCounts[i]` is the # of combinations that can generated from the 1st `i` arrays.
    let pageCounts = arrays.map((values, j) => arrays
        .filter((_, k) => k < j)
        .map(a => a.length)
        .reduce((a, b) => a * b, 1));
    return arrays.map((values, j) => values[Math.floor(pageI / pageCounts[j]) % values.length]);
};

// print only the first 40 pages, otherwise stackoverflow will truncate the console
for (let i = 0; i < 40; i++)
    console.log(getPage(i, [numbers, fruits, colours, names]).join(' '));
junvar
  • 11,151
  • 2
  • 30
  • 46
1

I find a cartesian generator would work really well here, as it lets you calculate every combination and stop at the n-th value without calculating ahead. Here's an example using an implementation from this answer.

let numbers = ['One', 'Two']
let fruits  = ['Apples', 'Oranges']
let colours = ['Red', 'Green']
let names   = ['John', 'Jane']

let totalPages = numbers.length * fruits.length * colours.length * names.length

// Function from https://stackoverflow.com/a/44338759
function* cartesian(head, ...tail) {
  let remainder = tail.length ? cartesian(...tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];
}

function getPage(n) {
    const combinations = cartesian(numbers, fruits, colours, names);
    for (let i = 0; i < (n-1) % totalPages; i++) combinations.next();
    return combinations.next().value;
}

// loops back, so 1 is the same as 17
console.log(getPage(1))
console.log(getPage(17))

On a side note, I would advise you to refactor your code use an array instead of separate variables as it will make it more maintainable.

const pages = [numbers, fruits, colours, names];

const totalPages = pages.length ? pages.reduce((a,b) => a * b.length, 1) : 0;

...

function getPage(n) {
  const combinations = cartesian(...pages);
  ...
skara9
  • 4,042
  • 1
  • 6
  • 21