go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/kana-server/pkg/types/quiz.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package types 9 10 import ( 11 "sort" 12 "time" 13 14 "go.charczuk.com/sdk/mathutil" 15 "go.charczuk.com/sdk/uuid" 16 17 "go.charczuk.com/projects/kana-server/pkg/kana" 18 ) 19 20 // Quiz is a quiz. 21 type Quiz struct { 22 // ID is a unique identifier for the quiz. 23 ID uuid.UUID `db:"id,pk"` 24 // UserID is the user the quiz corresponds to. 25 UserID uuid.UUID `db:"user_id"` 26 // CreatedUTC is the time the quiz was created. 27 CreatedUTC time.Time `db:"created_utc"` 28 // LastAnsweredUTC is the last time the quiz was answered. 29 LastAnsweredUTC time.Time `db:"last_answered_utc"` 30 // Hiragana indicates if we should include prompts from the hiragana set. 31 Hiragana bool `db:"hiragana"` 32 // Katakana indicates if we should include prompts from the katakana set. 33 Katakana bool `db:"katakana"` 34 // MaxQuestions is the maximum number of questions to ask per quiz. 35 MaxQuestions int `db:"max_questions"` 36 // MaxPrompts is the maximum number of prompts to pull from either prompt set (or in total) 37 MaxPrompts int `db:"max_prompts"` 38 // MaxRepeatHistory is the debounce history list length. 39 MaxRepeatHistory int `db:"max_repeat_history"` 40 // Results are the individual prompts and answers. 41 Results []QuizResult `db:"-"` 42 // Prompts are the individual mappings between kana and roman to quiz. 43 Prompts map[string]string `db:"prompts,json"` 44 // PromptWeights are used for selection bias based on incorrect answers. 45 PromptWeights map[string]float64 `db:"prompt_weights,json"` 46 // PromptHistory are the recent prompts used to debounce them. 47 PromptHistory []string `db:"prompt_history,json"` 48 } 49 50 // TableName returns the database tablename for the type. 51 func (q Quiz) TableName() string { return "quiz" } 52 53 // IsZero returns if the quiz is set or not. 54 func (q Quiz) IsZero() bool { 55 return q.ID.IsZero() 56 } 57 58 // LatestResult returns the latest result. 59 func (q Quiz) LatestResult() *QuizResult { 60 if len(q.Results) > 0 { 61 return &q.Results[0] 62 } 63 return nil 64 } 65 66 // Stats returns the stats for the quiz. 67 func (q Quiz) Stats() (stats QuizStats) { 68 var elapsedTimes []time.Duration 69 70 for _, qr := range q.Results { 71 elapsedTimes = append(elapsedTimes, qr.Elapsed()) 72 if qr.Correct() { 73 stats.Correct++ 74 } 75 stats.Total++ 76 } 77 78 sortedElapsedTimes := mathutil.CopySort(elapsedTimes) 79 stats.ElapsedAverage = mathutil.Mean(sortedElapsedTimes) 80 stats.ElapsedP90 = mathutil.PercentileSorted(sortedElapsedTimes, 90.0) 81 stats.ElapsedP95 = mathutil.PercentileSorted(sortedElapsedTimes, 95.0) 82 stats.ElapsedMin, stats.ElapsedMax = mathutil.MinMax(sortedElapsedTimes) 83 return 84 } 85 86 // PromptStats returns stats for each prompt. 87 func (q Quiz) PromptStats() (output []*PromptStats) { 88 lookup := make(map[string]*PromptStats) 89 90 for _, res := range q.Results { 91 stats, ok := lookup[res.Prompt] 92 if ok { 93 stats.Total++ 94 if res.Correct() { 95 stats.Correct++ 96 } 97 stats.ElapsedTimes = append(stats.ElapsedTimes, res.Elapsed()) 98 } else { 99 var newStats PromptStats 100 newStats.Prompt = res.Prompt 101 newStats.Weight = q.PromptWeights[res.Prompt] 102 newStats.Total = 1 103 if res.Correct() { 104 newStats.Correct = 1 105 } 106 newStats.ElapsedTimes = append(newStats.ElapsedTimes, res.Elapsed()) 107 output = append(output, &newStats) 108 lookup[res.Prompt] = &newStats 109 } 110 } 111 112 for _, stats := range output { 113 stats.ElapsedAverage = mathutil.Mean(stats.ElapsedTimes) 114 stats.ElapsedP90 = mathutil.Percentile(stats.ElapsedTimes, 90.0) 115 stats.ElapsedP95 = mathutil.Percentile(stats.ElapsedTimes, 95.0) 116 stats.ElapsedMin, stats.ElapsedMax = mathutil.MinMax(stats.ElapsedTimes) 117 } 118 sort.Slice(output, func(i, j int) bool { 119 return output[i].Prompt < output[j].Prompt 120 }) 121 return 122 } 123 124 // NewTestQuiz returns a new test quiz. 125 func NewTestQuiz(userID uuid.UUID) *Quiz { 126 prompts := kana.SelectCount(kana.Merge(kana.Hiragana, kana.Katakana), 10) 127 return &Quiz{ 128 ID: uuid.V4(), 129 UserID: userID, 130 CreatedUTC: time.Now().UTC(), 131 Hiragana: true, 132 Katakana: true, 133 MaxPrompts: 10, 134 MaxQuestions: 0, 135 MaxRepeatHistory: 5, 136 Prompts: prompts, 137 PromptWeights: kana.CreateWeights(prompts), 138 } 139 }