├── tests ├── __init__.py └── test_sm_two.py ├── supermemo2 ├── __init__.py └── sm_two.py ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /supermemo2/__init__.py: -------------------------------------------------------------------------------- 1 | from .sm_two import * 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune *.egg-info 2 | recursive-include tests *.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-cov==2.10.1 2 | freezegun==1.5.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Env 2 | venv 3 | 4 | # Python 5 | __pycache__ 6 | 7 | # VSCode 8 | .vscode 9 | 10 | # pypi 11 | dist 12 | old_dist 13 | *.egg-info 14 | README.txt 15 | 16 | .* 17 | 18 | # pytest-cov 19 | htmlcov 20 | 21 | # Github workflows 22 | !.github -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Alan Kan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Generate coverage report 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements.txt 24 | pytest 25 | upload_coverage: 26 | runs-on: macos-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Setup Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.10" 33 | - name: Generate coverage report 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install -r requirements.txt 37 | pytest --cov=supermemo2 --cov-report=xml 38 | - name: "Upload coverage to Codecov" 39 | uses: codecov/codecov-action@v4 40 | with: 41 | fail_ci_if_error: true 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | from setuptools import setup, find_packages, Command 4 | 5 | classifiers = [ 6 | "Development Status :: 5 - Production/Stable", 7 | "Intended Audience :: Education", 8 | "Operating System :: OS Independent", 9 | "License :: OSI Approved :: MIT License", 10 | "Programming Language :: Python", 11 | "Programming Language :: Python :: 3", 12 | ] 13 | 14 | with open("README.md", encoding="utf-8") as f: 15 | readme_content = f.read().strip() 16 | 17 | 18 | class CleanCommand(Command): 19 | """Custom clean command to tidy up the project root.""" 20 | 21 | user_options = [] 22 | 23 | def initialize_options(self): 24 | pass 25 | 26 | def finalize_options(self): 27 | pass 28 | 29 | def run(self): 30 | os.system("rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info") 31 | 32 | 33 | setup( 34 | name="supermemo2", 35 | version="3.0.1", 36 | description="Implemented the SM-2 algorithm for spaced repetition learning.", 37 | long_description=readme_content, 38 | long_description_content_type="text/markdown", 39 | url="https://github.com/alankan886/SuperMemo2", 40 | author="Alan Kan", 41 | author_email="", 42 | license="MIT", 43 | classifiers=classifiers, 44 | keywords="spaced-repetition SM-2 SuperMemo Python", 45 | packages=find_packages(), 46 | include_package_data=True, 47 | install_requires=["attrs"], 48 | cmdclass={ 49 | "clean": CleanCommand, 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /supermemo2/sm_two.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from datetime import datetime, timedelta 3 | from typing import Optional, Union, Dict 4 | 5 | 6 | def review( 7 | quality: int, 8 | easiness: float, 9 | interval: int, 10 | repetitions: int, 11 | review_datetime: Optional[Union[datetime, str]] = None, 12 | ) -> Dict: 13 | if not review_datetime: 14 | review_datetime = datetime.utcnow().isoformat(sep=" ", timespec="seconds") 15 | 16 | if isinstance(review_datetime, str): 17 | review_datetime = datetime.fromisoformat(review_datetime).replace(microsecond=0) 18 | 19 | if quality < 3: 20 | interval = 1 21 | repetitions = 0 22 | else: 23 | if repetitions == 0: 24 | interval = 1 25 | elif repetitions == 1: 26 | interval = 6 27 | else: 28 | interval = ceil(interval * easiness) 29 | 30 | repetitions += 1 31 | 32 | easiness += 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02) 33 | if easiness < 1.3: 34 | easiness = 1.3 35 | 36 | review_datetime += timedelta(days=interval) 37 | 38 | return { 39 | "easiness": easiness, 40 | "interval": interval, 41 | "repetitions": repetitions, 42 | "review_datetime": str(review_datetime), 43 | } 44 | 45 | 46 | def first_review( 47 | quality: int, 48 | review_datetime: Optional[Union[datetime, str]] = None, 49 | ) -> Dict: 50 | if not review_datetime: 51 | review_datetime = datetime.utcnow() 52 | 53 | return review(quality, 2.5, 0, 0, review_datetime) 54 | -------------------------------------------------------------------------------- /tests/test_sm_two.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import pytest 4 | from freezegun import freeze_time 5 | 6 | from supermemo2 import first_review, review 7 | 8 | FREEZE_DATE = "2024-01-01" 9 | MOCK_TODAY = datetime.fromisoformat(FREEZE_DATE).replace(microsecond=0) 10 | 11 | 12 | @freeze_time(FREEZE_DATE) 13 | @pytest.mark.parametrize( 14 | "quality, expected_easiness, expected_interval, expected_repetitions, expected_review_date", 15 | [ 16 | ( 17 | 0, 18 | 1.7000000000000002, 19 | 1, 20 | 0, 21 | str(MOCK_TODAY + timedelta(days=1)), 22 | ), 23 | ( 24 | 1, 25 | 1.96, 26 | 1, 27 | 0, 28 | str(MOCK_TODAY + timedelta(days=1)), 29 | ), 30 | ( 31 | 2, 32 | 2.1799999999999997, 33 | 1, 34 | 0, 35 | str(MOCK_TODAY + timedelta(days=1)), 36 | ), 37 | ( 38 | 3, 39 | 2.36, 40 | 1, 41 | 1, 42 | str(MOCK_TODAY + timedelta(days=1)), 43 | ), 44 | ( 45 | 4, 46 | 2.5, 47 | 1, 48 | 1, 49 | str(MOCK_TODAY + timedelta(days=1)), 50 | ), 51 | ( 52 | 5, 53 | 2.6, 54 | 1, 55 | 1, 56 | str(MOCK_TODAY + timedelta(days=1)), 57 | ), 58 | ], 59 | ) 60 | def test_first_review( 61 | quality, 62 | expected_easiness, 63 | expected_interval, 64 | expected_repetitions, 65 | expected_review_date, 66 | ): 67 | 68 | reviewed = first_review(quality) 69 | 70 | assert reviewed["easiness"] == expected_easiness 71 | assert reviewed["interval"] == expected_interval 72 | assert reviewed["repetitions"] == expected_repetitions 73 | assert reviewed["review_datetime"] == expected_review_date 74 | 75 | 76 | @freeze_time(FREEZE_DATE) 77 | @pytest.mark.parametrize( 78 | "quality, review_datetime, expected_easiness, expected_interval, expected_repetitions, expected_review_date", 79 | [ 80 | (0, MOCK_TODAY, 1.7000000000000002, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 81 | (1, MOCK_TODAY, 1.96, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 82 | (2, MOCK_TODAY, 2.1799999999999997, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 83 | (3, MOCK_TODAY, 2.36, 1, 1, str(MOCK_TODAY + timedelta(days=1))), 84 | (4, MOCK_TODAY, 2.5, 1, 1, str(MOCK_TODAY + timedelta(days=1))), 85 | (5, MOCK_TODAY, 2.6, 1, 1, str(MOCK_TODAY + timedelta(days=1))), 86 | ], 87 | ) 88 | def test_first_review_given_date( 89 | quality, 90 | review_datetime, 91 | expected_easiness, 92 | expected_interval, 93 | expected_repetitions, 94 | expected_review_date, 95 | ): 96 | reviewed = first_review(quality, review_datetime) 97 | 98 | assert reviewed["easiness"] == expected_easiness 99 | assert reviewed["interval"] == expected_interval 100 | assert reviewed["repetitions"] == expected_repetitions 101 | assert reviewed["review_datetime"] == expected_review_date 102 | 103 | 104 | @freeze_time(FREEZE_DATE) 105 | @pytest.mark.parametrize( 106 | "quality, easiness, interval, repetitions, expected_easiness, expected_interval, expected_repetitions, expected_review_date", 107 | [ 108 | (0, 2.3, 12, 3, 1.5, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 109 | (1, 2.3, 12, 3, 1.7599999999999998, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 110 | (2, 2.3, 12, 3, 1.9799999999999998, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 111 | ( 112 | 3, 113 | 2.3, 114 | 12, 115 | 3, 116 | 2.1599999999999997, 117 | 28, 118 | 4, 119 | str(MOCK_TODAY + timedelta(days=28)), 120 | ), 121 | (4, 2.3, 12, 3, 2.3, 28, 4, str(MOCK_TODAY + timedelta(days=28))), 122 | (5, 2.3, 12, 3, 2.4, 28, 4, str(MOCK_TODAY + timedelta(days=28))), 123 | ], 124 | ) 125 | def test_review( 126 | quality, 127 | easiness, 128 | interval, 129 | repetitions, 130 | expected_easiness, 131 | expected_interval, 132 | expected_repetitions, 133 | expected_review_date, 134 | ): 135 | reviewed = review(quality, easiness, interval, repetitions) 136 | 137 | assert reviewed["easiness"] == expected_easiness 138 | assert reviewed["interval"] == expected_interval 139 | assert reviewed["repetitions"] == expected_repetitions 140 | assert reviewed["review_datetime"] == expected_review_date 141 | 142 | 143 | @pytest.mark.parametrize( 144 | "quality, easiness, interval, repetitions, review_datetime, expected_easiness, expected_interval, expected_repetitions, expected_review_date", 145 | [ 146 | ( 147 | 0, 148 | 2.3, 149 | 12, 150 | 3, 151 | MOCK_TODAY, 152 | 1.5, 153 | 1, 154 | 0, 155 | str(MOCK_TODAY + timedelta(days=1)), 156 | ), 157 | ( 158 | 1, 159 | 2.3, 160 | 12, 161 | 3, 162 | MOCK_TODAY, 163 | 1.7599999999999998, 164 | 1, 165 | 0, 166 | str(MOCK_TODAY + timedelta(days=1)), 167 | ), 168 | ( 169 | 2, 170 | 2.3, 171 | 12, 172 | 3, 173 | MOCK_TODAY, 174 | 1.9799999999999998, 175 | 1, 176 | 0, 177 | str(MOCK_TODAY + timedelta(days=1)), 178 | ), 179 | ( 180 | 3, 181 | 2.3, 182 | 12, 183 | 3, 184 | MOCK_TODAY, 185 | 2.1599999999999997, 186 | 28, 187 | 4, 188 | str(MOCK_TODAY + timedelta(days=28)), 189 | ), 190 | ( 191 | 4, 192 | 2.3, 193 | 12, 194 | 3, 195 | MOCK_TODAY, 196 | 2.3, 197 | 28, 198 | 4, 199 | str(MOCK_TODAY + timedelta(days=28)), 200 | ), 201 | (5, 2.3, 12, 3, MOCK_TODAY, 2.4, 28, 4, str(MOCK_TODAY + timedelta(days=28))), 202 | # test case for when easiness drops lower than 1.3 203 | (0, 1.3, 12, 3, MOCK_TODAY, 1.3, 1, 0, str(MOCK_TODAY + timedelta(days=1))), 204 | # test case for for repetitions equals to 2 205 | (4, 2.5, 1, 1, MOCK_TODAY, 2.5, 6, 2, str(MOCK_TODAY + timedelta(days=6))), 206 | ], 207 | ) 208 | def test_review_given_date( 209 | quality, 210 | easiness, 211 | interval, 212 | repetitions, 213 | review_datetime, 214 | expected_easiness, 215 | expected_interval, 216 | expected_repetitions, 217 | expected_review_date, 218 | ): 219 | reviewed = review(quality, easiness, interval, repetitions, review_datetime) 220 | 221 | assert reviewed["easiness"] == expected_easiness 222 | assert reviewed["interval"] == expected_interval 223 | assert reviewed["repetitions"] == expected_repetitions 224 | assert reviewed["review_datetime"] == expected_review_date 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperMemo2 2 | ![Python](https://img.shields.io/badge/python-3.8+-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a) 3 | [![Version](https://img.shields.io/pypi/v/supermemo2?logo=pypi&logoColor=white&style=flat-square&colorA=4c566a&colorB=90A2BC)](https://pypi.org/project/supermemo2/) 4 | [![Build](https://img.shields.io/github/workflow/status/alankan886/SuperMemo2/CI?logo=github-actions&logoColor=white&style=flat-square&colorA=4c566a&colorB=90BCA8)](https://github.com/alankan886/SuperMemo2/actions?query=workflow%3ACI) 5 | [![Coverage](https://img.shields.io/codecov/c/github/alankan886/SuperMemo2?logo=codecov&logoColor=white&style=flat-square&colorA=4c566a&colorB=90BCA8)](https://codecov.io/gh/alankan886/SuperMemo2) 6 | [![Downloads](https://static.pepy.tech/personalized-badge/supermemo2?period=total&units=international_system&left_color=grey&right_color=blue&left_text=downloads)](https://pepy.tech/project/supermemo2) 7 | 8 | A package that implemented the spaced repetition algorithm SM-2 for you to quickly calculate your next review date for whatever you are learning. 9 | 10 | 📌  **Note:** The algorithm SM-2 doesn't equal to the computer implementation SuperMemo2. In fact, the 3 earliest implementations (SuperMemo1, SuperMemo2 and SuperMemo3) all used algorithm SM-2. I didn't notice that when I first published the package on PyPI, and I can't change the package name. 11 | 12 | 📦  [PyPI page](https://pypi.org/project/supermemo2/) 13 | 14 | ## Table of Contents 15 | - [Motivation](#motivation) 16 | - [Installing and Supported Versions](#install-versions) 17 | - [A Simple Example](#example) 18 | - [Features](#features) 19 | - [What is SM-2?](#sm2) 20 | - [Code Reference](#code) 21 | - [Testing](#testing) 22 | - [Changelog](#changelog) 23 | - [Credits](#credits) 24 | 25 | 26 | 27 | ## Motivation 28 | The goal was to have an efficient way to calculate the next review date for studying/learning. Removes the burden of remembering the algorithm, equations, and math from the users. 29 | 30 | 31 | 32 | ## Installation and Supported Versions 33 | 34 | ### Package Install 35 | Install and upate the package using [pip](https://pip.pypa.io/en/stable/quickstart/): 36 | 37 | ```bash 38 | pip install -U supermemo2 39 | ``` 40 | 41 | 42 | 43 | ### To Play Around with the Code 44 | Download the code: 45 | 46 | ```bash 47 | git clone https://github.com/alankan886/SuperMemo2.git 48 | ``` 49 | 50 | Install dependencies to run the code: 51 | ```bash 52 | pip install -r requirements.txt 53 | ``` 54 | 55 | supermemo2 supports Python 3.8+ 56 | 57 | 58 | 59 | ## A Simple Example 60 | 61 | ```python 62 | from supermemo2 import first_review, review 63 | 64 | # first review 65 | # using quality=4 as an example, read below for what each value from 0 to 5 represents 66 | # review date would default to datetime.utcnow() (UTC timezone) if not provided 67 | r = first_review(4, "2024-06-22") 68 | # review prints { "easiness": 2.36, "interval": 1, "repetitions": 1, "review_datetime": "2024-06-23 01:06:02")) 69 | 70 | # second review 71 | second_review = review(4, r["easiness"], r["interval"], r["repetitions"], r["review_datetime"]) 72 | # or just unpack the first review dictionary 73 | second_review = review(4, **r) 74 | # second_review prints similar to example above. 75 | ``` 76 | 77 | 78 | 79 | ## Features 80 | 📣  Calculates the review date of the task following the SM-2 algorithm. 81 |
📣  The first_review method to calculate the review date at ease without having to know the initial values. 82 | 83 |
84 | 85 | ## What is SM-2? 86 | 🎥  If you are curious of what spaced repetition is, check this [short video](https://youtu.be/-uMMRjrzPmE?t=94) out. 87 | 88 | 📌  A longer but interactive [article](https://ncase.me/remember/) on spaced repetition learning. 89 | 90 | 📎  [The SM-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2) 91 | 92 | ### What are the "values"? 93 | The values are the: 94 | 95 | - Quality: The quality of recalling the answer from a scale of 0 to 5. 96 | - 5: perfect response. 97 | - 4: correct response after a hesitation. 98 | - 3: correct response recalled with serious difficulty. 99 | - 2: incorrect response; where the correct one seemed easy to recall. 100 | - 1: incorrect response; the correct one remembered. 101 | - 0: complete blackout. 102 | - Easiness: The easiness factor, a multipler that affects the size of the interval, determine by the quality of the recall. 103 | - Interval: The gap/space between your next review. 104 | - Repetitions: The count of correct response (quality >= 3) you have in a row. 105 | 106 | 107 | 108 | ## Code Reference 109 | **first_review(** quality, review_datetime=None**)** 110 | 111 |       function that calcualtes the next review datetime for the your first review without having to know the initial values, and returns a dictionary containing the new values. 112 | 113 | **Parameters:** 114 | - quality (int) - the recall quality of the review. 115 | - review_datetime (str or datetime.datetime) - optional parameter, the datetime in ISO format up to seconds in UTC timezone of the review. 116 | 117 | **Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_datetime. 118 | 119 | **Return Type:** Dict 120 | 121 | **Usage:** 122 | ```python 123 | from supermemo2 import first_review 124 | # using default datetime.utcnow() if you just reviewed it 125 | first_review(3) 126 | 127 | # providing string date in Year-Month-Day format 128 | first_review(3, "2024-06-22") 129 | 130 | # providing date object date 131 | from datetime import datetime 132 | d = datetime(2024, 1, 1) 133 | first_review(3, d) 134 | ``` 135 | 136 | **review(** quality, easiness, interval, repetitions, review_datetime=None **)** 137 | 138 |       Calcualtes the next review date based on previous values, and returns a dictionary containing the new values. 139 | 140 | **Parameters:** 141 | - quality (int) - the recall quality of the review. 142 | - easiness (float) - the easiness determines the interval. 143 | - interval (int) - the interval between the latest review date and the next review date. 144 | - repetitions (int) - the count of consecutive reviews with quality larger than 2. 145 | - review_datetime (str or datetime.datetime) - optional parameter, the datetime in ISO format up to seconds in UTC timezone of the review. 146 | 147 | **Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_datetime. 148 | 149 | **Return Type:** Dict 150 | 151 | **Usage:** 152 | ```python 153 | from supermemo2 import first_review, review 154 | # using previous values from first_review call 155 | r = first_review(3) 156 | 157 | # using default datetime.utcnow() if you just reviewed it 158 | review(3, r["easiness"], r["interval"], r["repetitions"]) 159 | 160 | # providing review_datetime from previous review 161 | review(3, r["easiness"], r["interval"], r["repetitions"], r["review_datetime"]) 162 | 163 | # providing string review_datetime 164 | review(3, r["easiness"], r["interval"], r["repetitions"], "2024-01-01") 165 | 166 | # providing datetime object review_datetime 167 | from datetime import datetime 168 | d = datetime(2024, 1, 1) 169 | review(3, r["easiness"], r["interval"], r["repetitions"], d) 170 | ``` 171 | 172 | 173 | 174 | ## Testing 175 | 176 | Assuming you [dowloaded the code and installed requirements](#download). 177 | 178 | ### Run the tests 179 | ```bash 180 | pytest tests/ 181 | ``` 182 | 183 | ### Check test coverages 184 | ```bash 185 | pytest --cov 186 | ``` 187 | Check coverage on [Codecov](https://codecov.io/gh/alankan886/SuperMemo2). 188 | 189 | 190 | 191 | ## Changelog 192 | 3.0.1 (2024-06-22): Minor changes, Update recommended 193 | - Forgot to update some code and tests from review_date to review_datetime, the returned dictionary was review_date instead review_datetime. 194 | 195 | 3.0.0 (2024-06-22): Major changes/rebuild, Update recommended 196 | - Rewrote the code to remove the class structure, simplfying the code and usability. 197 | - Update to provide datetime instead of just date, more specific with when to review. 198 | 199 | 2.0.0 (2021-03-28): Major changes/rebuild, Update recommended 200 | - Rebuilt and simplfied the package. 201 | 202 | 1.0.3 (2021-01-30): Minor bug fix, Update recommended 203 | - Re-evaluate the default date argument to first_review() on each call. 204 | 205 | 1.0.2 (2021-01-18): Major and Minor bug fix, Update recommended 206 | - Add required attrs package version to setup.py. 207 | - Allow users to access SMTwo model. 208 | - Fix E-Factor calculation when q < 3. 209 | 210 | 1.0.1 (2021-01-02): Fix tests, update README and add Github actions, Update not required 211 | - Add missing assertions to test_api.py. 212 | - Update README badges and fix format. 213 | - Add Github actions to run tests against Python versions 3.6 to 3.9 in different OS, and upload coverage to Codecov. 214 | 215 | 1.0.0 (2021-01-01): Complete rebuild, Update recommended 216 | - Build a new SMTwo class using the attrs package. 217 | - Provide API methods to quickly access the SMTwo class. 218 | - Develop 100% coverage integration and unit tests in a TDD manner. 219 | - Write new documentation. 220 | 221 | 0.1.0 (2020-07-14): Add tests, Update not required 222 | - Add passing unit tests with a coverage of 100%. 223 | 224 | 0.0.4 (2020-07-10): Minor bug fix, Update recommended 225 | - Fix interval calculation error when q < 3. 226 | 227 | 0.0.3 (2020-07-06): Documentation Update, Update not required 228 | - Add new section about SM-2 in documentation, and fix some formats in README. 229 | 230 | 0.0.2 (2020-07-05): Refactor feature, Update recommended 231 | - Refactor the supermemo2 algorithm code into a simpler structure, and remove unnecessary methods in the class. 232 | 233 | 0.0.1 (2020-07-02): Feature release 234 | - Initial Release 235 | 236 | 237 | 238 | ## Credits 239 | 240 | 1. [pytest](https://docs.pytest.org/en/stable/) 241 | 2. [The SM-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2) 242 | 243 | --------------------------------------------------------------------------------