Skip to main content

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}