ratio_matrix/
lib.rs

1//! # Ratio's matrix data library
2//!
3//! ## License
4//!
5//! This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
6//! If a copy of the MPL was not distributed with this file,
7//! You can obtain one at https://mozilla.org/MPL/2.0/.
8//!
9//! **Code examples both in the docstrings and rendered documentation are free to use.**
10
11use std::marker::PhantomData;
12use std::ops::{Add, AddAssign, Range, Sub, SubAssign};
13
14use snafu::prelude::*;
15
16use crate::traits::{Contains2D, Dimensions2D, Domain2D, Matrix2D, MatrixMut, MatrixRef};
17
18pub mod traits;
19
20#[cfg(test)]
21mod tests;
22
23/// Matrix dimensions don't agree. Values ({n_values}) modulo columns ({n_cols}) should be 0.
24#[derive(Clone, Debug, Snafu)]
25pub struct MatrixDimensionError {
26    /// Number of values.
27    pub n_values: usize,
28    /// Invalid number of columns in a row.
29    pub n_cols: usize,
30}
31
32/// Matrix view out of bounds.
33#[derive(Clone, Debug, Snafu)]
34pub struct MatrixViewOutOfBoundsError {
35    /// Input data dimensions.
36    pub dimensions: MatrixRange,
37    /// Invalid range.
38    pub range: MatrixRange,
39}
40
41/// Matrix cell access out of bounds.
42#[derive(Clone, Debug, Snafu)]
43pub struct CoordinatesOutOfBoundsError {
44    /// Matrix data area.
45    pub range: MatrixRange,
46    /// Attempted access coordinates.
47    pub coordinates: MatrixCoordinates,
48}
49
50/// Two-dimensional matrix.
51#[derive(Clone, Debug, Default, PartialEq)]
52#[cfg_attr(
53    feature = "serde",
54    derive(serde::Serialize, serde::Deserialize),
55    serde(default, rename_all = "camelCase")
56)]
57pub struct Matrix<C> {
58    /// Matrix values as a single vector.
59    pub values: Vec<C>,
60    /// Matrix dimensions.
61    pub dimensions: MatrixDimensions,
62}
63impl<C> Matrix<C> {
64    /// Get the vector index based on a set of coordinates.
65    pub fn index<T: Into<MatrixCoordinates>>(
66        &self,
67        coordinates: T,
68    ) -> Result<usize, CoordinatesOutOfBoundsError> {
69        let x: MatrixCoordinates = coordinates.into();
70        if self.range().contains(&x) {
71            Ok(x.row * self.n_cols() + x.col)
72        } else {
73            Err(CoordinatesOutOfBoundsError {
74                coordinates: x,
75                range: self.range(),
76            })
77        }
78    }
79    /// Slice this matrix to these rows and columns.
80    pub fn slice<R: Into<MatrixRange>>(
81        &self,
82        range: R,
83    ) -> Result<MatrixSlice<'_, C>, MatrixSliceError> {
84        let range: MatrixRange = range.into();
85        if self.range().contains(&range) {
86            Ok(MatrixSlice {
87                values: &self.values,
88                input_dimensions: self.dimensions,
89                view_range: range,
90            })
91        } else {
92            MatrixViewOutOfBoundsSnafu {
93                range,
94                dimensions: self.dimensions,
95            }
96            .fail()
97            .context(SliceRangeOutOfBoundsSnafu)
98        }
99    }
100}
101impl<C> Matrix2D<C> for Matrix<C> {
102    fn new(values: Vec<C>, n_cols: usize) -> Result<Self, MatrixDimensionError> {
103        let n_values = values.len();
104        let dimensions = MatrixDimensions::new(n_values / n_cols, n_cols);
105        if dimensions.len() == n_values {
106            Ok(Self { values, dimensions })
107        } else {
108            MatrixDimensionSnafu { n_values, n_cols }.fail()
109        }
110    }
111}
112impl<C> MatrixRef<C> for Matrix<C> {
113    fn get<T: Into<MatrixCoordinates>>(
114        &self,
115        coordinates: T,
116    ) -> Result<&C, CoordinatesOutOfBoundsError> {
117        let x: MatrixCoordinates = coordinates.into();
118        let index = self.index(x)?;
119        self.values.get(index).context(CoordinatesOutOfBoundsSnafu {
120            range: self.range(),
121            coordinates: x,
122        })
123    }
124}
125impl<C> MatrixMut<C> for Matrix<C> {
126    fn set<T: Into<MatrixCoordinates>>(
127        &mut self,
128        coordinates: T,
129        value: C,
130    ) -> Result<C, CoordinatesOutOfBoundsError> {
131        let coordinates: MatrixCoordinates = coordinates.into();
132        let index = self.index(coordinates)?;
133        if let Some(entry) = self.values.get_mut(index) {
134            Ok(std::mem::replace(entry, value))
135        } else {
136            Err(CoordinatesOutOfBoundsError {
137                range: self.range(),
138                coordinates,
139            })
140        }
141    }
142}
143impl<C> Domain2D for Matrix<C> {
144    fn row_range(&self) -> Range<usize> {
145        0..self.dimensions.n_rows
146    }
147
148    fn col_range(&self) -> Range<usize> {
149        0..self.dimensions.n_cols
150    }
151}
152
153/// Matrix dimensions don't agree. Values ({n_values}) modulo columns ({n_cols}) should be 0.
154#[derive(Clone, Debug, Snafu)]
155pub enum MatrixSliceError {
156    SliceInputDimension { source: MatrixDimensionError },
157    SliceRangeOutOfBounds { source: MatrixViewOutOfBoundsError },
158}
159
160/// Slice of a matrix.
161#[derive(Clone, Debug, Default, PartialEq)]
162pub struct MatrixSlice<'a, C> {
163    /// Input matrix values as a single slice reference.
164    pub values: &'a [C],
165    /// Dimensions of the input data.
166    pub input_dimensions: MatrixDimensions,
167    /// Slice or view range with respect to the input data.
168    pub view_range: MatrixRange,
169}
170impl<'a, C> MatrixSlice<'a, C> {
171    /// Create a new slice from a matrix. Takes a reference to the input values' slice and the
172    /// number of columns in each row as well as the intended range of the slice.
173    pub fn new<R: Into<MatrixRange>>(
174        values: &'a [C],
175        n_cols: usize,
176        range: R,
177    ) -> Result<Self, MatrixSliceError> {
178        let n_values = values.len();
179        let n_rows = n_values / n_cols;
180        let input_dimensions = MatrixDimensions::new(n_rows, n_cols);
181        if values.len() != input_dimensions.len() {
182            return MatrixDimensionSnafu { n_values, n_cols }
183                .fail()
184                .context(SliceInputDimensionSnafu);
185        }
186        let domain: MatrixRange = input_dimensions.into();
187        let range: MatrixRange = range.into();
188        if domain.contains(&range) {
189            Ok(Self {
190                values,
191                input_dimensions,
192                view_range: range,
193            })
194        } else {
195            MatrixViewOutOfBoundsSnafu {
196                dimensions: input_dimensions,
197                range,
198            }
199            .fail()
200            .context(SliceRangeOutOfBoundsSnafu)
201        }
202    }
203
204    /// Get the index for item access in the reference slice.
205    /// The provided coordinates should be with respect to the slice's start!
206    pub fn index<T: Into<MatrixCoordinates>>(
207        &self,
208        coordinates: T,
209    ) -> Result<usize, CoordinatesOutOfBoundsError> {
210        let x: MatrixCoordinates = coordinates.into();
211        let x = x + self.view_range.start;
212        if self.view_range.contains(&x) {
213            Ok(x.row * self.input_dimensions.n_cols + x.col)
214        } else {
215            Err(CoordinatesOutOfBoundsError {
216                coordinates: x,
217                range: self.view_range,
218            })
219        }
220    }
221}
222
223impl<C> MatrixSlice<'_, C>
224where
225    C: Clone + Default,
226{
227    /// Clone the data into this slice into a matrix of it's own size.
228    pub fn to_matrix(&self) -> Matrix<C> {
229        Matrix::new(
230            self.row_range()
231                .flat_map(|row| {
232                    self.col_range().filter_map(move |col| {
233                        self.get(MatrixCoordinates::new(row, col)).ok().cloned()
234                    })
235                })
236                .collect(),
237            self.view_range.col_range().end,
238        )
239        .unwrap_or_default()
240    }
241}
242impl<C> MatrixRef<C> for MatrixSlice<'_, C> {
243    fn get<T: Into<MatrixCoordinates>>(
244        &self,
245        coordinates: T,
246    ) -> Result<&C, CoordinatesOutOfBoundsError> {
247        let coordinates: MatrixCoordinates = coordinates.into();
248        let index = self.index(coordinates)?;
249        self.values.get(index).context(CoordinatesOutOfBoundsSnafu {
250            range: self.view_range,
251            coordinates,
252        })
253    }
254}
255impl<C> Domain2D for MatrixSlice<'_, C> {
256    fn row_range(&self) -> Range<usize> {
257        0..(self.view_range.end.row - self.view_range.start.row)
258    }
259    fn col_range(&self) -> Range<usize> {
260        0..(self.view_range.end.col - self.view_range.start.col)
261    }
262}
263
264/// Matrix cell.
265#[derive(Clone, Debug, PartialEq)]
266pub struct MatrixCellRef<'a, M: MatrixRef<C> + ?Sized, C> {
267    /// Matrix to pull values from.
268    pub matrix: &'a M,
269    /// Coordinates of this cell.
270    pub coordinates: MatrixCoordinates,
271    /// Phantom data marker for the cell value type.
272    _phantom: PhantomData<C>,
273}
274impl<'a, M: MatrixRef<C> + ?Sized, C> MatrixCellRef<'a, M, C> {
275    /// Create a new cell reference using the given coordinates.
276    pub fn new<T: Into<MatrixCoordinates>>(matrix: &'a M, coordinates: T) -> Self {
277        Self {
278            matrix,
279            coordinates: coordinates.into(),
280            _phantom: PhantomData,
281        }
282    }
283}
284
285/// Location in a 2D matrix.
286#[derive(Copy, Clone, Debug, PartialEq)]
287#[cfg_attr(
288    feature = "serde",
289    derive(serde::Serialize, serde::Deserialize),
290    serde(rename_all = "camelCase")
291)]
292pub enum MatrixLocation {
293    /// Entire row in a matrix.
294    Row(usize),
295    /// Entire column in a matrix.
296    Col(usize),
297    /// Specific coordinates in a matrix.
298    Coordinates(MatrixCoordinates),
299    /// 2D area in a matrix.
300    Range(MatrixRange),
301}
302impl Default for MatrixLocation {
303    fn default() -> Self {
304        Self::Coordinates(Default::default())
305    }
306}
307impl From<MatrixCoordinates> for MatrixLocation {
308    fn from(value: MatrixCoordinates) -> Self {
309        Self::Coordinates(value)
310    }
311}
312impl From<MatrixRange> for MatrixLocation {
313    fn from(value: MatrixRange) -> Self {
314        Self::Range(value)
315    }
316}
317impl MatrixLocation {
318    /// Create a new optional location from an optional row and column coordinate.
319    pub fn from_coords(row: Option<usize>, col: Option<usize>) -> Option<Self> {
320        match (row, col) {
321            (Some(row), Some(col)) => Some(Self::Coordinates(MatrixCoordinates::new(row, col))),
322            (Some(row), None) => Some(Self::Row(row)),
323            (None, Some(col)) => Some(Self::Col(col)),
324            (None, None) => None,
325        }
326    }
327    /// Row or starting row of this location.
328    pub fn row(&self) -> Option<usize> {
329        match self {
330            Self::Row(row) => Some(*row),
331            Self::Col(_) => None,
332            Self::Coordinates(coord) => Some(coord.row),
333            Self::Range(area) => Some(area.row_range().start),
334        }
335    }
336    /// Column or starting column of this location.
337    pub fn col(&self) -> Option<usize> {
338        match self {
339            Self::Row(_) => None,
340            Self::Col(col) => Some(*col),
341            Self::Coordinates(coord) => Some(coord.col),
342            Self::Range(area) => Some(area.col_range().start),
343        }
344    }
345}
346impl Add<MatrixCoordinates> for MatrixLocation {
347    type Output = MatrixLocation;
348    fn add(self, rhs: MatrixCoordinates) -> Self::Output {
349        match self {
350            Self::Col(col) => Self::Col(col + rhs.col),
351            Self::Coordinates(x) => Self::Coordinates(x + rhs),
352            Self::Range(range) => Self::Range(range + rhs),
353            Self::Row(row) => Self::Row(row + rhs.row),
354        }
355    }
356}
357impl AddAssign<MatrixCoordinates> for MatrixLocation {
358    fn add_assign(&mut self, rhs: MatrixCoordinates) {
359        match self {
360            Self::Col(col) => col.add_assign(rhs.col),
361            Self::Coordinates(x) => x.add_assign(rhs),
362            Self::Range(range) => range.add_assign(rhs),
363            Self::Row(row) => row.add_assign(rhs.row),
364        };
365    }
366}
367impl Sub<MatrixCoordinates> for MatrixLocation {
368    type Output = MatrixLocation;
369    fn sub(self, rhs: MatrixCoordinates) -> Self::Output {
370        match self {
371            Self::Col(col) => Self::Col(col - rhs.col),
372            Self::Coordinates(x) => Self::Coordinates(x - rhs),
373            Self::Range(range) => Self::Range(range - rhs),
374            Self::Row(row) => Self::Row(row - rhs.row),
375        }
376    }
377}
378impl SubAssign<MatrixCoordinates> for MatrixLocation {
379    fn sub_assign(&mut self, rhs: MatrixCoordinates) {
380        match self {
381            Self::Col(col) => col.sub_assign(rhs.col),
382            Self::Coordinates(x) => x.sub_assign(rhs),
383            Self::Range(range) => range.sub_assign(rhs),
384            Self::Row(row) => row.sub_assign(rhs.row),
385        };
386    }
387}
388impl Add<MatrixLocation> for MatrixLocation {
389    type Output = MatrixLocation;
390    fn add(self, rhs: MatrixLocation) -> Self::Output {
391        match self {
392            Self::Col(col) => Self::Col(col + rhs.col().unwrap_or_default()),
393            Self::Coordinates(x) => Self::Coordinates(
394                x + MatrixCoordinates::new(
395                    rhs.row().unwrap_or_default(),
396                    rhs.col().unwrap_or_default(),
397                ),
398            ),
399            Self::Range(range) => Self::Range(
400                range
401                    + MatrixCoordinates::new(
402                        rhs.row().unwrap_or_default(),
403                        rhs.col().unwrap_or_default(),
404                    ),
405            ),
406            Self::Row(row) => Self::Row(row + rhs.row().unwrap_or_default()),
407        }
408    }
409}
410impl AddAssign<MatrixLocation> for MatrixLocation {
411    fn add_assign(&mut self, rhs: MatrixLocation) {
412        match self {
413            Self::Col(col) => col.add_assign(rhs.col().unwrap_or_default()),
414            Self::Coordinates(x) => x.add_assign(MatrixCoordinates::new(
415                rhs.row().unwrap_or_default(),
416                rhs.col().unwrap_or_default(),
417            )),
418            Self::Range(range) => range.add_assign(MatrixCoordinates::new(
419                rhs.row().unwrap_or_default(),
420                rhs.col().unwrap_or_default(),
421            )),
422            Self::Row(row) => row.add_assign(rhs.row().unwrap_or_default()),
423        }
424    }
425}
426impl Sub<MatrixLocation> for MatrixLocation {
427    type Output = MatrixLocation;
428    fn sub(self, rhs: MatrixLocation) -> Self::Output {
429        match self {
430            Self::Col(col) => Self::Col(col - rhs.col().unwrap_or_default()),
431            Self::Coordinates(x) => Self::Coordinates(
432                x - MatrixCoordinates::new(
433                    rhs.row().unwrap_or_default(),
434                    rhs.col().unwrap_or_default(),
435                ),
436            ),
437            Self::Range(range) => Self::Range(
438                range
439                    - MatrixCoordinates::new(
440                        rhs.row().unwrap_or_default(),
441                        rhs.col().unwrap_or_default(),
442                    ),
443            ),
444            Self::Row(row) => Self::Row(row - rhs.row().unwrap_or_default()),
445        }
446    }
447}
448impl SubAssign<MatrixLocation> for MatrixLocation {
449    fn sub_assign(&mut self, rhs: MatrixLocation) {
450        match self {
451            Self::Col(col) => col.sub_assign(rhs.col().unwrap_or_default()),
452            Self::Coordinates(x) => x.sub_assign(MatrixCoordinates::new(
453                rhs.row().unwrap_or_default(),
454                rhs.col().unwrap_or_default(),
455            )),
456            Self::Range(range) => range.sub_assign(MatrixCoordinates::new(
457                rhs.row().unwrap_or_default(),
458                rhs.col().unwrap_or_default(),
459            )),
460            Self::Row(row) => row.sub_assign(rhs.row().unwrap_or_default()),
461        }
462    }
463}
464impl Domain2D for MatrixLocation {
465    fn row_range(&self) -> Range<usize> {
466        match self {
467            Self::Coordinates(coordinates) => Range {
468                start: coordinates.row,
469                end: coordinates.row + 1,
470            },
471            Self::Row(row) => Range {
472                start: *row,
473                end: row + 1,
474            },
475            Self::Col(_) => Range { start: 0, end: 0 },
476            Self::Range(range) => range.row_range(),
477        }
478    }
479    fn col_range(&self) -> Range<usize> {
480        match self {
481            Self::Coordinates(coordinates) => Range {
482                start: coordinates.col,
483                end: coordinates.col + 1,
484            },
485            Self::Row(_) => Range { start: 0, end: 0 },
486            Self::Col(col) => Range {
487                start: *col,
488                end: col + 1,
489            },
490            Self::Range(range) => range.col_range(),
491        }
492    }
493}
494
495/// Coordinate in a 2D matrix.
496#[derive(Copy, Clone, Debug, Default, PartialEq)]
497#[cfg_attr(
498    feature = "serde",
499    derive(serde::Serialize, serde::Deserialize),
500    serde(default, rename_all = "camelCase")
501)]
502pub struct MatrixCoordinates {
503    /// Coordinate row.
504    pub row: usize,
505    /// Coordinate column.
506    pub col: usize,
507}
508impl MatrixCoordinates {
509    /// Create a new matrix coordinate instance.
510    pub fn new(row: usize, col: usize) -> Self {
511        Self { row, col }
512    }
513}
514impl From<(usize, usize)> for MatrixCoordinates {
515    fn from(value: (usize, usize)) -> Self {
516        Self {
517            row: value.0,
518            col: value.1,
519        }
520    }
521}
522impl Add<MatrixCoordinates> for MatrixCoordinates {
523    type Output = Self;
524    fn add(self, rhs: MatrixCoordinates) -> Self::Output {
525        MatrixCoordinates {
526            row: self.row + rhs.row,
527            col: self.col + rhs.col,
528        }
529    }
530}
531impl AddAssign<MatrixCoordinates> for MatrixCoordinates {
532    fn add_assign(&mut self, rhs: MatrixCoordinates) {
533        self.row += rhs.row;
534        self.col += rhs.col;
535    }
536}
537impl Sub<MatrixCoordinates> for MatrixCoordinates {
538    type Output = Self;
539    fn sub(self, rhs: MatrixCoordinates) -> Self::Output {
540        MatrixCoordinates {
541            row: self.row - rhs.row,
542            col: self.col - rhs.col,
543        }
544    }
545}
546impl SubAssign<MatrixCoordinates> for MatrixCoordinates {
547    fn sub_assign(&mut self, rhs: MatrixCoordinates) {
548        self.row -= rhs.row;
549        self.col -= rhs.col;
550    }
551}
552
553/// 2D matrix dimensions.
554#[derive(Copy, Clone, Debug, Default, PartialEq)]
555#[cfg_attr(
556    feature = "serde",
557    derive(serde::Serialize, serde::Deserialize),
558    serde(default, rename_all = "camelCase")
559)]
560pub struct MatrixDimensions {
561    /// Coordinate row.
562    pub n_rows: usize,
563    /// Coordinate column.
564    pub n_cols: usize,
565}
566impl MatrixDimensions {
567    /// Create a new matrix dimensions instance.
568    pub fn new(n_rows: usize, n_cols: usize) -> Self {
569        Self { n_rows, n_cols }
570    }
571}
572impl From<(usize, usize)> for MatrixDimensions {
573    fn from(value: (usize, usize)) -> Self {
574        Self {
575            n_rows: value.0,
576            n_cols: value.1,
577        }
578    }
579}
580impl Dimensions2D for MatrixDimensions {
581    fn n_rows(&self) -> usize {
582        self.n_rows
583    }
584    fn n_cols(&self) -> usize {
585        self.n_cols
586    }
587}
588impl Add<MatrixDimensions> for MatrixDimensions {
589    type Output = Self;
590    fn add(self, rhs: MatrixDimensions) -> Self::Output {
591        MatrixDimensions {
592            n_rows: self.n_rows + rhs.n_rows,
593            n_cols: self.n_cols + rhs.n_cols,
594        }
595    }
596}
597impl AddAssign<MatrixDimensions> for MatrixDimensions {
598    fn add_assign(&mut self, rhs: MatrixDimensions) {
599        self.n_rows += rhs.n_rows;
600        self.n_cols += rhs.n_cols;
601    }
602}
603impl Sub<MatrixDimensions> for MatrixDimensions {
604    type Output = Self;
605    fn sub(self, rhs: MatrixDimensions) -> Self::Output {
606        MatrixDimensions {
607            n_rows: self.n_rows - rhs.n_rows,
608            n_cols: self.n_cols - rhs.n_cols,
609        }
610    }
611}
612impl SubAssign<MatrixDimensions> for MatrixDimensions {
613    fn sub_assign(&mut self, rhs: MatrixDimensions) {
614        self.n_rows -= rhs.n_rows;
615        self.n_cols -= rhs.n_cols;
616    }
617}
618
619/// 2D range in a matrix.
620#[derive(Copy, Clone, Debug, Default, PartialEq)]
621#[cfg_attr(
622    feature = "serde",
623    derive(serde::Serialize, serde::Deserialize),
624    serde(default, rename_all = "camelCase")
625)]
626pub struct MatrixRange {
627    /// Starting coordinates of the range (inclusive).
628    pub start: MatrixCoordinates,
629    /// Ending coordinates of the range (exclusive).
630    pub end: MatrixCoordinates,
631}
632impl MatrixRange {
633    /// Create a new matrix area instance.
634    pub fn new(rows: Range<usize>, cols: Range<usize>) -> Self {
635        Self {
636            start: MatrixCoordinates::new(rows.start, cols.start),
637            end: MatrixCoordinates::new(rows.end, cols.end),
638        }
639    }
640}
641impl Domain2D for MatrixRange {
642    /// Row range of this area.
643    fn row_range(&self) -> Range<usize> {
644        self.start.row..self.end.row
645    }
646
647    /// Column range of this area.
648    fn col_range(&self) -> Range<usize> {
649        self.start.col..self.end.col
650    }
651}
652impl From<(Range<usize>, Range<usize>)> for MatrixRange {
653    fn from(value: (Range<usize>, Range<usize>)) -> Self {
654        Self::new(value.0, value.1)
655    }
656}
657impl From<((usize, usize), (usize, usize))> for MatrixRange {
658    /// Create a range from two sets of coordinates (start, end).
659    fn from(value: ((usize, usize), (usize, usize))) -> Self {
660        Self {
661            start: value.0.into(),
662            end: value.1.into(),
663        }
664    }
665}
666impl From<MatrixDimensions> for MatrixRange {
667    fn from(value: MatrixDimensions) -> Self {
668        Self::new(0..value.n_rows, 0..value.n_cols)
669    }
670}
671impl Add<MatrixCoordinates> for MatrixRange {
672    type Output = Self;
673    fn add(self, rhs: MatrixCoordinates) -> Self::Output {
674        Self {
675            start: self.start + rhs,
676            end: self.end + rhs,
677        }
678    }
679}
680impl AddAssign<MatrixCoordinates> for MatrixRange {
681    fn add_assign(&mut self, rhs: MatrixCoordinates) {
682        self.start += rhs;
683        self.end += rhs;
684    }
685}
686impl Sub<MatrixCoordinates> for MatrixRange {
687    type Output = Self;
688    fn sub(self, rhs: MatrixCoordinates) -> Self::Output {
689        Self {
690            start: self.start - rhs,
691            end: self.end - rhs,
692        }
693    }
694}
695impl SubAssign<MatrixCoordinates> for MatrixRange {
696    fn sub_assign(&mut self, rhs: MatrixCoordinates) {
697        self.start -= rhs;
698        self.end -= rhs;
699    }
700}