Initial Commit
This commit is contained in:
198
lib/curriculum.ts
Normal file
198
lib/curriculum.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
export interface Topic {
|
||||
slug: string;
|
||||
title: string;
|
||||
shortTitle: string;
|
||||
week: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Unit {
|
||||
number: 1 | 2 | 3 | 4;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
weeks: string;
|
||||
color: "unit-1" | "unit-2" | "unit-3" | "unit-4";
|
||||
topics: Topic[];
|
||||
}
|
||||
|
||||
export const curriculum: Unit[] = [
|
||||
{
|
||||
number: 1,
|
||||
slug: "unit-1-fractions",
|
||||
title: "Fractions",
|
||||
description: "Add, subtract, multiply, divide fractions and apply to quantities",
|
||||
weeks: "Weeks 1-3",
|
||||
color: "unit-1",
|
||||
topics: [
|
||||
{
|
||||
slug: "add-subtract",
|
||||
title: "Add and Subtract Fractions",
|
||||
shortTitle: "Add & Subtract",
|
||||
week: 1,
|
||||
description: "Common and uncommon denominators, Butterfly Method",
|
||||
},
|
||||
{
|
||||
slug: "multiply",
|
||||
title: "Multiply Fractions",
|
||||
shortTitle: "Multiply",
|
||||
week: 1,
|
||||
description: "Multiply numerators and denominators, simplify",
|
||||
},
|
||||
{
|
||||
slug: "divide",
|
||||
title: "Divide Fractions",
|
||||
shortTitle: "Divide",
|
||||
week: 1,
|
||||
description: "Invert and multiply (reciprocal method)",
|
||||
},
|
||||
{
|
||||
slug: "mixed-operations",
|
||||
title: "Mixed Operations (BODMAS)",
|
||||
shortTitle: "BODMAS",
|
||||
week: 2,
|
||||
description: "Order of operations with fractions",
|
||||
},
|
||||
{
|
||||
slug: "fraction-of-quantity",
|
||||
title: "Fraction of a Quantity",
|
||||
shortTitle: "Of a Quantity",
|
||||
week: 3,
|
||||
description: "Calculate a fraction of a given amount",
|
||||
},
|
||||
{
|
||||
slug: "whole-from-fractions",
|
||||
title: "Calculate the Whole from Fractions",
|
||||
shortTitle: "Find the Whole",
|
||||
week: 3,
|
||||
description: "Find the whole when given a part and its fraction",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
slug: "unit-2-decimals",
|
||||
title: "Decimals",
|
||||
description: "Compare, order, round decimals and express in standard form",
|
||||
weeks: "Weeks 4-5",
|
||||
color: "unit-2",
|
||||
topics: [
|
||||
{
|
||||
slug: "compare-order",
|
||||
title: "Compare and Order Decimals",
|
||||
shortTitle: "Compare & Order",
|
||||
week: 4,
|
||||
description: "Place value, ascending, descending, greater/less than",
|
||||
},
|
||||
{
|
||||
slug: "approximate",
|
||||
title: "Approximate Decimals",
|
||||
shortTitle: "Approximation",
|
||||
week: 4,
|
||||
description: "Round to whole numbers, decimal places, significant figures",
|
||||
},
|
||||
{
|
||||
slug: "standard-form",
|
||||
title: "Standard Form (Scientific Notation)",
|
||||
shortTitle: "Standard Form",
|
||||
week: 5,
|
||||
description: "Express numbers as a × 10^n",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
slug: "unit-3-decimal-operations",
|
||||
title: "Decimal Operations",
|
||||
description: "Convert, add, subtract, multiply and divide decimals",
|
||||
weeks: "Weeks 6-7",
|
||||
color: "unit-3",
|
||||
topics: [
|
||||
{
|
||||
slug: "convert",
|
||||
title: "Convert Decimals and Fractions",
|
||||
shortTitle: "Convert",
|
||||
week: 6,
|
||||
description: "Decimals to fractions and fractions to decimals",
|
||||
},
|
||||
{
|
||||
slug: "add-subtract",
|
||||
title: "Add and Subtract Decimals",
|
||||
shortTitle: "Add & Subtract",
|
||||
week: 6,
|
||||
description: "Align decimal points, insert zeros as placeholders",
|
||||
},
|
||||
{
|
||||
slug: "multiply-divide",
|
||||
title: "Multiply and Divide Decimals",
|
||||
shortTitle: "Multiply & Divide",
|
||||
week: 7,
|
||||
description: "By powers of 10, whole numbers, and decimals",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
slug: "unit-4-ratio-proportion",
|
||||
title: "Ratio & Proportion",
|
||||
description: "Define, simplify, and apply ratios to solve problems",
|
||||
weeks: "Weeks 8-9",
|
||||
color: "unit-4",
|
||||
topics: [
|
||||
{
|
||||
slug: "define-ratio",
|
||||
title: "Define a Ratio",
|
||||
shortTitle: "Define Ratio",
|
||||
week: 8,
|
||||
description: "Relationship between two or more quantities",
|
||||
},
|
||||
{
|
||||
slug: "fractions-and-ratios",
|
||||
title: "Fractions and Ratios",
|
||||
shortTitle: "Fractions & Ratios",
|
||||
week: 8,
|
||||
description: "Understand the relationship between fractions and ratios",
|
||||
},
|
||||
{
|
||||
slug: "simplify-ratios",
|
||||
title: "Simplify Ratios",
|
||||
shortTitle: "Simplify",
|
||||
week: 8,
|
||||
description: "Write ratios in their simplest form",
|
||||
},
|
||||
{
|
||||
slug: "divide-in-ratio",
|
||||
title: "Divide a Quantity in a Given Ratio",
|
||||
shortTitle: "Divide in Ratio",
|
||||
week: 9,
|
||||
description: "Share amounts using ratios",
|
||||
},
|
||||
{
|
||||
slug: "word-problems",
|
||||
title: "Proportional Parts Word Problems",
|
||||
shortTitle: "Word Problems",
|
||||
week: 9,
|
||||
description: "Solve real-world problems with ratios",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getUnit(slug: string): Unit | undefined {
|
||||
return curriculum.find((u) => u.slug === slug);
|
||||
}
|
||||
|
||||
export function getTopic(unitSlug: string, topicSlug: string): Topic | undefined {
|
||||
const unit = getUnit(unitSlug);
|
||||
return unit?.topics.find((t) => t.slug === topicSlug);
|
||||
}
|
||||
|
||||
export function getUnitColor(unitNumber: 1 | 2 | 3 | 4): string {
|
||||
const colors = {
|
||||
1: "unit-1",
|
||||
2: "unit-2",
|
||||
3: "unit-3",
|
||||
4: "unit-4",
|
||||
};
|
||||
return colors[unitNumber];
|
||||
}
|
||||
65
lib/math/decimals.ts
Normal file
65
lib/math/decimals.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export function roundToWholeNumber(value: number): number {
|
||||
return Math.round(value);
|
||||
}
|
||||
|
||||
export function roundToDecimalPlaces(value: number, places: number): number {
|
||||
const factor = Math.pow(10, places);
|
||||
return Math.round(value * factor) / factor;
|
||||
}
|
||||
|
||||
export function roundToSignificantFigures(value: number, sigFigs: number): number {
|
||||
if (value === 0) return 0;
|
||||
const magnitude = Math.floor(Math.log10(Math.abs(value)));
|
||||
const factor = Math.pow(10, sigFigs - 1 - magnitude);
|
||||
return Math.round(value * factor) / factor;
|
||||
}
|
||||
|
||||
export function toStandardForm(value: number): { coefficient: number; exponent: number } {
|
||||
if (value === 0) return { coefficient: 0, exponent: 0 };
|
||||
const exponent = Math.floor(Math.log10(Math.abs(value)));
|
||||
const coefficient = value / Math.pow(10, exponent);
|
||||
return {
|
||||
coefficient: roundToDecimalPlaces(coefficient, 10),
|
||||
exponent,
|
||||
};
|
||||
}
|
||||
|
||||
export function fromStandardForm(coefficient: number, exponent: number): number {
|
||||
return coefficient * Math.pow(10, exponent);
|
||||
}
|
||||
|
||||
export function compareDecimals(a: number, b: number): -1 | 0 | 1 {
|
||||
if (a > b) return 1;
|
||||
if (a < b) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getPlaceValue(value: number): { digit: number; place: string }[] {
|
||||
const places = [
|
||||
"thousands", "hundreds", "tens", "units", "tenths", "hundredths", "thousandths",
|
||||
];
|
||||
const str = Math.abs(value).toFixed(3);
|
||||
const [intPart, decPart] = str.split(".");
|
||||
const paddedInt = intPart.padStart(4, "0");
|
||||
const result: { digit: number; place: string }[] = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
result.push({ digit: parseInt(paddedInt[i]), place: places[i] });
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
result.push({ digit: parseInt(decPart[i]), place: places[4 + i] });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function multiplyByPowerOf10(value: number, power: number): number {
|
||||
return roundToDecimalPlaces(value * Math.pow(10, power), 10);
|
||||
}
|
||||
|
||||
export function divideByPowerOf10(value: number, power: number): number {
|
||||
return roundToDecimalPlaces(value / Math.pow(10, power), 10);
|
||||
}
|
||||
|
||||
export function standardFormToKatex(coefficient: number, exponent: number): string {
|
||||
return `${coefficient} \\times 10^{${exponent}}`;
|
||||
}
|
||||
107
lib/math/fractions.ts
Normal file
107
lib/math/fractions.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export function gcd(a: number, b: number): number {
|
||||
a = Math.abs(a);
|
||||
b = Math.abs(b);
|
||||
while (b) {
|
||||
[a, b] = [b, a % b];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export function lcm(a: number, b: number): number {
|
||||
return Math.abs(a * b) / gcd(a, b);
|
||||
}
|
||||
|
||||
export function simplify(num: number, den: number): [number, number] {
|
||||
if (den === 0) return [num, den];
|
||||
const g = gcd(Math.abs(num), Math.abs(den));
|
||||
const sign = den < 0 ? -1 : 1;
|
||||
return [(num / g) * sign, (den / g) * sign];
|
||||
}
|
||||
|
||||
export function add(
|
||||
n1: number, d1: number,
|
||||
n2: number, d2: number,
|
||||
): [number, number] {
|
||||
const num = n1 * d2 + n2 * d1;
|
||||
const den = d1 * d2;
|
||||
return simplify(num, den);
|
||||
}
|
||||
|
||||
export function subtract(
|
||||
n1: number, d1: number,
|
||||
n2: number, d2: number,
|
||||
): [number, number] {
|
||||
const num = n1 * d2 - n2 * d1;
|
||||
const den = d1 * d2;
|
||||
return simplify(num, den);
|
||||
}
|
||||
|
||||
export function multiply(
|
||||
n1: number, d1: number,
|
||||
n2: number, d2: number,
|
||||
): [number, number] {
|
||||
return simplify(n1 * n2, d1 * d2);
|
||||
}
|
||||
|
||||
export function divide(
|
||||
n1: number, d1: number,
|
||||
n2: number, d2: number,
|
||||
): [number, number] {
|
||||
return simplify(n1 * d2, d1 * n2);
|
||||
}
|
||||
|
||||
export function toDecimal(num: number, den: number): number {
|
||||
return num / den;
|
||||
}
|
||||
|
||||
export function fromDecimal(decimal: number, precision: number = 6): [number, number] {
|
||||
const str = decimal.toFixed(precision);
|
||||
const parts = str.split(".");
|
||||
if (!parts[1]) return [parseInt(parts[0]), 1];
|
||||
const den = Math.pow(10, parts[1].length);
|
||||
const num = Math.round(decimal * den);
|
||||
return simplify(num, den);
|
||||
}
|
||||
|
||||
export function compare(
|
||||
n1: number, d1: number,
|
||||
n2: number, d2: number,
|
||||
): -1 | 0 | 1 {
|
||||
const diff = n1 * d2 - n2 * d1;
|
||||
if (diff > 0) return 1;
|
||||
if (diff < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function fractionOfQuantity(num: number, den: number, quantity: number): number {
|
||||
return (num / den) * quantity;
|
||||
}
|
||||
|
||||
export function wholeFromFraction(num: number, den: number, part: number): number {
|
||||
return (part * den) / num;
|
||||
}
|
||||
|
||||
export function isProper(num: number, den: number): boolean {
|
||||
return Math.abs(num) < Math.abs(den);
|
||||
}
|
||||
|
||||
export function toMixed(num: number, den: number): [number, number, number] {
|
||||
const whole = Math.floor(Math.abs(num) / Math.abs(den));
|
||||
const remainder = Math.abs(num) % Math.abs(den);
|
||||
const sign = (num < 0) !== (den < 0) ? -1 : 1;
|
||||
return [whole * sign, remainder, Math.abs(den)];
|
||||
}
|
||||
|
||||
export function fromMixed(whole: number, num: number, den: number): [number, number] {
|
||||
const sign = whole < 0 ? -1 : 1;
|
||||
return [sign * (Math.abs(whole) * den + num), den];
|
||||
}
|
||||
|
||||
export function isSimplified(num: number, den: number): boolean {
|
||||
return gcd(Math.abs(num), Math.abs(den)) === 1;
|
||||
}
|
||||
|
||||
export function toKatex(num: number, den: number): string {
|
||||
if (den === 1) return `${num}`;
|
||||
return `\\frac{${num}}{${den}}`;
|
||||
}
|
||||
35
lib/math/ratios.ts
Normal file
35
lib/math/ratios.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { gcd } from "./fractions";
|
||||
|
||||
export function simplifyRatio(parts: number[]): number[] {
|
||||
if (parts.length === 0) return [];
|
||||
let g = parts[0];
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
g = gcd(g, parts[i]);
|
||||
}
|
||||
return parts.map((p) => p / g);
|
||||
}
|
||||
|
||||
export function divideInRatio(total: number, parts: number[]): number[] {
|
||||
const sum = parts.reduce((a, b) => a + b, 0);
|
||||
const onePart = total / sum;
|
||||
return parts.map((p) => p * onePart);
|
||||
}
|
||||
|
||||
export function ratioToFraction(a: number, b: number): [number, number] {
|
||||
return [a, a + b];
|
||||
}
|
||||
|
||||
export function areEquivalent(ratio1: number[], ratio2: number[]): boolean {
|
||||
if (ratio1.length !== ratio2.length) return false;
|
||||
const s1 = simplifyRatio(ratio1);
|
||||
const s2 = simplifyRatio(ratio2);
|
||||
return s1.every((v, i) => v === s2[i]);
|
||||
}
|
||||
|
||||
export function totalParts(parts: number[]): number {
|
||||
return parts.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function ratioToKatex(parts: number[]): string {
|
||||
return parts.join(":");
|
||||
}
|
||||
94
lib/math/validation.ts
Normal file
94
lib/math/validation.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { gcd } from "./fractions";
|
||||
import { simplifyRatio } from "./ratios";
|
||||
|
||||
export type AnswerResult =
|
||||
| { correct: true; simplified: boolean }
|
||||
| { correct: false; message: string };
|
||||
|
||||
export function checkFractionAnswer(
|
||||
userNum: number,
|
||||
userDen: number,
|
||||
expectedNum: number,
|
||||
expectedDen: number,
|
||||
requireSimplified: boolean = true,
|
||||
): AnswerResult {
|
||||
if (userDen === 0) {
|
||||
return { correct: false, message: "Denominator cannot be zero" };
|
||||
}
|
||||
|
||||
// Check mathematical equivalence
|
||||
const isEquivalent = userNum * expectedDen === expectedNum * userDen;
|
||||
|
||||
if (!isEquivalent) {
|
||||
return { correct: false, message: "That's not quite right. Try again!" };
|
||||
}
|
||||
|
||||
const isSimplified = gcd(Math.abs(userNum), Math.abs(userDen)) === 1;
|
||||
|
||||
if (requireSimplified && !isSimplified) {
|
||||
return { correct: true, simplified: false };
|
||||
}
|
||||
|
||||
return { correct: true, simplified: true };
|
||||
}
|
||||
|
||||
export function checkDecimalAnswer(
|
||||
userValue: number,
|
||||
expectedValue: number,
|
||||
tolerance: number = 0.001,
|
||||
): AnswerResult {
|
||||
if (Math.abs(userValue - expectedValue) <= tolerance) {
|
||||
return { correct: true, simplified: true };
|
||||
}
|
||||
return { correct: false, message: "That's not quite right. Try again!" };
|
||||
}
|
||||
|
||||
export function checkRatioAnswer(
|
||||
userParts: number[],
|
||||
expectedParts: number[],
|
||||
requireSimplified: boolean = true,
|
||||
): AnswerResult {
|
||||
if (userParts.length !== expectedParts.length) {
|
||||
return { correct: false, message: "Check the number of parts in your ratio" };
|
||||
}
|
||||
|
||||
const userSimplified = simplifyRatio(userParts);
|
||||
const expectedSimplified = simplifyRatio(expectedParts);
|
||||
|
||||
const isEquivalent = userSimplified.every((v, i) => v === expectedSimplified[i]);
|
||||
|
||||
if (!isEquivalent) {
|
||||
return { correct: false, message: "That's not quite right. Try again!" };
|
||||
}
|
||||
|
||||
if (requireSimplified) {
|
||||
const isAlreadySimplified = userParts.every((v, i) => v === userSimplified[i]);
|
||||
return { correct: true, simplified: isAlreadySimplified };
|
||||
}
|
||||
|
||||
return { correct: true, simplified: true };
|
||||
}
|
||||
|
||||
export function checkIntegerAnswer(
|
||||
userValue: number,
|
||||
expectedValue: number,
|
||||
): AnswerResult {
|
||||
if (userValue === expectedValue) {
|
||||
return { correct: true, simplified: true };
|
||||
}
|
||||
return { correct: false, message: "That's not quite right. Try again!" };
|
||||
}
|
||||
|
||||
export function checkOrderingAnswer(
|
||||
userOrder: number[],
|
||||
expectedOrder: number[],
|
||||
): AnswerResult {
|
||||
if (userOrder.length !== expectedOrder.length) {
|
||||
return { correct: false, message: "Make sure you've ordered all the numbers" };
|
||||
}
|
||||
const isCorrect = userOrder.every((v, i) => v === expectedOrder[i]);
|
||||
if (isCorrect) {
|
||||
return { correct: true, simplified: true };
|
||||
}
|
||||
return { correct: false, message: "Check your ordering. Try again!" };
|
||||
}
|
||||
287
lib/problems/generators/decimal-problems.ts
Normal file
287
lib/problems/generators/decimal-problems.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import type { MathProblem, Difficulty } from "../types";
|
||||
import { randomInt, randomChoice, shuffle } from "@/lib/utils";
|
||||
import { roundToDecimalPlaces, roundToSignificantFigures, toStandardForm, standardFormToKatex } from "@/lib/math/decimals";
|
||||
|
||||
let counter = 0;
|
||||
function nextId() {
|
||||
return `dp-${++counter}-${Date.now()}`;
|
||||
}
|
||||
|
||||
export function generateDecimalCompareOrder(difficulty: Difficulty): MathProblem {
|
||||
const count = difficulty === 1 ? 3 : difficulty === 2 ? 4 : 5;
|
||||
const values: number[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const intPart = randomInt(0, difficulty === 1 ? 2 : 10);
|
||||
const decPlaces = randomInt(1, difficulty === 1 ? 2 : 3);
|
||||
const decPart = randomInt(1, Math.pow(10, decPlaces) - 1);
|
||||
const val = intPart + decPart / Math.pow(10, decPlaces);
|
||||
values.push(Math.round(val * 1000) / 1000);
|
||||
}
|
||||
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const shuffled = shuffle(values);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Order from smallest to largest: } ${shuffled.join(", ")}`,
|
||||
answer: { kind: "decimal", value: 0 },
|
||||
hints: [
|
||||
"Compare the whole number part first",
|
||||
"If the whole parts are equal, compare tenths, then hundredths",
|
||||
"Writing them with the same number of decimal places can help",
|
||||
],
|
||||
steps: sorted.map((v, i) => ({
|
||||
explanation: `Position ${i + 1}: ${v}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDecimalRounding(difficulty: Difficulty): MathProblem {
|
||||
const value = randomInt(100, 99999) / (difficulty === 1 ? 100 : 1000);
|
||||
const roundType = randomChoice(
|
||||
difficulty === 1
|
||||
? ["whole", "1dp"] as const
|
||||
: difficulty === 2
|
||||
? ["1dp", "2dp", "whole"] as const
|
||||
: ["1sf", "2sf", "3sf"] as const,
|
||||
);
|
||||
|
||||
let answer: number;
|
||||
let instruction: string;
|
||||
|
||||
switch (roundType) {
|
||||
case "whole":
|
||||
answer = Math.round(value);
|
||||
instruction = "the nearest whole number";
|
||||
break;
|
||||
case "1dp":
|
||||
answer = roundToDecimalPlaces(value, 1);
|
||||
instruction = "1 decimal place";
|
||||
break;
|
||||
case "2dp":
|
||||
answer = roundToDecimalPlaces(value, 2);
|
||||
instruction = "2 decimal places";
|
||||
break;
|
||||
case "1sf":
|
||||
answer = roundToSignificantFigures(value, 1);
|
||||
instruction = "1 significant figure";
|
||||
break;
|
||||
case "2sf":
|
||||
answer = roundToSignificantFigures(value, 2);
|
||||
instruction = "2 significant figures";
|
||||
break;
|
||||
case "3sf":
|
||||
answer = roundToSignificantFigures(value, 3);
|
||||
instruction = "3 significant figures";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Round } ${value} \\text{ to ${instruction}}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
"Look at the digit to the right of where you're rounding",
|
||||
"If it's 5 or more, round up. If it's less than 5, round down",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Rounding ${value} to ${instruction}` },
|
||||
{ explanation: `Answer: ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateStandardForm(difficulty: Difficulty): MathProblem {
|
||||
let value: number;
|
||||
if (difficulty === 1) {
|
||||
value = randomChoice([7438, 1578, 62, 93000, 5200, 340]);
|
||||
} else if (difficulty === 2) {
|
||||
value = randomChoice([0.086, 0.748, 0.0055, 0.34, 0.007]);
|
||||
} else {
|
||||
value = randomChoice([12436.3, 0.00092, 504000, 0.0000067, 83100]);
|
||||
}
|
||||
|
||||
const sf = toStandardForm(value);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Express } ${value} \\text{ in standard form}`,
|
||||
answer: { kind: "standardForm", coefficient: sf.coefficient, exponent: sf.exponent },
|
||||
hints: [
|
||||
"Move the decimal point so there's one non-zero digit before it",
|
||||
"Count how many places you moved the decimal",
|
||||
value >= 1
|
||||
? "Number is ≥ 1, so the power is positive"
|
||||
: "Number is < 1, so the power is negative",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Move the decimal point to get a number between 1 and 10` },
|
||||
{ explanation: `Coefficient: ${sf.coefficient}` },
|
||||
{ explanation: `The decimal moved ${Math.abs(sf.exponent)} places, power = ${sf.exponent}` },
|
||||
{ explanation: "Answer", math: standardFormToKatex(sf.coefficient, sf.exponent) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDecimalAddSubtract(difficulty: Difficulty): MathProblem {
|
||||
const operation = randomChoice(["add", "subtract"] as const);
|
||||
const op = operation === "add" ? "+" : "-";
|
||||
const places = difficulty === 1 ? 1 : difficulty === 2 ? 2 : 3;
|
||||
|
||||
const a = randomInt(100, 9999) / Math.pow(10, places);
|
||||
const b = randomInt(100, 9999) / Math.pow(10, places);
|
||||
const [first, second] = operation === "subtract" ? [Math.max(a, b), Math.min(a, b)] : [a, b];
|
||||
|
||||
const answer = roundToDecimalPlaces(
|
||||
operation === "add" ? first + second : first - second,
|
||||
places,
|
||||
);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `${first} ${op} ${second}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
"Line up the decimal points",
|
||||
"Add zeros to fill empty places",
|
||||
operation === "add" ? "Add column by column from right to left" : "Subtract column by column, borrow if needed",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Line up: ${first} ${op} ${second}` },
|
||||
{ explanation: `= ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDecimalMultiplyDivide(difficulty: Difficulty): MathProblem {
|
||||
const operation = randomChoice(["multiply", "divide"] as const);
|
||||
|
||||
if (difficulty === 1) {
|
||||
// By powers of 10
|
||||
const power = randomChoice([10, 100, 1000]);
|
||||
const base = randomInt(1, 999) / 100;
|
||||
|
||||
if (operation === "multiply") {
|
||||
const answer = roundToDecimalPlaces(base * power, 10);
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `${base} \\times ${power}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
`When multiplying by ${power}, move decimal ${Math.log10(power)} place(s) right`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Move decimal point ${Math.log10(power)} place(s) to the right` },
|
||||
{ explanation: `= ${answer}` },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const answer = roundToDecimalPlaces(base / power, 10);
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `${base} \\div ${power}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
`When dividing by ${power}, move decimal ${Math.log10(power)} place(s) left`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Move decimal point ${Math.log10(power)} place(s) to the left` },
|
||||
{ explanation: `= ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// By whole numbers or decimals
|
||||
const a = randomInt(10, 999) / 10;
|
||||
const b = difficulty === 2 ? randomInt(2, 12) : randomInt(10, 99) / 10;
|
||||
|
||||
if (operation === "multiply") {
|
||||
const answer = roundToDecimalPlaces(a * b, 4);
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `${a} \\times ${b}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
"Multiply as if there are no decimal points",
|
||||
"Count total decimal places in both numbers",
|
||||
"Place the decimal point in your answer",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `${a} × ${b}` },
|
||||
{ explanation: `= ${answer}` },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const answer = roundToDecimalPlaces(a / b, 4);
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `${a} \\div ${b}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
"Place decimal point in answer directly above the one in the dividend",
|
||||
"Divide as normal",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `${a} ÷ ${b}` },
|
||||
{ explanation: `= ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function generateDecimalConversion(difficulty: Difficulty): MathProblem {
|
||||
const direction = randomChoice(["toFraction", "toDecimal"] as const);
|
||||
|
||||
if (direction === "toFraction") {
|
||||
const decimal = randomChoice(
|
||||
difficulty === 1
|
||||
? [0.5, 0.25, 0.75, 0.1, 0.2, 0.4]
|
||||
: [0.125, 0.375, 0.625, 0.875, 0.15, 0.35, 0.45],
|
||||
);
|
||||
const den = decimal === 0.5 ? 2 : decimal === 0.25 || decimal === 0.75 ? 4 : [0.125, 0.375, 0.625, 0.875].includes(decimal) ? 8 : 100;
|
||||
const num = Math.round(decimal * den);
|
||||
const g = gcdSimple(num, den);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Convert } ${decimal} \\text{ to a fraction}`,
|
||||
answer: { kind: "fraction", numerator: num / g, denominator: den / g },
|
||||
hints: [
|
||||
`Write as ${num}/${den}`,
|
||||
"Simplify by dividing numerator and denominator by their GCD",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `${decimal} = ${num}/${den}` },
|
||||
{ explanation: `Simplified: ${num / g}/${den / g}` },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const den = randomChoice(difficulty === 1 ? [2, 4, 5, 10] : [3, 8, 20, 25, 40, 50]);
|
||||
const num = randomInt(1, den - 1);
|
||||
const answer = roundToDecimalPlaces(num / den, 6);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Convert } \\frac{${num}}{${den}} \\text{ to a decimal}`,
|
||||
answer: { kind: "decimal", value: answer },
|
||||
hints: [
|
||||
`Divide ${num} by ${den}`,
|
||||
"Or find an equivalent fraction with denominator 10, 100, etc.",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `${num} ÷ ${den} = ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function gcdSimple(a: number, b: number): number {
|
||||
a = Math.abs(a);
|
||||
b = Math.abs(b);
|
||||
while (b) {
|
||||
[a, b] = [b, a % b];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
163
lib/problems/generators/fraction-problems.ts
Normal file
163
lib/problems/generators/fraction-problems.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { MathProblem, Difficulty } from "../types";
|
||||
import { randomInt, randomChoice } from "@/lib/utils";
|
||||
import * as frac from "@/lib/math/fractions";
|
||||
|
||||
let counter = 0;
|
||||
function nextId() {
|
||||
return `fp-${++counter}-${Date.now()}`;
|
||||
}
|
||||
|
||||
export function generateFractionAddSubtract(difficulty: Difficulty): MathProblem {
|
||||
const operation = randomChoice(["add", "subtract"] as const);
|
||||
const op = operation === "add" ? "+" : "-";
|
||||
|
||||
let n1: number, d1: number, n2: number, d2: number;
|
||||
|
||||
if (difficulty === 1) {
|
||||
// Common denominators
|
||||
d1 = randomChoice([2, 3, 4, 5, 6, 8, 10, 12]);
|
||||
d2 = d1;
|
||||
n1 = randomInt(1, d1 - 1);
|
||||
n2 = operation === "subtract" ? randomInt(1, n1) : randomInt(1, d1 - n1);
|
||||
} else if (difficulty === 2) {
|
||||
// One denominator is a multiple of the other
|
||||
d1 = randomChoice([2, 3, 4, 5, 6]);
|
||||
const multiplier = randomChoice([2, 3]);
|
||||
d2 = d1 * multiplier;
|
||||
n1 = randomInt(1, d1 - 1);
|
||||
n2 = randomInt(1, d2 - 1);
|
||||
} else {
|
||||
// Unlike denominators
|
||||
d1 = randomChoice([3, 4, 5, 7, 8, 9]);
|
||||
d2 = randomChoice([3, 4, 5, 7, 8, 9].filter((d) => d !== d1));
|
||||
n1 = randomInt(1, d1 - 1);
|
||||
n2 = randomInt(1, d2 - 1);
|
||||
}
|
||||
|
||||
const [ansNum, ansDen] = operation === "add"
|
||||
? frac.add(n1, d1, n2, d2)
|
||||
: frac.subtract(n1, d1, n2, d2);
|
||||
|
||||
const prompt = `\\frac{${n1}}{${d1}} ${op} \\frac{${n2}}{${d2}}`;
|
||||
|
||||
const commonDen = frac.lcm(d1, d2);
|
||||
const steps = d1 === d2
|
||||
? [
|
||||
{ explanation: "Denominators are the same, so just " + (operation === "add" ? "add" : "subtract") + " the numerators" },
|
||||
{ explanation: `${n1} ${op} ${n2} = ${operation === "add" ? n1 + n2 : n1 - n2}`, math: `\\frac{${operation === "add" ? n1 + n2 : n1 - n2}}{${d1}}` },
|
||||
]
|
||||
: [
|
||||
{ explanation: `Find the common denominator: LCM(${d1}, ${d2}) = ${commonDen}` },
|
||||
{ explanation: `Convert: ${n1}×${commonDen / d1}/${d1}×${commonDen / d1} and ${n2}×${commonDen / d2}/${d2}×${commonDen / d2}` },
|
||||
{ explanation: `${op === "+" ? "Add" : "Subtract"} numerators`, math: `\\frac{${ansNum}}{${ansDen}}` },
|
||||
];
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt,
|
||||
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
|
||||
hints: [
|
||||
d1 === d2 ? "The denominators are already the same!" : "Find a common denominator first",
|
||||
d1 !== d2 ? `Try using the Butterfly Method: cross multiply` : `Just ${operation} the numerators`,
|
||||
],
|
||||
steps,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateFractionMultiply(difficulty: Difficulty): MathProblem {
|
||||
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [2, 3, 4, 5, 6, 7, 8] : [3, 5, 7, 9, 11, 12, 15];
|
||||
const d1 = randomChoice(denChoices);
|
||||
const d2 = randomChoice(denChoices);
|
||||
const n1 = randomInt(1, d1 - 1);
|
||||
const n2 = randomInt(1, d2 - 1);
|
||||
|
||||
const [ansNum, ansDen] = frac.multiply(n1, d1, n2, d2);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\frac{${n1}}{${d1}} \\times \\frac{${n2}}{${d2}}`,
|
||||
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
|
||||
hints: [
|
||||
"Multiply numerator × numerator",
|
||||
"Multiply denominator × denominator",
|
||||
"Simplify if possible",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Multiply numerators: ${n1} × ${n2} = ${n1 * n2}` },
|
||||
{ explanation: `Multiply denominators: ${d1} × ${d2} = ${d1 * d2}` },
|
||||
{ explanation: "Simplify", math: `\\frac{${ansNum}}{${ansDen}}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateFractionDivide(difficulty: Difficulty): MathProblem {
|
||||
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [2, 3, 4, 5, 6, 7, 8] : [3, 5, 7, 9, 11, 14, 15];
|
||||
const d1 = randomChoice(denChoices);
|
||||
const d2 = randomChoice(denChoices);
|
||||
const n1 = randomInt(1, d1 - 1);
|
||||
const n2 = randomInt(1, d2 - 1);
|
||||
|
||||
const [ansNum, ansDen] = frac.divide(n1, d1, n2, d2);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\frac{${n1}}{${d1}} \\div \\frac{${n2}}{${d2}}`,
|
||||
answer: { kind: "fraction", numerator: ansNum, denominator: ansDen },
|
||||
hints: [
|
||||
"Keep the first fraction the same",
|
||||
"Flip the second fraction (reciprocal)",
|
||||
"Then multiply",
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Keep ${n1}/${d1}, flip ${n2}/${d2} to ${d2}/${n2}` },
|
||||
{ explanation: `Multiply: ${n1}×${d2} / ${d1}×${n2}` },
|
||||
{ explanation: "Simplify", math: `\\frac{${ansNum}}{${ansDen}}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateFractionOfQuantity(difficulty: Difficulty): MathProblem {
|
||||
const denChoices = difficulty === 1 ? [2, 4, 5, 10] : difficulty === 2 ? [3, 4, 5, 6, 8] : [7, 8, 9, 12, 15];
|
||||
const den = randomChoice(denChoices);
|
||||
const num = randomInt(1, den - 1);
|
||||
const multiplier = randomChoice(difficulty === 1 ? [2, 4, 5, 10] : [3, 6, 7, 8, 9, 12]);
|
||||
const quantity = den * multiplier;
|
||||
const answer = num * multiplier;
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\frac{${num}}{${den}} \\text{ of } ${quantity}`,
|
||||
answer: { kind: "integer", value: answer },
|
||||
hints: [
|
||||
`Divide ${quantity} by ${den} first`,
|
||||
`Then multiply by ${num}`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Divide the quantity by the denominator: ${quantity} ÷ ${den} = ${quantity / den}` },
|
||||
{ explanation: `Multiply by the numerator: ${quantity / den} × ${num} = ${answer}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateWholeFromFraction(difficulty: Difficulty): MathProblem {
|
||||
const denChoices = difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [3, 5, 6, 8] : [7, 8, 9, 12];
|
||||
const den = randomChoice(denChoices);
|
||||
const num = randomInt(1, den - 1);
|
||||
const whole = den * randomChoice(difficulty === 1 ? [2, 3, 4, 5] : [3, 5, 6, 7, 8, 10]);
|
||||
const part = (num / den) * whole;
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\frac{${num}}{${den}} \\text{ of a number is } ${part}\\text{. Find the number.}`,
|
||||
answer: { kind: "integer", value: whole },
|
||||
hints: [
|
||||
`If ${num}/${den} of the number is ${part}...`,
|
||||
`Find 1/${den} first by dividing ${part} by ${num}`,
|
||||
`Then multiply by ${den} to get the whole`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `1/${den} of the number = ${part} ÷ ${num} = ${part / num}` },
|
||||
{ explanation: `The whole number = ${part / num} × ${den} = ${whole}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
110
lib/problems/generators/ratio-problems.ts
Normal file
110
lib/problems/generators/ratio-problems.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { MathProblem, Difficulty } from "../types";
|
||||
import { randomInt, randomChoice } from "@/lib/utils";
|
||||
import { simplifyRatio, divideInRatio } from "@/lib/math/ratios";
|
||||
|
||||
let counter = 0;
|
||||
function nextId() {
|
||||
return `rp-${++counter}-${Date.now()}`;
|
||||
}
|
||||
|
||||
export function generateSimplifyRatio(difficulty: Difficulty): MathProblem {
|
||||
const gcd = randomChoice(difficulty === 1 ? [2, 3, 4, 5] : difficulty === 2 ? [3, 4, 5, 6, 7] : [4, 6, 8, 9, 12]);
|
||||
const a = randomInt(1, difficulty === 1 ? 5 : 10) * gcd;
|
||||
const b = randomInt(1, difficulty === 1 ? 5 : 10) * gcd;
|
||||
|
||||
const simplified = simplifyRatio([a, b]);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Simplify the ratio } ${a}:${b}`,
|
||||
answer: { kind: "ratio", parts: simplified },
|
||||
hints: [
|
||||
`Find the GCD of ${a} and ${b}`,
|
||||
`The GCD is ${gcd}`,
|
||||
`Divide both parts by ${gcd}`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `GCD of ${a} and ${b} is ${gcd}` },
|
||||
{ explanation: `${a} ÷ ${gcd} = ${simplified[0]}, ${b} ÷ ${gcd} = ${simplified[1]}` },
|
||||
{ explanation: `Simplified: ${simplified.join(":")}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateDivideInRatio(difficulty: Difficulty): MathProblem {
|
||||
const numParts = difficulty === 3 ? 3 : 2;
|
||||
const parts: number[] = [];
|
||||
for (let i = 0; i < numParts; i++) {
|
||||
parts.push(randomInt(1, difficulty === 1 ? 5 : 9));
|
||||
}
|
||||
|
||||
const sum = parts.reduce((a, b) => a + b, 0);
|
||||
const multiplier = randomChoice(difficulty === 1 ? [2, 3, 4, 5] : [3, 4, 5, 6, 7, 8]);
|
||||
const total = sum * multiplier;
|
||||
|
||||
const values = divideInRatio(total, parts);
|
||||
const intValues = values.map(Math.round);
|
||||
|
||||
const names = ["first share", "second share", "third share"];
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{Share } ${total} \\text{ in the ratio } ${parts.join(":")}`,
|
||||
answer: { kind: "ratio", parts: intValues },
|
||||
hints: [
|
||||
`Total parts: ${parts.join(" + ")} = ${sum}`,
|
||||
`One part = ${total} ÷ ${sum} = ${multiplier}`,
|
||||
`Multiply each ratio number by ${multiplier}`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Total parts = ${parts.join(" + ")} = ${sum}` },
|
||||
{ explanation: `One part = ${total} ÷ ${sum} = ${multiplier}` },
|
||||
...parts.map((p, i) => ({
|
||||
explanation: `${names[i]}: ${p} × ${multiplier} = ${intValues[i]}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateRatioWordProblem(difficulty: Difficulty): MathProblem {
|
||||
const a = randomInt(2, 7);
|
||||
const b = randomInt(2, 7);
|
||||
const diff = Math.abs(a - b);
|
||||
|
||||
if (diff === 0) {
|
||||
return generateDivideInRatio(difficulty);
|
||||
}
|
||||
|
||||
const contexts = [
|
||||
{ item: "sweets", nameA: "Josh", nameB: "Nathan" },
|
||||
{ item: "marbles", nameA: "Amy", nameB: "Ben" },
|
||||
{ item: "stickers", nameA: "Karen", nameB: "Natasha" },
|
||||
];
|
||||
const ctx = randomChoice(contexts);
|
||||
|
||||
const onePart = randomChoice(difficulty === 1 ? [5, 10, 12] : [6, 8, 9, 15, 20]);
|
||||
const extraAmount = diff * onePart;
|
||||
const totalParts = a + b;
|
||||
const total = totalParts * onePart;
|
||||
|
||||
const bigger = a > b ? ctx.nameA : ctx.nameB;
|
||||
const biggerRatio = Math.max(a, b);
|
||||
const smallerRatio = Math.min(a, b);
|
||||
|
||||
return {
|
||||
id: nextId(),
|
||||
prompt: `\\text{${ctx.item.charAt(0).toUpperCase() + ctx.item.slice(1)} were shared between ${ctx.nameA} and ${ctx.nameB} in the ratio ${a}:${b}. If ${bigger} received ${extraAmount} more ${ctx.item}, find the total shared.}`,
|
||||
answer: { kind: "integer", value: total },
|
||||
hints: [
|
||||
`The difference in ratio parts is ${biggerRatio} - ${smallerRatio} = ${diff}`,
|
||||
`${diff} parts = ${extraAmount}, so 1 part = ${onePart}`,
|
||||
`Total parts = ${a} + ${b} = ${totalParts}`,
|
||||
],
|
||||
steps: [
|
||||
{ explanation: `Difference in parts: ${biggerRatio} - ${smallerRatio} = ${diff}` },
|
||||
{ explanation: `${diff} parts = ${extraAmount}, so 1 part = ${extraAmount} ÷ ${diff} = ${onePart}` },
|
||||
{ explanation: `Total parts: ${a} + ${b} = ${totalParts}` },
|
||||
{ explanation: `Total ${ctx.item}: ${totalParts} × ${onePart} = ${total}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
21
lib/problems/types.ts
Normal file
21
lib/problems/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type Difficulty = 1 | 2 | 3;
|
||||
|
||||
export type MathAnswer =
|
||||
| { kind: "fraction"; numerator: number; denominator: number }
|
||||
| { kind: "decimal"; value: number }
|
||||
| { kind: "integer"; value: number }
|
||||
| { kind: "ratio"; parts: number[] }
|
||||
| { kind: "standardForm"; coefficient: number; exponent: number };
|
||||
|
||||
export interface SolutionStep {
|
||||
explanation: string;
|
||||
math?: string;
|
||||
}
|
||||
|
||||
export interface MathProblem {
|
||||
id: string;
|
||||
prompt: string;
|
||||
answer: MathAnswer;
|
||||
hints: string[];
|
||||
steps: SolutionStep[];
|
||||
}
|
||||
23
lib/utils.ts
Normal file
23
lib/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function randomChoice<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
export function shuffle<T>(arr: T[]): T[] {
|
||||
const result = [...arr];
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[result[i], result[j]] = [result[j], result[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user