What is it about?
- Learning to use the basic math operation (+, -, *,/).
- Print out of statistics to show you where you are.
- Being able to define your own training sessions.
- Sessions with number of tasks or with a time limit.
What has changed?
- Revision 2: Contains now the tasks numbering.
- Revision 2: You can see overall how long you didn't a training.
- Revision 2: You also can see per kind of session how long you didn't a training.
- Revision 3: Trainings parameter as kind of policy in a separate class also used as key for statistic.
- Revision 3: New session type: timeout (as many tasks as possible until timeout exceeded)
- Revision 3: I'm also sorry to say that this revision breaks compatibility with previously stored sessions.
- Revision 4: Integer division supported.
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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | """
@author Thomas Lehmann
@file Learn2Calc.py
@brief Tools to train yourself in calculation
Training on regularly base is something that really helps
you to improve your calculations. Two main criteria are of
importance: speed and accuracy.
The basic statistic at the beginning and at the end
allows you to monitor your training success.
With the training parameters as provided with this script it
took myself about one minute per session (average). You cannot
say - I guess - that less than 5 minutes training per day is
really much time you lose, do you?
"""
from datetime import datetime
import random
import sys
import os
import pickle
class Tools:
""" some tool functions """
@staticmethod
def dateBack(theDateAndTime, precise=False, fromDate=None):
""" provides a human readable format for a time delta.
@param theDateAndTime this is time equal or older than now or the date in 'fromDate'
@param precise when true then milliseconds and microseconds are included
@param fromDate when None the 'now' is used otherwise a concrete date is expected
@return the time delta as text
@note I don't calculate months and years because those varies (28,29,30 or 31 days a month
and 365 or 366 days the year depending on leap years). In addition please refer
to the documentation for timedelta limitations.
@see http://code.activestate.com/recipes/578113
"""
if not fromDate:
fromDate = datetime.now()
if theDateAndTime > fromDate: return None
elif theDateAndTime == fromDate: return "now"
delta = fromDate - theDateAndTime
# the timedelta structure does not have all units; bigger units are converted
# into given smaller ones (hours -> seconds, minutes -> seconds, weeks > days, ...)
# but we need all units:
deltaMinutes = delta.seconds // 60
deltaHours = delta.seconds // 3600
deltaMinutes -= deltaHours * 60
deltaWeeks = delta.days // 7
deltaSeconds = delta.seconds - deltaMinutes * 60 - deltaHours * 3600
deltaDays = delta.days - deltaWeeks * 7
deltaMilliSeconds = delta.microseconds // 1000
deltaMicroSeconds = delta.microseconds - deltaMilliSeconds * 1000
valuesAndNames =[ (deltaWeeks ,"week" ), (deltaDays ,"day" ),
(deltaHours ,"hour" ), (deltaMinutes,"minute"),
(deltaSeconds,"second") ]
if precise:
valuesAndNames.append((deltaMilliSeconds, "millisecond"))
valuesAndNames.append((deltaMicroSeconds, "microsecond"))
text =""
for value, name in valuesAndNames:
if value > 0:
text += len(text) and ", " or ""
text += "%d %s" % (value, name)
text += (value > 1) and "s" or ""
# replacing last occurrence of a comma by an 'and'
if text.find(",") > 0:
text = " and ".join(text.rsplit(", ",1))
if not len(text):
text = "a tick"
return text
@staticmethod
def getDuration(started, finished):
""" @return a float representing seconds """
td = finished - started
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
class DivisionCache:
""" provides possible integer divisions """
divisions = {}
@staticmethod
def update(rangeLimit, tasksLimit):
""" Using multiplication this mechanism tries to find
valid integer division limited in range and
number of tasks """
for a in range(2, rangeLimit+1):
for b in range(2, rangeLimit+1):
if a == b:
continue
if b % a == 0:
# stores tasks with two numbers
key = (len("%d" % b), len("%d" % a))
if not key in DivisionCache.divisions:
DivisionCache.divisions[key] = [(b, a)]
else:
DivisionCache.divisions[key].append((b, a))
c = a * b
if not c == a and c % b == 0 and (c/b) % a == 0:
# stores tasks with three numbers
key = (len("%d" % c), len("%d" % b), len("%d" % a))
if not key in DivisionCache.divisions:
DivisionCache.divisions[key] = [(c, b, a)]
else:
DivisionCache.divisions[key].append((c, b, a))
# deletes all divisions where not enough tasks can be done
for key in list(DivisionCache.divisions.keys())[0:]:
if len(DivisionCache.divisions[key]) < tasksLimit:
del DivisionCache.divisions[key]
class Task(object):
""" A task is one concrete calculation for which a user
has to provide an answer. The task, the answer and
the timing is stored for later evaluation. """
def __init__(self, task):
self.started = None
self.finished = None
self.task = task
self.answerByUser = ""
def getValidAnswer(self):
""" @return a string of calculated correct answer """
return str(int(eval(self.task)))
def isValid(self):
""" @return true, when the user has provided correct answer """
return self.answerByUser == self.getValidAnswer()
def getDuration(self):
""" @return a float representing seconds """
return Tools.getDuration(self.started, self.finished)
def run(self):
""" asking the user to answer for a concrete calculation """
self.started = datetime.now()
self.answerByUser = input("%s = " % self.task)
self.finished = datetime.now()
class TrainingParameter(object):
""" training parameter for one session """
MODE_COUNT = 0 # means: a certain number of tasks have to be done in a session
MODE_TIMEOUT = 1 # means: you can do many tasks except the timeout has exceeded
def __init__(self , digitsPerNumber=None , operation='*' , mode=MODE_COUNT , modeValue=10):
if not digitsPerNumber: digitsPerNumber = [1 , 1]
self.digitsPerNumber = digitsPerNumber
self.operation = operation
self.mode = mode
self.modeValue = modeValue
def __hash__(self):
""" @return identifies the "kind" of session to allow grouping of same sessions """
return hash(str((self.operation, self.mode, self.modeValue, str(self.digitsPerNumber))))
def __eq__(self, other):
""" comparing two training parameter setups """
if not self.digitsPerNumber == other.digitsPerNumber:
return False
if not self.operation == other.operation:
return False
if not self.mode == other.mode:
return False
if not self.modeValue == other.modeValue:
return False
return True
def __repr__(self):
return """operation: %(operation)c, digits per number: %(digitsPerNumber)s, mode: %(mode)d, modeValue: %(modeValue)d""" % self.__dict__
class Session(object):
""" one sessions finally represents a number of tasks with all information like
date, time, concrete tasks and the answer of the user """
def __init__(self, trainingParameter):
self.started = None
self.finished = None
self.trainingParameter = trainingParameter
self.doneTasks = []
self.numberOfTasks = 0
def __iter__(self):
""" provides iteration over tasks """
return iter(self.doneTasks)
def getKey(self):
""" @return identifies the "kind" of session to allow grouping of same sessions """
return self.trainingParameter
def getDuration(self):
""" @return a float representing seconds """
return Tools.getDuration(self.started, self.finished)
def getErrorRate(self):
return sum([1 for task in self.doneTasks if not task.isValid()]) * 100.0 / len(self.doneTasks)
def createNumber(self, digits):
""" creates a random number >= 2 and with given number of digits """
minimum = 10**(digits-1)
if minimum == 1: minimum = 2
return random.randrange(minimum, 10**digits - 1)
def createTask(self):
""" generates a new task which can be passed through the eval function """
if self.trainingParameter.operation in ['*', '+', '-']:
numbers = str([self.createNumber(digits) for digits in self.trainingParameter.digitsPerNumber])
return numbers.replace(", " , " %c " % self.trainingParameter.operation)[1:-1]
elif self.trainingParameter.operation == '/':
key = tuple(self.trainingParameter.digitsPerNumber)
if key in DivisionCache.divisions:
divisions = DivisionCache.divisions[key]
numbers = str(divisions[random.randrange(0, len(divisions)-1)])
return numbers.replace(", " , " %c " % self.trainingParameter.operation)[1:-1]
# not supported
return ""
def run(self):
""" generates tasks """
exampleTask = self.createTask()
if not len(exampleTask):
print("Error: cannot create task for %s" % self.trainingParameter)
return False
print("\nNext session has the form %s = %s" % (exampleTask, int(eval(exampleTask))))
if self.trainingParameter.mode == TrainingParameter.MODE_COUNT:
input("Are you ready for %d tasks? (press enter)"
% self.trainingParameter.modeValue)
elif self.trainingParameter.mode == TrainingParameter.MODE_TIMEOUT:
input("Are you ready for as many tasks you can do in %s seconds? (press enter)"
% self.trainingParameter.modeValue)
else:
print("Error: not handled training parameter!")
return False
self.started = datetime.now()
succeeded, failed = 0, 0
results = []
taskNr = 1
while True:
# displaying the task number before the task
print("%2d)" % taskNr , end=" ")
task = self.createTask()
# ensure not to ask the same task twice
while task in results:
task = self.createTask()
newTask = Task(task)
newTask.run()
if newTask.isValid():
print(" ...right!")
succeeded += 1
else:
print(" ...wrong, the right answer is %s" %
newTask.getValidAnswer())
failed += 1
print(" ...took %f second - %d succeeded and %d failed" %
(newTask.getDuration(), succeeded, failed))
self.doneTasks.append(newTask)
results.append(newTask.task)
taskNr += 1
# defined number of tasks done?
if self.trainingParameter.mode == TrainingParameter.MODE_COUNT:
if taskNr > self.trainingParameter.modeValue:
break
# defined timeout exceeded?
elif self.trainingParameter.mode == TrainingParameter.MODE_TIMEOUT:
currentDuration = Tools.getDuration(self.started, datetime.now())
if currentDuration > self.trainingParameter.modeValue:
break
self.finished = datetime.now()
self.numberOfTasks = taskNr-1
return True
class Statistic:
""" provides functionality to print summary and detailed statistic """
def __init__(self, sessions):
""" stores sessions by session key """
self.sessionsByKey = {}
for session in sessions:
key = session.getKey()
if not key in self.sessionsByKey: self.sessionsByKey[key] = [session]
else: self.sessionsByKey[key].append(session)
def printSummary(self):
""" independent of type of session or task you get an overview """
succeeded, failed = 0, 0
taskDurations = []
sessionDurations = []
lastSession = None
for sessions in self.sessionsByKey.values():
for session in sessions:
sessionDurations.append(session.getDuration())
for task in session:
taskDurations.append(task.getDuration())
if task.isValid():
succeeded += 1
else:
failed += 1
if not lastSession:
lastSession = session
elif session.finished > lastSession.finished:
lastSession = session
# the first time you have no tasks yet
if not len(taskDurations):
return
errorRate = failed * 100.0 / len(taskDurations)
print("\n...last session has been %s"
% (Tools.dateBack(lastSession.finished) + " ago"))
print("...overall number of sessions is %d"
% len(sessionDurations))
print("...overall number of tasks is %d, %d succeeded, %d failed - error rate is about %.1f%%"
% (len(taskDurations), succeeded, failed, errorRate))
print("...best task time was %f seconds, longest task time was %f seconds"
% (min(taskDurations), max(taskDurations)))
print("...best session time was %f seconds, longest session time was %f seconds"
% (min(sessionDurations), max(sessionDurations)))
print("...average time over all kind of tasks is %f seconds"
% (sum(taskDurations)/len(taskDurations)))
print("...average time over all kind of sessions is %f seconds"
% (sum(sessionDurations)/len(sessionDurations)))
print("...overall session time %f seconds"
% (sum(sessionDurations)))
def printDetailedStatistic(self):
""" prints a statistic per session key. The session key includes the
math operation, how many numbers, the digits for the numbers
and how many tasks; this is to have comparable sessions.
"""
for key, sessions in self.sessionsByKey.items():
print("\n...separate statistic for %s" % key)
succeeded, failed = 0, 0
taskDurations = []
sessionDurations = []
lastSession = None
for session in sessions:
sessionDurations.append(session.getDuration())
for task in session:
taskDurations.append(task.getDuration())
if task.isValid():
succeeded += 1
else:
failed += 1
if not lastSession:
lastSession = session
elif session.finished > lastSession.finished:
lastSession = session
errorRate = failed * 100.0 / len(taskDurations)
print("......last session has been %s"
% (Tools.dateBack(lastSession.finished) + " ago"))
print("......number of sessions is %d"
% len(sessionDurations))
print("......number of tasks is %d, %d succeeded, %d failed - error rate is about %.1f%%"
% (len(taskDurations), succeeded, failed, errorRate))
print("......best task time was %f seconds, longest task time was %f seconds"
% (min(taskDurations), max(taskDurations)))
print("......best session time was %f seconds, longest session time was %f seconds"
% (min(sessionDurations), max(sessionDurations)))
print("......average time over all tasks is %f seconds"
% (sum(taskDurations)/len(taskDurations)))
print("......average time over all sessions is %f seconds"
% (sum(sessionDurations)/len(sessionDurations)))
print("......sessions time %f seconds"
% (sum(sessionDurations)))
class SessionManager:
""" organizes load/save of sessions """
def __init__(self):
""" initializing to have no sessions initially """
self.sessions = []
def add(self, session):
""" adding further session to be saved """
self.sessions.append(session)
def save(self, pathAndFileName):
""" saving all sessions """
pickle.dump(self.sessions, open(pathAndFileName, "wb"))
def load(self, pathAndFileName):
""" loading all sessions """
if os.path.isfile(pathAndFileName):
self.sessions = pickle.load(open(pathAndFileName, "rb"))
def dumpStatistic(self, detailed=False):
""" dumping some basic statistic to give you an overview
about your training results """
statistic = Statistic(self.sessions)
statistic.printSummary()
if detailed:
statistic.printDetailedStatistic()
def main():
""" application entry point to start your training """
print("Learn2Calc v0.4 by Thomas Lehmann 2012")
print("...Python %s" % sys.version.replace("\n", ""))
sessionManager = SessionManager()
# loading previous training results
sessionManager.load("Learn2Calc.dat")
sessionManager.dumpStatistic()
# ensure at least 30 divisions per pattern (digits per number)
# increase this if you need more but be aware that the creation
# of the cache takes more time then!
DivisionCache.update(250, 30)
# here you can adjust your training parameters (each entry will finally represent one session):
sessionParameter = [ # multiplication of three numbers (each one digit) with exact 10 tasks
TrainingParameter([1,1,1] , '*', TrainingParameter.MODE_COUNT, 10),
# addition of four numbers (each one digit) with timeout of 1 minute
TrainingParameter([1,1,1,1], '+', TrainingParameter.MODE_TIMEOUT, 60),
# subtraction of three values (decreasing size) with exact 10 tasks
TrainingParameter([3,2,1] , '-', TrainingParameter.MODE_COUNT, 10),
# multiplication of two numbers (decreasing size) with timeout of 1 minute
TrainingParameter([2,1] , '*', TrainingParameter.MODE_TIMEOUT, 60),
# integer division of two numbers (decreasing size) with exact 10 tasks
TrainingParameter([3, 1] , '/', TrainingParameter.MODE_COUNT, 10)
]
# creating and running sessions depending on your training parameters
count = 0
for parameter in sessionParameter:
session = Session(parameter)
if session.run():
sessionManager.add(session)
count += 1
if count > 0:
# storing current training results
sessionManager.save("Learn2Calc.dat")
sessionManager.dumpStatistic(True)
if __name__ == "__main__":
main()
|
Print out of a summary each time you start and each time you finish
Well, please don't judge my results I'm at the start and I've written the program to improve my calculations. Anyway you see a summary independent how difficult a session or task has been...
C:\Python31\python.exe E:/Python/Learn2Calc/Main.py
Learn2Calc v0.1 by Thomas Lehmann 2012
...Python 3.1.2 (r312:79149, Mar 21 2010, 00:41:52) [MSC v.1500 32 bit (Intel)]
...overall number of sessions is 13
...overall number of tasks is 130, 117 succeeded, 13 failed - error rate is about 10.0%
...best task time was 2.808000 seconds, longest task time was 27.635000 seconds
...best session time was 39.833000 seconds, longest session time was 149.820000 seconds
...average time over all kind of tasks is 7.362438 seconds
...average time over all kind of sessions is 73.632846 seconds
...overall session time 957.227000 seconds
Next session: 4 * 4 * 5 (as an example)
Are you ready for 10 tasks? (press enter)
Task for task
Each task has a number, you get the time you have required, your success state and your failures and the right answer when you have missed.
5) 595 - 40 - 4 = 551
...right!
...took 8.345000 second - 4 succeeded and 1 failed
6) 321 - 88 - 4 = 239
...wrong, the right answer is 229
...took 8.179000 second - 4 succeeded and 2 failed
Do not read too much because after pressing enter the timing starts again except after the last. The information are more for later use.
Final statistic per kind of session
The next is an extract only out of some kind of sessions I'm currently using...
...separate statistic for operation: +, digits per number: [1, 1, 1, 1], mode: 1, modeValue: 60
......last session has been 2 minutes and 55 seconds ago
......number of sessions is 3
......number of tasks is 46, 43 succeeded, 3 failed - error rate is about 6.5%
......best task time was 2.461000 seconds, longest task time was 7.931000 seconds
......best session time was 60.019000 seconds, longest session time was 63.647000 seconds
......average time over all tasks is 4.006761 seconds
......average time over all sessions is 61.441000 seconds
......sessions time 184.323000 seconds
Customizable training parameters
Feel free to modify this for your own needs. Yes, finally it's a good idea to place this in a config file but I intended to keep everything together for this recipe. Currently I'm not sure how to handle division because I would like train with integer numbers. The values in the list says how many digits are requires per number...
# here you can adjust your training parameters (each entry will finally represent one session):
sessionParameter = [ # multiplication of three numbers (each one digit) with exact 10 tasks
TrainingParameter([1,1,1] , '*', TrainingParameter.MODE_COUNT, 10),
# addition of four numbers (each one digit) with timeout of 1 minute
TrainingParameter([1,1,1,1], '+', TrainingParameter.MODE_TIMEOUT, 60),
# subtraction of three values (decreasing size) with exact 10 tasks
TrainingParameter([3,2,1] , '-', TrainingParameter.MODE_COUNT, 10),
# multiplication of two numbers (decreasing size) with timeout of 1 minute
TrainingParameter([2,1] , '*', TrainingParameter.MODE_TIMEOUT, 60),
# integer division of two numbers (decreasing size) with exact 10 tasks
TrainingParameter([3, 1] , '/', TrainingParameter.MODE_COUNT, 10)
]
About division
There're some restrictions. Let's assume you say [1,1] then you might have a task like 1/3= and that is 0.33333333.... That's why I'm supporting integer division only (9/3=). But integer division means I have to find out which numbers will fit. The DivisionCache class is for that. You can control how big numbers can be and how many task are required to be available. Anyway you can choose two or three numbers only otherwise you have to extend the cache. Be carefully because the creation of the cache can be expensive. If you have a better idea than please tell me.
Persistence
All sessions and tasks are stored into a file and loaded and start-up. This is required for statistic.
Future enhancements for this recipe here
I've decided to keep that recipe below or equal 500 lines. As long this is possible I will extend it whenever I have time for it:
- There's no numbering of the session.
- I would like to print out how you have improved since you have started to train.
- I would like introduce a session with tasks that have failed in the past.