A simple hackable commandline tool to help managing Leetcode problems
import math

SECS_MIN = lambda MIN: 60.0 * MIN
SECS_HOUR = lambda HOUR: 3600.0 * HOUR
SECS_DAY = lambda DAY: 3600.0 * 24.0 * DAY
INF = 1e16
PASS = "pass"
FAIL = "fail"

AUTO_ID = "##_AUTO_GENERATE_##"

TRACKER_DEFAULT_INTERVAL = SECS_DAY(1)
TRACKER_DEFAULT_OLDEST_EPOCH = SECS_DAY(60)

DEFAULT_NEXT_BOX_PROBABILITY = 1.0 - 0.8

STATE_STUDYING = "studying"
STATE_NEW = "new"
STATE_STUDIED = "studied"
STATES = [STATE_STUDYING, STATE_NEW, STATE_STUDIED]

INVALID_DURATION = -1

DEFAULT_BOX_TRANSFORMATION = lambda at_time, box: box + 1.0 * math.exp(-at_time)
IDENTITY_BOX_TRANSFORMATION = lambda at_time, box: box

LABEL_COMPETENCY = "competency"
LABEL_TAG = "tag"
LABEL_CONTENT = "content"
LABEL_STATE = "state"
LABEL_PASS_PCT = "pass_percentage"
LABEL_SINCE = "since"
LABEL_DURATION = "duration"
EXTRA_INFO = [
    LABEL_CONTENT,
    LABEL_TAG,
    LABEL_STATE,
    LABEL_SINCE,
    LABEL_COMPETENCY,
    LABEL_PASS_PCT,
    LABEL_DURATION,
]

DEFAULT_NUMBER_OF_SUGGESTION = {
    "competency": 1,
    "pass": 1,
    "recent": 1,
    "duration": 1,
}


BY_TO_PROPERTY = {
    "pass": "pass_pct",  # total n_pass
    "competency": "competency",  # time-aware box
    "view": "n_study",  # n_study
    "recent": "since_last_start_study",  # since last start
    "duration": "duration",  # total duration
}

BY_TO_INFO_SHOW = {
    "pass": {
        "show": [
            LABEL_PASS_PCT,
            LABEL_STATE,
            LABEL_CONTENT,
            LABEL_TAG,
            LABEL_SINCE,
        ],
        "emphasis": [LABEL_PASS_PCT],
    },  # total n_pass
    "competency": {
        "show": [
            LABEL_COMPETENCY,
            LABEL_STATE,
            LABEL_CONTENT,
            LABEL_TAG,
            LABEL_SINCE,
        ],
        "emphasis": [LABEL_COMPETENCY],
    },  # time-aware box
    "view": {
        "show": [
            LABEL_PASS_PCT,
            LABEL_STATE,
            LABEL_CONTENT,
            LABEL_TAG,
            LABEL_SINCE,
        ],
        "emphasis": [LABEL_PASS_PCT],
    },  # n_study
    "recent": {
        "show": [
            LABEL_SINCE,
            LABEL_STATE,
            LABEL_CONTENT,
            LABEL_TAG,
            LABEL_PASS_PCT,
        ],
        "emphasis": [LABEL_SINCE],
    },  # since last start
    "duration": {
        "show": [
            LABEL_DURATION,
            LABEL_STATE,
            LABEL_CONTENT,
            LABEL_TAG,
            LABEL_PASS_PCT,
        ],
        "emphasis": [LABEL_DURATION],
    },  # total duration
}


STYLE_HEADER = "\033[95m"
STYLE_OKBLUE = "\033[94m"
STYLE_OKCYAN = "\033[96m"
STYLE_OKGREEN = "\033[92m"
STYLE_WARNING = "\033[93m"
STYLE_FAIL = "\033[91m"
STYLE_BOLD = "\033[1m"
STYLE_UNDERLINE = "\033[4m"
STYLE_ENDC = "\033[0m"

BOLD_TEXT = lambda string: f"{STYLE_BOLD}{string}{STYLE_ENDC}"
UNDERLINE_TEXT = lambda string: f"{STYLE_UNDERLINE}{string}{STYLE_ENDC}"

TAG_STYLE = {
    "E": f"{STYLE_OKGREEN}{STYLE_BOLD}[E]{STYLE_ENDC}",
    "E-": f"{STYLE_OKGREEN}{STYLE_BOLD}[E-]{STYLE_ENDC}",
    "E+": f"{STYLE_OKGREEN}{STYLE_BOLD}[E+]{STYLE_ENDC}",
    "M": f"{STYLE_OKBLUE}{STYLE_BOLD}[M]{STYLE_ENDC}",
    "M+": f"{STYLE_OKBLUE}{STYLE_BOLD}[M+]{STYLE_ENDC}",
    "M-": f"{STYLE_OKBLUE}{STYLE_BOLD}[M-]{STYLE_ENDC}",
    "H": f"{STYLE_FAIL}{STYLE_BOLD}[H]{STYLE_ENDC}",
    "H+": f"{STYLE_FAIL}{STYLE_BOLD}[H+]{STYLE_ENDC}",
    "H-": f"{STYLE_FAIL}{STYLE_BOLD}[H-]{STYLE_ENDC}",
    "vy-todo": f"{STYLE_OKBLUE}vy-todo{STYLE_ENDC}",
    "review": f"{STYLE_FAIL}review{STYLE_ENDC}",
    "refactor": f"{STYLE_OKGREEN}refactor{STYLE_ENDC}",
}

def GET_STYLED_TAG(tag):
    if tag in TAG_STYLE:
        return TAG_STYLE[tag]
    return tag

STATE_STYLE = {
    STATE_NEW: f"{STYLE_WARNING}_new_{STYLE_ENDC}",
    STATE_STUDYING: f"{STYLE_FAIL}_studying_{STYLE_ENDC}",
    STATE_STUDIED: f"{STYLE_FAIL}_studied_{STYLE_ENDC}",
}

def GET_STYLED_STATE(state):
    if state in STATE_STYLE:
        return STATE_STYLE[state]
    return state

constants.py
qphong/repeat
import os
import re
import glob


def get_subject_root_prefix():
    dir_path = os.path.dirname(os.path.realpath(__file__))
    return f"{dir_path}/subjects/sbj-"


def list_all_subjects():
    subject_root_prefix = get_subject_root_prefix()
    subjects = []

    for subject_root in glob.glob(f"{subject_root_prefix}*/"):
        subjects.append(subject_root[len(subject_root_prefix) : -1])
    return subjects


def get_study_file_prefix(subject, identifier, text, maxlen):
    text = text[:maxlen]
    study_dir = get_study_directory(get_subject_root(subject))
    if len(text):
        return f"{study_dir}/{identifier}__{text}__"
    return f"{study_dir}/{identifier}__"


def get_study_file(subject, identifier, postfix, text="", maxlen=60):
    text = re.sub(r"\W", "-", text)
    return f"{get_study_file_prefix(subject, identifier, text, maxlen)}{postfix}"


def list_all_study_files(subject, identifier, text="", maxlen=60):
    return glob.glob(f"{get_study_file_prefix(subject, identifier, text, maxlen)}*")


def get_subject_root(subject):
    return f"{get_subject_root_prefix()}{subject}"


def get_manager_meta_data_prefix(subject_root):
    return f"{subject_root}/"


def get_tracker_directory(subject_root):
    return f"{subject_root}/tracker"


def get_item_directory(subject_root):
    return f"{subject_root}/item"


def get_study_directory(subject_root):
    return f"{subject_root}/study"


def get_study_filename(subject_root, filename):
    study_dir = get_study_directory(subject_root)
    return f"{study_dir}/filename"


def makedirs(directories):
    for directory in directories:
        try:
            os.makedirs(directory)
            print(f"Make {directory}")
        except FileExistsError:
            print(f"ERROR: {directory} exists!")


def makedirs_for_subject(subject):
    subject_root = get_subject_root(subject)
    tracker_dir = get_tracker_directory(subject_root)
    item_dir = get_item_directory(subject_root)
    study_dir = get_study_directory(subject_root)

    makedirs([subject_root, tracker_dir, item_dir, study_dir])

files.py
qphong/repeat
class Item:
    def __init__(
        self, identifier, tags=[], content={}
    ):  # use to refer to its tracker data
        self.identifier = identifier
        self.tags = set(tags)
        self.content = content

    def has_tag(self, tag):
        return tag in self.tags

    def to_dictionary(self):
        return {
            "identifier": self.identifier,
            "tags": list(self.tags),
            "content": self.content,
        }

    @staticmethod
    def load_from_dictionary(data_dict):
        item = Item("dummy_identifier")
        item.identifier = data_dict["identifier"]
        item.tags = set(data_dict["tags"])
        item.content = data_dict["content"]
        return item

item.py
qphong/repeat
from manager import Manager

import constants
import util
import files

import argparse

parser = argparse.ArgumentParser(
    prog="Repeat",
    description="Vy learns a subject with spaced-repetition",
    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)

parser.add_argument(
    "-c",
    "--command",
    choices=[
        "add_subject",  # subject
        "list_subjects",
        "list",  # subject, states, tags, by, direction
        "add",  # subject, identifier, name, tags
        "remove",  # subject, identifier
        "get_file",  # subject, identifier, postifx
        "get_name",  # subject, identifier
        "get_tags_by_identifier",  # subject, identifier
        "get_new_tags_to_identifier",  # subject, identifier
        "list_file",  # list all postfix # subject, identifier
        "review",  # subject, tags, k
        "add_tag",  # subject, identifier, tags
        "remove_tag",  # subject, identifier, tags
        "start",  # subject, identifier
        "end",  # subject, identifier, passfail
        "cancel",  # subject, identifier
        "list_tag",  # subject
        "list_state",  # subject, tags
        "list_state_by_tag",  # subject, tags
    ],
    type=str,
    required=True,
)

parser.add_argument("--subject", type=str, help="subject identifier")

parser.add_argument(
    "-i", "--identifier", type=str, help="item identifier", default=constants.AUTO_ID
)
parser.add_argument("-n", "--name", type=str, help="item name")
parser.add_argument(
    "-r",
    "--passfail",
    choices=[constants.PASS, constants.FAIL],
    type=str,
    help="result",
)

parser.add_argument(
    "-t", "--tags", type=str, help="tags separated by comma", default=None
)  # comma separated

parser.add_argument(
    "-a",
    "--states",
    type=str,
    # choices=constants.STATES,
    help="states separated by comma",
    default=None,
)  # comma separated
parser.add_argument(
    "-b",
    "--by",
    type=str,
    choices=list(constants.BY_TO_PROPERTY.keys()),
    default="view",
)
parser.add_argument(
    "-d", "--direction", type=str, choices=["inc", "dec"], default="inc"
)
parser.add_argument("-p", "--postfix", type=str, default="untitled.txt")

args = parser.parse_args()
config = vars(args)


command = config["command"]

if command == "add_subject":
    # add a new subject, create all necessary directories and files
    subject = util.parse_args(config, ["subject"], [True])
    files.makedirs_for_subject(subject)

elif command == "list_subjects":
    subjects = files.list_all_subjects()
    print(subjects)

elif command == "add":
    # subject, identifier, name, tags
    subject, identifier, name, tags = util.parse_args(
        config, ["subject", "identifier", "name", "tags"], [True, False, True, False]
    )

    manager = Manager(subject)
    manager.load()
    manager.add(identifier, tags, {"name": name})
    manager.save()

elif command == "remove":
    # subject, identifier
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )

    manager = Manager(subject)
    manager.load()
    manager.remove_item_by_identifier(identifier)
    manager.save()

elif command == "list":
    # subject, states, tags, by, direction
    subject, states, tags, by, direction = util.parse_args(
        config,
        ["subject", "states", "tags", "by", "direction"],
        [True, False, False, False, False],
    )
    manager = Manager(subject)
    manager.load()

    identifiers = manager.get_identifiers_by_states_and_tags(tags, states)
    info_list = manager.get_item_by_identifiers(identifiers)

    info_property = constants.BY_TO_PROPERTY[by]
    sorted_info_list = []
    for i, info_dict in enumerate(info_list):
        sorted_info_list.append((info_dict[info_property], i, info_dict))

    sorted_info_list = sorted(sorted_info_list)
    if direction == "dec":
        sorted_info_list = sorted_info_list[::-1]

    for _, _, info_dict in sorted_info_list:
        readable_info = util.get_readable_info(
            info_dict,
            content_fields=["name"],
            extra_info=constants.BY_TO_INFO_SHOW[by]["show"],
            emphasis=constants.BY_TO_INFO_SHOW[by]["emphasis"],
        )
        print(readable_info)

elif command == "get_file":
    # subject, identifier, postifx
    subject, identifier, postfix = util.parse_args(
        config, ["subject", "identifier", "postfix"], [True, True, True]
    )
    manager = Manager(subject)
    manager.load()
    content = manager.get_content_by_identifier(identifier)
    if not content:
        print("##_not_exist_##")
        exit(1)

    name = "unnamed"
    if "name" in content:
        name = content["name"]

    print(files.get_study_file(subject, identifier, postfix, name))

elif command == "get_name":
    # subject, identifier
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True, True]
    )
    manager = Manager(subject)
    manager.load()
    content = manager.get_content_by_identifier(identifier)
    if not content:
        print("##_not_exist_##")
        exit(1)

    name = "unnamed"
    if "name" in content:
        name = content["name"]
    print(name)

elif command == "get_tags_by_identifier":
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )
    manager = Manager(subject)
    manager.load()
    tags = manager.get_tags_by_identifier(identifier)
    if not tags:
        print(f"Cannot find any tag from {identifier}")
        exit(1)

    for tag in sorted(tags):
        print(tag)

elif command == "get_new_tags_to_identifier":  # subject, identifier
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )
    manager = Manager(subject)
    manager.load()
    tags = manager.get_tags_by_identifier(identifier)
    new_tags = list(manager.tags.difference(tags))

    if not new_tags:
        print(f"Cannot find any tag from {identifier}")
        exit(1)

    for tag in sorted(new_tags):
        print(tag)

elif command == "list_file":
    # list all postfix # subject, identifier
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )
    for file in files.list_all_study_files(subject, identifier):
        print(file)

elif command == "review":
    # subject, tags, k
    subject, tags, by = util.parse_args(
        config, ["subject", "tags", "by"], [True, False, True]
    )
    manager = Manager(subject)
    manager.load()
    identifiers = manager.suggest(constants.DEFAULT_NUMBER_OF_SUGGESTION[by], tags, by=by)

    info_list = manager.get_item_by_identifiers(identifiers)

    for info_dict in info_list:
        readable_info = util.get_readable_info(
            info_dict,
            content_fields=["name"],
            extra_info=[
                constants.LABEL_CONTENT,
                constants.LABEL_TAG,
                constants.LABEL_STATE,
                constants.LABEL_COMPETENCY,
                constants.LABEL_PASS_PCT,
            ],
        )
        print(readable_info)

elif command == "add_tag":
    # subject, identifier, tags
    subject, identifier, tags = util.parse_args(
        config, ["subject", "identifier", "tags"], [True, True, True]
    )
    if len(tags) == 0:
        print("No tag provided!")
        exit(1)

    manager = Manager(subject)
    manager.load()
    manager.add_tags_to_identifier(identifier, tags)
    manager.save()

elif command == "remove_tag":
    # subject, identifier, tags
    subject, identifier, tags = util.parse_args(
        config, ["subject", "identifier", "tags"], [True, True, True]
    )
    if len(tags) == 0:
        print("No tag provided!")
        exit(1)

    manager = Manager(subject)
    manager.load()
    manager.remove_tags_from_identifier(identifier, tags)
    manager.save()

elif command == "start":
    # subject, identifier
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )
    manager = Manager(subject)
    manager.load()
    manager.start_study_item(identifier)
    manager.save()

elif command == "end":
    # subject, identifier, passfail
    subject, identifier, passfail = util.parse_args(
        config, ["subject", "identifier", "passfail"], [True, True, True]
    )
    manager = Manager(subject)
    manager.load()
    manager.end_study_item(identifier, passfail)
    manager.save()

elif command == "cancel":
    subject, identifier = util.parse_args(
        config, ["subject", "identifier"], [True, True]
    )
    manager = Manager(subject)
    manager.load()
    manager.cancel_study_item(identifier)
    manager.save()

elif command == "list_tag":
    # subject
    subject, states = util.parse_args(config, ["subject", "states"], [True, False])
    manager = Manager(subject)
    manager.load()
    tag_count = manager.list_tags(states)
    for tag, count in tag_count:
        print(f"{tag:20s}: {count:>4d}")

elif command == "list_state":
    # subject, tags
    subject, tags = util.parse_args(config, ["subject", "tags"], [True, False])
    manager = Manager(subject)
    manager.load()
    state_count = manager.list_states(tags)
    for state, count in state_count:
        print(f"{state}: {count}")

elif command == "list_state_by_tag":
    # subject, states
    subject, states = util.parse_args(config, ["subject", "states"], [True])
    manager = Manager(subject)
    manager.load()

    tag_count = manager.list_tags(states)

    for tag, count in tag_count:

        print(f"{tag:20s} : {count:>4d}", end="")

        state_count = manager.list_states([tag], states)
        display_state = False
        for state, scount in state_count:
            if state != constants.STATE_STUDIED:
                styled_state = constants.GET_STYLED_STATE(state)

                if not display_state:
                    print(":", end="")
                print(f"{styled_state}({scount})", end=" ")
                display_state = True
        print("")

else:
    raise Exception(f"Unknown command: {command}")

main.py
qphong/repeat
import sys
import os
from collections import defaultdict
import json
import random
import bisect
import math

import files
import constants
import util

from tracker import Tracker
from item import Item


class Manager:
    def __init__(self, subject):
        self.item_tracker_data = {}  # identifier -> {"item": item, "tracker": tracker}
        self.subject = subject
        self.subject_root = files.get_subject_root(subject)
        self.tags = set()  # set of tags
        self.identifier_by_tag = defaultdict(set)  # tag -> set of item identifiers

    def save(self):
        item_dict = {}
        tracker_dict = {}

        for identifier, item_tracker in self.item_tracker_data.items():
            item_dict[identifier] = item_tracker["item"].to_dictionary()
            tracker_dict[identifier] = item_tracker["tracker"].to_dictionary()

        with open(
            f"{files.get_item_directory(self.subject_root)}/item.json",
            "w",
            encoding="utf-8",
        ) as file:
            json.dump(item_dict, file, indent=2)

        with open(
            f"{files.get_tracker_directory(self.subject_root)}/tracker.json",
            "w",
            encoding="utf-8",
        ) as file:
            json.dump(tracker_dict, file, indent=2)

    def load(self):
        item_path = f"{files.get_item_directory(self.subject_root)}/item.json"
        tracker_path = f"{files.get_tracker_directory(self.subject_root)}/tracker.json"

        item_dict = {}
        tracker_dict = {}

        if os.path.exists(item_path):
            with open(item_path, "r", encoding="utf-8") as file:
                item_dict = json.load(file)
        if os.path.exists(tracker_path):
            with open(tracker_path, "r", encoding="utf-8") as file:
                tracker_dict = json.load(file)

        for identifier in item_dict:
            item = Item.load_from_dictionary(item_dict[identifier])
            tracker = Tracker.load_from_dictionary(tracker_dict[identifier])

            self.item_tracker_data[identifier] = {
                "item": item,
                "tracker": tracker,
            }

            self.add_tags(identifier, item.tags)

    def get_n_item(self):
        return len(self.item_tracker_data)

    def remove_item_by_identifier(self, identifier):
        self.remove_tags(identifier, self.item_tracker_data[identifier]["item"].tags)
        del self.item_tracker_data[identifier]

    def remove_tags_from_identifier(self, identifier, tags):
        if identifier not in self.item_tracker_data:
            print(f"{identifier} does not exist!")
            return 0

        item = self.item_tracker_data[identifier]["item"]
        for tag in tags:
            if tag in item.tags:
                item.tags.remove(tag)
            else:
                print(f"{tag} does not exist in {identifier}!")
                continue

            self.identifier_by_tag[tag].remove(identifier)

            if len(self.identifier_by_tag[tag]) == 0:
                del self.identifier_by_tag[tag]
                self.tags.remove(tag)

    def add_tags_to_identifier(self, identifier, tags):
        if identifier not in self.item_tracker_data:
            print(f"{identifier} does not exists!")
            return 0

        item = self.item_tracker_data[identifier]["item"]
        for tag in tags:
            if tag not in item.tags:
                item.tags.add(tag)
            else:
                print(f"{tag} exists in {identifier}!")
                continue

            self.tags.add(tag)
            self.identifier_by_tag[tag].add(identifier)

    def remove_tags(self, identifier, tags):
        # do not modify tags of item
        for tag in tags:
            self.identifier_by_tag[tag].remove(identifier)
            if len(self.identifier_by_tag[tag]) == 0:
                del self.identifier_by_tag[tag]
                self.tags.remove(tag)

    def add_tags(self, identifier, tags):
        # do not modify tags of item
        for tag in tags:
            self.tags.add(tag)
            self.identifier_by_tag[tag].add(identifier)

    def list_tags(self, states=[]):
        if len(states) == 0:
            return sorted(
                [(tag, len(ids)) for tag, ids in self.identifier_by_tag.items()]
            )

        identifiers = self.get_identifiers_by_states(states)
        tag_count = defaultdict(int)

        for identifier in identifiers:
            item_tracker = self.item_tracker_data[identifier]
            item = item_tracker["item"]
            tracker = item_tracker["tracker"]

            if tracker.get_state() in states:
                for tag in item.tags:
                    tag_count[tag] += 1
        return sorted(list(tag_count.items()))

    def list_states(self, tags=[], states=[]):
        identifiers = self.get_identifiers_by_states_and_tags(tags, states)
        state_count = defaultdict(int)

        for identifier in identifiers:
            item_tracker = self.item_tracker_data[identifier]
            tracker = item_tracker["tracker"]
            state = tracker.get_state()
            state_count[state] += 1

        return sorted(list(state_count.items()))

    def add(self, identifier, tags, content):
        if identifier == constants.AUTO_ID:
            # automatically generate a unique item identifier
            identifier = util.get_random_string()
            while identifier in self.item_tracker_data:
                identifier = util.get_random_string()
            print(f"Randomly generate identifier: {identifier}.")

        elif identifier in self.item_tracker_data:
            print(f"{identifier} already exists!")
            return 1

        item = Item(identifier, tags, content)
        tracker = Tracker(
            identifier,
            interval=constants.TRACKER_DEFAULT_INTERVAL,
            oldest_epoch=constants.TRACKER_DEFAULT_OLDEST_EPOCH,
        )

        self.item_tracker_data[identifier] = {"item": item, "tracker": tracker}
        self.add_tags(identifier, tags)

    def get_content_by_identifier(self, identifier):
        if identifier in self.item_tracker_data:
            item = self.item_tracker_data[identifier]["item"]
            return item.content
        return None

    def get_tags_by_identifier(self, identifier):
        if identifier in self.item_tracker_data:
            item = self.item_tracker_data[identifier]["item"]
            if len(item.tags):
                return item.tags
        return None

    def get_identifiers_by_states(self, states=[], within_identifiers=set()):
        # len(states) == 0 means all all states
        # len(within_identifiers) == 0 means considering all identifiers
        if len(states) == 0:
            if len(within_identifiers) == 0:
                return list(self.item_tracker_data.keys())

            return list(
                set(self.item_tracker_data.keys()).intersection(within_identifiers)
            )

        identifiers = set()
        for identifier, item_tracker in self.item_tracker_data.items():
            if len(within_identifiers) and identifier not in within_identifiers:
                continue

            tracker = item_tracker["tracker"]
            state = tracker.get_state()
            if state in states:
                identifiers.add(identifier)

        return list(identifiers)

    def get_identifiers_by_tags(self, tags=[], within_identifiers=set()):
        # len(tags) == 0 means all tags
        # len(within_identifiers) == 0 means considering all identifiers
        if len(tags) == 0:
            if len(within_identifiers) == 0:
                return list(self.item_tracker_data.keys())

            return list(
                set(self.item_tracker_data.keys()).intersection(within_identifiers)
            )

        identifiers = set()
        for tag in tags:
            identifiers = identifiers.union(self.identifier_by_tag[tag])

        if len(within_identifiers) > 0:
            return list(set(within_identifiers).intersection(identifiers))

        return list(identifiers)

    def get_identifiers_by_states_and_tags(
        self, tags=[], states=[], within_identifiers=set()
    ):
        identifiers = self.get_identifiers_by_states(states, within_identifiers)
        if len(identifiers) == 0:
            return set()

        return self.get_identifiers_by_tags(tags, identifiers)

    def get_item_by_identifiers(self, identifiers):
        item_list = []
        for identifier in identifiers:
            item_tracker = self.item_tracker_data[identifier]

            item = item_tracker["item"]
            tracker = item_tracker["tracker"]

            item_list.append(util.get_info(identifier, item, tracker))

        return item_list

    def start_study_item(self, identifier):
        self.item_tracker_data[identifier]["tracker"].start_study()

    def end_study_item(self, identifier, passfail):
        self.item_tracker_data[identifier]["tracker"].end_study(passfail)

    def cancel_study_item(self, identifier):
        self.item_tracker_data[identifier]["tracker"].cancel_study()

    def suggest(
        self,
        k=1,
        tags=[],
        by="competency",
        next_box_prob=constants.DEFAULT_NEXT_BOX_PROBABILITY,
    ):
        within_identifiers = self.get_identifiers_by_states_and_tags(tags)

        if by == "competency":
            assessing_boxes = self.group_identifiers_by_assessing_box(
                within_identifiers
            )
            if len(assessing_boxes) == 0:
                return []
            return Manager.get_randomized_box(k, assessing_boxes, next_box_prob)

        elif by == "pass":
            unnormalized_probs = [0.0] * len(within_identifiers)

            for i, identifier in enumerate(within_identifiers):
                tracker = self.item_tracker_data[identifier]["tracker"]
                n_pass = tracker.get_overall_n_pass()
                n_study = tracker.get_overall_n_study()

                pass_pct = float(n_pass) / float(n_study) if n_study > 0 else 0.0
                unnormalized_probs[i] = math.exp(-2.5 * pass_pct)

            return Manager.get_randomized_identifier(
                k, within_identifiers, unnormalized_probs
            )

        elif by == "recent":
            unnormalized_probs = [0.0] * len(within_identifiers)

            for i, identifier in enumerate(within_identifiers):
                tracker = self.item_tracker_data[identifier]["tracker"]
                state = tracker.get_state()

                if state == constants.STATE_NEW or state == constants.STATE_STUDYING:
                    unnormalized_probs[i] = 0.0
                else:
                    unnormalized_probs[i] = tracker.get_duration_since_last_end_study()

            return Manager.get_randomized_identifier(
                k, within_identifiers, unnormalized_probs
            )

        elif by == "duration":
            unnormalized_probs = [0.0] * len(within_identifiers)

            for i, identifier in enumerate(within_identifiers):
                tracker = self.item_tracker_data[identifier]["tracker"]
                unnormalized_probs[i] = tracker.get_overall_duration()

            return Manager.get_randomized_identifier(
                k, within_identifiers, unnormalized_probs
            )
        else:
            raise Exception(f"Manager:suggest: Haven't implemented for {by}!")

    def group_identifiers_by_assessing_box(self, identifiers):
        assessing_boxes = defaultdict(list)  # box -> list of identifiers
        for identifier in identifiers:
            tracker = self.item_tracker_data[identifier]["tracker"]

            if tracker.is_studied():
                box = tracker.get_assessing_box(
                    transformation=constants.DEFAULT_BOX_TRANSFORMATION
                )
                assessing_boxes[box].append(identifier)
        return assessing_boxes

    @staticmethod
    def get_randomized_identifier(k, identifiers, unnormalized_probs):
        suggested_identifiers = []

        while len(suggested_identifiers) < k and len(identifiers):
            normalizer = sum(unnormalized_probs)

            cprobs = [0.0] * len(unnormalized_probs)
            cprobs[0] = unnormalized_probs[0] / float(normalizer)
            for i in range(1, len(cprobs)):
                cprobs[i] = cprobs[i - 1] + unnormalized_probs[i] / float(normalizer)

            t = min(random.random(), 1.0 - 1e-9)
            idx = bisect.bisect_left(cprobs, t)

            identifier = identifiers[idx]
            suggested_identifiers.append(identifier)
            identifiers.remove(identifier)
            del unnormalized_probs[idx]

        return suggested_identifiers

    @staticmethod
    def get_randomized_box(k, assessing_boxes, next_box_prob):
        suggested_identifiers = []
        sorted_boxes = sorted(list(assessing_boxes.keys()))

        while len(suggested_identifiers) < k and len(sorted_boxes):
            for i, box in enumerate(sorted_boxes):
                t = random.random()
                if t <= next_box_prob:
                    continue

                idx = random.randint(0, len(assessing_boxes[box]) - 1)
                identifier = assessing_boxes[box][idx]
                suggested_identifiers.append(identifier)

                assessing_boxes[box].remove(identifier)
                if len(assessing_boxes[box]) == 0:
                    sorted_boxes.remove(box)
                break

        return suggested_identifiers

manager.py
qphong/repeat
# https://leetcode.com/problems/best-time-to-buy-and-sell-stock/?envType=study-plan-v2&envId=top-interview-150
# 121. Best Time to Buy and Sell Stock
# Easy
# 28.6K
# 973
# Companies
#
# You are given an array prices where prices[i] is the price of a given stock on the ith day.
#
# You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.
#
# Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.


class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        min_prev = 1e9
        max_profit = 0

        for p in prices:
            max_profit = max(max_profit, p - min_prev)
            min_prev = min(min_prev, p)
        return max_profit


subjects/sbj-leetcode/study/121__best-time-to-buy-and-sell-stock__code.py
qphong/repeat
# https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/?envType=study-plan-v2&envId=top-interview-150
# 122. Best Time to Buy and Sell Stock II
# Medium
# 12.6K
# 2.6K
# Companies
#
# You are given an integer array prices where prices[i] is the price of a given stock on the ith day.
#
# On each day, you may decide to buy and/or sell the stock. You can only hold at most one share of the stock at any time. However, you can buy it then immediately sell it on the same day.
#
# Find and return the maximum profit you can achieve.

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        profit = 0
        min_prev = 1e9
        for p in prices:
            min_prev = min(min_prev, p)

            if p > min_prev:
                profit += p - min_prev
                min_prev = p
        return profit




subjects/sbj-leetcode/study/122__best-time-to-buy-and-sell-stock-ii__code.py
qphong/repeat
# https://leetcode.com/problems/integer-to-roman/?envType=study-plan-v2&envId=top-interview-150
# 12. Integer to Roman
# Medium
# 6.4K
# 5.3K
# Companies
#
# Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.
#
# Symbol       Value
# I             1
# V             5
# X             10
# L             50
# C             100
# D             500
# M             1000
#
# For example, 2 is written as II in Roman numeral, just two one's added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.
#
# Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:
#
#     I can be placed before V (5) and X (10) to make 4 and 9.
#     X can be placed before L (50) and C (100) to make 40 and 90.
#     C can be placed before D (500) and M (1000) to make 400 and 900.
#
# Given an integer, convert it to a roman numeral.


class Solution(object):
    def intToRoman(self, num):
        """
        :type num: int
        :rtype: str
        """
        mapping = {
            1: "I",
            4: "IV",
            5: "V",
            9: "IX",
            10: "X",
            40: "XL",
            50: "L",
            90: "XC",
            100: "C",
            400: "CD",
            500: "D",
            900: "CM",
            1000: "M",
        }

        digits = sorted(list(mapping.keys()))[::-1] # largest first

        i = 0
        roman = ""
        while num > 0:
            remainder = num - digits[i]
            if remainder < 0:
                i += 1
                continue
            roman += mapping[digits[i]]
            num = remainder

        return roman


subjects/sbj-leetcode/study/12__integer-to-roman__code.py
qphong/repeat
# https://leetcode.com/problems/gas-station/?envType=study-plan-v2&envId=top-interview-150
# 134. Gas Station
# Medium
# 11K
# 960
# Companies
#
# There are n gas stations along a circular route, where the amount of gas at the ith station is gas[i].
#
# You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from the ith station to its next (i + 1)th station. You begin the journey with an empty tank at one of the gas stations.
#
# Given two integer arrays gas and cost, return the starting gas station's index if you can travel around the circuit once in the clockwise direction, otherwise return -1. If there exists a solution, it is guaranteed to be unique

class Solution(object):
    def canCompleteCircuit(self, gas, cost):
        """
        :type gas: List[int]
        :type cost: List[int]
        :rtype: int
        """
        start = 0
        remain_gas = gas[start]

        cur = start
        nxt = (cur+1) % len(gas)

        while nxt != start:

            remain_gas -= cost[cur]

            if remain_gas < 0:
                if nxt <= start:
                    return -1

                start = nxt
                remain_gas = gas[start]
                cur = start
                nxt = (cur+1) % len(gas)

            else:
                cur = nxt
                remain_gas += gas[cur]
                nxt = (cur + 1) % len(gas)

        if remain_gas - cost[cur] < 0:
            return -1

        return start

if __name__ == "__main__":
    gas = [2,3,4]
    cost = [3,4,3]

    ans = Solution().canCompleteCircuit(gas, cost)
    print(ans)

subjects/sbj-leetcode/study/134__gas-station__code.py
qphong/repeat
# https://leetcode.com/problems/candy/?envType=study-plan-v2&envId=top-interview-150
# 135. Candy
# Hard
# 7.2K
# 537
# Companies
#
# There are n children standing in a line. Each child is assigned a rating value given in the integer array ratings.
#
# You are giving candies to these children subjected to the following requirements:
#
#     Each child must have at least one candy.
#     Children with a higher rating get more candies than their neighbors.
#
# Return the minimum number of candies you need to have to distribute the candies to the children.

class Solution(object):
    def candy(self, ratings):
        """
        :type ratings: List[int]
        :rtype: int
        """
        candies = [1] * len(ratings)
        for i,r in enumerate(ratings[1:]):
            if r > ratings[i]:
                candies[i+1] = candies[i] + 1

        for i in range(len(ratings) - 2, -1, -1):
            if ratings[i] > ratings[i+1]:
                candies[i] = max(candies[i], candies[i+1] + 1)

        return sum(candies)


subjects/sbj-leetcode/study/135__candy__code.py
qphong/repeat
# https://leetcode.com/problems/reverse-words-in-a-string/?envType=study-plan-v2&envId=top-interview-150
# 151. Reverse Words in a String
# Medium
# 7.4K
# 4.9K
# Companies
#
# Given an input string s, reverse the order of the words.
#
# A word is defined as a sequence of non-space characters. The words in s will be separated by at least one space.
#
# Return a string of the words in reverse order concatenated by a single space.
#
# Note that s may contain leading or trailing spaces or multiple spaces between two words. The returned string should only have a single space separating the words. Do not include any extra spaces.

class Solution(object):
    def reverseWords(self, s):
        """
        :type s: str
        :rtype: str
        """
        words = s.split()
        return ' '.join(words[::-1])

subjects/sbj-leetcode/study/151__reverse-words-in-a-string__code.py
qphong/repeat
# https://leetcode.com/problems/majority-element/?envType=study-plan-v2&envId=top-interview-150
# 169. Majority Element
# Easy
# 17.2K
# 505
# Companies
#
# Given an array nums of size n, return the majority element.
#
# The majority element is the element that appears more than ⌊n / 2⌋ times. You may assume that the majority element always exists in the array.

from collections import defaultdict

class Solution(object):
    def majorityElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        count = defaultdict(int)
        for n in nums:
            count[n] += 1
            if count[n] > len(nums) / 2:
                return n


subjects/sbj-leetcode/study/169__majority-element__code.py
qphong/repeat
# https://leetcode.com/problems/rotate-array/?envType=study-plan-v2&envId=top-interview-150
# 189. Rotate Array
# Medium
# 16.4K
# 1.8K
# Companies
#
# Given an integer array nums, rotate the array to the right by k steps, where k is non-negative.

class Solution(object):
    def rotate(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: None Do not return anything, modify nums in-place instead.
        """
        if k == 0:
            return

        i = len(nums) - k
        rotated_nums = [0] * len(nums)

        for j in range(len(nums)):
            idx = i % len(nums)
            rotated_nums[j] = nums[idx]
            i += 1

        for i in range(len(nums)):
            nums[i] = rotated_nums[i]


subjects/sbj-leetcode/study/189__rotate-array__code.py
qphong/repeat
# https://leetcode.com/problems/product-of-array-except-self/?envType=study-plan-v2&envId=top-interview-150
# 238. Product of Array Except Self
# Medium
# 20.2K
# 1.2K
# Companies
#
# Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].
#
# The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
#
# You must write an algorithm that runs in O(n) time and without using the division operation.

class Solution(object):
    def productExceptSelf(self, nums):
        """
        :type nums: List[int]
        :rtype: List[int]
        """
        left = [1] * len(nums)
        right = [1] * len(nums)

        for i in range(len(nums)-1):
            left[i+1] = left[i] * nums[i]

            right_i = len(nums)-2-i
            right[right_i] = right[right_i + 1] * nums[right_i + 1]

        ans = right
        for i in range(len(nums)):
            ans[i] *= left[i]

        return ans

subjects/sbj-leetcode/study/238__product-of-array-except-self__code.py
qphong/repeat
# https://leetcode.com/problems/remove-duplicates-from-sorted-array/?envType=study-plan-v2&envId=top-interview-150
# 26. Remove Duplicates from Sorted Array
# Easy
# 12.8K
# 16.9K
# Companies
#
# Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place such that each unique element appears only once. The relative order of the elements should be kept the same. Then return the number of unique elements in nums.
#
# Consider the number of unique elements of nums to be k, to get accepted, you need to do the following things:
#
#     Change the array nums such that the first k elements of nums contain the unique elements in the order they were present in nums initially. The remaining elements of nums are not important as well as the size of nums.
#     Return k.

class Solution(object):
    def removeDuplicates(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        prev = 0
        for i,n in enumerate(nums):
            if n != nums[prev] and i != prev:
                prev += 1
                nums[prev], nums[i] = n, nums[prev]

        return prev+1


subjects/sbj-leetcode/study/26__remove-duplicates-from-sorted-array__code.py
qphong/repeat
# https://leetcode.com/problems/h-index/?envType=study-plan-v2&envId=top-interview-150

# 274. H-Index
# Medium
# 764
# 266
# Companies
#
# Given an array of integers citations where citations[i] is the number of citations a researcher received for their ith paper, return the researcher's h-index.
#
# According to the definition of h-index on Wikipedia: The h-index is defined as the maximum value of h such that the given researcher has published at least h papers that have each been cited at least h times.
#
class Solution(object):
    def hIndex(self, citations):
        """
        :type citations: List[int]
        :rtype: int
        """
        dec_citations = sorted(citations)[::-1] + [0]

        for i,cite in enumerate(dec_citations):
            if cite < i + 1:
                return i

        return 0


subjects/sbj-leetcode/study/274__h-index__code.py
qphong/repeat
# https://leetcode.com/problems/remove-element/?envType=study-plan-v2&envId=top-interview-150

# 27. Remove Element
# Easy
# 1.3K
# 1.8K
# Companies
#
# Given an integer array nums and an integer val, remove all occurrences of val in nums in-place. The order of the elements may be changed. Then return the number of elements in nums which are not equal to val.
#
# Consider the number of elements in nums which are not equal to val be k, to get accepted, you need to do the following things:
#
#     Change the array nums such that the first k elements of nums contain the elements which are not equal to val. The remaining elements of nums are not important as well as the size of nums.
#     Return k.

class Solution(object):
    def removeElement(self, nums, val):
        """
        :type nums: List[int]
        :type val: int
        :rtype: int
        """
        k = len(nums) - 1
        i = 0

        while i <= k:
            if nums[i] == val:
                nums[i], nums[k] = nums[k], nums[i]
                k -= 1
            else:
                i += 1

        return k+1


subjects/sbj-leetcode/study/27__remove-element__code.py
qphong/repeat
# https://leetcode.com/problems/trapping-rain-water/?envType=study-plan-v2&envId=top-interview-150
# 42. Trapping Rain Water
# Hard
# 29.4K
# 420
# Companies
#
# Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.
class Solution(object):
    def trap(self, height):
        """
        :type height: List[int]
        :rtype: int
        """
        height = [0] + height + [0]

        left_i = 0
        left = height[left_i]

        water = 0
        middle_height = 0

        for i in range(1,len(height)):
            if height[i] >= left:
                water += (i - left_i - 1) * min(height[i], left) - middle_height
                left_i = i
                left = height[left_i]
                middle_height = 0
            else:
                middle_height += height[i]

        right_i = len(height) - 1
        right = height[right_i]
        middle_height = 0
        for i in range(len(height)-2, -1, -1):
            if height[i] > right:
                water += (right_i - i - 1) * min(height[i], right) - middle_height
                right_i = i
                right = height[right_i]
                middle_height = 0
            else:
                middle_height += height[i]

        return water

subjects/sbj-leetcode/study/42__trapping-rain-water__code.py
qphong/repeat
# https://leetcode.com/problems/jump-game-ii/?envType=study-plan-v2&envId=top-interview-150
# 45. Jump Game II
# Medium
# 13.5K
# 480
# Companies
#
# You are given a 0-indexed array of integers nums of length n. You are initially positioned at nums[0].
#
# Each element nums[i] represents the maximum length of a forward jump from index i. In other words, if you are at nums[i], you can jump to any nums[i + j] where:
#
#     0 <= j <= nums[i] and
#     i + j < n
#
# Return the minimum number of jumps to reach nums[n - 1]. The test cases are generated such that you can reach nums[n - 1].

class Solution(object):
    def jump(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if len(nums) <= 1:
            return 0

        min_jump = [1e9] * len(nums)
        min_jump[0] = 0

        for i,n in enumerate(nums):
            for j in range(i+1,min(len(nums),i+n+1)):
                min_jump[j] = min(min_jump[j], min_jump[i]+1)

        return min_jump[-1]

    def jump_linear_time(self, nums):
        if len(nums) < 2:
            return 0

        max_reach = [0] * len(nums)
        max_reach[0] = nums[0]

        for i,n in enumerate(nums[1:]):
            max_reach[i+1] = max(max_reach[i], i + 1 + n)

        n_jump = 1
        reach = max_reach[0]
        while reach < len(nums) - 1:
            reach = max_reach[reach]
            n_jump += 1

        return n_jump


subjects/sbj-leetcode/study/45__jump-game-ii__code.py
qphong/repeat
# https://leetcode.com/problems/jump-game/?envType=study-plan-v2&envId=top-interview-150
# 55. Jump Game
# Medium
# 17.9K
# 1K
# Companies
#
# You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.
#
# Return true if you can reach the last index, or false otherwise.

class Solution(object):
    def canJump(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        max_reach = 0
        for i,n in enumerate(nums):
            if i > max_reach:
                return False
            max_reach = max(i + n, max_reach)
        return max_reach >= len(nums) - 1

subjects/sbj-leetcode/study/55__jump-game__code.py
qphong/repeat
# https://leetcode.com/problems/text-justification/?envType=study-plan-v2&envId=top-interview-150
# 68. Text Justification
# Hard
# 3.3K
# 4.3K
# Companies
#
# Given an array of strings words and a width maxWidth, format the text such that each line has exactly maxWidth characters and is fully (left and right) justified.
#
# You should pack your words in a greedy approach; that is, pack as many words as you can in each line. Pad extra spaces ' ' when necessary so that each line has exactly maxWidth characters.
#
# Extra spaces between words should be distributed as evenly as possible. If the number of spaces on a line does not divide evenly between words, the empty slots on the left will be assigned more spaces than the slots on the right.
#
# For the last line of text, it should be left-justified, and no extra space is inserted between words.
#
# Note:
#
#     A word is defined as a character sequence consisting of non-space characters only.
#     Each word's length is guaranteed to be greater than 0 and not exceed maxWidth.
#     The input array words contains at least one word.
#
class Solution(object):
    def fullJustify(self, words, maxWidth):
        """
        :type words: List[str]
        :type maxWidth: int
        :rtype: List[str]
        """
        curlen = 0
        prev = 0
        justified = []

        for i, word in enumerate(words):
            curlen += len(word)
            n_spaces = i - prev

            if curlen + n_spaces > maxWidth:
                # justify this line of words[prev:i]
                n_gap = i - prev - 1
                ttl_spaces = maxWidth - curlen + len(word)

                if n_gap > 0:
                    equal_spaces = ttl_spaces // n_gap
                    remain_spaces = ttl_spaces % n_gap

                    spaces = [equal_spaces + 1] * remain_spaces + [equal_spaces] * (
                        n_gap - remain_spaces
                    )

                    line = [words[prev]]
                    for j, w in enumerate(words[prev + 1 : i]):
                        line.extend([" " * spaces[j], w])

                    justified.append("".join(line))

                else:  # n_gap == 0
                    justified.append(words[prev] + " " * ttl_spaces)

                curlen = len(word)
                prev = i

        line = " ".join(words[prev:])
        justified.append(line + " " * (maxWidth - len(line)))

        return justified


if __name__ == "__main__":
    words = ["This", "is", "an", "example", "of", "text", "justification."]
    maxWidth = 16
    ans = Solution().fullJustify(words, maxWidth)
    print(ans)

subjects/sbj-leetcode/study/68__text-justification__code.py
qphong/repeat
# https://leetcode.com/problems/zigzag-conversion/?envType=study-plan-v2&envId=top-interview-150

# 6. Zigzag Conversion
# Medium
# 6.8K
# 13.4K
# Companies
#
# The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)
#
# P   A   H   N
# A P L S I I G
# Y   I   R
#
# And then read line by line: "PAHNAPLSIIGYIR"
#
# Write the code that will take a string and make this conversion given a number of rows:
#
# string convert(string s, int numRows);

class Solution(object):
    def convert(self, s, numRows):
        """
        :type s: str
        :type numRows: int
        :rtype: str
        """
        if numRows == 1:
            return s

        rows = ["" for _ in range(numRows)]
        direction = 1
        i = 0

        for c in s:
            rows[i] += c
            if i == 0 and direction == -1:
                direction = 1
            elif i == numRows - 1 and direction == 1:
                direction = -1

            i += direction

        return ''.join(rows)



subjects/sbj-leetcode/study/6__zigzag-conversion__code.py
qphong/repeat
# https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/?envType=study-plan-v2&envId=top-interview-150
# 80. Remove Duplicates from Sorted Array II
# Medium
# 6K
# 1.1K
# Companies
#
# Given an integer array nums sorted in non-decreasing order, remove some duplicates in-place such that each unique element appears at most twice. The relative order of the elements should be kept the same.
#
# Since it is impossible to change the length of the array in some languages, you must instead have the result be placed in the first part of the array nums. More formally, if there are k elements after removing the duplicates, then the first k elements of nums should hold the final result. It does not matter what you leave beyond the first k elements.
#
# Return k after placing the final result in the first k slots of nums.
#
# Do not allocate extra space for another array. You must do this by modifying the input array in-place with O(1) extra memory.
#
class Solution(object):
    def removeDuplicates(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        prev = 0
        count = 1

        for i,n in enumerate(nums):
            if i == prev:
                continue

            if n == nums[prev]:
                if count:
                    count -= 1
                    prev += 1
                    nums[prev], nums[i] = nums[i], nums[prev]
            else:
                count = 1
                prev += 1
                nums[prev], nums[i] = nums[i], nums[prev]

        return prev + 1

subjects/sbj-leetcode/study/80__remove-duplicates-from-sorted-array-ii__code.py
qphong/repeat
# https://leetcode.com/problems/merge-sorted-array/?envType=study-plan-v2&envId=top-interview-150

# 88. Merge Sorted Array
# Easy
# 12.8K
# 1.4K
# Companies
#
# You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.
#
# Merge nums1 and nums2 into a single array sorted in non-decreasing order.
#
# The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.

class Solution(object):
    def merge(self, nums1, m, nums2, n):
        """
        :type nums1: List[int]
        :type m: int
        :type nums2: List[int]
        :type n: int
        :rtype: None Do not return anything, modify nums1 in-place instead.
        """
        i1 = 0
        i2 = 0
        i = 0
        nums = [0] * (m+n)

        while i1 < m and i2 < n:
            if nums1[i1] <  nums2[i2]:
                nums[i] = nums1[i1]
                i1 += 1
            else:
                nums[i] = nums2[i2]
                i2 += 1
            i += 1

        while i1 < m:
            nums[i] = nums1[i1]
            i1 += 1
            i += 1

        while i2 < n:
            nums[i] = nums2[i2]
            i2 += 1
            i += 1

        for i in range(m+n):
            nums1[i] = nums[i]



subjects/sbj-leetcode/study/88__merge-sorted-array__code.py
qphong/repeat
from collections import defaultdict

import util
import constants


class Tracker:
    @staticmethod
    def generate_tracker_data():
        return {
            "n_pass": 0,
            "n_fail": 0,
            "n_study": 0,
            "study_duration": 0.0,
            "box": 0,
        }

    def __init__(
        self,
        identifier,  # use to refer to its content, tags, ...
        interval=constants.SECS_MIN(1),
        oldest_epoch=constants.SECS_DAY(30),
    ):
        """
        0 - interval -> 1 - interval -> 2 - interval -> 3
        """
        self.time_aware_tracker_data = defaultdict(Tracker.generate_tracker_data)

        self.interval = interval
        self.oldest_epoch = oldest_epoch
        self.max_tracker_data_key = int(oldest_epoch / interval)

        self.identifier = identifier

        self.start_study_time = -constants.INF
        self.end_study_time = constants.INF
        self.prev_start_study_time = -constants.INF
        self.prev_end_study_time = constants.INF

    def to_dictionary(self):
        return {
            "identifier": self.identifier,
            "time_aware_tracker_data": dict(self.time_aware_tracker_data),
            "interval": self.interval,
            "oldest_epoch": self.oldest_epoch,
            "max_tracker_data_key": self.max_tracker_data_key,
            "start_study_time": self.start_study_time,
            "end_study_time": self.end_study_time,
            "prev_start_study_time": self.prev_start_study_time,
            "prev_end_study_time": self.prev_end_study_time,
            "state": self.get_state(),
        }

    @staticmethod
    def load_from_dictionary(data_dict):
        tracker = Tracker("dummy_identifier")

        tracker.identifier = data_dict["identifier"]
        tracker.interval = data_dict["interval"]
        tracker.oldest_epoch = data_dict["oldest_epoch"]
        tracker.max_tracker_data_key = int(tracker.oldest_epoch / tracker.interval)
        tracker.start_study_time = data_dict["start_study_time"]
        tracker.end_study_time = data_dict["end_study_time"]
        tracker.prev_start_study_time = data_dict["prev_start_study_time"]
        tracker.prev_end_study_time = data_dict["prev_end_study_time"]

        for key, data in data_dict["time_aware_tracker_data"].items():
            tracker.time_aware_tracker_data[int(key)] = data

        return tracker

    def epoch_to_discrete(self, epoch):
        return int(epoch / self.interval)

    def get_duration_since_last_end_study(self, at_time=None):
        if not at_time:
            at_time = util.get_now_epoch()

        if self.end_study_time < at_time:
            return at_time - self.end_study_time

        return constants.INVALID_DURATION

    def get_duration_since_last_start_study(self, at_time=None):
        if not at_time:
            at_time = util.get_now_epoch()

        return at_time - self.start_study_time

    def get_tracker_data_key(self, at_time=None):
        if not at_time:
            at_time = util.get_now_epoch()

        if self.prev_end_study_time < constants.INF:
            discrete_at_time = self.epoch_to_discrete(at_time)
            discrete_end_study_time = self.epoch_to_discrete(self.prev_end_study_time)

            since_last_study = discrete_at_time - discrete_end_study_time
        else:
            since_last_study = 0

        if since_last_study > self.max_tracker_data_key:
            since_last_study = self.max_tracker_data_key

        return since_last_study

    def get_tracker_data(self, at_time=None):
        key = self.get_tracker_data_key(at_time)
        return self.time_aware_tracker_data[key]

    def start_study(self, at_time=None):
        if self.is_studying():
            print(f"WARNING: {self.identifier} already started studying!")
            return

        if not at_time:
            at_time = util.get_now_epoch()

        self.prev_start_study_time = self.start_study_time
        self.prev_end_study_time = self.end_study_time

        self.start_study_time = at_time
        self.end_study_time = constants.INF

    def get_state(self):
        if self.is_new():
            return constants.STATE_NEW
        if self.is_studying():
            return constants.STATE_STUDYING
        if self.is_studied():
            return constants.STATE_STUDIED
        raise Exception(
            f"Unknown state: start_study_time:{self.start_study_time}, end_study_time: {self.end_study_time}"
        )

    def is_studying(self):
        return self.end_study_time == constants.INF and self.start_study_time > 0

    def is_new(self):
        return (
            self.start_study_time == -constants.INF
            and self.end_study_time == constants.INF
        )

    def is_studied(self):
        return self.end_study_time < constants.INF

    def get_overall_n_study(self):
        n_study = 0
        for tracker in self.time_aware_tracker_data.values():
            n_study += tracker["n_study"]
        return n_study

    def get_overall_n_pass(self):
        n_pass = 0
        for tracker in self.time_aware_tracker_data.values():
            n_pass += tracker["n_pass"]
        return n_pass

    def get_overall_n_fail(self):
        n_fail = 0
        for tracker in self.time_aware_tracker_data.values():
            n_fail += tracker["n_fail"]
        return n_fail

    def get_overall_duration(self):
        duration = 0.0
        for tracker in self.time_aware_tracker_data.values():
            duration += tracker["study_duration"]
        return duration

    def cancel_study(self):
        if self.is_studying():
            self.start_study_time = self.prev_start_study_time
            self.end_study_time = self.prev_end_study_time
        else:
            print("WARNING: cancel_study: nothing to cancel!")

    def end_study(self, status, at_time=None):
        if self.start_study_time == -constants.INF:
            raise Exception(
                "Cannot complete a study that haven't been started studying!"
            )
        if not at_time:
            at_time = util.get_now_epoch()

        if self.start_study_time > at_time:
            raise Exception("end_study: start_study_time > end_study_time!")

        self.end_study_time = at_time

        # update tracker_data
        at_key = self.get_tracker_data_key(at_time)
        tracker_data = self.time_aware_tracker_data[at_key]

        tracker_data["n_study"] += 1
        tracker_data["study_duration"] += self.end_study_time - self.start_study_time

        if status == constants.PASS:
            tracker_data["n_pass"] += 1
            # increase box of self.time_aware_tracker_data[key] to 0 for all key <= at_key by 1
            upper_key = self.get_upper_key(at_key)
            if upper_key:
                current_box = self.time_aware_tracker_data[upper_key]["box"]
            else:
                current_box = self.time_aware_tracker_data[at_key]["box"]

            for key in self.time_aware_tracker_data:
                if key <= at_key:
                    self.time_aware_tracker_data[key]["box"] = max(
                        current_box + 1, self.time_aware_tracker_data[key]["box"]
                    )

        elif status == constants.FAIL:
            tracker_data["n_fail"] += 1
            # reset box of self.time_aware_tracker_data[key] to 0 for all key
            for key in self.time_aware_tracker_data:
                self.time_aware_tracker_data[key]["box"] = 0

        else:
            raise Exception(f"Unknown status: {status}")

    def get_upper_key(self, at_key):
        if at_key not in self.time_aware_tracker_data:
            upper_key = None
            for key in self.time_aware_tracker_data:
                if key > at_key:
                    upper_key = min(upper_key, key) if upper_key else key

            return upper_key
        return at_key

    def get_assessing_box(
        self, at_time=None, transformation=constants.DEFAULT_BOX_TRANSFORMATION
    ):
        # to assess memory quality
        if not at_time:
            at_time = util.get_now_epoch()
        at_key = self.get_tracker_data_key(at_time)

        if at_key not in self.time_aware_tracker_data:
            upper_key = self.get_upper_key(at_key)

            if upper_key:
                return transformation(
                    at_key, self.time_aware_tracker_data[upper_key]["box"]
                )
            else:
                return 0

        return transformation(at_key, self.time_aware_tracker_data[at_key]["box"])

tracker.py
qphong/repeat
import random
import math
import datetime
import constants


def get_random_string():
    return f"{random.getrandbits(20)}"


def get_now_epoch():
    return (datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)).total_seconds()


def parse_args(config, arguments, required):
    results = []

    for i, argument in enumerate(arguments):
        if argument in [
            "command",
            "subject",
            "identifier",
            "name",
            "by",
            "direction",
            "postfix",
            "passfail",
        ]:
            if required[i] and (
                config[argument] == None or config[argument] == constants.AUTO_ID
            ):
                raise Exception(f"Require {argument}")

            results.append(config[argument])

        elif argument in ["tags", "states"]:
            if config[argument] is None or config[argument] == "all":
                results.append([])
            else:
                results.append(
                    [
                        elem.strip()
                        for elem in config[argument].split(",")
                        if len(elem.strip()) > 0
                    ]
                )

        else:
            raise Exception(f"Unknown argument: {argument}")

    if len(results) == 1:
        return results[0]
    return results


def get_info(identifier, item, tracker):
    state = tracker.get_state()
    box = tracker.get_assessing_box(transformation=constants.DEFAULT_BOX_TRANSFORMATION)
    n_pass = tracker.get_overall_n_pass()
    n_fail = tracker.get_overall_n_fail()
    n_study = n_pass + n_fail
    duration = tracker.get_overall_duration()

    info = {
        "identifier": identifier,
        "content": item.content,
        "tags": list(item.tags),
        "state": state,
        "competency": box,
        "n_pass": n_pass,
        "n_fail": n_fail,
        "n_study": n_study,
        "pass_pct": float(n_pass) / n_study if n_study > 0 else 0.0,
        "duration": duration,
    }

    since_last_end_study = tracker.get_duration_since_last_end_study()

    if state == constants.STATE_NEW:
        info["since_last_start_study"] = constants.INF
    else:
        info["since_last_start_study"] = tracker.get_duration_since_last_start_study()

    if state == constants.STATE_NEW:
        info["since_last_end_study"] = constants.INF
    else:
        info["since_last_end_study"] = tracker.get_duration_since_last_end_study()

    return info


def get_readable_tag(tag, style=False):
    if not style or tag not in constants.TAG_STYLE:
        return tag

    return constants.TAG_STYLE[tag]


def get_readable_state(state, style=False):
    if not style or state not in constants.STATE_STYLE:
        return state

    return constants.STATE_STYLE[state]


def get_readable_info(
    info_dict,
    content_fields,
    extra_info=[
        constants.LABEL_TAG,
        constants.LABEL_STATE,
        constants.LABEL_COMPETENCY,
        constants.LABEL_PASS_PCT,
        constants.LABEL_DURATION,
    ],
    emphasis=[],
):
    info_to_func = {
        constants.LABEL_CONTENT: lambda info_dict, emphasis: show_content(
            info_dict, content_fields, emphasis
        ),
        constants.LABEL_STATE: show_state,
        constants.LABEL_SINCE: show_since,
        constants.LABEL_PASS_PCT: show_passpct,
        constants.LABEL_DURATION: show_duration,
        constants.LABEL_TAG: show_tag,
        constants.LABEL_COMPETENCY: show_competency,
    }

    # info_dict is returned from get_info function
    string = f"[{info_dict['identifier']:6s}]"

    for info in extra_info:
        string += f" {info_to_func[info](info_dict, info in emphasis)}"

    return string


def show_content(info_dict, fields, emphasis=False):
    string = '"'

    for i, field in enumerate(fields):
        string += f"{info_dict['content'][field]}"
        if i > 0:
            string += "|"

    string += '"'

    return string


def show_state(info_dict, emphasis=False):
    state_str = ""

    if info_dict["state"] != constants.STATE_STUDIED:
        state_str = get_readable_state(info_dict["state"], style=True)

    if emphasis:
        return constants.BOLD_TEXT(state_str)
    return state_str


def show_since(info_dict, emphasis=False):
    since_str = ""

    if (
        "since_last_start_study" in info_dict
        and info_dict["since_last_start_study"] < constants.INF
    ):
        since_str = f"{get_readable_duration(info_dict['since_last_start_study'])} ago"

    elif (
        "since_last_end_study" in info_dict
        and info_dict["since_last_end_study"] < constants.INF
    ):
        since_str = f"{get_readable_duration(info_dict['since_last_end_study'])} ago"

    if emphasis:
        return f"{constants.BOLD_TEXT(since_str):<11s}"
    return f"{constants.UNDERLINE_TEXT(since_str):<11s}"


def show_passpct(info_dict, emphasis=False):
    pass_str = ""

    # if info_dict["n_study"] > 0:
    # pass_str = f"({info_dict['n_study']}:{info_dict['n_pass'] / info_dict['n_study'] * 100:.0f}%P)"
    # pass_str = f"{info_dict['n_pass']:3d}/{info_dict['n_study']:<3d}"

    pass_str = f"{info_dict['n_pass']:3d}/{info_dict['n_study']:<3d}"

    if emphasis:
        return constants.BOLD_TEXT(pass_str)
    return pass_str


def show_duration(info_dict, emphasis=False):
    study_duration_str = ""

    if info_dict["duration"] > 0:
        study_duration_str = f"in {get_readable_duration(info_dict['duration']):<7s}"

    if emphasis:
        return constants.BOLD_TEXT(study_duration_str)
    return study_duration_str


def show_tag(info_dict, emphasis=False):
    string = ":"
    sorted_tag = sorted(info_dict["tags"])
    for i, tag in enumerate(sorted_tag):
        tag_str = get_readable_tag(tag, style=True)
        string += tag_str + (":" if i < len(info_dict["tags"]) - 1 else "")
    string += ":"

    if emphasis:
        return constants.BOLD_TEXT(string)
    return string


def show_competency(info_dict, emphasis=False):
    competency_str = f"B:{int(info_dict['competency']):<2d}"

    if emphasis:
        return constants.BOLD_TEXT(competency_str)
    return competency_str


def get_readable_duration(duration):
    if duration == constants.INVALID_DURATION:
        return "INVALID_DURATION"

    tmp = duration / 24 / 60 / 60 / 30
    month = math.floor(tmp)
    tmp = (tmp - month) * 30
    day = math.floor(tmp)
    tmp = (tmp - day) * 24
    hour = math.floor(tmp)
    tmp = (tmp - hour) * 60
    minute = math.floor(tmp)
    second = math.floor((tmp - minute) * 60)
    duration_str = ""

    month_str = f"{month} {'months' if month > 1 else 'month'}"
    day_str = f"{day} {'days' if day > 1 else 'day'}"
    hour_str = f"{hour}h"
    minute_str = f"{minute}m"
    second_str = f"{second}s"

    if month > 0:
        duration_str = f"{month_str}{f' {day_str}' if day > 0 else ''}"
    elif day > 0:
        duration_str = f"{day_str}{f' {hour_str}' if hour > 0 else ''}"
    elif hour > 0:
        duration_str = (
            f"{hour_str if hour > 0 else ''}{minute_str if minute > 0 else ''}"
        )
    else:
        duration_str = (
            f"{minute_str if minute > 0 else ''}{second_str if second > 0 else ''}"
        )
    return duration_str

util.py
qphong/repeat