123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- """
- Cкрипт со статистикой чтния книг.
- Author: Milinuri Nirvalen
- Ver: 2.3.2
- date: Получение текущего дня в году
- json: Управление файлами с данными
- Path: Проверка существования файла данных
- """
- import argparse
- import json
- from datetime import date
- from pathlib import Path
- from typing import Optional
- r = "\033[0m"
- g = "\033[90m"
- # Путь к файлу с данными о книгах
- data_path = Path("books.json")
- # Цвета палитры в зависимости от прогресса
- color_model = {0: "\033[35m", 1: "\033[34m",
- 4: "\033[36m", 5: "\033[32m",
- 7: "\033[33m", 9: "\033[31m"}
- # Неполные блоков для графика и линии прогресса
- graph_model = {1:"▁", 3:"▂", 4:"▃", 5:"▄", 6:"▅", 8:"▆", 9:"▇"}
- progress_model = {1:"▏", 3:"▎", 4:"▍", 5:"▌", 6: "▋",7:"▋", 8:"▊", 9:"▉"}
- """
- Параметры книг по умолчанию
- ===========================
- pages: Кол-во страниц в книге
- days_left: Сколько дней осталось на чтение книги (30)
- days: Список номерами текущей страницы в каждый день
- today: Сегодняшнего день в году
- """
- model = {"pages":100, "days_left":30, "days":[0], "today":0}
- def save_file(data: dict) -> dict:
- """Записывает данные о книгах в json-файл.
- Args:
- data (dict): Словарь с данными для записи
- Returns:
- dict: Записанные в файл данные
- """
- with open(data_path, 'w') as f:
- f.write(json.dumps(data, indent=4))
- return data
- def load_file() -> dict:
- """Загружает json файл с данными о книгах.
- Если файла нет - создаёт его.
- Returns:
- dict: Инфопрмация о книгах
- """
- if not data_path.is_file():
- example_book = {
- "pages": 273,
- "days_left": 18,
- "days": [14, 27, 48, 53, 72],
- "today": 0
- }
- return save_file({"example":example_book})
- with open(data_path) as f:
- return json.loads(f.read())
- # Вспомогательные функции
- # =======================
- def ask_user(text: str, default: Optional[str] = None) -> str:
- text = f"\033[90m[?] {text} (\033[33m{default}\033[90m)\033[0m "
- return input(text) or default
- def value_by_nearest_key(n: int, model: Optional[dict] = color_model) -> str:
- return model[min(model, key=lambda x: abs(x - n))]
- def graph(data: list, size: Optional[int] = 10) -> None:
- """Отрисовывает цветной текстовый график.
- Args:
- data (list): Последовательность значений для графика
- size (int, optional): Высота графика (10)
- """
- # Нормальизуем значения в промежуток от 0.0 до size (10.0)
- md = max(data)
- pr = ([0] * (60 - len(data))) + [round(x/md*size, 1) for x in data[-60:]]
- # Пробегаемся по каждой строке (10 -> 0)
- for x in range(size, 0, -1):
- colored = False
- i = int(round(md/size*x, 2))
- line = f'{i:4}|'
- # Пробегаемся слева направо по строке
- for j in pr:
- if j >= x-1 and not colored:
- line += value_by_nearest_key(size-x)
- colored = True
- if j >= x:
- line += f'█'
- elif j >= x-1:
- line += value_by_nearest_key(int(j % 1 * 10), graph_model)
- else:
- line += '_'
- print(f'{line}\033[0m')
- def progress_line(i: int, total: int, size: Optional[int] = 30) -> str:
- """Возвращает строковую линию прогресса.
- Args:
- i (int): Текущее значение
- total (int): Максимальное значение
- size (int, optional): Размер линии прогресса
- Returns:
- str: Линия прогресса
- """
- pr = round(i/total*size, 1)
- lpr = int(pr)
- head_pr = int(pr % 1 * 10)
- color = value_by_nearest_key(10-(i/total*10))
- head = value_by_nearest_key(head_pr, progress_model) if pr <= size else ""
- fpr = round(i/total*100, 2)
- return f" {color}{'█'*(lpr)}{head}{' '*(size-lpr-1)}\033[0m: {fpr}%"
- def procent_diff(a: int, b: int) -> str:
- color = "\033[31m" if a >= b else "\033[32m"
- return f"({color}{abs(round((b/a)*100, 2))}%\033[0m)"
- # Вспомогтельный класс
- # ====================
- class BookStats:
- """Класс описывает статистику чтения книги.
- Attributes:
- ars (float): Средняя скорость чтения книги (страниц/день)
- cp (int): Текущая страница
- data (dict): Данные о прочтении книги
- name (str): Ключевое название книги
- pages_left (int): Сколько страниц осталось читать
- pr (float): Сколько процентов книги уже прочитано
- pr_up (float): Сколько процентов книги вы прочитали за сегодня
- read_p (float): Сколько прочитано страниц в каждый из дней
- rt (int): Сколько страниц вы прочитали за сегодня
- tp (int): Сколько страни нужно прочитать за этот день
- """
- def __init__(self, name: str, data: dict) -> None:
- super(BookStats, self).__init__()
- self.name = name
- self.data = data
- # Добавляет атрибуты: pages, days_left... из data
- self.__dict__.update(self.data)
- self._ars = None
- self.read_p = [self.days[x] - self.days[x-1] for x in range(1, len(self.days))]
- self.rt = self.read_p[-1]
- self.cp = self.days[-1]
- self.pages_left = self.pages - self.cp
- self.pdays_left = round(self.pages_left/self.ars, 2)
- self.tp = round(self.pages_left/self.days_left, 2)
- self.pr_up = round(self.rt/self.pages*100, 2)
- self.pr = round(self.cp/self.pages*100, 2)
- @property
- def ars(self) -> float:
- """Получает среднию скорость чтения за последнии 7 дней.
- Returns:
- float: Среднее страниц в день
- """
- if self._ars is None:
- i = min(len(self.read_p), 7)
- total_pages = sum(list(filter(lambda x: x > 3, self.read_p))[-7:])
- self._ars = round(total_pages / i, 2)
- return self._ars or 1
- def get_diff(self, a: int, b: int) -> str:
- """Получает разницу между числами А и B.
- Сколько прочитано страниц за вчера и сегодня.
- Args:
- a (int): Первое число для сравнения
- b (int): Второе число для сравнения
- Returns:
- str: Строковая раззница между числами
- """
- diff = f"{a-b} меньше" if a >= b else f"{b-a} больше"
- pr_diff = f" ({abs(round(b/a*100, 2))}%)" if a > 0 else ""
- return f"Прочитано на {diff}, чем вчера{pr_diff}"
- def status(self) -> None:
- """Краткая сводку о чтении книги."""
- c = value_by_nearest_key(10-round(self.cp/self.pages*10))
- text = f"{c}{self.days_left} {g}[{c}{self.pr}%{g}]{r} {self.name} "
- text += f"{self.cp}/{self.pages} ({self.pages_left})"
- readed = self.read_p[-1]
- if readed:
- readed_pr = round(readed/self.pages*100, 2)
- text += f" {c}+{readed} стр. +{readed_pr}%{r}"
- print(text)
- # Основные функции
- # ================
- def get_previous_book(book: BookStats) -> BookStats:
- """Получет статистику книги за вчерашний день.
- Args:
- book (BookStats): Экзмеляр BookStats за сегодня
- Returns:
- BookStats: Экземпляр BookStats за вчерашний день
- """
- data = book.data.copy()
- data["days_left"] += 1
- data["days"] = data["days"][:-1] or [0]
- data["today"] -= 1
- return BookStats(book.name, data)
- def read_info(book: BookStats) -> None:
- """Отображение статистики о прочтении книги."""
- print(progress_line(book.cp, book.pages), "Прочитано")
- if book.rt < book.tp:
- print(progress_line(book.rt, book.tp), "Цель на сегодня")
- if len(book.days) > 3:
- p_book = get_previous_book(book)
- ars_diff = f" {procent_diff(p_book.ars, book.ars)}"
- days_diff = f" {procent_diff(book.pdays_left, p_book.pdays_left)}"
- target_diff = f" {procent_diff(book.tp, p_book.tp)}"
- else:
- ars_diff = ""
- days_diff = ""
- target_diff = ""
- print(f" Читаете {len(book.days)} дней по ~{book.ars} страниц{ars_diff}")
- print(f" Закончите за ~{book.pdays_left} дней{days_diff}")
- print(f" Осталось {book.days_left} дней по {book.tp} в день{target_diff}")
- print(f" Cегодня {book.rt} страниц, это +{book.pr_up}%")
- if len(book.days) > 3:
- print(book.get_diff(book.read_p[-2], book.read_p[-1]))
- graph(book.read_p)
- def books_list(books: list[BookStats]) -> None:
- """Отображает список книг, которые вы читаете.
- Args:
- books (list): Список из экземпляров BookStats
- """
- def sort_func(x):
- return x.pages - x.days[-1]
- if len(books):
- for x in sorted(books, key=sort_func):
- x.status()
- else:
- print('Вы не читаете ни одной книги :(')
- def edit_data(data: dict, days_shift: Optional[bool] = False) -> dict:
- """Обновляет данные о читаемой книги.
- Запрашивает кол-во страниц и текущую страницу.
- Args:
- data (dict): Данные о книге
- days_shift (bool, optional): Принудительная смена дня
- Returns:
- dict: Изменённые данные книги
- """
- today = int(date.today().strftime('%j'))
- a = ask_user("Страниц в книге", str(data["pages"]))
- # Если пользователь указал 0 - удаляем книгу
- if a == "0":
- return None
- # Перерасчитываем статистику, работет в обоих направлениях
- elif a and a[0] == '*' and a[1:].isdigit():
- a = int(a[1:])
- data['days'] = list(map(lambda x: round(a/data['pages']*x),
- data['days']))
- data['pages'] = a
- # Если количество страниц изменилось, сбрасываем информацию
- elif a.isdigit() and int(a) != data["pages"]:
- data = model
- data['pages'] = int(a)
- # Спрашиваем о кол-ве дней, оставшихся на чтение книги
- a = int(ask_user('Осталось на чтение', data["days_left"]))
- data["days_lefta"] = a
- # Ежедневный сдвиг значения прочтения
- if today != data['today'] or days_shift:
- if len(data['days']):
- data['days'].append(data['days'][-1])
- else:
- data['days'].append(0)
- data['today'] = today
- data['days_left'] -= 1
- # Если время на чтение книги истекло - удаляем книгу.
- if data["days_left"] <= 0:
- return None
- # Изменение текущей страницы
- a = ask_user('Вы на странице', str(data["days"][-1]))
- if a.isdigit():
- data['days'][-1] = int(a)
- return data
- def main() -> None:
- parser = argparse.ArgumentParser("Статиска чтения книг")
- parser.add_argument("-e", "--edit", default=False, action="store_true",
- help="Изменить информацию о книге")
- parser.add_argument("-d", "--day-shift", default=False, action="store_true",
- help="Симулировать смену дня")
- parser.add_argument("book", nargs="*", help="Ключ книги")
- args = parser.parse_args()
- data = load_file()
- books = {k: BookStats(k, v) for k, v in data.items()}
- edit_mode = False
- for x in args.book:
- if args.edit:
- edited = edit_data(data.get(x, model), args.day_shift)
- if not edited and data.get(x):
- del data[x]
- del books[x]
- print(x, 'Удалена.')
- continue
- data[x] = edited
- books[x] = BookStats(x, edited)
- if x not in data:
- print(x, 'Не существует.')
- continue
- read_info(books[x])
- books_list(books.values())
- save_file(data)
- # запуск скрипта
- # ==============
- if __name__ == '__main__':
- main()
|