Skip to content

Commit

Permalink
Allow float precision tolerance (#32)
Browse files Browse the repository at this point in the history
* Update to tolerate equality precision

* Fix lint

* Pass rule number to error for temporalrules when no previous visit is found

* Update changelog
  • Loading branch information
echeng06 authored Dec 26, 2024
1 parent c7f050f commit a01240a
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Documentation of release versions of `nacc-form-validator`

## 0.4.1

* Updates JSON logic's `soft_equals` and util's `compare_values` to compare two floats for equality with a precision tolerance of 0.01
* Note this only compares with precision tolerance on explicit equals, e.g. `==` or `!=` and not `<=` and `>=`
* Fixes bug where the rule index was not being passed for a `temporalrules` error if no previous record was found

## 0.4.0

* Adds `_check_adcid` method to validate a provided ADCID against current list of ADCIDs. (Actual validation should be implemented by overriding the `is_valid_adcid` method in Datastore class)
Expand Down
2 changes: 1 addition & 1 deletion nacc_form_validator/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ python_distribution(
sdist=True,
provides=python_artifact(
name="nacc-form-validator",
version="0.4.0",
version="0.4.1",
description="The NACC form validator package",
author="NACC",
author_email="nacchelp@uw.edu",
Expand Down
11 changes: 11 additions & 0 deletions nacc_form_validator/json_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"""

import logging
import math
from functools import reduce

logger = logging.getLogger(__name__)
Expand All @@ -48,6 +49,16 @@ def soft_equals(a, b):
return str(a) == str(b)
if isinstance(a, bool) or isinstance(b, bool):
return bool(a) is bool(b)

# compare floats with a precision of 0.01
if isinstance(a, (str, int, float)) and isinstance(b, (str, int, float)):
try:
float(a) # don't actually set it to a in case we die at b
float(b)
return math.isclose(float(a), float(b), abs_tol=1e-2)
except ValueError:
pass

return a == b


Expand Down
5 changes: 3 additions & 2 deletions nacc_form_validator/nacc_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,8 @@ def _validate_temporalrules(self, temporalrules: List[Mapping], field: str,
swap_order = temporalrule.get(SchemaDefs.SWAP_ORDER, False)
ignore_empty_fields = temporalrule.get(SchemaDefs.IGNORE_EMPTY,
None)
rule_no = temporalrule.get(SchemaDefs.INDEX, rule_no + 1)

if isinstance(ignore_empty_fields, str):
ignore_empty_fields = [ignore_empty_fields]

Expand All @@ -727,14 +729,13 @@ def _validate_temporalrules(self, temporalrules: List[Mapping], field: str,
if not prev_ins:
if ignore_empty_fields:
continue
self._error(field, ErrorDefs.NO_PREV_VISIT)
self._error(field, ErrorDefs.NO_PREV_VISIT, rule_no)
return

# Extract operators if specified, default is AND
prev_operator = temporalrule.get(SchemaDefs.PREV_OP, "AND").upper()
curr_operator = temporalrule.get(SchemaDefs.CURR_OP, "AND").upper()

rule_no = temporalrule.get(SchemaDefs.INDEX, rule_no + 1)
prev_conds = temporalrule[SchemaDefs.PREVIOUS]
curr_conds = temporalrule[SchemaDefs.CURRENT]

Expand Down
18 changes: 16 additions & 2 deletions nacc_form_validator/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions."""

import logging
import math
import re
from typing import Any

Expand Down Expand Up @@ -68,12 +69,25 @@ def compare_values(comparator: str, value: object, base_value: object) -> bool:
Returns:
bool: True if the formula is satisfied, else False
"""
# try close enough equality if both are floats first
both_floats = False
if isinstance(value, (str, int, float)) \
and isinstance(base_value, (str, int, float)):
try:
float(value) # don't actually set it to a in case we die at b
float(base_value)
both_floats = True
except ValueError:
pass

# test these first as they don't care about null values
if comparator == "==":
return value == base_value
return value == base_value if not both_floats else \
math.isclose(float(value), float(base_value), abs_tol=1e-2)

if comparator == "!=":
return value != base_value
return value != base_value if not both_floats else \
not math.isclose(float(value), float(base_value), abs_tol=1e-2)

if comparator not in ["<=", ">=", "<", ">"]:
raise TypeError(f"Unrecognized comparator: {comparator}")
Expand Down
19 changes: 19 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,22 @@ def test_compare_values_null_values_valid():
assert not compare_values(">", None, None)
assert compare_values("<=", None, None)
assert compare_values(">=", None, None)

def test_compare_values_precision_tolerance():
""" Test comparing values with precision tolerance """
assert compare_values("==", 1.33, 1.333333)
assert not compare_values("==", 1.3, "1.333333")
assert not compare_values("==", 1.33, 1.4)
assert not compare_values("==", "1.33", "1.2")
assert not compare_values("==", 1.33, 1.34)
assert not compare_values("==", "3", 1.0)

assert not compare_values("!=", 1.33, 1.333333)
assert compare_values("!=", 1.3, "1.333333")
assert compare_values("!=", 1.33, 1.4)
assert compare_values("!=", "1.33", "1.2")
assert compare_values("!=", 1.33, 1.34)
assert compare_values("!=", "3", 1.0)

assert compare_values("!=", "3", "hello")
assert not compare_values("==", 2.5, "hello")

0 comments on commit a01240a

Please sign in to comment.