-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKeyTrainStats.cs
More file actions
207 lines (177 loc) · 7.87 KB
/
KeyTrainStats.cs
File metadata and controls
207 lines (177 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Linq;
using System.Diagnostics;
using System.Windows.Markup;
using static Pythonic.ListHelpers;
using static KeyTrain.KeyTrainStatsConversion;
using System.Windows.Media;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
namespace KeyTrain
{
/// <summary>
/// Class for legging and querying keyboard training statistics
/// </summary>
[Serializable]
public class KeyTrainStats
{
public DefaultDict<char, TimeData> charTimes { get; private set; } = new DefaultDict<char, TimeData>() ;
public DefaultDict<char, MissData> charMisses { get; private set; } = new DefaultDict<char, MissData>();
public List<double> WPMLOG { get; private set; } = new List<double>();
public double LastWPM => WPMLOG.DefaultIfEmpty(0).Last();
public List<int> MISSLOG { get; private set; } = new List<int>();
public int LastMissCount => MISSLOG.LastOrDefault();
public static List<(Color color, double delta)> RatingPalette { get; private set; } = new List<(Color, double)>
{
(Colors.OrangeRed, -150),
(Colors.Orange, -75),
(Colors.YellowGreen, -25),
(Colors.LawnGreen, 50),
(Colors.Green, 100),
(Colors.ForestGreen, double.MaxValue),
//(Color.FromArgb(150,99,99,99), double.PositiveInfinity) //invalid delta -> no data
(Colors.Transparent, double.PositiveInfinity) //invalid delta -> no data
}; //TODO: move to DarkStyles
/// <summary>
/// Log the lesson's data to database
/// </summary>
/// <param name="text">The text that was used</param>
/// <param name="times">The timestamps of each keypress</param>
/// <param name="misses">Index of characters which very missed</param>
/// <param name="totalMinutes">Time the lesson took in minutes. Calculated from times if left empty </param>
public void Enter(string text, IList<TimeSpan> times, SortedSet<int> misses, double? totalMinutes = null)
{
DefaultDict<char, (int misses, int total)> counts = new DefaultDict<char, (int, int)>();
for (int i= 0; i < text.Length; i++)
{
char c = char.ToUpper(text[i]);
charTimes[c].Add(i > 0 ? times[i] - times[i-1] : times[i] );
counts[c] = (counts[c].misses + (misses.Contains(i) ? 1 : 0), counts[c].total + 1);
}
foreach (char k in charTimes.Keys)
{
charMisses[k].Add(counts[k].misses, counts[k].total);
//Trace.WriteLine(
// $"{k} - AVGSPEED: {charTimes[k].average:0.00} " +
// $"MISSES: {string.Join(';', charMisses[k].values.Select(x => $"{x.missed}/{x.total}").ToList())} ");
//Trace.WriteLine($"{k} - AVGSPEED: {charTimes[k].average:0.00} values: {string.Join(';', charTimes[k].values)} ");
}
totalMinutes ??= times.Last().TotalMinutes - times.First().TotalMinutes;
WPMLOG.Add(WPM(text.Length, (double)totalMinutes));
MISSLOG.Add(misses.Count);
}
/// <summary>
/// Compares each letter's average to the overall letter average and rates them via colors defined by RatingPalette
/// </summary>
/// <param name="alwaysInclude">Include these characters even if there's no data about them.</param>
/// <returns>For each letter: Tuple of rating (color) and whether there was data available (hadData bool) </returns>
public DefaultDict<char, (Color color, bool hadData)> GetLetterRatings(HashSet<char> alwaysInclude = null)
{
alwaysInclude ??= new HashSet<char>();
DefaultDict<char, (Color, bool)> result = new DefaultDict<char, (Color, bool)>();
double avgAllLetters = LPM_From_WPM(WPMLOG.DefaultIfEmpty(0).Average());
foreach (char c in charTimes.Keys.Union(alwaysInclude))
{
double avgThisLetter = charTimes[c].avgLPM;
double delta = avgThisLetter - avgAllLetters;
bool hadData = double.IsFinite(avgThisLetter);
if (hadData || alwaysInclude.Contains(c))
{
for (int i = 0; i < RatingPalette.Count; i++)
{
if (delta <= RatingPalette[i].delta)
{
result[c] = (RatingPalette[i].color, hadData);
break;
}
}
}
//Trace.WriteLine($"{c}: {avgThisLetter}");
}
return result;
}
public int updateTest = 213;
}
static class KeyTrainStatsConversion
{
public static double LPM(int length, double minutes) => length / minutes;
public static double LPM_From_WPM(double WPM) => 5 * WPM;
public static double WPM_From_ms(double milliseconds) => 12000 / milliseconds;
public static double WPM(int length, double minutes) => LPM(length, minutes) / 5;
}
static class KeyTrainSerializer
{
public static void Serialize(KeyTrainStats obj, string path)
{
if (File.Exists(path)) File.Delete(path);
FileStream stream = File.Create(path);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(stream, obj);
stream.Close();
}
public static KeyTrainStats Deserialize(string path)
{
if (File.Exists(path) == false) {
Trace.WriteLine("No profile found");
return new KeyTrainStats();
}
FileStream stream = File.OpenRead(path);
if(stream.Length == 0){
Trace.WriteLine("Empty profile");
return new KeyTrainStats();
}
BinaryFormatter bf = new BinaryFormatter();
KeyTrainStats kts = (KeyTrainStats)bf.Deserialize(stream);
stream.Close();
return kts;
}
}
[Serializable]
public class TimeData
{
public List<double> values { get; private set; }
public double average { get; private set; }
public double avgLPM => 60000 / average; //returns infinity when there's no data
public TimeData()
{
values = new List<double>();
average = 0;
}
public void Add(TimeSpan s)
{
Add(s.TotalMilliseconds);
}
private void Add(double d)
{
d = Math.Round(d);
if(d != 0)
{
values.Add(d);
//average = ((average * values.Count - 1) + d) / values.Count;
average = values.Average();
}
}
}
//Counts are logged separately for each text in order to facilitate by-time visualization
[Serializable]
public class MissData
{
public List<(int missed, int total)> values { get; private set; }
public int totalMissed => values.Select(x => x.missed).Sum();
public int total => values.Select(x => x.total).Sum();
public double errorRate { get; private set; }
public MissData()
{
values = new List<(int, int)>();
errorRate = 0;
}
public void Add(int missed, int outOf)
{
values.Add((missed, outOf));
errorRate = total > 0 ? (double)totalMissed / total : 0;
}
}
}