diff --git a/README.md b/README.md index 1b4272b..e1f8477 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,33 @@ schema = { - strip-whitespaces - left-strip +##### map + +Maps a raw value to another one. By default, a value which is not one of the +`values` keys raises an error. Set the optional `default` parameter to map those +values instead of failing: + +```python +{ + "key": "unit-of-measurement", + "column-number": 12, + "type": "int", + "pre-processors": [ + { + "name": "map", + "parameters": {"values": {"K": 0, "A": 1, "L": 2}, "default": 0}, + } + ], +} +``` + +An empty string can be used as a key to map blank values: +`{"values": {"": 0, "K": 1}}`. Note that a field whose raw value is empty is +never given to its pre-processors: it fails as a required field, or is parsed as +`None` when the field is declared `"optional": True`. Mapping an empty value +therefore only applies when a previous pre-processor produces an empty string, +for instance `strip-whitespaces` on a blank columnar field. + #### Validators - regex-matches diff --git a/magicparse/pre_processors.py b/magicparse/pre_processors.py index 2026f65..c7c4c82 100644 --- a/magicparse/pre_processors.py +++ b/magicparse/pre_processors.py @@ -38,16 +38,27 @@ def key() -> str: return "left-pad-zeroes" +class _NoDefault: + "Sentinel allowing 'None' to be used as a mapping default value" + + +NO_DEFAULT = _NoDefault() + + class Map(PreProcessor): - def __init__(self, on_error: OnError, values: dict[str, Any]) -> None: + def __init__(self, on_error: OnError, values: dict[str, Any], default: Any = NO_DEFAULT) -> None: super().__init__(on_error) self.values = values + self.default = default self._keys = ", ".join(f"'{key}'" for key in self.values.keys()) def apply(self, value: str) -> str: try: return self.values[value] except: + if not isinstance(self.default, _NoDefault): + return self.default + raise ValueError(f"value '{value}' does not map to any values in [{self._keys}]") @staticmethod diff --git a/tests/test_pre_processors.py b/tests/test_pre_processors.py index a4413e2..a771e09 100644 --- a/tests/test_pre_processors.py +++ b/tests/test_pre_processors.py @@ -1,6 +1,7 @@ import re from typing import Any from magicparse.pre_processors import ( + NO_DEFAULT, LeftPadZeroes, Map, PreProcessor, @@ -23,6 +24,15 @@ def test_map(self): pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"input": "output"}}}) assert isinstance(pre_processor, Map) assert pre_processor.values == {"input": "output"} + assert pre_processor.default is NO_DEFAULT + + def test_map_with_default(self): + pre_processor = PreProcessor.build( + {"name": "map", "parameters": {"values": {"input": "output"}, "default": "fallback"}} + ) + assert isinstance(pre_processor, Map) + assert pre_processor.values == {"input": "output"} + assert pre_processor.default == "fallback" def test_replace(self): pre_processor = PreProcessor.build({"name": "replace", "parameters": {"pattern": "aa", "replacement": "bb"}}) @@ -81,6 +91,26 @@ def test_known_input(self): pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"A": "1", "B": "2"}}}) assert pre_processor.apply("A") == "1" + def test_unknown_input_with_default(self): + pre_processor = PreProcessor.build( + {"name": "map", "parameters": {"values": {"A": "1", "B": "2"}, "default": "0"}} + ) + assert pre_processor.apply("an input") == "0" + + def test_known_input_with_default(self): + pre_processor = PreProcessor.build( + {"name": "map", "parameters": {"values": {"A": "1", "B": "2"}, "default": "0"}} + ) + assert pre_processor.apply("A") == "1" + + def test_none_default(self): + pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"A": "1"}, "default": None}}) + assert pre_processor.apply("an input") is None + + def test_empty_value_can_be_mapped(self): + pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"": "1", "A": "2"}}}) + assert pre_processor.apply("") == "1" + class TestReplace(TestCase): def test_pattern_not_found(self):