Build a Playlist Remix Engine - Build a Playlist Remix Engine

Tell us what’s happening:

  1. remixPlaylist should call the helper functions in order to produce the final schedule.

Test 14 still isn’t passing.

The result I logged looked correct to me.

console.log(remixPlaylist(playlists,1));
[ { slot: 1, trackId: 'trk101' },
  { slot: 2, trackId: 'trk102' },
  { slot: 3, trackId: 'trk103' },
  { slot: 4, trackId: 'trk201' } ]

Your code so far

const playlists = [
  [
    {
      trackId: "trk101",
      artist: "Velvet Comet",
      title: "Crimson Afterglow",
      votes: 5,
      bpm: 122
    },
    {
      trackId: "trk102",
      artist: "Neon Harbor",
      title: "Static Horizon",
      votes: 2,
      bpm: 108
    },
    {
      trackId: "trk103",
      artist: "Lunar Arcade",
      title: "Midnight Frequency",
      votes: 4,
      bpm: 128
    }
  ],
  [
    {
      trackId: "trk201",
      artist: "Solar Echo",
      title: "Glass Skyline",
      votes: 3,
      bpm: 115
    },
    {
      trackId: "trk202",
      artist: "Velvet Comet",
      title: "Satellite Hearts",
      votes: 6,
      bpm: 124
    }
  ]
];


const flattenPlaylists = (lists) => {
  // input is not an array
  if (!Array.isArray(lists)) {
    return [];
  }
  

  // input is an array containing arrays.
  // each array containing objects.
  // i is playlist index.
  for (let i = 0; i < lists.length; i++) {
  // j is track index.
    for (let j = 0; j < lists[i].length; j++) {
      if (
        JSON.stringify(Object.keys(lists[i][j])) !==
        JSON.stringify(["trackId", "artist", "title", "votes", "bpm"])
      ) {
        return;
      }
      lists[i][j]["source"] = [i, j];
    }
  }

  // lists.flat() is an array containing objects.
  return lists.flat();
};


const scoreTracks = (tracks) => {
  for (const track of tracks) {
    track.score = track.votes * 10 - Math.abs(track.bpm - 120);
  }
  return tracks;
}

const dedupeTracks = (tracks) => {
  for (let i = 0; i<tracks.length -1 ; i++) {
    for (let j = i+1; j <tracks.length; j++) {
      if (tracks[i].trackId === tracks[j].trackId) {
        tracks.splice(j, 1);
      }
    }
  }
  return tracks;
}


const enforceArtistQuota = (tracks, max) => {

  if (max < 1) {
    return 0;
  }


  for (let i = 0; i<tracks.length -1 ; i++) {

  let occurrences = 0;

    for (let j = i+1; j <tracks.length; j++) {
      if (tracks[i].artist === tracks[j].artist) {
        occurrences++;
        if (occurrences >= max) {
          tracks.splice(j, 1);          
        } 
      }      
    }

  }
  return tracks;
}


const buildSchedule = (tracks) => {
  let schedule = [];
  for (let i = 0; i<tracks.length; i++) {
    schedule.push({ slot: i+1, trackId: tracks[i].trackId});
  } 
  return schedule;
}


const remixPlaylist = (lists, max) => {

  let copy = [...lists];
  let tracks = [];

  tracks = flattenPlaylists(copy);
  tracks = scoreTracks(tracks);
  tracks = dedupeTracks(tracks);
  tracks = enforceArtistQuota(tracks, max);
  
  const remix = buildSchedule(tracks);
  
  return remix;
}

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0

Challenge Information:

Build a Playlist Remix Engine - Build a Playlist Remix Engine

GitHub Link: freeCodeCamp/curriculum/challenges/english/blocks/lab-playlist-remix-engine/6976db7ecfa770bf21307b20.md at main · freeCodeCamp/freeCodeCamp · GitHub

Hi @Leendert,

Try testing your dedupeTracks function. Is it doing what is expected?

Happy coding

@dhess

As far as I can tell, the dedupeTracks function is working correctly.

If you test with console.log(dedupeTracks(playlists)), does it return what is expected? Are there any duplicate tracks in playlists?

@dhess

Did you mean

console.log(dedupeTracks(scoreTracks(flattenPlaylists(playlists))));

instead of

console.log(dedupeTracks(playlists));

?

  1. You should create a function named dedupeTracks that accepts an array of track objects as returned by scoreTracks and returns a new array with duplicate trackId entries removed, keeping only the first occurrence of each.

Either way will work. All scoreTracks does is add the score property to each playlist object, so it doesn’t matter which you test with.

playlists is an array containing two nested arrays, and each nested array contains objects.

However, both dedupeTracks and scoreTracks only accept a single array of objects.

So neither

dedupeTracks(playlists);

nor

scoreTracks(playlists);

works.

On the other hand, both

dedupeTracks(flattenPlaylists(playlists));

and

scoreTracks(flattenPlaylists(playlists));

work as expected.

You’re right, and that’s the second or third time I’ve second-guessed myself on this. I’ll take another look at this after more coffee. :yawning_face:

@dhess

Still, I really appreciate you taking the time to look into my question.

I’d love to have a cup of coffee as well, but it’s already 10 pm where I am.

Well, the brain cells started perking up and I think I’ve located your issue.

Try testing like this:
console.log(enforceArtistQuota(dedupeTracks(scoreTracks(playlists.flat())),1))

If you add console.log("in enforce",tracks[i]) just inside the outer loop, you should see why this is broken.

@dhess

Do you think I should rewrite enforceArtistQuota?

I moved the comparison logic around between the inner and outer loops.

It looks cleaner now, but the test still fails.

const enforceArtistQuota = (tracks, max) => {
  if (max < 1) {
    return 0;
  }
  for (let i = 0; i < tracks.length - 1; i++) {
    console.log(`in enforce, tracks[${i}]`);
    let occurrences = 0;
    if (occurrences < max) {
      for (let j = i + 1; j < tracks.length; j++) {
        if (tracks[i].artist === tracks[j].artist) {
          tracks.splice(j, 1);
          occurrences = occurrences + 1;
        }
      }
    }
  }
  return tracks;
};
 for (let i = 0; i<tracks.length -1 ; i++) {
    for (let j = i+1; j <tracks.length; j++) {

You’ve got this pattern going on in both your dedupe function and your enforceArtistQuota function.

With this pattern, your outer loop never looks at the last array item.

@dhess

I set it up this way on purpose.

My understanding is that the inner loop can still compare against the last object in the array, so the outer loop does not need to reach the final index.

For example, if the array length is 5, the indices range from 0 to 4.

When the outer loop index is 0:

tracks[0]

the inner loop checks:

tracks[1]
tracks[2]
tracks[3]
tracks[4]

When the outer loop index is 1:

tracks[1]

the inner loop checks:

tracks[2]
tracks[3]
tracks[4]

When the outer loop index is 2:

tracks[2]

the inner loop checks:

tracks[3]
tracks[4]

When the outer loop index is 3:

tracks[3]

the inner loop checks:

tracks[4]

So there’s no need for the outer loop index to reach 4.

I see it; and testing both ways, even after swapping tracks 201 and 202, still gives what should be expected. I just don’t get what the problem is. Going now to look at what GitHub is testing…

Do you think I should open an issue on freeCodeCamp’s GitHub repo?

No. There’s an error in the browser’s console after running the tests:
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

Edit: I found this about that error:

> The “TypeError: ‘X’ is not iterable” occurs when using the for…of loop with a right-hand side value that is not iterable, such as an object. To solve the error, use the Object.keys() or Object.values() methods to get an array with which you can use the for…of loop.

Try using a simple for loop rather than a for..of loop in your scoreTracks function.

you are creating a shallow copy here,

this is not returning a new array, it is creating issues down the line

this is also not a deep copy, but a shallow copy, that can also give issues

make sure you always return a new array as requested by the user stories

I absolutely do not understand why, but removing this bit of code without changing anything else, passes the tests:

:person_shrugging:

@dhess
Wow, thanks for digging into that strange issue.

For the first time, all the tests passed after I removed the code you mentioned, even without using a deep copy.

That was really interesting to notice.

@ILM
Then I tried rewriting the original codebase with a deep copy, and the tests still passed whether I kept or removed the code you pointed out.

const playlists = [
  [
    {
      trackId: "trk101",
      artist: "Velvet Comet",
      title: "Crimson Afterglow",
      votes: 5,
      bpm: 122,
    },
    {
      trackId: "trk102",
      artist: "Neon Harbor",
      title: "Static Horizon",
      votes: 2,
      bpm: 108,
    },
    {
      trackId: "trk103",
      artist: "Lunar Arcade",
      title: "Midnight Frequency",
      votes: 4,
      bpm: 128,
    },
  ],
  [
    {
      trackId: "trk201",
      artist: "Solar Echo",
      title: "Glass Skyline",
      votes: 3,
      bpm: 115,
    },
    {
      trackId: "trk202",
      artist: "Velvet Comet",
      title: "Satellite Hearts",
      votes: 6,
      bpm: 124,
    },
  ],
];

const flattenPlaylists = (lists) => {
  // input is not an array
  if (!Array.isArray(lists)) {
    return [];
  }

  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(lists));
  // input is an array containing arrays.
  // each array containing objects.
  // i is playlist index.
  for (let i = 0; i < deepcopy.length; i++) {
    // j is track index.
    for (let j = 0; j < deepcopy[i].length; j++) {
      if (
        JSON.stringify(Object.keys(deepcopy[i][j])) !==
        JSON.stringify(["trackId", "artist", "title", "votes", "bpm"])
      ) {
        return;
      }
      // each object has trackId, artist, title, votes, bpm, and source propertie
      deepcopy[i][j]["source"] = [i, j];
    }
  }

  // lists.flat() is an array containing objects.
  return deepcopy.flat();
};

const scoreTracks = (tracks) => {
  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(tracks));
  for (const track of deepcopy) {
    track.score = track.votes * 10 - Math.abs(track.bpm - 120);
  }
  return deepcopy;
};

const dedupeTracks = (tracks) => {
  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(tracks));
  for (let i = 0; i < deepcopy.length - 1; i++) {
    for (let j = i + 1; j < deepcopy.length; j++) {
      if (deepcopy[i].trackId === deepcopy[j].trackId) {
        deepcopy.splice(j, 1);
      }
    }
  }
  return deepcopy;
};

const enforceArtistQuota = (tracks, max) => {
  if (max < 1) {
    return 0;
  }
  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(tracks));

  for (let i = 0; i < deepcopy.length - 1; i++) {
    // console.log(`in enforce, tracks[${i}]`);
    let occurrences = 0;
    if (occurrences < max) {
      for (let j = i + 1; j < deepcopy.length; j++) {
        if (deepcopy[i].artist === deepcopy[j].artist) {
          deepcopy.splice(j, 1);
          occurrences = occurrences + 1;
        }
      }
    }
  }
  return deepcopy;
};

const buildSchedule = (tracks) => {
  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(tracks));
  let schedule = [];
  for (let i = 0; i < deepcopy.length; i++) {
    schedule.push({ slot: i + 1, trackId: deepcopy[i].trackId });
  }
  return schedule;
};

const remixPlaylist = (lists, max) => {
  // deep copy is implemented
  let deepcopy = JSON.parse(JSON.stringify(lists));
  let tracks = [];

  const remix = buildSchedule(
    enforceArtistQuota(
      dedupeTracks(scoreTracks(flattenPlaylists(deepcopy))),
      max,
    ),
  );

  return remix;
};

@dhess
@ILM

The code I posted here passes all the tests, but enforceArtistQuota still has some underlying issues that Gemini identified and I verified myself.