diff --git a/src/euring/codes.py b/src/euring/codes.py index 5c394fd..73c9176 100644 --- a/src/euring/codes.py +++ b/src/euring/codes.py @@ -14,7 +14,7 @@ load_species_map, ) from .exceptions import EuringConstraintException, EuringLookupException -from .utils import euring_dms_to_float +from .utils import euring_dms_to_float, is_all_hyphens LOOKUP_EURING_CODE_IDENTIFIER = load_code_map("euring_code_identifier") LOOKUP_CONDITION = load_code_map("condition") @@ -211,7 +211,7 @@ def parse_direction(value: str) -> int | None: if value is None: raise EuringConstraintException(f'Value "{value}" is not a valid direction.') value_str = f"{value}" - if value_str and set(value_str) == {"-"}: + if is_all_hyphens(value_str): return None if value_str.startswith("-"): raise EuringConstraintException(f'Value "{value}" is not a valid direction.') @@ -315,7 +315,7 @@ def lookup_date(value: str | int) -> date: def parse_date(value: str) -> str: """Validate that date placeholders are not used, then return the raw value.""" - if value and set(value) == {"-"}: + if is_all_hyphens(value): raise EuringConstraintException("Date cannot be all dashes; provide an estimated real date instead.") return value diff --git a/src/euring/field_schema.py b/src/euring/field_schema.py index 2590237..1e90ce8 100644 --- a/src/euring/field_schema.py +++ b/src/euring/field_schema.py @@ -5,7 +5,7 @@ from datetime import date as dt_date from typing import Any -from euring.utils import euring_lat_to_dms, euring_lng_to_dms, is_empty +from euring.utils import euring_lat_to_dms, euring_lng_to_dms, is_all_hyphens, is_empty from .codes import lookup_description from .exceptions import EuringConstraintException, EuringTypeException @@ -93,7 +93,7 @@ def _validate_raw(self, raw: str) -> str | None: def _coerce_type(self, raw: str) -> Any: if self.euring_type == TYPE_INTEGER: - if set(raw) == {"-"}: + if is_all_hyphens(raw): return None return int(raw) if self.euring_type == TYPE_NUMERIC: @@ -111,7 +111,7 @@ def _coerce_value_type(self, raw: str) -> Any: if self.value_type == "code_str": return raw if self.value_type == "int": - if set(raw) == {"-"}: + if is_all_hyphens(raw): return None return int(raw) if self.value_type == "float": @@ -187,7 +187,7 @@ def encode_for_format(self, value: Any | None, *, format: str) -> str: raise EuringTypeException(f'Value "{str_value}" is not valid for type {self.euring_type}.') return str_value - if self.euring_type == TYPE_INTEGER and isinstance(value, str) and value and set(value) == {"-"}: + if self.euring_type == TYPE_INTEGER and isinstance(value, str) and is_all_hyphens(value): return self.encode_for_format(None, format=format) str_value = f"{value}" diff --git a/src/euring/record.py b/src/euring/record.py index a0a6fa6..f26d9fb 100644 --- a/src/euring/record.py +++ b/src/euring/record.py @@ -17,7 +17,7 @@ unknown_format_error, ) from .rules import record_rule_errors, requires_euring2020 -from .utils import euring_lat_to_dms, euring_lng_to_dms, is_empty +from .utils import euring_lat_to_dms, euring_lng_to_dms, is_all_hyphens, is_empty class EuringRecord: @@ -148,7 +148,7 @@ def _validate_fields(self) -> list[dict[str, object]]: field_obj = replace(field_obj, variable_length=False) encoded_value = _serialize_field_value(field, value, self.format) raw_value = encoded_value - if key == "date" and had_empty_value and raw_value and set(raw_value) == {"-"}: + if key == "date" and had_empty_value and is_all_hyphens(raw_value): # Treat placeholder dashes for missing required dates as empty so # non-strict mode only reports a missing-required-field error. raw_value = "" diff --git a/src/euring/utils.py b/src/euring/utils.py index 4d61997..927fa33 100644 --- a/src/euring/utils.py +++ b/src/euring/utils.py @@ -21,6 +21,11 @@ def is_empty(value: Any) -> bool: return value in (None, "") +def is_all_hyphens(value: str) -> bool: + """Return True when a non-empty string consists of only hyphens.""" + return bool(value) and set(value) == {"-"} + + def euring_dms_to_float(value: str) -> float: """Convert EURING DMS coordinate text into decimal degrees.""" try: diff --git a/tests/test_utils.py b/tests/test_utils.py index c645b6e..75d740f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -13,41 +13,57 @@ euring_scheme_export_format, euring_species_export_format, ) +from euring.utils import is_all_hyphens, is_empty -class TestUtils: - def test_dms_conversion(self): - # Test DMS to float - lat_decimal = euring_dms_to_float("+420500") - lng_decimal = euring_dms_to_float("-0100203") - assert abs(lat_decimal - 42.083333) < 1e-5 - assert abs(lng_decimal - (-10.034167)) < 1e-5 - - # Test float to DMS (round trip) - assert euring_lat_to_dms(lat_decimal) == "+420500" - assert euring_lng_to_dms(lng_decimal) == "-0100203" - dms = euring_float_to_dms(12.25) - assert dms["quadrant"] == "+" - assert dms["degrees"] == 12 - assert dms["minutes"] == 15 - assert dms["seconds"] == 0.0 - - def test_dms_conversion_invalid(self): - with pytest.raises(EuringConstraintException): - euring_dms_to_float("bogus") - - def test_identification_format(self): - assert euring_identification_display_format("ab.12-3") == "AB123" - assert euring_identification_export_format("AB123") == "AB.....123" - - def test_scheme_format(self): - assert euring_scheme_export_format("GB") == " GB" - assert euring_scheme_export_format("ABCDE") == "ABC" - - def test_species_format(self): - assert euring_species_export_format("123") == "00123" - assert euring_species_export_format("12345") == "12345" - with pytest.raises(ValueError): - euring_species_export_format("123456") - with pytest.raises(ValueError): - euring_species_export_format("not-a-number") +def test_dms_conversion(): + # Test DMS to float + lat_decimal = euring_dms_to_float("+420500") + lng_decimal = euring_dms_to_float("-0100203") + assert abs(lat_decimal - 42.083333) < 1e-5 + assert abs(lng_decimal - (-10.034167)) < 1e-5 + + # Test float to DMS (round trip) + assert euring_lat_to_dms(lat_decimal) == "+420500" + assert euring_lng_to_dms(lng_decimal) == "-0100203" + dms = euring_float_to_dms(12.25) + assert dms["quadrant"] == "+" + assert dms["degrees"] == 12 + assert dms["minutes"] == 15 + assert dms["seconds"] == 0.0 + + +def test_dms_conversion_invalid(): + with pytest.raises(EuringConstraintException): + euring_dms_to_float("bogus") + + +def test_identification_format(): + assert euring_identification_display_format("ab.12-3") == "AB123" + assert euring_identification_export_format("AB123") == "AB.....123" + + +def test_scheme_format(): + assert euring_scheme_export_format("GB") == " GB" + assert euring_scheme_export_format("ABCDE") == "ABC" + + +def test_species_format(): + assert euring_species_export_format("123") == "00123" + assert euring_species_export_format("12345") == "12345" + with pytest.raises(ValueError): + euring_species_export_format("123456") + with pytest.raises(ValueError): + euring_species_export_format("not-a-number") + + +def test_is_empty(): + assert is_empty("") + assert is_empty(None) + assert not is_empty(0) + assert not is_empty(False) + + +def test_is_all_hyphens(): + assert is_all_hyphens("-") + assert not is_all_hyphens("")