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