635 lines
21 KiB
Go
635 lines
21 KiB
Go
package questiongen
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"boostai-backend/internal/sqlc"
|
||
)
|
||
|
||
type Service struct{}
|
||
|
||
type GenerateParams struct {
|
||
Topic sqlc.QuestionTopic
|
||
Difficulty sqlc.QuestionDifficulty
|
||
Count int
|
||
Seed int64
|
||
}
|
||
|
||
type GeneratedQuestion struct {
|
||
Title string
|
||
Prompt string
|
||
CorrectAnswer string
|
||
WorkedSolution []string
|
||
Tags []string
|
||
}
|
||
|
||
func NewService() *Service {
|
||
return &Service{}
|
||
}
|
||
|
||
func (s *Service) Generate(params GenerateParams) ([]GeneratedQuestion, int64, error) {
|
||
count := params.Count
|
||
if count <= 0 {
|
||
count = 1
|
||
}
|
||
|
||
seed := params.Seed
|
||
if seed == 0 {
|
||
seed = time.Now().UnixNano()
|
||
}
|
||
|
||
rng := rand.New(rand.NewSource(seed))
|
||
items := make([]GeneratedQuestion, 0, count)
|
||
for i := 0; i < count; i++ {
|
||
question, err := s.generateOne(rng, params.Topic, params.Difficulty)
|
||
if err != nil {
|
||
return nil, seed, err
|
||
}
|
||
items = append(items, question)
|
||
}
|
||
|
||
return items, seed, nil
|
||
}
|
||
|
||
func (s *Service) generateOne(rng *rand.Rand, topic sqlc.QuestionTopic, difficulty sqlc.QuestionDifficulty) (GeneratedQuestion, error) {
|
||
switch topic {
|
||
case sqlc.QuestionTopicPlaceValue:
|
||
return generatePlaceValueQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicArithmetic:
|
||
return generateArithmeticQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicNegativeNumbers:
|
||
return generateNegativeNumbersQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicBidmas:
|
||
return generateBidmasQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicFractions:
|
||
return generateFractionsQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicAlgebra:
|
||
return generateAlgebraQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicGeometry:
|
||
return generateGeometryQuestion(rng, difficulty), nil
|
||
case sqlc.QuestionTopicData:
|
||
return generateDataQuestion(rng, difficulty), nil
|
||
default:
|
||
return GeneratedQuestion{}, fmt.Errorf("unsupported topic: %s", topic)
|
||
}
|
||
}
|
||
|
||
// Future word_problem work should not just bolt a `word_problem` tag onto an already-built
|
||
// abstract question. Each topic generator should eventually expose dedicated word-problem
|
||
// template families so the RNG chooses both the maths structure and a fitting real-world context
|
||
// together. That will keep prompts, answers, and worked steps consistent instead of doing a late
|
||
// text rewrite after the numbers are chosen.
|
||
func buildGeneratedTags(topic sqlc.QuestionTopic, difficulty sqlc.QuestionDifficulty, extra ...string) []string {
|
||
tags := []string{string(topic), string(difficulty), "rng_generated"}
|
||
tags = append(tags, extra...)
|
||
|
||
unique := make(map[string]struct{}, len(tags))
|
||
normalized := make([]string, 0, len(tags))
|
||
for _, tag := range tags {
|
||
value := strings.ToLower(strings.TrimSpace(tag))
|
||
if value == "" {
|
||
continue
|
||
}
|
||
if _, exists := unique[value]; exists {
|
||
continue
|
||
}
|
||
unique[value] = struct{}{}
|
||
normalized = append(normalized, value)
|
||
}
|
||
sort.Strings(normalized)
|
||
return normalized
|
||
}
|
||
|
||
func generatePlaceValueQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
var digits, targetIndex int
|
||
switch difficulty {
|
||
case sqlc.QuestionDifficultyEasy:
|
||
digits = 2
|
||
targetIndex = randomInt(rng, 0, 1)
|
||
case sqlc.QuestionDifficultyMedium:
|
||
digits = 3
|
||
targetIndex = randomInt(rng, 0, 2)
|
||
default:
|
||
digits = randomInt(rng, 4, 5)
|
||
targetIndex = randomInt(rng, 1, digits-1)
|
||
}
|
||
|
||
numberDigits := make([]int, digits)
|
||
numberDigits[0] = randomInt(rng, 1, 9)
|
||
for i := 1; i < digits; i++ {
|
||
numberDigits[i] = randomInt(rng, 0, 9)
|
||
}
|
||
number := digitsToInt(numberDigits)
|
||
digit := numberDigits[targetIndex]
|
||
placePower := digits - targetIndex - 1
|
||
placeValue := digit
|
||
for i := 0; i < placePower; i++ {
|
||
placeValue *= 10
|
||
}
|
||
|
||
placeName := placeNameFromPower(placePower)
|
||
prompt := fmt.Sprintf("What is the value of the digit %d in %d?", digit, number)
|
||
return GeneratedQuestion{
|
||
Title: fmt.Sprintf("%s Place Value", strings.Title(string(difficulty))),
|
||
Prompt: prompt,
|
||
CorrectAnswer: fmt.Sprintf("%d", placeValue),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("In %d, the digit %d is in the %s place.", number, digit, placeName),
|
||
fmt.Sprintf("So its value is %d.", placeValue),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicPlaceValue, difficulty, placeName),
|
||
}
|
||
}
|
||
|
||
func generateArithmeticQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
a := randomInt(rng, 1, 9)
|
||
b := randomInt(rng, 1, 9)
|
||
if rng.Intn(2) == 0 {
|
||
return GeneratedQuestion{
|
||
Title: "Easy Addition",
|
||
Prompt: fmt.Sprintf("Calculate %d + %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", a+b),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Add the ones: %d + %d = %d.", a, b, a+b),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "addition", "single_digit"),
|
||
}
|
||
}
|
||
|
||
if a < b {
|
||
a, b = b, a
|
||
}
|
||
return GeneratedQuestion{
|
||
Title: "Easy Subtraction",
|
||
Prompt: fmt.Sprintf("Calculate %d - %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", a-b),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Subtract %d from %d.", b, a),
|
||
fmt.Sprintf("%d - %d = %d.", a, b, a-b),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "subtraction", "single_digit"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
if rng.Intn(2) == 0 {
|
||
a := randomInt(rng, 10, 99)
|
||
b := randomInt(rng, 10, 99)
|
||
return GeneratedQuestion{
|
||
Title: "Medium Addition",
|
||
Prompt: fmt.Sprintf("Work out %d + %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", a+b),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Add the numbers together: %d + %d = %d.", a, b, a+b),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "addition", "two_digit"),
|
||
}
|
||
}
|
||
|
||
a := randomInt(rng, 2, 12)
|
||
b := randomInt(rng, 2, 12)
|
||
return GeneratedQuestion{
|
||
Title: "Medium Multiplication",
|
||
Prompt: fmt.Sprintf("Calculate %d × %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", a*b),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Use multiplication facts: %d × %d = %d.", a, b, a*b),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "multiplication", "times_tables"),
|
||
}
|
||
}
|
||
|
||
if rng.Intn(2) == 0 {
|
||
divisor := randomInt(rng, 3, 12)
|
||
quotient := randomInt(rng, 4, 12)
|
||
dividend := divisor * quotient
|
||
return GeneratedQuestion{
|
||
Title: "Hard Division",
|
||
Prompt: fmt.Sprintf("Calculate %d ÷ %d.", dividend, divisor),
|
||
CorrectAnswer: fmt.Sprintf("%d", quotient),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Use the inverse of multiplication: %d × %d = %d.", divisor, quotient, dividend),
|
||
fmt.Sprintf("So %d ÷ %d = %d.", dividend, divisor, quotient),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "division", "inverse_operations"),
|
||
}
|
||
}
|
||
|
||
a := randomInt(rng, 20, 99)
|
||
b := randomInt(rng, 11, 49)
|
||
return GeneratedQuestion{
|
||
Title: "Hard Subtraction",
|
||
Prompt: fmt.Sprintf("Work out %d - %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", a-b),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Subtract %d from %d carefully, using column subtraction if needed.", b, a),
|
||
fmt.Sprintf("%d - %d = %d.", a, b, a-b),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "subtraction", "column_method"),
|
||
}
|
||
}
|
||
|
||
func generateNegativeNumbersQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
a := randomInt(rng, -9, 9)
|
||
b := randomInt(rng, -9, 9)
|
||
result := a + b
|
||
return GeneratedQuestion{
|
||
Title: "Easy Negative Numbers",
|
||
Prompt: fmt.Sprintf("Calculate %d + %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Start at %d on the number line.", a),
|
||
fmt.Sprintf("Move %d steps to get %d.", b, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "addition"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
a := randomInt(rng, -20, 20)
|
||
b := randomInt(rng, -20, 20)
|
||
result := a - b
|
||
return GeneratedQuestion{
|
||
Title: "Medium Negative Numbers",
|
||
Prompt: fmt.Sprintf("Calculate %d - (%d).", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Subtracting %d is the same as adding %d.", b, -b),
|
||
fmt.Sprintf("So %d - (%d) = %d + %d = %d.", a, b, a, -b, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "subtraction"),
|
||
}
|
||
}
|
||
|
||
a := randomInt(rng, -30, 30)
|
||
b := randomInt(rng, -30, 30)
|
||
c := randomInt(rng, -15, 15)
|
||
result := a - b + c
|
||
prompt := fmt.Sprintf("Calculate %d - (%d) + %d.", a, b, c)
|
||
return GeneratedQuestion{
|
||
Title: "Hard Negative Numbers",
|
||
Prompt: prompt,
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("First change subtraction of a negative: %d - (%d) = %d + %d.", a, b, a, -b),
|
||
fmt.Sprintf("Then add %d to get %d.", c, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "multi_step"),
|
||
}
|
||
}
|
||
|
||
func generateBidmasQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
a := randomInt(rng, 1, 20)
|
||
b := randomInt(rng, 2, 9)
|
||
c := randomInt(rng, 2, 9)
|
||
result := a + b*c
|
||
return GeneratedQuestion{
|
||
Title: "Easy BIDMAS",
|
||
Prompt: fmt.Sprintf("Work out %d + %d × %d.", a, b, c),
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Do multiplication first: %d × %d = %d.", b, c, b*c),
|
||
fmt.Sprintf("Then add %d + %d = %d.", a, b*c, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "order_of_operations"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
a := randomInt(rng, 2, 12)
|
||
b := randomInt(rng, 3, 12)
|
||
c := randomInt(rng, 2, 10)
|
||
result := (a + b) * c
|
||
return GeneratedQuestion{
|
||
Title: "Medium BIDMAS",
|
||
Prompt: fmt.Sprintf("Work out (%d + %d) × %d.", a, b, c),
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Work inside brackets first: %d + %d = %d.", a, b, a+b),
|
||
fmt.Sprintf("Then multiply %d × %d = %d.", a+b, c, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "brackets"),
|
||
}
|
||
}
|
||
|
||
a := randomInt(rng, 2, 12)
|
||
b := randomInt(rng, 2, 6)
|
||
c := randomInt(rng, 2, 12)
|
||
d := randomInt(rng, 2, 5)
|
||
left := a * b
|
||
right := c * d
|
||
result := left + right
|
||
return GeneratedQuestion{
|
||
Title: "Hard BIDMAS",
|
||
Prompt: fmt.Sprintf("Work out %d × %d + %d × %d.", a, b, c, d),
|
||
CorrectAnswer: fmt.Sprintf("%d", result),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Complete each multiplication first: %d × %d = %d and %d × %d = %d.", a, b, left, c, d, right),
|
||
fmt.Sprintf("Then add %d + %d = %d.", left, right, result),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "multiple_operations"),
|
||
}
|
||
}
|
||
|
||
func generateFractionsQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
denominator := randomInt(rng, 2, 9)
|
||
numerator := randomInt(rng, 1, denominator-1)
|
||
maxMultiplier := 9 / denominator
|
||
if maxMultiplier < 1 {
|
||
maxMultiplier = 1
|
||
}
|
||
multiplier := randomInt(rng, 1, maxMultiplier)
|
||
prompt := fmt.Sprintf("What is %d/%d of %d?", numerator, denominator, denominator*multiplier)
|
||
answer := numerator * multiplier
|
||
return GeneratedQuestion{
|
||
Title: "Easy Fractions",
|
||
Prompt: prompt,
|
||
CorrectAnswer: fmt.Sprintf("%d", answer),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Find one part first: %d ÷ %d = %d.", denominator*multiplier, denominator, multiplier),
|
||
fmt.Sprintf("Then take %d parts: %d × %d = %d.", numerator, multiplier, numerator, answer),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "single_digit", "fraction_of_amount"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
denominator := randomInt(rng, 3, 10)
|
||
a := randomInt(rng, 1, denominator-1)
|
||
b := randomInt(rng, 1, denominator-1)
|
||
resultN, resultD := simplifyFraction(a+b, denominator)
|
||
return GeneratedQuestion{
|
||
Title: "Medium Fractions",
|
||
Prompt: fmt.Sprintf("Work out %d/%d + %d/%d. Give your answer in simplest form.", a, denominator, b, denominator),
|
||
CorrectAnswer: formatFraction(resultN, resultD),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("The denominators are the same, so add the numerators: %d + %d = %d.", a, b, a+b),
|
||
fmt.Sprintf("This gives %d/%d.", a+b, denominator),
|
||
fmt.Sprintf("Simplify to %s.", formatFraction(resultN, resultD)),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "addition", "simplify"),
|
||
}
|
||
}
|
||
|
||
aN := randomInt(rng, 1, 8)
|
||
aD := randomInt(rng, 2, 9)
|
||
bN := randomInt(rng, 1, 8)
|
||
bD := randomInt(rng, 2, 9)
|
||
resultN, resultD := simplifyFraction(aN*bN, aD*bD)
|
||
return GeneratedQuestion{
|
||
Title: "Hard Fractions",
|
||
Prompt: fmt.Sprintf("Work out %d/%d × %d/%d. Give your answer in simplest form.", aN, aD, bN, bD),
|
||
CorrectAnswer: formatFraction(resultN, resultD),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Multiply the numerators: %d × %d = %d.", aN, bN, aN*bN),
|
||
fmt.Sprintf("Multiply the denominators: %d × %d = %d.", aD, bD, aD*bD),
|
||
fmt.Sprintf("This gives %d/%d, which simplifies to %s.", aN*bN, aD*bD, formatFraction(resultN, resultD)),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "multiplication", "simplify"),
|
||
}
|
||
}
|
||
|
||
func generateAlgebraQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
x := randomInt(rng, 1, 12)
|
||
a := randomInt(rng, 1, 12)
|
||
b := x + a
|
||
return GeneratedQuestion{
|
||
Title: "Easy Algebra",
|
||
Prompt: fmt.Sprintf("Solve x + %d = %d.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("x = %d", x),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Subtract %d from both sides.", a),
|
||
fmt.Sprintf("x = %d - %d = %d.", b, a, x),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "one_step"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
x := randomInt(rng, 2, 12)
|
||
a := randomInt(rng, 2, 9)
|
||
b := randomInt(rng, 1, 12)
|
||
c := a*x + b
|
||
return GeneratedQuestion{
|
||
Title: "Medium Algebra",
|
||
Prompt: fmt.Sprintf("Solve %dx + %d = %d.", a, b, c),
|
||
CorrectAnswer: fmt.Sprintf("x = %d", x),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Subtract %d from both sides to get %dx = %d.", b, a, c-b),
|
||
fmt.Sprintf("Divide both sides by %d, so x = %d.", a, x),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "two_step"),
|
||
}
|
||
}
|
||
|
||
x := randomInt(rng, -6, 12)
|
||
a := randomInt(rng, 2, 6)
|
||
b := randomInt(rng, 1, 8)
|
||
c := randomInt(rng, 2, 6)
|
||
d := a*(x+b) - c
|
||
return GeneratedQuestion{
|
||
Title: "Hard Algebra",
|
||
Prompt: fmt.Sprintf("Solve %d(x + %d) - %d = %d.", a, b, c, d),
|
||
CorrectAnswer: fmt.Sprintf("x = %d", x),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Add %d to both sides: %d(x + %d) = %d.", c, a, b, d+c),
|
||
fmt.Sprintf("Divide by %d: x + %d = %d.", a, b, x+b),
|
||
fmt.Sprintf("Subtract %d, so x = %d.", b, x),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "brackets", "multi_step"),
|
||
}
|
||
}
|
||
|
||
func generateGeometryQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
side := randomInt(rng, 2, 9)
|
||
perimeter := side * 4
|
||
return GeneratedQuestion{
|
||
Title: "Easy Geometry",
|
||
Prompt: fmt.Sprintf("A square has side length %d cm. What is its perimeter?", side),
|
||
CorrectAnswer: fmt.Sprintf("%d cm", perimeter),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("A square has 4 equal sides, so calculate 4 × %d.", side),
|
||
fmt.Sprintf("The perimeter is %d cm.", perimeter),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "perimeter", "square"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
length := randomInt(rng, 4, 15)
|
||
width := randomInt(rng, 3, 12)
|
||
area := length * width
|
||
return GeneratedQuestion{
|
||
Title: "Medium Geometry",
|
||
Prompt: fmt.Sprintf("A rectangle has length %d cm and width %d cm. What is its area?", length, width),
|
||
CorrectAnswer: fmt.Sprintf("%d cm²", area),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Area of a rectangle = length × width."),
|
||
fmt.Sprintf("%d × %d = %d, so the area is %d cm².", length, width, area, area),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "area", "rectangle"),
|
||
}
|
||
}
|
||
|
||
a := randomInt(rng, 20, 100)
|
||
b := randomInt(rng, 20, 100)
|
||
missing := 180 - a - b
|
||
return GeneratedQuestion{
|
||
Title: "Hard Geometry",
|
||
Prompt: fmt.Sprintf("Two angles in a triangle are %d° and %d°. Find the third angle.", a, b),
|
||
CorrectAnswer: fmt.Sprintf("%d°", missing),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Angles in a triangle add to 180°."),
|
||
fmt.Sprintf("First add the known angles: %d + %d = %d.", a, b, a+b),
|
||
fmt.Sprintf("Then calculate 180 - %d = %d°.", a+b, missing),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "angles", "triangle"),
|
||
}
|
||
}
|
||
|
||
func generateDataQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
|
||
if difficulty == sqlc.QuestionDifficultyEasy {
|
||
values := sortedRandomValues(rng, 5, 1, 9)
|
||
median := values[len(values)/2]
|
||
return GeneratedQuestion{
|
||
Title: "Easy Data",
|
||
Prompt: fmt.Sprintf("Find the median of these numbers: %s.", joinInts(values)),
|
||
CorrectAnswer: fmt.Sprintf("%d", median),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Put the numbers in order: %s.", joinInts(values)),
|
||
fmt.Sprintf("The middle value is %d, so the median is %d.", median, median),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "median"),
|
||
}
|
||
}
|
||
|
||
if difficulty == sqlc.QuestionDifficultyMedium {
|
||
values := sortedRandomValues(rng, 5, 2, 20)
|
||
sum := 0
|
||
for _, value := range values {
|
||
sum += value
|
||
}
|
||
mean := sum / len(values)
|
||
adjustment := sum % len(values)
|
||
if adjustment != 0 {
|
||
values[len(values)-1] += len(values) - adjustment
|
||
sort.Ints(values)
|
||
sum = 0
|
||
for _, value := range values {
|
||
sum += value
|
||
}
|
||
mean = sum / len(values)
|
||
}
|
||
return GeneratedQuestion{
|
||
Title: "Medium Data",
|
||
Prompt: fmt.Sprintf("Find the mean of these numbers: %s.", joinInts(values)),
|
||
CorrectAnswer: fmt.Sprintf("%d", mean),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("Add the numbers: the total is %d.", sum),
|
||
fmt.Sprintf("Divide by %d: %d ÷ %d = %d.", len(values), sum, len(values), mean),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "mean"),
|
||
}
|
||
}
|
||
|
||
values := sortedRandomValues(rng, 6, 5, 30)
|
||
modeIndex := randomInt(rng, 0, len(values)-1)
|
||
modeValue := values[modeIndex]
|
||
values = append(values, modeValue)
|
||
sort.Ints(values)
|
||
return GeneratedQuestion{
|
||
Title: "Hard Data",
|
||
Prompt: fmt.Sprintf("Find the mode of these numbers: %s.", joinInts(values)),
|
||
CorrectAnswer: fmt.Sprintf("%d", modeValue),
|
||
WorkedSolution: []string{
|
||
fmt.Sprintf("The mode is the value that appears most often."),
|
||
fmt.Sprintf("%d appears more than any other value, so the mode is %d.", modeValue, modeValue),
|
||
},
|
||
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "mode"),
|
||
}
|
||
}
|
||
|
||
func randomInt(rng *rand.Rand, min, max int) int {
|
||
if max <= min {
|
||
return min
|
||
}
|
||
return min + rng.Intn(max-min+1)
|
||
}
|
||
|
||
func digitsToInt(digits []int) int {
|
||
value := 0
|
||
for _, digit := range digits {
|
||
value = value*10 + digit
|
||
}
|
||
return value
|
||
}
|
||
|
||
func placeNameFromPower(power int) string {
|
||
switch power {
|
||
case 0:
|
||
return "ones"
|
||
case 1:
|
||
return "tens"
|
||
case 2:
|
||
return "hundreds"
|
||
case 3:
|
||
return "thousands"
|
||
case 4:
|
||
return "ten-thousands"
|
||
default:
|
||
return "place"
|
||
}
|
||
}
|
||
|
||
func gcd(a, b int) int {
|
||
for b != 0 {
|
||
a, b = b, a%b
|
||
}
|
||
if a < 0 {
|
||
return -a
|
||
}
|
||
return a
|
||
}
|
||
|
||
func simplifyFraction(numerator, denominator int) (int, int) {
|
||
if denominator == 0 {
|
||
return numerator, denominator
|
||
}
|
||
divisor := gcd(numerator, denominator)
|
||
return numerator / divisor, denominator / divisor
|
||
}
|
||
|
||
func formatFraction(numerator, denominator int) string {
|
||
if denominator == 1 {
|
||
return fmt.Sprintf("%d", numerator)
|
||
}
|
||
return fmt.Sprintf("%d/%d", numerator, denominator)
|
||
}
|
||
|
||
func sortedRandomValues(rng *rand.Rand, count, min, max int) []int {
|
||
values := make([]int, count)
|
||
for i := 0; i < count; i++ {
|
||
values[i] = randomInt(rng, min, max)
|
||
}
|
||
sort.Ints(values)
|
||
return values
|
||
}
|
||
|
||
func joinInts(values []int) string {
|
||
parts := make([]string, 0, len(values))
|
||
for _, value := range values {
|
||
parts = append(parts, fmt.Sprintf("%d", value))
|
||
}
|
||
return strings.Join(parts, ", ")
|
||
}
|