Skip to content

Commit

Permalink
feat(feeds): add old and best sort
Browse files Browse the repository at this point in the history
  • Loading branch information
estebanabaroa committed Aug 30, 2024
1 parent 5828c2a commit e5b30dc
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 0 deletions.
48 changes: 48 additions & 0 deletions src/stores/feeds/feed-sorter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,52 @@ describe('feedSorter', () => {
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '11'},
])
})

test('sort by old', async () => {
const sorted = feedSorter.sort('old', feed)
expect(sorted).toEqual([
{timestamp: day(1), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', pinned: true, cid: '16'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub1', pinned: true, cid: '17'},
{timestamp: day(0), upvoteCount: 10001, downvoteCount: 1000, subplebbitAddress: 'sub1', cid: '2'},
{timestamp: day(0), upvoteCount: 10000, downvoteCount: 1000, subplebbitAddress: 'sub2', cid: '10'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub1', cid: '1'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub2', cid: '9'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '0'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '3'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 100, subplebbitAddress: 'sub1', cid: '7'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '8'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '11'},
{timestamp: day(0), lastReplyTimestamp: day(4) + 2, upvoteCount: 100, downvoteCount: 100, subplebbitAddress: 'sub3', cid: '15'},
{timestamp: day(1), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '6'},
{timestamp: day(1), lastReplyTimestamp: day(4) + 1, upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub3', cid: '14'},
{timestamp: day(2), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '5'},
{timestamp: day(2), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub3', cid: '13'},
{timestamp: day(3), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '4'},
{timestamp: day(3), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '12'},
])
})

test('sort by best', async () => {
const sorted = feedSorter.sort('best', feed)
expect(sorted).toEqual([
{timestamp: day(1), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', pinned: true, cid: '16'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub1', pinned: true, cid: '17'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub1', cid: '1'},
{timestamp: day(0), upvoteCount: 1000, downvoteCount: 1, subplebbitAddress: 'sub2', cid: '9'},
{timestamp: day(0), upvoteCount: 10001, downvoteCount: 1000, subplebbitAddress: 'sub1', cid: '2'},
{timestamp: day(0), upvoteCount: 10000, downvoteCount: 1000, subplebbitAddress: 'sub2', cid: '10'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '0'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '3'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '8'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '11'},
{timestamp: day(1), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '6'},
{timestamp: day(1), lastReplyTimestamp: day(4) + 1, upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub3', cid: '14'},
{timestamp: day(2), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '5'},
{timestamp: day(2), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub3', cid: '13'},
{timestamp: day(3), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub1', cid: '4'},
{timestamp: day(3), upvoteCount: 100, downvoteCount: 10, subplebbitAddress: 'sub2', cid: '12'},
{timestamp: day(0), upvoteCount: 100, downvoteCount: 100, subplebbitAddress: 'sub1', cid: '7'},
{timestamp: day(0), lastReplyTimestamp: day(4) + 2, upvoteCount: 100, downvoteCount: 100, subplebbitAddress: 'sub3', cid: '15'},
])
})
})
46 changes: 46 additions & 0 deletions src/stores/feeds/feed-sorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,46 @@ const sortByActive = (feed: any[]) => {
.sort((a, b) => (b.lastReplyTimestamp || b.timestamp || 0) - (a.lastReplyTimestamp || a.timestamp || 0))
}

const sortByOld = (feed: any[]) => {
// sort by upvoteCount first for tiebreaker, then timestamp
return feed.sort((a, b) => (b.upvoteCount || 0) - (a.upvoteCount || 0)).sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0))
}

// "best" sort from reddit replies
// https://web.archive.org/web/20100305052116/http://blog.reddit.com/2009/10/reddits-new-comment-sorting-system.html
// https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
// http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
// https://github.com/reddit-archive/reddit/blob/753b17407e9a9dca09558526805922de24133d53/r2/r2/lib/db/_sorts.pyx#L70
const sortByBest = (feed: any[]) => {
const postScores: {[key: string]: number} = {}
for (const post of feed) {
let upvoteCount = post.upvoteCount || 0
upvoteCount++ // reddit initial upvotes is 1, plebbit is 0
const downvoteCount = post.downvoteCount || 0

// n is the total number of ratings
const n = upvoteCount + downvoteCount
if (n === 0) {
postScores[post.cid] = 0
continue
}

// zα/2 is the (1-α/2) quantile of the standard normal distribution
const z = 1.281551565545

// p is the observed fraction of positive ratings
const p = upvoteCount / n

const left = p + (1 / (2 * n)) * z * z
const right = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n))
const under = 1 + (1 / n) * z * z
postScores[post.cid] = (left - right) / under
}

// sort by old first for tiebreaker (like reddit does)
return feed.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).sort((a, b) => (postScores[b.cid] || 0) - (postScores[a.cid] || 0))
}

export const sort = (sortType: string, feed: any[]) => {
// pinned posts are not sorted, maybe in a future version we can sort them based on something
const pinnedPosts = feed.filter((post) => post.pinned)
Expand All @@ -105,6 +145,12 @@ export const sort = (sortType: string, feed: any[]) => {
if (sortType.match('active')) {
return [...pinnedPosts, ...sortByActive(feed)]
}
if (sortType.match('old')) {
return [...pinnedPosts, ...sortByOld(feed)]
}
if (sortType.match('best')) {
return [...pinnedPosts, ...sortByBest(feed)]
}
throw Error(`feedsStore feedSorter sort type '${sortType}' doesn't exist`)
}

Expand Down

0 comments on commit e5b30dc

Please sign in to comment.