diff --git a/include/iris/interval.hpp b/include/iris/interval.hpp new file mode 100644 index 0000000..136bd33 --- /dev/null +++ b/include/iris/interval.hpp @@ -0,0 +1,308 @@ +#ifndef IRIS_ZZ_INTERVAL_HPP +#define IRIS_ZZ_INTERVAL_HPP + +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace iris { + +template +struct interval +{ + using value_type = T; + T lower, upper; + + constexpr interval() noexcept + : lower{} + , upper{} + {} + + constexpr interval(T lower, T upper) noexcept + : lower(lower) + , upper(upper) + {} + + // Returns `true` when `*this` is ∅ or malformed (flipped). + [[nodiscard]] constexpr bool empty() const noexcept + { + return lower >= upper; + } + + // Returns the length of the bounds. + // Note: Always returns `0` for any ∅ positioned at any offset, + // including the ones with malformed bounds. + [[nodiscard]] constexpr value_type length() const noexcept + { + return empty() ? value_type{0} : static_cast(upper - lower); + } + + // Returns `true` if bounds are not flipped. + // Note: Always returns `true` if `*this` is ∅ or malformed. + [[nodiscard]] constexpr bool is_proper() const noexcept + { + return lower <= upper; + } + + // Returns `true` if `other` shares any point with `*this`. + // Note 1: intersects(∅) always returns `false`. + // Note 2: Adjacent-only contact is touches(); for intersects-or-touches use connected(). + template + [[nodiscard]] constexpr bool intersects(interval const other) const noexcept + { + return (lower < other.upper && other.lower < upper) && !empty() && !other.empty(); + } + + // !intersects + // Note: disjoint(∅) always returns `true`. + template + [[nodiscard]] constexpr bool disjoint(interval const other) const noexcept + { + return (upper <= other.lower || other.upper <= lower) || empty() || other.empty(); + } + + // Closures meet but the sets share no point. + // Note: touches(∅) always returns `false`. + template + [[nodiscard]] constexpr bool touches(interval const other) const noexcept + { + return (upper == other.lower || other.upper == lower) && !empty() && !other.empty(); + } + + // intersects || touches + // Note: connected(∅) always returns `false`. + template + [[nodiscard]] constexpr bool connected(interval const other) const noexcept + { + return (lower <= other.upper && other.lower <= upper) && !empty() && !other.empty(); + } + + // Returns `true` if every point of `other` is a point of `*this`. + // Note: covers(∅) always returns `true`. + // See also: `encloses(other)`. + template + [[nodiscard]] constexpr bool covers(interval const other) const noexcept + { + return (lower <= other.lower && other.upper <= upper) || other.empty(); + } + + // Returns `true` if `other`'s bounds lie within [lower, upper]. + // For nonempty `other`: identical to `covers(other)`. + // For empty `other`: position-respecting (treats it like a 0-length "text caret".) + template + [[nodiscard]] constexpr bool encloses(interval const other) const noexcept + { + return lower <= other.lower && other.upper <= upper; + } + + // Returns `true` if p ∈ [lower, upper). + [[nodiscard]] constexpr bool contains(value_type p) const noexcept + { + return lower <= p && p < upper; + } + + // Returns `is_proper() && interval{0, r.size()}.encloses(*this)`. + template + [[nodiscard]] constexpr bool within(R const& r) const + noexcept(noexcept(std::ranges::size(r))) + { + return is_proper() && 0 <= lower && static_cast>(upper) <= std::ranges::size(r); + } + + // Returns `is_proper() && interval{0, N - 1}.encloses(*this)`. + template + [[nodiscard]] constexpr bool within(CharT const (&)[N]) const noexcept + { + static_assert(N >= 1); + return is_proper() && 0 <= lower && static_cast(upper) <= N - 1; + } + + // Returns `true` if both intervals have exactly same bounds. + // Note: All empty intervals denote ∅ and are mutually equal regardless of + // bounds. Differs from `operator==`, which compares data representations. + template + [[nodiscard]] constexpr bool equals(interval const other) const noexcept + { + return (lower == other.lower && upper == other.upper) || (empty() && other.empty()); + } + + // ------------------------------------------- + + // A ∩ B. Result is canonical empty [0,0) when the sets share no point. + template + [[nodiscard]] constexpr interval intersection(interval const other) const noexcept + { + auto const lo = std::max(lower, static_cast(other.lower)); + auto const hi = std::min(upper, static_cast(other.upper)); + return lo < hi ? interval{lo, hi} : interval{}; + } + template + [[nodiscard]] constexpr interval operator&(interval const other) const noexcept + { + return intersection(other); + } + + // ------------------------------------------- + + template + requires (!std::ranges::borrowed_range) + constexpr void as_subview_of(R const&&) const = delete; + + template + [[nodiscard]] constexpr auto as_subview_of(R const& r) const + { + if (!is_proper() || lower < 0) { + throw std::domain_error(std::format("interval [{},{}) cannot form a subview; requires 0 <= lower <= upper", lower, upper)); + } + if constexpr (std::ranges::sized_range) { + auto const size = std::ranges::size(r); + if (static_cast>(upper) > size) { + throw std::out_of_range(std::format("interval [{},{}) cannot form a subview; requires 0 <= lower <= upper <= {}", lower, upper, size)); + } + } + + if constexpr (requires { r.subview(lower, length()); }) { + return r.subview(lower, length()); + + } else if constexpr (StringLike) { + using SV = std::basic_string_view>; + return SV{r}.substr( + static_cast(lower), + static_cast(length()) + ); + + } else { + auto const n = static_cast>(length()); + auto const first = std::ranges::next(std::ranges::begin(r), lower, std::ranges::end(r)); + return std::ranges::subrange(first, std::ranges::next(first, n, std::ranges::end(r))); + } + } + + template + [[nodiscard]] constexpr auto as_subview_of(CharT const (&r)[N]) const + { + static_assert(N >= 1); + return this->as_subview_of(std::basic_string_view{r, N - 1}); + } + + // ------------------------------------------- + + // Note: This does not reflect mathematical definition like `equals(...)`; this always checks exact data representation + [[nodiscard]] constexpr bool operator==(interval const&) const noexcept = default; + + // Note: This does not reflect mathematical definition like `equals(...)`; this always checks exact data representation + [[nodiscard]] constexpr std::strong_ordering operator<=>(interval const&) const noexcept = default; +}; + +template +[[nodiscard]] constexpr T& get(interval& iv) noexcept +{ + static_assert(I == 0 || I == 1); + if constexpr (I == 0) { return iv.lower; } else { return iv.upper; } +} +template +[[nodiscard]] constexpr T const& get(interval const& iv) noexcept +{ + static_assert(I == 0 || I == 1); + if constexpr (I == 0) { return iv.lower; } else { return iv.upper; } +} +template +[[nodiscard]] constexpr T&& get(interval&& iv) noexcept +{ + static_assert(I == 0 || I == 1); + if constexpr (I == 0) { return std::move(iv).lower; } else { return std::move(iv).upper; } +} +template +[[nodiscard]] constexpr T const&& get(interval const&& iv) noexcept +{ + static_assert(I == 0 || I == 1); + if constexpr (I == 0) { return std::move(iv).lower; } else { return std::move(iv).upper; } +} + +} // iris + +template +struct std::tuple_size> + : std::integral_constant +{}; + +template +struct std::tuple_element> +{ + using type = T; +}; + +template +struct std::formatter, CharT> +{ + [[nodiscard]] constexpr std::basic_format_parse_context::const_iterator + parse(std::basic_format_parse_context& ctx) + { + auto const first = ctx.begin(); + if (first == ctx.end()) return first; + if (*first == iris::format_traits::brace_close) return first; + + // Bound the search to this replacement field + auto const close_it = std::find( + first, ctx.end(), + iris::format_traits::brace_close + ); + if (close_it == ctx.end()) { + throw std::format_error("unterminated format specifier"); + } + + auto const comma_it = std::find( + first, close_it, + iris::format_traits::comma + ); + if (comma_it == close_it) { + throw std::format_error("expected ',' in format specifier"); + } + + { + std::basic_format_parse_context left_ctx{ + std::basic_string_view{first, comma_it} + }; + if (left_fmt_.parse(left_ctx) != left_ctx.end()) { + throw std::format_error("trailing characters in lower format specifier"); + } + } + { + std::basic_format_parse_context right_ctx{ + std::basic_string_view{std::next(comma_it), close_it} + }; + if (right_fmt_.parse(right_ctx) != right_ctx.end()) { + throw std::format_error("trailing characters in upper format specifier"); + } + } + return close_it; + } + + template + Ctx::iterator format(iris::interval const& iv, Ctx& ctx) const + { + ctx.advance_to(std::format_to(ctx.out(), "{}", iris::format_traits::square_brace_open)); + left_fmt_.format(iv.lower, ctx); + ctx.advance_to(std::format_to(ctx.out(), "{}", iris::format_traits::comma)); + right_fmt_.format(iv.upper, ctx); + return std::format_to(ctx.out(), "{}", iris::format_traits::paren_close); + } + +private: + std::formatter left_fmt_, right_fmt_; +}; + +#endif diff --git a/include/iris/interval_algo.hpp b/include/iris/interval_algo.hpp new file mode 100644 index 0000000..863e329 --- /dev/null +++ b/include/iris/interval_algo.hpp @@ -0,0 +1,170 @@ +#ifndef IRIS_ZZ_INTERVAL_ALGO_HPP +#define IRIS_ZZ_INTERVAL_ALGO_HPP + +// SPDX-License-Identifier: MIT + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace iris { + +namespace detail { + +template +struct interval_word_candidate +{ + IntervalT span; + WordID word_id; + + [[nodiscard]] constexpr bool operator==(interval_word_candidate const&) const noexcept = default; +}; + +// Typically: `std::vector` +template +concept IntervalCandidateList = + std::ranges::forward_range && + std::convertible_to, IntervalT>; + +// Typically: `std::vector>` +template +concept WordCandidateList = + std::ranges::forward_range && + IntervalCandidateList, IntervalT>; + +} // detail + + +// Selects one span per word so that the extent of the selection (the hull +// from the smallest `lower` to the largest `upper`) is minimal, i.e. the chosen +// spans lie as close together as possible. +// +// Input (`words`) is typically a range of ranges that has the value type of `IntervalT`, +// for example `std::vector>>`. +// +// The result is sorted by `lower`. Overlapping selections are merged into one +// interval; touching ones stay separate. +// +// Some corner cases: +// - Words without spans are skipped. +// - Ties prefer the leftmost selection. +template< + class WordsR, + class IntervalT = std::ranges::range_value_t>, + class IntervalListT = std::vector, + class WordID = int +> + requires detail::WordCandidateList +[[nodiscard]] +IntervalListT select_min_extent(WordsR&& words) +{ + static_assert(std::same_as, IntervalT>); + static_assert(std::convertible_to); + + using Cand = detail::interval_word_candidate; + using interval_value_type = IntervalT::value_type; + + std::vector cands; + int active = 0; + { + WordID word_id = WordID{0}; + for (auto word_it = std::ranges::begin(words); word_it != std::ranges::end(words); ++word_it, ++word_id) { + if (std::ranges::empty(*word_it)) continue; // word matched nowhere: skip + + ++active; + for (IntervalT const& span : *word_it) { + cands.emplace_back(span, word_id); + } + } + } + if (active == 0) return {}; + + // Descending by lower: the sweep adds spans as the threshold t moves left. + std::ranges::sort(cands, [](Cand const& a, Cand const& b) { + return a.span.lower > b.span.lower; + }); + + std::map suffmin; // word -> min upper with lower >= t + std::multiset uppers; // current suffmin values + + using score_type = std::common_type_t; + score_type best_score = std::numeric_limits::max(); + interval_value_type best_t = 0; + + for (std::size_t i = 0; i < cands.size(); ) { + interval_value_type const t = cands[i].span.lower; + for (; i < cands.size() && cands[i].span.lower == t; ++i) { + auto const [it, fresh] = suffmin.try_emplace(cands[i].word_id, cands[i].span.upper); + if (fresh) { + uppers.insert(cands[i].span.upper); + + } else if (cands[i].span.upper < it->second) { + uppers.erase(uppers.find(it->second)); + it->second = cands[i].span.upper; + uppers.insert(cands[i].span.upper); + } + } + if (int(suffmin.size()) == active) { + score_type const score = static_cast(*uppers.rbegin()) - t; + if (score <= best_score) { + best_score = score; + best_t = t; // leftmost tie-break + } + } + } + + // Reconstruct: per word, the span with lower >= best_t minimizing (upper, lower). + std::map chosen; + for (Cand const& cand : cands) { + if (cand.span.lower < best_t) continue; + + auto const [it, fresh] = chosen.try_emplace(cand.word_id, cand.span); + if ( + !fresh && + ( + cand.span.upper < it->second.upper || + (cand.span.upper == it->second.upper && cand.span.lower < it->second.lower) + ) + ) { + it->second = cand.span; + } + } + + std::vector out; + for (auto const& [word_id, span] : chosen) { + out.emplace_back(span); + } + std::ranges::sort(out, [](IntervalT const& a, IntervalT const& b) { + return a.lower != b.lower ? a.lower < b.lower : a.upper < b.upper; + }); + + // Different words' winners may overlap (substring terms): merge strict overlaps, + // dedupe identical spans; touching winners stay separate matches. + IntervalListT merged; + for (auto const& span : out) { + if ( + !merged.empty() && + (span.lower < merged.back().upper || span == merged.back()) + ) { + merged.back().upper = std::max(merged.back().upper, span.upper); + + } else { + merged.emplace_back(span); + } + } + return merged; +} + +} // iris + +#endif diff --git a/include/iris/interval_set.hpp b/include/iris/interval_set.hpp new file mode 100644 index 0000000..f2998ed --- /dev/null +++ b/include/iris/interval_set.hpp @@ -0,0 +1,259 @@ +#ifndef IRIS_ZZ_INTERVAL_SET_HPP +#define IRIS_ZZ_INTERVAL_SET_HPP + +// SPDX-License-Identifier: MIT + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace iris { + +template< + class IntervalT, + class MapT = std::map +> +class interval_set +{ +public: + using interval_type = IntervalT; + using map_type = MapT; + using offset_type = IntervalT::value_type; + + class const_iterator : public iterator_base + { + using typename iterator_base::iterator_base_type; + static_assert(std::bidirectional_iterator); + iterator_base_type it_; + + public: + using value_type = IntervalT; + using pointer = IntervalT const*; + using reference = IntervalT; + + constexpr const_iterator() noexcept = default; + + constexpr explicit const_iterator(iterator_base_type it) noexcept + : it_(std::move(it)) + {} + + [[nodiscard]] constexpr IntervalT operator*() const noexcept + { + return {it_->first, it_->second}; + } + + constexpr const_iterator& operator++() noexcept + { + ++it_; + return *this; + } + + [[nodiscard]] constexpr const_iterator operator++(int) noexcept + { + auto temp{*this}; + ++it_; + return temp; + } + + constexpr const_iterator& operator--() noexcept + { + --it_; + return *this; + } + + [[nodiscard]] constexpr const_iterator operator--(int) noexcept + { + auto temp{*this}; + --it_; + return temp; + } + + [[nodiscard]] constexpr bool operator==(const_iterator const&) const noexcept = default; + [[nodiscard]] constexpr auto operator<=>(const_iterator const&) const noexcept = default; + }; + + using iterator = const_iterator; + + constexpr interval_set() = default; + + constexpr explicit interval_set(std::initializer_list il) + { + auto it = il.begin(); + if (it == il.end()) return; + if (!it->empty()) { + map_.emplace(it->lower, it->upper); + } + for (++it; it != il.end(); ++it) { + this->insert(*it); + } + } + + template Se> + requires std::convertible_to, IntervalT> + constexpr interval_set(It it, Se se) + { + if (it == se) return; + if (IntervalT const iv = *it; !iv.empty()) { + map_.emplace(iv.lower, iv.upper); + } + for (++it; it != se; ++it) { + this->insert(*it); + } + } + + template + requires std::convertible_to, IntervalT> + constexpr interval_set(std::from_range_t, R&& r) + : interval_set(std::ranges::begin(r), std::ranges::end(r)) + {} + + [[nodiscard]] constexpr const_iterator begin() const noexcept + { + return const_iterator{map_.begin()}; + } + + [[nodiscard]] constexpr const_iterator end() const noexcept + { + return const_iterator{map_.end()}; + } + + [[nodiscard]] constexpr bool empty() const noexcept { return map_.empty(); } + [[nodiscard]] constexpr std::size_t size() const noexcept { return map_.size(); } + + constexpr void clear() noexcept + { + map_.clear(); + } + + // Total number of covered offsets (sum of lengths) + constexpr offset_type coverage() const noexcept + { + offset_type total = 0; + for (auto const& [lower, upper] : map_) { + total += upper - lower; + } + return total; + } + + // O(1) + [[nodiscard]] constexpr IntervalT extent() const noexcept + { + if (map_.empty()) return {}; + return {map_.begin()->first, std::prev(map_.end())->second}; + } + + // Insert [iv.lower, iv.upper), merging with any interval it overlaps or touches + constexpr void insert(IntervalT iv) + { + if (iv.empty()) return; + + auto it = map_.lower_bound(iv.lower); + if (it != map_.begin()) { + auto prev = std::prev(it); + if (prev->second >= iv.lower) { + it = prev; + } + } + + // Absorb every interval that overlaps or touches [iv.lower, iv.upper) + while (it != map_.end() && it->first <= iv.upper) { + if (it->first < iv.lower) iv.lower = it->first; + if (it->second > iv.upper) iv.upper = it->second; + it = map_.erase(it); + } + + map_.emplace(iv.lower, iv.upper); + } + + constexpr void insert(offset_type lower, offset_type upper) + { + this->insert(IntervalT{lower, upper}); + } + + // -------------------------------------- + + [[nodiscard]] constexpr bool intersects(IntervalT const iv) const + { + if (iv.empty()) return false; + auto const it = map_.upper_bound(iv.lower); + if (it != map_.begin() && std::prev(it)->second > iv.lower) return true; + return it != map_.end() && it->first < iv.upper; + } + + [[nodiscard]] constexpr bool covers(IntervalT const iv) const + { + if (iv.empty()) return true; + auto const it = map_.upper_bound(iv.lower); + if (it == map_.begin()) return false; + auto const& [lower, upper] = *std::prev(it); + return lower <= iv.lower && iv.upper <= upper; + } + + [[nodiscard]] constexpr bool contains(offset_type p) const + { + auto const it = map_.upper_bound(p); + if (it == map_.begin()) return false; + return std::prev(it)->second > p; + } + + [[nodiscard]] constexpr bool operator==(interval_set const&) const noexcept = default; + [[nodiscard]] constexpr std::strong_ordering operator<=>(interval_set const&) const noexcept = default; + + constexpr void swap(interval_set& other) noexcept + { + using std::swap; + swap(map_, other.map_); + } + +private: + MapT map_; +}; + +template +constexpr void swap(interval_set& a, interval_set& b) noexcept +{ + a.swap(b); +} + +} // iris + +template +struct std::formatter, CharT> +{ + [[nodiscard]] constexpr std::basic_format_parse_context::const_iterator + parse(std::basic_format_parse_context& ctx) + { + return iv_fmt_.parse(ctx); + } + + template + Ctx::iterator format(iris::interval_set const& ivs, Ctx& ctx) const + { + ctx.advance_to(std::format_to(ctx.out(), "{{")); + bool is_first = true; + for (auto const& iv : ivs) { + if (is_first) { + ctx.advance_to(iv_fmt_.format(iv, ctx)); + is_first = false; + } else { + ctx.advance_to(std::format_to(ctx.out(), " ")); + ctx.advance_to(iv_fmt_.format(iv, ctx)); + } + } + return std::format_to(ctx.out(), "}}"); + } + +private: + std::formatter iv_fmt_; +}; + +#endif diff --git a/include/iris/iterator.hpp b/include/iris/iterator.hpp new file mode 100644 index 0000000..472e538 --- /dev/null +++ b/include/iris/iterator.hpp @@ -0,0 +1,70 @@ +#ifndef IRIS_ZZ_ITERATOR_HPP +#define IRIS_ZZ_ITERATOR_HPP + +// SPDX-License-Identifier: MIT + +#include + +#include +#include + +namespace iris { + +template +struct iterator_tags_base; + +template + requires requires { + typename std::iterator_traits::iterator_category; + typename std::iterator_traits::iterator_concept; + } +struct iterator_tags_base +{ + using iterator_base_type = It; + using iterator_category = std::iterator_traits::iterator_category; + using iterator_concept = std::iterator_traits::iterator_concept; + + [[nodiscard]] constexpr bool operator==(iterator_tags_base const&) const noexcept = default; + [[nodiscard]] constexpr std::strong_ordering operator<=>(iterator_tags_base const&) const noexcept = default; +}; + +template + requires + requires { typename std::iterator_traits::iterator_category; } && + (!requires { typename std::iterator_traits::iterator_concept; }) +struct iterator_tags_base +{ + using iterator_base_type = It; + using iterator_category = std::iterator_traits::iterator_category; + + [[nodiscard]] constexpr bool operator==(iterator_tags_base const&) const noexcept = default; + [[nodiscard]] constexpr std::strong_ordering operator<=>(iterator_tags_base const&) const noexcept = default; +}; + +template + requires + (!requires { typename std::iterator_traits::iterator_category; }) && + requires { typename std::iterator_traits::iterator_concept; } +struct iterator_tags_base +{ + using iterator_base_type = It; + using iterator_concept = std::iterator_traits::iterator_concept; + + [[nodiscard]] constexpr bool operator==(iterator_tags_base const&) const noexcept = default; + [[nodiscard]] constexpr std::strong_ordering operator<=>(iterator_tags_base const&) const noexcept = default; +}; + +// ---------------------------------------------- + +template +struct iterator_base : iterator_tags_base +{ + using difference_type = std::iterator_traits::difference_type; + + [[nodiscard]] constexpr bool operator==(iterator_base const&) const noexcept = default; + [[nodiscard]] constexpr std::strong_ordering operator<=>(iterator_base const&) const noexcept = default; +}; + +} // iris + +#endif diff --git a/include/iris/snippet.hpp b/include/iris/snippet.hpp new file mode 100644 index 0000000..5cf90b4 --- /dev/null +++ b/include/iris/snippet.hpp @@ -0,0 +1,419 @@ +#ifndef IRIS_ZZ_SNIPPET_HPP +#define IRIS_ZZ_SNIPPET_HPP + +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace iris::snip { + +namespace detail { + +struct gap_traits +{ + template + requires requires(SinkT const& csink) { + { csink.gap_first_chars() } -> std::convertible_to; + } + [[nodiscard]] static constexpr std::size_t gap_first_chars(SinkT const& csink) noexcept + { + return csink.gap_first_chars(); + } + + template + requires (!requires(SinkT const& csink) { + { csink.gap_first_chars() } -> std::convertible_to; + }) + [[nodiscard]] static constexpr std::size_t gap_first_chars(SinkT const& csink) noexcept + { + return csink.gap_chars(); + } + + template + requires requires(SinkT& sink) { + sink.gap_first(); + } + static constexpr void gap_first(SinkT& sink) + { + return sink.gap_first(); + } + + template + requires (!requires(SinkT& sink) { + sink.gap_first(); + }) + static constexpr void gap_first(SinkT& sink) + { + return sink.gap(); + } + + // ---------------------------------------- + + template + requires requires(SinkT const& csink) { + { csink.gap_last_chars() } -> std::convertible_to; + } + [[nodiscard]] static constexpr std::size_t gap_last_chars(SinkT const& csink) noexcept + { + return csink.gap_last_chars(); + } + + template + requires (!requires(SinkT const& csink) { + { csink.gap_last_chars() } -> std::convertible_to; + }) + [[nodiscard]] static constexpr std::size_t gap_last_chars(SinkT const& csink) noexcept + { + return csink.gap_chars(); + } + + template + requires requires(SinkT& sink) { + sink.gap_last(); + } + static constexpr void gap_last(SinkT& sink) + { + return sink.gap_last(); + } + + template + requires (!requires(SinkT& sink) { + sink.gap_last(); + }) + static constexpr void gap_last(SinkT& sink) + { + return sink.gap(); + } +}; + +} // detail + +template +concept SnippetSink = requires(SinkT& sink, SinkT const& csink, std::basic_string_view sv) { + sink.context(sv); + sink.match(sv); + + sink.gap(); + { csink.gap_chars() } -> std::convertible_to; + + { detail::gap_traits::gap_first_chars(csink) } -> std::convertible_to; + detail::gap_traits::gap_first(sink); + + { detail::gap_traits::gap_last_chars(csink) } -> std::convertible_to; + detail::gap_traits::gap_last(sink); + + { sink.clear() } noexcept; +}; + +template +struct recording_sink +{ + using char_type = CharT; + + std::vector>> + events; + + // The adjacent text connected to the left or the right of `match` + void context(std::basic_string_view sv) + { + events.emplace_back(static_cast('C'), sv); + } + + void match(std::basic_string_view sv) + { + events.emplace_back(static_cast('M'), sv); + } + + void gap() + { + events.emplace_back(std::piecewise_construct, std::forward_as_tuple(static_cast('G')), std::forward_as_tuple()); + } + + [[nodiscard]] std::size_t gap_chars() const noexcept + { + return 0; + } + + void clear() noexcept + { + events.clear(); + } + + [[nodiscard]] std::string to_string() const + { + std::string str; + bool is_first = true; + for (auto const& [ch, substr] : events) { + if (is_first) { + is_first = false; + } else { + str += ' '; + } + if (ch == static_cast('G')) { + str += 'G'; + } else { + std::format_to(std::back_inserter(str), "{}\"{}\"", iris::to_string_ref(ch), iris::to_string_ref(substr)); + } + } + return str; + } +}; + +template +struct bbcode_search_result_tokens; + +template<> +struct bbcode_search_result_tokens +{ + static constexpr std::string_view GAP = " ... "; + static constexpr std::string_view GAP_FIRST = "... "; + static constexpr std::string_view GAP_LAST = " ..."; + static constexpr std::string_view MATCH_START = "[b]"; + static constexpr std::string_view MATCH_END = "[/b]"; +}; + +template<> +struct bbcode_search_result_tokens +{ + static constexpr std::u32string_view GAP = U" ... "; + static constexpr std::u32string_view GAP_FIRST = U"... "; + static constexpr std::u32string_view GAP_LAST = U" ..."; + static constexpr std::u32string_view MATCH_START = U"[b]"; + static constexpr std::u32string_view MATCH_END = U"[/b]"; +}; + +template> +struct search_result_sink +{ + using char_type = CharT; + + std::basic_string result_str; + + void context(std::basic_string_view sv) + { + result_str += sv; + } + + void match(std::basic_string_view sv) + { + result_str += TokensT::MATCH_START; + result_str += sv; + result_str += TokensT::MATCH_END; + } + + void gap() { result_str += TokensT::GAP; } + [[nodiscard]] std::size_t gap_chars() const noexcept { return TokensT::GAP.size(); } + + void gap_first() { result_str += TokensT::GAP_FIRST; } + [[nodiscard]] std::size_t gap_first_chars() const noexcept { return TokensT::GAP_FIRST.size(); } + + void gap_last() { result_str += TokensT::GAP_LAST; } + [[nodiscard]] std::size_t gap_last_chars() const noexcept { return TokensT::GAP_LAST.size(); } + + void clear() noexcept + { + result_str.clear(); + } + + [[nodiscard]] decltype(auto) to_string() const + { + return iris::to_string_ref(result_str); + } +}; + +template +class snippet_generator +{ +public: + template + requires std::convertible_to, interval> + void process( + std::basic_string_view const input_text, + MatchesR&& matches_r, + interval_set> const& frags, + SnippetSink auto& sink + ) + { + input_text_ = input_text; + matches_.clear(); + frags_.clear(); + sink.clear(); + + matches_.assign_range(std::forward(matches_r)); + frags_ = frags; + this->process_impl(sink); + } + + template + requires std::convertible_to, interval> + void process( + std::basic_string const&& input_text, + MatchesR&& matches_r, + interval_set> const& frags, + SnippetSink auto& sink + ) = delete; + + template + requires std::convertible_to, interval> + void process( + std::basic_string_view const input_text, + MatchesR&& matches_r, + int const result_max_chars, + SnippetSink auto& sink + ) + { + input_text_ = input_text; + matches_.clear(); + frags_.clear(); + sink.clear(); + + matches_.assign_range(std::forward(matches_r)); + this->generate_frags( + result_max_chars, + static_cast(detail::gap_traits::gap_first_chars(sink)), + static_cast(sink.gap_chars()), + static_cast(detail::gap_traits::gap_last_chars(sink)) + ); + this->process_impl(sink); + } + + template + requires std::convertible_to, interval> + void process( + std::basic_string const&& input_text, + MatchesR&& matches_r, + int const result_max_chars, + SnippetSink auto& sink + ) = delete; + +private: + void process_impl(SnippetSink auto& sink) + { + if (!frags_.extent().within(input_text_)) { + throw std::out_of_range{"frags is outside input text"}; + } + + auto match_it = matches_.begin(); + int last_upper = 0; + for (auto const& frag : frags_) { + if (frag.lower != last_upper) { + if (last_upper == 0) { + detail::gap_traits::gap_first(sink); + } else { + sink.gap(); + } + } + + int pos = frag.lower; + for (; match_it != matches_.end() && frag.encloses(*match_it) && pos <= match_it->lower; ++match_it) { + if (pos != match_it->lower) { + sink.context(interval{pos, match_it->lower}.as_subview_of(input_text_)); + } + sink.match(match_it->as_subview_of(input_text_)); + pos = match_it->upper; + } + if (pos != frag.upper) { + sink.context(interval{pos, frag.upper}.as_subview_of(input_text_)); + } + last_upper = frag.upper; + } + if (match_it != matches_.end()) { + sink.clear(); + throwf("match {} not covered by any fragment", *match_it); + } + + if (!frags_.empty() && last_upper != static_cast(input_text_.size())) { + detail::gap_traits::gap_last(sink); + } + } + + void generate_frags(int const result_max_chars, int const gap_first_chars, int const gap_chars, int const gap_last_chars) + { + assert(frags_.empty()); + if (input_text_.empty()) { + matches_.clear(); + return; + } + + int sum = 0; + for (auto const& match : matches_) { + sum += match.length(); + } + + int pad = 0; + while (!matches_.empty()) { + int const n = static_cast(matches_.size()); + // worst case: leading + trailing + (n - 1) inner gaps + int const reserve = gap_first_chars + gap_last_chars + (n - 1) * gap_chars; + int const budget = result_max_chars - reserve - sum; + + if (budget < 0) { + if (n == 1) { + this->truncate_sole_match(result_max_chars, gap_first_chars, gap_last_chars); + return; + } + sum -= matches_.back().length(); + matches_.pop_back(); + continue; + } + pad = budget / (2 * n); + if (pad == 0 && std::ranges::any_of(matches_, &interval::empty)) { + std::erase_if(matches_, [](auto const m) noexcept { return m.empty(); }); + continue; + } + break; + } + if (matches_.empty()) return; + + interval const bounds{0, static_cast(input_text_.size())}; + for (auto const& match : matches_) { + frags_.insert(interval{match.lower - pad, match.upper + pad}.intersection(bounds)); + } + } + + void truncate_sole_match( + int const result_max_chars, + int const gap_first_chars, int const gap_last_chars + ) { + assert(matches_.size() == 1); + auto& match = matches_.front(); + + // leading gap iff text precedes the match; trailing assumed + int const allowed = result_max_chars - (match.lower > 0 ? gap_first_chars : 0) - gap_last_chars; + if (allowed <= 0 || match.empty()) { + matches_.clear(); + return; + } + match = interval{match.lower, match.lower + std::min(match.length(), allowed)}; + frags_.insert(match); + } + + std::basic_string_view input_text_; + std::vector> matches_; + interval_set> frags_; +}; + +} // iris::snip + +#endif diff --git a/include/iris/unicode/string.hpp b/include/iris/unicode/string.hpp index 5ed3033..6618f3e 100644 --- a/include/iris/unicode/string.hpp +++ b/include/iris/unicode/string.hpp @@ -1460,24 +1460,52 @@ template template constexpr std::string_view to_string_ref(std::string const&&) = delete; +template +[[nodiscard]] constexpr std::string_view to_string_ref(char const& ch) +{ + return std::string_view{&ch, 1}; +} +template +constexpr std::string_view to_string_ref(char const&&) = delete; + +// ------------------------------------------------------- + template [[nodiscard]] constexpr std::string to_string_ref(std::u8string_view str) { return unicode::transcode(str); } +template +[[nodiscard]] constexpr std::string to_string_ref(char8_t ch) +{ + return unicode::transcode(ch); +} + +// ------------------------------------------------------- + //template //[[nodiscard]] constexpr std::string to_string_ref(std::u16string_view str) //{ // return unicode::transcode(str); //} +// ------------------------------------------------------- + template [[nodiscard]] constexpr std::string to_string_ref(std::u32string_view str) { return unicode::transcode(str); } +template +[[nodiscard]] constexpr std::string to_string_ref(char32_t ch) +{ + return unicode::transcode(ch); +} + +// ------------------------------------------------------- + template [[nodiscard]] constexpr std::u32string to_u32string_ref(std::string_view str) { @@ -1502,7 +1530,7 @@ template return str; } template -constexpr std::u32string_view to_string_ref(std::u32string const&&) = delete; +constexpr std::u32string_view to_u32string_ref(std::u32string const&&) = delete; } // iris diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 66a345c..846ac6c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -184,6 +184,10 @@ if(PROJECT_IS_TOP_LEVEL) colorize_format preprocess string_algo + interval + interval_algo + interval_set + snippet ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) diff --git a/test/interval.cpp b/test/interval.cpp new file mode 100644 index 0000000..1a82b56 --- /dev/null +++ b/test/interval.cpp @@ -0,0 +1,608 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; + +using iris::interval; + +// NOLINTBEGIN(readability-container-size-empty) + +TEST_CASE("interval: type traits") +{ + STATIC_CHECK(std::is_trivially_copyable_v>); + + STATIC_CHECK(std::is_nothrow_default_constructible_v>); + STATIC_CHECK(std::is_nothrow_constructible_v, int, int>); + STATIC_CHECK(std::is_nothrow_copy_constructible_v>); + STATIC_CHECK(std::is_nothrow_move_constructible_v>); + STATIC_CHECK(std::is_nothrow_copy_assignable_v>); + STATIC_CHECK(std::is_nothrow_move_assignable_v>); + STATIC_CHECK(std::is_nothrow_destructible_v>); + STATIC_CHECK(std::is_nothrow_swappable_v>); +} + +TEST_CASE("interval: tuple") +{ + { + interval const iv{1, 2}; + auto const [lower, upper] = iv; // structured bindings + CHECK(lower == 1); + CHECK(upper == 2); + } + + { + interval iv{1, 2}; + auto&& lower = iris::get<0>(iv); + STATIC_CHECK(std::same_as); + CHECK(lower == 1); + } + { + interval const iv{1, 2}; + auto&& lower = iris::get<0>(iv); + STATIC_CHECK(std::same_as); + CHECK(lower == 1); + } + { + interval iv{1, 2}; + auto&& lower = iris::get<0>(std::move(iv)); + STATIC_CHECK(std::same_as); + CHECK(lower == 1); + } + { + interval const iv{1, 2}; + auto&& lower = iris::get<0>(std::move(iv)); + STATIC_CHECK(std::same_as); + CHECK(lower == 1); + } +} + +TEST_CASE("interval: members") +{ + // Check in constexpr context to detect uninitialized value and other UBs + + // [x, x) + { + constexpr interval iv{}; + STATIC_CHECK(iv.lower == 0); + STATIC_CHECK(iv.upper == 0); + STATIC_CHECK(iv.length() == 0); + STATIC_CHECK(iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + { + constexpr interval iv{1, 1}; + STATIC_CHECK(iv.lower == 1); + STATIC_CHECK(iv.upper == 1); + STATIC_CHECK(iv.length() == 0); + STATIC_CHECK(iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + { + constexpr interval iv{-1, -1}; + STATIC_CHECK(iv.lower == -1); + STATIC_CHECK(iv.upper == -1); + STATIC_CHECK(iv.length() == 0); + STATIC_CHECK(iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + { + constexpr interval iv{5, -1}; // malformed + STATIC_CHECK(iv.lower == 5); + STATIC_CHECK(iv.upper == -1); + STATIC_CHECK(iv.length() == 0); + STATIC_CHECK(iv.empty()); + STATIC_CHECK(!iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + { + constexpr interval iv{-1, -5}; // malformed + STATIC_CHECK(iv.lower == -1); + STATIC_CHECK(iv.upper == -5); + STATIC_CHECK(iv.length() == 0); + STATIC_CHECK(iv.empty()); + STATIC_CHECK(!iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + + // [-, -) + { + constexpr interval iv{-5, -2}; + STATIC_CHECK(iv.lower == -5); + STATIC_CHECK(iv.upper == -2); + STATIC_CHECK(iv.length() == 3); + STATIC_CHECK(!iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + + // [-, +) + { + constexpr interval iv{-5, 2}; + STATIC_CHECK(iv.lower == -5); + STATIC_CHECK(iv.upper == 2); + STATIC_CHECK(iv.length() == 7); + STATIC_CHECK(!iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } + + // [+, +) + { + constexpr interval iv{2, 5}; + STATIC_CHECK(iv.lower == 2); + STATIC_CHECK(iv.upper == 5); + STATIC_CHECK(iv.length() == 3); + STATIC_CHECK(!iv.empty()); + STATIC_CHECK(iv.is_proper()); + STATIC_CHECK(iv.equals(iv)); + STATIC_CHECK(iv == iv); + STATIC_CHECK((iv <=> iv) == std::strong_ordering::equal); + } +} + +TEST_CASE("interval: relationship") +{ + STATIC_CHECK(interval{2, 5}.equals(interval{2, 5})); + STATIC_CHECK(interval{2, 5} == interval{2, 5}); + STATIC_CHECK((interval{2, 5} <=> interval{2, 5}) == std::strong_ordering::equal); + + // b = empty interval + STATIC_CHECK(interval{0, 0}.equals(interval{0, 0})); + STATIC_CHECK(interval{0, 0}.equals(interval{1, 1})); + STATIC_CHECK(interval{0, 0} == interval{0, 0}); + STATIC_CHECK(interval{0, 0} != interval{1, 1}); + STATIC_CHECK((interval{0, 0} <=> interval{0, 0}) == std::strong_ordering::equal); + STATIC_CHECK((interval{0, 0} <=> interval{1, 1}) == std::strong_ordering::less); + + // ---------------------------------------------------- + + STATIC_CHECK(!interval{2, 5}.intersects({0, 1})); STATIC_CHECK( interval{2, 5}.disjoint({0, 1})); + STATIC_CHECK(!interval{2, 5}.intersects({0, 2})); STATIC_CHECK( interval{2, 5}.disjoint({0, 2})); + STATIC_CHECK( interval{2, 5}.intersects({0, 3})); STATIC_CHECK(!interval{2, 5}.disjoint({0, 3})); + STATIC_CHECK( interval{2, 5}.intersects({4, 7})); STATIC_CHECK(!interval{2, 5}.disjoint({4, 7})); + STATIC_CHECK(!interval{2, 5}.intersects({5, 7})); STATIC_CHECK( interval{2, 5}.disjoint({5, 7})); + STATIC_CHECK(!interval{2, 5}.intersects({6, 7})); STATIC_CHECK( interval{2, 5}.disjoint({6, 7})); + + // b = empty interval + STATIC_CHECK(!interval{2, 5}.intersects({0, 0})); STATIC_CHECK( interval{2, 5}.disjoint({0, 0})); + STATIC_CHECK(!interval{2, 5}.intersects({1, 1})); STATIC_CHECK( interval{2, 5}.disjoint({1, 1})); + STATIC_CHECK(!interval{2, 5}.intersects({2, 2})); STATIC_CHECK( interval{2, 5}.disjoint({2, 2})); + STATIC_CHECK(!interval{2, 5}.intersects({3, 3})); STATIC_CHECK( interval{2, 5}.disjoint({3, 3})); + STATIC_CHECK(!interval{2, 5}.intersects({4, 4})); STATIC_CHECK( interval{2, 5}.disjoint({4, 4})); + STATIC_CHECK(!interval{2, 5}.intersects({5, 5})); STATIC_CHECK( interval{2, 5}.disjoint({5, 5})); + STATIC_CHECK(!interval{2, 5}.intersects({6, 6})); STATIC_CHECK( interval{2, 5}.disjoint({6, 6})); + + // ---------------------------------------------------- + + STATIC_CHECK(!interval{2, 5}.touches({0, 1})); + STATIC_CHECK( interval{2, 5}.touches({0, 2})); + STATIC_CHECK(!interval{2, 5}.touches({0, 3})); + STATIC_CHECK(!interval{2, 5}.touches({4, 7})); + STATIC_CHECK( interval{2, 5}.touches({5, 7})); + STATIC_CHECK(!interval{2, 5}.touches({6, 7})); + + // b = empty interval + STATIC_CHECK(!interval{2, 5}.touches({0, 0})); + STATIC_CHECK(!interval{2, 5}.touches({1, 1})); + STATIC_CHECK(!interval{2, 5}.touches({2, 2})); + STATIC_CHECK(!interval{2, 5}.touches({3, 3})); + STATIC_CHECK(!interval{2, 5}.touches({4, 4})); + STATIC_CHECK(!interval{2, 5}.touches({5, 5})); + STATIC_CHECK(!interval{2, 5}.touches({6, 6})); + + // ---------------------------------------------------- + + STATIC_CHECK(!interval{2, 5}.connected({0, 1})); + STATIC_CHECK( interval{2, 5}.connected({0, 2})); + STATIC_CHECK( interval{2, 5}.connected({0, 3})); + STATIC_CHECK( interval{2, 5}.connected({4, 7})); + STATIC_CHECK( interval{2, 5}.connected({5, 7})); + STATIC_CHECK(!interval{2, 5}.connected({6, 7})); + + // b = empty interval + STATIC_CHECK(!interval{2, 5}.connected({0, 0})); + STATIC_CHECK(!interval{2, 5}.connected({1, 1})); + STATIC_CHECK(!interval{2, 5}.connected({2, 2})); + STATIC_CHECK(!interval{2, 5}.connected({3, 3})); + STATIC_CHECK(!interval{2, 5}.connected({4, 4})); + STATIC_CHECK(!interval{2, 5}.connected({5, 5})); + STATIC_CHECK(!interval{2, 5}.connected({6, 6})); + + // ---------------------------------------------------- + + STATIC_CHECK(!interval{2, 5}.covers({0, 1})); + STATIC_CHECK(!interval{2, 5}.covers({0, 2})); + STATIC_CHECK(!interval{2, 5}.covers({0, 3})); + + STATIC_CHECK(!interval{2, 5}.covers({1, 2})); + STATIC_CHECK(!interval{2, 5}.covers({1, 3})); + + STATIC_CHECK( interval{2, 5}.covers({2, 3})); + STATIC_CHECK( interval{2, 5}.covers({2, 4})); + STATIC_CHECK( interval{2, 5}.covers({2, 5})); + STATIC_CHECK(!interval{2, 5}.covers({2, 6})); + + STATIC_CHECK( interval{2, 5}.covers({3, 4})); + STATIC_CHECK( interval{2, 5}.covers({3, 5})); + STATIC_CHECK(!interval{2, 5}.covers({3, 6})); + + STATIC_CHECK( interval{2, 5}.covers({4, 5})); + STATIC_CHECK(!interval{2, 5}.covers({4, 6})); + + STATIC_CHECK(!interval{2, 5}.covers({5, 6})); + + STATIC_CHECK(!interval{2, 5}.covers({6, 7})); + + // b = empty interval + STATIC_CHECK( interval{2, 5}.covers({0, 0})); + STATIC_CHECK( interval{2, 5}.covers({1, 1})); + STATIC_CHECK( interval{2, 5}.covers({2, 2})); + STATIC_CHECK( interval{2, 5}.covers({3, 3})); + STATIC_CHECK( interval{2, 5}.covers({4, 4})); + STATIC_CHECK( interval{2, 5}.covers({5, 5})); + STATIC_CHECK( interval{2, 5}.covers({6, 6})); + + // ---------------------------------------------------- + + STATIC_CHECK(interval{2, 5}.encloses({0, 1}) == interval{2, 5}.covers({0, 1})); + STATIC_CHECK(interval{2, 5}.encloses({0, 2}) == interval{2, 5}.covers({0, 2})); + STATIC_CHECK(interval{2, 5}.encloses({0, 3}) == interval{2, 5}.covers({0, 3})); + + STATIC_CHECK(interval{2, 5}.encloses({1, 2}) == interval{2, 5}.covers({1, 2})); + STATIC_CHECK(interval{2, 5}.encloses({1, 3}) == interval{2, 5}.covers({1, 3})); + + STATIC_CHECK(interval{2, 5}.encloses({2, 3}) == interval{2, 5}.covers({2, 3})); + STATIC_CHECK(interval{2, 5}.encloses({2, 4}) == interval{2, 5}.covers({2, 4})); + STATIC_CHECK(interval{2, 5}.encloses({2, 5}) == interval{2, 5}.covers({2, 5})); + STATIC_CHECK(interval{2, 5}.encloses({2, 6}) == interval{2, 5}.covers({2, 6})); + + STATIC_CHECK(interval{2, 5}.encloses({3, 4}) == interval{2, 5}.covers({3, 4})); + STATIC_CHECK(interval{2, 5}.encloses({3, 5}) == interval{2, 5}.covers({3, 5})); + STATIC_CHECK(interval{2, 5}.encloses({3, 6}) == interval{2, 5}.covers({3, 6})); + + STATIC_CHECK(interval{2, 5}.encloses({4, 5}) == interval{2, 5}.covers({4, 5})); + STATIC_CHECK(interval{2, 5}.encloses({4, 6}) == interval{2, 5}.covers({4, 6})); + + STATIC_CHECK(interval{2, 5}.encloses({5, 6}) == interval{2, 5}.covers({5, 6})); + + STATIC_CHECK(interval{2, 5}.encloses({6, 7}) == interval{2, 5}.covers({6, 7})); + + // b = empty interval + STATIC_CHECK(!interval{2, 5}.encloses({0, 0})); + STATIC_CHECK(!interval{2, 5}.encloses({1, 1})); + STATIC_CHECK( interval{2, 5}.encloses({2, 2})); + STATIC_CHECK( interval{2, 5}.encloses({3, 3})); + STATIC_CHECK( interval{2, 5}.encloses({4, 4})); + STATIC_CHECK( interval{2, 5}.encloses({5, 5})); + STATIC_CHECK(!interval{2, 5}.encloses({6, 6})); + + // ---------------------------------------------------- + + STATIC_CHECK(!interval{2, 5}.contains(0)); + STATIC_CHECK(!interval{2, 5}.contains(1)); + STATIC_CHECK( interval{2, 5}.contains(2)); + STATIC_CHECK( interval{2, 5}.contains(3)); + STATIC_CHECK( interval{2, 5}.contains(4)); + STATIC_CHECK(!interval{2, 5}.contains(5)); + STATIC_CHECK(!interval{2, 5}.contains(6)); + + // ---------------------------------------------------- + + STATIC_CHECK( interval{0, 5}.within("abcdef")); + STATIC_CHECK( interval{0, 6}.within("abcdef")); + STATIC_CHECK(!interval{0, 7}.within("abcdef")); + STATIC_CHECK( interval{3, 6}.within("abcdef")); + STATIC_CHECK( interval{4, 6}.within("abcdef")); + STATIC_CHECK( interval{5, 6}.within("abcdef")); + STATIC_CHECK( interval{6, 6}.within("abcdef")); + STATIC_CHECK(!interval{8, 9}.within("abcdef")); + + STATIC_CHECK( interval{0, 5}.within("abcdef"sv)); + STATIC_CHECK( interval{0, 6}.within("abcdef"sv)); + STATIC_CHECK(!interval{0, 7}.within("abcdef"sv)); + STATIC_CHECK( interval{3, 6}.within("abcdef"sv)); + STATIC_CHECK( interval{4, 6}.within("abcdef"sv)); + STATIC_CHECK( interval{5, 6}.within("abcdef"sv)); + STATIC_CHECK( interval{6, 6}.within("abcdef"sv)); + STATIC_CHECK(!interval{8, 9}.within("abcdef"sv)); + + // Empty interval + STATIC_CHECK( interval{0, 0}.within("abcdef")); + STATIC_CHECK( interval{1, 1}.within("abcdef")); + STATIC_CHECK( interval{2, 2}.within("abcdef")); + STATIC_CHECK( interval{3, 3}.within("abcdef")); + STATIC_CHECK( interval{4, 4}.within("abcdef")); + STATIC_CHECK( interval{5, 5}.within("abcdef")); + STATIC_CHECK( interval{6, 6}.within("abcdef")); + STATIC_CHECK(!interval{7, 7}.within("abcdef")); + + STATIC_CHECK( interval{0, 0}.within("abcdef"sv)); + STATIC_CHECK( interval{1, 1}.within("abcdef"sv)); + STATIC_CHECK( interval{2, 2}.within("abcdef"sv)); + STATIC_CHECK( interval{3, 3}.within("abcdef"sv)); + STATIC_CHECK( interval{4, 4}.within("abcdef"sv)); + STATIC_CHECK( interval{5, 5}.within("abcdef"sv)); + STATIC_CHECK( interval{6, 6}.within("abcdef"sv)); + STATIC_CHECK(!interval{7, 7}.within("abcdef"sv)); + + // Malformed interval + STATIC_CHECK(!interval{-1, -5}.within("abcdef")); + STATIC_CHECK(!interval{ 5, 2}.within("abcdef")); + STATIC_CHECK(!interval{ 2, -5}.within("abcdef")); +} + +TEST_CASE("interval: intersection") +{ + STATIC_CHECK(interval(2, 5).intersection({0, 1}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({0, 2}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({0, 3}) == interval{2, 3}); + STATIC_CHECK(interval(2, 5).intersection({0, 4}) == interval{2, 4}); + STATIC_CHECK(interval(2, 5).intersection({0, 5}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({0, 6}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({0, 7}) == interval{2, 5}); + + STATIC_CHECK(interval(2, 5).intersection({1, 2}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({1, 3}) == interval{2, 3}); + STATIC_CHECK(interval(2, 5).intersection({1, 4}) == interval{2, 4}); + STATIC_CHECK(interval(2, 5).intersection({1, 5}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({1, 6}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({1, 7}) == interval{2, 5}); + + STATIC_CHECK(interval(2, 5).intersection({2, 3}) == interval{2, 3}); + STATIC_CHECK(interval(2, 5).intersection({2, 4}) == interval{2, 4}); + STATIC_CHECK(interval(2, 5).intersection({2, 5}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({2, 6}) == interval{2, 5}); + STATIC_CHECK(interval(2, 5).intersection({2, 7}) == interval{2, 5}); + + STATIC_CHECK(interval(2, 5).intersection({3, 4}) == interval{3, 4}); + STATIC_CHECK(interval(2, 5).intersection({3, 5}) == interval{3, 5}); + STATIC_CHECK(interval(2, 5).intersection({3, 6}) == interval{3, 5}); + STATIC_CHECK(interval(2, 5).intersection({3, 7}) == interval{3, 5}); + + STATIC_CHECK(interval(2, 5).intersection({4, 5}) == interval{4, 5}); + STATIC_CHECK(interval(2, 5).intersection({4, 6}) == interval{4, 5}); + STATIC_CHECK(interval(2, 5).intersection({4, 7}) == interval{4, 5}); + + STATIC_CHECK(interval(2, 5).intersection({5, 6}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({5, 7}) == interval{0, 0}); + + STATIC_CHECK(interval(2, 5).intersection({6, 7}) == interval{0, 0}); + + // Empty + STATIC_CHECK(interval(2, 5).intersection({0, 0}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({1, 1}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({2, 2}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({3, 3}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({4, 4}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({5, 5}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({6, 6}) == interval{0, 0}); + STATIC_CHECK(interval(2, 5).intersection({7, 7}) == interval{0, 0}); + + // ----------------------------------------------------- + + STATIC_CHECK(interval(-5, -2).intersection({-7, -6}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-7, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-7, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, -2).intersection({-7, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, -2).intersection({-7, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-7, -1}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-7, 0}) == interval{-5, -2}); + + STATIC_CHECK(interval(-5, -2).intersection({-6, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-6, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, -2).intersection({-6, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, -2).intersection({-6, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-6, -1}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-6, 0}) == interval{-5, -2}); + + STATIC_CHECK(interval(-5, -2).intersection({-5, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, -2).intersection({-5, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, -2).intersection({-5, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-5, -1}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-5, 0}) == interval{-5, -2}); + + STATIC_CHECK(interval(-5, -2).intersection({-4, -3}) == interval{-4, -3}); + STATIC_CHECK(interval(-5, -2).intersection({-4, -2}) == interval{-4, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-4, -1}) == interval{-4, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-4, 0}) == interval{-4, -2}); + + STATIC_CHECK(interval(-5, -2).intersection({-3, -2}) == interval{-3, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-3, -1}) == interval{-3, -2}); + STATIC_CHECK(interval(-5, -2).intersection({-3, 0}) == interval{-3, -2}); + + STATIC_CHECK(interval(-5, -2).intersection({-2, -1}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-2, 0}) == interval{0, 0}); + + STATIC_CHECK(interval(-5, -2).intersection({-1, 0}) == interval{0, 0}); + + // Empty + STATIC_CHECK(interval(-5, -2).intersection({-7, -7}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-6, -6}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-5, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-4, -4}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-3, -3}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-2, -2}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({-1, -1}) == interval{0, 0}); + STATIC_CHECK(interval(-5, -2).intersection({0, 0}) == interval{0, 0}); + + // ----------------------------------------------------- + + STATIC_CHECK(interval(-5, 2).intersection({-7, -6}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-7, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-7, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, 2).intersection({-7, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, 2).intersection({-7, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, 2).intersection({-7, -1}) == interval{-5, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-7, 0}) == interval{-5, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-7, 1}) == interval{-5, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-7, 2}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-7, 3}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-7, 4}) == interval{-5, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-6, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-6, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, 2).intersection({-6, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, 2).intersection({-6, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, 2).intersection({-6, -1}) == interval{-5, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-6, 0}) == interval{-5, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-6, 1}) == interval{-5, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-6, 2}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-6, 3}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-6, 4}) == interval{-5, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-5, -4}) == interval{-5, -4}); + STATIC_CHECK(interval(-5, 2).intersection({-5, -3}) == interval{-5, -3}); + STATIC_CHECK(interval(-5, 2).intersection({-5, -2}) == interval{-5, -2}); + STATIC_CHECK(interval(-5, 2).intersection({-5, -1}) == interval{-5, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-5, 0}) == interval{-5, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-5, 1}) == interval{-5, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-5, 2}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-5, 3}) == interval{-5, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-5, 4}) == interval{-5, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-4, -3}) == interval{-4, -3}); + STATIC_CHECK(interval(-5, 2).intersection({-4, -2}) == interval{-4, -2}); + STATIC_CHECK(interval(-5, 2).intersection({-4, -1}) == interval{-4, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-4, 0}) == interval{-4, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-4, 1}) == interval{-4, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-4, 2}) == interval{-4, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-4, 3}) == interval{-4, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-4, 4}) == interval{-4, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-3, -2}) == interval{-3, -2}); + STATIC_CHECK(interval(-5, 2).intersection({-3, -1}) == interval{-3, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-3, 0}) == interval{-3, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-3, 1}) == interval{-3, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-3, 2}) == interval{-3, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-3, 3}) == interval{-3, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-3, 4}) == interval{-3, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-2, -1}) == interval{-2, -1}); + STATIC_CHECK(interval(-5, 2).intersection({-2, 0}) == interval{-2, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-2, 1}) == interval{-2, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-2, 2}) == interval{-2, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-2, 3}) == interval{-2, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-2, 4}) == interval{-2, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({-1, 0}) == interval{-1, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-1, 1}) == interval{-1, 1}); + STATIC_CHECK(interval(-5, 2).intersection({-1, 2}) == interval{-1, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-1, 3}) == interval{-1, 2}); + STATIC_CHECK(interval(-5, 2).intersection({-1, 4}) == interval{-1, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({0, 1}) == interval{0, 1}); + STATIC_CHECK(interval(-5, 2).intersection({0, 2}) == interval{0, 2}); + STATIC_CHECK(interval(-5, 2).intersection({0, 3}) == interval{0, 2}); + STATIC_CHECK(interval(-5, 2).intersection({0, 4}) == interval{0, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({1, 2}) == interval{1, 2}); + STATIC_CHECK(interval(-5, 2).intersection({1, 3}) == interval{1, 2}); + STATIC_CHECK(interval(-5, 2).intersection({1, 4}) == interval{1, 2}); + + STATIC_CHECK(interval(-5, 2).intersection({2, 3}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({2, 4}) == interval{0, 0}); + + STATIC_CHECK(interval(-5, 2).intersection({3, 4}) == interval{0, 0}); + + // Empty + STATIC_CHECK(interval(-5, 2).intersection({-7, -7}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-6, -6}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-5, -5}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-4, -4}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-3, -3}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-2, -2}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({-1, -1}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({0, 0}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({1, 1}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({2, 2}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({3, 3}) == interval{0, 0}); + STATIC_CHECK(interval(-5, 2).intersection({4, 4}) == interval{0, 0}); +} + +TEST_CASE("interval: subview") +{ + { + CHECK(interval{}.as_subview_of("abcdefg"sv) == ""sv); + CHECK(interval(2, 5).as_subview_of("abcdefg"sv) == "cde"sv); + CHECK(interval(2, 5).as_subview_of("abcdefg") == "cde"sv); // array adapter, N-1 + + std::string const str = "abcdefg"; // lvalue: dangling guard + CHECK(interval(2, 5).as_subview_of(str) == "cde"sv); + + std::vector const ivec{0, 1, 2, 3, 4, 5, 6, 7}; + CHECK(interval{}.as_subview_of(ivec).empty()); + CHECK(std::ranges::equal(interval(2, 5).as_subview_of(ivec), std::array{2, 3, 4})); + } + + // null character boundary + { + // raw char array + CHECK(interval(0, 2).as_subview_of("abc") == "ab"sv); + CHECK(interval(0, 3).as_subview_of("abc") == "abc"sv); + CHECK_THROWS_AS(interval(0, 4).as_subview_of("abc"), std::out_of_range); + + // string_view + CHECK(interval(0, 2).as_subview_of("abc"sv) == "ab"sv); + CHECK(interval(0, 3).as_subview_of("abc"sv) == "abc"sv); + CHECK_THROWS_AS(interval(0, 4).as_subview_of("abc"sv), std::out_of_range); + } + + // Malformed + { + CHECK_THROWS_AS(interval(0, -2).as_subview_of("abcdefg"sv), std::domain_error); + CHECK_THROWS_AS(interval(5, 4).as_subview_of("abcdefg"sv), std::domain_error); + CHECK_THROWS_AS(interval(-4, -5).as_subview_of("abcdefg"sv), std::domain_error); + CHECK_THROWS_AS(interval(0, 50).as_subview_of("abcdefg"sv), std::out_of_range); + CHECK_THROWS_AS(interval(4, 50).as_subview_of("abcdefg"sv), std::out_of_range); + CHECK_THROWS_AS(interval(50, 51).as_subview_of("abcdefg"sv), std::out_of_range); + + std::vector const ivec{0, 1, 2, 3, 4, 5, 6}; + CHECK_THROWS_AS(interval(0, -2).as_subview_of(ivec), std::domain_error); + CHECK_THROWS_AS(interval(5, 4).as_subview_of(ivec), std::domain_error); + CHECK_THROWS_AS(interval(-4, -5).as_subview_of(ivec), std::domain_error); + CHECK_THROWS_AS(interval(0, 50).as_subview_of(ivec), std::out_of_range); + CHECK_THROWS_AS(interval(4, 50).as_subview_of(ivec), std::out_of_range); + CHECK_THROWS_AS(interval(50, 51).as_subview_of(ivec), std::out_of_range); + } +} + +TEST_CASE("interval: format") +{ + CHECK(std::format("{}", interval{}) == "[0,0)"sv); + CHECK(std::format("{}", interval{1, 2}) == "[1,2)"sv); + CHECK(std::format("{:2d,}", interval{1, 2}) == "[ 1,2)"sv); + CHECK(std::format("{:2d,3d}", interval{1, 2}) == "[ 1, 2)"sv); +} + +// NOLINTEND(readability-container-size-empty) diff --git a/test/interval_algo.cpp b/test/interval_algo.cpp new file mode 100644 index 0000000..bad994d --- /dev/null +++ b/test/interval_algo.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; + +using iris::interval; +using I = interval; + +// NOLINTBEGIN(readability-container-size-empty) + +TEST_CASE("interval_algo: select_min_extent") +{ + using ivec = std::vector>; + using words_t = std::vector; + + // The closest candidate of a multi-span word is chosen + CHECK(iris::select_min_extent(words_t{{{10, 13}}, {{15, 17}}, {{0, 10}, {200, 210}}}) + == ivec{{0, 10}, {10, 13}, {15, 17}}); + + // The redundant wide span of word 0 must not inflate the hull + CHECK(iris::select_min_extent(words_t{{{10, 12}, {11, 200}}, {{11, 13}}}) + == ivec{{10, 13}}); // overlapping winners merge into one match + + // Two equally tight clusters: the leftmost wins + CHECK(iris::select_min_extent(words_t{{{0, 2}, {100, 102}}, {{5, 7}, {105, 107}}}) + == ivec{{0, 2}, {5, 7}}); + + // A word without candidates is skipped + CHECK(iris::select_min_extent(words_t{{}, {{5, 7}}}) == ivec{{5, 7}}); + + CHECK(iris::select_min_extent(words_t{{}, {}}).empty()); + CHECK(iris::select_min_extent(words_t{}).empty()); + + // Single word: the shortest span wins + CHECK(iris::select_min_extent(words_t{{{8, 12}, {3, 5}, {20, 21}}}) == ivec{{20, 21}}); + + // Equal lengths: the leftmost wins + CHECK(iris::select_min_extent(words_t{{{10, 11}, {5, 6}}}) == ivec{{5, 6}}); + + // Identical spans from different words are deduplicated + CHECK(iris::select_min_extent(words_t{{{7, 9}}, {{7, 9}}}) == ivec{{7, 9}}); + + // Touching winners stay separate matches + CHECK(iris::select_min_extent(words_t{{{0, 5}}, {{5, 9}}}) == ivec{{0, 5}, {5, 9}}); + + // A caret is a valid winner + CHECK(iris::select_min_extent(words_t{{{10, 10}}, {{12, 14}}}) + == ivec{{10, 10}, {12, 14}}); + + // A caret inside another word's winner is absorbed by the merge + CHECK(iris::select_min_extent(words_t{{{10, 10}}, {{8, 12}}}) == ivec{{8, 12}}); +} + +TEST_CASE("interval_algo: select_min_extent (brute hull)") +{ + // Brute force over every one-span-per-word combination: + // - The selector's hull must match the minimum + // - Its output must be sorted and non-overlapping (touching allowed) + + auto const brute_hull = [](std::vector> const& words) -> long long { + std::vector> lists; + for (auto const& w : words) { + if (!w.empty()) { + lists.emplace_back(w); + } + } + if (lists.empty()) return -1; + + long long best = std::numeric_limits::max(); + std::vector idx(lists.size(), 0); + while (true) { + int lo = std::numeric_limits::max(); + int up = std::numeric_limits::min(); + for (std::size_t k = 0; k < lists.size(); ++k) { + lo = std::min(lo, lists[k][idx[k]].lower); + up = std::max(up, lists[k][idx[k]].upper); + } + best = std::min(best, static_cast(std::max(up, lo)) - lo); + + std::size_t k = 0; + while (k < lists.size() && ++idx[k] == lists[k].size()) { + idx[k++] = 0; + } + if (k == lists.size()) break; + } + return best; + }; + + std::mt19937 rng(37); + + for (int i = 0; i < 20000; ++i) { + std::vector> words(1 + rng() % 4); + for (auto& w : words) { + std::size_t const k = rng() % 5; // zero-candidate words included + for (std::size_t j = 0; j < k; ++j) { + int const lo = static_cast(rng() % 40); + w.emplace_back(lo, lo + static_cast(rng() % 7)); // carets included + } + } + auto const out = iris::select_min_extent(words); + long long const want = brute_hull(words); + if (want < 0) { + CHECK(out.empty()); + continue; + } + REQUIRE(!out.empty()); + CHECK(static_cast(out.back().upper) - out.front().lower == want); + for (std::size_t k = 1; k < out.size(); ++k) { + CHECK(out[k - 1].upper <= out[k].lower); + } + } +} + +// NOLINTEND(readability-container-size-empty) diff --git a/test/interval_set.cpp b/test/interval_set.cpp new file mode 100644 index 0000000..ff8c1e4 --- /dev/null +++ b/test/interval_set.cpp @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; +using iris::interval; +using IVS = iris::interval_set>; + +// NOLINTBEGIN(readability-container-size-empty) + +TEST_CASE("interval_set: type_traits") +{ + STATIC_CHECK(std::bidirectional_iterator); + STATIC_CHECK(std::bidirectional_iterator); + + STATIC_CHECK(std::ranges::bidirectional_range); + STATIC_CHECK(std::ranges::sized_range); + + STATIC_CHECK(std::is_default_constructible_v); + STATIC_CHECK(std::is_nothrow_destructible_v); + STATIC_CHECK(std::is_copy_constructible_v); + STATIC_CHECK(std::is_move_constructible_v); + STATIC_CHECK(std::is_nothrow_move_constructible_v == std::is_nothrow_move_constructible_v); + STATIC_CHECK(std::is_nothrow_move_assignable_v == std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::is_copy_assignable_v); + STATIC_CHECK(std::is_nothrow_swappable_v); + + STATIC_CHECK(!std::is_constructible_v>); + STATIC_CHECK(std::is_constructible_v>>); +} + +TEST_CASE("interval_set: construction") +{ + { + STATIC_CHECK(std::is_constructible_v>::iterator, std::vector>::iterator>); + std::vector> v; + [[maybe_unused]] IVS ivs{v.begin(), v.end()}; + } + + { + STATIC_CHECK(std::is_constructible_v>>); + [[maybe_unused]] IVS ivs{std::from_range, std::vector>{}}; + } + + { + std::vector ivec; + auto view = ivec | std::views::transform([](int const lower) -> interval { + return {lower, lower + 1}; + }); + using View = decltype(view); + STATIC_CHECK(std::is_constructible_v); + STATIC_CHECK(std::is_constructible_v); + STATIC_CHECK(std::is_constructible_v); + + IVS const ivs{std::from_range, view}; + CHECK(ivs.empty()); + } + { + std::vector ivec{0}; + auto view = ivec | std::views::transform([](int const lower) -> interval { + return {lower, lower + 1}; + }); + IVS const ivs{std::from_range, view}; + CHECK(ivs.size() == 1); + CHECK(ivs == IVS{{0, 1}}); + } + { + std::vector ivec{0}; + auto view = ivec | std::views::transform([](int const lower) -> interval { + return {lower, lower}; + }); + IVS const ivs{std::from_range, view}; + CHECK(ivs.empty()); + } + + { + std::vector ivec{0, 4, 8}; + auto view = ivec | std::views::transform([](int const lower) -> interval { + return {lower, lower + 1}; + }); + + IVS const ivs{std::from_range, view}; + CHECK(ivs.size() == 3); + CHECK(ivs == IVS{{0, 1}, {4, 5}, {8, 9}}); + } + { + std::vector ivec{0, 1, 2}; + auto view = ivec | std::views::transform([](int const lower) -> interval { + return {lower, lower + 1}; + }); + IVS const ivs{std::from_range, view}; + CHECK(ivs.size() == 1); + CHECK(ivs == IVS{{0, 3}}); + } +} + +TEST_CASE("interval_set: identity") +{ + { + IVS ivs; + CHECK(ivs.size() == 0); + CHECK(ivs.empty()); + CHECK(ivs.begin() == ivs.end()); + CHECK(ivs.extent() == interval{}); + CHECK(ivs == ivs); + CHECK((ivs <=> ivs) == std::strong_ordering::equal); + } + + { + IVS ivs{{0, 0}}; + CHECK(ivs.size() == 0); + CHECK(ivs.empty()); + CHECK(ivs.begin() == ivs.end()); + CHECK(ivs.extent() == interval{}); + CHECK(ivs == ivs); + CHECK((ivs <=> ivs) == std::strong_ordering::equal); + } + { + IVS ivs{{1, 1}}; + CHECK(ivs.size() == 0); + CHECK(ivs.empty()); + CHECK(ivs.begin() == ivs.end()); + CHECK(ivs.extent() == interval{}); // not {1, 1} + CHECK(ivs == ivs); + CHECK((ivs <=> ivs) == std::strong_ordering::equal); + } + + { + IVS ivs{{0, 1}}; + CHECK(ivs.size() == 1); + CHECK(!ivs.empty()); + CHECK(ivs.begin() != ivs.end()); + CHECK(std::distance(ivs.begin(), ivs.end()) == 1); + CHECK(ivs == ivs); + CHECK((ivs <=> ivs) == std::strong_ordering::equal); + } +} + +TEST_CASE("interval_set: extent") +{ + CHECK(IVS{}.extent() == interval{0, 0}); + CHECK(IVS{{0, 0}}.extent() == interval{0, 0}); + CHECK(IVS{{1, 1}}.extent() == interval{0, 0}); + + CHECK(IVS{{2, 5}}.extent() == interval{2, 5}); + + CHECK(IVS{{2, 5}, {0, 1}}.extent() == interval{0, 5}); + CHECK(IVS{{2, 5}, {0, 2}}.extent() == interval{0, 5}); + CHECK(IVS{{2, 5}, {0, 3}}.extent() == interval{0, 5}); + CHECK(IVS{{2, 5}, {0, 4}}.extent() == interval{0, 5}); + CHECK(IVS{{2, 5}, {0, 5}}.extent() == interval{0, 5}); + CHECK(IVS{{2, 5}, {0, 6}}.extent() == interval{0, 6}); + CHECK(IVS{{2, 5}, {0, 7}}.extent() == interval{0, 7}); + + CHECK(IVS{{2, 5}, {1, 2}}.extent() == interval{1, 5}); + CHECK(IVS{{2, 5}, {1, 3}}.extent() == interval{1, 5}); + CHECK(IVS{{2, 5}, {1, 4}}.extent() == interval{1, 5}); + CHECK(IVS{{2, 5}, {1, 5}}.extent() == interval{1, 5}); + CHECK(IVS{{2, 5}, {1, 6}}.extent() == interval{1, 6}); + CHECK(IVS{{2, 5}, {1, 7}}.extent() == interval{1, 7}); + + CHECK(IVS{{2, 5}, {2, 3}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {2, 4}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {2, 5}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {2, 6}}.extent() == interval{2, 6}); + CHECK(IVS{{2, 5}, {2, 7}}.extent() == interval{2, 7}); + + CHECK(IVS{{2, 5}, {3, 4}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {3, 5}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {3, 6}}.extent() == interval{2, 6}); + CHECK(IVS{{2, 5}, {3, 7}}.extent() == interval{2, 7}); + + CHECK(IVS{{2, 5}, {4, 5}}.extent() == interval{2, 5}); + CHECK(IVS{{2, 5}, {4, 6}}.extent() == interval{2, 6}); + CHECK(IVS{{2, 5}, {4, 7}}.extent() == interval{2, 7}); + + CHECK(IVS{{2, 5}, {5, 6}}.extent() == interval{2, 6}); + CHECK(IVS{{2, 5}, {5, 7}}.extent() == interval{2, 7}); + + CHECK(IVS{{2, 5}, {6, 7}}.extent() == interval{2, 7}); + + CHECK(IVS{{-5, -3}, {4, 10}}.extent() == interval{-5, 10}); + CHECK(IVS{{-5, -3}, {-1, 2}, {4, 10}}.extent() == interval{-5, 10}); +} + +#define IRIS_CHECK_REL(rel, a0, a1, b0, b1) \ + CHECK(interval a0, a1 .rel(b0, b1) == IVS{a0, a1}.rel(b0, b1)) + +#define IRIS_CHECK_REL_P(rel, a0, a1, p) \ + CHECK(interval a0, a1 .rel(p) == IVS{a0, a1}.rel(p)) + +TEST_CASE("interval_set: relationship") +{ + IRIS_CHECK_REL(intersects, {2, 5}, {0, 1}); + IRIS_CHECK_REL(intersects, {2, 5}, {0, 2}); + IRIS_CHECK_REL(intersects, {2, 5}, {0, 3}); + IRIS_CHECK_REL(intersects, {2, 5}, {4, 7}); + IRIS_CHECK_REL(intersects, {2, 5}, {5, 7}); + IRIS_CHECK_REL(intersects, {2, 5}, {6, 7}); + + // b = empty interval + IRIS_CHECK_REL(intersects, {2, 5}, {0, 0}); + IRIS_CHECK_REL(intersects, {2, 5}, {1, 1}); + IRIS_CHECK_REL(intersects, {2, 5}, {2, 2}); + IRIS_CHECK_REL(intersects, {2, 5}, {3, 3}); + IRIS_CHECK_REL(intersects, {2, 5}, {4, 4}); + IRIS_CHECK_REL(intersects, {2, 5}, {5, 5}); + IRIS_CHECK_REL(intersects, {2, 5}, {6, 6}); + + // ---------------------------------------------------- + + IRIS_CHECK_REL(covers, {2, 5}, {0, 1}); + IRIS_CHECK_REL(covers, {2, 5}, {0, 2}); + IRIS_CHECK_REL(covers, {2, 5}, {0, 3}); + + IRIS_CHECK_REL(covers, {2, 5}, {1, 2}); + IRIS_CHECK_REL(covers, {2, 5}, {1, 3}); + + IRIS_CHECK_REL(covers, {2, 5}, {2, 3}); + IRIS_CHECK_REL(covers, {2, 5}, {2, 4}); + IRIS_CHECK_REL(covers, {2, 5}, {2, 5}); + IRIS_CHECK_REL(covers, {2, 5}, {2, 6}); + + IRIS_CHECK_REL(covers, {2, 5}, {3, 4}); + IRIS_CHECK_REL(covers, {2, 5}, {3, 5}); + IRIS_CHECK_REL(covers, {2, 5}, {3, 6}); + + IRIS_CHECK_REL(covers, {2, 5}, {4, 5}); + IRIS_CHECK_REL(covers, {2, 5}, {4, 6}); + + IRIS_CHECK_REL(covers, {2, 5}, {5, 6}); + + IRIS_CHECK_REL(covers, {2, 5}, {6, 7}); + + // b = empty interval + IRIS_CHECK_REL(covers, {2, 5}, {0, 0}); + IRIS_CHECK_REL(covers, {2, 5}, {1, 1}); + IRIS_CHECK_REL(covers, {2, 5}, {2, 2}); + IRIS_CHECK_REL(covers, {2, 5}, {3, 3}); + IRIS_CHECK_REL(covers, {2, 5}, {4, 4}); + IRIS_CHECK_REL(covers, {2, 5}, {5, 5}); + IRIS_CHECK_REL(covers, {2, 5}, {6, 6}); + + // ---------------------------------------------------- + + IRIS_CHECK_REL_P(contains, {2, 5}, 0); + IRIS_CHECK_REL_P(contains, {2, 5}, 1); + IRIS_CHECK_REL_P(contains, {2, 5}, 2); + IRIS_CHECK_REL_P(contains, {2, 5}, 3); + IRIS_CHECK_REL_P(contains, {2, 5}, 4); + IRIS_CHECK_REL_P(contains, {2, 5}, 5); + IRIS_CHECK_REL_P(contains, {2, 5}, 6); +} + +TEST_CASE("interval_set: insertion") +{ + // Insertion of empty interval is no-op + { + IVS ivs{{2, 5}}; ivs.insert({0, 0}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 1}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({2, 2}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({3, 3}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({4, 4}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({5, 5}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({6, 6}); + CHECK(ivs == IVS{{2, 5}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({0, 1}); + CHECK(ivs == IVS{{0, 1}, {2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 2}); + CHECK(ivs == IVS{{0, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 3}); + CHECK(ivs == IVS{{0, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 4}); + CHECK(ivs == IVS{{0, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 5}); + CHECK(ivs == IVS{{0, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 6}); + CHECK(ivs == IVS{{0, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({0, 7}); + CHECK(ivs == IVS{{0, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({1, 2}); + CHECK(ivs == IVS{{1, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 3}); + CHECK(ivs == IVS{{1, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 4}); + CHECK(ivs == IVS{{1, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 5}); + CHECK(ivs == IVS{{1, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 6}); + CHECK(ivs == IVS{{1, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({1, 7}); + CHECK(ivs == IVS{{1, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({2, 3}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({2, 4}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({2, 5}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({2, 6}); + CHECK(ivs == IVS{{2, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({2, 7}); + CHECK(ivs == IVS{{2, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({3, 4}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({3, 5}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({3, 6}); + CHECK(ivs == IVS{{2, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({3, 7}); + CHECK(ivs == IVS{{2, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({4, 5}); + CHECK(ivs == IVS{{2, 5}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({4, 6}); + CHECK(ivs == IVS{{2, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({4, 7}); + CHECK(ivs == IVS{{2, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({5, 6}); + CHECK(ivs == IVS{{2, 6}}); + } + { + IVS ivs{{2, 5}}; ivs.insert({5, 7}); + CHECK(ivs == IVS{{2, 7}}); + } + // -------------------------------------------------------------- + { + IVS ivs{{2, 5}}; ivs.insert({6, 7}); + CHECK(ivs == IVS{{2, 5}, {6, 7}}); + } +} + +TEST_CASE("interval_set: format") +{ + CHECK(std::format("{}", IVS{}) == "{}"sv); + CHECK(std::format("{}", IVS{{0, 1}}) == "{[0,1)}"sv); + CHECK(std::format("{}", IVS{{0, 1}, {2, 3}}) == "{[0,1) [2,3)}"sv); + CHECK(std::format("{:2d,3d}", IVS{{0, 1}, {2, 3}}) == "{[ 0, 1) [ 2, 3)}"sv); +} + +// NOLINTEND(readability-container-size-empty) diff --git a/test/snippet.cpp b/test/snippet.cpp new file mode 100644 index 0000000..5f63604 --- /dev/null +++ b/test/snippet.cpp @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include +#include + +#include +#include +#include + +using namespace std::string_view_literals; + +namespace snip = iris::snip; + +// NOLINTBEGIN(readability-container-size-empty) + +TEST_CASE("snippet: type traits") +{ + STATIC_CHECK(snip::SnippetSink>); +} + +TEST_CASE("snippet: process predefined frags") +{ + // Match flush at fragment start; full coverage, no gaps + { + auto const input = U"XXabc"sv; + + std::vector> matches{ + {0, 2}, // XX + }; + iris::interval_set> frags; + frags.insert({0, 5}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(M"XX" C"abc")"); + } + + // Match flush at fragment end + { + auto const input = U"abcXX"sv; + + std::vector> matches{ + {3, 5}, // XX + }; + iris::interval_set> frags; + frags.insert({0, 5}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(C"abc" M"XX")"); + } + + // Two matches merged into one fragment (close regime), no gaps + { + auto const input = U"aaXXbbYYcc"sv; + + std::vector> matches{ + {2, 4}, // XX + {6, 8}, // YY + }; + iris::interval_set> frags; + frags.insert({0, 10}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(C"aa" M"XX" C"bb" M"YY" C"cc")"); + } + + // Merged fragment with both leading and trailing gap + { + auto const input = U"aaXXbbYYcc"sv; + + std::vector> matches{ + {2, 4}, // XX + {6, 8}, // YY + }; + iris::interval_set> frags; + frags.insert({1, 9}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(G C"a" M"XX" C"bb" M"YY" C"c" G)"); + } + + // Adjacent matches: no empty context between matches + { + auto const input = U"abXXYYcd"sv; + + std::vector> matches{ + {2, 4}, // XX + {4, 6}, // YY + }; + iris::interval_set> frags; + frags.insert({0, 8}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(C"ab" M"XX" M"YY" C"cd")"); + } + + // Match equals fragment: match only, gaps both sides + { + auto const input = U"aXXb"sv; + + std::vector> matches{ + {1, 3}, // XX + }; + iris::interval_set> frags; + frags.insert({1, 3}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(G M"XX" G)"); + } + + // Caret match mid-fragment: empty match + { + auto const input = U"abcdef"sv; + + std::vector> matches{ + {3, 3}, // caret + }; + iris::interval_set> frags; + frags.insert({1, 5}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(G C"bc" M"" C"de" G)"); + } + + // Caret match at fragment upper edge (gap domain membership) + { + auto const input = U"abcdef"sv; + + std::vector> matches{ + {5, 5}, // caret at edge + }; + iris::interval_set> frags; + frags.insert({1, 5}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(G C"bcde" M"" G)"); + } + + // Three fragments: mixed edge-flush matches, inner gaps, no outer gaps + { + auto const input = U"XXaaaaYYbbbbZZ"sv; + + std::vector> matches{ + { 0, 2}, // XX + { 6, 8}, // YY + {12, 14}, // ZZ + }; + iris::interval_set> frags; + frags.insert({ 0, 3}); + frags.insert({ 5, 9}); + frags.insert({11, 14}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == R"(M"XX" C"a" G C"a" M"YY" C"b" G C"b" M"ZZ")"); + } + + // Empty matches and empty frags: no events + { + auto const input = U"abcdef"sv; + + std::vector> matches; + iris::interval_set> frags; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, frags, sink); + + CHECK(sink.to_string() == ""); + CHECK(sink.events.empty()); + } + + // Match not covered by any fragment + { + auto const input = U"abcdefghi"sv; + + std::vector> matches{ + {3, 5}, + }; + iris::interval_set> frags; + frags.insert({6, 9}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + CHECK_THROWS_AS(sgen.process(input, matches, frags, sink), std::invalid_argument); + CHECK(sink.events.empty()); + } + + // Caret match outside every fragment + { + auto const input = U"abcdef"sv; + + std::vector> matches{ + {8, 8}, // stray caret + }; + iris::interval_set> frags; + frags.insert({1, 5}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + CHECK_THROWS_AS(sgen.process(input, matches, frags, sink), std::invalid_argument); + CHECK(sink.events.empty()); + } + + // Nonempty matches with empty frags + { + auto const input = U"abcdef"sv; + + std::vector> matches{ + {1, 3}, + }; + iris::interval_set> frags; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + CHECK_THROWS_AS(sgen.process(input, matches, frags, sink), std::invalid_argument); + CHECK(sink.events.empty()); + } + + // Unsorted matches + { + auto const input = U"abcXXdefghiYYjkl"sv; + + std::vector> matches{ + {11, 13}, // YY first + { 3, 5}, // XX + }; + iris::interval_set> frags; + frags.insert({1, 7}); + frags.insert({9, 15}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + CHECK_THROWS_AS(sgen.process(input, matches, frags, sink), std::invalid_argument); + CHECK(sink.events.empty()); + } + + // Fragment extent outside input text + { + auto const input = U"abc"sv; + + std::vector> matches{ + {1, 2}, + }; + iris::interval_set> frags; + frags.insert({0, 100}); + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + CHECK_THROWS_AS(sgen.process(input, matches, frags, sink), std::out_of_range); + CHECK(sink.events.empty()); + } +} + +TEST_CASE("snippet budget (recording_sink)") +{ + // Gap widths of recording_sink are all 0, so the budget is text chars only. + + // Match longer than cap: truncated + { + auto const input = U"abcdeMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMvwxyz"sv; // 40 chars + + std::vector> matches{ + {5, 35}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 20, sink); + + CHECK(sink.to_string() == R"(G M"MMMMMMMMMMMMMMMMMMMM" G)"); + } + + // Match at offset 0: no leading gap + { + auto const input = U"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMabcdefghij"sv; // 40 chars + + std::vector> matches{ + {0, 30}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 25, sink); + + CHECK(sink.to_string() == R"(M"MMMMMMMMMMMMMMMMMMMMMMMMM" G)"); + } + + // Cap fits only the first two matches + { + auto const input = + U"012345678901234567890123456789012345678901234567890123456789"sv; // 60 chars + + std::vector> matches{ + { 5, 7}, + {20, 22}, + {40, 42}, // dropped + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 5, sink); + + CHECK(sink.to_string() == R"(G M"56" G M"01" G)"); + } + + // No room for context: caret dropped, freed budget pads the survivor + { + auto const input = U"0123456789012345678901234567890123456789"sv; // 40 chars + + std::vector> matches{ + { 3, 5}, + {20, 20}, // caret, dropped + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 4, sink); + + CHECK(sink.to_string() == R"(G C"2" M"34" C"5" G)"); + } + + // Caret with enough budget for context + { + auto const input = U"0123456789012345678901234567890123456789"sv; // 40 chars + + std::vector> matches{ + {10, 10}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 10, sink); + + CHECK(sink.to_string() == R"(G C"56789" M"" C"01234" G)"); + } + + // Caret without any context budget: nothing to show + { + auto const input = U"0123456789012345678901234567890123456789"sv; + + std::vector> matches{ + {10, 10}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 1, sink); + + CHECK(sink.events.empty()); + } + + // Empty text + { + auto const input = U""sv; + + std::vector> matches{ + {0, 0}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 50, sink); + + CHECK(sink.events.empty()); + } + + // Cap zero + { + auto const input = U"01234567890123456789"sv; + + std::vector> matches{ + {5, 7}, + }; + + snip::recording_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 0, sink); + + CHECK(sink.events.empty()); + } +} + +TEST_CASE("snippet budget (search_result_sink)") +{ + // Gap widths: "... " = 4, " ... " = 5, " ..." = 4 + + // Match longer than cap: truncated to 20 - 4 - 4 = 12 chars + { + auto const input = U"abcdeMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMvwxyz"sv; // 40 chars + + std::vector> matches{ + {5, 35}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 20, sink); + + CHECK(sink.result_str == U"... [b]MMMMMMMMMMMM[/b] ..."); + } + + // Match at offset 0: no leading gap, so 35 - 4 = 31 chars fit it untruncated + { + auto const input = U"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMabcdefghij"sv; // 40 chars + + std::vector> matches{ + {0, 30}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 35, sink); + + CHECK(sink.result_str == U"[b]MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM[/b] ..."); + } + + // Cap fits only the first two matches, 1 char of context each side + { + auto const input = + U"012345678901234567890123456789012345678901234567890123456789"sv; // 60 chars + + std::vector> matches{ + { 5, 7}, + {20, 22}, + {40, 42}, // dropped + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 21, sink); + + CHECK(sink.result_str == U"... 4[b]56[/b]7 ... 9[b]01[/b]2 ..."); + } + + // No room for context: caret dropped, freed budget pads the survivor + { + auto const input = U"0123456789012345678901234567890123456789"sv; // 40 chars + + std::vector> matches{ + { 3, 5}, + {20, 20}, // caret, dropped + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 15, sink); + + CHECK(sink.result_str == U"... 12[b]34[/b]56 ..."); + } + + // Caret with context budget renders as empty [b][/b] + { + auto const input = U"0123456789012345678901234567890123456789"sv; // 40 chars + + std::vector> matches{ + {10, 10}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 20, sink); + + CHECK(sink.result_str == U"... 456789[b][/b]012345 ..."); + } + + // Caret without any context budget: nothing to show + { + auto const input = U"0123456789012345678901234567890123456789"sv; + + std::vector> matches{ + {10, 10}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 5, sink); + + CHECK(sink.result_str.empty()); + } + + // Empty text + { + auto const input = U""sv; + + std::vector> matches{ + {0, 0}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 50, sink); + + CHECK(sink.result_str.empty()); + } + + // Cap zero + { + auto const input = U"01234567890123456789"sv; + + std::vector> matches{ + {5, 7}, + }; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + sgen.process(input, matches, 0, sink); + + CHECK(sink.result_str.empty()); + } + + // Reused generator and sink: second result replaces the first + { + auto const input = U"0123456789012345678901234567890123456789"sv; + + snip::search_result_sink<> sink; + snip::snippet_generator<> sgen; + + std::vector> first{{3, 5}}; + sgen.process(input, first, 15, sink); + std::vector> second{{10, 10}}; + sgen.process(input, second, 20, sink); + + CHECK(sink.result_str == U"... 456789[b][/b]012345 ..."); + } +} + +// NOLINTEND(readability-container-size-empty)