diff --git a/server/spec/models/detection_model.jest.ts b/server/spec/models/detection_model.jest.ts index e660696..da07aab 100644 --- a/server/spec/models/detection_model.jest.ts +++ b/server/spec/models/detection_model.jest.ts @@ -1,5 +1,5 @@ import { TEST_ANALYTIC_UNIT_ID } from '../utils_for_tests/analytic_units'; -import { buildSpans, insertSpans, clearSpansDB, convertSpansToOptions } from '../utils_for_tests/detection_spans'; +import { insertSpans, clearSpansDB, convertSpansToOptions } from '../utils_for_tests/detection_spans'; import * as Detection from '../../src/models/detection_model'; @@ -67,9 +67,3 @@ describe('getIntersectedSpans', () => { }); }); -describe('getSpanBorders', () => { - it('should sort and find span borders', () => { - const borders = Detection.getSpanBorders(buildSpans(INITIAL_SPANS_CONFIGS)); - expect(borders).toEqual([1, 3, 3, 4]); - }); -}); diff --git a/server/spec/utils/spans.jest.ts b/server/spec/utils/spans.jest.ts index efc2cdb..40900bf 100644 --- a/server/spec/utils/spans.jest.ts +++ b/server/spec/utils/spans.jest.ts @@ -1,35 +1,51 @@ -import { getNonIntersectedSpans } from '../../src/utils/spans'; +import { cutSpanWithSpans } from '../../src/utils/spans'; import 'jest'; -describe('getNonIntersectedSpans', function() { - - let spanBorders = [3, 5, 6, 8, 10, 20]; - - it('functional test', function() { - expect(getNonIntersectedSpans(4, 11, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(5, 11, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(4, 10, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(5, 10, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(4, 20, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(4, 21, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}, {from: 20, to: 21}]); - expect(getNonIntersectedSpans(2, 20, spanBorders)).toEqual([{from: 2, to: 3}, {from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(2, 21, spanBorders)).toEqual([{from: 2, to: 3}, {from: 5, to: 6}, {from: 8, to: 10}, {from: 20, to: 21}]); - expect(getNonIntersectedSpans(3, 11, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(3, 20, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(3, 20, spanBorders)).toEqual([{from: 5, to: 6}, {from: 8, to: 10}]); - expect(getNonIntersectedSpans(4, 7, [3, 5, 6, 8])).toEqual([{from: 5, to: 6}]); + +function cutSpan(from: number, to: number, cuts: [number, number][]): [number, number][] { + return cutSpanWithSpans( + { from: from, to: to }, + cuts.map(([from, to]) => ({ from, to })) + ).map(({ from, to }) => [from, to]); +} + +describe('cutSpanWithSpans', function() { + + it('should find spans in simple non-intersected borders', function() { + let cutSpans = [[3, 5], [6, 8], [10, 20]] as [number, number][]; + + expect(cutSpan(4, 11, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(5, 11, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(4, 10, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(5, 10, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(4, 20, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(4, 21, cutSpans)).toEqual([[5, 6], [8, 10], [20, 21]]); + expect(cutSpan(2, 20, cutSpans)).toEqual([[2, 3], [5, 6], [8, 10]]); + expect(cutSpan(2, 21, cutSpans)).toEqual([[2, 3], [5, 6], [8, 10], [20, 21]]); + expect(cutSpan(3, 11, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(3, 20, cutSpans)).toEqual([[5, 6], [8, 10]]); + expect(cutSpan(4, 7, [[3, 5], [6, 8]])).toEqual([[5, 6]]); + }); + + it('should handle empty input spans list case', function() { + expect(cutSpan(4, 10, [])).toEqual([[4, 10]]); + }); + + it('should handle case when from and to are inside of one big span', function() { + expect(cutSpan(4, 10, [[1, 20]])).toEqual([]); + expect(cutSpan(4, 10, [[1, 10]])).toEqual([]); + expect(cutSpan(4, 10, [[4, 20]])).toEqual([]); + expect(cutSpan(4, 10, [[4, 10]])).toEqual([]); }); - it('empty borders list', function() { - expect(getNonIntersectedSpans(4, 10, [])).toEqual([]); + it('should be ready to get not-sorted cuts', function() { + expect(cutSpan(0, 20, [[3, 5], [1, 2]])).toEqual([[0, 1], [2, 3], [5, 20]]); + expect(cutSpan(0, 20, [[3, 5], [1, 2], [0.1, 0.5]])).toEqual([[0, 0.1], [0.5, 1], [2, 3], [5, 20]]); }); - it('all in span', function() { - expect(getNonIntersectedSpans(4, 10, [1, 20])).toEqual([]); - expect(getNonIntersectedSpans(4, 10, [1, 10])).toEqual([]); - expect(getNonIntersectedSpans(4, 10, [4, 20])).toEqual([]); - expect(getNonIntersectedSpans(4, 10, [4, 10])).toEqual([]); + it('should be ready to get overlayed cuts', function() { + expect(cutSpan(0, 20, [[3, 5], [4, 10]])).toEqual([[0,3], [10, 20]]); }); }); diff --git a/server/src/controllers/analytics_controller.ts b/server/src/controllers/analytics_controller.ts index f1a6e74..0e12c28 100644 --- a/server/src/controllers/analytics_controller.ts +++ b/server/src/controllers/analytics_controller.ts @@ -10,7 +10,7 @@ import { AlertService } from '../services/alert_service'; import { HASTIC_API_KEY } from '../config'; import { DataPuller } from '../services/data_puller'; import { getGrafanaUrl } from '../utils/grafana'; -import { getNonIntersectedSpans } from '../utils/spans'; +import { cutSpanWithSpans } from '../utils/spans'; import { queryByMetric, GrafanaUnavailable, DatasourceUnavailable } from 'grafana-datasource-kit'; @@ -564,9 +564,7 @@ export async function getDetectionSpans( } } - const spanBorders = Detection.getSpanBorders(readySpans); - - let newDetectionSpans = getNonIntersectedSpans(from, to, spanBorders); + let newDetectionSpans = cutSpanWithSpans({ from, to }, readySpans); if(newDetectionSpans.length === 0) { return [ new Detection.DetectionSpan(analyticUnitId, from, to, Detection.DetectionStatus.READY) ]; } diff --git a/server/src/models/detection_model.ts b/server/src/models/detection_model.ts index cf1d592..4b2348c 100644 --- a/server/src/models/detection_model.ts +++ b/server/src/models/detection_model.ts @@ -2,7 +2,6 @@ import { AnalyticUnitId } from './analytic_units'; import { Collection, makeDBQ } from '../services/data_service'; import * as _ from 'lodash'; -import { getNonIntersectedSpans } from '../utils/spans'; let db = makeDBQ(Collection.DETECTION_SPANS); @@ -137,22 +136,6 @@ export async function insertSpan(span: DetectionSpan) { return db.insertOne(spanToInsert); } -/** - * Sorts spans by `from` field and @returns an array of their borders - */ -// TODO: remove after getNonIntersectedSpans refactoring -export function getSpanBorders(spans: DetectionSpan[]): number[] { - let spanBorders: number[] = []; - - _.sortBy(spans.map(span => span.toObject()), 'from') - .forEach(span => { - spanBorders.push(span.from); - spanBorders.push(span.to); - }); - - return spanBorders; -} - export function clearSpans(analyticUnitId: AnalyticUnitId) { return db.removeMany({ analyticUnitId }); } diff --git a/server/src/utils/spans.ts b/server/src/utils/spans.ts index 7707bdf..d3a6eeb 100644 --- a/server/src/utils/spans.ts +++ b/server/src/utils/spans.ts @@ -2,53 +2,62 @@ import * as _ from 'lodash'; + export declare type Span = { from: number, to: number } -// TODO: move from utils and refactor -export function getNonIntersectedSpans(from: number, to: number, spanBorders: number[]): Span[] { - // spanBorders array must be sorted ascending - let isFromProcessed = false; - let alreadyDetected = false; - let startDetectionRange = null; - let result: Span[] = []; - - for(var border of spanBorders) { - if(!isFromProcessed && border >= from) { - isFromProcessed = true; - if(border === from) { - if(alreadyDetected) { - startDetectionRange = from; - } - } else { - if(!alreadyDetected) { - startDetectionRange = from; - } - } - } +// TODO: move from utils and use generator +/** + * + * @param inputSpan a big span which we will cut + * @param cutSpans spans which to cut the inputSpan. Spans can overlay. + * + * @returns array of spans which are holes + */ +export function cutSpanWithSpans(inputSpan: Span, cutSpans: Span[]): Span[] { + if(cutSpans.length === 0) { + return [inputSpan]; + } - if(border >= to) { - if(!alreadyDetected) { - result.push({ from: startDetectionRange, to }); + // we sort and merge out cuts to normalize it + cutSpans = _.sortBy(cutSpans, s => s.from); + var mergedSortedCuts =_.reduce(cutSpans, + ((acc: Span[], s: Span) => { + if(acc.length === 0) return [s]; + let last = acc[acc.length - 1]; + if(s.to <= last.to) return acc; + if(s.from <= last.to) { + last.to = s.to; + return acc; } - break; + acc.push(s); + return acc; + }), [] + ); + + // this is what we get if we cut `mergedSortedCuts` from (-Infinity, Infinity) + var holes = mergedSortedCuts.map((cut, i) => { + let from = -Infinity; + let to = cutSpans[0].from; + if(i > 0) { + from = mergedSortedCuts[i - 1].to; + to = cut.from; } - - if(alreadyDetected) { //end of already detected region, start point for new detection - startDetectionRange = border; - } else { //end of new detection region - if(startDetectionRange !== null) { - result.push({ from: startDetectionRange, to: border}); - } + return { from, to } + }).concat({ + from: mergedSortedCuts[mergedSortedCuts.length - 1].to, + to: Infinity + }); + + return _(holes).map(c => { + if(c.to <= inputSpan.from) return undefined; + if(inputSpan.to <= c.from) return undefined; + return { + from: Math.max(c.from, inputSpan.from), + to: Math.min(c.to, inputSpan.to), } - alreadyDetected = !alreadyDetected; - } - - if(border < to) { - result.push({ from: startDetectionRange, to }); - } + }).compact().value(); - return result; }