Add logic to prevent repeated random quotes (#2693)

* Add logic to prevent repeated random quotes
This commit is contained in:
Bruce Berrios 2022-03-13 14:30:07 -04:00 committed by GitHub
parent 4213b6576c
commit cb6ddc5fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 207 additions and 101 deletions

View file

@ -0,0 +1,156 @@
import { shuffle } from "../utils/misc";
import { subscribe } from "../observables/config-event";
interface Quote {
text: string;
source: string;
length: number;
id: number;
}
interface QuoteData {
language: string;
quotes: Quote[];
groups: number[][];
}
interface QuoteCollection {
quotes: MonkeyTypes.Quote[];
length: number;
language: string | null;
groups: MonkeyTypes.Quote[][];
}
const defaultQuoteCollection: QuoteCollection = {
quotes: [],
length: 0,
language: null,
groups: [],
};
class QuotesController {
private quoteCollection: QuoteCollection = defaultQuoteCollection;
private quoteQueue: MonkeyTypes.Quote[] = [];
private queueIndex = 0;
async getQuotes(
language: string,
quoteLengths?: number[]
): Promise<QuoteCollection> {
const normalizedLanguage = language.replace(/_\d*k$/g, "");
if (this.quoteCollection.language !== normalizedLanguage) {
try {
const data: QuoteData = await $.getJSON(`quotes/${language}.json`);
if (data.quotes === undefined || data.quotes.length === 0) {
return defaultQuoteCollection;
}
this.quoteCollection = {
quotes: [],
length: data.quotes.length,
groups: [],
language: data.language,
};
// Transform JSON Quote schema to MonkeyTypes Quote schema
data.quotes.forEach((quote: Quote) => {
const monkeyTypeQuote: MonkeyTypes.Quote = {
text: quote.text,
source: quote.source,
length: quote.length,
id: quote.id,
language: language,
};
this.quoteCollection.quotes.push(monkeyTypeQuote);
});
data.groups.forEach((quoteGroup, groupIndex) => {
const lower = quoteGroup[0];
const upper = quoteGroup[1];
this.quoteCollection.groups[groupIndex] =
this.quoteCollection.quotes.filter((quote) => {
if (quote.length >= lower && quote.length <= upper) {
quote.group = groupIndex;
return true;
}
return false;
});
});
if (quoteLengths !== undefined) {
this.updateQuoteQueue(quoteLengths);
}
} catch {
return defaultQuoteCollection;
}
}
return this.quoteCollection;
}
getQuoteById(id: number): MonkeyTypes.Quote | undefined {
const targetQuote = this.quoteCollection.quotes.find(
(quote: MonkeyTypes.Quote) => {
return quote.id === id;
}
);
return targetQuote;
}
updateQuoteQueue(quoteGroups: number[]): void {
this.quoteQueue = [];
quoteGroups.forEach((group) => {
if (group < 0) {
return;
}
this.quoteCollection.groups[group]?.forEach((quote) => {
this.quoteQueue.push(quote);
});
});
shuffle(this.quoteQueue);
this.queueIndex = 0;
}
getRandomQuote(): MonkeyTypes.Quote | null {
if (this.quoteQueue.length === 0) {
return null;
}
if (this.queueIndex >= this.quoteQueue.length) {
this.queueIndex = 0;
shuffle(this.quoteQueue);
}
const randomQuote = this.quoteQueue[this.queueIndex];
this.queueIndex += 1;
return randomQuote;
}
getCurrentQuote(): MonkeyTypes.Quote | null {
if (this.quoteQueue.length === 0) {
return null;
}
return this.quoteQueue[this.queueIndex];
}
}
const quoteController = new QuotesController();
subscribe((key, newValue) => {
if (key === "quoteLength") {
quoteController.updateQuoteQueue(newValue as number[]);
}
});
export default quoteController;

View file

@ -3,7 +3,7 @@ import Config from "../config";
import * as TestWords from "../test/test-words";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import * as Misc from "../utils/misc";
import QuotesController from "../controllers/quotes-controller";
const CAPTCHA_ID = 1;
@ -37,7 +37,7 @@ export async function show(options = defaultOptions): Promise<void> {
state.previousPopupShowCallback = previousPopupShowCallback;
const { quotes } = await Misc.getQuotes(Config.language);
const { quotes } = await QuotesController.getQuotes(Config.language);
state.quoteToReport = quotes.find((quote) => {
return quote.id === quoteId;
});

View file

@ -6,7 +6,6 @@ import * as Notifications from "../elements/notifications";
import * as QuoteSubmitPopup from "./quote-submit-popup";
import * as QuoteApprovePopup from "./quote-approve-popup";
import * as QuoteReportPopup from "./quote-report-popup";
import * as Misc from "../utils/misc";
import {
buildSearchService,
SearchService,
@ -14,6 +13,7 @@ import {
} from "../utils/search-service";
import { debounce } from "../utils/debounce";
import { splitByAndKeep } from "../utils/strings";
import QuotesController from "../controllers/quotes-controller";
export let selectedId = 1;
@ -55,7 +55,7 @@ function highlightMatches(text: string, matchedText: string[]): string {
}
async function updateResults(searchText: string): Promise<void> {
const { quotes } = await Misc.getQuotes(Config.language);
const { quotes } = await QuotesController.getQuotes(Config.language);
const quoteSearchService = getSearchService<MonkeyTypes.Quote>(
Config.language,

View file

@ -3,6 +3,7 @@ import * as TestUI from "./test-ui";
import * as ManualRestart from "./manual-restart-tracker";
import Config, * as UpdateConfig from "../config";
import * as Misc from "../utils/misc";
import QuotesController from "../controllers/quotes-controller";
import * as Notifications from "../elements/notifications";
import * as CustomText from "./custom-text";
import * as TestStats from "./test-stats";
@ -881,12 +882,13 @@ export async function init(): Promise<void> {
}
}
}
} else if (Config.mode == "quote") {
// setLanguage(Config.language.replace(/_\d*k$/g, ""), true);
} else if (Config.mode === "quote") {
const quotesCollection = await QuotesController.getQuotes(
Config.language,
Config.quoteLength
);
const quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, ""));
if (quotes.length === 0) {
if (quotesCollection.length === 0) {
TestUI.setTestRestarting(false);
Notifications.add(
`No ${Config.language.replace(/_\d*k$/g, "")} quotes found`,
@ -902,49 +904,24 @@ export async function init(): Promise<void> {
let rq: MonkeyTypes.Quote | undefined = undefined;
if (Config.quoteLength.includes(-2) && Config.quoteLength.length == 1) {
quotes.groups.forEach((group) => {
const filtered = (<MonkeyTypes.Quote[]>group).filter(
(quote) => quote.id == QuoteSearchPopup.selectedId
);
if (filtered.length > 0) {
rq = filtered[0];
}
});
if (rq === undefined) {
rq = <MonkeyTypes.Quote>quotes.groups[0][0];
const targetQuote = QuotesController.getQuoteById(
QuoteSearchPopup.selectedId
);
if (targetQuote === undefined) {
rq = <MonkeyTypes.Quote>quotesCollection.groups[0][0];
Notifications.add("Quote Id Does Not Exist", 0);
} else {
rq = targetQuote;
}
} else {
const quoteLengths = Config.quoteLength;
let groupIndex;
if (quoteLengths.length > 1) {
groupIndex =
quoteLengths[Math.floor(Math.random() * quoteLengths.length)];
while (quotes.groups[groupIndex].length === 0) {
groupIndex =
quoteLengths[Math.floor(Math.random() * quoteLengths.length)];
}
} else {
groupIndex = quoteLengths[0];
if (quotes.groups[groupIndex].length === 0) {
Notifications.add("No quotes found for selected quote length", 0);
TestUI.setTestRestarting(false);
return;
}
const randomQuote = QuotesController.getRandomQuote();
if (randomQuote === null) {
Notifications.add("No quotes found for selected quote length", 0);
TestUI.setTestRestarting(false);
return;
}
rq = quotes.groups[groupIndex][
Math.floor(Math.random() * quotes.groups[groupIndex].length)
] as MonkeyTypes.Quote;
if (
TestWords.randomQuote != null &&
typeof rq !== "number" &&
rq.id === TestWords.randomQuote.id
) {
rq = quotes.groups[groupIndex][
Math.floor(Math.random() * quotes.groups[groupIndex].length)
] as MonkeyTypes.Quote;
}
rq = randomQuote;
}
if (rq === undefined) return;

View file

@ -57,6 +57,7 @@ class Words {
}
}
}
export const words = new Words();
export let hasTab = false;
export let randomQuote = null as unknown as MonkeyTypes.Quote;

View file

@ -132,60 +132,6 @@ export async function getFunbox(
});
}
type QuoteCollection = {
quotes: MonkeyTypes.Quote[];
length?: number;
language?: string;
groups: number[][] | MonkeyTypes.Quote[][];
};
let quotes: QuoteCollection;
export async function getQuotes(language: string): Promise<QuoteCollection> {
if (
quotes === undefined ||
quotes.language !== language.replace(/_\d*k$/g, "")
) {
Loader.show();
try {
const data: QuoteCollection = await $.getJSON(`quotes/${language}.json`);
Loader.hide();
if (data.quotes === undefined || data.quotes.length === 0) {
quotes = {
quotes: [],
length: 0,
groups: [],
};
return quotes;
}
quotes = data;
quotes.length = data.quotes.length;
quotes.groups?.forEach((qg, i) => {
const lower = qg[0];
const upper = qg[1];
quotes.groups[i] = quotes.quotes.filter((q) => {
if (q.length >= lower && q.length <= upper) {
q.group = i;
return true;
} else {
return false;
}
});
});
return quotes;
} catch {
Loader.hide();
quotes = {
quotes: [],
length: 0,
groups: [],
};
return quotes;
}
} else {
return quotes;
}
}
let layoutsList: MonkeyTypes.Layouts = {};
export async function getLayoutsList(): Promise<MonkeyTypes.Layouts> {
if (Object.keys(layoutsList).length === 0) {
@ -1025,3 +971,29 @@ export async function downloadResultsCSV(
link.remove();
Loader.hide();
}
/**
* Gets an integer between min and max, both are inclusive.
* @param min
* @param max
* @returns Random integer betwen min and max.
*/
export function randomIntFromRange(min: number, max: number): number {
const minNorm = Math.ceil(min);
const maxNorm = Math.floor(max);
return Math.floor(Math.random() * (maxNorm - minNorm + 1) + minNorm);
}
/**
* Shuffle an array of elements using the FisherYates algorithm.
* This function mutates the input array.
* @param elements
*/
export function shuffle<T>(elements: T[]): void {
for (let i = elements.length - 1; i > 0; --i) {
const j = randomIntFromRange(0, i);
const temp = elements[j];
elements[j] = elements[i];
elements[i] = temp;
}
}