Home > Software engineering >  Looking to form 2 sets of data from a object of arrays - Javascript
Looking to form 2 sets of data from a object of arrays - Javascript

Time:02-25

I am looking for help with the data in a football related app I am building.

Take this as a sample of the object I am dealing with:

const squad = {
  goalkeepers: [
    { player1: { score: 10 } },
    { player2: { score: 12 } }
  ],
  defenders: [
    { player3: { score: 3 } },
    { player4: { score: 19 } },
    { player5: { score: 5 } },
    { player6: { score: 21 } },
    { player7: { score: 6 } },
  ],
  midfielders: [
    { player8: { score: 7 } },
    { player9: { score: 1 } },
    { player10: { score: 18 } },
    { player11: { score: 11 } },
    { player12: { score: 8 } },
  ],
  attackers: [
    { player13: { score: 7 } },
    { player14: { score: 2 } },
    { player15: { score: 16 } }
  ]
}

There are 15 players here and I want to divide them into two groups:

  • A strongest possible outfield team of 11 players based on their score value.
  • The remaining 4 players are to go on the bench.

The twist here is that there is a minimum and maximum number of players required in each position of the 11 outfield players.

  • Goalkeepers: EXACTLY 1.
  • Defenders: MIN 3, MAX 5.
  • Midfielders: MIN 2, MAX 5.
  • Forwards: MIN 1, MAX 3.

For anyone familiar with Fantasy Premier League, the rules work the same way:

Your team can play in any formation providing that 1 goalkeeper, at least 3 defenders and at least 1 forward are selected at all times.

I've tried concatinating the arrays to one large array and sorting them by player score value but I can't work out how to calculate the strongest first 11 players from that point while adhering to the position rules.

Any help would be greatly appreciated.

CodePudding user response:

I find a modular approach more intuitive and resilient.

This solution breaks the problem down into first converting your data to a more useful format, then finding all the combination of eleven players, then filtering out those that don't match the rules, then choosing the one with the largest score:

// utility functions
const maximumBy = (fn) => (xs) => 
  xs .reduce ((a, x, i) => fn (x) > fn (a) ? x : a, xs [0] || null)

const choose = (n, xs) =>
  n < 1 || n > xs .length
    ? []
  : n == 1
    ? [...xs .map (x => [x])]
  : [
      ...choose (n - 1, xs .slice (1)) .map (ys => [xs [0], ...ys]),
      ...choose (n , xs .slice (1))
    ]


// helper functions
const simplify = (squad) => 
  Object .entries (squad) .flatMap (
    ([position, vs]) => vs .flatMap (
      (v) => Object.entries (v) .flatMap (([name, s]) => ({position, name, ...s}))
    )
  )

const validate = (rules) => (lineup) => rules .every (({position, min, max}) => {
   const count = lineup .filter (({position: p}) => p == position) .length
   return count >= min && count <= max
})

const totalScore = (lineup) =>
  lineup .reduce ((t, {score}) => t   score, 0)


// main function
const bestLineup = (squad, rules) => 
  maximumBy (totalScore) (choose (11, simplify (squad)) .filter (validate (rules)))


// sample data
const rules = [{position: 'goalkeepers', min: 1, max: 1}, {position: 'defenders', min: 3, max: 5}, {position: 'midfielders', min: 2, max: 5}, {position: 'attackers', min: 1, max: 3}]

const squad = {goalkeepers: [{player1: {score: 10}}, {player2: {score: 12}}], defenders: [{player3: {score: 3}}, {player4: {score: 19}}, {player5: {score: 5}}, {player6: {score: 21}}, {player7: {score: 6}}], midfielders: [{player8: {score: 7}}, {player9: {score: 1}}, {player10: {score: 18}}, {player11: {score: 11}}, {player12: {score: 8}}], attackers: [{player13: {score: 7}}, {player14: {score: 2}}, {player15: {score: 16}}]}


// demo
console .log (bestLineup (squad, rules))
.as-console-wrapper {max-height: 100% !important; top: 0}

We start with two general-purpose utility functions we might use in other projects

  • maximumBy takes a function that converts a value into a comparable one -- in this problem we use it to extract the score -- and return a function that accepts an array of values, runs that function on each and chooses the one with the largest score. (This version is less efficient than it could be. I'd rather focus on simplicity at the moment.

  • choose finds all subsets of n elements of your array of values. For instance, choose (2, ['a', 'b', 'c', 'd']) returns [['a', 'b'], ['a', 'c'], ['a', 'd']. ['b', 'c'], ['b', 'd'], ['c', 'd']].

Then we have some helper functions,

  • simplify turns your initial squad format into something more tractable:

    [
      {name: "player1", position: "goalkeepers", score: 10},
      {name: "player2", position: "goalkeepers", score: 12},
      {name: "player3", position: "defenders", score: 3},
      // ...
      {name: "player15", position: "attackers", score: 16}
    ]
    
  • validate takes an array of rules such as

    {position: 'defenders', min: 3, max: 5}
    

    and returns a function that takes a lineup from the squad and reports whether that lineup obeys all the rules.

  • totalScore takes a lineup and sums up the scores of all the players

Finally, our main function, bestLineup accepts a squad and an array of rules, simplifies the squad, chooses all lineups of eleven players, filters it down to just those which validate according to the rules, and chooses the maximum by our function that calculates their total score.

If you want the output in the same format as the input, we can just call one more helper to undo our simplify; let's call it complexify:

const complexify = (xs) => 
  xs .reduce (
    (a, {name, position, score}) => (
      (a [position] = a [position] || []), (a [position] .push ({[name]: {score}})), a
    ), {})

which will transform an array of the simple format used above back into something like this:

{
    goalkeepers: [
        {player2: {score: 12}}
    ],
    defenders: [
        {player4: {score: 19}},
        {player5: {score: 5}},
        {player6: {score: 21}},
        {player7: {score: 6}}
    ],
    midfielders: [
        {player8: {score: 7}},
        {player10: {score: 18}},
        {player11: {score: 11}},
        {player12: {score: 8}}
    ]
    attackers: [
        {player13: {score: 7}},
        {player15: {score: 16}}
    ],
}

But I would only do this if your unusual data format is forced upon you.

  • Related