├── 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 | 
3 | [](https://pypi.org/project/supermemo2/)
4 | [](https://github.com/alankan886/SuperMemo2/actions?query=workflow%3ACI)
5 | [](https://codecov.io/gh/alankan886/SuperMemo2)
6 | [](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 |
--------------------------------------------------------------------------------