dataset_ml/table.rs
1//! Column storage for a parsed dataset.
2//!
3//! Every loader in this crate parses its source into a [`Table`] and returns
4//! that table. This module holds the table and the two types it is built from.
5//!
6//! # Contents
7//!
8//! - [`ColumnData`] holds the values of one column, in the type the source
9//! uses. It has one variant per storage type: [`Numeric`](ColumnData::Numeric),
10//! [`Integer`](ColumnData::Integer), [`String`](ColumnData::String), and
11//! [`Bytes`](ColumnData::Bytes).
12//! - [`Column`] adds the name that the source gives those values.
13//! - [`Table`] holds one [`Column`] per source column. It checks the columns
14//! when it builds them, and it finds a column by name.
15//!
16//! # How a loader fills a table
17//!
18//! A loader builds one [`Column`] for each column of its source, then passes
19//! them all to [`Table::new`]. The table keeps them in source order. The loader
20//! stores each value in the type the source uses, and applies no encoding. The
21//! choice between an ordinal code and a one-hot code stays with the caller.
22//!
23//! # How a caller reads a table
24//!
25//! [`Table::column`] finds a single column by name, and the `as_*` methods of
26//! [`Column`] read its values in their source type.
27//! [`Table::numeric_matrix`] builds one `f64` matrix out of the columns the
28//! caller names, in the order the caller names them.
29//!
30//! Each loader lists the names of its columns in associated constants, such as
31//! `Iris::FEATURE_NAMES` and `Iris::TARGET`. Pass one of these constants to
32//! [`Table::numeric_matrix`], or name the columns directly.
33//!
34//! # Guarantees
35//!
36//! [`Table::new`] checks these three conditions before it builds a table. Every
37//! method of [`Table`] relies on them:
38//!
39//! - The table holds at least one column.
40//! - Every column holds the same number of samples.
41//! - No two columns share a name.
42//!
43//! # Examples
44//!
45//! ```rust
46//! use dataset_ml::table::{Column, ColumnData, Table};
47//! use ndarray::array;
48//!
49//! let table = Table::new(
50//! "example",
51//! vec![
52//! Column::new("width", ColumnData::Numeric(array![1.0, 2.0])),
53//! Column::new("height", ColumnData::Numeric(array![3.0, 4.0])),
54//! Column::new(
55//! "species",
56//! ColumnData::String(array!["a".to_string(), "b".to_string()]),
57//! ),
58//! ],
59//! )
60//! .unwrap();
61//!
62//! assert_eq!(table.n_samples(), 2);
63//!
64//! // Name the columns you want, in the order you want them.
65//! let matrix = table.numeric_matrix(&["height", "width"]).unwrap();
66//! assert_eq!(matrix.shape(), &[2, 2]);
67//! assert_eq!(matrix.row(0).to_vec(), vec![3.0, 1.0]);
68//!
69//! // Reach one column by name, whatever its position.
70//! let species = table.column("species").unwrap().as_string().unwrap();
71//! assert_eq!(species[0], "a");
72//! ```
73
74use dataset_core::DatasetError;
75use ndarray::{Array1, Array2};
76
77/// The values of one column, in the type the source uses.
78///
79/// Every variant except [`ColumnData::Bytes`] holds one value per sample.
80/// `Bytes` holds one **row** per sample, for a source whose columns have no
81/// individual names.
82///
83/// # Examples
84///
85/// ```rust
86/// use dataset_ml::table::ColumnData;
87/// use ndarray::array;
88///
89/// let counts = ColumnData::Integer(array![3, 5, 8]);
90/// assert_eq!(counts.len(), 3);
91/// assert_eq!(counts.kind(), "integer");
92/// assert_eq!(counts.width(), 1);
93/// ```
94#[derive(Debug, Clone, PartialEq)]
95pub enum ColumnData {
96 /// A value with a fraction. A missing value is `f64::NAN`.
97 Numeric(Array1<f64>),
98 /// A whole number.
99 Integer(Array1<i64>),
100 /// Text, spelled as the source spells it. A missing value is the empty
101 /// string, unless the dataset documents its own token.
102 String(Array1<String>),
103 /// A fixed-width row of unsigned bytes per sample, such as the pixels of an
104 /// image. The columns inside the row have no individual names.
105 Bytes(Array2<u8>),
106}
107
108impl ColumnData {
109 /// The number of samples the column holds.
110 ///
111 /// # Returns
112 ///
113 /// - `usize` - the sample count. For [`ColumnData::Bytes`], this is the
114 /// number of rows, not the number of bytes.
115 pub fn len(&self) -> usize {
116 match self {
117 ColumnData::Numeric(values) => values.len(),
118 ColumnData::Integer(values) => values.len(),
119 ColumnData::String(values) => values.len(),
120 ColumnData::Bytes(values) => values.nrows(),
121 }
122 }
123
124 /// Whether the column holds no sample.
125 ///
126 /// # Returns
127 ///
128 /// - `bool` - `true` if the column holds no sample, and `false` if it holds
129 /// at least one.
130 pub fn is_empty(&self) -> bool {
131 self.len() == 0
132 }
133
134 /// The name of the variant this column holds.
135 ///
136 /// # Returns
137 ///
138 /// - `&'static str` - one of `"numeric"`, `"integer"`, `"string"`, or
139 /// `"bytes"`.
140 ///
141 /// # Notes
142 ///
143 /// [`Table::numeric_matrix`] puts this name in the error it returns for a
144 /// column it cannot read as a number.
145 pub fn kind(&self) -> &'static str {
146 match self {
147 ColumnData::Numeric(_) => "numeric",
148 ColumnData::Integer(_) => "integer",
149 ColumnData::String(_) => "string",
150 ColumnData::Bytes(_) => "bytes",
151 }
152 }
153
154 /// The number of values this column contributes to one row of a matrix.
155 ///
156 /// # Returns
157 ///
158 /// - `usize` - the row width. Every variant contributes `1`, except
159 /// [`ColumnData::Bytes`], which contributes the number of bytes in one of
160 /// its rows.
161 pub fn width(&self) -> usize {
162 match self {
163 ColumnData::Bytes(values) => values.ncols(),
164 _ => 1,
165 }
166 }
167}
168
169/// One named column of a [`Table`].
170///
171/// A column pairs the values of one source column with the name that the source
172/// gives it. The name is fixed when the column is built.
173///
174/// # Examples
175///
176/// ```rust
177/// use dataset_ml::table::{Column, ColumnData};
178/// use ndarray::array;
179///
180/// let column = Column::new("petal_width", ColumnData::Numeric(array![0.2, 1.4]));
181///
182/// assert_eq!(column.name(), "petal_width");
183/// assert_eq!(column.len(), 2);
184/// assert_eq!(column.as_numeric().unwrap()[0], 0.2);
185///
186/// // An `as_*` method returns `None` for every other variant.
187/// assert!(column.as_string().is_none());
188/// ```
189#[derive(Debug, Clone, PartialEq)]
190pub struct Column {
191 /// The name the source gives this column.
192 name: &'static str,
193 /// The values in this column.
194 data: ColumnData,
195}
196
197impl Column {
198 /// Build a column from its name and its values.
199 ///
200 /// # Parameters
201 ///
202 /// - `name` - The name the source gives the column.
203 /// - `data` - The values in the column.
204 ///
205 /// # Returns
206 ///
207 /// - `Self` - the new column.
208 pub fn new(name: &'static str, data: ColumnData) -> Self {
209 Column { name, data }
210 }
211
212 /// The column's name.
213 ///
214 /// # Returns
215 ///
216 /// - `&'static str` - the name, as the source spells it.
217 pub fn name(&self) -> &'static str {
218 self.name
219 }
220
221 /// The column's values.
222 ///
223 /// # Returns
224 ///
225 /// - `&ColumnData` - a shared reference to the values.
226 pub fn data(&self) -> &ColumnData {
227 &self.data
228 }
229
230 /// The column's values, for in-place editing.
231 ///
232 /// # Returns
233 ///
234 /// - `&mut ColumnData` - a mutable reference to the values.
235 ///
236 /// # Notes
237 ///
238 /// Change a value, but do not change the number of samples. A [`Table`] that
239 /// holds this column relies on every column having the same length.
240 pub fn data_mut(&mut self) -> &mut ColumnData {
241 &mut self.data
242 }
243
244 /// The number of samples the column holds.
245 ///
246 /// # Returns
247 ///
248 /// - `usize` - the sample count.
249 pub fn len(&self) -> usize {
250 self.data.len()
251 }
252
253 /// Whether the column holds no sample.
254 ///
255 /// # Returns
256 ///
257 /// - `bool` - `true` if the column holds no sample, and `false` if it holds
258 /// at least one.
259 pub fn is_empty(&self) -> bool {
260 self.data.is_empty()
261 }
262
263 /// The values, if the column is [`ColumnData::Numeric`].
264 ///
265 /// # Returns
266 ///
267 /// - `Some(&Array1<f64>)` - the values, one per sample.
268 /// - `None` - if the column holds another variant.
269 pub fn as_numeric(&self) -> Option<&Array1<f64>> {
270 match &self.data {
271 ColumnData::Numeric(values) => Some(values),
272 _ => None,
273 }
274 }
275
276 /// The values, if the column is [`ColumnData::Integer`].
277 ///
278 /// # Returns
279 ///
280 /// - `Some(&Array1<i64>)` - the values, one per sample.
281 /// - `None` - if the column holds another variant.
282 pub fn as_integer(&self) -> Option<&Array1<i64>> {
283 match &self.data {
284 ColumnData::Integer(values) => Some(values),
285 _ => None,
286 }
287 }
288
289 /// The values, if the column is [`ColumnData::String`].
290 ///
291 /// # Returns
292 ///
293 /// - `Some(&Array1<String>)` - the values, one per sample.
294 /// - `None` - if the column holds another variant.
295 pub fn as_string(&self) -> Option<&Array1<String>> {
296 match &self.data {
297 ColumnData::String(values) => Some(values),
298 _ => None,
299 }
300 }
301
302 /// The values, if the column is [`ColumnData::Bytes`].
303 ///
304 /// # Returns
305 ///
306 /// - `Some(&Array2<u8>)` - the rows, one per sample.
307 /// - `None` - if the column holds another variant.
308 pub fn as_bytes(&self) -> Option<&Array2<u8>> {
309 match &self.data {
310 ColumnData::Bytes(values) => Some(values),
311 _ => None,
312 }
313 }
314
315 /// The values as `f64`, one per sample.
316 ///
317 /// # Returns
318 ///
319 /// - `Some(Array1<f64>)` - the values. [`ColumnData::Numeric`] returns them
320 /// unchanged, and [`ColumnData::Integer`] converts each one.
321 /// - `None` - if the column is [`ColumnData::String`], which has no numeric
322 /// reading, or [`ColumnData::Bytes`], which holds more than one value per
323 /// sample. For a `Bytes` column, use [`Table::numeric_matrix`].
324 ///
325 /// # Notes
326 ///
327 /// An `i64` above 2^53 loses precision as an `f64`.
328 pub fn to_numeric(&self) -> Option<Array1<f64>> {
329 match &self.data {
330 ColumnData::Numeric(values) => Some(values.clone()),
331 ColumnData::Integer(values) => Some(values.mapv(|value| value as f64)),
332 _ => None,
333 }
334 }
335}
336
337/// A parsed dataset: named columns of equal length.
338///
339/// A table holds one [`Column`] per source column, in source order, together
340/// with the dataset name. [`Table::new`] checks the columns, so every table a
341/// caller receives meets the guarantees in the [module documentation](self).
342///
343/// # Examples
344///
345/// ```rust
346/// use dataset_ml::table::{Column, ColumnData, Table};
347/// use ndarray::array;
348///
349/// let table = Table::new(
350/// "example",
351/// vec![
352/// Column::new("id", ColumnData::Integer(array![1, 2])),
353/// Column::new("width", ColumnData::Numeric(array![1.5, 2.5])),
354/// ],
355/// )
356/// .unwrap();
357///
358/// assert_eq!(table.name(), "example");
359/// assert_eq!(table.n_samples(), 2);
360/// assert_eq!(table.n_columns(), 2);
361/// assert_eq!(table.names().collect::<Vec<_>>(), vec!["id", "width"]);
362/// ```
363#[derive(Debug, Clone, PartialEq)]
364pub struct Table {
365 /// The dataset name. It appears in the errors this table returns.
366 name: &'static str,
367 /// The columns, in the order the source lists them.
368 columns: Vec<Column>,
369 /// The number of samples every column holds.
370 n_samples: usize,
371}
372
373impl Table {
374 /// Build a table and check its columns.
375 ///
376 /// # Parameters
377 ///
378 /// - `name` - The dataset name. It appears in the errors this table
379 /// returns.
380 /// - `columns` - The columns, in the order the source lists them.
381 ///
382 /// # Returns
383 ///
384 /// - `Self` - the new table, if the columns pass every check.
385 ///
386 /// # Errors
387 ///
388 /// - `DatasetError::DataFormatError` with `EmptyDataset` - if `columns` is
389 /// empty, or if the columns hold no sample.
390 /// - `DatasetError::DataFormatError` with `LengthMismatch` - if two columns
391 /// hold a different number of samples.
392 /// - `DatasetError::DataFormatError` with `InvalidValue` - if two columns
393 /// share a name.
394 pub fn new(name: &'static str, columns: Vec<Column>) -> Result<Self, DatasetError> {
395 let Some(first) = columns.first() else {
396 return Err(DatasetError::empty_dataset(name));
397 };
398
399 let n_samples = first.len();
400 if n_samples == 0 {
401 return Err(DatasetError::empty_dataset(name));
402 }
403
404 for column in &columns {
405 if column.len() != n_samples {
406 return Err(DatasetError::length_mismatch(
407 name,
408 column.name(),
409 n_samples,
410 column.len(),
411 ));
412 }
413 }
414
415 for (index, column) in columns.iter().enumerate() {
416 if columns[..index]
417 .iter()
418 .any(|other| other.name() == column.name())
419 {
420 return Err(DatasetError::invalid_value(
421 name,
422 "column name",
423 column.name(),
424 index + 1,
425 ));
426 }
427 }
428
429 Ok(Table {
430 name,
431 columns,
432 n_samples,
433 })
434 }
435
436 /// The dataset name.
437 ///
438 /// # Returns
439 ///
440 /// - `&'static str` - the name the loader passed to [`Table::new`].
441 pub fn name(&self) -> &'static str {
442 self.name
443 }
444
445 /// The number of samples every column holds.
446 ///
447 /// # Returns
448 ///
449 /// - `usize` - the sample count. It is always at least `1`.
450 pub fn n_samples(&self) -> usize {
451 self.n_samples
452 }
453
454 /// The number of columns.
455 ///
456 /// # Returns
457 ///
458 /// - `usize` - the column count. It is always at least `1`.
459 pub fn n_columns(&self) -> usize {
460 self.columns.len()
461 }
462
463 /// Every column, in source order.
464 ///
465 /// # Returns
466 ///
467 /// - `&[Column]` - a shared slice of every column.
468 pub fn columns(&self) -> &[Column] {
469 &self.columns
470 }
471
472 /// Every column, in source order, for in-place editing.
473 ///
474 /// # Returns
475 ///
476 /// - `&mut [Column]` - a mutable slice of every column.
477 ///
478 /// # Notes
479 ///
480 /// A change to a value keeps the table valid. Do not change the length of a
481 /// column: the table's guarantees no longer hold if you do.
482 pub fn columns_mut(&mut self) -> &mut [Column] {
483 &mut self.columns
484 }
485
486 /// Every column name, in source order.
487 ///
488 /// # Returns
489 ///
490 /// - `impl Iterator<Item = &'static str>` - the names, in source order.
491 pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
492 self.columns.iter().map(Column::name)
493 }
494
495 /// Find one column by name.
496 ///
497 /// # Parameters
498 ///
499 /// - `name` - The name to look for. The comparison is exact.
500 ///
501 /// # Returns
502 ///
503 /// - `Some(&Column)` - the column of that name.
504 /// - `None` - if the table holds no column of that name.
505 pub fn column(&self, name: &str) -> Option<&Column> {
506 self.columns.iter().find(|column| column.name() == name)
507 }
508
509 /// Find one column by name, for in-place editing.
510 ///
511 /// # Parameters
512 ///
513 /// - `name` - The name to look for. The comparison is exact.
514 ///
515 /// # Returns
516 ///
517 /// - `Some(&mut Column)` - the column of that name.
518 /// - `None` - if the table holds no column of that name.
519 ///
520 /// # Notes
521 ///
522 /// A change to a value keeps the table valid. Do not change the length of
523 /// the column: the table's guarantees no longer hold if you do.
524 pub fn column_mut(&mut self, name: &str) -> Option<&mut Column> {
525 self.columns.iter_mut().find(|column| column.name() == name)
526 }
527
528 /// Build one `f64` matrix out of the named columns.
529 ///
530 /// The matrix keeps the order of `names`, which does not have to be the
531 /// source order. A name may repeat, and the matrix then holds that column
532 /// once per mention.
533 ///
534 /// # Parameters
535 ///
536 /// - `names` - The columns to put in the matrix, in the order you want
537 /// them. A [`ColumnData::Bytes`] column contributes its full row width,
538 /// and every other column contributes one value.
539 ///
540 /// # Returns
541 ///
542 /// - `Array2<f64>` - a matrix of [`Table::n_samples`] rows. Its width is the
543 /// sum of the [`ColumnData::width`] of every named column.
544 ///
545 /// # Errors
546 ///
547 /// - `DatasetError::DataFormatError` with `LengthMismatch` - if `names` is
548 /// empty.
549 /// - `DatasetError::DataFormatError` with `UnknownColumn` - if the table
550 /// holds no column of a given name.
551 /// - `DatasetError::DataFormatError` with `ColumnTypeMismatch` - if a named
552 /// column is [`ColumnData::String`], which has no numeric reading.
553 ///
554 /// # Performance
555 ///
556 /// This builds a new matrix on every call, and that matrix holds
557 /// `n_samples × width` values. Call it once and keep the result.
558 pub fn numeric_matrix(&self, names: &[&str]) -> Result<Array2<f64>, DatasetError> {
559 if names.is_empty() {
560 return Err(DatasetError::length_mismatch(
561 self.name,
562 "requested columns",
563 1,
564 0,
565 ));
566 }
567
568 let mut selected = Vec::with_capacity(names.len());
569 for name in names {
570 let Some(column) = self.column(name) else {
571 return Err(DatasetError::unknown_column(self.name, name));
572 };
573 selected.push(column);
574 }
575
576 let width: usize = selected.iter().map(|column| column.data().width()).sum();
577 let mut values: Vec<f64> = Vec::with_capacity(self.n_samples * width);
578
579 for row in 0..self.n_samples {
580 for column in &selected {
581 match column.data() {
582 ColumnData::Numeric(source) => values.push(source[row]),
583 ColumnData::Integer(source) => values.push(source[row] as f64),
584 ColumnData::Bytes(source) => {
585 for col in 0..source.ncols() {
586 values.push(f64::from(source[[row, col]]));
587 }
588 }
589 other => {
590 return Err(DatasetError::column_type_mismatch(
591 self.name,
592 column.name(),
593 "numeric",
594 other.kind(),
595 ));
596 }
597 }
598 }
599 }
600
601 Array2::from_shape_vec((self.n_samples, width), values)
602 .map_err(|e| DatasetError::array_shape_error(self.name, "numeric matrix", e))
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use ndarray::array;
610
611 fn numeric(name: &'static str, values: [f64; 3]) -> Column {
612 Column::new(name, ColumnData::Numeric(Array1::from_vec(values.to_vec())))
613 }
614
615 fn sample_table() -> Table {
616 Table::new(
617 "sample",
618 vec![
619 numeric("a", [1.0, 2.0, 3.0]),
620 numeric("b", [4.0, 5.0, 6.0]),
621 Column::new(
622 "label",
623 ColumnData::String(array!["x".into(), "y".into(), "x".into()]),
624 ),
625 ],
626 )
627 .unwrap()
628 }
629
630 #[test]
631 fn new_rejects_an_empty_column_list() {
632 assert!(Table::new("t", vec![]).is_err());
633 }
634
635 #[test]
636 fn new_rejects_zero_samples() {
637 let column = Column::new("a", ColumnData::Numeric(Array1::zeros(0)));
638 assert!(Table::new("t", vec![column]).is_err());
639 }
640
641 #[test]
642 fn new_rejects_columns_of_different_lengths() {
643 let short = Column::new("a", ColumnData::Numeric(array![1.0, 2.0]));
644 let long = Column::new("b", ColumnData::Numeric(array![1.0, 2.0, 3.0]));
645 let error = Table::new("t", vec![short, long]).unwrap_err().to_string();
646 assert!(error.contains("expected 2"), "{error}");
647 }
648
649 #[test]
650 fn new_rejects_a_repeated_name() {
651 let one = numeric("a", [1.0, 2.0, 3.0]);
652 let two = numeric("a", [4.0, 5.0, 6.0]);
653 assert!(Table::new("t", vec![one, two]).is_err());
654 }
655
656 #[test]
657 fn a_table_reports_its_name_shape_and_column_names() {
658 let table = sample_table();
659 assert_eq!(table.name(), "sample");
660 assert_eq!(table.n_samples(), 3);
661 assert_eq!(table.n_columns(), 3);
662 assert_eq!(table.names().collect::<Vec<_>>(), vec!["a", "b", "label"]);
663 }
664
665 #[test]
666 fn column_lookup_is_by_name_not_position() {
667 let table = sample_table();
668 assert_eq!(table.column("b").unwrap().as_numeric().unwrap()[0], 4.0);
669 assert!(table.column("missing").is_none());
670 }
671
672 #[test]
673 fn numeric_matrix_keeps_the_requested_order() {
674 let table = sample_table();
675 let matrix = table.numeric_matrix(&["b", "a"]).unwrap();
676 assert_eq!(matrix.shape(), &[3, 2]);
677 assert_eq!(matrix.row(0).to_vec(), vec![4.0, 1.0]);
678 assert_eq!(matrix.row(2).to_vec(), vec![6.0, 3.0]);
679 }
680
681 #[test]
682 fn numeric_matrix_takes_a_subset() {
683 let table = sample_table();
684 let matrix = table.numeric_matrix(&["a"]).unwrap();
685 assert_eq!(matrix.shape(), &[3, 1]);
686 }
687
688 #[test]
689 fn numeric_matrix_repeats_a_repeated_name() {
690 let table = sample_table();
691 let matrix = table.numeric_matrix(&["a", "a"]).unwrap();
692 assert_eq!(matrix.shape(), &[3, 2]);
693 assert_eq!(matrix.row(1).to_vec(), vec![2.0, 2.0]);
694 }
695
696 #[test]
697 fn numeric_matrix_converts_integers() {
698 let table = Table::new(
699 "t",
700 vec![
701 Column::new("count", ColumnData::Integer(array![1, 2])),
702 Column::new("when", ColumnData::Integer(array![10, 20])),
703 ],
704 )
705 .unwrap();
706 let matrix = table.numeric_matrix(&["count", "when"]).unwrap();
707 assert_eq!(matrix.row(1).to_vec(), vec![2.0, 20.0]);
708 }
709
710 #[test]
711 fn numeric_matrix_expands_a_bytes_column_to_its_width() {
712 let pixels = Array2::from_shape_vec((2, 3), vec![1u8, 2, 3, 4, 5, 6]).unwrap();
713 let table =
714 Table::new("t", vec![Column::new("pixels", ColumnData::Bytes(pixels))]).unwrap();
715 let matrix = table.numeric_matrix(&["pixels"]).unwrap();
716 assert_eq!(matrix.shape(), &[2, 3]);
717 assert_eq!(matrix.row(1).to_vec(), vec![4.0, 5.0, 6.0]);
718 }
719
720 #[test]
721 fn numeric_matrix_rejects_an_empty_request() {
722 let table = sample_table();
723 let error = table.numeric_matrix(&[]).unwrap_err().to_string();
724 assert!(error.contains("requested columns"), "{error}");
725 }
726
727 #[test]
728 fn numeric_matrix_rejects_an_unknown_name() {
729 let table = sample_table();
730 let error = table.numeric_matrix(&["a", "missing"]).unwrap_err();
731 let message = error.to_string();
732 assert!(message.contains("no column named `missing`"), "{message}");
733 }
734
735 #[test]
736 fn numeric_matrix_rejects_a_string_column() {
737 let table = sample_table();
738 let message = table.numeric_matrix(&["label"]).unwrap_err().to_string();
739 assert!(message.contains("`label`"), "{message}");
740 assert!(message.contains("expected `numeric`"), "{message}");
741 }
742
743 #[test]
744 fn numeric_matrix_names_the_string_column_wherever_it_sits() {
745 let table = sample_table();
746 // The offending column is neither first nor last in the request.
747 let message = table
748 .numeric_matrix(&["a", "label", "b"])
749 .unwrap_err()
750 .to_string();
751 assert!(message.contains("`label`"), "{message}");
752 assert!(message.contains("`string`"), "{message}");
753 }
754
755 #[test]
756 fn to_numeric_reads_numbers_and_refuses_the_rest() {
757 let pixels = Array2::<u8>::zeros((3, 4));
758 let table = Table::new(
759 "t",
760 vec![
761 numeric("a", [1.0, 2.0, 3.0]),
762 Column::new("count", ColumnData::Integer(array![1, 2, 3])),
763 Column::new(
764 "label",
765 ColumnData::String(Array1::from_vec(vec!["x".to_string(); 3])),
766 ),
767 Column::new("pixels", ColumnData::Bytes(pixels)),
768 ],
769 )
770 .unwrap();
771 assert_eq!(table.column("a").unwrap().to_numeric().unwrap()[0], 1.0);
772 assert_eq!(table.column("count").unwrap().to_numeric().unwrap()[2], 3.0);
773 assert!(table.column("label").unwrap().to_numeric().is_none());
774 assert!(table.column("pixels").unwrap().to_numeric().is_none());
775 }
776
777 #[test]
778 fn column_mut_edits_in_place() {
779 let mut table = sample_table();
780 if let Some(ColumnData::Numeric(values)) = table.column_mut("a").map(Column::data_mut) {
781 values[0] = 99.0;
782 }
783 assert_eq!(table.column("a").unwrap().as_numeric().unwrap()[0], 99.0);
784 }
785
786 #[test]
787 fn width_is_one_except_for_bytes() {
788 assert_eq!(ColumnData::Numeric(array![1.0]).width(), 1);
789 assert_eq!(ColumnData::Integer(array![1]).width(), 1);
790 assert_eq!(ColumnData::String(array!["x".to_string()]).width(), 1);
791 let pixels = Array2::<u8>::zeros((1, 5));
792 assert_eq!(ColumnData::Bytes(pixels).width(), 5);
793 }
794}