Create Time.py

This commit is contained in:
smolkik-code
2025-10-22 16:58:01 +07:00
committed by GitHub
parent f539293b6d
commit 9a39cfa2ef

314
Time.py Normal file
View File

@@ -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)