Welcome, guest | Sign In | My Account | Store | Cart

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.
Python, 467 lines
  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.