You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.4 KiB
185 lines
5.4 KiB
//TODO: move this code to span model |
|
|
|
import * as _ from 'lodash'; |
|
|
|
|
|
export declare type Segment = { |
|
readonly from: number, |
|
readonly to: number |
|
} |
|
|
|
export class IntegerSegment { |
|
readonly from: number; |
|
readonly to: number; |
|
|
|
constructor(from: number, to: number) { |
|
if(!(Number.isInteger(from) || !Number.isFinite(from))) { |
|
throw new Error(`From should be an Integer or Infinity, but got ${from}`); |
|
} |
|
if(!(Number.isInteger(to) || !Number.isFinite(to))) { |
|
throw new Error(`To should be an Integer or Infinity, but got ${from}`); |
|
} |
|
|
|
let l = IntegerSegment.lengthBetweenPoints(from, to); |
|
if(l < 1) { |
|
throw new Error( |
|
`Length of segment is less than 1: [${from}, ${to}]. |
|
It's not possible for IntegerSegment` |
|
); |
|
} |
|
this.from = from; |
|
this.to = to; |
|
} |
|
|
|
get length(): number { |
|
return IntegerSegment.lengthBetweenPoints(this.from, this.to); |
|
} |
|
|
|
insersect(segment: IntegerSegment): IntegerSegment | undefined { |
|
let from = Math.max(this.from, segment.from); |
|
let to = Math.min(this.to, segment.to); |
|
if(IntegerSegment.lengthBetweenPoints(from, to) >= 1) { |
|
return new IntegerSegment(from, to); |
|
} |
|
return undefined; |
|
} |
|
|
|
toString(): string { |
|
return `[${this.from}, ${this.to}]`; |
|
} |
|
|
|
static lengthBetweenPoints(from: number, to: number): number { |
|
let l = to - from + 1; // because [x, x] has length 1 |
|
if(isNaN(l)) { // when [Infinity, Infinity] or [-Infinity, -Infinity] |
|
return 0; |
|
} else { |
|
return Math.max(l, 0); // becase [x, x - 1] we consider as zero length |
|
} |
|
} |
|
} |
|
|
|
export class IntegerSegmentsSet { |
|
|
|
private _segments: IntegerSegment[]; |
|
|
|
constructor(segments: IntegerSegment[], noramlized: boolean = false) { |
|
this._segments = segments; |
|
if(noramlized !== true) { |
|
this._normalize(); |
|
} |
|
} |
|
|
|
private _normalize() { |
|
if(this._segments.length === 0) { |
|
return; |
|
} |
|
let sortedSegments = _.sortBy(this._segments, s => s.from); |
|
let lastFrom = sortedSegments[0].from; |
|
let lastTo = sortedSegments[0].to; |
|
let mergedSegments: IntegerSegment[] = []; |
|
for(let i = 1; i < sortedSegments.length; i++) { |
|
let currentSegment = sortedSegments[i]; |
|
if(lastTo + 1 >= currentSegment.from) { // because [a, x], [x + 1, b] is [a, b] |
|
lastTo = Math.max(currentSegment.to, lastTo); // we can be inside previous |
|
continue; |
|
} |
|
mergedSegments.push(new IntegerSegment(lastFrom, lastTo)); |
|
lastFrom = currentSegment.from; |
|
lastTo = currentSegment.to; |
|
} |
|
mergedSegments.push(new IntegerSegment(lastFrom, lastTo)); |
|
this._segments = mergedSegments; |
|
} |
|
|
|
get segments(): IntegerSegment[] { |
|
return this._segments; |
|
} |
|
|
|
inversed(): IntegerSegmentsSet { |
|
var invertedSegments: IntegerSegment[] = []; |
|
if(this._segments.length === 0) { |
|
invertedSegments = [new IntegerSegment(-Infinity, Infinity)]; |
|
} else { |
|
let push = (f: number, t: number) => { |
|
if(IntegerSegment.lengthBetweenPoints(f, t) > 0) { |
|
invertedSegments.push(new IntegerSegment(f, t)); |
|
} |
|
} |
|
_.reduce(this._segments, (prev: IntegerSegment | null, s: IntegerSegment) => { |
|
if(prev === null) { |
|
push(-Infinity, s.from - 1); |
|
} else { |
|
push(prev.to + 1, s.from - 1); |
|
} |
|
return s; |
|
}, null); |
|
push(this._segments[this._segments.length - 1].to + 1, Infinity); |
|
} |
|
return new IntegerSegmentsSet(invertedSegments, true); |
|
} |
|
|
|
intersect(other: IntegerSegmentsSet): IntegerSegmentsSet { |
|
let result: IntegerSegment[] = []; |
|
|
|
if(this._segments.length === 0 || other.segments.length === 0) { |
|
return new IntegerSegmentsSet([], true); |
|
} |
|
|
|
let currentSegmentIndex = 0; |
|
let withSegmentIndex = 0; |
|
|
|
do { |
|
let currentSegemet = this.segments[currentSegmentIndex]; |
|
let withSegment = other.segments[withSegmentIndex]; |
|
if(currentSegemet.to < withSegment.from) { |
|
currentSegmentIndex++; |
|
continue; |
|
} |
|
if(withSegment.to < currentSegemet.from) { |
|
withSegmentIndex++; |
|
continue; |
|
} |
|
let segmentsIntersection = currentSegemet.insersect(withSegment); |
|
if(segmentsIntersection === undefined) { |
|
throw new Error( |
|
`Impossible condition, segments ${currentSegemet} and ${withSegment} don't interset` |
|
) |
|
} |
|
result.push(segmentsIntersection); |
|
|
|
if(currentSegemet.to < withSegment.to) { |
|
currentSegmentIndex++; |
|
} else { |
|
withSegmentIndex++; |
|
} |
|
} while ( |
|
currentSegmentIndex < this._segments.length && |
|
withSegmentIndex < other.segments.length |
|
) |
|
|
|
return new IntegerSegmentsSet(result, true); |
|
} |
|
|
|
sub(other: IntegerSegmentsSet): IntegerSegmentsSet { |
|
let inversed = other.inversed(); |
|
return this.intersect(inversed); |
|
} |
|
|
|
} |
|
|
|
// TODO: move from utils and use generator |
|
/** |
|
* |
|
* @param inputSegment a big segment which we will cut |
|
* @param cutSegments segments to cut the inputSegment. Segments can overlay. |
|
* |
|
* @returns array of segments remain after cut |
|
*/ |
|
export function cutSegmentWithSegments(inputSegment: Segment, cutSegments: Segment[]): Segment[] { |
|
let setA = new IntegerSegmentsSet([new IntegerSegment(inputSegment.from, inputSegment.to)]); |
|
let setB = new IntegerSegmentsSet(cutSegments.map( |
|
s => new IntegerSegment(s.from, s.to) |
|
)); |
|
let setResult = setA.sub(setB); |
|
return setResult.segments.map(s => ({ from: s.from, to: s.to })); |
|
}
|
|
|