diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 616d723..4d3df16 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,39 +1,42 @@
name: CI
on:
push:
- branches: ["master"]
+ branches: ["main"]
pull_request:
- branches: ["master"]
+ branches: ["main"]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Setup Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Generate coverage report
run: |
python -m pip install --upgrade pip
- pip install pytest
+ pip install -r requirements.txt
pytest
upload_coverage:
runs-on: macos-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Setup Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
- python-version: '3.9'
+ python-version: "3.10"
- name: Generate coverage report
run: |
python -m pip install --upgrade pip
- pip install pytest-cov
+ pip install -r requirements.txt
pytest --cov=supermemo2 --cov-report=xml
- name: "Upload coverage to Codecov"
- uses: codecov/codecov-action@v1
\ No newline at end of file
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: true
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/README.md b/README.md
index 80ef08c..0e1c7ca 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# SuperMemo2
-![Python](https://img.shields.io/badge/python-3+-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
+![Python](https://img.shields.io/badge/python-3.8+-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
[![Version](https://img.shields.io/pypi/v/supermemo2?logo=pypi&logoColor=white&style=flat-square&colorA=4c566a&colorB=90A2BC)](https://pypi.org/project/supermemo2/)
[![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)
[![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)
@@ -35,7 +35,7 @@ The goal was to have an efficient way to calculate the next review date for stud
Install and upate the package using [pip](https://pip.pypa.io/en/stable/quickstart/):
```bash
-pip3 install -U supermemo2
+pip install -U supermemo2
```
@@ -49,26 +49,26 @@ git clone https://github.com/alankan886/SuperMemo2.git
Install dependencies to run the code:
```bash
-pip3 install -r requirements.txt
+pip install -r requirements.txt
```
-supermemo2 supports Python 3+
+supermemo2 supports Python 3.8+
## A Simple Example
```python
-from supermemo2 import SMTwo
+from supermemo2 import first_review, review
# first review
# using quality=4 as an example, read below for what each value from 0 to 5 represents
-# review date would default to date.today() if not provided
-review = SMTwo.first_review(4, "2021-3-14")
-# review prints SMTwo(easiness=2.36, interval=1, repetitions=1, review_date=datetime.date(2021, 3, 15))
+# review date would default to datetime.utcnow() (UTC timezone) if not provided
+first_review = first_review(4, "2024-06-22")
+# review prints { "easiness": 2.36, "interval": 1, "repetitions": 1, "review_datetime": "2024-06-23 01:06:02"))
# second review
-review = SMTwo(review.easiness, review.interval, review.repetitions).review(4, "2021-3-14")
+second_review = review(4, first_review["easiness"], first_review["interval"], first_review["repetitions"], first_review["review_datetime"])
# review prints similar to example above.
```
@@ -104,78 +104,67 @@ The values are the:
## Code Reference
-### *class* supermemo2.SMTwo(easiness, interval, repetitions)
+**first_review(** quality, review_datetime=None**)**
-**Parameters:**
-- easiness (float) - the easiness determines the interval.
-- interval (int) - the interval between the latest review date and the next review date.
-- repetitions (int) - the count of consecutive reviews with quality larger than 2.
-
-
-
-**first_review(** quality, review_date=None, date_fmt=None **)**
-
- Static method that calcualtes the next review date for the first review without having to know the initial values, and returns a dictionary containing the new values.
+ 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.
**Parameters:**
- quality (int) - the recall quality of the review.
-- review_date (str or datetime.date) - optional parameter, the date of the review.
-- date_fmt (string) - optional parameter, the format of the review_date. Formats like `year_mon_day`, `mon_day_year` and `day_mon_year`.
+- review_datetime (str or datetime.datetime) - optional parameter, the datetime in ISO format up to seconds in UTC timezone of the review.
-**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_date.
+**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_datetime.
**Return Type:** Dict
**Usage:**
```python
-from supermemo2 import SMTwo, mon_day_year
-# using default date date.today()
-SMTwo.first_review(3)
+from supermemo2 import first_review
+# using default datetime.utcnow() if you just reviewed it
+first_review(3)
# providing string date in Year-Month-Day format
-SMTwo.first_review(3, "2021-12-01")
-
-# providing string date in Month-Day-Year format
-SMTwo.first_review(3, "12-01-2021", mon_day_year)
+first_review(3, "2024-06-22")
# providing date object date
-from datetime import date
-d = date(2021, 12, 1)
-SMTwo.first_review(3, d)
+from datetime import datetime
+d = datetime(2024, 1, 1)
+first_review(3, d)
```
-**review(** quality, review_date=None, date_fmt=None **)**
+**review(** quality, easiness, interval, repetitions, review_datetime=None **)**
Calcualtes the next review date based on previous values, and returns a dictionary containing the new values.
**Parameters:**
- quality (int) - the recall quality of the review.
-- review_date (str or datetime.date) - optional parameter, the date of the review.
-- date_fmt (string) - optional parameter, the format of the review_date. Formats like `year_mon_day`, `mon_day_year` and `day_mon_year`.
+- easiness (float) - the easiness determines the interval.
+- interval (int) - the interval between the latest review date and the next review date.
+- repetitions (int) - the count of consecutive reviews with quality larger than 2.
+- review_datetime (str or datetime.datetime) - optional parameter, the datetime in ISO format up to seconds in UTC timezone of the review.
-**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_date.
+**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_datetime.
**Return Type:** Dict
**Usage:**
```python
-from supermemo2 import SMTwo, mon_day_year
+from supermemo2 import first_review, review
# using previous values from first_review call
-r = SMTwo.first_review(3)
+r = first_review(3)
-# using default date date.today()
-SMTwo(r.easiness, r.interval, r.repetitions).review(3)
+# using default datetime.utcnow() if you just reviewed it
+review(3, r["easiness"], r["interval"], r["repetitions"])
-# providing string date in Year-Month-Day format
-SMTwo(r.easiness, r.interval, r.repetitions).review(3, "2021-12-01")
+# providing review_datetime from previous review
+review(3, r["easiness"], r["interval"], r["repetitions"], r["review_datetime"])
-# providing string date in Month-Day-Year format
-SMTwo(r.easiness, r.interval, r.repetitions).review(3, "12-01-2021", mon_day_year)
+# providing string review_datetime
+review(3, r["easiness"], r["interval"], r["repetitions"], "2024-01-01")
-# providing date object date
-from datetime import date
-d = date(2021, 12, 1)
-SMTwo(r.easiness, r.interval, r.repetitions).review(3, d)
+# providing datetime object review_datetime
+from datetime import datetime
+d = datetime(2024, 1, 1)
+review(3, r["easiness"], r["interval"], r["repetitions"], d)
```
@@ -198,6 +187,10 @@ Check coverage on [Codecov](https://codecov.io/gh/alankan886/SuperMemo2).
## Changelog
+3.0.0 (2024-06-22): Major changes/rebuild, Update recommended
+- Rewrote the code to remove the class structure, simplfying the code and usability.
+- Update to provide datetime instead of just date, more specific with when to review.
+
2.0.0 (2021-03-28): Major changes/rebuild, Update recommended
- Rebuilt and simplfied the package.
diff --git a/requirements.txt b/requirements.txt
index a236589..451dfe2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
pytest-cov==2.10.1
-
+freezegun==1.5.1
diff --git a/supermemo2/sm_two.py b/supermemo2/sm_two.py
index 15e55bc..899529e 100644
--- a/supermemo2/sm_two.py
+++ b/supermemo2/sm_two.py
@@ -1,69 +1,53 @@
from math import ceil
-from datetime import date, datetime, timedelta
+from datetime import datetime, timedelta
from typing import Optional, Union, Dict
-import attr
+def review(
+ quality: int,
+ easiness: float,
+ interval: int,
+ repetitions: int,
+ review_datetime: Optional[Union[datetime, str]] = None,
+) -> Dict:
+ if not review_datetime:
+ review_datetime = datetime.utcnow().isoformat(sep=" ", timespec="seconds")
+
+ if isinstance(review_datetime, str):
+ review_datetime = datetime.fromisoformat(review_datetime).replace(microsecond=0)
+
+ if quality < 3:
+ interval = 1
+ repetitions = 0
+ else:
+ if repetitions == 0:
+ interval = 1
+ elif repetitions == 1:
+ interval = 6
+ else:
+ interval = ceil(interval * easiness)
-year_mon_day = "%Y-%m-%d"
-mon_day_year = "%m-%d-%Y"
-day_mon_year = "%d-%m-%Y"
-
-
-@attr.s
-class SMTwo:
- easiness = attr.ib(validator=attr.validators.instance_of(float))
- interval = attr.ib(validator=attr.validators.instance_of(int))
- repetitions = attr.ib(validator=attr.validators.instance_of(int))
- review_date = attr.ib(init=False)
-
- @staticmethod
- def first_review(
- quality: int,
- review_date: Optional[Union[date, str]] = None,
- date_fmt: Optional[str] = None,
- ) -> "SMTwo":
- if not review_date:
- review_date = date.today()
-
- if not date_fmt:
- date_fmt = year_mon_day
-
- return SMTwo(2.5, 0, 0).review(quality, review_date, date_fmt)
-
- def review(
- self,
- quality: int,
- review_date: Optional[Union[date, str]] = None,
- date_fmt: Optional[str] = None,
- ) -> "SMTwo":
- if not review_date:
- review_date = date.today()
-
- if not date_fmt:
- date_fmt = year_mon_day
+ repetitions += 1
- if isinstance(review_date, str):
- review_date = datetime.strptime(review_date, date_fmt).date()
+ easiness += 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)
+ if easiness < 1.3:
+ easiness = 1.3
- if quality < 3:
- self.interval = 1
- self.repetitions = 0
- else:
- if self.repetitions == 0:
- self.interval = 1
- elif self.repetitions == 1:
- self.interval = 6
- else:
- self.interval = ceil(self.interval * self.easiness)
+ review_datetime += timedelta(days=interval)
- self.repetitions = self.repetitions + 1
+ return {
+ "easiness": easiness,
+ "interval": interval,
+ "repetitions": repetitions,
+ "review_date": str(review_datetime),
+ }
- self.easiness += 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)
- if self.easiness < 1.3:
- self.easiness = 1.3
- review_date += timedelta(days=self.interval)
- self.review_date = review_date
+def first_review(
+ quality: int,
+ review_datetime: Optional[Union[datetime, str]] = None,
+) -> Dict:
+ if not review_datetime:
+ review_datetime = datetime.utcnow()
- return self
+ return review(quality, 2.5, 0, 0, review_datetime)
diff --git a/tests/test_sm_two.py b/tests/test_sm_two.py
index 02f7006..96c89d8 100644
--- a/tests/test_sm_two.py
+++ b/tests/test_sm_two.py
@@ -1,19 +1,60 @@
-from datetime import date, timedelta
+from datetime import datetime, timedelta
import pytest
+from freezegun import freeze_time
-from supermemo2 import SMTwo, year_mon_day, mon_day_year, day_mon_year
+from supermemo2 import first_review, review
+FREEZE_DATE = "2024-01-01"
+MOCK_TODAY = datetime.fromisoformat(FREEZE_DATE).replace(microsecond=0)
+
+@freeze_time(FREEZE_DATE)
@pytest.mark.parametrize(
"quality, expected_easiness, expected_interval, expected_repetitions, expected_review_date",
[
- (0, 1.7000000000000002, 1, 0, date.today() + timedelta(days=1)),
- (1, 1.96, 1, 0, date.today() + timedelta(days=1)),
- (2, 2.1799999999999997, 1, 0, date.today() + timedelta(days=1)),
- (3, 2.36, 1, 1, date.today() + timedelta(days=1)),
- (4, 2.5, 1, 1, date.today() + timedelta(days=1)),
- (5, 2.6, 1, 1, date.today() + timedelta(days=1)),
+ (
+ 0,
+ 1.7000000000000002,
+ 1,
+ 0,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
+ (
+ 1,
+ 1.96,
+ 1,
+ 0,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
+ (
+ 2,
+ 2.1799999999999997,
+ 1,
+ 0,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
+ (
+ 3,
+ 2.36,
+ 1,
+ 1,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
+ (
+ 4,
+ 2.5,
+ 1,
+ 1,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
+ (
+ 5,
+ 2.6,
+ 1,
+ 1,
+ str(MOCK_TODAY + timedelta(days=1)),
+ ),
],
)
def test_first_review(
@@ -24,23 +65,24 @@ def test_first_review(
expected_review_date,
):
- reviewed = SMTwo.first_review(quality)
+ reviewed = first_review(quality)
- assert reviewed.easiness == expected_easiness
- assert reviewed.interval == expected_interval
- assert reviewed.repetitions == expected_repetitions
- assert reviewed.review_date == expected_review_date
+ assert reviewed["easiness"] == expected_easiness
+ assert reviewed["interval"] == expected_interval
+ assert reviewed["repetitions"] == expected_repetitions
+ assert reviewed["review_date"] == expected_review_date
+@freeze_time(FREEZE_DATE)
@pytest.mark.parametrize(
"quality, review_date, expected_easiness, expected_interval, expected_repetitions, expected_review_date",
[
- (0, date.today(), 1.7000000000000002, 1, 0, date.today() + timedelta(days=1)),
- (1, date.today(), 1.96, 1, 0, date.today() + timedelta(days=1)),
- (2, date.today(), 2.1799999999999997, 1, 0, date.today() + timedelta(days=1)),
- (3, date.today(), 2.36, 1, 1, date.today() + timedelta(days=1)),
- (4, date.today(), 2.5, 1, 1, date.today() + timedelta(days=1)),
- (5, date.today(), 2.6, 1, 1, date.today() + timedelta(days=1)),
+ (0, MOCK_TODAY, 1.7000000000000002, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (1, MOCK_TODAY, 1.96, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (2, MOCK_TODAY, 2.1799999999999997, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (3, MOCK_TODAY, 2.36, 1, 1, str(MOCK_TODAY + timedelta(days=1))),
+ (4, MOCK_TODAY, 2.5, 1, 1, str(MOCK_TODAY + timedelta(days=1))),
+ (5, MOCK_TODAY, 2.6, 1, 1, str(MOCK_TODAY + timedelta(days=1))),
],
)
def test_first_review_given_date(
@@ -51,41 +93,33 @@ def test_first_review_given_date(
expected_repetitions,
expected_review_date,
):
- reviewed = SMTwo.first_review(quality, review_date)
+ reviewed = first_review(quality, review_date)
- assert reviewed.easiness == expected_easiness
- assert reviewed.interval == expected_interval
- assert reviewed.repetitions == expected_repetitions
- assert reviewed.review_date == expected_review_date
-
-
-@pytest.mark.parametrize(
- "str_date, date_fmt",
- [
- ("2021-12-01", None),
- ("2021-12-01", year_mon_day),
- ("12-01-2021", mon_day_year),
- ("01-12-2021", day_mon_year),
- ],
-)
-def test_first_review_given_date_in_str(str_date, date_fmt):
- reviewed = SMTwo.first_review(3, str_date, date_fmt)
-
- assert reviewed.easiness == 2.36
- assert reviewed.interval == 1
- assert reviewed.repetitions == 1
- assert reviewed.review_date == date(2021, 12, 1) + timedelta(days=1)
+ assert reviewed["easiness"] == expected_easiness
+ assert reviewed["interval"] == expected_interval
+ assert reviewed["repetitions"] == expected_repetitions
+ assert reviewed["review_date"] == expected_review_date
+@freeze_time(FREEZE_DATE)
@pytest.mark.parametrize(
"quality, easiness, interval, repetitions, expected_easiness, expected_interval, expected_repetitions, expected_review_date",
[
- (0, 2.3, 12, 3, 1.5, 1, 0, date.today() + timedelta(days=1)),
- (1, 2.3, 12, 3, 1.7599999999999998, 1, 0, date.today() + timedelta(days=1)),
- (2, 2.3, 12, 3, 1.9799999999999998, 1, 0, date.today() + timedelta(days=1)),
- (3, 2.3, 12, 3, 2.1599999999999997, 28, 4, date.today() + timedelta(days=28)),
- (4, 2.3, 12, 3, 2.3, 28, 4, date.today() + timedelta(days=28)),
- (5, 2.3, 12, 3, 2.4, 28, 4, date.today() + timedelta(days=28)),
+ (0, 2.3, 12, 3, 1.5, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (1, 2.3, 12, 3, 1.7599999999999998, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (2, 2.3, 12, 3, 1.9799999999999998, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
+ (
+ 3,
+ 2.3,
+ 12,
+ 3,
+ 2.1599999999999997,
+ 28,
+ 4,
+ str(MOCK_TODAY + timedelta(days=28)),
+ ),
+ (4, 2.3, 12, 3, 2.3, 28, 4, str(MOCK_TODAY + timedelta(days=28))),
+ (5, 2.3, 12, 3, 2.4, 28, 4, str(MOCK_TODAY + timedelta(days=28))),
],
)
def test_review(
@@ -98,13 +132,12 @@ def test_review(
expected_repetitions,
expected_review_date,
):
- sm = SMTwo(easiness, interval, repetitions)
- reviewed = sm.review(quality)
+ reviewed = review(quality, easiness, interval, repetitions)
- assert reviewed.easiness == expected_easiness
- assert reviewed.interval == expected_interval
- assert reviewed.repetitions == expected_repetitions
- assert reviewed.review_date == expected_review_date
+ assert reviewed["easiness"] == expected_easiness
+ assert reviewed["interval"] == expected_interval
+ assert reviewed["repetitions"] == expected_repetitions
+ assert reviewed["review_date"] == expected_review_date
@pytest.mark.parametrize(
@@ -115,61 +148,61 @@ def test_review(
2.3,
12,
3,
- date.today(),
+ MOCK_TODAY,
1.5,
1,
0,
- date.today() + timedelta(days=1),
+ str(MOCK_TODAY + timedelta(days=1)),
),
(
1,
2.3,
12,
3,
- date.today(),
+ MOCK_TODAY,
1.7599999999999998,
1,
0,
- date.today() + timedelta(days=1),
+ str(MOCK_TODAY + timedelta(days=1)),
),
(
2,
2.3,
12,
3,
- date.today(),
+ MOCK_TODAY,
1.9799999999999998,
1,
0,
- date.today() + timedelta(days=1),
+ str(MOCK_TODAY + timedelta(days=1)),
),
(
3,
2.3,
12,
3,
- date.today(),
+ MOCK_TODAY,
2.1599999999999997,
28,
4,
- date.today() + timedelta(days=28),
+ str(MOCK_TODAY + timedelta(days=28)),
),
(
4,
2.3,
12,
3,
- date.today(),
+ MOCK_TODAY,
2.3,
28,
4,
- date.today() + timedelta(days=28),
+ str(MOCK_TODAY + timedelta(days=28)),
),
- (5, 2.3, 12, 3, date.today(), 2.4, 28, 4, date.today() + timedelta(days=28)),
+ (5, 2.3, 12, 3, MOCK_TODAY, 2.4, 28, 4, str(MOCK_TODAY + timedelta(days=28))),
# test case for when easiness drops lower than 1.3
- (0, 1.3, 12, 3, date.today(), 1.3, 1, 0, date.today() + timedelta(days=1)),
+ (0, 1.3, 12, 3, MOCK_TODAY, 1.3, 1, 0, str(MOCK_TODAY + timedelta(days=1))),
# test case for for repetitions equals to 2
- (4, 2.5, 1, 1, date.today(), 2.5, 6, 2, date.today() + timedelta(days=6)),
+ (4, 2.5, 1, 1, MOCK_TODAY, 2.5, 6, 2, str(MOCK_TODAY + timedelta(days=6))),
],
)
def test_review_given_date(
@@ -183,10 +216,9 @@ def test_review_given_date(
expected_repetitions,
expected_review_date,
):
- sm = SMTwo(easiness, interval, repetitions)
- reviewed = sm.review(quality, review_date)
+ reviewed = review(quality, easiness, interval, repetitions, review_date)
- assert reviewed.easiness == expected_easiness
- assert reviewed.interval == expected_interval
- assert reviewed.repetitions == expected_repetitions
- assert reviewed.review_date == expected_review_date
+ assert reviewed["easiness"] == expected_easiness
+ assert reviewed["interval"] == expected_interval
+ assert reviewed["repetitions"] == expected_repetitions
+ assert reviewed["review_date"] == expected_review_date