read_stats.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. """
  2. Cкрипт со статистикой чтния книг.
  3. Author: Milinuri Nirvalen
  4. Ver: 2.3.2
  5. date: Получение текущего дня в году
  6. json: Управление файлами с данными
  7. Path: Проверка существования файла данных
  8. """
  9. import argparse
  10. import json
  11. from datetime import date
  12. from pathlib import Path
  13. from typing import Optional
  14. r = "\033[0m"
  15. g = "\033[90m"
  16. # Путь к файлу с данными о книгах
  17. data_path = Path("books.json")
  18. # Цвета палитры в зависимости от прогресса
  19. color_model = {0: "\033[35m", 1: "\033[34m",
  20. 4: "\033[36m", 5: "\033[32m",
  21. 7: "\033[33m", 9: "\033[31m"}
  22. # Неполные блоков для графика и линии прогресса
  23. graph_model = {1:"▁", 3:"▂", 4:"▃", 5:"▄", 6:"▅", 8:"▆", 9:"▇"}
  24. progress_model = {1:"▏", 3:"▎", 4:"▍", 5:"▌", 6: "▋",7:"▋", 8:"▊", 9:"▉"}
  25. """
  26. Параметры книг по умолчанию
  27. ===========================
  28. pages: Кол-во страниц в книге
  29. days_left: Сколько дней осталось на чтение книги (30)
  30. days: Список номерами текущей страницы в каждый день
  31. today: Сегодняшнего день в году
  32. """
  33. model = {"pages":100, "days_left":30, "days":[0], "today":0}
  34. def save_file(data: dict) -> dict:
  35. """Записывает данные о книгах в json-файл.
  36. Args:
  37. data (dict): Словарь с данными для записи
  38. Returns:
  39. dict: Записанные в файл данные
  40. """
  41. with open(data_path, 'w') as f:
  42. f.write(json.dumps(data, indent=4))
  43. return data
  44. def load_file() -> dict:
  45. """Загружает json файл с данными о книгах.
  46. Если файла нет - создаёт его.
  47. Returns:
  48. dict: Инфопрмация о книгах
  49. """
  50. if not data_path.is_file():
  51. example_book = {
  52. "pages": 273,
  53. "days_left": 18,
  54. "days": [14, 27, 48, 53, 72],
  55. "today": 0
  56. }
  57. return save_file({"example":example_book})
  58. with open(data_path) as f:
  59. return json.loads(f.read())
  60. # Вспомогательные функции
  61. # =======================
  62. def ask_user(text: str, default: Optional[str] = None) -> str:
  63. text = f"\033[90m[?] {text} (\033[33m{default}\033[90m)\033[0m "
  64. return input(text) or default
  65. def value_by_nearest_key(n: int, model: Optional[dict] = color_model) -> str:
  66. return model[min(model, key=lambda x: abs(x - n))]
  67. def graph(data: list, size: Optional[int] = 10) -> None:
  68. """Отрисовывает цветной текстовый график.
  69. Args:
  70. data (list): Последовательность значений для графика
  71. size (int, optional): Высота графика (10)
  72. """
  73. # Нормальизуем значения в промежуток от 0.0 до size (10.0)
  74. md = max(data)
  75. pr = ([0] * (60 - len(data))) + [round(x/md*size, 1) for x in data[-60:]]
  76. # Пробегаемся по каждой строке (10 -> 0)
  77. for x in range(size, 0, -1):
  78. colored = False
  79. i = int(round(md/size*x, 2))
  80. line = f'{i:4}|'
  81. # Пробегаемся слева направо по строке
  82. for j in pr:
  83. if j >= x-1 and not colored:
  84. line += value_by_nearest_key(size-x)
  85. colored = True
  86. if j >= x:
  87. line += f'█'
  88. elif j >= x-1:
  89. line += value_by_nearest_key(int(j % 1 * 10), graph_model)
  90. else:
  91. line += '_'
  92. print(f'{line}\033[0m')
  93. def progress_line(i: int, total: int, size: Optional[int] = 30) -> str:
  94. """Возвращает строковую линию прогресса.
  95. Args:
  96. i (int): Текущее значение
  97. total (int): Максимальное значение
  98. size (int, optional): Размер линии прогресса
  99. Returns:
  100. str: Линия прогресса
  101. """
  102. pr = round(i/total*size, 1)
  103. lpr = int(pr)
  104. head_pr = int(pr % 1 * 10)
  105. color = value_by_nearest_key(10-(i/total*10))
  106. head = value_by_nearest_key(head_pr, progress_model) if pr <= size else ""
  107. fpr = round(i/total*100, 2)
  108. return f" {color}{'█'*(lpr)}{head}{' '*(size-lpr-1)}\033[0m: {fpr}%"
  109. def procent_diff(a: int, b: int) -> str:
  110. color = "\033[31m" if a >= b else "\033[32m"
  111. return f"({color}{abs(round((b/a)*100, 2))}%\033[0m)"
  112. # Вспомогтельный класс
  113. # ====================
  114. class BookStats:
  115. """Класс описывает статистику чтения книги.
  116. Attributes:
  117. ars (float): Средняя скорость чтения книги (страниц/день)
  118. cp (int): Текущая страница
  119. data (dict): Данные о прочтении книги
  120. name (str): Ключевое название книги
  121. pages_left (int): Сколько страниц осталось читать
  122. pr (float): Сколько процентов книги уже прочитано
  123. pr_up (float): Сколько процентов книги вы прочитали за сегодня
  124. read_p (float): Сколько прочитано страниц в каждый из дней
  125. rt (int): Сколько страниц вы прочитали за сегодня
  126. tp (int): Сколько страни нужно прочитать за этот день
  127. """
  128. def __init__(self, name: str, data: dict) -> None:
  129. super(BookStats, self).__init__()
  130. self.name = name
  131. self.data = data
  132. # Добавляет атрибуты: pages, days_left... из data
  133. self.__dict__.update(self.data)
  134. self._ars = None
  135. self.read_p = [self.days[x] - self.days[x-1] for x in range(1, len(self.days))]
  136. self.rt = self.read_p[-1]
  137. self.cp = self.days[-1]
  138. self.pages_left = self.pages - self.cp
  139. self.pdays_left = round(self.pages_left/self.ars, 2)
  140. self.tp = round(self.pages_left/self.days_left, 2)
  141. self.pr_up = round(self.rt/self.pages*100, 2)
  142. self.pr = round(self.cp/self.pages*100, 2)
  143. @property
  144. def ars(self) -> float:
  145. """Получает среднию скорость чтения за последнии 7 дней.
  146. Returns:
  147. float: Среднее страниц в день
  148. """
  149. if self._ars is None:
  150. i = min(len(self.read_p), 7)
  151. total_pages = sum(list(filter(lambda x: x > 3, self.read_p))[-7:])
  152. self._ars = round(total_pages / i, 2)
  153. return self._ars or 1
  154. def get_diff(self, a: int, b: int) -> str:
  155. """Получает разницу между числами А и B.
  156. Сколько прочитано страниц за вчера и сегодня.
  157. Args:
  158. a (int): Первое число для сравнения
  159. b (int): Второе число для сравнения
  160. Returns:
  161. str: Строковая раззница между числами
  162. """
  163. diff = f"{a-b} меньше" if a >= b else f"{b-a} больше"
  164. pr_diff = f" ({abs(round(b/a*100, 2))}%)" if a > 0 else ""
  165. return f"Прочитано на {diff}, чем вчера{pr_diff}"
  166. def status(self) -> None:
  167. """Краткая сводку о чтении книги."""
  168. c = value_by_nearest_key(10-round(self.cp/self.pages*10))
  169. text = f"{c}{self.days_left} {g}[{c}{self.pr}%{g}]{r} {self.name} "
  170. text += f"{self.cp}/{self.pages} ({self.pages_left})"
  171. readed = self.read_p[-1]
  172. if readed:
  173. readed_pr = round(readed/self.pages*100, 2)
  174. text += f" {c}+{readed} стр. +{readed_pr}%{r}"
  175. print(text)
  176. # Основные функции
  177. # ================
  178. def get_previous_book(book: BookStats) -> BookStats:
  179. """Получет статистику книги за вчерашний день.
  180. Args:
  181. book (BookStats): Экзмеляр BookStats за сегодня
  182. Returns:
  183. BookStats: Экземпляр BookStats за вчерашний день
  184. """
  185. data = book.data.copy()
  186. data["days_left"] += 1
  187. data["days"] = data["days"][:-1] or [0]
  188. data["today"] -= 1
  189. return BookStats(book.name, data)
  190. def read_info(book: BookStats) -> None:
  191. """Отображение статистики о прочтении книги."""
  192. print(progress_line(book.cp, book.pages), "Прочитано")
  193. if book.rt < book.tp:
  194. print(progress_line(book.rt, book.tp), "Цель на сегодня")
  195. if len(book.days) > 3:
  196. p_book = get_previous_book(book)
  197. ars_diff = f" {procent_diff(p_book.ars, book.ars)}"
  198. days_diff = f" {procent_diff(book.pdays_left, p_book.pdays_left)}"
  199. target_diff = f" {procent_diff(book.tp, p_book.tp)}"
  200. else:
  201. ars_diff = ""
  202. days_diff = ""
  203. target_diff = ""
  204. print(f" Читаете {len(book.days)} дней по ~{book.ars} страниц{ars_diff}")
  205. print(f" Закончите за ~{book.pdays_left} дней{days_diff}")
  206. print(f" Осталось {book.days_left} дней по {book.tp} в день{target_diff}")
  207. print(f" Cегодня {book.rt} страниц, это +{book.pr_up}%")
  208. if len(book.days) > 3:
  209. print(book.get_diff(book.read_p[-2], book.read_p[-1]))
  210. graph(book.read_p)
  211. def books_list(books: list[BookStats]) -> None:
  212. """Отображает список книг, которые вы читаете.
  213. Args:
  214. books (list): Список из экземпляров BookStats
  215. """
  216. def sort_func(x):
  217. return x.pages - x.days[-1]
  218. if len(books):
  219. for x in sorted(books, key=sort_func):
  220. x.status()
  221. else:
  222. print('Вы не читаете ни одной книги :(')
  223. def edit_data(data: dict, days_shift: Optional[bool] = False) -> dict:
  224. """Обновляет данные о читаемой книги.
  225. Запрашивает кол-во страниц и текущую страницу.
  226. Args:
  227. data (dict): Данные о книге
  228. days_shift (bool, optional): Принудительная смена дня
  229. Returns:
  230. dict: Изменённые данные книги
  231. """
  232. today = int(date.today().strftime('%j'))
  233. a = ask_user("Страниц в книге", str(data["pages"]))
  234. # Если пользователь указал 0 - удаляем книгу
  235. if a == "0":
  236. return None
  237. # Перерасчитываем статистику, работет в обоих направлениях
  238. elif a and a[0] == '*' and a[1:].isdigit():
  239. a = int(a[1:])
  240. data['days'] = list(map(lambda x: round(a/data['pages']*x),
  241. data['days']))
  242. data['pages'] = a
  243. # Если количество страниц изменилось, сбрасываем информацию
  244. elif a.isdigit() and int(a) != data["pages"]:
  245. data = model
  246. data['pages'] = int(a)
  247. # Спрашиваем о кол-ве дней, оставшихся на чтение книги
  248. a = int(ask_user('Осталось на чтение', data["days_left"]))
  249. data["days_lefta"] = a
  250. # Ежедневный сдвиг значения прочтения
  251. if today != data['today'] or days_shift:
  252. if len(data['days']):
  253. data['days'].append(data['days'][-1])
  254. else:
  255. data['days'].append(0)
  256. data['today'] = today
  257. data['days_left'] -= 1
  258. # Если время на чтение книги истекло - удаляем книгу.
  259. if data["days_left"] <= 0:
  260. return None
  261. # Изменение текущей страницы
  262. a = ask_user('Вы на странице', str(data["days"][-1]))
  263. if a.isdigit():
  264. data['days'][-1] = int(a)
  265. return data
  266. def main() -> None:
  267. parser = argparse.ArgumentParser("Статиска чтения книг")
  268. parser.add_argument("-e", "--edit", default=False, action="store_true",
  269. help="Изменить информацию о книге")
  270. parser.add_argument("-d", "--day-shift", default=False, action="store_true",
  271. help="Симулировать смену дня")
  272. parser.add_argument("book", nargs="*", help="Ключ книги")
  273. args = parser.parse_args()
  274. data = load_file()
  275. books = {k: BookStats(k, v) for k, v in data.items()}
  276. edit_mode = False
  277. for x in args.book:
  278. if args.edit:
  279. edited = edit_data(data.get(x, model), args.day_shift)
  280. if not edited and data.get(x):
  281. del data[x]
  282. del books[x]
  283. print(x, 'Удалена.')
  284. continue
  285. data[x] = edited
  286. books[x] = BookStats(x, edited)
  287. if x not in data:
  288. print(x, 'Не существует.')
  289. continue
  290. read_info(books[x])
  291. books_list(books.values())
  292. save_file(data)
  293. # запуск скрипта
  294. # ==============
  295. if __name__ == '__main__':
  296. main()