diff --git a/include/gl/types/flat_jagged_vector.hpp b/include/gl/types/flat_jagged_vector.hpp index e267ddb..4d2e180 100644 --- a/include/gl/types/flat_jagged_vector.hpp +++ b/include/gl/types/flat_jagged_vector.hpp @@ -228,7 +228,7 @@ class flat_jagged_vector { /// @brief Reverse const iterator using const_reverse_iterator = std::reverse_iterator; - // --- constructors --- + // --- constructors and assignment --- /// @brief Default constructor creates an empty `flat_jagged_vector`. /// @post `empty() == true`, `size() == 0`, `data_size() == 0` @@ -330,7 +330,7 @@ class flat_jagged_vector { /// @return `true` if both vectors have the same structure and elements friend bool operator==(const flat_jagged_vector&, const flat_jagged_vector&) = default; - // --- capacity --- + // --- size and capacity --- /// @brief Returns the number of segments in this container. /// @return The count of segments @@ -846,8 +846,6 @@ class flat_jagged_vector { requires std::convertible_to, value_type> void push_back(R&& r) { this->_ensure_offset_capacity(); - if constexpr (std::ranges::sized_range) - this->_data.reserve(this->_data.size() + std::ranges::size(r)); if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); @@ -908,8 +906,6 @@ class flat_jagged_vector { const auto old_size = this->_data.size(); this->_ensure_offset_capacity(); - if constexpr (std::ranges::sized_range) - this->_data.reserve(this->_data.size() + std::ranges::size(r)); if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp new file mode 100644 index 0000000..f7a033b --- /dev/null +++ b/include/gl/types/flat_matrix.hpp @@ -0,0 +1,1293 @@ +// Copyright (c) 2024-2026 Jakub MusiaƂ +// This file is part of the CPP-GL project (https://github.com/SpectraL519/cpp-gl). +// Licensed under the MIT License. See the LICENSE file in the project root for full license information. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gl { + +/// @brief A flattened 2D matrix providing efficient storage and uniform access for a rectangular grid of elements. +/// +/// This container stores all elements in a single contiguous memory block (`_data`) of size `n_rows * n_cols` +/// using row-major ordering. Row accesses are contiguous in memory, while column accesses are resolved mathematically +/// via strided views. Both provide $O(1)$ random access and native compatibility with C++20/23 ranges. +/// +/// @tparam T A semiregular type to be stored in the matrix. Must be copy-constructible and assignable. +/// +/// @warning Iterator invalidation follows `std::vector` semantics: modifying the dimensions or structural +/// capacity of the matrix invalidates all iterators, pointers, and references to its elements. +template +class flat_matrix { +public: + /// @brief Type of elements stored in the matrix + using value_type = T; + /// @brief Unsigned integral type used for sizes and indices + using size_type = std::size_t; + /// @brief Reference to an element + using reference = value_type&; + /// @brief Const reference to an element + using const_reference = const value_type&; + /// @brief Span type representing a non-owning uniform row of elements + using row_type = std::span; + /// @brief Const span type representing a non-owning uniform const row of elements + using const_row_type = std::span; + + // --- iterators --- + + /// @brief Random access iterator over the rows of the `flat_matrix`. + /// + /// This iterator dereferences to a `row_type` (span of elements representing a single matrix row), + /// allowing efficient iteration and random access. It calculates the memory offsets mathematically + /// based on the column dimension. + /// + /// @tparam Const If `true`, produces const iterators; if `false`, produces mutable iterators. + /// @note Provides random access semantics: `O(1)` for all operations except construction. + /// @warning Invalidated when the `flat_matrix` structural dimensions are modified or memory is reallocated. + template + class row_iterator { + using data_ptr_type = std::conditional_t; + + public: + /// @brief Satisfies random access iterator concept + using iterator_concept = std::random_access_iterator_tag; + /// @brief Legacy iterator category (random access) + using iterator_category = std::random_access_iterator_tag; + /// @brief Type of row this iterator dereferences to (span or const span) + using value_type = std::conditional_t; + /// @brief Signed integral difference type + using difference_type = std::ptrdiff_t; + /// @brief Pointer type (void because iterators dereference to spans) + using pointer = void; + /// @brief Reference type (span of elements) + using reference = value_type; + + /// @brief Default constructor creates a null iterator + row_iterator() = default; + + /// @brief Constructs an iterator pointing to a specific row. + /// @param data_ptr Pointer to the underlying flat element data + /// @param n_cols The number of columns in the matrix + /// @param row_idx The index of the row this iterator currently points to + row_iterator(data_ptr_type data_ptr, size_type n_cols, size_type row_idx) noexcept + : _data_ptr(data_ptr), _row_size(n_cols), _row_idx(row_idx) {} + + /// @brief Implicit conversion from mutable to const iterator + /// @return A const iterator pointing to the same row + operator row_iterator() const noexcept + requires(not Const) + { + return row_iterator(this->_data_ptr, this->_row_size, this->_row_idx); + } + + /// @brief Dereferences the iterator to the current row. + /// @return A span representing the row at the current position + [[nodiscard]] reference operator*() const noexcept { + return reference(this->_data_ptr + this->_row_idx * this->_row_size, this->_row_size); + } + + /// @brief Random access to a row at an offset from the current position. + /// @param n Offset (can be negative) + /// @return Row at offset n from the current position + /// @pre `0 <= current_position + n < n_rows()`; otherwise Undefined Behavior + [[nodiscard]] reference operator[](difference_type n) const noexcept { + return *(*this + n); + } + + /// @brief Pre-increment operator. + /// @return Reference to this iterator after advancing to the next row + row_iterator& operator++() noexcept { + ++this->_row_idx; + return *this; + } + + /// @brief Post-increment operator. + /// @return A copy of this iterator before the increment + row_iterator operator++(int) noexcept { + auto tmp = *this; + ++this->_row_idx; + return tmp; + } + + /// @brief Pre-decrement operator. + /// @return Reference to this iterator after moving to the previous row + row_iterator& operator--() noexcept { + --this->_row_idx; + return *this; + } + + /// @brief Post-decrement operator. + /// @return A copy of this iterator before the decrement + row_iterator operator--(int) noexcept { + auto tmp = *this; + --this->_row_idx; + return tmp; + } + + /// @brief Advances the iterator by n rows. + /// @param n Number of rows to advance (can be negative) + /// @return Reference to this iterator + row_iterator& operator+=(difference_type n) noexcept { + this->_row_idx += n; + return *this; + } + + /// @brief Moves the iterator backward by n rows. + /// @param n Number of rows to move backward (can be negative) + /// @return Reference to this iterator + row_iterator& operator-=(difference_type n) noexcept { + this->_row_idx -= n; + return *this; + } + + /// @brief Creates a new iterator advanced by n rows from the given iterator. + /// @param it Iterator to advance from + /// @param n Number of rows to advance + /// @return New iterator at the advanced position + [[nodiscard]] friend row_iterator operator+(row_iterator it, difference_type n) noexcept { + return it += n; + } + + /// @brief Creates a new iterator advanced by n rows (commutative form). + /// @param n Number of rows to advance + /// @param it Iterator to advance from + /// @return New iterator at the advanced position + [[nodiscard]] friend row_iterator operator+(difference_type n, row_iterator it) noexcept { + return it += n; + } + + /// @brief Creates a new iterator moved backward by n rows. + /// @param it Iterator to move backward from + /// @param n Number of rows to move backward + /// @return New iterator at the moved position + [[nodiscard]] friend row_iterator operator-(row_iterator it, difference_type n) noexcept { + return it -= n; + } + + /// @brief Computes the distance between two iterators. + /// @param lhs The later iterator + /// @param rhs The earlier iterator + /// @return Number of rows between the iterators; negative if lhs < rhs + [[nodiscard]] friend difference_type operator-( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx - rhs._row_idx; + } + + /// @brief Tests equality of two iterators. + /// @param lhs Left iterator + /// @param rhs Right iterator + /// @return `true` if both iterators point to the same row index + [[nodiscard]] friend bool operator==( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx == rhs._row_idx; + } + + /// @brief Three-way comparison of two iterators. + /// @param lhs Left iterator + /// @param rhs Right iterator + /// @return Comparison result indicating iterator ordering + [[nodiscard]] friend auto operator<=>( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx <=> rhs._row_idx; + } + + private: + data_ptr_type _data_ptr{nullptr}; + size_type _row_size{0uz}; + size_type _row_idx{0uz}; + }; + + /// @brief Mutable random access iterator over rows + using iterator = row_iterator; + /// @brief Const random access iterator over rows + using const_iterator = row_iterator; + /// @brief Reverse mutable iterator over rows + using reverse_iterator = std::reverse_iterator; + /// @brief Reverse const iterator over rows + using const_reverse_iterator = std::reverse_iterator; + + // --- constructors and assignment --- + + /// @brief Default constructor creates an empty `flat_matrix`. + /// @post `empty() == true`, `n_rows() == 0`, `n_cols() == 0`, `data_size() == 0` + flat_matrix() = default; + + /// @brief Copy constructor creates a deep copy of another `flat_matrix`. + /// @param other The `flat_matrix` to copy + /// @post `*this == other` + flat_matrix(const flat_matrix&) = default; + + /// @brief Copy assignment creates a deep copy of another `flat_matrix`. + /// @param other The source `flat_matrix` + /// @return Reference to `*this` + /// @post `*this == other` + flat_matrix& operator=(const flat_matrix&) = default; + + /// @brief Move constructor transfers ownership of data from another `flat_matrix`. + /// @param other The source `flat_matrix` (left in an empty state) + /// @post `other.empty() == true`; all data is transferred to `*this` + /// @warning Invalidates all iterators, pointers, and references to `other`'s elements. + flat_matrix(flat_matrix&& other) noexcept + : _n_rows(std::exchange(other._n_rows, 0uz)), + _n_cols(std::exchange(other._n_cols, 0uz)), + _data(std::move(other._data)) {} + + /// @brief Move assignment transfers ownership of data from another `flat_matrix`. + /// @param other The source `flat_matrix` + /// @return Reference to `*this` + /// @post `other.empty() == true`; all data from `other` is transferred to `*this` + /// @warning Invalidates all iterators, pointers, and references to this container's elements. + /// @note This operator safely handles self-assignment. + flat_matrix& operator=(flat_matrix&& other) noexcept { + if (this != &other) { + this->_n_rows = std::exchange(other._n_rows, 0uz); + this->_n_cols = std::exchange(other._n_cols, 0uz); + this->_data = std::move(other._data); + } + return *this; + } + + /// @brief Destructor cleans up all managed memory. + ~flat_matrix() = default; + + /// @brief Constructs a `flat_matrix` with specified dimensions. + /// @param n_rows The number of rows + /// @param n_cols The number of columns + /// @param value The value to initialize all elements with (default constructed if omitted) + /// @post `n_rows() == n_rows`, `n_cols() == n_cols`, and elements equal `value` + /// @exception std::bad_alloc May throw if memory allocation fails + flat_matrix(size_type n_rows, size_type n_cols, const value_type& value = value_type{}) + : _n_rows(n_rows), _n_cols(n_cols), _data(n_rows * n_cols, value) {} + + /// @brief Constructs a `flat_matrix` from an initializer list of rows. + /// @param ilist Initializer list of initializer lists, each representing a row + /// @post Dimensions are established based on the list geometry + /// @exception std::invalid_argument If the rows in the list do not have identical lengths + /// @exception std::bad_alloc May throw if memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after construction + flat_matrix(std::initializer_list> ilist) { + this->_n_rows = ilist.size(); + if (this->_n_rows == 0uz) + return; + + this->_n_cols = ilist.begin()->size(); + this->_data.reserve(this->_n_rows * this->_n_cols); + + for (const auto& row : ilist) { + if (row.size() != this->_n_cols) { + throw std::invalid_argument(std::format( + "flat_matrix: row size mismatch in initializer_list (expected {}, got {})", + this->_n_cols, + row.size() + )); + } + this->_data.insert(this->_data.end(), row.begin(), row.end()); + } + } + + /// @brief Constructs a `flat_matrix` from a 2D range of ranges. + /// + /// The matrix establishes its column count from the size of the first extracted row. + /// All subsequent rows must perfectly match this dimension. Provides a strong exception guarantee + /// if the source is an unsized pure `input_range` and fails validation mid-extraction. + /// + /// @tparam R A range type whose elements are input ranges of `value_type` + /// @param r The 2D range to initialize from + /// @post Dimensions match the structure of `r` + /// @exception std::invalid_argument If any extracted row size mismatches the first row's size + /// @exception std::bad_alloc May throw if memory allocation fails + template + requires std::ranges::input_range> + and std::convertible_to< + std::ranges::range_reference_t>, + value_type> + explicit flat_matrix(R&& r) { + if constexpr (std::ranges::sized_range) + this->_n_rows = std::ranges::size(r); + + bool first = true; + for (auto&& subrange : r) { + // can't consume an input range + // size has to be determined by consuming the range and counting how many elements were inserted into _data + const auto old_size = this->_data.size(); + + if constexpr (std::ranges::contiguous_range) { + auto* ptr = std::ranges::data(subrange); + const auto dist = std::ranges::size(subrange); + this->_data.insert(this->_data.end(), ptr, ptr + dist); + } + else { + this->_data.insert( + this->_data.end(), std::ranges::begin(subrange), std::ranges::end(subrange) + ); + } + + const auto row_size = this->_data.size() - old_size; + if (first) { + this->_n_cols = row_size; + + // prevent reallocation during loop for sized range + if constexpr (std::ranges::sized_range) + this->_data.reserve(this->_n_rows * this->_n_cols); + + first = false; + } + else if (row_size != this->_n_cols) { + throw std::invalid_argument(std::format( + "flat_matrix: row size mismatch in range constructor (expected {}, got {})", + this->_n_cols, + row_size + )); + } + } + + // calculate the number of rows for an unsized range + if constexpr (not std::ranges::sized_range) + this->_n_rows = this->_data.size() / (this->_n_cols > 0uz ? this->_n_cols : 1uz); + } + + // --- comparison --- + + /// @brief Tests equality of two `flat_matrix` instances. + /// @param lhs Left operand + /// @param rhs Right operand + /// @return `true` if dimensions and all elements match + friend bool operator==(const flat_matrix&, const flat_matrix&) = default; + + // --- size and capacity --- + + /// @brief Returns the number of rows in the matrix. + /// @return The count of rows + /// @note Required to idiomaticaly satisfy `std::ranges::sized_range`. + [[nodiscard]] size_type size() const noexcept { + return this->_n_rows; + } + + /// @brief Returns the number of rows in the matrix. + /// @return The count of rows + [[nodiscard]] size_type n_rows() const noexcept { + return this->_n_rows; + } + + /// @brief Returns the number of columns in the matrix. + /// @return The count of columns + [[nodiscard]] size_type n_cols() const noexcept { + return this->_n_cols; + } + + /// @brief Checks if the container is entirely empty. + /// @return `true` if `data_size() == 0`, `false` otherwise + [[nodiscard]] bool empty() const noexcept { + return this->_data.empty(); + } + + /// @brief Returns the current capacity for data elements. + /// @return The number of total elements that can be stored in `_data` without reallocation + [[nodiscard]] size_type data_capacity() const noexcept { + return this->_data.capacity(); + } + + /// @brief Reserves space for at least n total elements without changing the dimensions. + /// @param n The total number of matrix elements to reserve space for + /// @post `data_capacity() >= n` + /// @warning Invalidates all iterators and pointers to elements if reallocation occurs + void reserve_data(size_type n) { + this->_data.reserve(n); + } + + /// @brief Reduces capacity of the internal array to match the current data size. + /// @post `data_capacity() == data_size()` + /// @warning Invalidates all iterators, pointers, and references to elements if reallocation occures + void shrink_to_fit() { + this->_data.shrink_to_fit(); + } + + /// @brief Resizes the mathematical dimensions of the matrix. + /// + /// If the new dimensions require structural changes (e.g. changing the number of columns), + /// the mathematical grid is rebuilt and existing items are relocated to their new coordinate slots. + /// + /// @param new_rows The new number of rows + /// @param new_cols The new number of columns + /// @param value The value to initialize any newly exposed slots with + /// @post `n_rows() == new_rows` and `n_cols() == new_cols` + /// @warning Invalidates all iterators, pointers, and references. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are the new dimensions, due to remapping + /// elements in 2D space. If only the row count changes, it is $O(K)$ where $K$ is the number of + /// inserted or removed trailing elements. + void resize(size_type new_rows, size_type new_cols, const value_type& value = value_type{}) { + if (new_rows == this->_n_rows and new_cols == this->_n_cols) + return; + + if (new_cols == this->_n_cols) { + this->_data.resize(new_rows * new_cols, value); + this->_n_rows = new_rows; + return; + } + + std::vector new_data(new_rows * new_cols, value); + const auto min_rows = std::min(this->_n_rows, new_rows); + const auto min_cols = std::min(this->_n_cols, new_cols); + + for (auto r = 0uz; r < min_rows; ++r) + for (auto c = 0uz; c < min_cols; ++c) + new_data[r * new_cols + c] = std::move(this->_data[r * this->_n_cols + c]); + + this->_data = std::move(new_data); + this->_n_rows = new_rows; + this->_n_cols = new_cols; + } + + /// @brief Removes all dimensions and elements, leaving the matrix empty. + /// @post `n_rows() == 0`, `n_cols() == 0`, `data_size() == 0` + /// @warning Invalidates all iterators, pointers, and references to elements + void clear() { + this->_data.clear(); + this->_n_rows = 0uz; + this->_n_cols = 0uz; + } + + // --- accessors --- + + /// @brief Computes the underlying flattened 1D index for a 2D coordinate. + /// @param r The row index + /// @param c The column index + /// @return The 1D index mapping for `_data` + /// @note Provides $O(1)$ constant time lookup calculation + [[nodiscard]] constexpr size_type index(size_type r, size_type c) const noexcept { + return r * this->_n_cols + c; + } + + /// @brief Returns the row at the given index without bounds checking. + /// @param r The index of the row to access + /// @return A span representing the row at index r + /// @pre `r < n_rows()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed for performance. + [[nodiscard]] row_type operator[](size_type r) { + return row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + } + + /// @brief Returns a const row at the given index without bounds checking. + /// @param r The index of the row to access + /// @return A const span representing the row at index r + /// @pre `r < n_rows()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] const_row_type operator[](size_type r) const { + return const_row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + } + + /// @brief Returns a reference to an element without bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Reference to the element at the given coordinates + /// @pre `r < n_rows()` and `c < n_cols()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] reference operator[](size_type r, size_type c) { + return this->_data[this->index(r, c)]; + } + + /// @brief Returns a const reference to an element without bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Const reference to the element at the given coordinates + /// @pre `r < n_rows()` and `c < n_cols()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] const_reference operator[](size_type r, size_type c) const { + return this->_data[this->index(r, c)]; + } + + /// @brief Returns the row at the given index with bounds checking. + /// @param r The index of the row + /// @return A span representing the row at index r + /// @exception std::out_of_range If `r >= n_rows()` + [[nodiscard]] row_type at(size_type r) { + this->_check_row(r); + return (*this)[r]; + } + + /// @brief Returns a const row at the given index with bounds checking. + /// @param r The index of the row + /// @return A const span representing the row at index r + /// @exception std::out_of_range If `r >= n_rows()` + [[nodiscard]] const_row_type at(size_type r) const { + this->_check_row(r); + return (*this)[r]; + } + + /// @brief Returns a reference to an element with bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Reference to the element + /// @exception std::out_of_range If `r >= n_rows()` or `c >= n_cols()` + [[nodiscard]] reference at(size_type r, size_type c) { + this->_check_row(r); + this->_check_col(c); + return (*this)[r, c]; + } + + /// @brief Returns a const reference to an element with bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Const reference to the element + /// @exception std::out_of_range If `r >= n_rows()` or `c >= n_cols()` + [[nodiscard]] const_reference at(size_type r, size_type c) const { + this->_check_row(r); + this->_check_col(c); + return (*this)[r, c]; + } + + /// @brief Returns the first row without bounds checking. + /// @return A span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] row_type front() noexcept { + return (*this)[0uz]; + } + + /// @brief Returns a const reference to the first row without bounds checking. + /// @return A const span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] const_row_type front() const noexcept { + return (*this)[0uz]; + } + + /// @brief Returns the last row without bounds checking. + /// @return A span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] row_type back() noexcept { + return (*this)[this->_n_rows - 1uz]; + } + + /// @brief Returns a const reference to the last row without bounds checking. + /// @return A const span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] const_row_type back() const noexcept { + return (*this)[this->_n_rows - 1uz]; + } + + /// @brief Explicitly named alias for `front()` yielding the first row. + /// @return A span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + [[nodiscard]] row_type front_row() noexcept { + return this->front(); + } + + /// @brief Explicitly named alias for `front()` yielding the first const row. + /// @return A const span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + [[nodiscard]] const_row_type front_row() const noexcept { + return this->front(); + } + + /// @brief Explicitly named alias for `back()` yielding the last row. + /// @return A span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + [[nodiscard]] row_type back_row() noexcept { + return this->back(); + } + + /// @brief Explicitly named alias for `back()` yielding the last const row. + /// @return A const span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + [[nodiscard]] const_row_type back_row() const noexcept { + return this->back(); + } + + /// @brief Returns an unchecked $O(1)$ random-access view over the first column. + /// @return A strided view representing the first column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] auto front_col() noexcept { + return this->_col_impl(0uz); + } + + /// @brief Returns an unchecked $O(1)$ random-access const view over the first column. + /// @return A strided view representing the const first column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] auto front_col() const noexcept { + return this->_col_impl(0uz); + } + + /// @brief Returns an unchecked $O(1)$ random-access view over the last column. + /// @return A strided view representing the last column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] auto back_col() noexcept { + return this->_col_impl(this->_n_cols - 1uz); + } + + /// @brief Returns an unchecked $O(1)$ random-access const view over the last column. + /// @return A strided view representing the const last column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. + [[nodiscard]] auto back_col() const noexcept { + return this->_col_impl(this->_n_cols - 1uz); + } + + /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked row. + /// @param r The row index + /// @return A span representing the row + /// @exception std::out_of_range If `r >= n_rows()` + [[nodiscard]] row_type row(size_type r) { + return this->at(r); + } + + /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked const row. + /// @param r The row index + /// @return A const span representing the row + /// @exception std::out_of_range If `r >= n_rows()` + [[nodiscard]] const_row_type row(size_type r) const { + return this->at(r); + } + + /// @brief Returns a bounds-checked $O(1)$ random-access view over a specific column. + /// @param c The column index + /// @return A strided view representing the column + /// @exception std::out_of_range If `c >= n_cols()` + [[nodiscard]] auto col(size_type c) { + this->_check_col(c); + return this->_col_impl(c); + } + + /// @brief Returns a bounds-checked $O(1)$ random-access const view over a specific column. + /// @param c The column index + /// @return A strided view representing the const column + /// @exception std::out_of_range If `c >= n_cols()` + [[nodiscard]] auto col(size_type c) const { + this->_check_col(c); + return this->_col_impl(c); + } + + /// @brief Returns a view of all rows for iteration. + /// @return A random-access view of all row spans + [[nodiscard]] auto rows() noexcept { + return std::views::iota(size_type{0}, this->_n_rows) + | std::views::transform([this](size_type i) -> row_type { return (*this)[i]; }); + } + + /// @brief Returns a const view of all rows for iteration. + /// @return A random-access const view of all row const spans + [[nodiscard]] auto rows() const noexcept { + return std::views::iota(size_type{0}, this->_n_rows) + | std::views::transform([this](size_type i) -> const_row_type { return (*this)[i]; }); + } + + /// @brief Returns a view of all columns for iteration. + /// @return A random-access view of all column strided-views + [[nodiscard]] auto cols() noexcept { + return std::views::iota(size_type{0}, this->_n_cols) + | std::views::transform([this](size_type c) { return this->_col_impl(c); }); + } + + /// @brief Returns a const view of all columns for iteration. + /// @return A random-access view of all const column strided-views + [[nodiscard]] auto cols() const noexcept { + return std::views::iota(size_type{0}, this->_n_cols) + | std::views::transform([this](size_type c) { return this->_col_impl(c); }); + } + + // --- accessors (data) --- + + /// @brief Returns the total number of elements structurally stored in the matrix. + /// @return The result of `n_rows() * n_cols()` + [[nodiscard]] size_type data_size() const noexcept { + return this->_data.size(); + } + + /// @brief Returns a span over all element data in flattened 1D form. + /// @return A span of all elements in the underlying `_data` array. + [[nodiscard]] row_type data_view() noexcept { + return row_type(this->_data); + } + + /// @brief Returns a const span over all element data in flattened 1D form. + /// @return A const span of all elements in the underlying `_data` array. + [[nodiscard]] const_row_type data_view() const noexcept { + return const_row_type(this->_data); + } + + /// @brief Returns a reference to the underlying flat data container. + /// @return A mutable reference to the underlying `_data` array. + /// @warning Modifying this vector directly can fatally corrupt the matrix structure. Use for advanced operations only. + [[nodiscard]] std::vector& data_storage() noexcept { + return this->_data; + } + + /// @brief Returns a const reference to the underlying flat data container. + /// @return A const reference to the underlying `_data` array. + [[nodiscard]] const std::vector& data_storage() const noexcept { + return this->_data; + } + + /// @brief Returns a raw pointer to the underlying flat data array. + /// @return A raw pointer to the first element in the `_data` array. + /// @warning No bounds checking is performed. + [[nodiscard]] value_type* data_ptr() noexcept { + return this->_data.data(); + } + + /// @brief Returns a const raw pointer to the underlying flat data array. + /// @return A const raw pointer to the first element in the `_data` array. + [[nodiscard]] const value_type* data_ptr() const noexcept { + return this->_data.data(); + } + + // --- modifiers (rows) --- + + /// @brief Appends a range as a new row at the bottom of the matrix. + /// @tparam R An input range of elements convertible to `value_type` + /// @param r The range to append + /// @post `n_rows()` increases by 1 + /// @exception std::invalid_argument If the row size does not match `n_cols()` (for non-empty matrices) + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. + template + requires std::convertible_to, value_type> + void push_row(R&& r) { + this->insert_row(this->_n_rows, std::forward(r)); + } + + /// @brief Appends an initializer list as a new row at the bottom of the matrix. + /// @param ilist The list to append + /// @post `n_rows()` increases by 1 + /// @exception std::invalid_argument If the list size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. + void push_row(std::initializer_list ilist) { + this->insert_row(this->_n_rows, std::span{ilist}); + } + + /// @brief Appends a newly created row filled with a specific value. + /// @param value The value to fill the new row with + /// @post `n_rows()` increases by 1 + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. + void push_row(const value_type& value) { + this->insert_row(this->_n_rows, value); + } + + /// @brief Inserts a new row at the specified position from a range. + /// @tparam R An input range of elements convertible to `value_type` + /// @param pos The row position where the elements will be inserted + /// @param r The range to insert + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::invalid_argument If the range size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @note Provides strong exception guarantee if size validation fails for unsized ranges. + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. + template + requires std::convertible_to, value_type> + void insert_row(size_type pos, R&& r) { + if (pos > this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::insert_row: pos (which is {}) > this->rows() (which is {})", + pos, + this->_n_rows + )); + } + + const auto insert_idx = pos * this->_n_cols; + + if constexpr (std::ranges::sized_range) { + const auto row_size = static_cast(std::ranges::size(r)); + if (this->_n_rows > 0uz and row_size != this->_n_cols) { + throw std::invalid_argument(std::format( + "flat_matrix::insert_row: row size mismatch (expected {}, got {})", + this->_n_cols, + row_size + )); + } + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_cols = row_size; + + if constexpr (std::ranges::contiguous_range) { + auto* ptr = std::ranges::data(r); + this->_data.insert(this->_data.begin() + insert_idx, ptr, ptr + row_size); + } + else { + this->_data.insert( + this->_data.begin() + insert_idx, std::ranges::begin(r), std::ranges::end(r) + ); + } + } + else { + // single-pass input range: insert, validate size, rollback if mismatched (strong exception guarantee) + const auto old_size = this->_data.size(); + + this->_data.insert( + this->_data.begin() + insert_idx, std::ranges::begin(r), std::ranges::end(r) + ); + + const auto row_size = this->_data.size() - old_size; + if (this->_n_rows > 0uz and row_size != this->_n_cols) { + this->_data.erase( + this->_data.begin() + insert_idx, this->_data.begin() + insert_idx + row_size + ); + throw std::invalid_argument(std::format( + "flat_matrix::insert_row: row size mismatch (expected {}, got {})", + this->_n_cols, + row_size + )); + } + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_cols = row_size; + } + + ++this->_n_rows; + } + + /// @brief Inserts a new row at the specified position from an initializer list. + /// @param pos The row position where the elements will be inserted + /// @param ilist The list to insert + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::invalid_argument If the list size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. + void insert_row(size_type pos, std::initializer_list ilist) { + this->insert_row(pos, std::span{ilist}); + } + + /// @brief Inserts a newly created row filled with a specific value at the specified position. + /// @param pos The row position where the elements will be inserted + /// @param value The value to fill the new row with + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. + void insert_row(size_type pos, const value_type& value) { + if (pos > this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::insert_row: pos (which is {}) > this->rows() (which is {})", + pos, + this->_n_rows + )); + } + + this->_data.insert(this->_data.begin() + (pos * this->_n_cols), this->_n_cols, value); + ++this->_n_rows; + } + + /// @brief Removes the last row from the matrix. + /// @post If not empty, `n_rows()` decreases by 1 + /// @note Safe to call on an empty matrix (no-op). + /// @warning Invalidates all iterators, pointers, and references to elements in the last row. + /// @note **Time Complexity:** $O(C)$ to truncate the underlying `_data` vector. + void pop_row() { + if (this->empty()) + return; + + this->_data.resize(this->_data.size() - this->_n_cols); + --this->_n_rows; + + if (this->_n_rows == 0uz) + this->_n_cols = 0uz; + } + + /// @brief Erases the row at the specified position. + /// @param pos The position of the row to erase + /// @post The row is removed; subsequent rows are shifted up; `n_rows()` decreases by 1 + /// @exception std::out_of_range If `pos >= n_rows()` + /// @warning Invalidates all iterators, pointers, and references at or after the erased position. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of elements after the erased row + /// and $C$ is the number of columns (the size of the erased row). + void erase_row(size_type pos) { + this->_check_row(pos); + if (this->_n_rows == 1uz) { + this->clear(); + return; + } + + const auto start_it = this->_data.begin() + (pos * this->_n_cols); + this->_data.erase(start_it, start_it + this->_n_cols); + --this->_n_rows; + } + + // --- modifiers (columns) --- + + /// @brief Appends a range as a new column at the right edge of the matrix. + /// @tparam R An input range of elements convertible to `value_type` + /// @param r The range to append + /// @post `n_cols()` increases by 1 + /// @exception std::invalid_argument If the column size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + template + requires std::convertible_to, value_type> + void push_col(R&& r) { + this->insert_col(this->_n_cols, std::forward(r)); + } + + /// @brief Appends an initializer list as a new column at the right edge of the matrix. + /// @param ilist The list to append + /// @post `n_cols()` increases by 1 + /// @exception std::invalid_argument If the list size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void push_col(std::initializer_list ilist) { + this->insert_col(this->_n_cols, std::span{ilist}); + } + + /// @brief Appends a newly created column filled with a specific value at the right edge. + /// @param value The value to fill the new column with + /// @post `n_cols()` increases by 1 + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void push_col(const value_type& value) { + this->insert_col(this->_n_cols, value); + } + + /// @brief Inserts a new column at the specified position from a range. + /// @tparam R An input range of elements convertible to `value_type` + /// @param pos The column position where elements will be inserted + /// @param r The range to insert + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::invalid_argument If the range size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + template + requires std::convertible_to, value_type> + void insert_col(size_type pos, R&& r) { + if (pos > this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::insert_col: pos (which is {}) > this->cols() (which is {})", + pos, + this->_n_cols + )); + } + + if constexpr (std::ranges::sized_range) { + const auto col_size = static_cast(std::ranges::size(r)); + if (this->_n_cols > 0uz and col_size != this->_n_rows) { + throw std::invalid_argument(std::format( + "flat_matrix::insert_col: col size mismatch (expected {}, got {})", + this->_n_rows, + col_size + )); + } + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_rows = col_size; + + // pre-allocate new vector to guarantee O(RxC) structural shift, avoiding cubic complexity with multiple inserts + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); + + auto r_it = std::ranges::begin(r); + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + // move old row elements up to insertion point + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + // insert new column element + new_data.push_back(*r_it++); + // move the remainder of old row + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + ++this->_n_cols; + } + else { + // create a temporary sized range and recursively call the method to leverage the sized range logic, + // ensuring strong exception guarantee for unsized input + this->insert_col(pos, std::ranges::to>(std::forward(r))); + } + } + + /// @brief Inserts a new column at the specified position from an initializer list. + /// @param pos The column position where elements will be inserted + /// @param ilist The list to insert + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::invalid_argument If the list size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void insert_col(size_type pos, std::initializer_list ilist) { + this->insert_col(pos, std::span{ilist}); + } + + /// @brief Inserts a newly created column filled with a specific value at the specified position. + /// @param pos The column position where elements will be inserted + /// @param value The value to fill the new column with + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void insert_col(size_type pos, const value_type& value) { + if (pos > this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::insert_col: pos (which is {}) > this->cols() (which is {})", + pos, + this->_n_cols + )); + } + + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); + + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + new_data.push_back(value); // Insert the fill value + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + ++this->_n_cols; + } + + /// @brief Removes the last column from the matrix. + /// @post If not empty, `n_cols()` decreases by 1 + /// @note Safe to call on an empty matrix (no-op). + /// @warning This operation forces a reallocation and structural shift. All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void pop_col() { + if (this->empty() || this->_n_cols == 0uz) + return; + + this->erase_col(this->_n_cols - 1uz); + } + + /// @brief Erases the column at the specified position. + /// @param pos The position of the column to erase + /// @post The column is removed; subsequent columns are mathematically shifted left; `n_cols()` decreases by 1 + /// @exception std::out_of_range If `pos >= n_cols()` + /// @warning This operation forces a reallocation and structural shift. All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. + void erase_col(size_type pos) { + this->_check_col(pos); + + if (this->_n_cols == 1uz) { + this->clear(); + return; + } + + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols - 1uz)); + + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos + 1uz), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + --this->_n_cols; + } + + // --- iterators --- + + /// @brief Returns a mutable iterator to the first row. + /// @return Iterator to the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] iterator begin() noexcept { + return iterator(this->_data.data(), this->_n_cols, 0uz); + } + + /// @brief Returns a mutable iterator past the last row (end sentinel). + /// @return Iterator one position past the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] iterator end() noexcept { + return iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + /// @brief Returns a const iterator to the first row. + /// @return Const iterator to the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_iterator begin() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, 0uz); + } + + /// @brief Returns a const iterator past the last row (end sentinel). + /// @return Const iterator one position past the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_iterator end() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + /// @brief Returns a const iterator to the first row (explicit const form). + /// @return Const iterator to the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_iterator cbegin() const noexcept { + return this->begin(); + } + + /// @brief Returns a const iterator past the last row (explicit const form). + /// @return Const iterator one past the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_iterator cend() const noexcept { + return this->end(); + } + + /// @brief Returns a reverse iterator to the last row. + /// @return Reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] reverse_iterator rbegin() noexcept { + return reverse_iterator(this->end()); + } + + /// @brief Returns a reverse iterator before the first row (end sentinel). + /// @return Reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] reverse_iterator rend() noexcept { + return reverse_iterator(this->begin()); + } + + /// @brief Returns a const reverse iterator to the last row. + /// @return Const reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(this->end()); + } + + /// @brief Returns a const reverse iterator before the first row (end sentinel). + /// @return Const reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(this->begin()); + } + + /// @brief Returns a const reverse iterator to the last row (explicit const form). + /// @return Const reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_reverse_iterator crbegin() const noexcept { + return this->rbegin(); + } + + /// @brief Returns a const reverse iterator before the first row (explicit const form). + /// @return Const reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications + [[nodiscard]] const_reverse_iterator crend() const noexcept { + return this->rend(); + } + + // --- transformations --- + + /// @brief Transposes the matrix mathematically (rows become columns, columns become rows). + /// @return A new `flat_matrix` instance containing the transposed data + /// @note **Time Complexity:** $O(R \times C)$ to generate and fill the new matrix. + [[nodiscard]] flat_matrix transpose() const { + flat_matrix result(this->_n_cols, this->_n_rows); + for (size_type r = 0uz; r < this->_n_rows; ++r) + for (size_type c = 0uz; c < this->_n_cols; ++c) + result[c, r] = (*this)[r, c]; + return result; + } + +private: + /// @brief Validates that the row index is within mathematical bounds. + /// @param r The row index to check + /// @exception std::out_of_range If `r >= n_rows()` + /// @note Used internally by checked accessors + void _check_row(size_type r) const { + if (r >= this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::_check_row: r (which is {}) >= this->n_rows() (which is {})", + r, + this->_n_rows + )); + } + } + + /// @brief Validates that the column index is within mathematical bounds. + /// @param c The column index to check + /// @exception std::out_of_range If `c >= n_cols()` + /// @note Used internally by checked accessors + void _check_col(size_type c) const { + if (c >= this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::_check_col: c (which is {}) >= this->n_cols() (which is {})", + c, + this->_n_cols + )); + } + } + + /// @brief Internal non-throwing helper generating a mutable strided view over a column. + /// @param c The column index (assumed valid) + /// @return A zero-overhead `std::views::stride` representing the column elements + [[nodiscard]] auto _col_impl(size_type c) noexcept { + return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); + } + + /// @brief Internal non-throwing helper generating a const strided view over a column. + /// @param c The column index (assumed valid) + /// @return A zero-overhead `std::views::stride` representing the const column elements + [[nodiscard]] auto _col_impl(size_type c) const noexcept { + return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); + } + + size_type _n_rows{0uz}; + size_type _n_cols{0uz}; + std::vector _data; +}; + +} // namespace gl diff --git a/tests/source/gl/test_flat_jagged_vector.cpp b/tests/source/gl/test_flat_jagged_vector.cpp index fe5554c..53a956a 100644 --- a/tests/source/gl/test_flat_jagged_vector.cpp +++ b/tests/source/gl/test_flat_jagged_vector.cpp @@ -162,47 +162,47 @@ struct test_flat_jagged_vector_comparison { TEST_CASE_FIXTURE( test_flat_jagged_vector_comparison, - "equality operator should return true for equal segment_vectors" + "equality operator should return true for equal flat_jagged_vectors" ) { - sut_type sv1{ + sut_type jv1{ {1, 2}, {3, 4} }; - sut_type sv2{ + sut_type jv2{ {1, 2}, {3, 4} }; - CHECK_EQ(sv1, sv2); + CHECK_EQ(jv1, jv2); } TEST_CASE_FIXTURE( test_flat_jagged_vector_comparison, - "inequality operator should return true for different segment_vectors" + "inequality operator should return true for different flat_jagged_vectors" ) { - sut_type sv1{ + sut_type jv1{ {1, 2}, {3, 4} }; - sut_type sv2{ + sut_type jv2{ {1, 2}, {3, 5} }; - sut_type sv3{ + sut_type jv3{ {1, 2}, {3, 4}, {5} }; - CHECK_NE(sv1, sv2); - CHECK_NE(sv1, sv3); + CHECK_NE(jv1, jv2); + CHECK_NE(jv1, jv3); } -TEST_CASE_FIXTURE(test_flat_jagged_vector_comparison, "empty segment_vectors should be equal") { - sut_type sv1; - sut_type sv2; +TEST_CASE_FIXTURE(test_flat_jagged_vector_comparison, "empty flat_jagged_vectors should be equal") { + sut_type jv1; + sut_type jv2; - CHECK_EQ(sv1, sv2); + CHECK_EQ(jv1, jv2); } struct test_flat_jagged_vector_capacity { @@ -385,7 +385,7 @@ TEST_CASE_FIXTURE(test_flat_jagged_vector_capacity, "clear should remove all seg CHECK_EQ(sut.data_size(), 0uz); } -struct test_segment_vector_accessors { +struct test_flat_jagged_vector_accessors { using sut_type = gl::flat_jagged_vector; sut_type sut{ @@ -406,7 +406,9 @@ struct test_segment_vector_accessors { std::size_t dummy_offset = 999uz; }; -TEST_CASE_FIXTURE(test_segment_vector_accessors, "operator[] should return segment at given index") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "operator[] should return segment at given index" +) { auto seg0 = sut[0uz]; CHECK(std::ranges::equal(seg0, data[0uz])); seg0.front() = dummy_value; @@ -424,7 +426,7 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "operator[] should return segme } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const operator[] should return const segment at given index" + test_flat_jagged_vector_accessors, "const operator[] should return const segment at given index" ) { const auto& const_sut = sut; @@ -435,7 +437,7 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::equal(seg1, data[1uz])); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should return segment at given index") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "at() should return segment at given index") { auto seg0 = sut.at(0uz); CHECK(std::ranges::equal(seg0, data[0uz])); seg0.front() = dummy_value; @@ -452,13 +454,13 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should return segment at CHECK_EQ(sut.at(2uz).front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should throw for out of range index") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "at() should throw for out of range index") { CHECK_THROWS_AS(static_cast(sut.at(3uz)), std::out_of_range); CHECK_THROWS_AS(static_cast(sut.at(10uz)), std::out_of_range); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const at() should return const segment at given index" + test_flat_jagged_vector_accessors, "const at() should return const segment at given index" ) { const auto& const_sut = sut; @@ -472,13 +474,17 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::equal(seg2, data[2uz])); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const at() should throw for out of range index") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const at() should throw for out of range index" +) { const auto& const_sut = sut; CHECK_THROWS_AS(static_cast(const_sut.at(3uz)), std::out_of_range); CHECK_THROWS_AS(static_cast(const_sut.at(10uz)), std::out_of_range); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "segments() should return a view of all segments") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "segments() should return a view of all segments" +) { auto n_segments = 0uz; for (auto seg : sut.segments()) { CHECK(std::ranges::equal(seg, data[n_segments])); @@ -493,7 +499,7 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "segments() should return a vie } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const segments() should return a const view of all segments" + test_flat_jagged_vector_accessors, "const segments() should return a const view of all segments" ) { const auto& const_sut = sut; @@ -507,7 +513,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "empty(i) should return true for empty segments and false for non-empty segments" ) { CHECK_FALSE(sut.empty(0uz)); @@ -519,7 +525,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "segment_size(i) should return the size of a segment" + test_flat_jagged_vector_accessors, "segment_size(i) should return the size of a segment" ) { CHECK_EQ(sut.segment_size(0uz), 3uz); CHECK_EQ(sut.segment_size(1uz), 2uz); @@ -527,26 +533,28 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "data_size() should return the total number of elements" + test_flat_jagged_vector_accessors, "data_size() should return the total number of elements" ) { CHECK_EQ(sut.data_size(), 6uz); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "data_view() should return a span of all data") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "data_view() should return a span of all data" +) { CHECK(std::ranges::equal(sut.data_view(), flat_data)); sut.data_view().front() = dummy_value; CHECK_EQ(sut.data_view().front(), dummy_value); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const data_view() should return a const span of all data" + test_flat_jagged_vector_accessors, "const data_view() should return a const span of all data" ) { const auto& const_sut = sut; CHECK(std::ranges::equal(const_sut.data_view(), flat_data)); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "data_storage() should return a mutable reference to the internal vector" ) { auto& storage_ref = sut.data_storage(); @@ -559,7 +567,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const data_storage() should return a const reference to the internal vector" ) { const auto& const_sut = sut; @@ -570,7 +578,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "data_ptr() should return a mutable raw pointer to the first element" ) { auto* ptr = sut.data_ptr(); @@ -584,7 +592,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const data_ptr() should return a const raw pointer to the first element" ) { const auto& const_sut = sut; @@ -595,7 +603,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "offsets_view() should return a span of all offsets" + test_flat_jagged_vector_accessors, "offsets_view() should return a span of all offsets" ) { CHECK(std::ranges::equal(sut.offsets_view(), offsets)); sut.offsets_view().front() = dummy_offset; @@ -603,14 +611,15 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const offsets_view() should return a const span of all offsets" + test_flat_jagged_vector_accessors, + "const offsets_view() should return a const span of all offsets" ) { const auto& const_sut = sut; CHECK(std::ranges::equal(const_sut.offsets_view(), offsets)); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "offsets_storage() should return a mutable reference to the internal vector" ) { auto& storage_ref = sut.offsets_storage(); @@ -623,7 +632,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const offsets_storage() should return a const reference to the internal vector" ) { const auto& const_sut = sut; @@ -634,7 +643,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "offsets_ptr() should return a mutable raw pointer to the first element" ) { auto* ptr = sut.offsets_ptr(); @@ -648,7 +657,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const offsets_ptr() should return a const raw pointer to the first element" ) { const auto& const_sut = sut; @@ -658,7 +667,7 @@ TEST_CASE_FIXTURE( CHECK(std::equal(ptr, ptr + const_sut.offsets_view().size(), offsets.begin())); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "front() should return the first segment") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "front() should return the first segment") { auto front_seg = sut.front(); CHECK(std::ranges::equal(front_seg, data.front())); @@ -666,13 +675,15 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "front() should return the firs CHECK_EQ(sut.front().front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const front() should return const first segment") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const front() should return const first segment" +) { const auto& const_sut = sut; auto front_seg = const_sut.front(); CHECK(std::ranges::equal(front_seg, data.front())); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "back() should return the last segment") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "back() should return the last segment") { auto back_seg = sut.back(); CHECK(std::ranges::equal(back_seg, data.back())); @@ -680,7 +691,9 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "back() should return the last CHECK_EQ(sut.back().front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const back() should return const last segment") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const back() should return const last segment" +) { const auto& const_sut = sut; auto back_seg = const_sut.back(); CHECK(std::ranges::equal(back_seg, data.back())); @@ -711,7 +724,6 @@ TEST_CASE_FIXTURE( sut[0uz, 1uz] = dummy_value; CHECK_EQ(sut[0uz, 1uz], dummy_value); - CHECK_EQ(sut[0uz, 2uz], seg0[2uz]); sut[0uz, 2uz] = dummy_value; CHECK_EQ(sut[0uz, 2uz], dummy_value); diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp new file mode 100644 index 0000000..1696d3d --- /dev/null +++ b/tests/source/gl/test_flat_matrix.cpp @@ -0,0 +1,1655 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace gl_testing { + +TEST_SUITE_BEGIN("test_flat_matrix"); + +struct test_flat_matrix_constructors { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "default constructor should create empty flat_matrix" +) { + sut_type sut; + CHECK(sut.empty()); + CHECK_EQ(sut.size(), 0uz); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + CHECK_EQ(sut.data_size(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "copy constructor should create an equal copy") { + sut_type original; + original.push_row({1, 2, 3}); + original.push_row({4, 5, 6}); + + sut_type copy = original; + + CHECK_EQ(copy, original); + CHECK_EQ(copy.size(), 2uz); + CHECK_EQ(copy.n_rows(), 2uz); + CHECK_EQ(copy.n_cols(), 3uz); + CHECK_EQ(copy.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "copy assignment should create an equal copy") { + sut_type original; + original.push_row({1, 2, 3}); + original.push_row({4, 5, 6}); + + sut_type dest; + dest = original; + + CHECK_EQ(dest, original); + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "move constructor should transfer ownership") { + sut_type source; + source.push_row({1, 2, 3}); + source.push_row({4, 5, 6}); + + sut_type dest = std::move(source); + + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); + CHECK(source.empty()); + CHECK_EQ(source.n_rows(), 0uz); + CHECK_EQ(source.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "move assignment should transfer ownership") { + sut_type source; + source.push_row({1, 2, 3}); + source.push_row({4, 5, 6}); + + sut_type dest; + dest = std::move(source); + + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); + CHECK(source.empty()); + CHECK_EQ(source.n_rows(), 0uz); + CHECK_EQ(source.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "move assignment should handle self-assignment correctly" +) { + sut_type sut; + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + + sut = std::move(sut); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "(n_rows, n_cols) constructor should build a matrixed filled with a default type value" +) { + sut_type sut(2uz, 3uz); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::all_of(sut.data_view(), [](const auto v) { return v == int{}; })); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "(n_rows, n_cols, value) constructor should build a matrixed filled with the given value" +) { + int fill_value = 123; + sut_type sut(2uz, 3uz, fill_value); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::all_of(sut.data_view(), [fill_value](const auto v) { + return v == fill_value; + })); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "initializer list constructor should initialize matrix for same-size rows" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "initializer list constructor should throw for different size rows" +) { + const auto create_sut = []() { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7} + }; + }; + CHECK_THROWS_AS(create_sut(), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "range constructor should initialize matrix for same-size rows" +) { + std::vector> data{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + sut_type sut{std::move(data)}; + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "range constructor should throw for different size rows" +) { + std::vector> data{ + {1, 2, 3}, + {4, 5, 6}, + {7} + }; + const auto create_sut = [&data]() { sut_type sut{std::move(data)}; }; + CHECK_THROWS_AS(create_sut(), std::invalid_argument); +} + +struct test_flat_matrix_comparison { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_comparison, "equality operator should return true for equal matrices" +) { + sut_type m1{ + {1, 2}, + {3, 4} + }; + sut_type m2{ + {1, 2}, + {3, 4} + }; + + CHECK_EQ(m1, m2); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_comparison, "inequality operator should return true for different matrices" +) { + sut_type m1{ + {1, 2}, + {3, 4} + }; + sut_type m2{ + {1, 2}, + {3, 5} + }; + sut_type m3{ + {1, 2}, + {3, 4}, + {5, 6} + }; + + CHECK_NE(m1, m2); + CHECK_NE(m1, m3); +} + +TEST_CASE_FIXTURE(test_flat_matrix_comparison, "empty matrices should be equal") { + sut_type m1; + sut_type m2; + + CHECK_EQ(m1, m2); +} + +struct test_flat_matrix_capacity { + using sut_type = gl::flat_matrix; + sut_type sut; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "size and n_rows should return the number of rows and n_cols should return the number of " + "columns" +) { + CHECK_EQ(sut.size(), 0uz); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + + sut.push_row({1, 2, 3}); + CHECK_EQ(sut.size(), 1uz); + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + + sut.push_row({4, 5, 6}); + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + + sut.push_row({7, 8, 9}); + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "empty should return true only when there are no elements in the matrix" +) { + CHECK(sut.empty()); + + sut.push_row({1, 2, 3}); + CHECK_FALSE(sut.empty()); + + sut.pop_row(); + CHECK(sut.empty()); + + sut.push_col({1, 2, 3}); + CHECK_FALSE(sut.empty()); + + sut.pop_col(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "reserve_data should reserve space for elements") { + sut.reserve_data(10uz); + CHECK_EQ(sut.data_capacity(), 10uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "shrink_to_fit should reduce capacity") { + sut.push_row({1, 2, 3}); + sut.shrink_to_fit(); + + CHECK_EQ(sut.data_capacity(), 3uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should shrink container when n_rows < current n_rows" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(2uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should shrink container when c_cols < current n_cols" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 2uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should grow container with the fiven value when n_rows > current " + "n_rows" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(4uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 4uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 12uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-1, -1, -1})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should grow container with default type value when n_cols > current " + "n_cols" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 4uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 12uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should properly change the dimensions of the matrix for mixed size " + "differences" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(4uz, 4uz, -1); + + CHECK_EQ(sut.n_rows(), 4uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 16uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-1, -1, -1, -1})); + + sut.resize(3uz, 5uz, -2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 5uz); + CHECK_EQ(sut.data_size(), 15uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1, -2})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1, -2})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1, -2})); + + sut.resize(5uz, 3uz, -3); + + CHECK_EQ(sut.n_rows(), 5uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 15uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-3, -3, -3})); + CHECK(std::ranges::equal(sut[4uz], std::vector{-3, -3, -3})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should do nothing when dimensions are not changed" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "clear should remove all data") { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + + sut.clear(); + + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + CHECK_EQ(sut.data_size(), 0uz); +} + +struct test_flat_matrix_accessors { + using sut_type = gl::flat_matrix; + + sut_type sut{ + {1, 2, 3}, + {4, 5, 6} + }; + + std::vector sut_row0{1, 2, 3}; + std::vector sut_row1{4, 5, 6}; + + std::vector sut_col0{1, 4}; + std::vector sut_col1{2, 5}; + std::vector sut_col2{3, 6}; + + std::vector> sut_rows{sut_row0, sut_row1}; // rename to sut_rows + std::vector> sut_cols{sut_col0, sut_col1, sut_col2}; + std::vector flat_data{1, 2, 3, 4, 5, 6}; + + std::size_t sut_n_rows = 2uz; + std::size_t sut_n_cols = 3uz; + + int dummy_value = 111; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "operator[r] should return row at given index") { + auto r0 = sut[0uz]; + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut[0uz].front(), dummy_value); + + auto r1 = sut[1uz]; + CHECK(std::ranges::equal(r1, sut_rows[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut[1uz].front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const operator[r] should return const row at given index" +) { + const auto& const_sut = sut; + + auto r0 = const_sut[0uz]; + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + + auto r1 = const_sut[1uz]; + CHECK(std::ranges::equal(r1, sut_rows[1uz])); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "operator[](r, r) should return element at given row and position" +) { + CHECK_EQ(sut[0uz, 0uz], sut_row0[0uz]); + sut[0uz, 0uz] = dummy_value; + CHECK_EQ(sut[0uz, 0uz], dummy_value); + + CHECK_EQ(sut[0uz, 1uz], sut_row0[1uz]); + sut[0uz, 1uz] = dummy_value; + CHECK_EQ(sut[0uz, 1uz], dummy_value); + + CHECK_EQ(sut[0uz, 2uz], sut_row0[2uz]); + sut[0uz, 2uz] = dummy_value; + CHECK_EQ(sut[0uz, 2uz], dummy_value); + + CHECK_EQ(sut[1uz, 0uz], sut_row1[0uz]); + sut[1uz, 0uz] = dummy_value; + CHECK_EQ(sut[1uz, 0uz], dummy_value); + + CHECK_EQ(sut[1uz, 1uz], sut_row1[1uz]); + sut[1uz, 1uz] = dummy_value; + CHECK_EQ(sut[1uz, 1uz], dummy_value); + + CHECK_EQ(sut[1uz, 2uz], sut_row1[2uz]); + sut[1uz, 2uz] = dummy_value; + CHECK_EQ(sut[1uz, 2uz], dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const operator[](r, c) should return const element") { + const auto& const_sut = sut; + + CHECK_EQ(const_sut[0uz, 0uz], sut_row0[0uz]); + CHECK_EQ(const_sut[0uz, 1uz], sut_row0[1uz]); + CHECK_EQ(const_sut[0uz, 2uz], sut_row0[2uz]); + CHECK_EQ(const_sut[1uz, 0uz], sut_row1[0uz]); + CHECK_EQ(const_sut[1uz, 1uz], sut_row1[1uz]); + CHECK_EQ(const_sut[1uz, 2uz], sut_row1[2uz]); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r) should return row at given index") { + auto r0 = sut.at(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut.at(0uz).front(), dummy_value); + + auto r1 = sut.at(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut.at(1uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.at(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should return const row at given index") { + const auto& const_sut = sut; + + auto r0 = const_sut.at(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + + auto r1 = const_sut.at(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should throw for out of range index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "at(r, r) should return element at given row and position" +) { + CHECK_EQ(sut.at(0uz, 0uz), sut_row0[0uz]); + sut.at(0uz, 0uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 0uz), dummy_value); + + CHECK_EQ(sut.at(0uz, 1uz), sut_row0[1uz]); + sut.at(0uz, 1uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 1uz), dummy_value); + + CHECK_EQ(sut.at(0uz, 2uz), sut_row0[2uz]); + sut.at(0uz, 2uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 2uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 0uz), sut_row1[0uz]); + sut.at(1uz, 0uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 0uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 1uz), sut_row1[1uz]); + sut.at(1uz, 1uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 1uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 2uz), sut_row1[2uz]); + sut.at(1uz, 2uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 2uz), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r, c) should throw for invalid row index") { + CHECK_THROWS_AS(static_cast(sut.at(2uz, 0uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(10uz, 0uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r, c) should throw for invalid col index") { + CHECK_THROWS_AS(static_cast(sut.at(0uz, 3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(1uz, 10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should return const element") { + const auto& const_sut = sut; + + CHECK_EQ(const_sut.at(0uz, 0uz), sut_row0[0uz]); + CHECK_EQ(const_sut.at(0uz, 1uz), sut_row0[1uz]); + CHECK_EQ(const_sut.at(0uz, 2uz), sut_row0[2uz]); + CHECK_EQ(const_sut.at(1uz, 0uz), sut_row1[0uz]); + CHECK_EQ(const_sut.at(1uz, 1uz), sut_row1[1uz]); + CHECK_EQ(const_sut.at(1uz, 2uz), sut_row1[2uz]); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should throw for invalid row index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(2uz, 0uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(10uz, 0uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should throw for invalid col index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(0uz, 3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(1uz, 10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front() should return the first row") { + auto front_row = sut.front(); + CHECK(std::ranges::equal(front_row, sut_rows.front())); + + front_row.front() = dummy_value; + CHECK_EQ(sut.front().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front() should return const first row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.front(), sut_rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row") { + auto back_row = sut.back(); + CHECK(std::ranges::equal(back_row, sut_rows.back())); + + back_row.front() = dummy_value; + CHECK_EQ(sut.back().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back() should return const last row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.back(), sut_rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front_row() should return the first row") { + auto front_row = sut.front_row(); + CHECK(std::ranges::equal(front_row, sut_rows.front())); + + front_row.front() = dummy_value; + CHECK_EQ(sut.front_row().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front_row() should return const first row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.front_row(), sut_rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back_row() should return the last row") { + auto back_row = sut.back_row(); + CHECK(std::ranges::equal(back_row, sut_rows.back())); + + back_row.front() = dummy_value; + CHECK_EQ(sut.back_row().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back_row() should return const last row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.back_row(), sut_rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front_col() should return the first column") { + auto front_col = sut.front_col(); + CHECK(std::ranges::equal(front_col, sut_cols.front())); + + front_col.front() = dummy_value; + CHECK_EQ(sut.front_col().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front_col() should return const first column") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.front_col(), sut_cols.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back_col() should return the last column") { + auto back_col = sut.back_col(); + CHECK(std::ranges::equal(back_col, sut_cols.back())); + + back_col.front() = dummy_value; + CHECK_EQ(sut.back_col().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back_col() should return const last column") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.back_col(), sut_cols.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "row(r) should return row at given index") { + auto r0 = sut.row(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut.row(0uz).front(), dummy_value); + + auto r1 = sut.row(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut.row(1uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "row(r) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.row(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.row(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const row(r) should return const row at given index" +) { + const auto& const_sut = sut; + + auto r0 = const_sut.row(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + + auto r1 = const_sut.row(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const row(r) should throw for out of range index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.row(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.row(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "col(c) should return column at given index") { + auto c0 = sut.col(0uz); + CHECK(std::ranges::equal(c0, sut_col0)); + c0.front() = dummy_value; + CHECK_EQ(sut.col(0uz).front(), dummy_value); + + auto c1 = sut.col(1uz); + CHECK(std::ranges::equal(c1, sut_col1)); + c1.front() = dummy_value; + CHECK_EQ(sut.col(1uz).front(), dummy_value); + + auto c2 = sut.col(2uz); + CHECK(std::ranges::equal(c2, sut_col2)); + c2.front() = dummy_value; + CHECK_EQ(sut.col(2uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "col(c) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.col(3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.col(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const col(c) should return const column at given index" +) { + const auto& const_sut = sut; + + auto c0 = const_sut.col(0uz); + CHECK(std::ranges::equal(c0, sut_col0)); + + auto c1 = const_sut.col(1uz); + CHECK(std::ranges::equal(c1, sut_col1)); + + auto c2 = const_sut.col(2uz); + CHECK(std::ranges::equal(c2, sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const col(c) should throw for out of range index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.col(3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.col(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "rows() should return a view of all rows") { + auto n_rows = 0uz; + for (auto row : sut.rows()) { + CHECK(std::ranges::equal(row, sut_rows[n_rows])); + const auto orig_val = std::exchange(row.front(), dummy_value); + CHECK_EQ(sut[n_rows].front(), dummy_value); + row.front() = orig_val; // revert change + + n_rows++; + } + + CHECK_EQ(n_rows, sut_n_rows); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const rows() should return a const view of all rows" +) { + const auto& const_sut = sut; + + auto n_rows = 0uz; + for (auto row : const_sut.rows()) { + CHECK(std::ranges::equal(row, sut_rows[n_rows])); + n_rows++; + } + + CHECK_EQ(n_rows, sut_n_rows); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "cols() should return a view of all columns") { + auto n_cols = 0uz; + for (auto col : sut.cols()) { + CHECK(std::ranges::equal(col, sut_cols[n_cols])); + const auto orig_val = std::exchange(col.front(), dummy_value); + CHECK_EQ(sut.col(n_cols).front(), dummy_value); + col.front() = orig_val; // revert change + + n_cols++; + } + + CHECK_EQ(n_cols, sut_n_cols); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const cols() should return a const view of all columns" +) { + const auto& const_sut = sut; + + auto n_cols = 0uz; + for (auto col : const_sut.cols()) { + CHECK(std::ranges::equal(col, sut_cols[n_cols])); + n_cols++; + } + + CHECK_EQ(n_cols, sut_n_cols); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_size() should return the total number of elements in the matrix" +) { + CHECK_EQ(sut.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "data_view() should return a span of all data") { + CHECK(std::ranges::equal(sut.data_view(), flat_data)); + sut.data_view().front() = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const data_view() should return a const span of all data" +) { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.data_view(), flat_data)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_storage() should return a mutable reference to the internal vector" +) { + auto& storage_ref = sut.data_storage(); + CHECK(std::ranges::equal(storage_ref, flat_data)); + CHECK_EQ(storage_ref.data(), sut.data_view().data()); + + // check mutability + storage_ref.front() = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "const data_storage() should return a const reference to the internal vector" +) { + const auto& const_sut = sut; + const auto& storage_ref = const_sut.data_storage(); + + CHECK(std::ranges::equal(storage_ref, flat_data)); + CHECK_EQ(storage_ref.data(), const_sut.data_view().data()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_ptr() should return a mutable raw pointer to the first element" +) { + auto* ptr = sut.data_ptr(); + + CHECK_EQ(ptr, sut.data_view().data()); + CHECK(std::equal(ptr, ptr + sut.data_size(), flat_data.begin())); + + // check mutability + *ptr = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "const data_ptr() should return a const raw pointer to the first element" +) { + const auto& const_sut = sut; + const auto* ptr = const_sut.data_ptr(); + + CHECK_EQ(ptr, const_sut.data_view().data()); + CHECK(std::equal(ptr, ptr + const_sut.data_size(), flat_data.begin())); +} + +struct test_flat_matrix_row_modifiers { + using sut_type = gl::flat_matrix; + + sut_type sut; + std::vector sut_row0{1, 2, 3}; + std::vector sut_row1{4, 5, 6}; + std::vector sut_row2{7, 8, 9}; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "push_row with span should add new row") { + sut.push_row(std::span{sut_row0}); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "push_row with vector should add new row") { + sut.push_row(sut_row0); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "push_row with initializer list should add new row" +) { + sut.push_row({1, 2, 3}); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "push_row should throw for a not matching row size" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.push_row({11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "multiple push_row calls should add multiple rows" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row should remove last row") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + REQUIRE(std::ranges::equal(sut.back(), sut_row1)); + + sut.pop_row(); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size()); + CHECK(std::ranges::equal(sut.back(), sut_row0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row on empty container should do nothing") { + sut.pop_row(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row should remove all rows sequentially") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut.back(), sut_row2)); + + sut.pop_row(); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size()); + CHECK(std::ranges::equal(sut.back(), sut_row1)); + + sut.pop_row(); + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size()); + CHECK(std::ranges::equal(sut.back(), sut_row0)); + + sut.pop_row(); + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "insert_row should add row at given position") { + sut.push_row(sut_row0); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row2)); + + sut.insert_row(1uz, sut_row1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "insert_row should throw for an invalid position" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.insert_row(3uz, sut_row2), std::out_of_range); + CHECK_THROWS_AS(sut.insert_row(10uz, sut_row2), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "insert_row should throw for a not matching row size" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.insert_row(1uz, {11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "erase_row should remove row at given position") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); + + sut.erase_row(1uz); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "erase_row should throw for an invalid position") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.erase_row(2uz), std::out_of_range); + CHECK_THROWS_AS(sut.erase_row(10uz), std::out_of_range); +} + +struct test_flat_matrix_col_modifiers { + using sut_type = gl::flat_matrix; + + sut_type sut; + std::vector sut_col0{1, 2, 3}; + std::vector sut_col1{4, 5, 6}; + std::vector sut_col2{7, 8, 9}; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "push_col with span should add new column") { + sut.push_col(std::span{sut_col0}); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "push_col with vector should add new column") { + sut.push_col(sut_col0); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "push_col with initializer list should add new column" +) { + sut.push_col({1, 2, 3}); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "push_col should throw for a not matching col size" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.push_col({11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "multiple push_col calls should add multiple columns" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col should remove last column") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + REQUIRE(std::ranges::equal(sut.back_col(), sut_col1)); + + sut.pop_col(); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), sut_col0.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col on empty container should do nothing") { + sut.pop_col(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col should remove all columns sequentially") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col2)); + + sut.pop_col(); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col1)); + + sut.pop_col(); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), sut_col0.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col0)); + + sut.pop_col(); + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "insert_col should add column at given position") { + sut.push_col(sut_col0); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col2)); + + sut.insert_col(1uz, sut_col1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "insert_col should throw for an invalid position" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.insert_row(4uz, sut_col2), std::out_of_range); + CHECK_THROWS_AS(sut.insert_row(10uz, sut_col2), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "insert_col should throw for a not matching col size" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.insert_col(1uz, {11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "erase_col should remove column at given position" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); + + sut.erase_col(1uz); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "erase_col should throw for an invalid position") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.erase_col(2uz), std::out_of_range); + CHECK_THROWS_AS(sut.erase_col(10uz), std::out_of_range); +} + +struct test_flat_matrix_complex_operations { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "interleaved row and col operations should work correctly" +) { + sut_type sut; + + sut.push_row({1, 2}); + sut.push_col({3}); + sut.insert_row(0uz, {10, 20, 30}); + sut.insert_col(1uz, {15, 25}); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 8uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{10, 15, 20, 30})); + CHECK(std::ranges::equal(sut[1uz], std::vector{1, 25, 2, 3})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "clearing and refilling should work correctly" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6} + }; + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + + sut.clear(); + + CHECK(sut.empty()); + + sut.push_row({9, 8}); + sut.push_row({7, 6}); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), 4uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{9, 8})); + CHECK(std::ranges::equal(sut[1uz], std::vector{7, 6})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "large flat_matrix operations should maintain integrity" +) { + sut_type sut; + + for (int i = 0; i < 100; ++i) { + std::vector row; + for (int j = 0; j < 10; ++j) + row.push_back(i * 10 + j); + sut.push_row(row); + } + + CHECK_EQ(sut.n_rows(), 100uz); + CHECK_EQ(sut.n_cols(), 10uz); + CHECK_EQ(sut.data_size(), 1000uz); + for (int i = 0; i < 100; ++i) { + auto row = sut[i]; + for (int j = 0; j < 10; ++j) + CHECK_EQ(row[j], i * 10 + j); + } +} + +struct test_flat_matrix_iterators { + using sut_type = gl::flat_matrix; + + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + std::vector> rows{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "begin() should return iterator to first row") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "end() should return iterator past last row") { + auto it_begin = sut.begin(); + auto it_end = sut.end(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "const begin() should return const iterator to first row" +) { + const auto& const_sut = sut; + auto it = const_sut.begin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "const end() should return const iterator past last row" +) { + const auto& const_sut = sut; + auto it_begin = const_sut.begin(); + auto it_end = const_sut.end(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "cbegin() should return const iterator") { + auto it = sut.cbegin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "cend() should return const iterator") { + auto it_begin = sut.cbegin(); + auto it_end = sut.cend(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "non-const iterator should convert to const iterator implicitly" +) { + auto non_const_it = sut.begin(); + typename sut_type::const_iterator const_it = non_const_it; + CHECK(std::ranges::equal(*const_it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "dereferencing iterator should return row") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + ++it; + CHECK(std::ranges::equal(*it, rows[1uz])); + + ++it; + CHECK(std::ranges::equal(*it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator[] should access row at offset") { + auto it = sut.begin(); + CHECK(std::ranges::equal(it[0uz], rows[0uz])); + CHECK(std::ranges::equal(it[1uz], rows[1uz])); + CHECK(std::ranges::equal(it[2uz], rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "pre-increment should advance iterator") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + auto& ret = ++it; + CHECK(std::ranges::equal(*ret, rows[1uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "post-increment should return old iterator") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + auto old_it = it++; + CHECK(std::ranges::equal(*old_it, rows[0uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "pre-decrement should move iterator backward") { + auto it = sut.end(); + --it; + CHECK(std::ranges::equal(*it, rows[2uz])); + + auto& ret = --it; + CHECK(std::ranges::equal(*ret, rows[1uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "post-decrement should return old iterator") { + auto it = sut.end(); + --it; + CHECK(std::ranges::equal(*it, rows[2uz])); + + auto old_it = it--; + CHECK(std::ranges::equal(*old_it, rows[2uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator+= should advance iterator") { + auto it = sut.begin(); + it += 2; + CHECK(std::ranges::equal(*it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator-= should move iterator backward") { + auto it = sut.end(); + it -= 1; + CHECK(std::ranges::equal(*it, rows[2uz])); + + it -= 2; + CHECK(std::ranges::equal(*it, rows[0uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator+ should create new iterator") { + auto it = sut.begin(); + auto new_it = it + 1; + + CHECK(std::ranges::equal(*it, rows[0uz])); + CHECK(std::ranges::equal(*new_it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "reverse operator+ should create new iterator") { + auto it = sut.begin(); + auto new_it = 2 + it; + + CHECK(std::ranges::equal(*it, rows[0uz])); + CHECK(std::ranges::equal(*new_it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator- should create new iterator") { + auto it = sut.end(); + auto new_it = it - 1; + + CHECK(std::ranges::equal(*new_it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator- with two iterators should give distance") { + auto it1 = sut.begin(); + auto it2 = sut.end(); + + CHECK_EQ(it2 - it1, sut.n_rows()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator== should compare iterators") { + auto it1 = sut.begin(); + auto it2 = sut.begin(); + auto it3 = sut.begin() + 1; + + CHECK_EQ(it1, it2); + CHECK_NE(it1, it3); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator<=> should compare iterators") { + auto it1 = sut.begin(); + auto it2 = sut.begin() + 1; + auto it3 = sut.begin() + 2; + + CHECK_LT(it1, it2); + CHECK_LT(it2, it3); + CHECK_LE(it1, it2); + CHECK_LE(it1, it1); + CHECK_GT(it2, it1); + CHECK_GT(it3, it2); + CHECK_GE(it2, it1); + CHECK_GE(it2, it2); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "range-based for loop should iterate all rows") { + std::size_t idx = 0uz; + for (auto row : sut) + CHECK(std::ranges::equal(row, rows[idx++])); + CHECK_EQ(idx, 3uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "const range-based for loop should iterate all rows") { + const auto& const_sut = sut; + std::size_t idx = 0uz; + for (auto row : const_sut) + CHECK(std::ranges::equal(row, rows[idx++])); + CHECK_EQ(idx, 3uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "rbegin() should return reverse iterator") { + auto it = sut.rbegin(); + CHECK(std::ranges::equal(*it, rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "rend() should return reverse iterator past first") { + auto it_rbegin = sut.rbegin(); + auto it_rend = sut.rend(); + CHECK_EQ(it_rend - it_rbegin, rows.size()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "crbegin() should return const reverse iterator") { + auto it = sut.crbegin(); + CHECK(std::ranges::equal(*it, rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "crend() should return const reverse iterator") { + auto it_rbegin = sut.crbegin(); + auto it_rend = sut.crend(); + CHECK_EQ(it_rend - it_rbegin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "reverse range-based for loop should iterate in reverse" +) { + std::size_t idx = 2uz; + for (auto it : std::ranges::reverse_view(sut)) { + CHECK(std::ranges::equal(it, rows[idx])); + if (idx > 0uz) + --idx; + } +} + +struct test_flat_matrix_transformations { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose should correctly transpose a square matrix" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + + auto transposed = sut.transpose(); + + CHECK_EQ(transposed.n_rows(), 3uz); + CHECK_EQ(transposed.n_cols(), 3uz); + CHECK_EQ(transposed.data_size(), 9uz); + CHECK(std::ranges::equal(transposed[0uz], std::vector{1, 4, 7})); + CHECK(std::ranges::equal(transposed[1uz], std::vector{2, 5, 8})); + CHECK(std::ranges::equal(transposed[2uz], std::vector{3, 6, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose should correctly transpose a rectangular matrix" +) { + sut_type sut{ + {1, 2, 3, 4}, + {5, 6, 7, 8} + }; + + auto transposed = sut.transpose(); + + CHECK_EQ(transposed.n_rows(), 4uz); + CHECK_EQ(transposed.n_cols(), 2uz); + CHECK_EQ(transposed.data_size(), 8uz); + CHECK(std::ranges::equal(transposed[0uz], std::vector{1, 5})); + CHECK(std::ranges::equal(transposed[1uz], std::vector{2, 6})); + CHECK(std::ranges::equal(transposed[2uz], std::vector{3, 7})); + CHECK(std::ranges::equal(transposed[3uz], std::vector{4, 8})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose on an empty matrix should return an empty matrix" +) { + sut_type sut; + + auto transposed = sut.transpose(); + + CHECK(transposed.empty()); + CHECK_EQ(transposed.n_rows(), 0uz); + CHECK_EQ(transposed.n_cols(), 0uz); + CHECK_EQ(transposed.data_size(), 0uz); +} + +TEST_SUITE_END(); // test_flat_matrix + +} // namespace gl_testing