Hello everyone!
After a long break from competitive programming, I’m back with something I wanted to have ready in my template: an ordered set and multiset with a clean member-function interface.
One of the main reasons I created this project was to support the competitive programming community at Latakia University (Previously Tishreen University). I wanted contestants to have a reusable ordered-set and multiset template they can keep in their own templates and adapt to their problems.
The project is Organized C++ Ordered Set. You can write ordered_set<int> or ordered_multiset<int>, access sorted ranks with s[i], and use normal-looking bounds and erasure functions. It uses GNU PBDS with a strict comparator, including when duplicates are present.
Code, examples, and complete API reference
GitLab mirror
Usage first
ordered_set<int> s = {7, 2, 7, 5};
ordered_multiset<int> ms = {7, 2, 7, 5};
cout << s.size() << '\n'; // 3
cout << ms.size() << '\n'; // 4
cout << s[0] << '\n'; // 2
cout << ms[2] << '\n'; // 7
cout << ms.order_of_key(7) << '\n'; // 2
cout << ms.order_of_key_upper(7) << '\n'; // 4
cout << ms.count(7) << '\n'; // 2
cout << ms.count_range(5, 7) << '\n'; // 3
ms.erase_one(7);
cout << ms.count(7) << '\n'; // 1
ms.erase(7);
cout << ms.contains(7) << '\n'; // 0
for (int value : ms) {
cout << value << ' ';
}
cout << '\n'; // 2 5
Every rank is zero-based. s[i] returns a read-only reference and costs $$$O(\log(n+1))$$$, not $$$O(1)$$$. Both s[i] and s.at(i) throw out_of_range if the rank is invalid; find_by_order(i) returns end() instead.
Use C++17 or newer with GNU libstdc++ and PBDS. GCC works; Clang works with that library. Plain MSVC does not provide these GNU headers. Keep the includes before macros such as #define endl '\n'.
Two operations behind the interface
order_of_key(x) counts keys strictly before x. find_by_order(i) finds sorted rank i. PBDS maintains subtree sizes, which makes both operations logarithmic. GCC documents these order-statistics operations here.
For ascending values {2, 5, 7, 7}:
| Expression | Result |
s[0] | 2 |
s.order_of_key(7) | 2 |
s.order_of_key_upper(7) | 4 |
s.count(7) | 2 |
s.first_index(7) | 2 |
s.last_index(7) | 3 |
An upper-rank operation is useful because order_of_key(x + 1) can overflow and does not generalize to strings or custom keys.
Duplicates without less_equal
Please do not replace less with less_equal in a PBDS tree. The comparator must define a strict weak ordering; in particular, comparing a key with itself must return false. The standard associative-container requirements use the same rule.
The multiset stores (value, insertion ID). It compares values first and IDs only when the values are comparator-equivalent. The IDs are deterministic counters, not random priorities or hashes. Public iterators and indexed access expose only the value.
For example, inserting 7, 2, 7, 5 creates (7,0), (2,1), (7,2), (5,3). In sorted order they become (2,1), (5,3), (7,0), (7,2).
Bounds and value-rank queries walk through the existing tree metadata directly. They do not copy the searched key or manufacture sentinel pairs. The default ID is uint64_t; exhaustion is checked before wraparound. clear() resets the counter, while ordinary erasure does not.
Interface and complexity
Let $$$n$$$ count stored occurrences, including duplicates. Let $$$q$$$ be the number erased and $$$m$$$ the number inserted in a batch. Comparisons, key operations, and comparator operations are assumed constant-cost below.
| Operation | Meaning | Time |
insert(x), emplace(args...) | Unique set returns {iterator, inserted}; multiset returns the new occurrence’s iterator | $$$O(\log(n+1))$$$ |
find(x), contains(x), count(x) | Normal value lookup; count is logarithmic even with many duplicates | $$$O(\log(n+1))$$$ |
lower_bound(x), upper_bound(x), equal_range(x) | Normal bounds in comparator order | $$$O(\log(n+1))$$$ |
order_of_key(x), order_of_key_upper(x) | Number before / before-or-equivalent to x | $$$O(\log(n+1))$$$ |
find_by_order(i), s[i], at(i) | Zero-based selection | $$$O(\log(n+1))$$$ |
first_index(x), last_index(x) | First/last equivalent rank, or -1 | $$$O(\log(n+1))$$$ |
index_of(it) | Rank of this exact occurrence; end() maps to size() | $$$O(\log(n+1))$$$ |
count_range(a, b) | Inclusive value range; reversed bounds return zero | $$$O(\log(n+1))$$$ |
erase_one(x), erase(it), erase_by_order(i) | Remove one occurrence | $$$O(\log(n+1))$$$ |
erase(x) | Remove all equivalent occurrences | Unique: $$$O(\log(n+1))$$$; multi: $$$O((q+1)\log(n+1))$$$ |
erase(first, last) | Half-open iterator range | $$$O(1+q\log(n+1))$$$ |
insert(first, last), insert({a, b}) | Insert a batch | $$$O(1+m\log(n+m+1))$$$ |
size(), empty(), front(), back() | Size and cached endpoints; empty endpoints throw | $$$O(1)$$$ |
clear() | Remove all entries and reset multiset IDs | $$$O(n)$$$ |
| Forward/reverse traversal | Read-only bidirectional iteration | $$$O(n)$$$ for a full traversal |
Bulk erasure uses individual PBDS node erasures. It does not inherit std::multiset’s $$$O(\log(n+1)+q)$$$ erase-by-value bound. emplace constructs a key and inserts it; PBDS still copies that key into its node.
Both classes support range/initializer-list construction, copying, moving, and swapping. Moves allocate an empty PBDS sentinel, so they are not noexcept; move assignment also destroys the destination’s previous contents. Swapping is constant-time when the comparator swap is constant-time. The repository’s API reference documents every overload, iterator rule, and exception.
A sliding-window example
The outgoing value might have duplicates, so remove one copy. For a lower median, use rank (k - 1) / 2:
vector<int> SlidingMedian(const vector<int>& v, int k) {
if (k < 1 || (size_t)k > v.size()) {
throw invalid_argument("Invalid window size");
}
ordered_multiset<int> s;
vector<int> ans;
for (size_t i = 0; i < v.size(); i++) {
s.insert(v[i]);
if (i >= (size_t)k) {
s.erase_one(v[i - k]);
}
if (i + 1 >= (size_t)k) {
ans.push_back(s[(k - 1) / 2]);
}
}
return ans;
}
For {5, 1, 5, 2, 9, 2} and k = 3, this returns {5, 2, 5, 2}. Time is $$$O(n\log(k+1))$$$. The tree uses $$$O(k)$$$ space, apart from the returned answers.
Details that can cost a submission
erase(x) removes every copy. erase_one(x) removes one. greater<T> changes the meaning of “before.” Rank zero becomes the largest key, and an inclusive descending range is written in that order, such as count_range(9, 3). - Stored keys are immutable through iterators and indexing. Erase and reinsert to change a key.
- Check for
end() before dereferencing a bound or an invalid selection. - Use the unique set when duplicates are unnecessary. The multiset stores one ID per occurrence.
Validation
The included reference suite exercises 384,000 randomized operations, 265,720 bounded exhaustive trace states, and 27,073,587 checks per successful full run. It compares against standard sets/multisets in both sort directions, including custom keys, numeric limits, iterator stability, copy/move/swap, and checked ID exhaustion.
The article examples and recipe algorithms are checked separately. Current compiler results and any execution restrictions are recorded in the repository’s validation report. Finite tests help find bugs; they do not turn an invalid comparator into a valid one. :)
Complete implementation
Copy this above your solution functions. The code has no custom-header dependency when pasted as a whole.
Ordered set + multiset implementation (C++17)/*
MIT License
Copyright (c) 2026 Tareq Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace std;
template<typename T, typename Compare>
class OrderedSet;
template<typename T, typename Compare, typename IdType>
class OrderedMultiSet;
// Shared read-only iterator: multiset users see T, never its hidden insertion ID.
namespace ordered_detail {
template<typename T, typename BaseIterator, bool Multi>
class Iterator {
private:
BaseIterator current;
explicit Iterator(BaseIterator it) : current(it) {
}
template<typename, typename>
friend class ::OrderedSet;
template<typename, typename, typename>
friend class ::OrderedMultiSet;
public:
using iterator_category = bidirectional_iterator_tag;
using value_type = T;
using difference_type = ptrdiff_t;
using pointer = const T*;
using reference = const T&;
Iterator() = default;
reference operator*() const {
if constexpr (Multi) {
return current->first;
}
else {
return *current;
}
}
pointer operator->() const {
return std::addressof(**this);
}
Iterator& operator++() {
++current;
return *this;
}
Iterator operator++(int) {
Iterator old = *this;
++*this;
return old;
}
Iterator& operator--() {
--current;
return *this;
}
Iterator operator--(int) {
Iterator old = *this;
--*this;
return old;
}
friend bool operator==(Iterator a, Iterator b) {
return a.current == b.current;
}
friend bool operator!=(Iterator a, Iterator b) {
return !(a == b);
}
};
}
template<typename T, typename Compare = less<T>>
class OrderedSet {
private:
using Tree = __gnu_pbds::tree<T, __gnu_pbds::null_type, Compare,
__gnu_pbds::rb_tree_tag,
__gnu_pbds::tree_order_statistics_node_update>;
Tree tree;
public:
using value_type = T;
using key_type = T;
using key_compare = Compare;
using size_type = typename Tree::size_type;
using difference_type = ptrdiff_t;
using iterator = ordered_detail::Iterator<T, typename Tree::const_iterator, false>;
using const_iterator = iterator;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = reverse_iterator;
explicit OrderedSet(const Compare& compare = Compare()) : tree(compare) {
}
template<typename Iterator>
OrderedSet(Iterator first, Iterator last, const Compare& compare = Compare()) : tree(compare) {
insert(first, last);
}
OrderedSet(initializer_list<T> values, const Compare& compare = Compare()) : tree(compare) {
insert(values.begin(), values.end());
}
OrderedSet(const OrderedSet&) = default;
OrderedSet& operator=(const OrderedSet&) = default;
// PBDS needs a new empty sentinel, so moving may allocate and is not noexcept.
OrderedSet(OrderedSet&& other) : tree(other.key_comp()) {
swap(other);
}
OrderedSet& operator=(OrderedSet&& other) {
if (this != &other) {
OrderedSet current(std::move(other));
swap(current);
}
return *this;
}
bool empty() const {
return tree.empty();
}
size_type size() const {
return tree.size();
}
Compare key_comp() const {
return tree.get_cmp_fn();
}
iterator begin() const {
return iterator(tree.begin());
}
iterator end() const {
return iterator(tree.end());
}
iterator cbegin() const {
return begin();
}
iterator cend() const {
return end();
}
reverse_iterator rbegin() const {
return reverse_iterator(end());
}
reverse_iterator rend() const {
return reverse_iterator(begin());
}
const_reverse_iterator crbegin() const {
return rbegin();
}
const_reverse_iterator crend() const {
return rend();
}
// Unique keys: return the existing/new iterator and whether insertion happened.
pair<iterator, bool> insert(const T& value) {
auto result = tree.insert(value);
return {iterator(result.first), result.second};
}
void insert(initializer_list<T> values) {
insert(values.begin(), values.end());
}
template<typename... Args>
pair<iterator, bool> emplace(Args&&... args) {
return insert(T(std::forward<Args>(args)...));
}
template<typename Iterator>
void insert(Iterator first, Iterator last) {
for (; first != last; ++first) {
insert(*first);
}
}
iterator find(const T& value) const {
return iterator(tree.find(value));
}
bool contains(const T& value) const {
return find(value) != end();
}
size_type count(const T& value) const {
return contains(value);
}
iterator lower_bound(const T& value) const {
return iterator(tree.lower_bound(value));
}
iterator upper_bound(const T& value) const {
return iterator(tree.upper_bound(value));
}
pair<iterator, iterator> equal_range(const T& value) const {
return {lower_bound(value), upper_bound(value)};
}
// Like multiset::erase(value), remove all equivalent occurrences and count them.
size_type erase(const T& value) {
return tree.erase(value);
}
// Remove one equivalent occurrence; useful when a sliding window has duplicates.
bool erase_one(const T& value) {
return tree.erase(value);
}
iterator erase(iterator it) {
return iterator(tree.erase(it.current));
}
// Half-open iterator range. Each PBDS node erase costs O(log(size() + 1)).
iterator erase(iterator first, iterator last) {
while (first != last) {
first = erase(first);
}
return last;
}
bool erase_by_order(size_type idx) {
iterator it = find_by_order(idx);
if (it == end()) {
return 0;
}
erase(it);
return 1;
}
// Zero-based sorted rank. Invalid ranks return end(), not an element.
iterator find_by_order(size_type idx) const {
return iterator(tree.find_by_order(idx));
}
size_type order_of_key(const T& value) const {
return tree.order_of_key(value);
}
// Count keys before or equivalent to value without computing value + 1.
size_type order_of_key_upper(const T& value) const {
auto node = tree.node_begin();
auto last = tree.node_end();
const Compare& compare = tree.get_cmp_fn();
size_type ans = 0;
while (node != last) {
if (compare(value, **node)) {
node = node.get_l_child();
}
else {
auto left = node.get_l_child();
ans += 1 + (left == last ? 0 : left.get_metadata());
node = node.get_r_child();
}
}
return ans;
}
// Inclusive VALUE range in comparator order; this is not an iterator range.
size_type count_range(const T& first, const T& last) const {
if (tree.get_cmp_fn()(last, first)) {
return 0;
}
return order_of_key_upper(last) - order_of_key(first);
}
// end() maps to size(); a multiset iterator identifies one exact occurrence.
size_type index_of(iterator it) const {
return it == end() ? size() : order_of_key(*it);
}
difference_type first_index(const T& value) const {
iterator it = find(value);
return it == end() ? -1 : (difference_type)order_of_key(value);
}
difference_type last_index(const T& value) const {
return first_index(value);
}
// Checked, read-only rank access. Its logarithmic cost also applies to operator[].
const T& at(size_type idx) const {
if (idx >= size()) {
throw out_of_range("OrderedSet index is out of range");
}
return *find_by_order(idx);
}
const T& operator[](size_type idx) const {
return at(idx);
}
const T& front() const {
if (empty()) {
throw out_of_range("OrderedSet is empty");
}
return *begin();
}
const T& back() const {
if (empty()) {
throw out_of_range("OrderedSet is empty");
}
return *rbegin();
}
void clear() {
tree.clear();
}
void swap(OrderedSet& other) {
tree.swap(other.tree);
}
friend void swap(OrderedSet& a, OrderedSet& b) {
a.swap(b);
}
};
template<typename T, typename Compare = less<T>, typename IdType = uint64_t>
class OrderedMultiSet {
private:
static_assert(is_integral_v<IdType> && is_unsigned_v<IdType> &&
!is_same_v<IdType, bool>, "Insertion IDs must be unsigned integers");
using Id = IdType;
using Entry = pair<T, Id>;
// Keep a strict ordering: compare values first, then insertion IDs for ties.
struct EntryCompare {
Compare compare;
explicit EntryCompare(const Compare& valueCompare) : compare(valueCompare) {
}
bool operator()(const Entry& a, const Entry& b) const {
if (compare(a.first, b.first)) {
return 1;
}
if (compare(b.first, a.first)) {
return 0;
}
return a.second < b.second;
}
};
using Tree = __gnu_pbds::tree<Entry, __gnu_pbds::null_type, EntryCompare,
__gnu_pbds::rb_tree_tag,
__gnu_pbds::tree_order_statistics_node_update>;
using BaseIterator = typename Tree::const_iterator;
Tree tree;
Id nextId = 0;
// One root-to-leaf walk. Upper selects < value or <= value in comparator order.
template<bool Upper>
typename Tree::size_type GetOrder(const T& value) const {
auto node = tree.node_begin();
auto last = tree.node_end();
const Compare& compare = tree.get_cmp_fn().compare;
typename Tree::size_type ans = 0;
while (node != last) {
const T& current = (**node).first;
bool before;
if constexpr (Upper) {
before = !compare(value, current);
}
else {
before = compare(current, value);
}
if (before) {
auto left = node.get_l_child();
ans += 1 + (left == last ? 0 : left.get_metadata());
node = node.get_r_child();
}
else {
node = node.get_l_child();
}
}
return ans;
}
// Search using T directly; no temporary pair, sentinel key, or key copy is needed.
template<bool Upper>
BaseIterator GetBound(const T& value) const {
auto node = tree.node_begin();
auto last = tree.node_end();
const Compare& compare = tree.get_cmp_fn().compare;
BaseIterator ans = tree.end();
while (node != last) {
const T& current = (**node).first;
bool before;
if constexpr (Upper) {
before = !compare(value, current);
}
else {
before = compare(current, value);
}
if (before) {
node = node.get_r_child();
}
else {
ans = *node;
node = node.get_l_child();
}
}
return ans;
}
public:
using value_type = T;
using key_type = T;
using key_compare = Compare;
using size_type = typename Tree::size_type;
using difference_type = ptrdiff_t;
using iterator = ordered_detail::Iterator<T, BaseIterator, true>;
using const_iterator = iterator;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = reverse_iterator;
explicit OrderedMultiSet(const Compare& compare = Compare()) : tree(EntryCompare(compare)) {
}
template<typename Iterator>
OrderedMultiSet(Iterator first, Iterator last, const Compare& compare = Compare()) : tree(EntryCompare(compare)) {
insert(first, last);
}
OrderedMultiSet(initializer_list<T> values, const Compare& compare = Compare()) : tree(EntryCompare(compare)) {
insert(values.begin(), values.end());
}
OrderedMultiSet(const OrderedMultiSet&) = default;
OrderedMultiSet& operator=(const OrderedMultiSet& other) {
if (this != &other) {
OrderedMultiSet current(other);
swap(current);
}
return *this;
}
// Transfer the tree and ID state together; the source becomes empty and reusable.
OrderedMultiSet(OrderedMultiSet&& other) : tree(EntryCompare(other.key_comp())) {
swap(other);
}
OrderedMultiSet& operator=(OrderedMultiSet&& other) {
if (this != &other) {
OrderedMultiSet current(std::move(other));
swap(current);
}
return *this;
}
bool empty() const {
return tree.empty();
}
size_type size() const {
return tree.size();
}
Compare key_comp() const {
return tree.get_cmp_fn().compare;
}
iterator begin() const {
return iterator(tree.begin());
}
iterator end() const {
return iterator(tree.end());
}
iterator cbegin() const {
return begin();
}
iterator cend() const {
return end();
}
reverse_iterator rbegin() const {
return reverse_iterator(end());
}
reverse_iterator rend() const {
return reverse_iterator(begin());
}
const_reverse_iterator crbegin() const {
return rbegin();
}
const_reverse_iterator crend() const {
return rend();
}
// Every occurrence gets a distinct ID; reject exhaustion before any wraparound.
iterator insert(const T& value) {
if (nextId == numeric_limits<Id>::max()) {
throw length_error("OrderedMultiSet insertion IDs are exhausted");
}
auto result = tree.insert({value, nextId});
nextId++;
return iterator(result.first);
}
void insert(initializer_list<T> values) {
insert(values.begin(), values.end());
}
template<typename... Args>
iterator emplace(Args&&... args) {
return insert(T(std::forward<Args>(args)...));
}
template<typename Iterator>
void insert(Iterator first, Iterator last) {
for (; first != last; ++first) {
insert(*first);
}
}
iterator lower_bound(const T& value) const {
return iterator(GetBound<false>(value));
}
iterator upper_bound(const T& value) const {
return iterator(GetBound<true>(value));
}
iterator find(const T& value) const {
iterator it = lower_bound(value);
if (it == end() || tree.get_cmp_fn().compare(value, *it)) {
return end();
}
return it;
}
bool contains(const T& value) const {
return find(value) != end();
}
size_type order_of_key(const T& value) const {
return GetOrder<false>(value);
}
// Count keys before or equivalent to value without computing value + 1.
size_type order_of_key_upper(const T& value) const {
return GetOrder<true>(value);
}
size_type count(const T& value) const {
return order_of_key_upper(value) - order_of_key(value);
}
// Inclusive VALUE range in comparator order; this is not an iterator range.
size_type count_range(const T& first, const T& last) const {
if (tree.get_cmp_fn().compare(last, first)) {
return 0;
}
return order_of_key_upper(last) - order_of_key(first);
}
pair<iterator, iterator> equal_range(const T& value) const {
return {lower_bound(value), upper_bound(value)};
}
iterator erase(iterator it) {
return iterator(tree.erase(it.current));
}
// Half-open iterator range. Each PBDS node erase costs O(log(size() + 1)).
iterator erase(iterator first, iterator last) {
while (first != last) {
first = erase(first);
}
return last;
}
// Remove one equivalent occurrence; useful when a sliding window has duplicates.
bool erase_one(const T& value) {
iterator it = find(value);
if (it == end()) {
return 0;
}
erase(it);
return 1;
}
// Like multiset::erase(value), remove all equivalent occurrences and count them.
size_type erase(const T& value) {
auto range = equal_range(value);
size_type ans = 0;
while (range.first != range.second) {
range.first = erase(range.first);
ans++;
}
return ans;
}
bool erase_by_order(size_type idx) {
iterator it = find_by_order(idx);
if (it == end()) {
return 0;
}
erase(it);
return 1;
}
// Zero-based sorted rank. Invalid ranks return end(), not an element.
iterator find_by_order(size_type idx) const {
return iterator(tree.find_by_order(idx));
}
// end() maps to size(); a multiset iterator identifies one exact occurrence.
size_type index_of(iterator it) const {
return it == end() ? size() : tree.order_of_key(*it.current);
}
difference_type first_index(const T& value) const {
iterator it = find(value);
return it == end() ? -1 : (difference_type)order_of_key(value);
}
difference_type last_index(const T& value) const {
iterator it = find(value);
return it == end() ? -1 : (difference_type)order_of_key_upper(value) - 1;
}
// Checked, read-only rank access. Its logarithmic cost also applies to operator[].
const T& at(size_type idx) const {
if (idx >= size()) {
throw out_of_range("OrderedMultiSet index is out of range");
}
return *find_by_order(idx);
}
const T& operator[](size_type idx) const {
return at(idx);
}
const T& front() const {
if (empty()) {
throw out_of_range("OrderedMultiSet is empty");
}
return *begin();
}
const T& back() const {
if (empty()) {
throw out_of_range("OrderedMultiSet is empty");
}
return *rbegin();
}
void clear() {
tree.clear();
nextId = 0;
}
void swap(OrderedMultiSet& other) {
tree.swap(other.tree);
std::swap(nextId, other.nextId);
}
friend void swap(OrderedMultiSet& a, OrderedMultiSet& b) {
a.swap(b);
}
};
template<typename T, typename Compare = less<T>>
using ordered_set = OrderedSet<T, Compare>;
template<typename T, typename Compare = less<T>, typename IdType = uint64_t>
using ordered_multiset = OrderedMultiSet<T, Compare, IdType>;
template<typename T, typename Compare = less<T>, typename IdType = uint64_t>
using multi_ordered_set = OrderedMultiSet<T, Compare, IdType>;
Visual walkthrough
For a step-by-step explanation, the companion website lets you change values and follow rank/select queries through a tree.
Try the interactive tree on the website
Read the detailed visual walkthrough on Medium
Thanks for reading. If you find a counterexample, please share the smallest reproducing case so it can become a regression test. <3
By Tareq Ali. This article was organized with AI assistance.
Auto comment: topic has been updated by Tareq_Ali (previous revision, new revision, compare).