MISC: Make implicit string conversion consistent across all coding contracts (#2608)

This commit is contained in:
catloversg
2026-04-03 13:53:16 +07:00
committed by GitHub
parent d1b6acc57a
commit 8dcccdc5bb
13 changed files with 376 additions and 60 deletions
+35
View File
@@ -83,6 +83,41 @@ export function removeBracketsFromArrayString(str: string): string {
return strCpy;
}
/**
* This function only performs very simple checks to add the outermost optional brackets. Callers must perform other
* preprocessing steps and postprocessing validations. For example:
* - "[ [0, 1]]" will be incorrectly wrapped and converted to "[[[0,1]]]". Callers need to remove redundant whitespace.
* - "[[1,2],3]" is not an array of arrays, but this function will return it as is. Callers need to call validateAnswer.
*
* Note:
* - "" will always be converted to an empty array ([]).
* - When parsing an array of arrays (isArrayOfArray = true), "[]" will be converted to an empty array ([]), not an
* array containing an empty array ([[]]).
*/
export function parseArrayString(answer: string, isArrayOfArray = false): unknown {
let modifiedAnswer = answer.trim();
if (isArrayOfArray && modifiedAnswer === "[]") {
return [];
}
// If it doesn't start with any bracket, it's definitely "naked".
if (!modifiedAnswer.startsWith("[")) {
modifiedAnswer = `[${modifiedAnswer}]`;
} else if (isArrayOfArray && !modifiedAnswer.startsWith("[[")) {
// If it's supposed to be an array of arrays but only has one "[".
modifiedAnswer = `[${modifiedAnswer}]`;
}
try {
return JSON.parse(modifiedAnswer);
} catch (error) {
console.error(`Invalid answer: ${answer}`);
console.error(error);
return null;
}
}
export function removeQuotesFromString(str: string): string {
let strCpy: string = str;
if (strCpy.startsWith('"') || strCpy.startsWith("'")) {
@@ -1,7 +1,6 @@
import { filterTruthy } from "../../utils/helpers/ArrayHelpers";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes";
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums";
export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContractName.FindAllValidMathExpressions> = {
@@ -107,10 +106,12 @@ export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContra
return result.every((sol) => solutions.has(sol));
},
convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).split(",");
return filterTruthy(sanitized).map((s) => removeQuotesFromString(s.replace(/\s/g, "")));
const parsedAnswer = parseArrayString(ans);
if (!findAllValidMathExpressions[CodingContractName.FindAllValidMathExpressions].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is string[] =>
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
},
};
@@ -1,5 +1,5 @@
import { CodingContractName } from "@enums";
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes";
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
@@ -67,10 +67,12 @@ export const generateIPAddresses: Pick<CodingContractTypes, CodingContractName.G
return ret.length === answer.length && ret.every((ip) => answer.includes(ip));
},
convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "");
return sanitized.split(",").map((ip) => ip.replace(/^(?<quote>['"])([\d.]*)\k<quote>$/g, "$2"));
const parsedAnswer = parseArrayString(ans);
if (!generateIPAddresses[CodingContractName.GenerateIPAddresses].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is string[] =>
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
},
};
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes } from "../ContractTypes";
import { parseArrayString, CodingContractTypes } from "../ContractTypes";
import { CodingContractName } from "@enums";
export const largestRectangle: Pick<CodingContractTypes, CodingContractName.LargestRectangleInAMatrix> = {
@@ -152,13 +152,7 @@ Answer: [[0,0],[3,1]]
return userArea === (solution[1][0] - solution[0][0] + 1) * (solution[1][1] - solution[0][1] + 1);
},
convertAnswer: (ans) => {
let parsedAnswer: unknown;
try {
parsedAnswer = JSON.parse(ans);
} catch (error) {
console.error("Invalid answer:", error);
return null;
}
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
if (!largestRectangle[CodingContractName.LargestRectangleInAMatrix].validateAnswer(parsedAnswer)) {
return null;
}
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, convert2DArrayToString, removeBracketsFromArrayString } from "../ContractTypes";
import { CodingContractTypes, convert2DArrayToString, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums";
export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContractName.MergeOverlappingIntervals> = {
@@ -70,20 +70,13 @@ export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContract
return result.length === answer.length && result.every((a, i) => a[0] === answer[i][0] && a[1] === answer[i][1]);
},
convertAnswer: (ans) => {
const arrayRegex = /\[\d+,\d+\]/g;
const matches = ans.replace(/\s/g, "").match(arrayRegex);
if (matches === null) return null;
const arr = matches.map((a) =>
removeBracketsFromArrayString(a)
.split(",")
.map((n) => parseInt(n)),
);
// An inline function is needed here, so that TS knows this returns true if it matches the type
if (((a: number[][]): a is [number, number][] => a.every((n) => n.length === 2))(arr)) return arr;
return null;
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
if (!mergeOverlappingIntervals[CodingContractName.MergeOverlappingIntervals].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is [number, number][] =>
typeof ans === "object" &&
Array.isArray(ans) &&
ans.every((a) => Array.isArray(a) && a.length === 2 && a.every((n) => typeof n === "number")),
},
@@ -1,4 +1,4 @@
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes";
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums";
export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractName.Proper2ColoringOfAGraph> = {
@@ -121,14 +121,12 @@ export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractNa
return data[1].every(([a, b]) => answer[a] !== answer[b]);
},
convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "");
if (sanitized === "") return [];
const arr = sanitized.split(",").map((s) => parseInt(s, 10));
// An inline function is needed here, so that TS knows this returns true if it matches the type
if (((a): a is (1 | 0)[] => !a.some((n) => n !== 1 && n !== 0))(arr)) return arr;
return null;
const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""));
if (!proper2ColoringOfAGraph[CodingContractName.Proper2ColoringOfAGraph].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is (1 | 0)[] =>
typeof ans === "object" && Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
validateAnswer: (ans): ans is (1 | 0)[] => Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
},
};
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes";
import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums";
export const sanitizeParenthesesInExpression: Pick<
@@ -113,10 +113,16 @@ export const sanitizeParenthesesInExpression: Pick<
return res.every((sol) => answer.includes(sol));
},
convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).split(",");
return sanitized.map((s) => removeQuotesFromString(s.replace(/\s/g, "")));
const parsedAnswer = parseArrayString(ans);
if (
!sanitizeParenthesesInExpression[CodingContractName.SanitizeParenthesesInExpression].validateAnswer(
parsedAnswer,
)
) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is string[] =>
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
},
};
@@ -24,7 +24,7 @@ export const shortestPathInAGrid: Pick<CodingContractTypes, CodingContractName.S
" [[0,1],\n",
" [1,0]]\n",
"\n",
"Answer: ''",
`Answer: ""`,
].join(" ");
},
difficulty: 7,
@@ -1,5 +1,5 @@
import { CodingContractName } from "@enums";
import { removeBracketsFromArrayString, type CodingContractTypes } from "../ContractTypes";
import { parseArrayString, type CodingContractTypes } from "../ContractTypes";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
@@ -127,10 +127,12 @@ export const spiralizeMatrix: Pick<CodingContractTypes, CodingContractName.Spira
return spiral.length === answer.length && spiral.every((n, i) => n === answer[i]);
},
convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "").split(",");
return sanitized.map((s) => parseInt(s));
const parsedAnswer = parseArrayString(ans);
if (!spiralizeMatrix[CodingContractName.SpiralizeMatrix].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
},
validateAnswer: (ans): ans is number[] =>
typeof ans === "object" && Array.isArray(ans) && ans.every((n) => typeof n === "number"),
validateAnswer: (ans): ans is number[] => Array.isArray(ans) && ans.every((n) => typeof n === "number"),
},
};