Skip to content

Instantly share code, notes, and snippets.

@ToadKing
Last active March 2, 2024 06:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ToadKing/fe6ed8c1117b8a76018847dbc72865d5 to your computer and use it in GitHub Desktop.
Save ToadKing/fe6ed8c1117b8a76018847dbc72865d5 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Relationship Combiner</title>
<style>
#output {
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Relationship Combiner</h1>
<select id="parseType">
<option value="perTrack">Per Track</option>
<option value="perDisc">Per Disc</option>
</select>
<br>
<textarea id="input" rows="24" cols="80"></textarea>
<div id="output"></div>
<script>
function parse() {
try {
const input = String(document.getElementById('input').value.replaceAll('\r\n', '\n').trim())
const parsed = {}
let maxMediumLength = 1
let maxTrackLength = 1
switch (document.getElementById('parseType').value) {
case 'perTrack': {
let curMedium = 0
let curTrack = 0
for (const line of input.split('\n')) {
let newTrack = line.match(/M[-.](?:(\d+)[-.])?(\d+)/i)
if (newTrack) {
curMedium = newTrack[1] ? Number(newTrack[1]) : 1
curTrack = Number(newTrack[2])
if (curMedium === 0 || curTrack === 0) {
throw new RangeError('medium or track should never be set to 0')
}
if (String(curMedium).length > maxMediumLength) {
maxMediumLength = String(curMedium).length
}
if (String(curTrack).length > maxTrackLength) {
maxTrackLength = String(curTrack).length
}
continue
}
if (!line.includes(':')) {
continue
}
if (curMedium === 0 || curTrack === 0) {
throw new RangeError('medium or track not set before first roles')
}
const [rawRoles, rawArtists] = line.split(':')
const roles = rawRoles.split(',').map((r) => r.trim())
const artists = rawArtists.split(',').map((r) => r.trim())
for (const role of roles) {
if (!parsed[role]) {
parsed[role] = {}
}
for (const artist of artists) {
if (!parsed[role][artist]) {
parsed[role][artist] = new Set()
}
parsed[role][artist].add([curMedium, curTrack])
}
}
}
break
}
case 'perDisc': {
let curMedium
let curRole
for (const rawline of input.split('\n')) {
const line = rawline.trim()
if (!line) {
continue
}
const newDisc = line.match(/^Disc (\d+)$/i)
if (newDisc) {
curMedium = Number(newDisc[1])
curRole = undefined
if (String(curMedium).length > maxMediumLength) {
maxMediumLength = String(curMedium).length
}
continue
}
const newRole = line.match(/^(.*?)(?: by:|:| by)$/i)
if (newRole) {
curRole = newRole[1].trim()
continue
}
const artistsAndTracks = line.match(/^(.*) \(([\d,-~ ]+)\)$/)
if (artistsAndTracks) {
const [, rawartists, rawtracks] = artistsAndTracks
const artists = rawartists.split(',').map(a => a.trim())
const tracks = rawtracks.split(',').map(t => {
const range = t.trim().match(/^(\d+)[-~](\d+)$/)
if (range) {
const [, rawfirst, rawlast] = range
const first = Number(rawfirst)
const last = Number(rawlast)
if (!(last > first)) {
throw new RangeError('range not start~end')
}
const nums = []
for (let i = first; i <= last; i++) {
nums.push(i)
}
if (String(last).length > maxTrackLength) {
maxTrackLength = String(last).length
}
return nums
} else {
const num = Number(t)
if (!num) {
throw new RangeError(`bad track number: ${t}`)
}
if (String(num).length > maxTrackLength) {
maxTrackLength = String(num).length
}
return [num]
}
}).flat().map(t => [curMedium, t])
if (!parsed[curRole]) {
parsed[curRole] = {}
}
for (const artist of artists) {
if (!parsed[curRole][artist]) {
parsed[curRole][artist] = new Set()
}
for (const track of tracks) {
parsed[curRole][artist].add(track)
}
}
continue
}
throw new RangeError(`could not parse line: ${line}`)
}
break
}
}
let out = ''
for (const [role, artistsAndTracks] of Object.entries(parsed)) {
out += `${role}:\n`
for (const [artist, unsortedTracks] of Object.entries(artistsAndTracks)) {
out += `${artist} (`
const tracks = [...unsortedTracks].sort(([am, at], [bm, bt]) => am - bm === 0 ? at - bt : am - bm)
const formattedTracks = []
for (let i = 0; i < tracks.length; i++) {
const [medium, track] = tracks[i]
// format three or more tracks sequentially like 'D.M~N'
if (i + 2 < tracks.length && tracks[i + 1][0] === medium && tracks[i + 1][1] === track + 1 && tracks[i + 2][0] === medium && tracks[i + 2][1] === track + 2) {
let endRange = track + 1
while (i + 1 < tracks.length && tracks[i + 1][0] === medium && tracks[i + 1][1] === endRange) {
i += 1
endRange = endRange + 1
}
formattedTracks.push(`${String(medium).padStart(maxMediumLength, '0')}.${String(track).padStart(maxTrackLength, '0')}~${String(endRange - 1).padStart(maxTrackLength, '0')}`)
} else {
formattedTracks.push(`${String(medium).padStart(maxMediumLength, '0')}.${String(track).padStart(maxTrackLength, '0')}`)
}
}
out += formattedTracks.join(', ')
out += ')\n'
}
out += '\n'
}
document.getElementById('output').textContent = out
} catch (e) {
document.getElementById('output').textContent = e
}
}
document.getElementById('input').addEventListener('input', parse)
document.getElementById('parseType').addEventListener('change', parse)
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment