1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# rangeCheck.py
#
# A sample program showing how parse actions can convert parsed
# strings into a data type or object, and to validate the parsed value.
#
# Updated to use new addCondition method and expr() copy.
#
# Copyright 2011,2015 Paul T. McGuire
#
import pyparsing as pp
from datetime import date
from typing import Any
def ranged_value(
expr: pp.ParserElement,
min_val: Any = None,
max_val: Any = None,
label: str = ""
) -> pp.ParserElement:
# have to specify at least one range boundary
if (min_val, max_val) == (None, None):
raise ValueError("min_val or max_val must be specified")
expr_label = label or "value"
# set range testing function and error message depending on
# whether either or both min and max values are given
in_range_condition = {
(False, True): lambda s, l, t: t[0] <= max_val,
(True, False): lambda s, l, t: min_val <= t[0],
(True, True): lambda s, l, t: min_val <= t[0] <= max_val,
}[min_val is not None, max_val is not None]
out_of_range_message = {
(False, True): f"{expr_label} is greater than {max_val}",
(True, False): f"{expr_label} is less than {min_val}",
(True, True): f"{expr_label} is not in the range ({min_val} to {max_val})",
}[min_val is not None, max_val is not None]
ret = expr().add_condition(in_range_condition, message=out_of_range_message)
if label:
ret.set_name(label)
return ret
# define the expressions for a date of the form YYYY/MM/DD or YYYY/MM (assumes YYYY/MM/01)
integer = pp.Word(pp.nums).set_name("integer")
integer.set_parse_action(lambda t: int(t[0]))
month = ranged_value(integer, 1, 12, "month")
day = ranged_value(integer, 1, 31, "day")
year = ranged_value(integer, 2000, None, "year")
SLASH = pp.Suppress("/")
dateExpr = year("year") + SLASH + month("month") + pp.Opt(SLASH + day("day"))
dateExpr.set_name("date")
# convert date fields to datetime (also validates dates as truly valid dates)
dateExpr.set_parse_action(lambda t: date(t.year, t.month, t.day or 1))
# add range checking on dates
min_date = date(2002, 1, 1)
max_date = date.today()
date_expr = ranged_value(dateExpr, min_date, max_date, "date")
date_expr.create_diagram("range_check.html")
# tests of valid dates
success_valid_tests, _ = date_expr.run_tests(
"""
# valid date
2011/5/8
# leap day
2004/2/29
# default day of month to 1
2004/2
"""
)
# tests of invalid dates
success_invalid_tests, _ = date_expr.run_tests(
"""
# all values are in range, but date is too early
2001/1/1
# not a leap day
2005/2/29
# year number is < 2000
1999/12/31
# bad year field
XXXX/1/1
# bad month field
2010/XX/1
# bad day field
2010/11/XX
""",
failure_tests=True
)
assert (success_valid_tests and success_invalid_tests)
|