1use thiserror::Error;
9
10use crate::{RObject, RStr, RValue};
11
12#[derive(Debug, Error, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ViewError {
16 #[error("missing value at {path}")]
17 Missing { path: String },
18 #[error("unexpected type at {path}: expected {expected}, got {actual}")]
19 UnexpectedType {
20 path: String,
21 expected: &'static str,
22 actual: &'static str,
23 },
24 #[error("unexpected length at {path}: expected {expected}, got {actual}")]
25 UnexpectedLength {
26 path: String,
27 expected: String,
28 actual: usize,
29 },
30 #[error("invalid dimensions at {path}: {reason}")]
31 InvalidDimensions { path: String, reason: String },
32 #[error("invalid string encoding at {path}")]
33 InvalidStringEncoding { path: String },
34}
35
36impl ViewError {
37 pub fn path(&self) -> &str {
39 match self {
40 Self::Missing { path }
41 | Self::UnexpectedType { path, .. }
42 | Self::UnexpectedLength { path, .. }
43 | Self::InvalidDimensions { path, .. }
44 | Self::InvalidStringEncoding { path } => path,
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct CharacterMatrix {
56 nrow: usize,
57 ncol: usize,
58 cells: Vec<Option<String>>,
59 row_names: Option<Vec<Option<String>>>,
60 column_names: Option<Vec<Option<String>>>,
61}
62
63impl CharacterMatrix {
64 pub fn from_object(object: &RObject) -> Result<Self, ViewError> {
66 let values = match object.value() {
67 RValue::Character(values) => values,
68 value => {
69 return Err(unexpected_type(
70 "CharacterMatrix",
71 "character vector",
72 kind_name(value),
73 ));
74 }
75 };
76
77 let dimensions = object
78 .attributes()
79 .get("dim")
80 .ok_or_else(|| missing("CharacterMatrix.attributes.dim"))?;
81 let dimensions = match dimensions.value() {
82 RValue::Integer(values) => values,
83 value => {
84 return Err(unexpected_type(
85 "CharacterMatrix.attributes.dim",
86 "integer vector",
87 kind_name(value),
88 ));
89 }
90 };
91 if dimensions.len() != 2 {
92 return Err(unexpected_length(
93 "CharacterMatrix.attributes.dim",
94 "2",
95 dimensions.len(),
96 ));
97 }
98
99 let mut shape = [0usize; 2];
100 for (index, value) in dimensions.iter().enumerate() {
101 let Some(value) = value else {
102 return Err(invalid_dimensions(
103 "CharacterMatrix.attributes.dim",
104 "dimensions must not contain NA",
105 ));
106 };
107 if *value < 0 {
108 return Err(invalid_dimensions(
109 "CharacterMatrix.attributes.dim",
110 "dimensions must not be negative",
111 ));
112 }
113 shape[index] = *value as usize;
114 }
115
116 let element_count = shape[0].checked_mul(shape[1]).ok_or_else(|| {
117 invalid_dimensions(
118 "CharacterMatrix.attributes.dim",
119 "dimension product overflows",
120 )
121 })?;
122 if values.len() != element_count {
123 return Err(unexpected_length(
124 "CharacterMatrix",
125 &element_count.to_string(),
126 values.len(),
127 ));
128 }
129
130 let (row_names, column_names) = match object.attributes().get("dimnames") {
131 None => (None, None),
132 Some(dimnames) => {
133 let dimnames = match dimnames.value() {
134 RValue::List(values) => values,
135 value => {
136 return Err(unexpected_type(
137 "CharacterMatrix.attributes.dimnames",
138 "list",
139 kind_name(value),
140 ));
141 }
142 };
143 if dimnames.len() != 2 {
144 return Err(unexpected_length(
145 "CharacterMatrix.attributes.dimnames",
146 "2",
147 dimnames.len(),
148 ));
149 }
150 (
151 decode_names(&dimnames[0], shape[0], 0)?,
152 decode_names(&dimnames[1], shape[1], 1)?,
153 )
154 }
155 };
156
157 let mut cells = Vec::with_capacity(element_count);
158 for row in 0..shape[0] {
159 for column in 0..shape[1] {
160 cells.push(decode_string(&values[row + column * shape[0]], || {
161 format!("CharacterMatrix[row={row},column={column}]")
162 })?);
163 }
164 }
165
166 Ok(Self {
167 nrow: shape[0],
168 ncol: shape[1],
169 cells,
170 row_names,
171 column_names,
172 })
173 }
174
175 pub fn nrow(&self) -> usize {
177 self.nrow
178 }
179
180 pub fn ncol(&self) -> usize {
182 self.ncol
183 }
184
185 pub fn get(&self, row: usize, column: usize) -> Option<Option<&str>> {
187 if row >= self.nrow || column >= self.ncol {
188 return None;
189 }
190 Some(self.cells[row * self.ncol + column].as_deref())
191 }
192
193 pub fn row_name(&self, row: usize) -> Option<&str> {
195 self.row_names
196 .as_ref()?
197 .get(row)?
198 .as_ref()
199 .map(String::as_str)
200 }
201
202 pub fn column_name(&self, column: usize) -> Option<&str> {
204 self.column_names
205 .as_ref()?
206 .get(column)?
207 .as_ref()
208 .map(String::as_str)
209 }
210}
211
212impl TryFrom<&RObject> for CharacterMatrix {
213 type Error = ViewError;
214
215 fn try_from(value: &RObject) -> Result<Self, Self::Error> {
216 Self::from_object(value)
217 }
218}
219
220fn decode_names(
221 object: &RObject,
222 expected: usize,
223 axis: usize,
224) -> Result<Option<Vec<Option<String>>>, ViewError> {
225 let values = match object.value() {
226 RValue::Null => return Ok(None),
227 RValue::Character(values) => values,
228 value => {
229 return Err(unexpected_type(
230 &format!("CharacterMatrix.attributes.dimnames[{axis}]"),
231 "NULL or character vector",
232 kind_name(value),
233 ));
234 }
235 };
236 if values.len() != expected {
237 return Err(unexpected_length(
238 &format!("CharacterMatrix.attributes.dimnames[{axis}]"),
239 &expected.to_string(),
240 values.len(),
241 ));
242 }
243
244 values
245 .iter()
246 .enumerate()
247 .map(|(index, value)| {
248 decode_string(value, || {
249 format!("CharacterMatrix.attributes.dimnames[{axis}][{index}]")
250 })
251 })
252 .collect::<Result<Vec<_>, _>>()
253 .map(Some)
254}
255
256fn decode_string(value: &RStr, path: impl FnOnce() -> String) -> Result<Option<String>, ViewError> {
260 match value.as_str() {
261 None => Ok(None),
262 Some(Ok(value)) => Ok(Some(value.into_owned())),
263 Some(Err(_)) => Err(ViewError::InvalidStringEncoding { path: path() }),
264 }
265}
266
267fn missing(path: &str) -> ViewError {
268 ViewError::Missing {
269 path: path.to_owned(),
270 }
271}
272
273fn unexpected_type(path: &str, expected: &'static str, actual: &'static str) -> ViewError {
274 ViewError::UnexpectedType {
275 path: path.to_owned(),
276 expected,
277 actual,
278 }
279}
280
281fn unexpected_length(path: &str, expected: &str, actual: usize) -> ViewError {
282 ViewError::UnexpectedLength {
283 path: path.to_owned(),
284 expected: expected.to_owned(),
285 actual,
286 }
287}
288
289fn invalid_dimensions(path: &str, reason: &str) -> ViewError {
290 ViewError::InvalidDimensions {
291 path: path.to_owned(),
292 reason: reason.to_owned(),
293 }
294}
295
296fn kind_name(value: &RValue) -> &'static str {
297 match value {
298 RValue::Null => "NULL",
299 RValue::Logical(_) => "logical vector",
300 RValue::Integer(_) => "integer vector",
301 RValue::Real(_) => "real vector",
302 RValue::Character(_) => "character vector",
303 RValue::List(_) => "list",
304 RValue::Symbol(_) => "symbol",
305 RValue::Persisted(_) => "persisted reference",
306 RValue::Environment(_) => "environment",
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::{Attribute, Attributes, REncoding, Symbol};
314
315 fn strings(values: &[Option<&str>]) -> Vec<RStr> {
316 values
317 .iter()
318 .map(|value| match value {
319 Some(value) => RStr::new(value.as_bytes(), REncoding::Utf8, None),
320 None => RStr::Na,
321 })
322 .collect()
323 }
324
325 fn character(values: &[Option<&str>]) -> RObject {
326 RObject::from_parts(RValue::Character(strings(values)), Attributes::default())
327 }
328
329 fn null() -> RObject {
330 RObject::from_parts(RValue::Null, Attributes::default())
331 }
332
333 fn matrix(
334 dim: Option<Vec<Option<i32>>>,
335 dimnames: Option<Vec<RObject>>,
336 values: &[Option<&str>],
337 ) -> RObject {
338 let mut attributes = Vec::new();
339 if let Some(dim) = dim {
340 attributes.push(Attribute::new(
341 Symbol::new("dim"),
342 RObject::from_parts(RValue::Integer(dim), Attributes::default()),
343 ));
344 }
345 if let Some(dimnames) = dimnames {
346 attributes.push(Attribute::new(
347 Symbol::new("dimnames"),
348 RObject::from_parts(RValue::List(dimnames), Attributes::default()),
349 ));
350 }
351 RObject::from_parts(
352 RValue::Character(strings(values)),
353 Attributes::new(attributes),
354 )
355 }
356
357 #[test]
358 fn requires_valid_dimensions_and_matching_cell_count() {
359 let missing_dim = matrix(None, None, &[]);
360 assert!(matches!(
361 CharacterMatrix::from_object(&missing_dim),
362 Err(ViewError::Missing { ref path }) if path == "CharacterMatrix.attributes.dim"
363 ));
364
365 let short_dim = matrix(Some(vec![Some(1)]), None, &[Some("x")]);
366 assert!(matches!(
367 CharacterMatrix::from_object(&short_dim),
368 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dim"
369 ));
370
371 let negative_dim = matrix(Some(vec![Some(-1), Some(0)]), None, &[]);
372 assert!(matches!(
373 CharacterMatrix::from_object(&negative_dim),
374 Err(ViewError::InvalidDimensions { ref path, .. }) if path == "CharacterMatrix.attributes.dim"
375 ));
376
377 let wrong_cell_count = matrix(Some(vec![Some(2), Some(2)]), None, &[Some("x")]);
378 assert!(matches!(
379 CharacterMatrix::from_object(&wrong_cell_count),
380 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix"
381 ));
382 }
383
384 #[test]
385 fn converts_to_row_major_and_preserves_na_and_empty_strings() {
386 let object = matrix(
387 Some(vec![Some(2), Some(2)]),
388 None,
389 &[Some("a"), None, Some(""), Some("d")],
390 );
391 let matrix = CharacterMatrix::try_from(&object).expect("valid matrix");
392
393 assert_eq!(matrix.nrow(), 2);
394 assert_eq!(matrix.ncol(), 2);
395 assert_eq!(matrix.get(0, 0), Some(Some("a")));
396 assert_eq!(matrix.get(0, 1), Some(Some("")));
397 assert_eq!(matrix.get(1, 0), Some(None));
398 assert_eq!(matrix.get(1, 1), Some(Some("d")));
399 assert_eq!(matrix.get(2, 0), None);
400 assert_eq!(matrix.get(0, 2), None);
401 assert_eq!(matrix.row_name(0), None);
402 assert_eq!(matrix.column_name(0), None);
403 }
404
405 #[test]
406 fn accepts_optional_well_shaped_dimnames() {
407 let object = matrix(
408 Some(vec![Some(2), Some(1)]),
409 Some(vec![character(&[Some("one"), None]), null()]),
410 &[Some("a"), Some("b")],
411 );
412 let matrix = CharacterMatrix::from_object(&object).expect("valid dimnames");
413
414 assert_eq!(matrix.row_name(0), Some("one"));
415 assert_eq!(matrix.row_name(1), None);
416 assert_eq!(matrix.row_name(2), None);
417 assert_eq!(matrix.column_name(0), None);
418 }
419
420 #[test]
421 fn rejects_malformed_dimnames_shapes() {
422 let short_list = matrix(
423 Some(vec![Some(1), Some(1)]),
424 Some(vec![null()]),
425 &[Some("x")],
426 );
427 assert!(matches!(
428 CharacterMatrix::from_object(&short_list),
429 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dimnames"
430 ));
431
432 let wrong_axis_length = matrix(
433 Some(vec![Some(1), Some(1)]),
434 Some(vec![character(&[Some("one"), Some("two")]), null()]),
435 &[Some("x")],
436 );
437 assert!(matches!(
438 CharacterMatrix::from_object(&wrong_axis_length),
439 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dimnames[0]"
440 ));
441 }
442}