Source code for mltype.base

"""Building blocks."""

from datetime import datetime
import pickle


STATUS_BACKSPACE = 1
STATUS_CORRECT = 2
STATUS_WRONG = 3


[docs]class Action: """Representation of one keypress. Parameters ---------- pressed_key : str What key was pressed. We define a convention that pressing a backspace will be represented as `pressed_key=None`. status : int What was the status AFTER pushing the key. It should be one of the following integers: * STATUS_BACKSPACE * STATUS_CORRECT * STATUS_WRONG ts : datetime The timestamp corresponding to this action. """ def __init__(self, pressed_key, status, ts): if pressed_key is not None and len(pressed_key) != 1: raise ValueError("The pressed key needs to be a single character") self.pressed_key = pressed_key self.status = status self.ts = ts def __eq__(self, other): """Check whether equal.""" if not isinstance(other, self.__class__): return False return ( self.pressed_key == other.pressed_key and self.status == other.status and self.ts == other.ts )
[docs]class TypedText: """Abstraction that represenets the text that needs to be typed. Parameters ---------- text : str Text that needs to be typed. Attributes ---------- actions : list List of lists of Action instances of length equal to `len(text)`. It logs per character all actions that have been taken on it. start_ts : datetime or None Timestamp of when the first action was performed (not the time of initialization). end_ts : datetime or None Timestamp of when the last action was taken. Note that it is the action that lead to the text being correctly typed in it's entirity. """ def __init__(self, text): self.text = text self.actions = [[] for _ in range(len(text))] self.start_ts = None self.end_ts = None
[docs] @classmethod def load(cls, path): """Load a pickled file. Parameters ---------- path : pathlib.Path Path to the pickle file. Returns ------- typed_text : TypedText Instance of the ``TypedText`` """ with path.open("rb") as f: text, actions, start_ts, end_ts = pickle.load(f) typed_text = cls(text) typed_text.actions = actions typed_text.start_ts = start_ts typed_text.end_ts = end_ts return typed_text
def __eq__(self, other): """Check if equal. Not considering start and end timestamps. """ if not isinstance(other, self.__class__): return False return self.text == other.text and self.actions == other.actions def _n_characters_with_status(self, status): """Count the number of characters with a given status. Parameters ---------- status : str The status we look for in the character. Returns ------- The number of characters with status `status`. """ return len([x for x in self.actions if x and x[-1].status == status]) @property def elapsed_seconds(self): """Get the number of seconds elapsed from the first action.""" if self.start_ts is None: return 0 end_ts = self.end_ts or datetime.now() return (end_ts - self.start_ts).total_seconds() @property def n_actions(self): """Get the number of actions that have been taken.""" return sum(len(x) for x in self.actions) @property def n_characters(self): """Get the number of characters in the text.""" return len(self.text) @property def n_backspace_actions(self): """Get the number of backspace actions.""" return sum( sum(1 for a in x if a.status == STATUS_BACKSPACE) for x in self.actions ) @property def n_backspace_characters(self): """Get the number of characters that have been backspaced.""" return self._n_characters_with_status(STATUS_BACKSPACE) @property def n_correct_characters(self): """Get the number of characters that have been typed correctly.""" return self._n_characters_with_status(STATUS_CORRECT) @property def n_untouched_characters(self): """Get the number of characters that have not been touched yet.""" return len([x for x in self.actions if not x]) @property def n_wrong_characters(self): """Get the number of characters that have been typed wrongly.""" return self._n_characters_with_status(STATUS_WRONG)
[docs] def compute_accuracy(self): """Compute the accuracy of the typing.""" try: acc = self.n_correct_characters / ( self.n_actions - self.n_backspace_actions ) except ZeroDivisionError: acc = 0 return acc
[docs] def compute_cpm(self): """Compute characters per minute.""" try: cpm = 60 * self.n_correct_characters / self.elapsed_seconds except ZeroDivisionError: # We actually set self.end_ts = self.start_ts in instant death cpm = 0 return cpm
[docs] def compute_wpm(self, word_size=5): """Compute words per minute.""" return self.compute_cpm() / word_size
[docs] def check_finished(self, force_perfect=True): """Determine whether the typing has been finished successfully. Parameters ---------- force_perfect : bool If True, one can only finished if all the characters were typed correctly. Otherwise, all characters need to be either correct or wrong. """ if force_perfect: return self.n_correct_characters == self.n_characters else: return ( self.n_correct_characters + self.n_wrong_characters == self.n_characters )
[docs] def save(self, path): """Save internal state of this TypedText. Can be loaded via the class method ``load``. Parameters ---------- path : pathlib.Path Where the .rlt file will be store. """ with path.open("wb") as f: all_obj = (self.text, self.actions, self.start_ts, self.end_ts) pickle.dump(all_obj, f)
[docs] def type_character(self, i, ch=None): """Type one single character. Parameters ---------- i : int Index of the character in the text. ch : str or None The character that was typed. Note that if None then we assume that the user used backspace. """ if not (0 <= i < self.n_characters): raise IndexError(f"The index {i} is outside of the text.") ts = datetime.now() # check if it is the first action if self.start_ts is None: self.start_ts = ts # check if it is a backspace if ch is None: self.actions[i].append(Action(ch, STATUS_BACKSPACE, ts)) return # check if the characters agree if ch == self.text[i]: self.actions[i].append(Action(ch, STATUS_CORRECT, ts)) else: self.actions[i].append(Action(ch, STATUS_WRONG, ts)) # check whether finished if self.check_finished(force_perfect=False): self.end_ts = ts
[docs] def unroll_actions(self): """Export actions in an order they appeared. Returns ------- res : list List of tuples of `(ix_char, Action(..))` """ return sorted( [(i, a) for i, x in enumerate(self.actions) for a in x], key=lambda x: x[1].ts, )