|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | from collections.abc import Collection, Iterable |
| 6 | +from decimal import Decimal, InvalidOperation |
| 7 | +from math import isinf |
| 8 | +from typing import Any |
6 | 9 |
|
7 | 10 | from pyxform import aliases |
8 | 11 | from pyxform.errors import ErrorCode, PyXFormError |
| 12 | +from pyxform.question_type_dictionary import QUESTION_TYPE_DICT |
| 13 | +from pyxform.validators.pyxform import parameters_generic |
9 | 14 | from pyxform.validators.pyxform.pyxform_reference import ( |
10 | 15 | is_pyxform_reference_candidate, |
11 | 16 | parse_pyxform_references, |
@@ -100,3 +105,140 @@ def validate_geo_parameter_incremental(value: str) -> None: |
100 | 105 | raise PyXFormError( |
101 | 106 | code=ErrorCode.SURVEY_003, |
102 | 107 | ) |
| 108 | + |
| 109 | + |
| 110 | +def process_range_question_type( |
| 111 | + row_number: int, |
| 112 | + row: dict[str, Any], |
| 113 | + parameters: parameters_generic.PARAMETERS_TYPE, |
| 114 | + appearance: str, |
| 115 | + choices: dict[str, Any], |
| 116 | +) -> dict[str, Any]: |
| 117 | + """ |
| 118 | + Returns a new row that includes the Range parameters start, end and step. |
| 119 | +
|
| 120 | + Raises PyXFormError when invalid range parameters are used. |
| 121 | + """ |
| 122 | + parameters = parameters_generic.validate( |
| 123 | + parameters=parameters, |
| 124 | + allowed={"start", "end", "step", "tick_interval", "placeholder", "tick_labelset"}, |
| 125 | + ) |
| 126 | + if ( |
| 127 | + appearance |
| 128 | + and appearance not in {"vertical", "no-ticks"} |
| 129 | + and any( |
| 130 | + k in parameters for k in ("tick_interval", "placeholder", "tick_labelset") |
| 131 | + ) |
| 132 | + ): |
| 133 | + raise PyXFormError(ErrorCode.RANGE_008.value.format(row=row_number)) |
| 134 | + no_ticks_appearance = appearance and appearance == "no-ticks" |
| 135 | + |
| 136 | + defaults = QUESTION_TYPE_DICT["range"]["parameters"] |
| 137 | + # set defaults |
| 138 | + for key in defaults: |
| 139 | + if key not in parameters: |
| 140 | + parameters[key] = defaults[key] |
| 141 | + |
| 142 | + def process_parameter(name: str) -> Decimal | None: |
| 143 | + value = parameters.get(name) |
| 144 | + if value is None: |
| 145 | + return value |
| 146 | + err = False |
| 147 | + try: |
| 148 | + value = Decimal(value) |
| 149 | + except InvalidOperation: |
| 150 | + err = True |
| 151 | + |
| 152 | + if err or isinf(value): |
| 153 | + raise PyXFormError( |
| 154 | + ErrorCode.RANGE_001.value.format(row=row_number, name=name) |
| 155 | + ) |
| 156 | + return value |
| 157 | + |
| 158 | + start = process_parameter(name="start") |
| 159 | + end = process_parameter(name="end") |
| 160 | + step = process_parameter(name="step") |
| 161 | + tick_interval = process_parameter(name="tick_interval") |
| 162 | + placeholder = process_parameter(name="placeholder") |
| 163 | + tick_labelset = parameters.get("tick_labelset") |
| 164 | + range_width = abs(end - start) |
| 165 | + |
| 166 | + if step == 0: |
| 167 | + raise PyXFormError(ErrorCode.RANGE_002.value.format(row=row_number, name="step")) |
| 168 | + if step > range_width: |
| 169 | + raise PyXFormError(ErrorCode.RANGE_003.value.format(row=row_number, name="step")) |
| 170 | + |
| 171 | + if tick_interval is not None: |
| 172 | + if tick_interval == 0: |
| 173 | + raise PyXFormError( |
| 174 | + ErrorCode.RANGE_002.value.format(row=row_number, name="tick_interval") |
| 175 | + ) |
| 176 | + if tick_interval > range_width: |
| 177 | + raise PyXFormError( |
| 178 | + ErrorCode.RANGE_003.value.format(row=row_number, name="tick_interval") |
| 179 | + ) |
| 180 | + if (tick_interval % step) != 0: |
| 181 | + raise PyXFormError( |
| 182 | + ErrorCode.RANGE_004.value.format(row=row_number, name="tick_interval") |
| 183 | + ) |
| 184 | + parameters["odk:tick-interval"] = parameters.pop("tick_interval") |
| 185 | + |
| 186 | + if placeholder is not None: |
| 187 | + if (placeholder - start) % step != 0: |
| 188 | + raise PyXFormError( |
| 189 | + ErrorCode.RANGE_004.value.format(row=row_number, name="placeholder") |
| 190 | + ) |
| 191 | + if placeholder < min(start, end) or placeholder > max(start, end): |
| 192 | + raise PyXFormError( |
| 193 | + ErrorCode.RANGE_005.value.format(row=row_number, name="placeholder") |
| 194 | + ) |
| 195 | + parameters["odk:placeholder"] = parameters.pop("placeholder") |
| 196 | + |
| 197 | + if tick_labelset: |
| 198 | + tick_list = choices.get(tick_labelset) |
| 199 | + if tick_list is None: |
| 200 | + raise PyXFormError(ErrorCode.RANGE_006.value.format(row=row_number)) |
| 201 | + |
| 202 | + no_ticks_labels = set() |
| 203 | + for item in tick_list: |
| 204 | + errored = False |
| 205 | + try: |
| 206 | + value = Decimal(item.get("name")) |
| 207 | + except InvalidOperation: |
| 208 | + errored = True |
| 209 | + |
| 210 | + if errored or isinf(value): |
| 211 | + raise PyXFormError(ErrorCode.RANGE_009.value.format(row=row_number)) |
| 212 | + |
| 213 | + if value < min(start, end) or value > max(start, end): |
| 214 | + raise PyXFormError(ErrorCode.RANGE_010.value.format(row=row_number)) |
| 215 | + if tick_interval is not None and (value - start) % tick_interval != 0: |
| 216 | + raise PyXFormError( |
| 217 | + ErrorCode.RANGE_011.value.format(row=row_number, name="tick_interval") |
| 218 | + ) |
| 219 | + elif (value - start) % step != 0: |
| 220 | + raise PyXFormError( |
| 221 | + ErrorCode.RANGE_011.value.format(row=row_number, name="step") |
| 222 | + ) |
| 223 | + if no_ticks_appearance: |
| 224 | + no_ticks_labels.add(value) |
| 225 | + |
| 226 | + if no_ticks_appearance: |
| 227 | + if len(no_ticks_labels) > 2: |
| 228 | + raise PyXFormError(ErrorCode.RANGE_007.value.format(row=row_number)) |
| 229 | + if no_ticks_labels != {start, end}: |
| 230 | + raise PyXFormError(ErrorCode.RANGE_012.value.format(row=row_number)) |
| 231 | + |
| 232 | + parameters["odk:tick-labelset"] = parameters.pop("tick_labelset") |
| 233 | + |
| 234 | + # Default is integer, but if the values have decimals then change the bind type. |
| 235 | + if any( |
| 236 | + i is not None and not i == i.to_integral_value() |
| 237 | + for i in (start, end, step, tick_interval, placeholder) |
| 238 | + ): |
| 239 | + row["bind"] = row.get("bind", {}) |
| 240 | + row["bind"].update({"type": "decimal"}) |
| 241 | + |
| 242 | + row["parameters"] = parameters |
| 243 | + |
| 244 | + return row |
0 commit comments