diff --git a/Time.py b/Time.py new file mode 100644 index 0000000..316141f --- /dev/null +++ b/Time.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import List, Tuple, Optional +from decimal import Decimal, ROUND_HALF_UP, getcontext +import sys + +# Точность для операций Decimal +getcontext().prec = 28 + +# ------------ Ввод/форматирование ------------ + +def parse_total_minutes(s: str) -> int: + """ + Поддерживает форматы: + - "8" (часы) + - "7.5" (десятичные часы) + - "7:30" (ЧЧ:ММ) + Возвращает целые минуты. + """ + s = s.strip().replace(",", ".") + if ":" in s: + parts = s.split(":") + if len(parts) != 2: + raise ValueError("Неверный формат времени (ожидается ЧЧ:ММ).") + h = int(parts[0]); m = int(parts[1]) + if h < 0 or m < 0: + raise ValueError("Часы и минуты не могут быть отрицательными.") + return h * 60 + m + hours = float(s) + if hours < 0: + raise ValueError("Часы не могут быть отрицательными.") + return int(round(hours * 60)) + + +def ask_nonempty(prompt: str, default: str = "") -> str: + v = input(prompt).strip() + return v if v else default + + +def ask_int(prompt: str, min_value: int = None, max_value: int = None) -> int: + while True: + v = input(prompt).strip() + try: + x = int(v) + if min_value is not None and x < min_value: + print(f"Значение должно быть ≥ {min_value}") + continue + if max_value is not None and x > max_value: + print(f"Значение должно быть ≤ {max_value}") + continue + return x + except ValueError: + print("Введите целое число.") + + +def ask_float(prompt: str, min_value: float = None, max_value: float = None, default: float = None) -> float: + while True: + v = input(prompt).strip().replace(",", ".") + if not v and default is not None: + return default + try: + x = float(v) + if min_value is not None and x < min_value: + print(f"Значение должно быть ≥ {min_value}") + continue + if max_value is not None and x > max_value: + print(f"Значение должно быть ≤ {max_value}") + continue + return x + except ValueError: + print("Введите число (можно с десятичной точкой).") + + +def format_hhmm(minutes: int) -> str: + h = minutes // 60 + m = minutes % 60 + return f"{h:02d}:{m:02d}" + +# ------------ Арифметика десятичных часов ------------ + +def minutes_to_decimal_hours_half_up(minutes: int, step: str) -> Decimal: + """ + Перевод минут в десятичные часы с округлением HALF_UP к заданному шагу: + step = '0.01' -> сотые часа + step = '0.1' -> десятые часа (0.1ч = 6 минут) + """ + d = Decimal(minutes) / Decimal(60) + q = Decimal(step) + return d.quantize(q, rounding=ROUND_HALF_UP) + + +def balanced_decimal_hours(minutes_by_task: List[int], total_minutes: int, step: str = "0.01") -> List[str]: + """ + Балансированное округление в десятичные часы так, что + сумма в выбранных сотых/десятых совпадает с общим временем. + """ + factor = 100 if step == "0.01" else 10 + exact = [Decimal(m) / Decimal(60) for m in minutes_by_task] + scaled = [x * factor for x in exact] # точные сотые/десятые + base = [int(x) for x in scaled] # нижняя целая часть + target = int(Decimal(total_minutes) / Decimal(60) * factor) + remainder = target - sum(base) + + # Метод наибольших остатков по дробным частям + order = sorted(range(len(scaled)), key=lambda i: (scaled[i] - base[i]), reverse=True) + for i in range(max(0, remainder)): + base[order[i]] += 1 + + # Форматированный вывод + if factor == 100: + return [f"{Decimal(v) / Decimal(100):.2f}" for v in base] + else: + return [f"{Decimal(v) / Decimal(10):.1f}" for v in base] + +# ------------ Распределение минут ------------ + +def distribute_by_weights(total_minutes: int, weights: List[float]) -> List[int]: + """ + Распределение целых минут методом наибольших остатков из точных долей. + """ + if total_minutes < 0: + raise ValueError("total_minutes < 0") + if not weights or any(w < 0 for w in weights): + raise ValueError("Некорректные веса.") + s = sum(weights) + n = len(weights) + if s == 0: + weights = [1.0] * n + s = float(n) + + exact = [total_minutes * (w / s) for w in weights] + base = [int(x) for x in exact] + allocated = sum(base) + remainder = total_minutes - allocated + + # Раздать остаток по наибольшим дробным частям + frac_with_idx = sorted(((i, exact[i] - base[i]) for i in range(n)), key=lambda t: t[1], reverse=True) + i = 0 + while remainder > 0 and i < n: + idx = frac_with_idx[i][0] + base[idx] += 1 + remainder -= 1 + i += 1 + return base + + +def distribute_quanta_tenths(total_minutes: int, weights: List[float]) -> List[int]: + """ + Распределение «квантов» по 0.1 часа (6 минут) методом наибольших остатков. + Требует, чтобы total_minutes делилось на 6. + """ + step_min = 6 # 0.1 часа = 6 минут + if total_minutes % step_min != 0: + raise ValueError("Для режима десятых часа общее время должно делиться на 6 без остатка.") + + total_quanta = total_minutes // step_min # всего десятых (квантов) + if not weights or any(w < 0 for w in weights): + raise ValueError("Некорректные веса.") + s = sum(weights) or float(len(weights)) + + quotas = [total_quanta * (w / s) for w in weights] + base = [int(q) for q in quotas] + allocated = sum(base) + remainder = total_quanta - allocated + + # Метод наибольших остатков по дробным частям квантов + order = sorted(range(len(weights)), key=lambda i: (quotas[i] - base[i]), reverse=True) + for i in range(remainder): + base[order[i]] += 1 + + # Переводим кванты обратно в минуты + return [q * step_min for q in base] + +# ------------ Таблица ------------ + +def render_table(rows: List[Tuple[int, str, float, int, Optional[str]]], + show_decimals: bool, + dec_step: Optional[str]) -> str: + """ + rows: [(idx, name, percent, minutes, dec_str)] + """ + idx_w = max(len("#"), max((len(str(r[0])) for r in rows), default=1)) + name_w = max(len("Задача"), max((len(r[1]) for r in rows), default=5)) + perc_w = len("%") + time_w = len("ЧЧ:ММ") + min_w = len("Мин") + + header = f"{'#'.rjust(idx_w)} {'Задача'.ljust(name_w)} {'%'.rjust(perc_w)} {'ЧЧ:ММ'.rjust(time_w)} {'Мин'.rjust(min_w)}" + if show_decimals and dec_step: + dec_hdr = "Часы(" + dec_step + ")" + dec_w = max(len(dec_hdr), max((len(r[4]) for r in rows if r[4] is not None), default=len(dec_hdr))) + header += f" {dec_hdr.rjust(dec_w)}" + sep = "-" * len(header) + lines = [header, sep] + + if show_decimals and dec_step: + dec_w = len(header) - len(f"{'#'.rjust(idx_w)} {'Задача'.ljust(name_w)} {'%'.rjust(perc_w)} {'ЧЧ:ММ'.rjust(time_w)} {'Мин'.rjust(min_w)} ") + for idx, name, percent, minutes, dec_str in rows: + lines.append( + f"{str(idx).rjust(idx_w)} {name.ljust(name_w)} {percent:>5.1f} {format_hhmm(minutes):>{time_w}} {str(minutes).rjust(min_w)} {dec_str.rjust(dec_w) if dec_str else ''.rjust(dec_w)}" + ) + else: + for idx, name, percent, minutes, _ in rows: + lines.append( + f"{str(idx).rjust(idx_w)} {name.ljust(name_w)} {percent:>5.1f} {format_hhmm(minutes):>{time_w}} {str(minutes).rjust(min_w)}" + ) + return "\n".join(lines) + +# ------------ Главная логика ------------ + +def main(): + print("Распределение времени по задачам (консольная утилита)\n") + + # 1) Общее время + while True: + total_str = input("Сколько часов всего? (напр., 8 | 7.5 | 7:30): ").strip() + try: + total_minutes = parse_total_minutes(total_str) + if total_minutes <= 0: + print("Время должно быть > 0.") + continue + break + except Exception as e: + print(f"Ошибка: {e}") + + # 2) Количество задач + n_tasks = ask_int("Сколько задач? (целое > 0): ", min_value=1) + + # 3) Режим распределения + mode = "" + while mode not in ("e", "w"): + mode = input("Распределить поровну (e) или по весам/приоритетам (w)? [e/w]: ").strip().lower() or "e" + + names: List[str] = [] + weights: List[float] = [] + + print() + if mode == "e": + print("Ввод названий задач (можно пусто — будет Task N).") + for i in range(1, n_tasks + 1): + name = ask_nonempty(f"Название задачи {i}: ", default=f"Task {i}") + names.append(name) + weights = [1.0] * n_tasks + else: + print("Ввод названий и весов (важности) задач; чем больше вес, тем больше времени.") + for i in range(1, n_tasks + 1): + name = ask_nonempty(f"Название задачи {i}: ", default=f"Task {i}") + weight = ask_float(f"Вес/приоритет для «{name}» (по умолчанию 1): ", min_value=0.0, default=1.0) + names.append(name) + weights.append(weight) + + # 4) Выбор режима распределения минут + quant_tenths = (input("Квантовать по 0.1 часа (6 мин) для точной суммы по десятым? [y/N]: ").strip().lower() or "n") == "y" + used_quant_tenths = False + if quant_tenths: + try: + minutes_by_task = distribute_quanta_tenths(total_minutes, weights) + used_quant_tenths = True + except Exception as e: + print(f"Предупреждение: {e} — переключаюсь на распределение по минутам без квантования.") + minutes_by_task = distribute_by_weights(total_minutes, weights) + else: + minutes_by_task = distribute_by_weights(total_minutes, weights) + + # 5) Настройка вывода десятичных часов + show_dec = (input("Показывать десятичные часы? [y/N]: ").strip().lower() or "n") == "y" + dec_step: Optional[str] = None + if show_dec: + choice = (input("Формат десятичных часов: 1) 0.01 (сотые), 2) 0.1 (десятые, 6 мин) [1/2, по умолчанию 1]: ").strip() or "1") + dec_step = "0.01" if choice == "1" else "0.1" + + # 6) Готовим строки таблицы + rows: List[Tuple[int, str, float, int, Optional[str]]] = [] + for i, (name, mins) in enumerate(zip(names, minutes_by_task), start=1): + percent = (mins / total_minutes) * 100 if total_minutes > 0 else 0.0 + rows.append((i, name, percent, mins, None)) + + # 7) Балансированный вывод десятичных часов (если требуется) + if show_dec and dec_step is not None: + # Для любого режима распределения делаем балансировку отображения, + # чтобы сумма десят. часов ровно совпала с общим временем в выбранном формате. + dec_list = balanced_decimal_hours([r[3] for r in rows], total_minutes, step=dec_step) + rows = [(r[0], r[1], r[2], r[3], dec_list[idx]) for idx, r in enumerate(rows)] + + print("\nИтоговая таблица:\n") + print(render_table(rows, show_dec, dec_step)) + + # 8) Контрольные итоги + sum_minutes = sum(r[3] for r in rows) + print("\nИтоги:") + print(f"- Всего: {format_hhmm(total_minutes)} ({total_minutes} мин)") + print(f"- Сумма по задачам: {format_hhmm(sum_minutes)} ({sum_minutes} мин)") + if show_dec and dec_step is not None: + # Показать суммарные десятичные часы + total_dec = minutes_to_decimal_hours_half_up(total_minutes, dec_step) + sum_dec = (sum(Decimal(r[4]) for r in rows if r[4] is not None)) if rows and rows[0][4] is not None else total_dec + if dec_step == "0.01": + print(f"- Десятичные часы (сумма): {sum_dec:.2f} (из {total_dec:.2f})") + else: + print(f"- Десятичные часы (сумма): {sum_dec:.1f} (из {total_dec:.1f})") + delta = sum_minutes - total_minutes + if delta == 0: + print("- Баланс минут: OK") + else: + sign = "+" if delta > 0 else "-" + print(f"- Баланс минут: {sign}{abs(delta)} мин (проверьте веса/округление)") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nПрервано пользователем.") + sys.exit(130)