diff --git a/doc/sql.extensions/README.window_functions.md b/doc/sql.extensions/README.window_functions.md index b4c9fad277d..600c505c678 100644 --- a/doc/sql.extensions/README.window_functions.md +++ b/doc/sql.extensions/README.window_functions.md @@ -4,7 +4,7 @@ By the SQL specification, window functions (also know as analytical functions) a That sort of functions are used with the `OVER` clause. Window functions may appear only in the select list or the `ORDER BY` clause of a query. -Additional to the `OVER` clause, Firebird window functions may use partitions, order and frames (FB 4.0). +Additional to the `OVER` clause, Firebird window functions may use partitions, order, frames (FB 4.0) and frame exclusions (FB 6.0). Syntax: @@ -23,7 +23,7 @@ Syntax: ORDER BY [] [] [, [] []] ... ::= - {RANGE | ROWS} + {RANGE | ROWS} [] ::= { | } @@ -35,6 +35,9 @@ Syntax: BETWEEN {UNBOUNDED PRECEDING | PRECEDING | FOLLOWING | CURRENT ROW} AND {UNBOUNDED FOLLOWING | PRECEDING | FOLLOWING | CURRENT ROW} + ::= + EXCLUDE {NO OTHERS | CURRENT ROW | GROUP | TIES} + ::= {ASC | DESC} @@ -279,6 +282,28 @@ With `ROWS`, order expressions is not limited by number or types. In this case, The frame syntax with `` specifies the start frame, with the end frame being `CURRENT ROW`. +The optional frame exclusion clause (FB 6.0) is part of the frame clause and removes rows from the frame after its bounds have been evaluated. It can only be specified together with an explicit `ROWS` or `RANGE` frame. `EXCLUDE NO OTHERS` is the default and keeps the frame unchanged. + +`EXCLUDE CURRENT ROW` removes only the current row from the frame. + +`EXCLUDE GROUP` removes the current row and all its peers. Peers are rows with the same `ORDER BY` values as the current row. If there is no `ORDER BY`, all rows in the partition are peers. + +`EXCLUDE TIES` removes the peers of the current row, but keeps the current row itself. If there is no `ORDER BY`, all other rows in the partition are removed from the frame. + +For example, with duplicate salaries, the following query sums the full ordered partition except the current salary peer group: + +```sql +select + id, + salary, + sum(salary) over ( + order by salary + rows between unbounded preceding and unbounded following + exclude group + ) sum_other_salaries + from employee + order by salary; +``` When `ORDER BY` window clause is used but frame clause is omitted, it defaults to `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. This fact makes the query below to produce "weird" behaviour for the "sum_salary" column. It sums from the partition start to the current key, instead of sum the whole partition. @@ -346,9 +371,9 @@ select | 5 | 10.00 | 3 | | 2 | 12.00 | 1 | -Some window functions discard frames. `ROW_NUMBER`, `LAG` and `LEAD` always work as `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. And `DENSE_RANK`, `RANK`, `PERCENT_RANK` and `CUME_DIST` always work as `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. +Some window functions discard frames and frame exclusions. `ROW_NUMBER`, `LAG` and `LEAD` always work as `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. And `DENSE_RANK`, `RANK`, `PERCENT_RANK` and `CUME_DIST` always work as `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. -`FIRST_VALUE`, `LAST_VALUE` and `NTH_VALUE` respect frames, but the `RANGE` unit works identically as `ROWS`. +`FIRST_VALUE`, `LAST_VALUE` and `NTH_VALUE` respect frames and frame exclusions, but the `RANGE` unit works identically as `ROWS`. ## 6. Named windows (FB 4.0) diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 14fee7d989c..79748d3e0f7 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -9644,12 +9644,14 @@ ValueExprNode* OverNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) const AggNode* aggNode = nodeAs(node->aggExpr); if (node->window && - node->window->extent && aggNode && + (node->window->extent || node->window->exclusion != WindowClause::Exclusion::NO_OTHERS) && (aggNode->getCapabilities() & AggNode::CAP_RESPECTS_WINDOW_FRAME) != AggNode::CAP_RESPECTS_WINDOW_FRAME) { - node->window->extent = WindowClause::FrameExtent::createDefault(dsqlScratch->getPool()); + if (node->window->extent) + node->window->extent = WindowClause::FrameExtent::createDefault(dsqlScratch->getPool()); + node->window->exclusion = WindowClause::Exclusion::NO_OTHERS; } diff --git a/src/dsql/WinNodes.cpp b/src/dsql/WinNodes.cpp index 09e115769a0..3ae85563f9d 100644 --- a/src/dsql/WinNodes.cpp +++ b/src/dsql/WinNodes.cpp @@ -436,7 +436,7 @@ void FirstValueWinNode::aggInit(thread_db* tdbb, Request* request) const dsc* FirstValueWinNode::winPass(thread_db* tdbb, Request* request, SlidingWindow* window) const { - if (!window->moveWithinFrame(-window->getInFrameOffset())) + if (!window->moveToFrameStart()) return nullptr; dsc* desc = EVL_expr(tdbb, request, arg); @@ -497,7 +497,7 @@ void LastValueWinNode::aggInit(thread_db* tdbb, Request* request) const dsc* LastValueWinNode::winPass(thread_db* tdbb, Request* request, SlidingWindow* window) const { - if (!window->moveWithinFrame(window->getFrameEnd() - window->getRecordPosition())) + if (!window->moveToFrameEnd()) return nullptr; dsc* desc = EVL_expr(tdbb, request, arg); @@ -587,11 +587,11 @@ dsc* NthValueWinNode::winPass(thread_db* tdbb, Request* request, SlidingWindow* const SLONG fromPos = desc ? MOV_get_long(tdbb, desc, 0) : FROM_FIRST; if (fromPos == FROM_FIRST) - records -= window->getInFrameOffset() + 1; + --records; else - records = window->getFrameEnd() - window->getRecordPosition() - records + 1; + records = window->getEffectiveFrameSize() - records; - if (!window->moveWithinFrame(records)) + if (!window->moveToFrameOffset(records)) return nullptr; desc = EVL_expr(tdbb, request, arg); diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index 6c1fa4dca7c..20b7367edcc 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -140 shift/reduce conflicts, 10 reduce/reduce conflicts. +138 shift/reduce conflicts, 10 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 2790ed6209c..58c52d59c70 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -8977,6 +8977,13 @@ window_clause : symbol_window_name_opt window_partition_opt order_clause_opt + { + $$ = newNode($1, $2, $3, + static_cast(NULL), WindowClause::Exclusion::NO_OTHERS); + } + | symbol_window_name_opt + window_partition_opt + order_clause_opt window_frame_extent window_frame_exclusion_opt { @@ -8992,9 +8999,7 @@ window_partition_opt %type window_frame_extent window_frame_extent - : /* nothing */ - { $$ = NULL; } - | RANGE + : RANGE { $$ = newNode(WindowClause::FrameExtent::Unit::RANGE); } window_frame($2) { $$ = $2; } diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index adee7a867a0..d1648297484 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -2656,11 +2656,6 @@ void WindowSourceNode::parseWindow(thread_db* tdbb, CompilerScratch* csb) break; case blr_window_win_exclusion: - //// TODO: CORE-5338 - write code for execution. - PAR_error(csb, - Arg::Gds(isc_wish_list) << - Arg::Gds(isc_random) << "window EXCLUDE clause"); - window.exclusion = (WindowClause::Exclusion) csb->csb_blr_reader.getByte(); switch (window.exclusion) diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index c05d68a9ad8..712505e9b0d 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -770,41 +770,43 @@ namespace Jrd { public: SlidingWindow(thread_db* aTdbb, const BaseBufferedStream* aStream, Request* request, - FB_UINT64 aPartitionStart, FB_UINT64 aPartitionEnd, - FB_UINT64 aFrameStart, FB_UINT64 aFrameEnd); + SINT64 aPartitionStart, SINT64 aPartitionEnd, + SINT64 aFrameStart, SINT64 aFrameEnd, + SINT64 aExclusionStart1, SINT64 aExclusionEnd1, + SINT64 aExclusionStart2, SINT64 aExclusionEnd2); ~SlidingWindow(); - FB_UINT64 getPartitionStart() const + SINT64 getPartitionStart() const { return partitionStart; } - FB_UINT64 getPartitionEnd() const + SINT64 getPartitionEnd() const { return partitionEnd; } - FB_UINT64 getPartitionSize() const + SINT64 getPartitionSize() const { return partitionEnd - partitionStart + 1; } - FB_UINT64 getFrameStart() const + SINT64 getFrameStart() const { return frameStart; } - FB_UINT64 getFrameEnd() const + SINT64 getFrameEnd() const { return frameEnd; } - FB_UINT64 getFrameSize() const + SINT64 getFrameSize() const { - return frameEnd - frameStart + 1; + return getEffectiveFrameSize(); } - FB_UINT64 getRecordPosition() const + SINT64 getRecordPosition() const { return savedPosition; } @@ -825,15 +827,37 @@ namespace Jrd bool moveWithinPartition(SINT64 delta); bool moveWithinFrame(SINT64 delta); + bool moveToFrameStart(); + bool moveToFrameEnd(); + bool moveToFrameOffset(SINT64 offset); + SINT64 getEffectiveFrameSize() const; + + private: + bool hasFrame() const + { + return frameStart <= frameEnd && frameStart >= partitionStart && frameEnd <= partitionEnd; + } + + bool isExcluded(SINT64 position) const + { + return (position >= exclusionStart1 && position <= exclusionEnd1) || + (position >= exclusionStart2 && position <= exclusionEnd2); + } + + bool moveToFramePosition(SINT64 position); private: thread_db* tdbb; const BaseBufferedStream* const stream; - FB_UINT64 partitionStart; - FB_UINT64 partitionEnd; - FB_UINT64 frameStart; - FB_UINT64 frameEnd; - FB_UINT64 savedPosition; + SINT64 partitionStart; + SINT64 partitionEnd; + SINT64 frameStart; + SINT64 frameEnd; + SINT64 exclusionStart1; + SINT64 exclusionEnd1; + SINT64 exclusionStart2; + SINT64 exclusionEnd2; + SINT64 savedPosition; bool moved = false; }; @@ -1032,6 +1056,12 @@ namespace Jrd void getFrameValue(thread_db* tdbb, Request* request, const Frame* frame, impure_value_ex* impureValue) const; + Block getPeerBlock(thread_db* tdbb, Request* request, Impure* impure, + SINT64 position) const; + void getExclusionBlocks(thread_db* tdbb, Request* request, Impure* impure, + SINT64 position, Block* exclusion1, Block* exclusion2) const; + bool isExcluded(SINT64 position, const Block& exclusion1, const Block& exclusion2) const; + SINT64 locateFrameRange(thread_db* tdbb, Request* request, Impure* impure, const Frame* frame, const dsc* offsetDesc, SINT64 position) const; diff --git a/src/jrd/recsrc/WindowedStream.cpp b/src/jrd/recsrc/WindowedStream.cpp index 7063e9bdeaf..c5990ecaee0 100644 --- a/src/jrd/recsrc/WindowedStream.cpp +++ b/src/jrd/recsrc/WindowedStream.cpp @@ -645,7 +645,11 @@ bool WindowedStream::WindowStream::internalGetRecord(thread_db* tdbb) const if (!m_next->getRecord(tdbb)) fb_assert(false); - if (impure->rangePending > 0) + Block exclusion1, exclusion2; + exclusion1.invalidate(); + exclusion2.invalidate(); + + if (impure->rangePending > 0 && m_exclusion == Exclusion::NO_OTHERS) --impure->rangePending; else { @@ -758,13 +762,14 @@ bool WindowedStream::WindowStream::internalGetRecord(thread_db* tdbb) const return false; } - if ((m_frameExtent->frame1->bound == Frame::Bound::PRECEDING && !m_frameExtent->frame1->value && - m_frameExtent->frame2->bound == Frame::Bound::FOLLOWING && !m_frameExtent->frame2->value) || - (m_frameExtent->unit == FrameExtent::Unit::RANGE && !m_order)) + if (m_exclusion == Exclusion::NO_OTHERS && + ((m_frameExtent->frame1->bound == Frame::Bound::PRECEDING && !m_frameExtent->frame1->value && + m_frameExtent->frame2->bound == Frame::Bound::FOLLOWING && !m_frameExtent->frame2->value) || + (m_frameExtent->unit == FrameExtent::Unit::RANGE && !m_order))) { impure->rangePending = MAX(0, impure->windowBlock.endPosition - position); } - else if (m_frameExtent->unit == FrameExtent::Unit::RANGE) + else if (m_exclusion == Exclusion::NO_OTHERS && m_frameExtent->unit == FrameExtent::Unit::RANGE) { SINT64 rangePos = position; cacheValues(tdbb, request, &m_order->expressions, impure->orderValues, @@ -792,10 +797,12 @@ bool WindowedStream::WindowStream::internalGetRecord(thread_db* tdbb) const //// TODO: There is no need to pass record by record when m_aggSources.isEmpty() - if (!impure->windowBlock.isValid() || + const bool invalidFrame = !impure->windowBlock.isValid() || impure->windowBlock.endPosition < impure->windowBlock.startPosition || impure->windowBlock.startPosition > impure->partitionBlock.endPosition || - impure->windowBlock.endPosition < impure->partitionBlock.startPosition) + impure->windowBlock.endPosition < impure->partitionBlock.startPosition; + + if (invalidFrame) { if (position == 0 || impure->windowBlock.isValid()) { @@ -811,53 +818,83 @@ bool WindowedStream::WindowStream::internalGetRecord(thread_db* tdbb) const impure->windowBlock.endPosition = MIN(impure->windowBlock.endPosition, impure->partitionBlock.endPosition); - // If possible, reuse the last window aggregation. - // - // This may be incompatible with some function like LIST, but currently LIST cannot - // be used in ordered windows anyway. - - if (!lastWindow.isValid() || - impure->windowBlock.startPosition > lastWindow.startPosition || - impure->windowBlock.endPosition < lastWindow.endPosition) + if (m_exclusion != Exclusion::NO_OTHERS) { + getExclusionBlocks(tdbb, request, impure, position, &exclusion1, &exclusion2); + aggInit(tdbb, request, m_windowMap); m_next->locate(tdbb, impure->windowBlock.startPosition); + + SINT64 aggPos = impure->windowBlock.startPosition; + + while (aggPos <= impure->windowBlock.endPosition) + { + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + if (!isExcluded(aggPos, exclusion1, exclusion2)) + aggPass(tdbb, request, m_aggSources, m_aggTargets); + + ++aggPos; + } + + aggExecute(tdbb, request, m_aggSources, m_aggTargets); + + m_next->locate(tdbb, position); + + if (!m_next->getRecord(tdbb)) + fb_assert(false); } else { - if (impure->windowBlock.startPosition < lastWindow.startPosition) + // If possible, reuse the last window aggregation. + // + // This may be incompatible with some function like LIST, but currently LIST cannot + // be used in ordered windows anyway. + + if (!lastWindow.isValid() || + impure->windowBlock.startPosition > lastWindow.startPosition || + impure->windowBlock.endPosition < lastWindow.endPosition) { + aggInit(tdbb, request, m_windowMap); m_next->locate(tdbb, impure->windowBlock.startPosition); - SINT64 pending = lastWindow.startPosition - impure->windowBlock.startPosition; - - while (pending-- > 0) + } + else + { + if (impure->windowBlock.startPosition < lastWindow.startPosition) { - if (!m_next->getRecord(tdbb)) - fb_assert(false); + m_next->locate(tdbb, impure->windowBlock.startPosition); + SINT64 pending = lastWindow.startPosition - impure->windowBlock.startPosition; - aggPass(tdbb, request, m_aggSources, m_aggTargets); + while (pending-- > 0) + { + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + aggPass(tdbb, request, m_aggSources, m_aggTargets); + } } - } - m_next->locate(tdbb, lastWindow.endPosition + 1); - } + m_next->locate(tdbb, lastWindow.endPosition + 1); + } - SINT64 aggPos = (SINT64) m_next->getPosition(request); + SINT64 aggPos = (SINT64) m_next->getPosition(request); - while (aggPos++ <= impure->windowBlock.endPosition) - { - if (!m_next->getRecord(tdbb)) - fb_assert(false); + while (aggPos++ <= impure->windowBlock.endPosition) + { + if (!m_next->getRecord(tdbb)) + fb_assert(false); - aggPass(tdbb, request, m_aggSources, m_aggTargets); - } + aggPass(tdbb, request, m_aggSources, m_aggTargets); + } - aggExecute(tdbb, request, m_aggSources, m_aggTargets); + aggExecute(tdbb, request, m_aggSources, m_aggTargets); - m_next->locate(tdbb, position); + m_next->locate(tdbb, position); - if (!m_next->getRecord(tdbb)) - fb_assert(false); + if (!m_next->getRecord(tdbb)) + fb_assert(false); + } } } @@ -867,7 +904,9 @@ bool WindowedStream::WindowStream::internalGetRecord(thread_db* tdbb) const { SlidingWindow window(tdbb, m_next, request, impure->partitionBlock.startPosition, impure->partitionBlock.endPosition, - impure->windowBlock.startPosition, impure->windowBlock.endPosition); + impure->windowBlock.startPosition, impure->windowBlock.endPosition, + exclusion1.startPosition, exclusion1.endPosition, + exclusion2.startPosition, exclusion2.endPosition); dsc* desc; const NestConst* const sourceEnd = m_winPassSources.end(); @@ -992,6 +1031,120 @@ void WindowedStream::WindowStream::getFrameValue(thread_db* tdbb, Request* reque } } +WindowedStream::WindowStream::Block WindowedStream::WindowStream::getPeerBlock( + thread_db* tdbb, Request* request, Impure* impure, SINT64 position) const +{ + Block peerBlock; + peerBlock.startPosition = peerBlock.endPosition = position; + + if (!m_order) + { + peerBlock.startPosition = impure->partitionBlock.startPosition; + peerBlock.endPosition = impure->partitionBlock.endPosition; + return peerBlock; + } + + cacheValues(tdbb, request, &m_order->expressions, impure->orderValues, DummyAdjustFunctor()); + + while (peerBlock.startPosition > impure->partitionBlock.startPosition) + { + m_next->locate(tdbb, peerBlock.startPosition - 1); + + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + if (lookForChange(tdbb, request, &m_order->expressions, m_order, impure->orderValues)) + break; + + --peerBlock.startPosition; + } + + m_next->locate(tdbb, position); + + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + while (peerBlock.endPosition < impure->partitionBlock.endPosition) + { + m_next->locate(tdbb, peerBlock.endPosition + 1); + + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + if (lookForChange(tdbb, request, &m_order->expressions, m_order, impure->orderValues)) + break; + + ++peerBlock.endPosition; + } + + m_next->locate(tdbb, position); + + if (!m_next->getRecord(tdbb)) + fb_assert(false); + + return peerBlock; +} + +void WindowedStream::WindowStream::getExclusionBlocks(thread_db* tdbb, Request* request, + Impure* impure, SINT64 position, Block* exclusion1, Block* exclusion2) const +{ + exclusion1->invalidate(); + exclusion2->invalidate(); + + if (m_exclusion == Exclusion::NO_OTHERS || + !impure->windowBlock.isValid() || + impure->windowBlock.startPosition > impure->windowBlock.endPosition) + { + return; + } + + auto setBlock = [&] (Block* block, SINT64 startPosition, SINT64 endPosition) + { + startPosition = MAX(startPosition, impure->windowBlock.startPosition); + endPosition = MIN(endPosition, impure->windowBlock.endPosition); + + if (startPosition <= endPosition) + { + block->startPosition = startPosition; + block->endPosition = endPosition; + } + }; + + switch (m_exclusion) + { + case Exclusion::CURRENT_ROW: + setBlock(exclusion1, position, position); + break; + + case Exclusion::GROUP: + { + const Block peerBlock = getPeerBlock(tdbb, request, impure, position); + setBlock(exclusion1, peerBlock.startPosition, peerBlock.endPosition); + break; + } + + case Exclusion::TIES: + { + const Block peerBlock = getPeerBlock(tdbb, request, impure, position); + setBlock(exclusion1, peerBlock.startPosition, position - 1); + setBlock(exclusion2, position + 1, peerBlock.endPosition); + break; + } + + case Exclusion::NO_OTHERS: + break; + } +} + +bool WindowedStream::WindowStream::isExcluded(SINT64 position, const Block& exclusion1, + const Block& exclusion2) const +{ + return (exclusion1.isValid() && + position >= exclusion1.startPosition && position <= exclusion1.endPosition) || + (exclusion2.isValid() && + position >= exclusion2.startPosition && position <= exclusion2.endPosition); +} + SINT64 WindowedStream::WindowStream::locateFrameRange(thread_db* tdbb, Request* request, Impure* impure, const Frame* frame, const dsc* offsetDesc, SINT64 position) const { @@ -1109,16 +1262,22 @@ SINT64 WindowedStream::WindowStream::locateFrameRange(thread_db* tdbb, Request* SlidingWindow::SlidingWindow(thread_db* aTdbb, const BaseBufferedStream* aStream, Request* request, - FB_UINT64 aPartitionStart, FB_UINT64 aPartitionEnd, - FB_UINT64 aFrameStart, FB_UINT64 aFrameEnd) + SINT64 aPartitionStart, SINT64 aPartitionEnd, + SINT64 aFrameStart, SINT64 aFrameEnd, + SINT64 aExclusionStart1, SINT64 aExclusionEnd1, + SINT64 aExclusionStart2, SINT64 aExclusionEnd2) : tdbb(aTdbb), // Note: instantiate the class only as local variable stream(aStream), partitionStart(aPartitionStart), partitionEnd(aPartitionEnd), frameStart(aFrameStart), - frameEnd(aFrameEnd) + frameEnd(aFrameEnd), + exclusionStart1(aExclusionStart1), + exclusionEnd1(aExclusionEnd1), + exclusionStart2(aExclusionStart2), + exclusionEnd2(aExclusionEnd2) { - savedPosition = stream->getPosition(request) - 1; + savedPosition = (SINT64) stream->getPosition(request) - 1; } SlidingWindow::~SlidingWindow() @@ -1153,13 +1312,88 @@ bool SlidingWindow::moveWithinPartition(SINT64 delta) return true; } +bool SlidingWindow::moveToFramePosition(SINT64 position) +{ + if (!hasFrame() || position < frameStart || position > frameEnd || isExcluded(position)) + return false; + + return moveWithinPartition(position - savedPosition); +} + // Move in the window without pass frame boundaries. bool SlidingWindow::moveWithinFrame(SINT64 delta) { const auto newPosition = savedPosition + delta; - if (newPosition < frameStart || newPosition > frameEnd) + if (!hasFrame() || newPosition < frameStart || newPosition > frameEnd || isExcluded(newPosition)) return false; return moveWithinPartition(delta); } + +bool SlidingWindow::moveToFrameStart() +{ + if (!hasFrame()) + return false; + + for (SINT64 position = frameStart; position <= frameEnd; ++position) + { + if (!isExcluded(position)) + return moveToFramePosition(position); + } + + return false; +} + +bool SlidingWindow::moveToFrameEnd() +{ + if (!hasFrame()) + return false; + + for (SINT64 position = frameEnd; position >= frameStart; --position) + { + if (!isExcluded(position)) + return moveToFramePosition(position); + } + + return false; +} + +bool SlidingWindow::moveToFrameOffset(SINT64 offset) +{ + if (!hasFrame() || offset < 0) + return false; + + for (SINT64 position = frameStart; position <= frameEnd; ++position) + { + if (isExcluded(position)) + continue; + + if (offset-- == 0) + return moveToFramePosition(position); + } + + return false; +} + +SINT64 SlidingWindow::getEffectiveFrameSize() const +{ + if (!hasFrame()) + return 0; + + SINT64 size = frameEnd - frameStart + 1; + + const auto subtractBlock = [&] (SINT64 start, SINT64 end) + { + start = MAX(start, frameStart); + end = MIN(end, frameEnd); + + if (start <= end) + size -= end - start + 1; + }; + + subtractBlock(exclusionStart1, exclusionEnd1); + subtractBlock(exclusionStart2, exclusionEnd2); + + return size; +}