Source code for pivpy.pivmat_compat
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Iterable, List
@dataclass(frozen=True)
class _BracketSpec:
prefix: str
expr: str
fmt: str
suffix: str
def _parse_range_expr(expr: str) -> list[float]:
"""Parse a restricted MATLAB-like range expression.
Supported forms:
- "1:5" (inclusive)
- "1:0.5:2" (inclusive end if close)
- "1 2 3" or "1,2,3"
This intentionally does NOT eval arbitrary code.
"""
s = expr.strip()
if not s:
return []
# Colon form
if ":" in s:
parts = [p.strip() for p in s.split(":")]
if len(parts) == 2:
start, end = (float(parts[0]), float(parts[1]))
step = 1.0 if end >= start else -1.0
elif len(parts) == 3:
start, step, end = (float(parts[0]), float(parts[1]), float(parts[2]))
if step == 0:
raise ValueError("Invalid range: step must be nonzero")
else:
raise ValueError(f"Invalid range expression: {expr!r}")
# Generate inclusive range, tolerant to float error.
out: list[float] = []
cur = start
# direction-aware loop
if step > 0:
while cur <= end + 1e-12:
out.append(cur)
cur += step
else:
while cur >= end - 1e-12:
out.append(cur)
cur += step
return out
# List form (spaces/commas)
toks = [t for t in re.split(r"[\s,]+", s) if t]
return [float(t) for t in toks]
def _split_first_bracket(s: str) -> _BracketSpec | None:
p1 = s.find("[")
if p1 < 0:
return None
p2 = s.find("]", p1 + 1)
if p2 < 0:
raise ValueError("Invalid string: Missing closing bracket ']'")
prefix = s[:p1]
inside = s[p1 + 1 : p2]
suffix = s[p2 + 1 :]
# Optional ",NZ" where NZ can be "5" or "2.3" (width.precision)
if "," in inside:
expr, nz = inside.split(",", 1)
nz = nz.strip()
try:
nz_val = float(nz)
except ValueError as e:
raise ValueError(f"Invalid NZ format specifier: {nz!r}") from e
else:
expr, nz_val = inside, 5.0
width = int(nz_val)
prec = int(round(10 * (nz_val - int(nz_val))))
if width > 16:
raise ValueError("Invalid number of zero padding: too large")
fmt = f"{{:0{width}.{prec}f}}"
return _BracketSpec(prefix=prefix, expr=expr.strip(), fmt=fmt, suffix=suffix)
[docs]
def expandstr(pattern: str) -> list[str]:
"""Expand indexed bracket strings (PIVMAT-compatible subset).
Port of PIVMAT's ``expandstr.m`` with a safety constraint: only a restricted
range grammar is supported (no arbitrary eval).
Examples
--------
- ``expandstr('DSC[2:2:8,4].JPG')`` -> ['DSC0002.JPG', ...]
- ``expandstr('dt=[1:0.5:2,2.3]s')`` -> ['dt=1.000s', ...]
- Multiple brackets are expanded recursively.
"""
spec = _split_first_bracket(str(pattern))
if spec is None:
return [str(pattern)]
nums = _parse_range_expr(spec.expr)
out = [f"{spec.prefix}{spec.fmt.format(v)}{spec.suffix}" for v in nums]
# Recurse if there are remaining brackets in the suffix.
if "[" in spec.suffix:
expanded: list[str] = []
for s in out:
expanded.extend(expandstr(s))
return expanded
return out