diff --git a/hitfactorpy/parsers/match_report/fields.py b/hitfactorpy/parsers/match_report/fields.py index 3f78e92..60e434c 100644 --- a/hitfactorpy/parsers/match_report/fields.py +++ b/hitfactorpy/parsers/match_report/fields.py @@ -93,6 +93,13 @@ def parse_power_factor(s: str) -> PowerFactor: return PowerFactor.UNKNOWN +def parse_power_factor_default_none(s: str | None) -> PowerFactor | None: + if s and (parsed_power_factor := parse_power_factor(s)): + if parsed_power_factor and parsed_power_factor != PowerFactor.UNKNOWN: + return parsed_power_factor + return None + + def parse_member_number(s: str): return re.sub(r"[^0-9A-Z]", "", _sanitize_string(s).upper()) diff --git a/hitfactorpy/parsers/match_report/models.py b/hitfactorpy/parsers/match_report/models.py index 09de03f..42bef59 100644 --- a/hitfactorpy/parsers/match_report/models.py +++ b/hitfactorpy/parsers/match_report/models.py @@ -38,8 +38,10 @@ class ParsedStage: class ParsedStageScore: """Stage score parsed from match report""" - competitor_id: Optional[int] = None stage_id: Optional[int] = None + competitor_id: Optional[int] = None + dq: bool = False + dnf: bool = False a: int = 0 b: int = 0 c: int = 0 @@ -58,8 +60,13 @@ class ParsedStageScore: t4: float = 0.0 t5: float = 0.0 time: float = 0.0 - dq: bool = False - dnf: bool = False + raw_points: Optional[float] = None + penalty_points: Optional[float] = None + total_points: Optional[float] = None + hit_factor: Optional[float] = None + stage_points: Optional[float] = None + stage_place: Optional[int] = None + stage_power_factor: Optional[PowerFactor] = None @dataclass(frozen=True) diff --git a/hitfactorpy/parsers/match_report/pandas/stage_score.py b/hitfactorpy/parsers/match_report/pandas/stage_score.py index 0552c8c..f284025 100644 --- a/hitfactorpy/parsers/match_report/pandas/stage_score.py +++ b/hitfactorpy/parsers/match_report/pandas/stage_score.py @@ -7,7 +7,7 @@ from pandas._typing import FilePath, ReadCsvBuffer from pandas.errors import EmptyDataError -from ..fields import parse_boolean, parse_power_factor +from ..fields import parse_boolean, parse_power_factor_default_none from ..models import ParsedStageScore _logger = logging.getLogger(__name__) @@ -28,6 +28,8 @@ class StageScoreColumnName(str, Enum): M = "Miss" NS = "No Shoot" PROC = "Procedural" + DOUBLE_POPPERS = "Double Poppers" + DOUBLE_POPPER_MISS = "Double Popper Miss" LATE_SHOT = "Late Shot" EXTRA_SHOT = "Extra Shot" EXTRA_HIT = "Extra Hit" @@ -42,13 +44,16 @@ class StageScoreColumnName(str, Enum): TIME = "Time" RAW_POINTS = "Raw Points" TOTAL_POINTS = "Total Points" + HIT_FACTOR = "Hit Factor" + STAGE_POINTS = "Stage Points" + STAGE_PLACE = "Stage Place" STAGE_POWER_FACTOR = "Stage Power Factor" CSV_CONVERTERS: Mapping[str, Callable[[str], Any]] = { StageScoreColumnName.DQ: parse_boolean, StageScoreColumnName.DNF: parse_boolean, - StageScoreColumnName.STAGE_POWER_FACTOR: parse_power_factor, + StageScoreColumnName.STAGE_POWER_FACTOR: parse_power_factor_default_none, } @@ -59,7 +64,6 @@ def read_stage_scores_csv(filepath_or_buffer: FilePath | ReadCsvBuffer[bytes] | index_col=None, converters=CSV_CONVERTERS, ) - # df['hit_factor'] = (df['A'] * 5) # TODO return df @@ -75,6 +79,8 @@ def parse_stage_scores(stage_scores_csv: str) -> List[ParsedStageScore]: ParsedStageScore( stage_id=stage_id, competitor_id=competitor_id, + dq=dq, + dnf=dnf, a=a, b=b, c=c, @@ -93,12 +99,19 @@ def parse_stage_scores(stage_scores_csv: str) -> List[ParsedStageScore]: t4=t4, t5=t5, time=time, - dq=dq, - dnf=dnf, + raw_points=raw_points, + penalty_points=penalty_points, + total_points=total_points, + hit_factor=hit_factor, + stage_points=stage_points, + stage_place=stage_place, + stage_power_factor=stage_power_factor, ) for ( stage_id, competitor_id, + dq, + dnf, a, b, c, @@ -117,11 +130,18 @@ def parse_stage_scores(stage_scores_csv: str) -> List[ParsedStageScore]: t4, t5, time, - dq, - dnf, + raw_points, + penalty_points, + total_points, + hit_factor, + stage_points, + stage_place, + stage_power_factor, ) in zip( df[StageScoreColumnName.STAGE_ID], df[StageScoreColumnName.COMPETITOR_ID], + df[StageScoreColumnName.DQ], + df[StageScoreColumnName.DNF], df[StageScoreColumnName.A], df[StageScoreColumnName.B], df[StageScoreColumnName.C], @@ -140,8 +160,13 @@ def parse_stage_scores(stage_scores_csv: str) -> List[ParsedStageScore]: df[StageScoreColumnName.T4], df[StageScoreColumnName.T5], df[StageScoreColumnName.TIME], - df[StageScoreColumnName.DQ], - df[StageScoreColumnName.DNF], + df[StageScoreColumnName.RAW_POINTS], + df[StageScoreColumnName.PENALTY_POINTS], + df[StageScoreColumnName.TOTAL_POINTS], + df[StageScoreColumnName.HIT_FACTOR], + df[StageScoreColumnName.STAGE_POINTS], + df[StageScoreColumnName.STAGE_PLACE], + df[StageScoreColumnName.STAGE_POWER_FACTOR], ) ] return stage_scores diff --git a/hitfactorpy/parsers/match_report/strict/stage_score.py b/hitfactorpy/parsers/match_report/strict/stage_score.py index 0ce23fd..4fc18ca 100644 --- a/hitfactorpy/parsers/match_report/strict/stage_score.py +++ b/hitfactorpy/parsers/match_report/strict/stage_score.py @@ -2,8 +2,9 @@ from enum import Enum, unique from typing import List, Optional +from ....enums import PowerFactor from ...csv_utils import parse_csv_row, parse_float_value, parse_int_value -from ..fields import parse_boolean +from ..fields import parse_boolean, parse_power_factor, parse_power_factor_default_none from ..models import ParsedStageScore _logger = logging.getLogger(__name__) @@ -24,6 +25,8 @@ class StageScoreColumn(int, Enum): M = 9 NS = 10 PROC = 11 + DOUBLE_POPPERS = 12 + DOUBLE_POPPER_MISS = 13 LATE_SHOT = 14 EXTRA_SHOT = 15 EXTRA_HIT = 16 @@ -38,6 +41,10 @@ class StageScoreColumn(int, Enum): TIME = 25 RAW_POINTS = 26 TOTAL_POINTS = 27 + HIT_FACTOR = 28 + STAGE_POINTS = 29 + STAGE_PLACE = 30 + STAGE_POWER_FACTOR = 31 def check_stage_score_columns(parsed_columns: List[str], fail_on_mismatch=False): @@ -61,9 +68,10 @@ def parse_match_report_stage_score_lines( for line in stage_lines: row = parse_csv_row(line) stage = ParsedStageScore( - # Integer attributes stage_id=parse_int_value(row[StageScoreColumn.STAGE_ID].strip()), competitor_id=parse_int_value(row[StageScoreColumn.SHOOTER_ID].strip()), + dq=parse_boolean(row[StageScoreColumn.DQ].strip()), + dnf=parse_boolean(row[StageScoreColumn.DNF].strip()), a=parse_int_value(row[StageScoreColumn.A].strip()) or 0, b=parse_int_value(row[StageScoreColumn.B].strip()) or 0, c=parse_int_value(row[StageScoreColumn.C].strip()) or 0, @@ -76,16 +84,19 @@ def parse_match_report_stage_score_lines( extra_shot=parse_int_value(row[StageScoreColumn.EXTRA_SHOT].strip()) or 0, extra_hit=parse_int_value(row[StageScoreColumn.EXTRA_HIT].strip()) or 0, other_penalty=parse_int_value(row[StageScoreColumn.OTHER_PENALTY].strip()) or 0, - # Float attributes t1=parse_float_value(row[StageScoreColumn.T1].strip()) or 0.0, t2=parse_float_value(row[StageScoreColumn.T2].strip()) or 0.0, t3=parse_float_value(row[StageScoreColumn.T3].strip()) or 0.0, t4=parse_float_value(row[StageScoreColumn.T4].strip()) or 0.0, t5=parse_float_value(row[StageScoreColumn.T5].strip()) or 0.0, time=parse_float_value(row[StageScoreColumn.TIME].strip()) or 0.0, - # Boolean attributes - dq=parse_boolean(row[StageScoreColumn.DQ].strip()), - dnf=parse_boolean(row[StageScoreColumn.DNF].strip()), + raw_points=parse_float_value(row[StageScoreColumn.RAW_POINTS].strip()), + penalty_points=parse_float_value(row[StageScoreColumn.PENALTY_POINTS].strip()), + total_points=parse_float_value(row[StageScoreColumn.TOTAL_POINTS].strip()), + hit_factor=parse_float_value(row[StageScoreColumn.HIT_FACTOR].strip()), + stage_points=parse_float_value(row[StageScoreColumn.STAGE_POINTS].strip()), + stage_place=parse_int_value(row[StageScoreColumn.STAGE_PLACE].strip()), + stage_power_factor=parse_power_factor_default_none(row[StageScoreColumn.STAGE_POWER_FACTOR].strip()), ) stages.append(stage) return stages diff --git a/tests/test_parsers_match_report/shared.py b/tests/test_parsers_match_report/shared.py index a844185..4f5485f 100644 --- a/tests/test_parsers_match_report/shared.py +++ b/tests/test_parsers_match_report/shared.py @@ -71,8 +71,15 @@ def assert_example_match_report(report: ParsedMatchReport): assert report.stage_scores[0].t4 == approx(0) assert report.stage_scores[0].t5 == approx(0) assert report.stage_scores[0].time == approx(17.34) + assert report.stage_scores[0].raw_points == approx(150.0) + assert report.stage_scores[0].penalty_points == approx(0) + assert report.stage_scores[0].total_points == approx(150.0) + assert report.stage_scores[0].hit_factor == approx(8.6505) + assert report.stage_scores[0].stage_points == approx(160.0) + assert report.stage_scores[0].stage_place == 1 assert report.stage_scores[0].dq is False assert report.stage_scores[0].dnf is False + assert report.stage_scores[0].stage_power_factor is None # Verify a stage score with a DQ assert report.stage_scores[37].dq is True diff --git a/tests/test_parsers_match_report/test_field_parsers.py b/tests/test_parsers_match_report/test_field_parsers.py index 3e2fe45..5438987 100644 --- a/tests/test_parsers_match_report/test_field_parsers.py +++ b/tests/test_parsers_match_report/test_field_parsers.py @@ -114,6 +114,20 @@ def test_parse_power_factor(test_input, expected): assert fields.parse_power_factor(test_input) == expected +@pytest.mark.parametrize( + "test_input,expected", + [ + ("mAjOr", PowerFactor.MAJOR), + ("minor", PowerFactor.MINOR), + ("", None), + (None, None), + ("y", None), + ], +) +def test_parse_power_factor_default_none(test_input, expected): + assert fields.parse_power_factor_default_none(test_input) == expected + + @pytest.mark.parametrize( "test_input,expected", [