Files
BoostAI/Backend/internal/questiongen/service.go
2026-05-25 17:05:06 +01:00

635 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, ", ")
}