Skip to content

Commit da60270

Browse files
committed
Expand SPSS conformance fixture coverage
1 parent 2bb07c7 commit da60270

1 file changed

Lines changed: 48 additions & 5 deletions

File tree

‎tests/conformance.py‎

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Reusable pyspssio fixtures and semantic comparators for SAV/ZSAV round trips."""
22

3-
import math
43
from pathlib import Path
54
from typing import Any
65

@@ -10,45 +9,79 @@
109

1110

1211
def write_supported_semantics_fixture(destination: str | Path) -> dict[str, Any]:
13-
"""Write the metadata pyspssio demonstrably exposes and restores."""
12+
"""Write a real SAV/ZSAV fixture for every pyspssio-supported core feature.
13+
14+
SPSS time-like values are deliberately stored as numeric values. Their
15+
interpretation belongs to the accompanying SPSS format, never to a SQL
16+
temporal coercion. The fixture also covers every legal user-missing
17+
shape: three discrete values, a range alone, and a range plus one value.
18+
``pyspssio`` exposes SPSS LOWEST/HIGHEST again as negative/positive
19+
infinity, which is the representation the strict SQL catalog preserves.
20+
"""
1421
destination = Path(destination)
1522
long_text = "Õ🙂漢字" * 90
1623
frame = pd.DataFrame({
24+
"discrete_missing": [1.0, 2.0, 3.0, 4.0],
25+
"range_only": [-1.0, 0.0, 1.0, 2.0],
1726
"lowest_range": [-2.0, -1.0, 0.0, 7.0],
1827
"highest_range": [-99.0, 0.0, 1.0, 42.0],
1928
"code": [3.0, 1.0, 2.0, 1.0],
2029
"status": ["NA", "DK", "ok", ""],
2130
"comment": [long_text, "", "näide", long_text],
2231
"interview_date": [23123.0, 23124.0, 23125.0, 23126.0],
32+
"interview_time": [3661.25, 0.0, 86399.5, 12.0],
33+
"interview_datetime": [23123.5, 23124.0, 23125.75, 23126.125],
34+
"interview_dtime": [90061.5, 0.0, 172800.25, 12.0],
35+
"formatted_comma": [12345.67, -12345.67, 0.0, 1.25],
36+
"formatted_dot": [12345.67, -12345.67, 0.0, 1.25],
37+
"formatted_pct": [12.5, 0.0, 100.0, -2.5],
2338
"resp_a": [1.0, 0.0, 1.0, 0.0],
2439
"resp_b": [0.0, 1.0, 1.0, 0.0],
2540
})
2641
metadata = {
2742
"var_types": {"status": 8, "comment": 1024},
2843
"var_formats": {
44+
"discrete_missing": "F8.0", "range_only": "F8.0",
2945
"lowest_range": "F12.1", "highest_range": "F12.1", "code": "F8.0",
3046
"status": "A8", "comment": "A1024", "interview_date": "DATE11",
47+
"interview_time": "TIME8", "interview_datetime": "DATETIME20",
48+
"interview_dtime": "DTIME10", "formatted_comma": "COMMA12.2",
49+
"formatted_dot": "DOT12.2", "formatted_pct": "PCT8.1",
3150
},
3251
"var_labels": {
52+
"discrete_missing": "Three numeric user-missing codes",
53+
"range_only": "Range-only numeric user missing",
3354
"lowest_range": "LOWEST-style missing range", "highest_range": "HIGHEST-style missing range",
3455
"code": "Ordered numeric code", "status": "String missing code",
3556
"comment": "Long UTF-8 comment", "interview_date": "SPSS numeric date",
57+
"interview_time": "SPSS numeric time", "interview_datetime": "SPSS numeric datetime",
58+
"interview_dtime": "SPSS numeric duration", "formatted_comma": "SPSS comma format",
59+
"formatted_dot": "SPSS dot format", "formatted_pct": "SPSS percent format",
3660
},
3761
"var_value_labels": {
62+
"discrete_missing": {3.0: "third", 1.0: "first", 2.0: "second"},
3863
"code": {3.0: "third", 1.0: "first", 2.0: "second"},
3964
"status": {"DK": "don't know", "NA": "not answered", "ok": "valid"},
4065
},
4166
"var_missing_values": {
67+
"discrete_missing": {"values": [1.0, 2.0, 3.0]},
68+
"range_only": {"lo": -1.0, "hi": 1.0},
4269
"lowest_range": {"lo": -sys_float_max(), "hi": -1.0, "values": [7.0]},
4370
"highest_range": {"lo": 1.0, "hi": sys_float_max(), "values": [-99.0]},
4471
"status": {"values": ["NA", "DK"]},
4572
},
4673
"var_measure_levels": {
47-
"lowest_range": "scale", "highest_range": "scale", "code": "scale",
74+
"discrete_missing": "scale", "range_only": "scale", "lowest_range": "scale",
75+
"highest_range": "scale", "code": "scale",
4876
"status": "nominal", "comment": "nominal", "interview_date": "scale",
4977
},
5078
"var_alignments": {column: "left" for column in frame.columns},
51-
"var_column_widths": {"lowest_range": 12, "highest_range": 12, "code": 8, "status": 12, "comment": 48, "interview_date": 11},
79+
"var_column_widths": {
80+
"discrete_missing": 8, "range_only": 8, "lowest_range": 12, "highest_range": 12,
81+
"code": 8, "status": 12, "comment": 48, "interview_date": 11,
82+
"interview_time": 8, "interview_datetime": 20, "interview_dtime": 10,
83+
"formatted_comma": 12, "formatted_dot": 12, "formatted_pct": 8,
84+
},
5285
"var_roles": {"code": "target", "status": "input"},
5386
"var_attributes": {"code": {"Origin": "fixture"}},
5487
"mrsets": {"$responses": {"label": "Responses", "counted_value": 1, "variable_list": ["resp_a", "resp_b"]}},
@@ -74,6 +107,14 @@ def _canonical_missing(metadata: dict[str, Any]) -> dict[str, tuple[tuple[Any, A
74107
return result
75108

76109

110+
def _ordered_value_labels(metadata: dict[str, Any]) -> dict[str, tuple[tuple[Any, str], ...]]:
111+
"""Retain the label order exposed by the SPSS engine, not merely its mapping."""
112+
return {
113+
name: tuple((value, str(label)) for value, label in labels.items())
114+
for name, labels in (metadata.get("var_value_labels") or {}).items()
115+
}
116+
117+
77118
def compare_sav_semantics(source: str | Path, exported: str | Path) -> dict[str, Any]:
78119
source_frame, source_metadata = pyspssio.read_sav(str(source), convert_datetimes=False, include_user_missing=True)
79120
exported_frame, exported_metadata = pyspssio.read_sav(str(exported), convert_datetimes=False, include_user_missing=True)
@@ -93,4 +134,6 @@ def compare_sav_semantics(source: str | Path, exported: str | Path) -> dict[str,
93134
failures.append(attribute)
94135
if _canonical_missing(source_metadata) != _canonical_missing(exported_metadata):
95136
failures.append("var_missing_values")
96-
return {"equivalent": not failures, "differences": failures}
137+
if _ordered_value_labels(source_metadata) != _ordered_value_labels(exported_metadata):
138+
failures.append("var_value_label_order")
139+
return {"equivalent": not failures, "differences": failures}

0 commit comments

Comments
 (0)