Co-authored-by: Dario <105294544+Dario-DC@users.noreply.github.com> Co-authored-by: Ilenia M <nethleen@gmail.com> Co-authored-by: majestic-owl448 <26656284+majestic-owl448@users.noreply.github.com> Co-authored-by: Jessica Wilkins <67210629+jdwilkin4@users.noreply.github.com> Co-authored-by: jdwilkin4 <jwilkin4@hotmail.com>
11 KiB
id, title, challengeType, dashedName
| id | title | challengeType | dashedName |
|---|---|---|---|
| 6976db7ecfa770bf21307b20 | Build a Playlist Remix Engine | 26 | build-a-playlist-remix-engine |
--description--
In this lab, you will build a program that creates a single remix playlist from multiple playlists submitted by listeners.
Each listener provides a list of songs they want to hear. Some songs may appear more than once, and some artists may show up too many times. Your job is to work through these playlists step by step: combine them into one list, score each song, remove duplicate songs, limit how often the same artist appears, and then create a final play order.
Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.
User Stories:
-
You should create a function named
flattenPlayliststhat accepts an array of playlists where each playlist is an array of objects with the following properties:trackId,artist,title,votes,bpm. If the input is not an array,flattenPlaylistsshould return an empty array. An example playlist has been provided for you. You can use this example to test out your function. -
flattenPlaylistsshould return a flat array of track objects, where each object includes all the original track properties plus asourceproperty set to an array with the playlist index and the track index indicating where the track originated. -
You should create a function named
scoreTracksthat accepts an array of track objects as returned byflattenPlaylists(each withtrackId,artist,title,votes,bpm, andsourceproperties) and returns a new array of track objects, each with ascoreproperty added using the formula:votes * 10 - Math.abs(bpm - 120). -
You should create a function named
dedupeTracksthat accepts an array of track objects as returned byscoreTracksand returns a new array with duplicatetrackIdentries removed, keeping only the first occurrence of each. -
You should create a function named
enforceArtistQuotathat accepts an array of track objects as returned bydedupeTracksand a number representing the maximum allowed occurrences per artist. The function should return a new array where no artist appears more times than the given number, keeping the earliest occurrences. -
You should create a function named
buildSchedulethat accepts an array of track objects as returned byenforceArtistQuotaand returns a new array of{ slot, trackId }objects, whereslotis a 1-based index representing each track's position in the broadcast order. -
You should create a function named
remixPlaylistthat accepts an array of playlists and the maximum number of allowed occurrences per artist. The function should return the final broadcast schedule as an array of{ slot, trackId }objects, by callingflattenPlaylists,scoreTracks,dedupeTracks,enforceArtistQuota, andbuildSchedulein order.
--hints--
You should have a function named flattenPlaylists.
assert.isFunction(flattenPlaylists);
You should return an empty array from flattenPlaylists when the input is not an array.
assert.deepEqual(flattenPlaylists(null), []);
assert.deepEqual(flattenPlaylists(undefined), []);
assert.deepEqual(flattenPlaylists({}), []);
assert.deepEqual(flattenPlaylists("not an array"), []);
assert.deepEqual(flattenPlaylists(123), []);
Each track returned by flattenPlaylists should include a source field that is an array containing the playlist index and the track index.
const playlists = [
[
{ trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120 },
{ trackId: "t2", artist: "B", title: "Song 2", votes: 1, bpm: 110 }
],
[{ trackId: "t3", artist: "C", title: "Song 3", votes: 5, bpm: 130 }]
];
const flat = flattenPlaylists(playlists);
assert.isArray(flat);
assert.lengthOf(flat, 3);
flat.forEach((t) => {
assert.property(t, "source");
assert.isArray(t.source);
assert.lengthOf(t.source, 2);
assert.isNumber(t.source[0]);
assert.isNumber(t.source[1]);
});
assert.deepEqual(flat[0].source, [0, 0]);
assert.deepEqual(flat[1].source, [0, 1]);
assert.deepEqual(flat[2].source, [1, 0]);
You should have a function named scoreTracks.
assert.isFunction(scoreTracks)
Each track returned by scoreTracks should include a numeric score field.
const tracks = [
{ trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120, source: [0, 0] }
];
const scored = scoreTracks(tracks);
assert.isArray(scored);
assert.lengthOf(scored, 1);
assert.property(scored[0], "score");
assert.isNumber(scored[0].score);
You should calculate score using a target BPM of 120 and this formula: votes * 10 - Math.abs(bpm - 120).
const tracks = [
{ trackId: "t1", artist: "A", title: "Song 1", votes: 3, bpm: 120, source: [0, 0] }, // 30
{ trackId: "t2", artist: "B", title: "Song 2", votes: 1, bpm: 100, source: [0, 1] }, // -10
{ trackId: "t3", artist: "C", title: "Song 3", votes: 2, bpm: 135, source: [1, 0] } // 5
];
const formulaScored = scoreTracks(tracks);
const s1 = formulaScored.find(t => t.trackId === "t1").score;
const s2 = formulaScored.find(t => t.trackId === "t2").score;
const s3 = formulaScored.find(t => t.trackId === "t3").score;
assert.equal(s1, 30);
assert.equal(s2, -10);
assert.equal(s3, 5);
You should have a function named dedupeTracks.
assert.isFunction(dedupeTracks)
When duplicate trackId values exist, dedupeTracks should keep only the first occurrence of the track.
const dupes = [
{ trackId: "t1", artist: "A", title: "Song 1", votes: 1, bpm: 120, source: [0, 0], score: 10 },
{ trackId: "t2", artist: "B", title: "Song 2", votes: 2, bpm: 119, source: [0, 1], score: 19 },
{ trackId: "t1", artist: "A", title: "Song 3", votes: 9, bpm: 140, source: [1, 0], score: 70 }
];
const deduped = dedupeTracks(dupes);
assert.isArray(deduped);
assert.lengthOf(deduped, 2);
assert.equal(deduped[0].trackId, "t1");
assert.equal(deduped[0].title, "Song 1");
assert.equal(deduped[1].trackId, "t2");
assert.equal(deduped[1].title, "Song 2");
You should have a function named enforceArtistQuota.
assert.isFunction(enforceArtistQuota)
enforceArtistQuota should ensure no artist appears more than maxPerArtist times by removing extra tracks while keeping the earliest ones.
const quotaTracks = [
{ trackId: "t1", artist: "A", title: "Song 1", votes: 1, bpm: 120, source: [0, 0], score: 10 },
{ trackId: "t2", artist: "A", title: "Song 2", votes: 1, bpm: 121, source: [0, 1], score: 9 },
{ trackId: "t3", artist: "B", title: "Song 3", votes: 1, bpm: 118, source: [0, 2], score: 8 },
{ trackId: "t4", artist: "A", title: "Song 4", votes: 1, bpm: 110, source: [1, 0], score: 0 }
];
const limited = enforceArtistQuota(quotaTracks, 2);
let artistACount = 0;
for (const track of limited) {
if (track.artist === "A") {
artistACount++;
}
}
assert.isAtMost(artistACount, 2);
assert.notInclude(limited.map(t => t.trackId), "t4");
You should have a function named buildSchedule.
assert.isFunction(buildSchedule);
buildSchedule should return an array of objects with the shape { slot, trackId }, where slot starts at 1.
const scheduleInput = [{ trackId: "t1" }, { trackId: "t2" }, { trackId: "t3" }];
const schedule = buildSchedule(scheduleInput);
assert.lengthOf(schedule, 3);
schedule.forEach((item, index) => {
assert.hasAllKeys(item, ["slot", "trackId"]);
assert.equal(item.slot, index + 1);
assert.equal(item.trackId, scheduleInput[index].trackId);
});
You should have a function named remixPlaylist.
assert.isFunction(remixPlaylist)
remixPlaylist should call the helper functions in order to produce the final schedule.
assert.lengthOf(remixPlaylist, 2);
const testPlaylists = [
[
{ trackId: "t1", artist: "A", title: "Song 1", votes: 2, bpm: 120 },
{ trackId: "t1", artist: "A", title: "Duplicate", votes: 9, bpm: 140 }
],
[
{ trackId: "t2", artist: "A", title: "Song 2", votes: 1, bpm: 110 },
{ trackId: "t3", artist: "B", title: "Song 3", votes: 4, bpm: 118 }
]
];
const expected = buildSchedule(enforceArtistQuota(dedupeTracks(scoreTracks(flattenPlaylists(testPlaylists))),1));
const actual = remixPlaylist(testPlaylists, 1);
assert.deepEqual(actual, expected);
--seed--
--seed-contents--
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
}
]
];
--solutions--
function flattenPlaylists(playlists) {
if (!Array.isArray(playlists)) return [];
const result = [];
for (let i = 0; i < playlists.length; i++) {
const playlist = playlists[i];
if (!Array.isArray(playlist)) continue;
for (let j = 0; j < playlist.length; j++) {
const track = playlist[j];
result.push({
trackId: track.trackId,
artist: track.artist,
title: track.title,
votes: track.votes,
bpm: track.bpm,
source: [i, j]
});
}
}
return result;
}
function scoreTracks(tracks) {
const targetBpm = 120;
const result = [];
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
result.push({
trackId: track.trackId,
artist: track.artist,
title: track.title,
votes: track.votes,
bpm: track.bpm,
source: track.source,
score: (track.votes * 10) - Math.abs(track.bpm - targetBpm)
});
}
return result;
}
function dedupeTracks(tracks) {
const seenTrackIds = [];
const result = [];
for (let i = 0; i < tracks.length; i++) {
if (!seenTrackIds.includes(tracks[i].trackId)) {
seenTrackIds.push(tracks[i].trackId);
result.push(tracks[i]);
}
}
return result;
}
function enforceArtistQuota(tracks, maxPerArtist) {
const artists = [];
const artistCounts = [];
const result = [];
for (let i = 0; i < tracks.length; i++) {
const artist = tracks[i].artist;
const artistIndex = artists.indexOf(artist);
if (artistIndex === -1) {
artists.push(artist);
artistCounts.push(1);
result.push(tracks[i]);
} else if (artistCounts[artistIndex] < maxPerArtist) {
artistCounts[artistIndex]++;
result.push(tracks[i]);
}
}
return result;
}
function buildSchedule(tracks) {
const schedule = [];
for (let i = 0; i < tracks.length; i++) {
schedule.push({
slot: i + 1,
trackId: tracks[i].trackId
});
}
return schedule;
}
function remixPlaylist(playlists, maxPerArtist) {
const flattened = flattenPlaylists(playlists);
const scored = scoreTracks(flattened);
const deduped = dedupeTracks(scored);
const limited = enforceArtistQuota(deduped, maxPerArtist);
return buildSchedule(limited);
}