diff --git a/src/arcaea_offline/utils/formatters/__init__.py b/src/arcaea_offline/utils/formatters/__init__.py new file mode 100644 index 0000000..3c27b5d --- /dev/null +++ b/src/arcaea_offline/utils/formatters/__init__.py @@ -0,0 +1,2 @@ +from .play_result import PlayResultFormatter +from .rating_class import RatingClassFormatter diff --git a/src/arcaea_offline/utils/formatters/play_result.py b/src/arcaea_offline/utils/formatters/play_result.py new file mode 100644 index 0000000..c8d97db --- /dev/null +++ b/src/arcaea_offline/utils/formatters/play_result.py @@ -0,0 +1,143 @@ +from typing import Any, Literal, overload + +from arcaea_offline.constants.enums import ( + ArcaeaPlayResultClearType, + ArcaeaPlayResultModifier, +) +from arcaea_offline.constants.play_result import ScoreLowerLimits + + +class PlayResultFormatter: + SCORE_GRADE_FORMAT_RESULTS = Literal["EX+", "EX", "AA", "A", "B", "C", "D"] + + @staticmethod + def score_grade(score: int) -> SCORE_GRADE_FORMAT_RESULTS: + """ + Returns the score grade, e.g. EX+. + + Raises `ValueError` if the score is negative. + """ + if not isinstance(score, int): + raise TypeError(f"Unsupported type {type(score)}, cannot format") + + if score >= ScoreLowerLimits.EX_PLUS: + return "EX+" + elif score >= ScoreLowerLimits.EX: + return "EX" + elif score >= ScoreLowerLimits.AA: + return "AA" + elif score >= ScoreLowerLimits.A: + return "A" + elif score >= ScoreLowerLimits.B: + return "B" + elif score >= ScoreLowerLimits.C: + return "C" + elif score >= ScoreLowerLimits.D: + return "D" + else: + raise ValueError("score cannot be negative") + + CLEAR_TYPE_FORMAT_RESULTS = Literal[ + "TRACK LOST", + "NORMAL CLEAR", + "FULL RECALL", + "PURE MEMORY", + "EASY CLEAR", + "HARD CLEAR", + "UNKNOWN", + "None", + ] + + @overload + @classmethod + def clear_type( + cls, clear_type: ArcaeaPlayResultClearType + ) -> CLEAR_TYPE_FORMAT_RESULTS: + """ + Returns the uppercased clear type name, e.g. NORMAL CLEAR. + """ + ... + + @overload + @classmethod + def clear_type(cls, clear_type: int) -> CLEAR_TYPE_FORMAT_RESULTS: + """ + Returns the uppercased clear type name, e.g. NORMAL CLEAR. + + The integer will be converted to `ArcaeaPlayResultClearType` enum, + and will return "UNKNOWN" if the convertion fails. + + Raises `ValueError` if the integer is negative. + """ + ... + + @overload + @classmethod + def clear_type(cls, clear_type: None) -> CLEAR_TYPE_FORMAT_RESULTS: + """ + Returns "None" + """ + ... + + @classmethod + def clear_type(cls, clear_type: Any) -> CLEAR_TYPE_FORMAT_RESULTS: + if clear_type is None: + return "None" + elif isinstance(clear_type, ArcaeaPlayResultClearType): + return clear_type.name.replace("_", " ").upper() # type: ignore + elif isinstance(clear_type, int): + if clear_type < 0: + raise ValueError("clear_type cannot be negative") + try: + return cls.clear_type(ArcaeaPlayResultClearType(clear_type)) + except ValueError: + return "UNKNOWN" + else: + raise TypeError(f"Unsupported type {type(clear_type)}, cannot format") + + MODIFIER_FORMAT_RESULTS = Literal["NORMAL", "EASY", "HARD", "UNKNOWN", "None"] + + @overload + @classmethod + def modifier(cls, modifier: ArcaeaPlayResultModifier) -> MODIFIER_FORMAT_RESULTS: + """ + Returns the uppercased clear type name, e.g. NORMAL CLEAR. + """ + ... + + @overload + @classmethod + def modifier(cls, modifier: int) -> MODIFIER_FORMAT_RESULTS: + """ + Returns the uppercased clear type name, e.g. NORMAL CLEAR. + + The integer will be converted to `ArcaeaPlayResultModifier` enum, + and will return "UNKNOWN" if the convertion fails. + + Raises `ValueError` if the integer is negative. + """ + ... + + @overload + @classmethod + def modifier(cls, modifier: None) -> MODIFIER_FORMAT_RESULTS: + """ + Returns "None" + """ + ... + + @classmethod + def modifier(cls, modifier: Any) -> MODIFIER_FORMAT_RESULTS: + if modifier is None: + return "None" + elif isinstance(modifier, ArcaeaPlayResultModifier): + return modifier.name + elif isinstance(modifier, int): + if modifier < 0: + raise ValueError("modifier cannot be negative") + try: + return cls.modifier(ArcaeaPlayResultModifier(modifier)) + except ValueError: + return "UNKNOWN" + else: + raise TypeError(f"Unsupported type {type(modifier)}, cannot format") diff --git a/src/arcaea_offline/utils/formatters/rating_class.py b/src/arcaea_offline/utils/formatters/rating_class.py new file mode 100644 index 0000000..38065b5 --- /dev/null +++ b/src/arcaea_offline/utils/formatters/rating_class.py @@ -0,0 +1,83 @@ +from typing import Any, Literal, overload + +from arcaea_offline.constants.enums import ArcaeaRatingClass + + +class RatingClassFormatter: + abbreviations = { + ArcaeaRatingClass.PAST: "PST", + ArcaeaRatingClass.PRESENT: "PRS", + ArcaeaRatingClass.FUTURE: "FTR", + ArcaeaRatingClass.BEYOND: "BYD", + ArcaeaRatingClass.ETERNAL: "ETR", + } + + NAME_FORMAT_RESULTS = Literal[ + "Past", "Present", "Future", "Beyond", "Eternal", "Unknown" + ] + + @overload + @classmethod + def name(cls, rating_class: ArcaeaRatingClass) -> NAME_FORMAT_RESULTS: + """ + Returns the capitalized rating class name, e.g. Future. + """ + ... + + @overload + @classmethod + def name(cls, rating_class: int) -> NAME_FORMAT_RESULTS: + """ + Returns the capitalized rating class name, e.g. Future. + + The integer will be converted to `ArcaeaRatingClass` enum, + and will return "Unknown" if the convertion fails. + """ + ... + + @classmethod + def name(cls, rating_class: Any) -> NAME_FORMAT_RESULTS: + if isinstance(rating_class, ArcaeaRatingClass): + return rating_class.name.lower().capitalize() # type: ignore + elif isinstance(rating_class, int): + try: + return cls.name(ArcaeaRatingClass(rating_class)) + except ValueError: + return "Unknown" + else: + raise TypeError(f"Unsupported type: {type(rating_class)}, cannot format") + + ABBREVIATION_FORMAT_RESULTS = Literal["PST", "PRS", "FTR", "BYD", "ETR", "UNK"] + + @overload + @classmethod + def abbreviation( + cls, rating_class: ArcaeaRatingClass + ) -> ABBREVIATION_FORMAT_RESULTS: + """ + Returns the uppercased rating class name, e.g. FTR. + """ + ... + + @overload + @classmethod + def abbreviation(cls, rating_class: int) -> ABBREVIATION_FORMAT_RESULTS: + """ + Returns the uppercased rating class name, e.g. FTR. + + The integer will be converted to `ArcaeaRatingClass` enum, + and will return "UNK" if the convertion fails. + """ + ... + + @classmethod + def abbreviation(cls, rating_class: Any) -> ABBREVIATION_FORMAT_RESULTS: + if isinstance(rating_class, ArcaeaRatingClass): + return cls.abbreviations[rating_class] # type: ignore + elif isinstance(rating_class, int): + try: + return cls.abbreviation(ArcaeaRatingClass(rating_class)) + except ValueError: + return "UNK" + else: + raise TypeError(f"Unsupported type: {type(rating_class)}, cannot format") diff --git a/src/arcaea_offline/utils/rating.py b/src/arcaea_offline/utils/rating.py deleted file mode 100644 index 9d2d539..0000000 --- a/src/arcaea_offline/utils/rating.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Optional - -RATING_CLASS_TEXT_MAP = { - 0: "Past", - 1: "Present", - 2: "Future", - 3: "Beyond", - 4: "Eternal", -} - -RATING_CLASS_SHORT_TEXT_MAP = { - 0: "PST", - 1: "PRS", - 2: "FTR", - 3: "BYD", - 4: "ETR", -} - - -def rating_class_to_text(rating_class: int) -> Optional[str]: - return RATING_CLASS_TEXT_MAP.get(rating_class) - - -def rating_class_to_short_text(rating_class: int) -> Optional[str]: - return RATING_CLASS_SHORT_TEXT_MAP.get(rating_class) diff --git a/src/arcaea_offline/utils/score.py b/src/arcaea_offline/utils/score.py deleted file mode 100644 index 394055f..0000000 --- a/src/arcaea_offline/utils/score.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any, Sequence - -SCORE_GRADE_FLOOR = [9900000, 9800000, 9500000, 9200000, 8900000, 8600000, 0] -SCORE_GRADE_TEXTS = ["EX+", "EX", "AA", "A", "B", "C", "D"] -MODIFIER_TEXTS = ["NORMAL", "EASY", "HARD"] -CLEAR_TYPE_TEXTS = [ - "TRACK LOST", - "NORMAL CLEAR", - "FULL RECALL", - "PURE MEMORY", - "EASY CLEAR", - "HARD CLEAR", -] - - -def zip_score_grade(score: int, __seq: Sequence, default: Any = "__PRESERVE__"): - """ - zip_score_grade is a simple wrapper that equals to: - ```py - for score_floor, val in zip(SCORE_GRADE_FLOOR, __seq): - if score >= score_floor: - return val - return seq[-1] if default == "__PRESERVE__" else default - ``` - Could be useful in specific cases. - """ - return next( - ( - val - for score_floor, val in zip(SCORE_GRADE_FLOOR, __seq) - if score >= score_floor - ), - __seq[-1] if default == "__PRESERVE__" else default, - ) - - -def score_to_grade_text(score: int) -> str: - return zip_score_grade(score, SCORE_GRADE_TEXTS) - - -def modifier_to_text(modifier: int) -> str: - return MODIFIER_TEXTS[modifier] - - -def clear_type_to_text(clear_type: int) -> str: - return CLEAR_TYPE_TEXTS[clear_type] diff --git a/src/arcaea_offline/utils/search_title.py b/src/arcaea_offline/utils/search_title.py deleted file mode 100644 index 4b22e1e..0000000 --- a/src/arcaea_offline/utils/search_title.py +++ /dev/null @@ -1,6 +0,0 @@ -import json -from typing import List, Optional - - -def recover_search_title(db_value: Optional[str]) -> List[str]: - return json.loads(db_value) if db_value else [] diff --git a/tests/utils/test_formatters.py b/tests/utils/test_formatters.py new file mode 100644 index 0000000..3e35832 --- /dev/null +++ b/tests/utils/test_formatters.py @@ -0,0 +1,126 @@ +import pytest + +from arcaea_offline.constants.enums import ( + ArcaeaPlayResultClearType, + ArcaeaPlayResultModifier, + ArcaeaRatingClass, +) +from arcaea_offline.utils.formatters.play_result import PlayResultFormatter +from arcaea_offline.utils.formatters.rating_class import RatingClassFormatter + + +class TestRatingClassFormatter: + def test_name(self): + assert RatingClassFormatter.name(ArcaeaRatingClass.PAST) == "Past" + assert RatingClassFormatter.name(ArcaeaRatingClass.PRESENT) == "Present" + assert RatingClassFormatter.name(ArcaeaRatingClass.FUTURE) == "Future" + assert RatingClassFormatter.name(ArcaeaRatingClass.BEYOND) == "Beyond" + assert RatingClassFormatter.name(ArcaeaRatingClass.ETERNAL) == "Eternal" + + assert RatingClassFormatter.name(2) == "Future" + + assert RatingClassFormatter.name(100) == "Unknown" + assert RatingClassFormatter.name(-1) == "Unknown" + + pytest.raises(TypeError, RatingClassFormatter.name, "2") + pytest.raises(TypeError, RatingClassFormatter.name, []) + pytest.raises(TypeError, RatingClassFormatter.name, None) + + def test_abbreviation(self): + assert RatingClassFormatter.abbreviation(ArcaeaRatingClass.PAST) == "PST" + assert RatingClassFormatter.abbreviation(ArcaeaRatingClass.PRESENT) == "PRS" + assert RatingClassFormatter.abbreviation(ArcaeaRatingClass.FUTURE) == "FTR" + assert RatingClassFormatter.abbreviation(ArcaeaRatingClass.BEYOND) == "BYD" + assert RatingClassFormatter.abbreviation(ArcaeaRatingClass.ETERNAL) == "ETR" + + assert RatingClassFormatter.abbreviation(2) == "FTR" + + assert RatingClassFormatter.abbreviation(100) == "UNK" + assert RatingClassFormatter.abbreviation(-1) == "UNK" + + pytest.raises(TypeError, RatingClassFormatter.abbreviation, "2") + pytest.raises(TypeError, RatingClassFormatter.abbreviation, []) + pytest.raises(TypeError, RatingClassFormatter.abbreviation, None) + + +class TestPlayResultFormatter: + def test_score_grade(self): + assert PlayResultFormatter.score_grade(10001284) == "EX+" + assert PlayResultFormatter.score_grade(9989210) == "EX+" + assert PlayResultFormatter.score_grade(9900000) == "EX+" + + assert PlayResultFormatter.score_grade(9899999) == "EX" + assert PlayResultFormatter.score_grade(9843717) == "EX" + assert PlayResultFormatter.score_grade(9800000) == "EX" + + assert PlayResultFormatter.score_grade(9799999) == "AA" + assert PlayResultFormatter.score_grade(9794015) == "AA" + assert PlayResultFormatter.score_grade(9750000) == "AA" + + assert PlayResultFormatter.score_grade(9499999) == "A" + assert PlayResultFormatter.score_grade(9356855) == "A" + assert PlayResultFormatter.score_grade(9200000) == "A" + + assert PlayResultFormatter.score_grade(9199999) == "B" + assert PlayResultFormatter.score_grade(9065785) == "B" + assert PlayResultFormatter.score_grade(8900000) == "B" + + assert PlayResultFormatter.score_grade(8899999) == "C" + assert PlayResultFormatter.score_grade(8756211) == "C" + assert PlayResultFormatter.score_grade(8600000) == "C" + + assert PlayResultFormatter.score_grade(8599999) == "D" + assert PlayResultFormatter.score_grade(5500000) == "D" + assert PlayResultFormatter.score_grade(0) == "D" + + pytest.raises(ValueError, PlayResultFormatter.score_grade, -1) + pytest.raises(TypeError, PlayResultFormatter.score_grade, "10001284") + pytest.raises(TypeError, PlayResultFormatter.score_grade, []) + pytest.raises(TypeError, PlayResultFormatter.score_grade, None) + + def test_clear_type(self): + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.TRACK_LOST) + == "TRACK LOST" + ) + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.NORMAL_CLEAR) + == "NORMAL CLEAR" + ) + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.FULL_RECALL) + == "FULL RECALL" + ) + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.PURE_MEMORY) + == "PURE MEMORY" + ) + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.EASY_CLEAR) + == "EASY CLEAR" + ) + assert ( + PlayResultFormatter.clear_type(ArcaeaPlayResultClearType.HARD_CLEAR) + == "HARD CLEAR" + ) + assert PlayResultFormatter.clear_type(None) == "None" + + assert PlayResultFormatter.clear_type(1) == "NORMAL CLEAR" + assert PlayResultFormatter.clear_type(6) == "UNKNOWN" + + pytest.raises(ValueError, PlayResultFormatter.clear_type, -1) + pytest.raises(TypeError, PlayResultFormatter.clear_type, "1") + pytest.raises(TypeError, PlayResultFormatter.clear_type, []) + + def test_modifier(self): + assert PlayResultFormatter.modifier(ArcaeaPlayResultModifier.NORMAL) == "NORMAL" + assert PlayResultFormatter.modifier(ArcaeaPlayResultModifier.EASY) == "EASY" + assert PlayResultFormatter.modifier(ArcaeaPlayResultModifier.HARD) == "HARD" + assert PlayResultFormatter.modifier(None) == "None" + + assert PlayResultFormatter.modifier(1) == "EASY" + assert PlayResultFormatter.modifier(6) == "UNKNOWN" + + pytest.raises(ValueError, PlayResultFormatter.modifier, -1) + pytest.raises(TypeError, PlayResultFormatter.modifier, "1") + pytest.raises(TypeError, PlayResultFormatter.modifier, [])