Combining arrays of features into a matrix

The part where I beg for programming help.

So, paradigms.

A paradigm can have some number of features (or more mathily, “dimensions”). You can chart the paradigm with columns and rows. If there are two dimensions, it’s easy:

let data = [
  ["masculine", "neuter", "feminine"],
  ["singular", "plural"]
]

That will just be:

masculine neuter feminine
singular M.Sg N.Sg F.Sg
plural M.Pl N.Pl F.Pl

You build that by mapping the first array onto the second, thuslywise:

let genders =  ["masculine", "neuter", "feminine"]
let numbers =  ["singular", "plural"]

genders.map(gender => numbers.map(number => [gender, number]))

And then you get an array of arrays like ["masculine", "singular"], which is what we want, for instance, if we want to build a table like the one above. (Actually of course, you would put inflections or something else that has those features in the cells, but all I’m concerned with here is generating a matrix of the combinations of the features.)

[
  [
    [ "masculine", "singular" ], [ "masculine", "plural" ]
  ],
  [
    [
      "neuter", "singular" ], [ "neuter", "plural" ]
  ],
  [
    [ "feminine", "singular" ], [ "feminine", "plural" ]
  ]
]

Once this is in place, rendering a table or whatever is quite easy.

But here’s what I want to do: I want to write a function that does this for any number of dimensions.

So maybe it’s number, case, gender. If there are 8 cases that will be 2 * 3 * 8 = 48 combinations in all, and it as a table it will have number as rows, case as columns, and gender as “sub-columns” (which is actually a rather weird layout, but again, whatevs).

For sure this requires some kind of thing with recurisive mapping, but I just can’t seem to get it to work as a general function:

let genders =  ["masculine", "neuter", "feminine"]
let cases =  [ "nominative", "accusative", "genitive", "dative", "instrumental", "ablative", "locative" ]
let numbers =  ["singular", "plural"]

genders.map(gender => numbers.map(number => cases.map(kase => [gender, number, kase])))

And you get this beast:

[
  [
    [
      [ "masculine", "singular", "nominative" ],
      [ "masculine", "singular", "accusative" ],
      [ "masculine", "singular", "genitive" ],
      [ "masculine", "singular", "dative" ],
      [ "masculine", "singular", "instrumental" ],
      [ "masculine", "singular", "ablative" ],
      [ "masculine", "singular", "locative" ]
    ],
    [
      [ "masculine", "plural", "nominative" ],
      [ "masculine", "plural", "accusative" ],
      [ "masculine", "plural", "genitive" ],
      [ "masculine", "plural", "dative" ],
      [ "masculine", "plural", "instrumental" ],
      [ "masculine", "plural", "ablative" ],
      [ "masculine", "plural", "locative" ]
    ]
  ],
  [
    [
      [ "neuter", "singular", "nominative" ],
      [ "neuter", "singular", "accusative" ],
      [ "neuter", "singular", "genitive" ],
      [ "neuter", "singular", "dative" ],
      [ "neuter", "singular", "instrumental" ],
      [ "neuter", "singular", "ablative" ],
      [ "neuter", "singular", "locative" ]
    ],
    [
      [ "neuter", "plural", "nominative" ],
      [ "neuter", "plural", "accusative" ],
      [ "neuter", "plural", "genitive" ],
      [ "neuter", "plural", "dative" ],
      [ "neuter", "plural", "instrumental" ],
      [ "neuter", "plural", "ablative" ],
      [ "neuter", "plural", "locative" ]
    ]
  ],
  [
    [
      [ "feminine", "singular", "nominative" ],
      [ "feminine", "singular", "accusative" ],
      [ "feminine", "singular", "genitive" ],
      [ "feminine", "singular", "dative" ],
      [ "feminine", "singular", "instrumental" ],
      [ "feminine", "singular", "ablative" ],
      [ "feminine", "singular", "locative" ]
    ],
    [
      [ "feminine", "plural", "nominative" ],
      [ "feminine", "plural", "accusative" ],
      [ "feminine", "plural", "genitive" ],
      [ "feminine", "plural", "dative" ],
      [ "feminine", "plural", "instrumental" ],
      [ "feminine", "plural", "ablative" ],
      [ "feminine", "plural", "locative" ]
    ]
  ]
]

So want to write:

let combine = dimensions => {

}

Such that it will work with:

combine(gender, number)

or

combine(gender, case, number)

…or whatever.

Thus ends my docling Stackoverflow question.

2 Likes

Here’s a recursive solution–doesn’t get you the exact list structure you want but maybe this is close enough :slight_smile:

function enumerateCombinations(base, vocabs) {
  if (vocabs.length === 0) {
    return [base];
  }
  let expansions = [];
  for (let item of vocabs[0]) {
    const newBase = [...base, item];
    const expanded = enumerateCombinations(newBase, vocabs.slice(1));
    expansions = expansions.concat(expanded);
  }
  return expansions;
}

enumerateCombinations([], [["a", "b"], [1,2,3], ["foo", "bar", "baz"]])
// =>
[['a', 1, 'foo'],
 ['a', 1, 'bar'],
 ['a', 1, 'baz'],
 ['a', 2, 'foo'],
 ['a', 2, 'bar'],
 ['a', 2, 'baz'],
 ['a', 3, 'foo'],
 ['a', 3, 'bar'],
 ['a', 3, 'baz'],
 ['b', 1, 'foo'],
 ['b', 1, 'bar'],
 ['b', 1, 'baz'],
 ['b', 2, 'foo'],
 ['b', 2, 'bar'],
 ['b', 2, 'baz'],
 ['b', 3, 'foo'],
 ['b', 3, 'bar'],
 ['b', 3, 'baz']]

edit: more code golf-y alternative that does get you the nested structure:

function enumerateCombinations(base, vocabs) {
  const [headVocab, ...remainingVocabs] = vocabs;
  if (!headVocab) {
    return [base];
  }
  let results = headVocab.map(
    (item) => enumerateCombinations([...base, item], remainingVocabs)
  );
  // uncomment this line for "flat" results
  //results = results.reduce((x,y) => x.concat(y)); 
  return results;
}
2 Likes

Local hero @BrenBarn explained how to do it in Python and I converted his version to JS:

def nester(*dims, vals=None):
	if vals is None:
		vals = []
	if len(dims) == 1:
		return [vals + [item] for item in dims[0]]
	return [nester(*dims[1:], vals=vals+[item]) for item in dims[0]]

Here’s the JS:

let nester = (dims, values=[]) => {
  if(dims.length == 1){
    return dims[0].map(item => values.concat(item))
  }
  return [...dims[0]].map(item => 
    nester([...dims.slice(1)], values.concat([item]))
  )
}

Your version is beautemous, @ldg! Thanks so much!

1 Like