refactor: одна функция метрики норматива времени для карточки 112 и карточки ДДС
This commit is contained in:
parent
36a9862a94
commit
a11206527a
2 changed files with 141 additions and 1 deletions
|
|
@ -5,9 +5,94 @@
|
||||||
нулевое время даёт полный вклад, два норматива и более — нулевой.
|
нулевое время даёт полный вклад, два норматива и более — нулевой.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from app.domain.events import Metric
|
||||||
|
from app.domain.taxonomy import Competency, ErrorCode, Finding, FindingSource
|
||||||
|
from app.scoring.taxonomy import METRIC_WEIGHTS
|
||||||
|
|
||||||
|
TIME_REF = "ТЗ задачи № 9; сессия вопросов и ответов 16.09.2026"
|
||||||
|
|
||||||
|
|
||||||
def time_credit(elapsed_ms: int | None, limit_ms: int) -> float:
|
def time_credit(elapsed_ms: int | None, limit_ms: int) -> float:
|
||||||
"""Доля веса временной метрики, 0–1; отсутствие доказанного времени — 0."""
|
"""Доля веса временной метрики, 0–1; отсутствие доказанного времени — 0."""
|
||||||
if elapsed_ms is None or elapsed_ms < 0 or limit_ms <= 0:
|
if elapsed_ms is None or elapsed_ms < 0 or limit_ms <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return round(max(0.0, min(1.0, 1.0 - elapsed_ms / (2 * limit_ms))), 4)
|
return round(max(0.0, min(1.0, 1.0 - elapsed_ms / (2 * limit_ms))), 4)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TimeNorm:
|
||||||
|
"""Формулировки норматива времени: карточка 112 и карточка ДДС различаются
|
||||||
|
только словами, расчёт и отметка E3 у них общие."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
title: str
|
||||||
|
summary: str
|
||||||
|
action: str
|
||||||
|
unmeasured: str
|
||||||
|
incomplete: str
|
||||||
|
|
||||||
|
|
||||||
|
CARD_FILL_TIME = TimeNorm(
|
||||||
|
key="card_fill_time",
|
||||||
|
title="Время заполнения карточки",
|
||||||
|
summary="Время заполнения карточки: норматив не выполнен",
|
||||||
|
action="сдать карточку",
|
||||||
|
unmeasured="время не зафиксировано",
|
||||||
|
incomplete="карточка не сдана",
|
||||||
|
)
|
||||||
|
DDS_WORK_TIME = TimeNorm(
|
||||||
|
key="dds_work_time",
|
||||||
|
title="Отработка карточки ДДС",
|
||||||
|
summary="ДДС: норматив времени отработки карточки не выполнен",
|
||||||
|
action="завершить",
|
||||||
|
unmeasured="время обработки не зафиксировано",
|
||||||
|
incomplete="карточка не завершена",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def time_metric(
|
||||||
|
norm: TimeNorm, elapsed_ms: int | None, limit_ms: int, *, completed: bool
|
||||||
|
) -> tuple[Metric, Finding | None]:
|
||||||
|
"""Метрика норматива и отметка E3, если он не выполнен.
|
||||||
|
|
||||||
|
Незавершённая работа не получает временного вклада: быстрое, но брошенное
|
||||||
|
заполнение не должно выглядеть скоростью.
|
||||||
|
"""
|
||||||
|
if elapsed_ms is None:
|
||||||
|
fact = norm.unmeasured
|
||||||
|
else:
|
||||||
|
delta_ms = elapsed_ms - limit_ms
|
||||||
|
if delta_ms > 0:
|
||||||
|
deviation = f"+{round(delta_ms / 1000)} с сверх норматива"
|
||||||
|
elif delta_ms < 0:
|
||||||
|
deviation = f"на {round(abs(delta_ms) / 1000)} с быстрее норматива"
|
||||||
|
else:
|
||||||
|
deviation = "точно в норматив"
|
||||||
|
fact = f"{round(elapsed_ms / 1000)} с ({deviation})"
|
||||||
|
if not completed:
|
||||||
|
fact = f"{norm.incomplete}; {fact}"
|
||||||
|
norm_text = f"{norm.action} за {round(limit_ms / 1000)} с"
|
||||||
|
passed = completed and elapsed_ms is not None and elapsed_ms <= limit_ms
|
||||||
|
metric = Metric(
|
||||||
|
key=norm.key,
|
||||||
|
title=norm.title,
|
||||||
|
fact=fact,
|
||||||
|
norm=norm_text,
|
||||||
|
ref=TIME_REF,
|
||||||
|
passed=passed,
|
||||||
|
weight=METRIC_WEIGHTS[norm.key],
|
||||||
|
credit=time_credit(elapsed_ms, limit_ms) if completed else 0.0,
|
||||||
|
)
|
||||||
|
if passed:
|
||||||
|
return metric, None
|
||||||
|
return metric, Finding(
|
||||||
|
code=ErrorCode.E3,
|
||||||
|
source=FindingSource.TIMERS,
|
||||||
|
summary=norm.summary,
|
||||||
|
fact=fact,
|
||||||
|
norm=norm_text,
|
||||||
|
ref=TIME_REF,
|
||||||
|
competency=Competency.NORMS,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.domain.events import Metric
|
from app.domain.events import Metric
|
||||||
|
from app.domain.taxonomy import Competency, ErrorCode, FindingSource
|
||||||
from app.scoring.competency import radar
|
from app.scoring.competency import radar
|
||||||
from app.scoring.gost import GostResult
|
from app.scoring.gost import GostResult
|
||||||
from app.scoring.timing import time_credit
|
from app.scoring.timing import CARD_FILL_TIME, DDS_WORK_TIME, time_credit, time_metric
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -23,3 +24,57 @@ def test_fractional_timing_credit_affects_total_and_competency_scores():
|
||||||
result = GostResult(metrics=[metric])
|
result = GostResult(metrics=[metric])
|
||||||
assert result.score == 75.0
|
assert result.score == 75.0
|
||||||
assert radar([metric])[0].value == 0.75
|
assert radar([metric])[0].value == 0.75
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("elapsed_ms", "completed", "fact", "passed"),
|
||||||
|
[
|
||||||
|
(200_000, True, "200 с (+20 с сверх норматива)", False),
|
||||||
|
(61_000, True, "61 с (на 119 с быстрее норматива)", True),
|
||||||
|
(180_000, True, "180 с (точно в норматив)", True),
|
||||||
|
(None, True, "время не зафиксировано", False),
|
||||||
|
(30_000, False, "карточка не сдана; 30 с (на 150 с быстрее норматива)", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_card_fill_time_metric_texts(elapsed_ms, completed, fact, passed):
|
||||||
|
metric, finding = time_metric(CARD_FILL_TIME, elapsed_ms, 180_000, completed=completed)
|
||||||
|
|
||||||
|
assert metric.key == "card_fill_time"
|
||||||
|
assert metric.title == "Время заполнения карточки"
|
||||||
|
assert metric.fact == fact
|
||||||
|
assert metric.norm == "сдать карточку за 180 с"
|
||||||
|
assert metric.ref == "ТЗ задачи № 9; сессия вопросов и ответов 16.09.2026"
|
||||||
|
assert metric.passed is passed
|
||||||
|
assert metric.credit == (time_credit(elapsed_ms, 180_000) if completed else 0.0)
|
||||||
|
if passed:
|
||||||
|
assert finding is None
|
||||||
|
else:
|
||||||
|
assert finding.code is ErrorCode.E3
|
||||||
|
assert finding.source is FindingSource.TIMERS
|
||||||
|
assert finding.competency is Competency.NORMS
|
||||||
|
assert finding.summary == "Время заполнения карточки: норматив не выполнен"
|
||||||
|
assert (finding.fact, finding.norm) == (fact, "сдать карточку за 180 с")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("elapsed_ms", "completed", "fact", "passed"),
|
||||||
|
[
|
||||||
|
(605_000, True, "605 с (+425 с сверх норматива)", False),
|
||||||
|
(25_000, True, "25 с (на 155 с быстрее норматива)", True),
|
||||||
|
(180_000, True, "180 с (точно в норматив)", True),
|
||||||
|
(None, False, "карточка не завершена; время обработки не зафиксировано", False),
|
||||||
|
(25_000, False, "карточка не завершена; 25 с (на 155 с быстрее норматива)", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_dds_work_time_metric_texts(elapsed_ms, completed, fact, passed):
|
||||||
|
metric, finding = time_metric(DDS_WORK_TIME, elapsed_ms, 180_000, completed=completed)
|
||||||
|
|
||||||
|
assert metric.key == "dds_work_time"
|
||||||
|
assert metric.title == "Отработка карточки ДДС"
|
||||||
|
assert metric.fact == fact
|
||||||
|
assert metric.norm == "завершить за 180 с"
|
||||||
|
assert metric.passed is passed
|
||||||
|
assert (finding is None) is passed
|
||||||
|
if finding is not None:
|
||||||
|
assert finding.summary == "ДДС: норматив времени отработки карточки не выполнен"
|
||||||
|
assert (finding.fact, finding.norm) == (fact, "завершить за 180 с")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue