Create Time.py
This commit is contained in:
314
Time.py
Normal file
314
Time.py
Normal 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)
|
||||
Reference in New Issue
Block a user