Skip to main content

rs_teststand/property/
array_dimensions.rs

1//! The shape of an array property.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::array_dimensions;
7
8/// The dimensions of an array `PropertyObject` (`ArrayDimensions`).
9///
10/// A non-array property reports zero dimensions, so this is safe to ask for on
11/// anything.
12///
13/// Bounds are read and written as strings such as `[0,0]` and `[2,4]`. The
14/// engine also exposes them as `SAFEARRAY`s, but the string form carries the
15/// same information through the ordinary dispatch path.
16#[derive(Debug)]
17pub struct ArrayDimensions {
18    dispatch: Box<dyn Dispatch>,
19}
20
21impl ArrayDimensions {
22    /// Wraps a dispatch handle returned by the engine.
23    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
24        Self { dispatch }
25    }
26
27    /// How many dimensions the array has; `0` when it is not an array
28    /// (`NumDimensions`).
29    ///
30    /// # Errors
31    /// [`Error`] if the COM call fails or returns an unexpected type.
32    pub fn num_dimensions(&self) -> Result<i32, Error> {
33        Ok(self
34            .dispatch
35            .get(array_dimensions::NUM_DIMENSIONS)?
36            .as_i32()?)
37    }
38
39    /// The lower bound of each dimension, e.g. `[0,0]` (`LowerBoundsString`).
40    ///
41    /// # Errors
42    /// [`Error`] if the COM call fails or returns an unexpected type.
43    pub fn lower_bounds_string(&self) -> Result<String, Error> {
44        Ok(self
45            .dispatch
46            .get(array_dimensions::LOWER_BOUNDS_STRING)?
47            .into_string()?)
48    }
49
50    /// The upper bound of each dimension, e.g. `[2,4]` (`UpperBoundsString`).
51    ///
52    /// # Errors
53    /// [`Error`] if the COM call fails or returns an unexpected type.
54    pub fn upper_bounds_string(&self) -> Result<String, Error> {
55        Ok(self
56            .dispatch
57            .get(array_dimensions::UPPER_BOUNDS_STRING)?
58            .into_string()?)
59    }
60
61    /// Sets both bounds from their string forms (`SetBoundsByStrings`).
62    ///
63    /// Resizing does not move existing elements to keep their indices, so an
64    /// element's position can change; rebuild the contents afterwards rather
65    /// than assuming they survived.
66    ///
67    /// # Errors
68    /// [`Error`] if the COM call fails.
69    pub fn set_bounds_by_strings(&self, lower: &str, upper: &str) -> Result<(), Error> {
70        self.dispatch.call(
71            array_dimensions::SET_BOUNDS_BY_STRINGS,
72            &[Value::Str(lower.to_owned()), Value::Str(upper.to_owned())],
73        )?;
74        Ok(())
75    }
76
77    /// The per-dimension lengths, derived from the bounds.
78    ///
79    /// `[0,0]`..`[2,4]` yields `[3, 5]`.
80    ///
81    /// # Errors
82    /// [`Error`] if the COM call fails or a bound cannot be parsed.
83    pub fn lengths(&self) -> Result<Vec<i32>, Error> {
84        let lower = parse_bounds(&self.lower_bounds_string()?)?;
85        let upper = parse_bounds(&self.upper_bounds_string()?)?;
86        // An empty array reports a lower bound but no upper one (`[0]` and
87        // `[]`): it has a dimension, that dimension just holds nothing.
88        if upper.is_empty() {
89            return Ok(vec![0; lower.len()]);
90        }
91        if lower.len() != upper.len() {
92            return Err(Error::UnexpectedType {
93                expected: "matching lower and upper bound counts",
94                actual: "differing bound counts",
95            });
96        }
97        Ok(lower
98            .iter()
99            .zip(upper.iter())
100            .map(|(low, high)| high - low + 1)
101            .collect())
102    }
103}
104
105/// Parses a bound string into one number per dimension.
106///
107/// The engine writes one bracketed group per dimension, so a 1D array reads
108/// `[9]` and a 2D one `[9][1]`. An empty string, or `[]`, means the array holds
109/// nothing.
110fn parse_bounds(text: &str) -> Result<Vec<i32>, Error> {
111    let trimmed = text.trim();
112    if trimmed.is_empty() || trimmed == "[]" {
113        return Ok(Vec::new());
114    }
115    trimmed
116        .split(']')
117        .filter(|group| !group.trim().is_empty())
118        .map(|group| {
119            group
120                .trim()
121                .trim_start_matches('[')
122                .trim()
123                .parse::<i32>()
124                .map_err(|_| Error::UnexpectedType {
125                    expected: "one bracketed integer per dimension, e.g. [0][0]",
126                    actual: "an unparsable bound string",
127                })
128        })
129        .collect()
130}
131
132#[cfg(test)]
133mod tests {
134    use super::parse_bounds;
135
136    #[test]
137    fn bounds_parse_one_group_per_dimension() -> Result<(), crate::Error> {
138        // Measured against a live engine: a 2D array reports "[0][0]".."[9][1]".
139        assert_eq!(parse_bounds("[0]")?, vec![0]);
140        assert_eq!(parse_bounds("[9][1]")?, vec![9, 1]);
141        assert_eq!(parse_bounds("[0][0][0]")?, vec![0, 0, 0]);
142        assert_eq!(parse_bounds("[-1]")?, vec![-1]);
143        Ok(())
144    }
145
146    #[test]
147    fn a_two_dimensional_shape_multiplies_out_to_its_element_count() -> Result<(), crate::Error> {
148        // "[0][0]".."[9][1]" is 10 x 2 = 20 elements, which is what the engine
149        // reports for the fixture's 2D string array.
150        let lower = parse_bounds("[0][0]")?;
151        let upper = parse_bounds("[9][1]")?;
152        let lengths: Vec<i32> = lower
153            .iter()
154            .zip(upper.iter())
155            .map(|(low, high)| high - low + 1)
156            .collect();
157        assert_eq!(lengths, vec![10, 2]);
158        assert_eq!(lengths.iter().product::<i32>(), 20);
159        Ok(())
160    }
161
162    #[test]
163    fn an_empty_array_has_a_dimension_of_length_zero() -> Result<(), crate::Error> {
164        // Measured: an empty 1D array reports lower "[0]" and upper "[]".
165        assert_eq!(parse_bounds("[0]")?, vec![0]);
166        assert!(parse_bounds("[]")?.is_empty());
167        Ok(())
168    }
169
170    #[test]
171    fn a_non_array_reports_no_bounds() -> Result<(), crate::Error> {
172        // A scalar has zero dimensions, and its bound string is empty rather
173        // than malformed, that must not read as an error.
174        assert!(parse_bounds("")?.is_empty());
175        assert!(parse_bounds("[]")?.is_empty());
176        Ok(())
177    }
178
179    #[test]
180    fn a_malformed_bound_is_reported_not_guessed() {
181        assert!(parse_bounds("[0,oops]").is_err());
182    }
183}