Repository: alankan886/SuperMemo2 (language: python)

Issue:
🔖 v2.0.0
After speaking with @blazej-czapp and receiving useful feedback, I'm looking to push out a new version that's simpler and more user-friendly.

To toss off the over-engineered code, my main goals are:

1. Keeping the class attributes simple (i.e. no previous and current values)
2. Adding `review(quality, review_date)` function and keeping `first_review(quality, review_date)`, removing everything else.
3. Allowing the user to input date as a string with format Year-Month-Day and other common formats.

The flow of using the package when pulling data from the database would be:
```python
from supermemo2 import SMTwo
# first review
review = SMTwo.first_review(quality, review_date)

# after first review
# your read data from db for your existing values
# new review
# returns an SMTwo object with the new values
review = SMTwo(easiness, interval, repetitions).review(quality, review_date)
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b539302..d29cb0e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,13 +10,12 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.6, 3.7, 3.8, 3.9]
     steps:
       - uses: actions/checkout@v2
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: ${{ matrix.python-version }}
+          python-version: '3.x'
       - name: Generate coverage report
         run: |
           python -m pip install --upgrade pip
diff --git a/README.md b/README.md
index 6ed0817..70f689d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
 # SuperMemo2
-![Python](https://img.shields.io/badge/python-3.6+-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
+![Python](https://img.shields.io/badge/python-3+-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)
-[![Download](https://img.shields.io/badge/downloads-2k-light--blue.svg?style=flat-square&colorA=4c566a&colorB=90A2BC)](https://pepy.tech/project/SuperMemo2)
+[![Download](https://img.shields.io/badge/downloads-4k-light--blue.svg?style=flat-square&colorA=4c566a&colorB=90A2BC)](https://pepy.tech/project/SuperMemo2)
 
 A package that implemented the spaced repetition algorithm SM-2 for you to quickly calculate your next review date for whatever you are learning.
 
@@ -16,12 +16,8 @@ A package that implemented the spaced repetition algorithm SM-2 for you to quick
 - [Installing and Supported Versions](#install-versions)
 - [A Simple Example](#example)
 - [Features](#features)
-	- [Potential Features](#potential)
-- [What is SuperMemo-2?](#sm2)
-- [API Reference](#api)
-	- [Main Interface](#main-interface)
-	- [Exceptions](#excep)
-	- [Lower-Level Classes](#classes)
+- [What is SM-2?](#sm2)
+- [Code Reference](#code)
 - [Testing](#testing)
 - [Changelog](#changelog)
 - [Credits](#credits)
@@ -56,51 +52,40 @@ Install dependencies to run the code:
 pip3 install -r requirements.txt
 ```
 
-supermemo2 supports Python 3.6+ and requires attrs >= 20.1.0.
+supermemo2 supports Python 3+
 
 <a name="example">
 
 ## A Simple Example
 
-We start with a recall quality of 3, and the review date defaults to today (let's pretend it's 2021-01-01). 
-
-Using the current values from the first review can help us calculate for the second review.
+```python
+from supermemo2 import SMTwo
 
-Grab the current values from the first review, and update the recall quality. Then calculate the next review date.
+# 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))
 
-```python
->>> from supermemo2 import first_review
->>> smtwo = first_review(3)
->>> print(smtwo.review_date)
-2021-01-02
->>> record = smtwo.as_dict(curr=True)
->>> record["quality"] = 5
->>> smtwo.calc(**record)
->>> print(smtwo.review_date)
-2021-01-08
+# second review
+review = SMTwo(review.easiness, review.interval, review.repetitions).review(4, "2021-3-14")
+# review prints similar to example above.
 ```
 
 <a name="features">
 
 ## Features
-📣 &nbsp;Calculates the next review date of the task following the SuperMemo-2/SM-2 algorithm.
-<br/> 📣 &nbsp;The first_review method to create a new instance at ease without having to know the initial values.
-<br/> 📣 &nbsp;The modify method to modify existing instance values that recalculates the new values.
-<br/> 📣 &nbsp;The json and dict methods to export the instance values and to help calculate the next review date.
-
-<a name="potential">
-
-### Potential Features
-- Allow users to pass the review date as a string in many formats.
+📣 &nbsp;Calculates the review date of the task following the SM-2 algorithm.
+<br/> 📣 &nbsp;The first_review method to calculate the review date at ease without having to know the initial values.
 
 <a name="sm2">
 
-## What is SuperMemo-2?
+## What is SM-2?
 🎥 &nbsp;If you are curious of what spaced repetition is, check this [short video](https://youtu.be/-uMMRjrzPmE?t=94) out.
 
 📌 &nbsp;A longer but interactive [article](https://ncase.me/remember/) on spaced repetition learning.
 
-📎 &nbsp;[The SuperMemo-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2)
+📎 &nbsp;[The SM-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2)
 
 ### What are the "values"?
 The values are the:
@@ -116,155 +101,83 @@ The values are the:
 - Interval: The gap/space between your next review.
 - Repetitions: The count of correct response (quality >= 3) you have in a row.
 
-<a name="api">
+<a name="code">
 
-## API Reference
+## Code Reference
+### *class* supermemo2.SMTwo(easiness, interval, repetitions)
 
-<a name="main-interface">
+**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.
 
-### Main Interface
-supermemo2.**first_review**(quality, review_date=datetime.date.today())
+<br>
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Calcualtes next review date without having to know the initial values, and returns an SMTwo object with new values.
+**first_review(** quality, review_date=None, date_fmt=None **)**
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **quality**(int) - the quality of the response/recall from a scale of 0 to 5.
-- **review_date** (Optional[datetime.date]) - the last review date.
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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.
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Returns:** SMTwo object
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Return Type:** supermemo2.SMTwo
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Usage:
-```python
->>> from supermemo2 import first_review
->>> from datetime import date
->>> smtwo = first_review(3, date(2021, 1, 1))
->>> print(smtwo.review_date)
-2021-01-02
-```
+**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`.
 
-supermemo2.**modify**(instance, quality=None, easiness=None, interval=None, repetitions=None, review_date=None)
+**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_date.
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Modifies previously inserted values.
+**Return Type:** Dict
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **instance** (SMTwo) - the SMTwo instance to modify.
-- **quality** (Optional[int]) - the quality value to replace the previous quality value.
-- **easiness** (Optional[float])- the easiness value to replace the previous easiness value.
-- **interval** (Optional[int]) - the interval value to replace the previous interval value.
-- **repetitions** (Optional[int])  - the repetitions value to replace the previous reptitions value.
-- **review_date** (Optional[datetime.date]) - the review date to replace the previous review date.
-
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Returns:** None
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Return Type:** None
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Usage:
+**Usage:**
 ```python
->>> from supermemo2 import first_review, modify
->>> smtwo = first_review(3)
->>> print(smtwo.quality)
-3
->>> modify(smtwo, quality=5)
->>> print(smtwo.quality)
-5
-```
-
-<a name="excep">
-
-### Exceptions
-exception supermemo2.exceptions.**CalcNotCalledYet**
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Other methods are called before the values are calculated.
+from supermemo2 import SMTwo, mon_day_year
+# using default date date.today()
+SMTwo.first_review(3)
 
-<a name="classes">
+# providing string date in Year-Month-Day format
+SMTwo.first_review(3, "2021-12-01")
 
-### Lower-Level Classes
-class supermemo2.**SMTwo()**
+# providing string date in Month-Day-Year format
+SMTwo.first_review(3, "12-01-2021", mon_day_year)
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Generates all the instances and contains the tools.
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;I would not recommend directly generating an instance from this class.
-
-**calc**(quality, easiness, interval, repetitions, review_date)
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Calculates the values. For the first review, the initial/previous values would be 2.5 for easiness, 1 for interval and 1 for repetitions.
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **quality** (Optional[int]) - the quality value received from the last calculation.
-- **easiness** (Optional[float])- the easiness value received from the last calculation.
-- **interval** (Optional[int]) - the interval value received from the last calculation.
-- **repetitions** (Optional[int])  - the repetitions value received from the last calculation.
-- **review_date** (Optional[datetime.date]) - the review date received from the last calculation.
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Returns:** None
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Return Type** None
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Usage:
-```python
->>> from supermemo2 import SMTwo
->>> from datetime import date
->>> smtwo = SMTwo()
->>> smtwo.calc(3, 2.5, 1, 1, date(2021, 1, 1))
->>> print(smtwo.review_date)
-2021-01-02
+# providing date object date
+from datetime import date
+d = date(2021, 12, 1)
+SMTwo.first_review(3, d)
 ```
 
-**json**(prev=None, curr=None)
+**review(** quality, review_date=None, date_fmt=None **)**
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Returns a string of the values in JSON format.
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Calcualtes the next review date based on previous values, and returns a dictionary containing the new values.
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **prev** (Optional[bool]) - If true, export only previous values.
-- **curr** (Optional[bool]) - If true, export only current 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`.
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Returns:** String in JSON format
+**Returns:** dictionary containing values like quality, easiness, interval, repetitions and review_date.
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Return Type** String
+**Return Type:** Dict
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Usage:
+**Usage:**
 ```python
->>> from supermemo2 import first_visit
->>> from datetime import date
->>> smtwo = first_visit(3, date(2021, 1, 1))
->>> print(smtwo.json())
-'{"quality": 3, "prev_easiness": 2.5, "prev_interval": 1, "prev_repetitions": 1, "prev_review_date": datetime.date(2021, 1, 1),"easiness": 2.36, "interval": 2, "repetitions": 1, "review_date": datetime.date(2021, 1, 2)}'
-```
-
-**dict**(prev=None, curr=None)
+from supermemo2 import SMTwo, mon_day_year
+# using previous values from first_review call
+r = SMTwo.first_review(3)
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Returns a the values in dictionary format.
+# using default date date.today()
+SMTwo(r.easiness, r.interval, r.repetitions).review(3)
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **prev** (Optional[bool]) - If true, export only previous values.
-- **curr** (Optional[bool]) - If true, export only current values.
+# providing string date in Year-Month-Day format
+SMTwo(r.easiness, r.interval, r.repetitions).review(3, "2021-12-01")
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Returns:** Dictionary
+# providing string date in Month-Day-Year format
+SMTwo(r.easiness, r.interval, r.repetitions).review(3, "12-01-2021", mon_day_year)
 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Return Type** Dict
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Usage:
-```python
->>> from supermemo2 import first_visit
->>> from datetime import date
->>> smtwo = first_visit(3, date(2021, 1, 1))
->>> print(smtwo.dict())
-'{"quality": 3, "prev_easiness": 2.5, "prev_interval": 1, "prev_repetitions": 1, "prev_review_date": datetime.date(2021, 1, 1),"easiness": 2.36, "interval": 2, "repetitions": 1, "review_date": datetime.date(2021, 1, 2)}'
+# providing date object date
+from datetime import date
+d = date(2021, 12, 1)
+SMTwo(r.easiness, r.interval, r.repetitions).review(3, d)
 ```
 
-class supermemo2.SMTwo.**Prev**(easiness, interval, repetitions, review_date)
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Stores the previous values.
-
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**Parameters:**
-- **easiness** (float)- the previous easiness value.
-- **interval** (int) - the previous interval value.
-- **repetitions** (int)  - the previous repetitions value.
-- **review_date** (datetime.date) - the previous review date.
-
 <a name="testing">
 
 ## Testing
@@ -278,13 +191,16 @@ pytest tests/
 
 ### Check test coverages
 ```bash
-pytest --cov=supermemo2
+pytest --cov
 ```
 Check coverage on [Codecov](https://codecov.io/gh/alankan886/SuperMemo2).
 
 <a name="changelog">
 
 ## Changelog
+2.0.0 (2021-03-28): Major changes/rebuild, Update recommended
+- Rebuilt and simplfied the package.
+
 1.0.3 (2021-01-30): Minor bug fix, Update recommended
 - Re-evaluate the default date argument to first_review() on each call.
 
@@ -311,7 +227,7 @@ Check coverage on [Codecov](https://codecov.io/gh/alankan886/SuperMemo2).
 - Fix interval calculation error when q < 3.
 
 0.0.3 (2020-07-06): Documentation Update, Update not required
-- Add new section about SuperMemo-2 in documentation, and fix some formats in README.
+- Add new section about SM-2 in documentation, and fix some formats in README.
 
 0.0.2 (2020-07-05): Refactor feature, Update recommended
 - Refactor the supermemo2 algorithm code into a simpler structure, and remove unnecessary methods in the class.
@@ -323,7 +239,6 @@ Check coverage on [Codecov](https://codecov.io/gh/alankan886/SuperMemo2).
 
 ## Credits
 
-1. [attrs](https://www.attrs.org/en/stable/index.html)
-2. [pytest](https://docs.pytest.org/en/stable/)
-3. [The SuperMemo-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2)
+1. [pytest](https://docs.pytest.org/en/stable/)
+2. [The SM-2 Algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2)
 
diff --git a/requirements.txt b/requirements.txt
index 20da86f..a236589 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,2 @@
-pytest==6.2.1
 pytest-cov==2.10.1
-tox==3.20.1
 
diff --git a/setup.py b/setup.py
index b26fee1..ef162e2 100644
--- a/setup.py
+++ b/setup.py
@@ -3,47 +3,49 @@
 from setuptools import setup, find_packages, Command
 
 classifiers = [
-    'Development Status :: 5 - Production/Stable',
-    'Intended Audience :: Education',
-    'Operating System :: OS Independent',
-    'License :: OSI Approved :: MIT License',
-    'Programming Language :: Python',
-    'Programming Language :: Python :: 3',
-    'Programming Language :: Python :: 3.6',
-    'Programming Language :: Python :: 3.7',
-    'Programming Language :: Python :: 3.8',
-    'Programming Language :: Python :: 3.9',
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Education",
+    "Operating System :: OS Independent",
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
 ]
 
-with open('README.md', encoding='utf-8') as f:
+with open("README.md", encoding="utf-8") as f:
     readme_content = f.read().strip()
 
+
 class CleanCommand(Command):
     """Custom clean command to tidy up the project root."""
+
     user_options = []
+
     def initialize_options(self):
         pass
+
     def finalize_options(self):
         pass
+
     def run(self):
-        os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
+        os.system("rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info")
+
 
 setup(
-    name='supermemo2',
-    version='1.0.3',
-    description='Implemented the SuperMemo-2/SM-2 algorithm for spaced repetition learning.',
+    name="supermemo2",
+    version="2.0.0",
+    description="Implemented the SM-2 algorithm for spaced repetition learning.",
     long_description=readme_content,
-    long_description_content_type='text/markdown',
-    url='https://github.com/alankan886/SuperMemo2',
-    author='Alan Kan',
-    author_email='',
-    license='MIT',
+    long_description_content_type="text/markdown",
+    url="https://github.com/alankan886/SuperMemo2",
+    author="Alan Kan",
+    author_email="",
+    license="MIT",
     classifiers=classifiers,
-    keywords='spaced-repetition SuperMemo-2 SM-2 SuperMemo Python',
+    keywords="spaced-repetition SM-2 SuperMemo Python",
     packages=find_packages(),
     include_package_data=True,
-    install_requires=['attrs>=20.1.0'],
+    install_requires=["attrs"],
     cmdclass={
-        'clean': CleanCommand,
+        "clean": CleanCommand,
     },
 )
diff --git a/supermemo2/__init__.py b/supermemo2/__init__.py
index ed0a08d..cd66f4b 100644
--- a/supermemo2/__init__.py
+++ b/supermemo2/__init__.py
@@ -1,2 +1,1 @@
-from .api import first_review, modify
-from .model import SMTwo
+from .sm_two import *
diff --git a/supermemo2/api.py b/supermemo2/api.py
deleted file mode 100644
index 4a5967e..0000000
--- a/supermemo2/api.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from datetime import date
-
-from .model import SMTwo
-
-
-def first_review(quality, last_review=None):
-    # Default arguments are evaluated only once - when the function is first parsed. We need it to reevaluate the date
-    # each time this function is called.
-    if last_review is None:
-        last_review = date.today()
-
-    smtwo = SMTwo()
-    smtwo.calc(quality, 2.5, 1, 1, last_review)
-    return smtwo
-
-
-def modify(instance, quality=None, easiness=None, interval=None, repetitions=None, review_date=None):
-    # specifically checking quality is None or not,
-    # because quality can equal to 0, and that can get pick up instead.
-    if not(quality is not None or easiness or interval or repetitions or review_date):
-        message = "a new value is not provided for modification"
-        raise ValueError(message)
-
-    q = instance.quality
-    ez = instance.prev.easiness
-    inter = instance.prev.interval
-    rep = instance.prev.repetitions
-    rd = instance.prev.review_date
-
-    if quality is not None:
-        q = quality
-
-    if easiness:
-        ez = easiness
-
-    if interval:
-        inter = interval
-
-    if repetitions:
-        rep = repetitions
-
-    if review_date is not None:
-        rd = review_date
-
-    instance.calc(q, ez, inter, rep, rd)
-    return instance
diff --git a/supermemo2/exceptions.py b/supermemo2/exceptions.py
deleted file mode 100644
index 6ea72b6..0000000
--- a/supermemo2/exceptions.py
+++ /dev/null
@@ -1,4 +0,0 @@
-class CalcNotCalledYet(Exception):
-    def __init__(self, message="SMTwo.calc method is required to be called first"):
-        self.message = message
-        super().__init__(self.message)
diff --git a/supermemo2/model.py b/supermemo2/model.py
deleted file mode 100644
index 1275d63..0000000
--- a/supermemo2/model.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import json
-from datetime import date, timedelta
-
-import attr
-
-from .exceptions import CalcNotCalledYet
-from .util import add_from_dict
-
-
-@attr.s
-class SMTwo:
-    __quality = attr.ib(
-        init=False,
-        validator=[
-            attr.validators.instance_of(int),
-            attr.validators.in_(range(6))
-        ],
-        on_setattr=attr.setters.validate
-    )
-    __easiness = attr.ib(init=False)
-    __interval = attr.ib(init=False)
-    __repetitions = attr.ib(init=False)
-    __review_date = attr.ib(init=False)
-    __prev = attr.ib(init=False)
-
-    @property
-    def quality(self):
-        return self.__quality
-
-    @property
-    def easiness(self):
-        return self.__easiness
-
-    @property
-    def interval(self):
-        return self.__interval
-
-    @property
-    def repetitions(self):
-        return self.__repetitions
-
-    @property
-    def review_date(self):
-        return self.__review_date
-
-    @property
-    def prev(self):
-        return self.__prev
-
-    @attr.s
-    class Prev:
-        __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(validator=attr.validators.instance_of(date))
-
-        @__easiness.validator
-        def __check_valid_easiness(self, attribute, value):
-            if value < 1.3:
-                message = "easiness has a minimum value of 1.3"
-                raise ValueError(message)
-
-        @__interval.validator
-        def __check_valid_interval(self, attribute, value):
-            if value < 1:
-                message = "interval has a minimum value of 1"
-                raise ValueError(message)
-
-        @__repetitions.validator
-        def __check_valid_repetitions(self, attribute, value):
-            if value < 1:
-                message = "repetitions has a minimum value of 1"
-                raise ValueError(message)
-
-        @property
-        def interval(self):
-            return self.__interval
-
-        @property
-        def easiness(self):
-            return self.__easiness
-
-        @property
-        def repetitions(self):
-            return self.__repetitions
-
-        @property
-        def review_date(self):
-            return self.__review_date
-
-    def calc(self, quality, easiness, interval, repetitions, review_date):
-        self.__quality = quality
-        self.__prev = self.Prev(easiness, interval, repetitions, review_date)
-
-        if quality < 3:
-            self.__interval = 1
-            self.__repetitions = 1
-        else:
-            if repetitions == 1:
-                self.__interval = 1
-            elif repetitions == 2:
-                self.__interval = 6
-            else:
-                self.__interval = round(interval * easiness)
-
-            self.__repetitions = repetitions + 1
-
-        self.__easiness = easiness - 0.8 + 0.28 * quality - 0.02 * quality**2
-        if self.__easiness < 1.3:
-            self.__easiness = 1.3
-
-        self.__review_date = review_date + timedelta(days=self.__interval)
-
-    def json(self, prev=None, curr=None):
-        try:
-            attrs = {"quality": self.__quality}
-            prev_attrs = {
-                "prev_easiness": self.__prev.easiness,
-                "prev_interval": self.__prev.interval,
-                "prev_repetitions": self.__prev.repetitions,
-                "prev_review_date": str(self.__prev.review_date)
-            }
-            cur_attrs = {
-                "easiness": self.__easiness,
-                "interval": self.__interval,
-                "repetitions": self.__repetitions,
-                "review_date": str(self.__review_date)
-            }
-        except AttributeError:
-            raise CalcNotCalledYet
-
-        if prev:
-            add_from_dict(prev_attrs, attrs)
-        elif curr:
-            add_from_dict(cur_attrs, attrs)
-        else:
-            add_from_dict(prev_attrs, attrs)
-            add_from_dict(cur_attrs, attrs)
-
-        return json.dumps(attrs)
-
-    def dict(self, prev=None, curr=None):
-        try:
-            attrs = {"quality": self.__quality}
-            prev_attrs = {
-                "prev_easiness": self.__prev.easiness,
-                "prev_interval": self.__prev.interval,
-                "prev_repetitions": self.__prev.repetitions,
-                "prev_review_date": self.__prev.review_date
-            }
-            cur_attrs = {
-                "easiness": self.__easiness,
-                "interval": self.__interval,
-                "repetitions": self.__repetitions,
-                "review_date": self.__review_date
-            }
-        except AttributeError:
-            raise CalcNotCalledYet
-
-        if prev:
-            add_from_dict(prev_attrs, attrs)
-        elif curr:
-            add_from_dict(cur_attrs, attrs)
-        else:
-            add_from_dict(prev_attrs, attrs)
-            add_from_dict(cur_attrs, attrs)
-
-        return attrs
diff --git a/supermemo2/sm_two.py b/supermemo2/sm_two.py
new file mode 100644
index 0000000..15e55bc
--- /dev/null
+++ b/supermemo2/sm_two.py
@@ -0,0 +1,69 @@
+from math import ceil
+from datetime import date, datetime, timedelta
+from typing import Optional, Union, Dict
+
+import attr
+
+
+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
+
+        if isinstance(review_date, str):
+            review_date = datetime.strptime(review_date, date_fmt).date()
+
+        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)
+
+            self.repetitions = self.repetitions + 1
+
+        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
+
+        return self
diff --git a/supermemo2/util.py b/supermemo2/util.py
deleted file mode 100644
index 4624117..0000000
--- a/supermemo2/util.py
+++ /dev/null
@@ -1,3 +0,0 @@
-def add_from_dict(dict, result):
-    for key, value in dict.items():
-        result[key] = value


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
deleted file mode 100644
index a481428..0000000
--- a/tests/conftest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from datetime import date
-
-import pytest
-
-from supermemo2.model import SMTwo
-
-
-@pytest.fixture
-def empty_smtwo():
-    return SMTwo()
-
-
-@pytest.fixture
-def smtwo_after_calc():
-    smtwo = SMTwo()
-    smtwo.calc(3, 2.5, 1, 1, date.today())
-    return smtwo
diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py
deleted file mode 100644
index 01c2b83..0000000
--- a/tests/integration/test_api.py
+++ /dev/null
@@ -1,121 +0,0 @@
-import json
-from datetime import date, timedelta
-from copy import deepcopy
-
-import pytest
-
-from supermemo2 import first_review, modify
-from supermemo2.exceptions import CalcNotCalledYet
-
-
-@pytest.mark.parametrize("quality", (0, 1, 2, 3, 4, 5))
-def test_first_review_default_review_date(quality):
-    smtwo = first_review(quality)
-    assert smtwo.quality == quality
-    assert smtwo.prev.easiness == 2.5
-    assert smtwo.prev.interval == 1
-    assert smtwo.prev.repetitions == 1
-    assert smtwo.prev.review_date == date.today()
-    assert smtwo.review_date == date.today() + timedelta(days=1)
-
-
-@pytest.mark.parametrize(
-    "quality, review_date",
-    (
-        [0, date.today() + timedelta(days=1)],
-        [1, date.today() + timedelta(days=12)],
-        [2, date.today() + timedelta(days=8)],
-        [3, date.today()],
-        [4, date.today() + timedelta(days=11)],
-        [5, date.today() + timedelta(days=2)]
-    )
-)
-def test_first_review(quality, review_date):
-    smtwo = first_review(quality, review_date)
-    assert smtwo.quality == quality
-    assert smtwo.prev.easiness == 2.5
-    assert smtwo.prev.interval == 1
-    assert smtwo.prev.repetitions == 1
-    assert smtwo.prev.review_date == review_date
-    assert smtwo.review_date == review_date + timedelta(days=1)
-
-
-@pytest.mark.parametrize("quality", (0, 1, 2, 3, 4, 5))
-def test_modify_quality(smtwo_after_calc, quality):
-    prev_ez = smtwo_after_calc.prev.easiness
-    prev_inter = smtwo_after_calc.prev.interval
-    prev_rep = smtwo_after_calc.prev.repetitions
-    prev_rd = smtwo_after_calc.prev.review_date
-    modify(smtwo_after_calc, quality=quality)
-
-    assert smtwo_after_calc.quality == quality
-    assert smtwo_after_calc.prev.easiness == prev_ez
-    assert smtwo_after_calc.prev.interval == prev_inter
-    assert smtwo_after_calc.prev.repetitions == prev_rep
-    assert smtwo_after_calc.prev.review_date == prev_rd
-
-
-@pytest.mark.parametrize("easiness", (1.3, 2.5, 3.0))
-def test_modify_easiness(smtwo_after_calc, easiness):
-    prev_q = smtwo_after_calc.quality
-    prev_inter = smtwo_after_calc.prev.interval
-    prev_rep = smtwo_after_calc.prev.repetitions
-    prev_rd = smtwo_after_calc.prev.review_date
-    modify(smtwo_after_calc, easiness=easiness)
-
-    assert smtwo_after_calc.quality == prev_q
-    assert smtwo_after_calc.prev.easiness == easiness
-    assert smtwo_after_calc.prev.interval == prev_inter
-    assert smtwo_after_calc.prev.repetitions == prev_rep
-    assert smtwo_after_calc.prev.review_date == prev_rd
-
-
-@pytest.mark.parametrize("interval", (1, 6, 24))
-def test_modify_interval(smtwo_after_calc, interval):
-    prev_q = smtwo_after_calc.quality
-    prev_ez = smtwo_after_calc.prev.easiness
-    prev_rep = smtwo_after_calc.prev.repetitions
-    prev_rd = smtwo_after_calc.prev.review_date
-    modify(smtwo_after_calc, interval=interval)
-
-    assert smtwo_after_calc.quality == prev_q
-    assert smtwo_after_calc.prev.easiness == prev_ez
-    assert smtwo_after_calc.prev.interval == interval
-    assert smtwo_after_calc.prev.repetitions == prev_rep
-    assert smtwo_after_calc.prev.review_date == prev_rd
-
-
-@pytest.mark.parametrize("repetitions", (1, 2, 99))
-def test_modify_repetitions(smtwo_after_calc, repetitions):
-    prev_q = smtwo_after_calc.quality
-    prev_ez = smtwo_after_calc.prev.easiness
-    prev_inter = smtwo_after_calc.prev.interval
-    prev_rd = smtwo_after_calc.prev.review_date
-    modify(smtwo_after_calc, repetitions=repetitions)
-
-    assert smtwo_after_calc.quality == prev_q
-    assert smtwo_after_calc.prev.easiness == prev_ez
-    assert smtwo_after_calc.prev.interval == prev_inter
-    assert smtwo_after_calc.prev.repetitions == repetitions
-    assert smtwo_after_calc.prev.review_date == prev_rd
-
-
-@pytest.mark.parametrize("review_date", (date(2020, 12, 20),))
-def test_modify_last_review(smtwo_after_calc, review_date):
-    prev_q = smtwo_after_calc.quality
-    prev_ez = smtwo_after_calc.prev.easiness
-    prev_inter = smtwo_after_calc.prev.interval
-    prev_rep = smtwo_after_calc.prev.repetitions
-    modify(smtwo_after_calc, review_date=review_date)
-
-    assert smtwo_after_calc.quality == prev_q
-    assert smtwo_after_calc.prev.easiness == prev_ez
-    assert smtwo_after_calc.prev.interval == prev_inter
-    assert smtwo_after_calc.prev.repetitions == prev_rep
-    assert smtwo_after_calc.prev.review_date == review_date
-
-
-def test_modify_no_inputs(smtwo_after_calc):
-    with pytest.raises(ValueError) as excinfo:
-        modify(smtwo_after_calc)
-    assert excinfo.value.args[0] == "a new value is not provided for modification"
diff --git a/tests/test_sm_two.py b/tests/test_sm_two.py
new file mode 100644
index 0000000..02f7006
--- /dev/null
+++ b/tests/test_sm_two.py
@@ -0,0 +1,192 @@
+from datetime import date, timedelta
+
+import pytest
+
+from supermemo2 import SMTwo, year_mon_day, mon_day_year, day_mon_year
+
+
+@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)),
+    ],
+)
+def test_first_review(
+    quality,
+    expected_easiness,
+    expected_interval,
+    expected_repetitions,
+    expected_review_date,
+):
+
+    reviewed = SMTwo.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
+
+
+@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)),
+    ],
+)
+def test_first_review_given_date(
+    quality,
+    review_date,
+    expected_easiness,
+    expected_interval,
+    expected_repetitions,
+    expected_review_date,
+):
+    reviewed = SMTwo.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)
+
+
+@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)),
+    ],
+)
+def test_review(
+    quality,
+    easiness,
+    interval,
+    repetitions,
+    expected_easiness,
+    expected_interval,
+    expected_repetitions,
+    expected_review_date,
+):
+    sm = SMTwo(easiness, interval, repetitions)
+    reviewed = sm.review(quality)
+
+    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(
+    "quality, easiness, interval, repetitions, review_date, expected_easiness, expected_interval, expected_repetitions, expected_review_date",
+    [
+        (
+            0,
+            2.3,
+            12,
+            3,
+            date.today(),
+            1.5,
+            1,
+            0,
+            date.today() + timedelta(days=1),
+        ),
+        (
+            1,
+            2.3,
+            12,
+            3,
+            date.today(),
+            1.7599999999999998,
+            1,
+            0,
+            date.today() + timedelta(days=1),
+        ),
+        (
+            2,
+            2.3,
+            12,
+            3,
+            date.today(),
+            1.9799999999999998,
+            1,
+            0,
+            date.today() + timedelta(days=1),
+        ),
+        (
+            3,
+            2.3,
+            12,
+            3,
+            date.today(),
+            2.1599999999999997,
+            28,
+            4,
+            date.today() + timedelta(days=28),
+        ),
+        (
+            4,
+            2.3,
+            12,
+            3,
+            date.today(),
+            2.3,
+            28,
+            4,
+            date.today() + timedelta(days=28),
+        ),
+        (5, 2.3, 12, 3, date.today(), 2.4, 28, 4, date.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)),
+        # test case for for repetitions equals to 2
+        (4, 2.5, 1, 1, date.today(), 2.5, 6, 2, date.today() + timedelta(days=6)),
+    ],
+)
+def test_review_given_date(
+    quality,
+    easiness,
+    interval,
+    repetitions,
+    review_date,
+    expected_easiness,
+    expected_interval,
+    expected_repetitions,
+    expected_review_date,
+):
+    sm = SMTwo(easiness, interval, repetitions)
+    reviewed = sm.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
diff --git a/tests/unit/test_sm_two.py b/tests/unit/test_sm_two.py
deleted file mode 100644
index 5379b88..0000000
--- a/tests/unit/test_sm_two.py
+++ /dev/null
@@ -1,218 +0,0 @@
-import json
-from datetime import date, timedelta
-
-import pytest
-
-
-def test_SMTwo_instance(empty_smtwo):
-    checklist = ["calc", "json", "dict"]
-    assert len(vars(empty_smtwo)) == 0
-    for method in checklist:
-        assert method in dir(empty_smtwo)
-
-
-@pytest.mark.parametrize(
-    "quality, easiness, interval, repetitions, expected_easiness, expected_interval, expected_repetitions, expected_review_date",
-    [
-        (0, 2.5, 6, 3, 1.7, 1, 1, date.today() + timedelta(days=1)),
-        (1, 2.5, 1, 1, 1.96, 1, 1, date.today() + timedelta(days=1)),
-        (2, 2.5, 1, 2, 2.1799999999999997, 1, 1, date.today() + timedelta(days=1)),
-        (3, 2.5, 1, 1, 2.36, 1, 2, date.today() + timedelta(days=1)),
-        (4, 2.5, 1, 1, 2.5000000000000004, 1, 2, date.today() + timedelta(days=1)),
-        (5, 2.5, 1, 1, 2.6, 1, 2, date.today() + timedelta(days=1)),
-        (3, 2.8, 1, 2, 2.6599999999999997, 6, 3, date.today() + timedelta(days=6)),
-        (4, 2.8, 1, 2, 2.8000000000000003, 6, 3, date.today() + timedelta(days=6)),
-        (5, 2.8, 1, 2, 2.9, 6, 3, date.today() + timedelta(days=6)),
-        (3, 3.1, 6, 3, 2.9599999999999995, 19,
-         4, date.today() + timedelta(days=19)),
-        (4, 3.1, 6, 3, 3.1, 19, 4, date.today() + timedelta(days=19)),
-        (5, 3.1, 6, 3, 3.2, 19, 4, date.today() + timedelta(days=19)),
-    ]
-)
-def test_SMTwo_calc(empty_smtwo, quality, easiness, interval, repetitions, expected_easiness, expected_interval, expected_repetitions, expected_review_date):
-    empty_smtwo.calc(quality, easiness, interval, repetitions, date.today())
-    assert empty_smtwo.quality == quality
-    assert empty_smtwo.easiness == expected_easiness
-    assert empty_smtwo.interval == expected_interval
-    assert empty_smtwo.repetitions == expected_repetitions
-    assert empty_smtwo.prev.easiness == easiness
-    assert empty_smtwo.prev.interval == interval
-    assert empty_smtwo.prev.repetitions == repetitions
-    assert empty_smtwo.prev.review_date == date.today()
-    assert empty_smtwo.review_date == expected_review_date
-
-
-@pytest.mark.parametrize("quality", ["abc", -1, 6, 3.3])
-def test_SMTwo_calc_invalid_quality(empty_smtwo, quality):
-    if not isinstance(quality, int):
-        with pytest.raises(TypeError) as excinfo:
-            empty_smtwo.calc(quality, 2.5, 1, 1, date.today())
-
-        if isinstance(quality, str):
-            message = f"'_SMTwo__quality' must be <class 'int'> (got '{quality}' that is a {type(quality)})."
-            assert excinfo.value.args[0] == message
-        else:
-            message = f"'_SMTwo__quality' must be <class 'int'> (got {quality} that is a {type(quality)})."
-            assert excinfo.value.args[0] == message
-    else:
-        with pytest.raises(ValueError) as excinfo:
-            empty_smtwo.calc(quality, 2.5, 1, 1, date.today())
-        message = f"'_SMTwo__quality' must be in range(0, 6) (got {quality})"
-        assert excinfo.value.args[0] == message
-
-
-@pytest.mark.parametrize("easiness", ["abc", 0, 1.2])
-def test_SMTwo_calc_invalid_easiness(empty_smtwo, easiness):
-    if not isinstance(easiness, float):
-        with pytest.raises(TypeError) as excinfo:
-            empty_smtwo.calc(3, easiness, 1, 1, date.today())
-
-        if isinstance(easiness, str):
-            message = f"'_Prev__easiness' must be <class 'float'> (got '{easiness}' that is a {type(easiness)})."
-            assert excinfo.value.args[0] == message
-        else:
-            message = f"'_Prev__easiness' must be <class 'float'> (got {easiness} that is a {type(easiness)})."
-            assert excinfo.value.args[0] == message
-    else:
-        with pytest.raises(ValueError) as excinfo:
-            empty_smtwo.calc(3, easiness, 1, 1, date.today())
-        assert excinfo.value.args[0] == "easiness has a minimum value of 1.3"
-
-
-@pytest.mark.parametrize("interval", ["abc", -1, 0, 1.2])
-def test_SMTwo_calc_invalid_interval(empty_smtwo, interval):
-    if not isinstance(interval, int):
-        with pytest.raises(TypeError) as excinfo:
-            empty_smtwo.calc(3, 2.5, interval, 1, date.today())
-
-        if isinstance(interval, str):
-            message = f"'_Prev__interval' must be <class 'int'> (got '{interval}' that is a {type(interval)})."
-            assert excinfo.value.args[0] == message
-        else:
-            message = f"'_Prev__interval' must be <class 'int'> (got {interval} that is a {type(interval)})."
-            assert excinfo.value.args[0] == message
-    else:
-        with pytest.raises(ValueError) as excinfo:
-            empty_smtwo.calc(3, 2.5, interval, 1, date.today())
-        assert excinfo.value.args[0] == "interval has a minimum value of 1"
-
-
-@pytest.mark.parametrize("repetitions", ["abc", -1, 0, 1.2])
-def test_SMTwo_calc_invalid_repetitions(empty_smtwo, repetitions):
-    if not isinstance(repetitions, int):
-        with pytest.raises(TypeError) as excinfo:
-            empty_smtwo.calc(3, 2.5, 1, repetitions, date.today())
-
-        if isinstance(repetitions, str):
-            message = f"'_Prev__repetitions' must be <class 'int'> (got '{repetitions}' that is a {type(repetitions)})."
-            assert excinfo.value.args[0] == message
-        else:
-            message = f"'_Prev__repetitions' must be <class 'int'> (got {repetitions} that is a {type(repetitions)})."
-            assert excinfo.value.args[0] == message
-    else:
-        with pytest.raises(ValueError) as excinfo:
-            empty_smtwo.calc(3, 2.5, 1, repetitions, date.today())
-        message = "repetitions has a minimum value of 1"
-        assert excinfo.value.args[0] == message
-
-
-# Down to line we can allow user to pass a string, and we can convert it to a date
-@pytest.mark.parametrize("review_date", ["abc", 123, 123.0, -123, "2020/12/20", "2020-12-20", "12/20/2020", "12-20-2020", "20-12-2020", "20/12/2020"])
-def test_SMTwo_calc_invalid_last_review(empty_smtwo, review_date):
-    with pytest.raises(TypeError) as excinfo:
-        empty_smtwo.calc(3, 2.5, 1, 1, review_date)
-
-    if isinstance(review_date, str):
-        message = f"'_Prev__review_date' must be <class 'datetime.date'> (got '{review_date}' that is a {type(review_date)})."
-        assert excinfo.value.args[0] == message
-    else:
-        message = f"'_Prev__review_date' must be <class 'datetime.date'> (got {review_date} that is a {type(review_date)})."
-        assert excinfo.value.args[0] == message
-
-
-def test_SMTwo_json(smtwo_after_calc):
-    json_fmt = json.dumps({
-        "quality": 3,
-        "prev_easiness": 2.5,
-        "prev_interval": 1,
-        "prev_repetitions": 1,
-        "prev_review_date": str(date.today()),
-        "easiness": 2.36,
-        "interval": 1,
-        "repetitions": 2,
-        "review_date": str(date.today() + timedelta(days=1))
-    })
-    assert smtwo_after_calc.json() == json_fmt
-
-
-def test_SMTwo_json_prev(smtwo_after_calc):
-    json_fmt = json.dumps({
-        "quality": 3,
-        "prev_easiness": 2.5,
-        "prev_interval": 1,
-        "prev_repetitions": 1,
-        "prev_review_date": str(date.today())
-    })
-    assert smtwo_after_calc.json(prev=True) == json_fmt
-
-
-def test_SMTwo_json_curr(smtwo_after_calc):
-    json_fmt = json.dumps({
-        "quality": 3,
-        "easiness": 2.36,
-        "interval": 1,
-        "repetitions": 2,
-        "review_date": str(date.today() + timedelta(days=1))
-    })
-    assert smtwo_after_calc.json(curr=True) == json_fmt
-
-
-def test_SMTwo_json_exception(empty_smtwo):
-    with pytest.raises(Exception) as excinfo:
-        empty_smtwo.json()
-
-    assert excinfo.value.args[0] == "SMTwo.calc method is required to be called first"
-
-
-def test_SMTwo_dict(smtwo_after_calc):
-    dict_fmt = {
-        "quality": 3,
-        "prev_easiness": 2.5,
-        "prev_interval": 1,
-        "prev_repetitions": 1,
-        "prev_review_date": date.today(),
-        "easiness": 2.36,
-        "interval": 1,
-        "repetitions": 2,
-        "review_date": date.today() + timedelta(days=1)
-    }
-    assert smtwo_after_calc.dict() == dict_fmt
-
-
-def test_SMTwo_dict_prev(smtwo_after_calc):
-    dict_fmt = {
-        "quality": 3,
-        "prev_easiness": 2.5,
-        "prev_interval": 1,
-        "prev_repetitions": 1,
-        "prev_review_date": date.today()
-    }
-    assert smtwo_after_calc.dict(prev=True) == dict_fmt
-
-
-def test_SMTwo_dict_curr(smtwo_after_calc):
-    dict_fmt = {
-        "quality": 3,
-        "easiness": 2.36,
-        "interval": 1,
-        "repetitions": 2,
-        "review_date": date.today() + timedelta(days=1)
-    }
-    assert smtwo_after_calc.dict(curr=True) == dict_fmt
-
-
-def test_SMTwo_dict_exception(empty_smtwo):
-    with pytest.raises(Exception) as excinfo:
-        empty_smtwo.dict()
-
-    assert excinfo.value.args[0] == "SMTwo.calc method is required to be called first"