Skip to content

Commit

Permalink
Merge pull request #174 from IATI/173-203-range-rule
Browse files Browse the repository at this point in the history
(2.03) Adding new range rule
  • Loading branch information
Ocre42 committed Mar 12, 2019
2 parents d8618c0 + 248c57a commit b88c627
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 5 deletions.
8 changes: 8 additions & 0 deletions iatirulesets/__init__.py
Expand Up @@ -160,6 +160,14 @@ def evaluates_to_true(self, case):
def if_then(self, case):
return self.element.xpath(case['then']) if self.element.xpath(case['if']) else True

def range(self, case):
# if min/max are missing, we use val as the sentinel
# so we can check also that val is at_least_value or no_more_than_value
return all([
Decimal(case.get('min', val)) <= Decimal(val) <= Decimal(case.get('max', val))
for val in self.path_matches_text
])


def test_rule(context_xpath, element, rule, case):
"""
Expand Down
10 changes: 10 additions & 0 deletions iatirulesets/text.py
Expand Up @@ -66,6 +66,16 @@ def rules_text(rules, reduced_path, show_all=False):
out.append('The sum of values matched at ``{0}`` must be ``{1}``.'.format(case_path, case['sum']))
elif rule == 'no_percent':
out.append('``{0}`` must not contain a ``%`` sign.'.format(case_path))
elif rule == 'range':
min_val = case.get('min')
max_val = case.get('max')
if min_val is not None or max_val is not None:
# only proceed if one of the two exists
txt = 'The value of each of the elements described by ``{0}`` must be'.format(case_path)
txt += ' at least ``{0}``'.format(min_val) if min_val is not None else ''
txt += ' no more than ``{0}``'.format(max_val) if max_val is not None else ''
txt += ' (inclusive).'
out.append(txt)
else:
print('Not implemented', rule, reduced_path)
else:
Expand Down
3 changes: 3 additions & 0 deletions meta_tests.sh
Expand Up @@ -127,6 +127,9 @@ test total_expenditure total_expenditure_bad False
test status_date status_date_good True
test status_date status_date_bad False

test range range_good True
test range range_bad False

# End with a newline
echo

Expand Down
11 changes: 11 additions & 0 deletions meta_tests/range.json
@@ -0,0 +1,11 @@
{
"//iati-activity": {
"range": {
"cases": [ {
"paths": ["test/@number"],
"min": 0.0,
"max": 100.0
} ]
}
}
}
3 changes: 3 additions & 0 deletions meta_tests/range_bad.xml
@@ -0,0 +1,3 @@
<iati-activities><iati-activity>
<test number="110.0"/>
</iati-activity></iati-activities>
3 changes: 3 additions & 0 deletions meta_tests/range_good.xml
@@ -0,0 +1,3 @@
<iati-activities><iati-activity>
<test number="0.0"/>
</iati-activity></iati-activities>
29 changes: 24 additions & 5 deletions rulesets/standard.json
Expand Up @@ -92,11 +92,6 @@
},
"evaluates_to_true": {
"cases": [
{"eval": "number(recipient-country/@percentage) >= 0.0 and number(recipient-country/@percentage) <= 100.0"},
{"eval": "number(recipient-region/@percentage) >= 0.0 and number(recipient-region/@percentage) <= 100.0"},
{"eval": "number(sector/@percentage) >= 0.0 and number(sector/@percentage) <= 100.0"},
{"eval": "number(capital-spend/@percentage) >= 0.0 and number(capital-spend/@percentage) <= 100.0"},
{"eval": "number(country-budget-items/budget-item/@percentage) >= 0.0 and number(country-budget-items/budget-item/@percentage) <= 100.0"},
{
"condition": "activity-status/@code='1'",
"eval": "not(activity-date[@type='2'])"
Expand All @@ -111,6 +106,30 @@
}
]
},
"range": {
"cases": [
{
"paths": ["recipient-country/@percentage"],
"min": 0.0, "max": 100.0
},
{
"paths": ["recipient-region/@percentage"],
"min": 0.0, "max": 100.0
},
{
"paths": ["sector/@percentage"],
"min": 0.0, "max": 100.0
},
{
"paths": ["capital-spend/@percentage"],
"min": 0.0, "max": 100.0
},
{
"paths": ["country-budget-items/budget-item/@percentage"],
"min": 0.0, "max": 100.0
}
]
},
"if_then": {
"cases":[
{"if": "count(@lang) = 0", "then": "count(narrative/@lang) > 0 and (count(narrative) = count(narrative/@lang))"},
Expand Down
1 change: 1 addition & 0 deletions rulestable.py
Expand Up @@ -31,6 +31,7 @@
'sum': 'Must sum to {0}',
'startswith': 'Must start with ``{0}``',
'unique': 'Unique',
'range': 'The value must be at least ``min`` and no more than ``max`` (inclusive).',
'evaluates_to_true': '```eval``` must evaluate to true',
'time_limit': 'Length must be under a year',
'date_now': 'Date must not be more recent than the current date',
Expand Down
25 changes: 25 additions & 0 deletions schema.json
Expand Up @@ -457,6 +457,31 @@
}
}
}
},
"range": {
"description": "The value of each of the elements described by ``paths`` must be between the values of ```min`` and ```max```. Min/max can be excluded, to check that the value is at least or no more the specified key.",
"type": "object",
"properties": {
"cases": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"paths": {
"type": "array",
"items":{"type":"string"}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
}
}
}
}
},
"additionalProperties": false
Expand Down

0 comments on commit b88c627

Please sign in to comment.