diff options
61 files changed, 722 insertions, 746 deletions
diff --git a/package.json b/package.json index bc97e547..6b0d0d56 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "#replacer": "./src/util/replacer.js", "#serialize": "./src/data/serialize.js", "#sugar": "./src/util/sugar.js", + "#sort": "./src/util/sort.js", "#test-lib": "./test/lib/index.js", "#thing": "./src/data/thing.js", "#things": "./src/data/things/index.js", diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 7fbe4e23..5853f115 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,5 +1,5 @@ +import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; -import {sortAlbumsTracksChronologically} from '#wiki-data'; import getChronologyRelations from '../util/getChronologyRelations.js'; diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index 6ffb9351..5b2e3404 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {atOffset, stitchArrays} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js index 9e1b4cde..93ebf5d4 100644 --- a/src/content/dependencies/generateAlbumSidebarGroupBox.js +++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {atOffset, empty} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index 62b88bcf..962f1b7f 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,5 +1,5 @@ +import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortAlbumsTracksChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js index 11cf8cdf..13779159 100644 --- a/src/content/dependencies/generateArtistGalleryPage.js +++ b/src/content/dependencies/generateArtistGalleryPage.js @@ -1,5 +1,5 @@ +import {sortAlbumsTracksChronologically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortAlbumsTracksChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 36daf9c4..0beeb271 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -1,10 +1,5 @@ -import {stitchArrays} from '#sugar'; - -import { - chunkByProperties, - sortAlbumsTracksChronologically, - sortEntryThingPairs, -} from '#wiki-data'; +import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; +import {chunkByProperties, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js index 7c2418b1..0bcadc7c 100644 --- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js @@ -1,10 +1,5 @@ -import {stitchArrays} from '#sugar'; - -import { - chunkByProperties, - sortAlbumsTracksChronologically, - sortEntryThingPairs, -} from '#wiki-data'; +import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; +import {chunkByProperties, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js index 75a1d05a..88a97af2 100644 --- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js @@ -1,10 +1,5 @@ -import {stitchArrays} from '#sugar'; - -import { - chunkByProperties, - sortEntryThingPairs, - sortFlashesChronologically, -} from '#wiki-data'; +import {sortEntryThingPairs, sortFlashesChronologically} from '#sort'; +import {chunkByProperties, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index a8209165..d68eba3f 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,10 +1,5 @@ -import {accumulateSum, empty, stitchArrays} from '#sugar'; - -import { - chunkByProperties, - sortAlbumsTracksChronologically, - sortEntryThingPairs, -} from '#wiki-data'; +import {sortAlbumsTracksChronologically, sortEntryThingPairs} from '#sort'; +import {accumulateSum, chunkByProperties, empty, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js index 5d38941a..3c3504d2 100644 --- a/src/content/dependencies/generateCommentaryIndexPage.js +++ b/src/content/dependencies/generateCommentaryIndexPage.js @@ -1,5 +1,5 @@ -import {accumulateSum, stitchArrays} from '#sugar'; -import {filterMultipleArrays, sortChronologically} from '#wiki-data'; +import {sortChronologically} from '#sort'; +import {accumulateSum, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: ['generatePageLayout', 'linkAlbumCommentary'], diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js index 3c631d92..0bbfa1f8 100644 --- a/src/content/dependencies/generateFlashActSidebar.js +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -1,6 +1,5 @@ import find from '#find'; -import {stitchArrays} from '#sugar'; -import {filterMultipleArrays} from '#wiki-data'; +import {filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'], diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index 86e6c61a..dfd83aef 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -1,5 +1,5 @@ +import {sortByName} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortByName} from '#wiki-data'; export default { extraDependencies: [ diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 490dcf10..b29c586f 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -1,10 +1,6 @@ +import {sortChronologically} from '#sort'; import {empty, stitchArrays} from '#sugar'; - -import { - filterItemsForCarousel, - getTotalDuration, - sortChronologically, -} from '#wiki-data'; +import {filterItemsForCarousel, getTotalDuration} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js index fa4d68a8..bcba7194 100644 --- a/src/content/dependencies/generateNewsEntryPage.js +++ b/src/content/dependencies/generateNewsEntryPage.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {atOffset} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js index 64279d7d..539af804 100644 --- a/src/content/dependencies/generateNewsIndexPage.js +++ b/src/content/dependencies/generateNewsIndexPage.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 9cce744b..7b70d4ff 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,5 +1,6 @@ +import {sortAlbumsTracksChronologically, sortFlashesChronologically} + from '#sort'; import {empty, stitchArrays} from '#sugar'; -import {sortAlbumsTracksChronologically, sortFlashesChronologically} from '#wiki-data'; import getChronologyRelations from '../util/getChronologyRelations.js'; diff --git a/src/content/dependencies/listAlbumsByDate.js b/src/content/dependencies/listAlbumsByDate.js index a5e31a0b..c83ffc97 100644 --- a/src/content/dependencies/listAlbumsByDate.js +++ b/src/content/dependencies/listAlbumsByDate.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkAlbum'], diff --git a/src/content/dependencies/listAlbumsByDateAdded.js b/src/content/dependencies/listAlbumsByDateAdded.js index 75114a48..d462ad46 100644 --- a/src/content/dependencies/listAlbumsByDateAdded.js +++ b/src/content/dependencies/listAlbumsByDateAdded.js @@ -1,4 +1,5 @@ -import {chunkByProperties, sortAlphabetically} from '#wiki-data'; +import {sortAlphabetically} from '#sort'; +import {chunkByProperties} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum'], diff --git a/src/content/dependencies/listAlbumsByDuration.js b/src/content/dependencies/listAlbumsByDuration.js index 1f95f5e3..c60685ab 100644 --- a/src/content/dependencies/listAlbumsByDuration.js +++ b/src/content/dependencies/listAlbumsByDuration.js @@ -1,5 +1,6 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkAlbum'], diff --git a/src/content/dependencies/listAlbumsByName.js b/src/content/dependencies/listAlbumsByName.js index 287dc0be..21419537 100644 --- a/src/content/dependencies/listAlbumsByName.js +++ b/src/content/dependencies/listAlbumsByName.js @@ -1,5 +1,5 @@ +import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortAlphabetically} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkAlbum'], diff --git a/src/content/dependencies/listAlbumsByTracks.js b/src/content/dependencies/listAlbumsByTracks.js index abf3c3ff..798e6c2e 100644 --- a/src/content/dependencies/listAlbumsByTracks.js +++ b/src/content/dependencies/listAlbumsByTracks.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum'], diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js index 627fdab4..bf48c966 100644 --- a/src/content/dependencies/listAllAdditionalFilesTemplate.js +++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js @@ -1,5 +1,5 @@ -import {empty, stitchArrays} from '#sugar'; -import {filterMultipleArrays, sortChronologically} from '#wiki-data'; +import {sortChronologically} from '#sort'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/listArtistsByCommentaryEntries.js b/src/content/dependencies/listArtistsByCommentaryEntries.js index aac3cfd1..eff2dba3 100644 --- a/src/content/dependencies/listArtistsByCommentaryEntries.js +++ b/src/content/dependencies/listArtistsByCommentaryEntries.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js index 61141155..0af586cd 100644 --- a/src/content/dependencies/listArtistsByContributions.js +++ b/src/content/dependencies/listArtistsByContributions.js @@ -1,11 +1,6 @@ -import {empty, stitchArrays, unique} from '#sugar'; - -import { - filterByCount, - filterMultipleArrays, - sortAlphabetically, - sortByCount, -} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {empty, filterByCount, filterMultipleArrays, stitchArrays, unique} + from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js index 056b126c..f677d82c 100644 --- a/src/content/dependencies/listArtistsByDuration.js +++ b/src/content/dependencies/listArtistsByDuration.js @@ -1,5 +1,6 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist'], diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js index 69f910c7..30884d24 100644 --- a/src/content/dependencies/listArtistsByGroup.js +++ b/src/content/dependencies/listArtistsByGroup.js @@ -1,10 +1,6 @@ -import {empty, stitchArrays, unique} from '#sugar'; - -import { - filterMultipleArrays, - getArtistNumContributions, - sortAlphabetically, -} from '#wiki-data'; +import {sortAlphabetically} from '#sort'; +import {empty, filterMultipleArrays, stitchArrays, unique} from '#sugar'; +import {getArtistNumContributions} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index 03c51699..0f709577 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -1,13 +1,12 @@ -import {empty, stitchArrays} from '#sugar'; +import {chunkMultipleArrays, empty, sortMultipleArrays, stitchArrays} + from '#sugar'; import T from '#things'; import { - chunkMultipleArrays, sortAlphabetically, sortAlbumsTracksChronologically, sortFlashesChronologically, - sortMultipleArrays, -} from '#wiki-data'; +} from '#sort'; const {Album, Flash} = T; diff --git a/src/content/dependencies/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js index 7704e97b..93218492 100644 --- a/src/content/dependencies/listArtistsByName.js +++ b/src/content/dependencies/listArtistsByName.js @@ -1,5 +1,6 @@ +import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; -import {getArtistNumContributions, sortAlphabetically} from '#wiki-data'; +import {getArtistNumContributions} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'], diff --git a/src/content/dependencies/listGroupsByAlbums.js b/src/content/dependencies/listGroupsByAlbums.js index 063b8269..4adfb6d9 100644 --- a/src/content/dependencies/listGroupsByAlbums.js +++ b/src/content/dependencies/listGroupsByAlbums.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkGroup'], diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js index e2a023e6..da2f26db 100644 --- a/src/content/dependencies/listGroupsByDuration.js +++ b/src/content/dependencies/listGroupsByDuration.js @@ -1,5 +1,6 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, getTotalDuration, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; +import {getTotalDuration} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkGroup'], diff --git a/src/content/dependencies/listGroupsByLatestAlbum.js b/src/content/dependencies/listGroupsByLatestAlbum.js index fa223664..48319314 100644 --- a/src/content/dependencies/listGroupsByLatestAlbum.js +++ b/src/content/dependencies/listGroupsByLatestAlbum.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {compareDates, filterMultipleArrays, sortChronologically, sortMultipleArrays} from '#wiki-data'; +import {compareDates, sortChronologically} from '#sort'; +import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: [ diff --git a/src/content/dependencies/listGroupsByName.js b/src/content/dependencies/listGroupsByName.js index 8f0c424d..696a49bd 100644 --- a/src/content/dependencies/listGroupsByName.js +++ b/src/content/dependencies/listGroupsByName.js @@ -1,5 +1,5 @@ +import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortAlphabetically} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'], diff --git a/src/content/dependencies/listGroupsByTracks.js b/src/content/dependencies/listGroupsByTracks.js index b3c55ca2..0b5e4e97 100644 --- a/src/content/dependencies/listGroupsByTracks.js +++ b/src/content/dependencies/listGroupsByTracks.js @@ -1,5 +1,5 @@ -import {accumulateSum, stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {accumulateSum, filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkGroup'], diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js index 18585696..ab2eca93 100644 --- a/src/content/dependencies/listRandomPageLinks.js +++ b/src/content/dependencies/listRandomPageLinks.js @@ -1,5 +1,5 @@ +import {sortChronologically} from '#sort'; import {empty} from '#sugar'; -import {sortChronologically} from '#wiki-data'; export default { contentDependencies: [ diff --git a/src/content/dependencies/listTagsByName.js b/src/content/dependencies/listTagsByName.js index 8571ccd0..d7022a55 100644 --- a/src/content/dependencies/listTagsByName.js +++ b/src/content/dependencies/listTagsByName.js @@ -1,5 +1,5 @@ +import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; -import {sortAlphabetically} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkArtTag'], diff --git a/src/content/dependencies/listTagsByUses.js b/src/content/dependencies/listTagsByUses.js index 98a50b89..00c700a5 100644 --- a/src/content/dependencies/listTagsByUses.js +++ b/src/content/dependencies/listTagsByUses.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkArtTag'], diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js index 25beb739..01ce4e2d 100644 --- a/src/content/dependencies/listTracksByDate.js +++ b/src/content/dependencies/listTracksByDate.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {chunkByProperties, sortAlbumsTracksChronologically} from '#wiki-data'; +import {sortAlbumsTracksChronologically} from '#sort'; +import {chunkByProperties, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], diff --git a/src/content/dependencies/listTracksByDuration.js b/src/content/dependencies/listTracksByDuration.js index bff9bd45..64feb4f1 100644 --- a/src/content/dependencies/listTracksByDuration.js +++ b/src/content/dependencies/listTracksByDuration.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkTrack'], diff --git a/src/content/dependencies/listTracksByDurationInAlbum.js b/src/content/dependencies/listTracksByDurationInAlbum.js index 4e83e921..c1ea32a1 100644 --- a/src/content/dependencies/listTracksByDurationInAlbum.js +++ b/src/content/dependencies/listTracksByDurationInAlbum.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, filterMultipleArrays, sortByCount, sortChronologically} from '#wiki-data'; +import {sortByCount, sortChronologically} from '#sort'; +import {filterByCount, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], diff --git a/src/content/dependencies/listTracksByName.js b/src/content/dependencies/listTracksByName.js index caf6886f..773b0473 100644 --- a/src/content/dependencies/listTracksByName.js +++ b/src/content/dependencies/listTracksByName.js @@ -1,4 +1,4 @@ -import {sortAlphabetically} from '#wiki-data'; +import {sortAlphabetically} from '#sort'; export default { contentDependencies: ['generateListingPage', 'linkTrack'], diff --git a/src/content/dependencies/listTracksByTimesReferenced.js b/src/content/dependencies/listTracksByTimesReferenced.js index 15a3461d..5838ded0 100644 --- a/src/content/dependencies/listTracksByTimesReferenced.js +++ b/src/content/dependencies/listTracksByTimesReferenced.js @@ -1,5 +1,5 @@ -import {stitchArrays} from '#sugar'; -import {filterByCount, sortAlbumsTracksChronologically, sortByCount} from '#wiki-data'; +import {sortAlbumsTracksChronologically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkTrack'], diff --git a/src/content/dependencies/listTracksInFlashesByAlbum.js b/src/content/dependencies/listTracksInFlashesByAlbum.js index 53ceb0ee..8ca0d993 100644 --- a/src/content/dependencies/listTracksInFlashesByAlbum.js +++ b/src/content/dependencies/listTracksInFlashesByAlbum.js @@ -1,5 +1,5 @@ -import {empty, stitchArrays} from '#sugar'; -import {filterMultipleArrays, sortChronologically} from '#wiki-data'; +import {sortChronologically} from '#sort'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], diff --git a/src/content/dependencies/listTracksInFlashesByFlash.js b/src/content/dependencies/listTracksInFlashesByFlash.js index c80d5822..6ab954ed 100644 --- a/src/content/dependencies/listTracksInFlashesByFlash.js +++ b/src/content/dependencies/listTracksInFlashesByFlash.js @@ -1,5 +1,5 @@ +import {sortFlashesChronologically} from '#sort'; import {empty, stitchArrays} from '#sugar'; -import {sortFlashesChronologically} from '#wiki-data'; export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'], diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js index c9f80f35..c7f42f9d 100644 --- a/src/content/dependencies/listTracksWithExtra.js +++ b/src/content/dependencies/listTracksWithExtra.js @@ -1,5 +1,5 @@ -import {empty, stitchArrays} from '#sugar'; -import {filterMultipleArrays, sortChronologically} from '#wiki-data'; +import {sortChronologically} from '#sort'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; export default { contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'], diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js index 679a09fd..0a1ebebc 100644 --- a/src/data/composite/things/album/withTrackSections.js +++ b/src/data/composite/things/album/withTrackSections.js @@ -1,8 +1,7 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; -import {empty, stitchArrays} from '#sugar'; +import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; import {isTrackSectionList} from '#validators'; -import {filterMultipleArrays} from '#wiki-data'; import {exitWithoutDependency, exitWithoutUpdateValue} from '#composite/control-flow'; diff --git a/src/data/composite/things/track/inferredAdditionalNameList.js b/src/data/composite/things/track/inferredAdditionalNameList.js index 9cf158c6..58e8d2a1 100644 --- a/src/data/composite/things/track/inferredAdditionalNameList.js +++ b/src/data/composite/things/track/inferredAdditionalNameList.js @@ -4,7 +4,7 @@ // shares the same name differing from this one's. import {input, templateCompositeFrom} from '#composite'; -import {chunkByProperties} from '#wiki-data'; +import {chunkByProperties} from '#sugar'; import {exitWithoutDependency} from '#composite/control-flow'; import {withFilteredList, withPropertyFromList} from '#composite/data'; diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index eda24160..77b0f96d 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -5,9 +5,8 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; -import {stitchArrays} from '#sugar'; +import {filterMultipleArrays, stitchArrays} from '#sugar'; import {is, isContributionList} from '#validators'; -import {filterMultipleArrays} from '#wiki-data'; import { raiseOutputWithoutDependency, diff --git a/src/data/composite/wiki-data/withThingsSortedAlphabetically.js b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js index d2487e42..5e85fa6a 100644 --- a/src/data/composite/wiki-data/withThingsSortedAlphabetically.js +++ b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js @@ -5,8 +5,8 @@ // a list so same-name entries are beside each other. import {input, templateCompositeFrom} from '#composite'; +import {compareCaseLessSensitive, normalizeName} from '#sort'; import {validateWikiData} from '#validators'; -import {compareCaseLessSensitive, normalizeName} from '#wiki-data'; import {raiseOutputWithoutDependency} from '#composite/control-flow'; import {withMappedList, withSortedList, withPropertiesFromList} diff --git a/src/data/things/album.js b/src/data/things/album.js index d92dd434..01a232d0 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -5,11 +5,10 @@ import * as path from 'node:path'; import {input} from '#composite'; import find from '#find'; import {traverse} from '#node-utils'; +import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; import {empty} from '#sugar'; import Thing from '#thing'; import {isDate} from '#validators'; -import {sortAlbumsTracksChronologically, sortChronologically} - from '#wiki-data'; import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions} from '#yaml'; diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 69fbb526..3149b310 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,9 +1,9 @@ export const ART_TAG_DATA_FILE = 'tags.yaml'; import {input} from '#composite'; +import {sortAlphabetically, sortAlbumsTracksChronologically} from '#sort'; import Thing from '#thing'; import {isName} from '#validators'; -import {sortAlphabetically, sortAlbumsTracksChronologically} from '#wiki-data'; import {exposeUpdateValueOrContinue} from '#composite/control-flow'; diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 6e61c28f..73acba69 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -6,10 +6,11 @@ import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; import find from '#find'; +import {sortAlphabetically} from '#sort'; import {stitchArrays, unique} from '#sugar'; import Thing from '#thing'; import {isName, validateArrayItems} from '#validators'; -import {getKebabCase, sortAlphabetically} from '#wiki-data'; +import {getKebabCase} from '#wiki-data'; import {withReverseContributionList} from '#composite/wiki-data'; diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 4823f723..81de3272 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -2,9 +2,9 @@ export const FLASH_DATA_FILE = 'flashes.yaml'; import {input} from '#composite'; import find from '#find'; +import {sortFlashesChronologically} from '#sort'; import Thing from '#thing'; import {anyOf, isColor, isDirectory, isNumber, isString} from '#validators'; -import {sortFlashesChronologically} from '#wiki-data'; import {parseDate, parseContributors} from '#yaml'; import {exposeDependency, exposeUpdateValueOrContinue} diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index cb8e3648..43d1638e 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,7 +1,7 @@ export const NEWS_DATA_FILE = 'news.yaml'; +import {sortChronologically} from '#sort'; import Thing from '#thing'; -import {sortChronologically} from '#wiki-data'; import {parseDate} from '#yaml'; import {contentString, directory, name, simpleDate} diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 69cbfa11..03274979 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -3,9 +3,9 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; import * as path from 'node:path'; import {traverse} from '#node-utils'; +import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {isName} from '#validators'; -import {sortAlphabetically} from '#wiki-data'; import {contentString, directory, name, simpleString} from '#composite/wiki-properties'; diff --git a/src/data/yaml.js b/src/data/yaml.js index 100e07b9..0f73bdfb 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -10,11 +10,11 @@ import yaml from 'js-yaml'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import {reportDuplicateDirectories, filterReferenceErrors} from '#data-checks'; +import {sortByName} from '#sort'; import {atOffset, empty, filterProperties, typeAppearance, withEntries} from '#sugar'; import Thing from '#thing'; import thingConstructors from '#things'; -import {sortByName} from '#wiki-data'; import { annotateErrorWithFile, diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index cecedc82..8a582693 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -156,8 +156,8 @@ import { import dimensionsOf from 'image-size'; import CacheableObject from '#cacheable-object'; -import {delay, empty, queue, stitchArrays, unique} from '#sugar'; -import {chunkMultipleArrays, filterMultipleArrays, sortByName} from '#wiki-data'; +import {commandExists, isMain, promisifyProcess, traverse} from '#node-utils'; +import {sortByName} from '#sort'; import { colors, @@ -171,11 +171,14 @@ import { } from '#cli'; import { - commandExists, - isMain, - promisifyProcess, - traverse, -} from '#node-utils'; + delay, + empty, + chunkMultipleArrays, + filterMultipleArrays, + queue, + stitchArrays, + unique, +} from '#sugar'; export const defaultMagickThreads = 8; diff --git a/src/upd8.js b/src/upd8.js index 13c625a3..790b73e9 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -47,9 +47,9 @@ import {bindFind, getAllFindSpecs} from '#find'; import {processLanguageFile, watchLanguageFile, internalDefaultStringsFile} from '#language'; import {isMain, traverse} from '#node-utils'; +import {sortByName} from '#sort'; import {empty, withEntries} from '#sugar'; import {generateURLs, urlSpec} from '#urls'; -import {sortByName} from '#wiki-data'; import {linkWikiDataArrays, loadAndProcessDataDocuments, sortWikiDataArrays} from '#yaml'; diff --git a/src/util/sort.js b/src/util/sort.js new file mode 100644 index 00000000..b3a90812 --- /dev/null +++ b/src/util/sort.js @@ -0,0 +1,405 @@ +// Sorting functions - all utils here are mutating, so make sure to initially +// slice/filter/somehow generate a new array from input data if retaining the +// initial sort matters! (Spoilers: If what you're doing involves any kind of +// parallelization, it definitely matters.) + +import {empty, sortMultipleArrays, unique} + from './sugar.js'; + +// General sorting utilities! These don't do any sorting on their own but are +// handy in the sorting functions below (or if you're making your own sort). + +export function compareCaseLessSensitive(a, b) { + // Compare two strings without considering capitalization... unless they + // happen to be the same that way. + + const al = a.toLowerCase(); + const bl = b.toLowerCase(); + + return al === bl + ? a.localeCompare(b, undefined, {numeric: true}) + : al.localeCompare(bl, undefined, {numeric: true}); +} + +// Subtract common prefixes and other characters which some people don't like +// to have considered while sorting. The words part of this is English-only for +// now, which is totally evil. +export function normalizeName(s) { + // Turn (some) ligatures into expanded variant for cleaner sorting, e.g. + // "ff" into "ff", in decompose mode, so that "ü" is represented as two + // bytes ("u" + \u0308 combining diaeresis). + s = s.normalize('NFKD'); + + // Replace one or more whitespace of any kind in a row, as well as certain + // punctuation, with a single typical space, then trim the ends. + s = s + .replace( + /[\p{Separator}\p{Dash_Punctuation}\p{Connector_Punctuation}]+/gu, + ' ' + ) + .trim(); + + // Discard anything that isn't a letter, number, or space. + s = s.replace(/[^\p{Letter}\p{Number} ]/gu, '').trim(); + + // Remove common English (only, for now) prefixes. + s = s.replace(/^(?:an?|the) /i, ''); + + return s; +} + +// Component sort functions - these sort by one particular property, applying +// unique particulars where appropriate. Usually you don't want to use these +// directly, but if you're making a custom sort they can come in handy. + +// Universal method for sorting things into a predictable order, as directory +// is taken to be unique. There are two exceptions where this function (and +// thus any of the composite functions that start with it) *can't* be taken as +// deterministic: +// +// 1) Mixed data of two different Things, as directories are only taken as +// unique within one given class of Things. For example, this function +// won't be deterministic if its array contains both <album:ithaca> and +// <track:ithaca>. +// +// 2) Duplicate directories, or multiple instances of the "same" Thing. +// This function doesn't differentiate between two objects of the same +// directory, regardless of any other properties or the overall "identity" +// of the object. +// +// These exceptions are unavoidable except for not providing that kind of data +// in the first place, but you can still ensure the overall program output is +// deterministic by ensuring the input is arbitrarily sorted according to some +// other criteria - ex, although sortByDirectory itself isn't determinstic when +// given mixed track and album data, the final output (what goes on the site) +// will always be the same if you're doing sortByDirectory([...albumData, +// ...trackData]), because the initial sort places albums before tracks - and +// sortByDirectory will handle the rest, given all directories are unique +// except when album and track directories overlap with each other. +export function sortByDirectory(data, { + getDirectory = object => object.directory, +} = {}) { + const directories = data.map(getDirectory); + + sortMultipleArrays(data, directories, + (a, b, directoryA, directoryB) => + compareCaseLessSensitive(directoryA, directoryB)); + + return data; +} + +export function sortByName(data, { + getName = object => object.name, +} = {}) { + const names = data.map(getName); + const normalizedNames = names.map(normalizeName); + + sortMultipleArrays(data, normalizedNames, names, + ( + a, b, + normalizedA, normalizedB, + nonNormalizedA, nonNormalizedB, + ) => + compareNormalizedNames( + normalizedA, normalizedB, + nonNormalizedA, nonNormalizedB, + )); + + return data; +} + +export function compareNormalizedNames( + normalizedA, normalizedB, + nonNormalizedA, nonNormalizedB, +) { + const comparison = compareCaseLessSensitive(normalizedA, normalizedB); + return ( + (comparison === 0 + ? compareCaseLessSensitive(nonNormalizedA, nonNormalizedB) + : comparison)); +} + +export function sortByDate(data, { + getDate = object => object.date, + latestFirst = false, +} = {}) { + const dates = data.map(getDate); + + sortMultipleArrays(data, dates, + (a, b, dateA, dateB) => + compareDates(dateA, dateB, {latestFirst})); + + return data; +} + +export function compareDates(a, b, { + latestFirst = false, +} = {}) { + if (a && b) { + return (latestFirst ? b - a : a - b); + } + + // It's possible for objects with and without dates to be mixed + // together in the same array. If that's the case, we put all items + // without dates at the end. + if (a) return -1; + if (b) return 1; + + // If neither of the items being compared have a date, don't move + // them relative to each other. This is basically the same as + // filtering out all non-date items and then pushing them at the + // end after sorting the rest. + return 0; +} + +export function getLatestDate(dates) { + const filtered = dates.filter(Boolean); + if (empty(filtered)) return null; + + return filtered + .reduce( + (accumulator, date) => + date > accumulator ? date : accumulator, + -Infinity); +} + +export function getEarliestDate(dates) { + const filtered = dates.filter(Boolean); + if (empty(filtered)) return null; + + return filtered + .reduce( + (accumulator, date) => + date < accumulator ? date : accumulator, + Infinity); +} + +// Funky sort which takes a data set and a corresponding list of "counts", +// which are really arbitrary numbers representing some property of each data +// object defined by the caller. It sorts and mutates *both* of these, so the +// sorted data will still correspond to the same indexed count. +export function sortByCount(data, counts, { + greatestFirst = false, +} = {}) { + sortMultipleArrays(data, counts, (data1, data2, count1, count2) => + (greatestFirst + ? count2 - count1 + : count1 - count2)); + + return data; +} + +export function sortByPositionInParent(data, { + getParent, + getChildren, +}) { + return data.sort((a, b) => { + const parentA = getParent(a); + const parentB = getParent(b); + + // Don't change the sort when the two items are from separate parents. + // This function doesn't change the order of parents or try to "merge" + // two separated chunks of items from the same parent together. + if (parentA !== parentB) { + return 0; + } + + // Don't change the sort when either (or both) of the items doesn't + // even have a parent (e.g. it's the passed data is a mixed array of + // children and parents). + if (!parentA || !parentB) { + return 0; + } + + const indexA = getChildren(parentA).indexOf(a); + const indexB = getChildren(parentB).indexOf(b); + + // If the getParent/getChildren relationship doesn't go both ways for + // some reason, don't change the sort. + if (indexA === -1 || indexB === -1) { + return 0; + } + + return indexA - indexB; + }); +} + +export function sortByPositionInAlbum(data) { + return sortByPositionInParent(data, { + getParent: track => track.album, + getChildren: album => album.tracks, + }); +} + +export function sortByPositionInFlashAct(data) { + return sortByPositionInParent(data, { + getParent: flash => flash.act, + getChildren: act => act.flashes, + }); +} + +// Sorts data so that items are grouped together according to whichever of a +// set of arbitrary given conditions is true first. If no conditions are met +// for a given item, it's moved over to the end! +export function sortByConditions(data, conditions) { + return data.sort((a, b) => { + const ai = conditions.findIndex((f) => f(a)); + const bi = conditions.findIndex((f) => f(b)); + + if (ai >= 0 && bi >= 0) { + return ai - bi; + } else if (ai >= 0) { + return -1; + } else if (bi >= 0) { + return 1; + } else { + return 0; + } + }); +} + +// Composite sorting functions - these consider multiple properties, generally +// always returning the same output regardless of how the input was originally +// sorted (or left unsorted). If you're working with arbitrarily sorted inputs +// (typically wiki data, either in full or unsorted filter), these make sure +// what gets put on the actual website (or wherever) is deterministic. Also +// they're just handy sorting utilities. +// +// Note that because these are each comprised of multiple component sorting +// functions, they expect more than just one property to be present for full +// sorting (listed above each function). If you're mapping thing objects to +// another representation, try to include all of these listed properties. + +// Expects thing properties: +// * directory (or override getDirectory) +// * name (or override getName) +export function sortAlphabetically(data, { + getDirectory, + getName, +} = {}) { + sortByDirectory(data, {getDirectory}); + sortByName(data, {getName}); + return data; +} + +// Expects thing properties: +// * directory (or override getDirectory) +// * name (or override getName) +// * date (or override getDate) +export function sortChronologically(data, { + latestFirst = false, + getDirectory, + getName, + getDate, +} = {}) { + sortAlphabetically(data, {getDirectory, getName}); + sortByDate(data, {latestFirst, getDate}); + return data; +} + +// This one's a little odd! Sorts an array of {entry, thing} pairs using +// the provided sortFunction, which will operate on each item's `thing`, not +// its entry (or the item as a whole). If multiple entries are associated +// with the same thing, they'll end up bunched together in the output, +// retaining their original relative positioning. +export function sortEntryThingPairs(data, sortFunction) { + const things = unique(data.map(item => item.thing)); + sortFunction(things); + + const outputArrays = []; + const thingToOutputArray = new Map(); + + for (const thing of things) { + const array = []; + thingToOutputArray.set(thing, array); + outputArrays.push(array); + } + + for (const item of data) { + thingToOutputArray.get(item.thing).push(item); + } + + data.splice(0, data.length, ...outputArrays.flat()); + + return data; +} + +/* +// Alternate draft version of sortEntryThingPairs. +// See: https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607412168 + +// Maps the provided "preparation" function across a list of arbitrary values, +// building up a list of sortable values; sorts these with the provided sorting +// function; and reorders the sources to match their corresponding prepared +// values. As usual, if multiple source items correspond to the same sorting +// data, this retains the source relative positioning. +export function prepareAndSort(sources, prepareForSort, sortFunction) { + const prepared = []; + const preparedToSource = new Map(); + + for (const original of originals) { + const prep = prepareForSort(source); + prepared.push(prep); + preparedToSource.set(prep, source); + } + + sortFunction(prepared); + + sources.splice(0, ...sources.length, prepared.map(prep => preparedToSource.get(prep))); + + return sources; +} +*/ + +// Highly contextual sort functions - these are only for very specific types +// of Things, and have appropriately hard-coded behavior. + +// Sorts so that tracks from the same album are generally grouped together in +// their original (album track list) order, while prioritizing date (by default +// release date but can be overridden) above all else. +// +// This function also works for data lists which contain only tracks. +export function sortAlbumsTracksChronologically(data, { + latestFirst = false, + getDate, +} = {}) { + // Sort albums before tracks... + sortByConditions(data, [(t) => t.album === undefined]); + + // Group tracks by album... + sortByDirectory(data, { + getDirectory: (t) => (t.album ? t.album.directory : t.directory), + }); + + // Sort tracks by position in album... + sortByPositionInAlbum(data); + + // ...and finally sort by date. If tracks from more than one album were + // released on the same date, they'll still be grouped together by album, + // and tracks within an album will retain their relative positioning (i.e. + // stay in the same order as part of the album's track listing). + sortByDate(data, {latestFirst, getDate}); + + return data; +} + +export function sortFlashesChronologically(data, { + latestFirst = false, + getDate, +} = {}) { + // Group flashes by act... + sortByDirectory(data, { + getDirectory: flash => flash.act.directory, + }); + + // Sort flashes by position in act... + sortByPositionInFlashAct(data); + + // ...and finally sort by date. If flashes from more than one act were + // released on the same date, they'll still be grouped together by act, + // and flashes within an act will retain their relative positioning (i.e. + // stay in the same order as the act's flash listing). + sortByDate(data, {latestFirst, getDate}); + + return data; +} diff --git a/src/util/sugar.js b/src/util/sugar.js index 70749d8a..46c4879c 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -406,6 +406,219 @@ export function bindOpts(fn, bind) { bindOpts.bindIndex = Symbol(); +// Sorts multiple arrays by an arbitrary function (which is the last argument). +// Paired values from each array are provided to the callback sequentially: +// +// (a_fromFirstArray, b_fromFirstArray, +// a_fromSecondArray, b_fromSecondArray, +// a_fromThirdArray, b_fromThirdArray) => +// relative positioning (negative, positive, or zero) +// +// Like native single-array sort, this is a mutating function. +export function sortMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + const length = arrays[0].length; + const symbols = new Array(length).fill(null).map(() => Symbol()); + const indexes = Object.fromEntries(symbols.map((symbol, index) => [symbol, index])); + + symbols.sort((a, b) => { + const indexA = indexes[a]; + const indexB = indexes[b]; + + const args = []; + for (let i = 0; i < arrays.length; i++) { + args.push(arrays[i][indexA]); + args.push(arrays[i][indexB]); + } + + return fn(...args); + }); + + for (const array of arrays) { + // Note: We're mutating this array pulling values from itself, but only all + // at once after all those values have been pulled. + array.splice(0, array.length, ...symbols.map(symbol => array[indexes[symbol]])); + } + + return arrays; +} + +// Filters multiple arrays by an arbitrary function (which is the last argument). +// Values from each array are provided to the callback sequentially: +// +// (value_fromFirstArray, +// value_fromSecondArray, +// value_fromThirdArray, +// index, +// [firstArray, secondArray, thirdArray]) => +// true or false +// +// Please be aware that this is a mutating function, unlike native single-array +// filter. The mutated arrays are returned. Also attached under `.removed` are +// corresponding arrays of items filtered out. +export function filterMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + const removed = new Array(arrays.length).fill(null).map(() => []); + + for (let i = arrays[0].length - 1; i >= 0; i--) { + const args = arrays.map(array => array[i]); + args.push(i, arrays); + + if (!fn(...args)) { + for (let j = 0; j < arrays.length; j++) { + const item = arrays[j][i]; + arrays[j].splice(i, 1); + removed[j].unshift(item); + } + } + } + + Object.assign(arrays, {removed}); + return arrays; +} + +// Corresponding filter function for sortByCount. By default, items whose +// corresponding count is zero will be removed. +export function filterByCount(data, counts, { + min = 1, + max = Infinity, +} = {}) { + filterMultipleArrays(data, counts, (data, count) => + count >= min && count <= max); +} + +// Reduces multiple arrays with an arbitrary function (which is the last +// argument). Note that this reduces into multiple accumulators, one for +// each input array, not just a single value. That's reflected in both the +// callback parameters: +// +// (accumulator1, +// accumulator2, +// value_fromFirstArray, +// value_fromSecondArray, +// index, +// [firstArray, secondArray]) => +// [newAccumulator1, newAccumulator2] +// +// As well as the final return value of reduceMultipleArrays: +// +// [finalAccumulator1, finalAccumulator2] +// +// This is not a mutating function. +export function reduceMultipleArrays(...args) { + const [arrays, fn, initialAccumulators] = + (typeof args.at(-1) === 'function' + ? [args.slice(0, -1), args.at(-1), null] + : [args.slice(0, -2), args.at(-2), args.at(-1)]); + + if (empty(arrays[0])) { + throw new TypeError(`Reduce of empty arrays with no initial value`); + } + + let [accumulators, i] = + (initialAccumulators + ? [initialAccumulators, 0] + : [arrays.map(array => array[0]), 1]); + + for (; i < arrays[0].length; i++) { + const args = [...accumulators, ...arrays.map(array => array[i])]; + args.push(i, arrays); + accumulators = fn(...args); + } + + return accumulators; +} + +export function chunkByConditions(array, conditions) { + if (empty(array)) { + return []; + } + + if (empty(conditions)) { + return [array]; + } + + const out = []; + let cur = [array[0]]; + for (let i = 1; i < array.length; i++) { + const item = array[i]; + const prev = array[i - 1]; + let chunk = false; + for (const condition of conditions) { + if (condition(item, prev)) { + chunk = true; + break; + } + } + if (chunk) { + out.push(cur); + cur = [item]; + } else { + cur.push(item); + } + } + out.push(cur); + return out; +} + +export function chunkByProperties(array, properties) { + return chunkByConditions( + array, + properties.map((p) => (a, b) => { + if (a[p] instanceof Date && b[p] instanceof Date) return +a[p] !== +b[p]; + + if (a[p] !== b[p]) return true; + + // Not sure if this line is still necessary with the specific check for + // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....? + if (a[p] != b[p]) return true; + + return false; + }) + ).map((chunk) => ({ + ...Object.fromEntries(properties.map((p) => [p, chunk[0][p]])), + chunk, + })); +} + +export function chunkMultipleArrays(...args) { + const arrays = args.slice(0, -1); + const fn = args.at(-1); + + if (arrays[0].length === 0) { + return []; + } + + const newChunk = index => arrays.map(array => [array[index]]); + const results = [newChunk(0)]; + + for (let i = 1; i < arrays[0].length; i++) { + const current = results.at(-1); + + const args = []; + for (let j = 0; j < arrays.length; j++) { + const item = arrays[j][i]; + const previous = current[j].at(-1); + args.push(item, previous); + } + + if (fn(...args)) { + results.push(newChunk(i)); + continue; + } + + for (let j = 0; j < arrays.length; j++) { + current[j].push(arrays[j][i]); + } + } + + return results; +} + // Delicious function annotations, such as: // // (*bound) soWeAreBackInTheMine diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index cdbffffa..89c04a29 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -1,6 +1,7 @@ // Utility functions for interacting with wiki data. -import {accumulateSum, empty, unique} from './sugar.js'; +import {accumulateSum, empty} from './sugar.js'; +import {sortByDate} from './sort.js'; // Generic value operations @@ -15,622 +16,6 @@ export function getKebabCase(name) { .toLowerCase(); } -export function chunkByConditions(array, conditions) { - if (empty(array)) { - return []; - } - - if (empty(conditions)) { - return [array]; - } - - const out = []; - let cur = [array[0]]; - for (let i = 1; i < array.length; i++) { - const item = array[i]; - const prev = array[i - 1]; - let chunk = false; - for (const condition of conditions) { - if (condition(item, prev)) { - chunk = true; - break; - } - } - if (chunk) { - out.push(cur); - cur = [item]; - } else { - cur.push(item); - } - } - out.push(cur); - return out; -} - -export function chunkByProperties(array, properties) { - return chunkByConditions( - array, - properties.map((p) => (a, b) => { - if (a[p] instanceof Date && b[p] instanceof Date) return +a[p] !== +b[p]; - - if (a[p] !== b[p]) return true; - - // Not sure if this line is still necessary with the specific check for - // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....? - if (a[p] != b[p]) return true; - - return false; - }) - ).map((chunk) => ({ - ...Object.fromEntries(properties.map((p) => [p, chunk[0][p]])), - chunk, - })); -} - -export function chunkMultipleArrays(...args) { - const arrays = args.slice(0, -1); - const fn = args.at(-1); - - if (arrays[0].length === 0) { - return []; - } - - const newChunk = index => arrays.map(array => [array[index]]); - const results = [newChunk(0)]; - - for (let i = 1; i < arrays[0].length; i++) { - const current = results.at(-1); - - const args = []; - for (let j = 0; j < arrays.length; j++) { - const item = arrays[j][i]; - const previous = current[j].at(-1); - args.push(item, previous); - } - - if (fn(...args)) { - results.push(newChunk(i)); - continue; - } - - for (let j = 0; j < arrays.length; j++) { - current[j].push(arrays[j][i]); - } - } - - return results; -} - -// Sorting functions - all utils here are mutating, so make sure to initially -// slice/filter/somehow generate a new array from input data if retaining the -// initial sort matters! (Spoilers: If what you're doing involves any kind of -// parallelization, it definitely matters.) - -// General sorting utilities! These don't do any sorting on their own but are -// handy in the sorting functions below (or if you're making your own sort). - -export function compareCaseLessSensitive(a, b) { - // Compare two strings without considering capitalization... unless they - // happen to be the same that way. - - const al = a.toLowerCase(); - const bl = b.toLowerCase(); - - return al === bl - ? a.localeCompare(b, undefined, {numeric: true}) - : al.localeCompare(bl, undefined, {numeric: true}); -} - -// Subtract common prefixes and other characters which some people don't like -// to have considered while sorting. The words part of this is English-only for -// now, which is totally evil. -export function normalizeName(s) { - // Turn (some) ligatures into expanded variant for cleaner sorting, e.g. - // "ff" into "ff", in decompose mode, so that "ü" is represented as two - // bytes ("u" + \u0308 combining diaeresis). - s = s.normalize('NFKD'); - - // Replace one or more whitespace of any kind in a row, as well as certain - // punctuation, with a single typical space, then trim the ends. - s = s - .replace( - /[\p{Separator}\p{Dash_Punctuation}\p{Connector_Punctuation}]+/gu, - ' ' - ) - .trim(); - - // Discard anything that isn't a letter, number, or space. - s = s.replace(/[^\p{Letter}\p{Number} ]/gu, '').trim(); - - // Remove common English (only, for now) prefixes. - s = s.replace(/^(?:an?|the) /i, ''); - - return s; -} - -// Sorts multiple arrays by an arbitrary function (which is the last argument). -// Paired values from each array are provided to the callback sequentially: -// -// (a_fromFirstArray, b_fromFirstArray, -// a_fromSecondArray, b_fromSecondArray, -// a_fromThirdArray, b_fromThirdArray) => -// relative positioning (negative, positive, or zero) -// -// Like native single-array sort, this is a mutating function. -export function sortMultipleArrays(...args) { - const arrays = args.slice(0, -1); - const fn = args.at(-1); - - const length = arrays[0].length; - const symbols = new Array(length).fill(null).map(() => Symbol()); - const indexes = Object.fromEntries(symbols.map((symbol, index) => [symbol, index])); - - symbols.sort((a, b) => { - const indexA = indexes[a]; - const indexB = indexes[b]; - - const args = []; - for (let i = 0; i < arrays.length; i++) { - args.push(arrays[i][indexA]); - args.push(arrays[i][indexB]); - } - - return fn(...args); - }); - - for (const array of arrays) { - // Note: We're mutating this array pulling values from itself, but only all - // at once after all those values have been pulled. - array.splice(0, array.length, ...symbols.map(symbol => array[indexes[symbol]])); - } - - return arrays; -} - -// Filters multiple arrays by an arbitrary function (which is the last argument). -// Values from each array are provided to the callback sequentially: -// -// (value_fromFirstArray, -// value_fromSecondArray, -// value_fromThirdArray, -// index, -// [firstArray, secondArray, thirdArray]) => -// true or false -// -// Please be aware that this is a mutating function, unlike native single-array -// filter. The mutated arrays are returned. Also attached under `.removed` are -// corresponding arrays of items filtered out. -export function filterMultipleArrays(...args) { - const arrays = args.slice(0, -1); - const fn = args.at(-1); - - const removed = new Array(arrays.length).fill(null).map(() => []); - - for (let i = arrays[0].length - 1; i >= 0; i--) { - const args = arrays.map(array => array[i]); - args.push(i, arrays); - - if (!fn(...args)) { - for (let j = 0; j < arrays.length; j++) { - const item = arrays[j][i]; - arrays[j].splice(i, 1); - removed[j].unshift(item); - } - } - } - - Object.assign(arrays, {removed}); - return arrays; -} - -// Reduces multiple arrays with an arbitrary function (which is the last -// argument). Note that this reduces into multiple accumulators, one for -// each input array, not just a single value. That's reflected in both the -// callback parameters: -// -// (accumulator1, -// accumulator2, -// value_fromFirstArray, -// value_fromSecondArray, -// index, -// [firstArray, secondArray]) => -// [newAccumulator1, newAccumulator2] -// -// As well as the final return value of reduceMultipleArrays: -// -// [finalAccumulator1, finalAccumulator2] -// -// This is not a mutating function. -export function reduceMultipleArrays(...args) { - const [arrays, fn, initialAccumulators] = - (typeof args.at(-1) === 'function' - ? [args.slice(0, -1), args.at(-1), null] - : [args.slice(0, -2), args.at(-2), args.at(-1)]); - - if (empty(arrays[0])) { - throw new TypeError(`Reduce of empty arrays with no initial value`); - } - - let [accumulators, i] = - (initialAccumulators - ? [initialAccumulators, 0] - : [arrays.map(array => array[0]), 1]); - - for (; i < arrays[0].length; i++) { - const args = [...accumulators, ...arrays.map(array => array[i])]; - args.push(i, arrays); - accumulators = fn(...args); - } - - return accumulators; -} - -// Component sort functions - these sort by one particular property, applying -// unique particulars where appropriate. Usually you don't want to use these -// directly, but if you're making a custom sort they can come in handy. - -// Universal method for sorting things into a predictable order, as directory -// is taken to be unique. There are two exceptions where this function (and -// thus any of the composite functions that start with it) *can't* be taken as -// deterministic: -// -// 1) Mixed data of two different Things, as directories are only taken as -// unique within one given class of Things. For example, this function -// won't be deterministic if its array contains both <album:ithaca> and -// <track:ithaca>. -// -// 2) Duplicate directories, or multiple instances of the "same" Thing. -// This function doesn't differentiate between two objects of the same -// directory, regardless of any other properties or the overall "identity" -// of the object. -// -// These exceptions are unavoidable except for not providing that kind of data -// in the first place, but you can still ensure the overall program output is -// deterministic by ensuring the input is arbitrarily sorted according to some -// other criteria - ex, although sortByDirectory itself isn't determinstic when -// given mixed track and album data, the final output (what goes on the site) -// will always be the same if you're doing sortByDirectory([...albumData, -// ...trackData]), because the initial sort places albums before tracks - and -// sortByDirectory will handle the rest, given all directories are unique -// except when album and track directories overlap with each other. -export function sortByDirectory(data, { - getDirectory = object => object.directory, -} = {}) { - const directories = data.map(getDirectory); - - sortMultipleArrays(data, directories, - (a, b, directoryA, directoryB) => - compareCaseLessSensitive(directoryA, directoryB)); - - return data; -} - -export function sortByName(data, { - getName = object => object.name, -} = {}) { - const names = data.map(getName); - const normalizedNames = names.map(normalizeName); - - sortMultipleArrays(data, normalizedNames, names, - ( - a, b, - normalizedA, normalizedB, - nonNormalizedA, nonNormalizedB, - ) => - compareNormalizedNames( - normalizedA, normalizedB, - nonNormalizedA, nonNormalizedB, - )); - - return data; -} - -export function compareNormalizedNames( - normalizedA, normalizedB, - nonNormalizedA, nonNormalizedB, -) { - const comparison = compareCaseLessSensitive(normalizedA, normalizedB); - return ( - (comparison === 0 - ? compareCaseLessSensitive(nonNormalizedA, nonNormalizedB) - : comparison)); -} - -export function sortByDate(data, { - getDate = object => object.date, - latestFirst = false, -} = {}) { - const dates = data.map(getDate); - - sortMultipleArrays(data, dates, - (a, b, dateA, dateB) => - compareDates(dateA, dateB, {latestFirst})); - - return data; -} - -export function compareDates(a, b, { - latestFirst = false, -} = {}) { - if (a && b) { - return (latestFirst ? b - a : a - b); - } - - // It's possible for objects with and without dates to be mixed - // together in the same array. If that's the case, we put all items - // without dates at the end. - if (a) return -1; - if (b) return 1; - - // If neither of the items being compared have a date, don't move - // them relative to each other. This is basically the same as - // filtering out all non-date items and then pushing them at the - // end after sorting the rest. - return 0; -} - -export function getLatestDate(dates) { - const filtered = dates.filter(Boolean); - if (empty(filtered)) return null; - - return filtered - .reduce( - (accumulator, date) => - date > accumulator ? date : accumulator, - -Infinity); -} - -export function getEarliestDate(dates) { - const filtered = dates.filter(Boolean); - if (empty(filtered)) return null; - - return filtered - .reduce( - (accumulator, date) => - date < accumulator ? date : accumulator, - Infinity); -} - -// Funky sort which takes a data set and a corresponding list of "counts", -// which are really arbitrary numbers representing some property of each data -// object defined by the caller. It sorts and mutates *both* of these, so the -// sorted data will still correspond to the same indexed count. -export function sortByCount(data, counts, { - greatestFirst = false, -} = {}) { - sortMultipleArrays(data, counts, (data1, data2, count1, count2) => - (greatestFirst - ? count2 - count1 - : count1 - count2)); - - return data; -} - -// Corresponding filter function for the above sort. By default, items whose -// corresponding count is zero will be removed. -export function filterByCount(data, counts, { - min = 1, - max = Infinity, -} = {}) { - filterMultipleArrays(data, counts, (data, count) => - count >= min && count <= max); -} - -export function sortByPositionInParent(data, { - getParent, - getChildren, -}) { - return data.sort((a, b) => { - const parentA = getParent(a); - const parentB = getParent(b); - - // Don't change the sort when the two items are from separate parents. - // This function doesn't change the order of parents or try to "merge" - // two separated chunks of items from the same parent together. - if (parentA !== parentB) { - return 0; - } - - // Don't change the sort when either (or both) of the items doesn't - // even have a parent (e.g. it's the passed data is a mixed array of - // children and parents). - if (!parentA || !parentB) { - return 0; - } - - const indexA = getChildren(parentA).indexOf(a); - const indexB = getChildren(parentB).indexOf(b); - - // If the getParent/getChildren relationship doesn't go both ways for - // some reason, don't change the sort. - if (indexA === -1 || indexB === -1) { - return 0; - } - - return indexA - indexB; - }); -} - -export function sortByPositionInAlbum(data) { - return sortByPositionInParent(data, { - getParent: track => track.album, - getChildren: album => album.tracks, - }); -} - -export function sortByPositionInFlashAct(data) { - return sortByPositionInParent(data, { - getParent: flash => flash.act, - getChildren: act => act.flashes, - }); -} - -// Sorts data so that items are grouped together according to whichever of a -// set of arbitrary given conditions is true first. If no conditions are met -// for a given item, it's moved over to the end! -export function sortByConditions(data, conditions) { - return data.sort((a, b) => { - const ai = conditions.findIndex((f) => f(a)); - const bi = conditions.findIndex((f) => f(b)); - - if (ai >= 0 && bi >= 0) { - return ai - bi; - } else if (ai >= 0) { - return -1; - } else if (bi >= 0) { - return 1; - } else { - return 0; - } - }); -} - -// Composite sorting functions - these consider multiple properties, generally -// always returning the same output regardless of how the input was originally -// sorted (or left unsorted). If you're working with arbitrarily sorted inputs -// (typically wiki data, either in full or unsorted filter), these make sure -// what gets put on the actual website (or wherever) is deterministic. Also -// they're just handy sorting utilities. -// -// Note that because these are each comprised of multiple component sorting -// functions, they expect more than just one property to be present for full -// sorting (listed above each function). If you're mapping thing objects to -// another representation, try to include all of these listed properties. - -// Expects thing properties: -// * directory (or override getDirectory) -// * name (or override getName) -export function sortAlphabetically(data, { - getDirectory, - getName, -} = {}) { - sortByDirectory(data, {getDirectory}); - sortByName(data, {getName}); - return data; -} - -// Expects thing properties: -// * directory (or override getDirectory) -// * name (or override getName) -// * date (or override getDate) -export function sortChronologically(data, { - latestFirst = false, - getDirectory, - getName, - getDate, -} = {}) { - sortAlphabetically(data, {getDirectory, getName}); - sortByDate(data, {latestFirst, getDate}); - return data; -} - -// This one's a little odd! Sorts an array of {entry, thing} pairs using -// the provided sortFunction, which will operate on each item's `thing`, not -// its entry (or the item as a whole). If multiple entries are associated -// with the same thing, they'll end up bunched together in the output, -// retaining their original relative positioning. -export function sortEntryThingPairs(data, sortFunction) { - const things = unique(data.map(item => item.thing)); - sortFunction(things); - - const outputArrays = []; - const thingToOutputArray = new Map(); - - for (const thing of things) { - const array = []; - thingToOutputArray.set(thing, array); - outputArrays.push(array); - } - - for (const item of data) { - thingToOutputArray.get(item.thing).push(item); - } - - data.splice(0, data.length, ...outputArrays.flat()); - - return data; -} - -/* -// Alternate draft version of sortEntryThingPairs. -// See: https://github.com/hsmusic/hsmusic-wiki/issues/90#issuecomment-1607412168 - -// Maps the provided "preparation" function across a list of arbitrary values, -// building up a list of sortable values; sorts these with the provided sorting -// function; and reorders the sources to match their corresponding prepared -// values. As usual, if multiple source items correspond to the same sorting -// data, this retains the source relative positioning. -export function prepareAndSort(sources, prepareForSort, sortFunction) { - const prepared = []; - const preparedToSource = new Map(); - - for (const original of originals) { - const prep = prepareForSort(source); - prepared.push(prep); - preparedToSource.set(prep, source); - } - - sortFunction(prepared); - - sources.splice(0, ...sources.length, prepared.map(prep => preparedToSource.get(prep))); - - return sources; -} -*/ - -// Highly contextual sort functions - these are only for very specific types -// of Things, and have appropriately hard-coded behavior. - -// Sorts so that tracks from the same album are generally grouped together in -// their original (album track list) order, while prioritizing date (by default -// release date but can be overridden) above all else. -// -// This function also works for data lists which contain only tracks. -export function sortAlbumsTracksChronologically(data, { - latestFirst = false, - getDate, -} = {}) { - // Sort albums before tracks... - sortByConditions(data, [(t) => t.album === undefined]); - - // Group tracks by album... - sortByDirectory(data, { - getDirectory: (t) => (t.album ? t.album.directory : t.directory), - }); - - // Sort tracks by position in album... - sortByPositionInAlbum(data); - - // ...and finally sort by date. If tracks from more than one album were - // released on the same date, they'll still be grouped together by album, - // and tracks within an album will retain their relative positioning (i.e. - // stay in the same order as part of the album's track listing). - sortByDate(data, {latestFirst, getDate}); - - return data; -} - -export function sortFlashesChronologically(data, { - latestFirst = false, - getDate, -} = {}) { - // Group flashes by act... - sortByDirectory(data, { - getDirectory: flash => flash.act.directory, - }); - - // Sort flashes by position in act... - sortByPositionInFlashAct(data); - - // ...and finally sort by date. If flashes from more than one act were - // released on the same date, they'll still be grouped together by act, - // and flashes within an act will retain their relative positioning (i.e. - // stay in the same order as the act's flash listing). - sortByDate(data, {latestFirst, getDate}); - - return data; -} - // Specific data utilities // Matches heading details from commentary data in roughly the formats: |