-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommon.mjs
More file actions
266 lines (227 loc) · 8.67 KB
/
Copy pathcommon.mjs
File metadata and controls
266 lines (227 loc) · 8.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import {resultsDiv} from './domelement.mjs';
import { getSong } from './data.mjs';
export function displayResult(questionId, questionText, answerText) {
// If there's no answer, we don't display the question at all.
if (!answerText) {
return;
}
// Create a definition list item for the question and answer
const dt = document.createElement('dt');
dt.setAttribute('id', questionId + '-question'); // Add ID for accessibility/testing
dt.textContent = questionText;
const dd = document.createElement('dd');
dd.setAttribute('id', questionId + '-answer'); // Add ID for accessibility/testing
dd.textContent = answerText;
// Append to the results display area
resultsDiv.appendChild(dt);
resultsDiv.appendChild(dd);
}
/**
* Calculates the most listened song or artist based on count or time.
* @param {Array<Object>} events - Filtered listen events.
* @param {string} type - 'song' or 'artist'.
* @param {boolean} byTime - True if calculating by play duration, false for count.
* @returns {string|null} The most listened song/artist and count/time, or null if no data.
*/
export function getMostListened(events, type, byTime) {
if (events.length === 0) {
return null;
}
const counts = new Map(); // Stores counts or total durations
events.forEach(event => {
const song = getSong(event.song_id);
if (!song) return; // Skip if song data is missing
let key;
let value = byTime ? song.duration_seconds : 1;
if (type === 'song') {
key = `${song.title} - ${song.artist}`;
} else if (type === 'artist') {
key = song.artist;
} else {
return null; // Invalid type
}
counts.set(key, (counts.get(key) || 0) + value);
});
if (counts.size === 0) {
return null;
}
// Find the item with the maximum count/duration
let mostListenedItem = null;
let maxCount = -1;
for (const [item, count] of counts.entries()) {
if (count > maxCount) {
maxCount = count;
mostListenedItem = item;
}
}
if (mostListenedItem === null) {
return null; // Should not happen if counts.size > 0
}
return `${mostListenedItem}`;
}
/**
* Filters listen events for Friday nights (5 PM Friday to 4 AM Saturday).
* @param {Array<Object>} events - The listen events for a user.
* @returns {Array<Object>} Filtered listen events.
*/
export function getFridayNightListens(events) {
return events.filter(event => {
const date = new Date(event.timestamp);
const dayOfWeek = date.getUTCDay(); // 0 for Sunday, 5 for Friday, 6 for Saturday
const hour = date.getUTCHours(); // UTC hour (0-23)
// Friday from 5 PM UTC (17) onwards
if (dayOfWeek === 5 && hour >= 17) {
return true;
}
// Saturday from midnight (0) to 4 AM UTC (exclusive of 4:00:00)
if (dayOfWeek === 6 && hour >= 0 && hour < 4) {
return true;
}
return false;
});
}
/**
* Calculates the song listened to the most times in a row.
* @param {Array<Object>} events - The listen events for a user.
* @returns {string|null} The song and its streak length, or null if no streak.
*/
export function getLongestStreakSong(events) {
if (events.length === 0) {
return null;
}
let maxStreak = 0;
let currentStreak = 0;
let longestStreakSongId = null;
let currentSongId = null;
let longestStreakSongs = []; // To handle ties
events.forEach(event => {
if (event.song_id === currentSongId) {
currentStreak++;
} else {
// Check if the previous streak was the longest
if (currentStreak > maxStreak) {
maxStreak = currentStreak;
longestStreakSongs = [{ song_id: currentSongId, count: maxStreak }];
} else if (currentStreak === maxStreak && maxStreak > 0) {
// If there's a tie, add to the list
longestStreakSongs.push({ song_id: currentSongId, count: maxStreak });
}
currentSongId = event.song_id;
currentStreak = 1; // Start new streak
}
});
// Check the last streak after loop finishes
if (currentStreak > maxStreak) {
maxStreak = currentStreak;
longestStreakSongs = [{ song_id: currentSongId, count: maxStreak }];
} else if (currentStreak === maxStreak && maxStreak > 0) {
longestStreakSongs.push({ song_id: currentSongId, count: maxStreak });
}
if (maxStreak === 0) { // No songs listened to or no streak
return null;
}
// Format the output for ties
if (longestStreakSongs.length > 1) {
const uniqueTitles = new Set();
const formattedTies = longestStreakSongs
.filter(item => {
const song = getSong(item.song_id);
const fullTitle = song ? `${song.title} - ${song.artist}` : 'Unknown Song';
if (!uniqueTitles.has(fullTitle)) {
uniqueTitles.add(fullTitle);
return true;
}
return false;
})
.map(item => {
const song = getSong(item.song_id);
return song ? `${song.title} - ${song.artist}` : 'Unknown Song';
});
return `Multiple songs tied with length ${maxStreak}: ${formattedTies.join(', ')}`;
} else if (longestStreakSongs.length === 1) {
const song = getSong(longestStreakSongs[0].song_id);
return song ? `${song.title} - ${song.artist} (length: ${maxStreak})` : 'Unknown Song (length: ${maxStreak})';
}
return null;
}
/**
* Calculates songs listened to on every day the user listened to music.
* @param {Array<Object>} events - The listen events for a user.
* @returns {string|null} The list of songs, or null if none.
*/
export function getEveryDaySongs(events) {
if (events.length === 0) {
return null;
}
const songsByDay = new Map(); // Map: 'YYYY-MM-DD' -> Set<songId>
const allDaysListened = new Set(); // Set of all unique days a user listened to music
events.forEach(event => {
const date = new Date(event.timestamp);
// Format date to 'YYYY-MM-DD' (UTC to avoid timezone issues with 'day')
const dayString = date.toISOString().split('T')[0];
if (!songsByDay.has(dayString)) {
songsByDay.set(dayString, new Set());
}
songsByDay.get(dayString).add(event.song_id);
allDaysListened.add(dayString);
});
if (allDaysListened.size === 0) {
return null;
}
const potentialEveryDaySongs = new Map(); // Map: songId -> count of days listened
// Populate potentialEveryDaySongs
for (const [day, songIds] of songsByDay.entries()) {
songIds.forEach(songId => {
potentialEveryDaySongs.set(songId, (potentialEveryDaySongs.get(songId) || 0) + 1);
});
}
const everyDaySongs = [];
for (const [songId, dayCount] of potentialEveryDaySongs.entries()) {
// If the song was listened to on as many days as the user listened to music in total
if (dayCount === allDaysListened.size) {
const song = getSong(songId);
if (song) {
everyDaySongs.push(`${song.title} - ${song.artist}`);
}
}
}
return everyDaySongs.length > 0 ? everyDaySongs.join(', ') : null;
}
/**
* Calculates the top genres by number of listens.
* @param {Array<Object>} events - The listen events for a user.
* @returns {string|null} A formatted string of top genres, or null if no genres.
*/
export function getTopGenres(events) {
if (events.length === 0) {
return null;
}
const genreCounts = new Map(); // Map: genre -> count
events.forEach(event => {
const song = getSong(event.song_id);
if (song && song.genre) {
genreCounts.set(song.genre, (genreCounts.get(song.genre) || 0) + 1);
}
});
if (genreCounts.size === 0) {
return null;
}
// Convert map to array, sort by count descending
const sortedGenres = Array.from(genreCounts.entries())
.sort((a, b) => b[1] - a[1]);
// Take top 3 or fewer if not available
const topGenres = sortedGenres.slice(0, 3).map(entry => entry[0]);
if (topGenres.length === 0) {
return null;
}
// Dynamically adjust the title based on the number of genres found
let title = "Top ";
if (topGenres.length === 1) {
title += "genre";
} else if (topGenres.length === 2) {
title += "2 genres";
} else { // topGenres.length === 3
title += "3 genres";
}
return `${title}: ${topGenres.join(', ')}`;
}