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, NativeEncodingSource, 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(
320 value.as_bytes(),
321 REncoding::Utf8,
322 NativeEncodingSource::Unknown,
323 ),
324 None => RStr::Na,
325 })
326 .collect()
327 }
328
329 fn character(values: &[Option<&str>]) -> RObject {
330 RObject::from_parts(RValue::Character(strings(values)), Attributes::default())
331 }
332
333 fn null() -> RObject {
334 RObject::from_parts(RValue::Null, Attributes::default())
335 }
336
337 fn matrix(
338 dim: Option<Vec<Option<i32>>>,
339 dimnames: Option<Vec<RObject>>,
340 values: &[Option<&str>],
341 ) -> RObject {
342 let mut attributes = Vec::new();
343 if let Some(dim) = dim {
344 attributes.push(Attribute::new(
345 Symbol::new("dim"),
346 RObject::from_parts(RValue::Integer(dim), Attributes::default()),
347 ));
348 }
349 if let Some(dimnames) = dimnames {
350 attributes.push(Attribute::new(
351 Symbol::new("dimnames"),
352 RObject::from_parts(RValue::List(dimnames), Attributes::default()),
353 ));
354 }
355 RObject::from_parts(
356 RValue::Character(strings(values)),
357 Attributes::new(attributes),
358 )
359 }
360
361 #[test]
362 fn requires_valid_dimensions_and_matching_cell_count() {
363 let missing_dim = matrix(None, None, &[]);
364 assert!(matches!(
365 CharacterMatrix::from_object(&missing_dim),
366 Err(ViewError::Missing { ref path }) if path == "CharacterMatrix.attributes.dim"
367 ));
368
369 let short_dim = matrix(Some(vec![Some(1)]), None, &[Some("x")]);
370 assert!(matches!(
371 CharacterMatrix::from_object(&short_dim),
372 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dim"
373 ));
374
375 let negative_dim = matrix(Some(vec![Some(-1), Some(0)]), None, &[]);
376 assert!(matches!(
377 CharacterMatrix::from_object(&negative_dim),
378 Err(ViewError::InvalidDimensions { ref path, .. }) if path == "CharacterMatrix.attributes.dim"
379 ));
380
381 let wrong_cell_count = matrix(Some(vec![Some(2), Some(2)]), None, &[Some("x")]);
382 assert!(matches!(
383 CharacterMatrix::from_object(&wrong_cell_count),
384 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix"
385 ));
386 }
387
388 #[test]
389 fn converts_to_row_major_and_preserves_na_and_empty_strings() {
390 let object = matrix(
391 Some(vec![Some(2), Some(2)]),
392 None,
393 &[Some("a"), None, Some(""), Some("d")],
394 );
395 let matrix = CharacterMatrix::try_from(&object).expect("valid matrix");
396
397 assert_eq!(matrix.nrow(), 2);
398 assert_eq!(matrix.ncol(), 2);
399 assert_eq!(matrix.get(0, 0), Some(Some("a")));
400 assert_eq!(matrix.get(0, 1), Some(Some("")));
401 assert_eq!(matrix.get(1, 0), Some(None));
402 assert_eq!(matrix.get(1, 1), Some(Some("d")));
403 assert_eq!(matrix.get(2, 0), None);
404 assert_eq!(matrix.get(0, 2), None);
405 assert_eq!(matrix.row_name(0), None);
406 assert_eq!(matrix.column_name(0), None);
407 }
408
409 #[test]
410 fn accepts_optional_well_shaped_dimnames() {
411 let object = matrix(
412 Some(vec![Some(2), Some(1)]),
413 Some(vec![character(&[Some("one"), None]), null()]),
414 &[Some("a"), Some("b")],
415 );
416 let matrix = CharacterMatrix::from_object(&object).expect("valid dimnames");
417
418 assert_eq!(matrix.row_name(0), Some("one"));
419 assert_eq!(matrix.row_name(1), None);
420 assert_eq!(matrix.row_name(2), None);
421 assert_eq!(matrix.column_name(0), None);
422 }
423
424 #[test]
425 fn rejects_malformed_dimnames_shapes() {
426 let short_list = matrix(
427 Some(vec![Some(1), Some(1)]),
428 Some(vec![null()]),
429 &[Some("x")],
430 );
431 assert!(matches!(
432 CharacterMatrix::from_object(&short_list),
433 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dimnames"
434 ));
435
436 let wrong_axis_length = matrix(
437 Some(vec![Some(1), Some(1)]),
438 Some(vec![character(&[Some("one"), Some("two")]), null()]),
439 &[Some("x")],
440 );
441 assert!(matches!(
442 CharacterMatrix::from_object(&wrong_axis_length),
443 Err(ViewError::UnexpectedLength { ref path, .. }) if path == "CharacterMatrix.attributes.dimnames[0]"
444 ));
445 }
446}