diff --git a/src/misc/CMakeLists.txt b/src/misc/CMakeLists.txt
index f45fa06960..031ad3e000 100644
--- a/src/misc/CMakeLists.txt
+++ b/src/misc/CMakeLists.txt
@@ -1,4 +1,5 @@
add_files(
+ alternating_iterator.hpp
binaryheap.hpp
dbg_helpers.cpp
dbg_helpers.h
diff --git a/src/misc/alternating_iterator.hpp b/src/misc/alternating_iterator.hpp
new file mode 100644
index 0000000000..a5b6d6f591
--- /dev/null
+++ b/src/misc/alternating_iterator.hpp
@@ -0,0 +1,145 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/** @file alternating_iterator.hpp Iterator adaptor that takes items alternating from a middle position. */
+
+#ifndef ALTERNATING_ITERATOR_HPP
+#define ALTERNATING_ITERATOR_HPP
+
+#include
+
+/**
+ * Iterator that alternately takes from the "middle" of a range.
+ * @tparam Titer Type of iterator.
+ */
+template
+class AlternatingIterator {
+public:
+ using value_type = typename Titer::value_type;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::forward_iterator_tag;
+ using pointer = typename Titer::pointer;
+ using reference = typename Titer::reference;
+
+ AlternatingIterator() = default;
+
+ /**
+ * Construct an AlternatingIterator.
+ * @param first Iterator to first element.
+ * @param last Iterator to last element.
+ * @param middle Iterator to "middle" element, from where to start.
+ * @param begin Whether this iterator points to the first or last elements.
+ */
+ AlternatingIterator(Titer first, Titer last, Titer middle, bool begin) : first(first), last(last), middle(middle)
+ {
+ /* Starting from the end is not supported, unless the range is empty. */
+ assert(first == last || middle != last);
+
+ this->position = begin ? 0 : std::distance(this->first, this->last);
+ this->before = middle;
+ this->after = middle;
+ this->next_state = this->before == this->first;
+ this->state = this->next_state;
+ }
+
+ bool operator==(const AlternatingIterator &rhs) const
+ {
+ assert(this->first == rhs.first);
+ assert(this->last == rhs.last);
+ assert(this->middle == rhs.middle);
+ return this->position == rhs.position;
+ }
+
+ std::strong_ordering operator<=>(const AlternatingIterator &rhs) const
+ {
+ assert(this->first == rhs.first);
+ assert(this->last == rhs.last);
+ assert(this->middle == rhs.middle);
+ return this->position <=> rhs.position;
+ }
+
+ inline reference operator*() const
+ {
+ return *this->Base();
+ }
+
+ AlternatingIterator &operator++()
+ {
+ size_t size = static_cast(std::distance(this->first, this->last));
+ assert(this->position < size);
+
+ ++this->position;
+ if (this->position < size) this->Next();
+
+ return *this;
+ }
+
+ AlternatingIterator operator++(int)
+ {
+ AlternatingIterator result = *this;
+ ++*this;
+ return result;
+ }
+
+ inline Titer Base() const
+ {
+ return this->state ? this->after : this->before;
+ }
+
+private:
+ Titer first; ///< Initial first iterator.
+ Titer last; ///< Initial last iterator.
+ Titer middle; ///< Initial middle iterator.
+
+ Titer after; ///< Current iterator after the middle.
+ Titer before; ///< Current iterator before the middle.
+
+ size_t position; ///< Position within the entire range.
+
+ bool next_state; ///< Next state for advancing iterator. If true take from after middle, otherwise take from before middle.
+ bool state; ///< Current state for reading iterator. If true take from after middle, otherwise take from before middle.
+
+ void Next()
+ {
+ this->state = this->next_state;
+ if (this->next_state) {
+ assert(this->after != this->last);
+ ++this->after;
+ this->next_state = this->before == this->first;
+ } else {
+ assert(this->before != this->first);
+ --this->before;
+ this->next_state = std::next(this->after) != this->last;
+ }
+ }
+};
+
+template
+class AlternatingView : public std::ranges::view_interface> {
+public:
+ AlternatingView(std::ranges::viewable_range auto &&range, Titer middle) :
+ first(std::ranges::begin(range)), last(std::ranges::end(range)), middle(middle)
+ {
+ }
+
+ auto begin() const
+ {
+ return AlternatingIterator{first, last, middle, true};
+ }
+
+ auto end() const
+ {
+ return AlternatingIterator{first, last, middle, false};
+ }
+
+private:
+ Titer first; ///< Iterator to first element.
+ Titer last; ///< Iterator to last element.
+ Titer middle; ///< Iterator to middle element.
+};
+
+#endif /* ALTERNATING_ITERATOR_HPP */
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index e2d5f11ad6..c603eb99d3 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,4 +1,5 @@
add_test_files(
+ alternating_iterator.cpp
bitmath_func.cpp
enum_over_optimisation.cpp
flatset_type.cpp
diff --git a/src/tests/alternating_iterator.cpp b/src/tests/alternating_iterator.cpp
new file mode 100644
index 0000000000..93775685f5
--- /dev/null
+++ b/src/tests/alternating_iterator.cpp
@@ -0,0 +1,48 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/** @file bitmath_func.cpp Test functionality from core/bitmath_func. */
+
+#include "../stdafx.h"
+
+#include "../3rdparty/catch2/catch.hpp"
+
+#include "../misc/alternating_iterator.hpp"
+
+#include "../safeguards.h"
+
+TEST_CASE("AlternatingIterator tests")
+{
+ auto test_case = [&](auto input, std::initializer_list expected) {
+ return std::ranges::equal(input, expected);
+ };
+
+ /* Sequence includes sentinel markers to detect out-of-bounds reads without relying on UB. */
+ std::initializer_list raw_sequence_even = {INT_MAX, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, INT_MAX};
+ const std::span sequence_even = std::span{raw_sequence_even.begin() + 1, raw_sequence_even.end() - 1};
+
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 0), { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 1), { 1, 0, 2, 3, 4, 5, 6, 7, 8, 9 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 2), { 2, 1, 3, 0, 4, 5, 6, 7, 8, 9 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 3), { 3, 2, 4, 1, 5, 0, 6, 7, 8, 9 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 4), { 4, 3, 5, 2, 6, 1, 7, 0, 8, 9 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 5), { 5, 4, 6, 3, 7, 2, 8, 1, 9, 0 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 6), { 6, 5, 7, 4, 8, 3, 9, 2, 1, 0 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 7), { 7, 6, 8, 5, 9, 4, 3, 2, 1, 0 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 8), { 8, 7, 9, 6, 5, 4, 3, 2, 1, 0 }));
+ CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 9), { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }));
+
+ /* Sequence includes sentinel markers to detect out-of-bounds reads without relying on UB. */
+ std::initializer_list raw_sequence_odd = {INT_MAX, 0, 1, 2, 3, 4, INT_MAX};
+ const std::span sequence_odd = std::span{raw_sequence_odd.begin() + 1, raw_sequence_odd.end() - 1};
+
+ CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 0), { 0, 1, 2, 3, 4 }));
+ CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 1), { 1, 0, 2, 3, 4 }));
+ CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 2), { 2, 1, 3, 0, 4 }));
+ CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 3), { 3, 2, 4, 1, 0 }));
+ CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 4), { 4, 3, 2, 1, 0 }));
+}