Skip to content

Commit

Permalink
Merge branch 'star' into starbestfit
Browse files Browse the repository at this point in the history
  • Loading branch information
drinkablebreeze committed Jan 8, 2024
2 parents f711aa4 + ea3696b commit a4cbef6
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 28 deletions.
49 changes: 33 additions & 16 deletions frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ const AvailabilityViewer = ({ times, people, table, eventId, timeFormat, timezon
// Desired meeting time duration in minutes (to calculate the best time)
const [meetingDuration, setMeetingDuration] = useState<number>(60)
const durationOptions = Array.from(Array(24 * 4).keys()).map(x => (x + 1) * 15)
const durationLabels = durationOptions.map(
x => `${t('group.hours', {count: Math.floor(x / 60)})} ${x % 60} ${t('minutes')}`
)
const durationLabels = durationOptions.map(x => {
const hours = Math.floor(x / 60)
const minutes: number = (x % 60)
const hoursFormatted = hours > 0 ? t('group.hours', {count: hours}) : ""
const minutesFormatted = minutes > 0 ? `${minutes} ${t('minutes')}` : ""
const sep = ((hours > 0) && (minutes > 0)) ? " " : ""
return `${hoursFormatted}${sep}${minutesFormatted}`
})

const results = useMemo(
() => calculateBestTime(
Expand All @@ -104,16 +109,23 @@ const AvailabilityViewer = ({ times, people, table, eventId, timeFormat, timezon
stars: t('stars', {count: averageAndRound(timeScore.score, filteredPeople.length)})
}
: undefined
const [bestFormatted, nextFormatted, fracFormatted] = useMemo(() => {
return [
formatTime(results.bestTime),
formatTime(results.nextBest),
(results.preferredFraction !== undefined) // preferredFraction could be 0
? (results.preferredFraction * 100).toFixed(2)
: undefined
]
},
[results, i18n.language, timeFormat, timezone, filteredPeople.length])
const formatPersonVotes = (count: number | undefined) =>
(count !== undefined) ? t('group.people', {count: count}) : undefined
const [bestFormatted, nextFormatted, bestVotesPeople, nextVotesPeople, noPrefPeople] =
useMemo(() => {
return [
formatTime(results.bestTime),
formatTime(results.nextBest),
formatPersonVotes(results.bestVotes),
formatPersonVotes(results.nextVotes),
((results.bestVotes !== undefined) && (results.nextVotes !== undefined))
? formatPersonVotes(
filteredPeople.length - results.bestVotes - results.nextVotes
)
: undefined
]
},
[results, i18n.language, timeFormat, timezone, filteredPeople.length])

/* End of STAR section */

Expand Down Expand Up @@ -291,7 +303,11 @@ const AvailabilityViewer = ({ times, people, table, eventId, timeFormat, timezon
_<strong className={styles.bestTime}>{{time: bestFormatted.time}}</strong>
</Trans>
</p>
{nextFormatted && (fracFormatted !== undefined) && <p>
{nextFormatted
&& (bestVotesPeople !== undefined)
&& (nextVotesPeople !== undefined)
&& (noPrefPeople !== undefined)
&& <p>
<Trans i18nKey="group.best_fit3" t={t} i18n={i18n}>
{/* eslint-disable-next-line */}
{/* @ts-ignore */}
Expand All @@ -301,8 +317,9 @@ const AvailabilityViewer = ({ times, people, table, eventId, timeFormat, timezon
_<strong>{{nextBest: nextFormatted.time}}</strong>
_{{ bestStars: bestFormatted.stars }}
_{{ nextStars: nextFormatted.stars }}
_{{ bestTime: bestFormatted.time }}
_{{ frac: fracFormatted }}_
_{{ bestVotesPeople }}
_{{ nextVotesPeople }}
_{{ noPrefPeople }}_
</Trans>
</p>}
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/i18n/locales/en/event.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@
"clipboard_message": "People available on {{date}}",
"best_fit1": "For an event duration of",
"best_fit2": "the best time is <1>{{time}}</1>",
"best_fit3": "<strong>{{bestTime}}</strong> and <strong>{{nextBest}}</strong> scored the highest with {{bestStars}} and {{nextStars}}<br/>Between these two finalists, <strong>{{bestTime}}</strong> was preferred by {{frac}}% of people with a preference",
"best_fit3": "<strong>{{bestTime}}</strong> and <strong>{{nextBest}}</strong> scored the highest with {{bestStars}} and {{nextStars}}<br/>Between these two finalists, {{bestVotesPeople}} preferred the best time, {{nextVotesPeople}} preferred the next best time, and {{noPrefPeople}} expressed no preference",
"hours_one": "{{count}} hour",
"hours_other": "{{count}} hours",
"minutes": "minutes"
"minutes": "minutes",
"people_one": "{{count}} person",
"people_other": "{{count}} people"
},

"you": {
Expand Down
49 changes: 39 additions & 10 deletions frontend/src/utils/star.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
* Basic rules: https://starvoting.org/star
* Tiebreaking rules: https://starvoting.org/ties
*
* In STAR Voting, candidate times are scored 0 to 5 stars. The two highest
* scoring candidates proceed to the runoff round. In the runoff, each person's
* full vote goes to the candidate they prefer, if they had a preference.
*
*
* # Scores for "Meeting Times" Average the Component Timeslot Scores
*
Expand Down Expand Up @@ -109,6 +113,28 @@
* The mini rounds in the runoff are: rankedRobin, score, fiveStars, then
* random. One winner is picked from the two candidates that advanced from the
* scoring round.
*
*
* # Inspection
*
* As of this writing, details about the tabulation are logged in the browser's
* console. You can look at this object to better understand how a result was
* computed.
*
* The top-level object contains info for the scoring round, the runoff round,
* and the final results to display. The rounds list the candidates going into
* the round, the mini rounds used in the computation, and the winner(s) of the
* round.
*
* The mini round objects include the mini round type, the candidates going into
* the mini round with updated metrics (rank wins are calculated at the start of
* the mini round), the winner(s), and any candidates that are still tied.
*
* Candidate objects include the time ("date") of the candidate in UTC, the
* scores each person gave to a full event of the desired duration if it starts
* at that time, and the metrics used to compute mini round results (the total
* score given to the time, number of ranked wins if it's a rankedRobin round,
* the number of five-stars, and the random tie value)
*/

import { Temporal } from '@js-temporal/polyfill'
Expand Down Expand Up @@ -153,7 +179,9 @@ export const averageAndRound = (score: number, numPeople: number): number => {
export interface StarResults {
bestTime?: TimeScore,
nextBest?: TimeScore,
preferredFraction?: number // exists if both bestTime and nextBest exist
// these are defined if both bestTime and nextTime are defined
bestVotes?: number, // number of people that preferred the best time
nextVotes?: number, // number of people that preferred the next best time
}

export const calculateBestTime = (
Expand All @@ -167,7 +195,8 @@ export const calculateBestTime = (
return ({
bestTime: undefined,
nextBest: undefined,
preferredFraction: undefined
bestVotes: undefined,
nextVotes: undefined,
})
}
const t0 = performance.now()
Expand Down Expand Up @@ -377,7 +406,8 @@ const calculateStarBest = (
return {
bestTime: undefined,
nextBest: undefined,
preferredFraction: undefined
bestVotes: undefined,
nextVotes: undefined,
}
}
if (candidates.length === 1) {
Expand All @@ -389,7 +419,8 @@ const calculateStarBest = (
score: candidates[0].score / numTimeslots,
},
nextBest: undefined,
preferredFraction: undefined
bestVotes: undefined,
nextVotes: undefined,
}
}
// scoring round -> find the top two by score
Expand All @@ -401,11 +432,8 @@ const calculateStarBest = (
const winner = runoffRound.winners[0] as Candidate
const second =
scoreRound.winners.find((w: Candidate) => w.date !== winner.date) as Candidate
const winnerRankedWins = getMetric(winner, "rankedRobin")
const secondRankedWins = getMetric(second, "rankedRobin")
const totalRankedWins = winnerRankedWins + secondRankedWins
// report the number of preferences expressed as 0 and not NaN
const preferredFraction = totalRankedWins ? winnerRankedWins / totalRankedWins : 0
const bestVotes = getMetric(winner, "rankedRobin")
const nextVotes = getMetric(second, "rankedRobin")
const result = ({
bestTime: {
time: winner.date,
Expand All @@ -415,7 +443,8 @@ const calculateStarBest = (
time: second.date,
score: second.score / numTimeslots,
},
preferredFraction,
bestVotes,
nextVotes,
})
console.log(({ scoreRound, runoffRound, result }))
return result
Expand Down

0 comments on commit a4cbef6

Please sign in to comment.