UNPKG

ts-math-utils

Version:

Math-based objects not included in JS, built in TS.

371 lines (339 loc) 17 kB
import { IInterval, Interval, IntervalNumber, NumericValue } from './interval'; /** * Represents interval set options. * mergeAddedInterval is optional and defaults to true. * mergeAddedInterval, when true, is used to merge overlapping Intervals when adding a new interval. */ export class IntervalSetOptions { mergeAddedInterval: boolean = true; } /** * Represents a set of intervals. * intervals is optional and defaults to an empty array. * mergeAddedInterval is optional and defaults to true. * mergeAddedInterval, when true, is used to merge overlapping Intervals when adding a new interval. * @example * const intervalSet: IntervalSet = new IntervalSet({ intervals: [new Interval({ a: new IntervalNumber(1), b: new IntervalNumber(10, false) }), new Interval({ a: new IntervalNumber(5), b: new IntervalNumber(15, false) })], options: { mergeAddedInterval: true }); */ export class IntervalSet { private _intervals: Interval[] = []; private _mergeAddedInterval: boolean = true; /** * Creates a new IntervalSet object. * intervalSet is optional and defaults to undefined. * @param intervalSet - The interval set object. * @example * const intervalSet: IntervalSet = new IntervalSet({ intervals: [new Interval({ a: new IntervalNumber(1), b: new IntervalNumber(10, false) }), new Interval({ a: new IntervalNumber(5), b: new IntervalNumber(15, false) })], options: { mergeAddedInterval: true }); */ constructor(intervalSet?: { intervals?: (IInterval | string)[], options?: IntervalSetOptions }) { if (intervalSet?.intervals) { for (const interval of intervalSet.intervals) { const intervalObject = new Interval(interval); this._intervals.push(intervalObject); } } this.mergeAddedInterval = intervalSet?.options?.mergeAddedInterval ?? this._mergeAddedInterval; } /** * Returns a copy of the intervals in the interval set. */ get intervals(): Interval[] { return this._intervals.map((r: Interval): Interval => new Interval(r)); } /** * Gets or sets the mergeAddedInterval option. * mergeAddedInterval, when true, is used to merge overlapping Intervals when adding a new interval. * Setting this option to true will merge the intervals in the interval set. */ get mergeAddedInterval(): boolean { return this._mergeAddedInterval; } set mergeAddedInterval(value: boolean) { this._mergeAddedInterval = value; if (this.mergeAddedInterval) { IntervalSet.mergeIntervals(this._intervals); } } /** * Sorts the intervals in the interval set, ascending based on the minimum value of each interval and isClosed is before !isClosed. */ static sort(intervals: Interval[]): void { intervals.sort((a: Interval, b: Interval): number => { if (a.min.number < b.min.number) { return -1; } else if (a.min.number > b.min.number) { return 1; } else { if (a.min.isClosed && !b.min.isClosed) { return -1; } else if (!a.min.isClosed && b.min.isClosed) { return 1; } else { return 0; } } }); } /** * Adds an interval to the interval set. * @param interval - The interval object or string representation. * @example * intervalSet.addInterval(new Interval({ a: new IntervalNumber(1), b: new IntervalNumber(10, false) })); * @example * intervalSet.addInterval('[1, 10)'); */ addInterval(interval: IInterval | string): void { const intervalObject = new Interval(interval); this._intervals.push(intervalObject); if (this._mergeAddedInterval) { IntervalSet.mergeIntervals(this._intervals); } } /** * Removes an interval from the interval set. * @param interval - The interval object or string representation. * @example * intervalSet.removeInterval(new Interval({ a: new IntervalNumber(1), b: new IntervalNumber(10, false) })); * @example * intervalSet.removeInterval('[1, 10)'); */ removeInterval(interval: IInterval | string): void { const intervalObject = new Interval(interval); this._intervals = this._intervals.filter((r: Interval): boolean => r.toString() !== intervalObject.toString()); } /** * Remove interval by name. * @param name - The name of the interval. * @example * intervalSet.removeIntervalByName('Interval 1'); */ removeIntervalByName(name: string): void { this._intervals = this._intervals.filter((r: Interval): boolean => r.name !== name); } /** * Removes all intervals from the interval set. */ clear(): void { this._intervals = []; } /** * Merge overlapping intervals in the interval set. */ private static mergeIntervals(intervals: Interval[]): void { IntervalSet.sort(intervals); let i = 0; while (i < intervals.length - 1) { const current = intervals[i]; const next = intervals[i + 1]; // Check if intervals overlap or are adjacent if ( current.overlaps(next) || (current.max.number === next.min.number && (current.max.isClosed || next.min.isClosed)) ) { // Merge intervals current.min = new IntervalNumber( safeMin(current.min.number, next.min.number), current.min.isClosed || next.min.isClosed ); current.max = new IntervalNumber( safeMax(current.max.number, next.max.number), current.max.isClosed || next.max.isClosed ); intervals.splice(i + 1, 1); // Remove the merged interval } else { i++; } } } /** * Returns the gaps between the intervals in the passed in interval.. * If interval is provided, the gaps are calculated based on the given interval. * If interval is not provided, the gaps are calculated based on the intervals in the interval set. * @param interval - The interval object or string representation. * @returns An array of Interval objects representing the gaps. */ getIntervalGaps(interval?: IInterval | string): Interval[] { const intervalObject = interval ? new Interval(interval) : undefined; const intervalsCopy = this._intervals.map((r) => new Interval(r)); if (!this._mergeAddedInterval) { IntervalSet.mergeIntervals(intervalsCopy); } IntervalSet.sort(intervalsCopy); return intervalObject ? this._getGapsForInterval(intervalObject, intervalsCopy) : this._getGapsForSet(intervalsCopy); } private _getGapsForInterval(interval: Interval, intervals: Interval[]): Interval[] { const gaps: Interval[] = []; const isContained = intervals.some((r) => r.contains(interval)); if (isContained) { return gaps; // No gaps if the interval is contained within existing intervals } const overlappingIntervals = intervals.filter((r) => interval.overlaps(r)); if (overlappingIntervals.length === 0) { return [interval]; } if (!overlappingIntervals[0].containsMin(interval.min)) { gaps.push(new Interval({ a: interval.min, b: new IntervalNumber(overlappingIntervals[0].min.number, !overlappingIntervals[0].min.isClosed), })); } for (let i = 0; i < overlappingIntervals.length - 1; i++) { const current = overlappingIntervals[i]; const next = overlappingIntervals[i + 1]; gaps.push(new Interval({ a: new IntervalNumber(current.max.number, !current.max.isClosed), b: new IntervalNumber(next.min.number, !next.min.isClosed), })); } if (!overlappingIntervals[overlappingIntervals.length - 1].containsMax(interval.max)) { gaps.push(new Interval({ a: new IntervalNumber(overlappingIntervals[overlappingIntervals.length - 1].max.number, !overlappingIntervals[overlappingIntervals.length - 1].max.isClosed), b: interval.max, })); } return gaps; } private _getGapsForSet(intervals: Interval[]): Interval[] { const gaps: Interval[] = []; if (intervals.length < 2) { return gaps; } if (intervals.length > 1) { for (let i = 0; i < intervals.length - 1; i++) { const current = intervals[i]; const next = intervals[i + 1]; // if they don't overlap, create a gap interval if (!current.overlaps(next) && (current.max.number !== next.min.number || (!current.max.isClosed && !next.min.isClosed))) { // Create a gap interval gaps.push(new Interval({ a: new IntervalNumber(current.max.number, !current.max.isClosed), b: new IntervalNumber(next.min.number, !next.min.isClosed), })); } } } return gaps; } /** * Create an interval gap in the interval set. * @param interval - The interval object or string representation. * @example * intervalSet.createIntervalGap(new Interval({ a: new IntervalNumber(5), b: new IntervalNumber(10, false) })); */ createIntervalGap(interval: IInterval | string): void { const intervalObject = new Interval(interval); // use internal intervals array to properly update the intervals const overlappingIntervals: Interval[] = this._intervals.filter((r: Interval): boolean => r.overlaps(intervalObject)); if (overlappingIntervals.length > 0) { const overlappingIntervalSet = new IntervalSet({ intervals: overlappingIntervals, options: { mergeAddedInterval: false } }); // get the overlapping intervals that contain the given interval's max but not min, and update their max const minIntervals: Interval[] = this._intervals.filter((r: Interval): boolean => intervalObject.containsMax(r.max) && !intervalObject.containsMin(r.min)); for (const minInterval of minIntervals) { minInterval.max = new IntervalNumber(intervalObject.min.number, !intervalObject.min.isClosed); overlappingIntervalSet.removeInterval(minInterval); } // get the overlapping intervals that contain the given interval's min but not max, and update their min const maxIntervals: Interval[] = this._intervals.filter((r: Interval): boolean => intervalObject.containsMin(r.min) && !intervalObject.containsMax(r.max)); for (const maxInterval of maxIntervals) { maxInterval.min = new IntervalNumber(intervalObject.max.number, !intervalObject.max.isClosed); overlappingIntervalSet.removeInterval(maxInterval); } // if there's only 1 overlapping interval and it contains the given interval, then split the overlapping interval into 2 intervals if (overlappingIntervalSet.intervals.length === 1 && overlappingIntervalSet.intervals[0].contains(intervalObject)) { this.clear(); this.addInterval(new Interval({ a: overlappingIntervalSet.intervals[0].min, b: new IntervalNumber(intervalObject.min.number, !intervalObject.min.isClosed) } as IInterval)); this.addInterval(new Interval({ a: new IntervalNumber(intervalObject.max.number, !intervalObject.max.isClosed), b: overlappingIntervalSet.intervals[0].max } as IInterval)); } else { // remove the overlapping intervals that are contained in the given interval for (const overlappingInterval of overlappingIntervalSet.intervals) { this.removeInterval(overlappingInterval); } } } } /** * Remove interval gaps in the interval set. * If interval set is already merged, then the gaps are removed depending on the replaceGapsWithNewIntervals parameter. * If interval set is not merged, then resolve the existing sets annd remove contained intervals, hen remove the gaps depending on the replaceGapsWithNewIntervals parameter. */ chainIntervals(): void { const intervalsCopy = this.intervals; IntervalSet.sort(intervalsCopy); if (this._mergeAddedInterval) { for (let i = 0; i < intervalsCopy.length - 1; i++) { const current = intervalsCopy[i]; const next = intervalsCopy[i + 1]; // Update the next interval's min if not already chained if (current.max.number !== next.min.number || current.max.isClosed === next.min.isClosed) { const localIntervalToUpdate = this._intervals.find((r) => r.toString() === next.toString()); if (localIntervalToUpdate) { localIntervalToUpdate.min = new IntervalNumber(current.max.number, !current.max.isClosed); } } } this.mergeAddedInterval = false; } else { for (let i = 0; i < intervalsCopy.length - 1; i++) { const current = intervalsCopy[i]; const next = intervalsCopy[i + 1]; if (current.containsMax(next.max) || next.max.number < current.max.number) { intervalsCopy.splice(i + 1, 1); // Remove the next interval // Adjust the current interval's max if necessary current.max = new IntervalNumber(safeMax(current.max.number, next.max.number), current.max.isClosed || next.max.isClosed); // update the current interval in the original intervals array const localIntervalToUpdate = this._intervals.find((r) => r.toString() === current.toString()); if (localIntervalToUpdate) { localIntervalToUpdate.max = current.max; } // Remove the next interval from the original intervals array this.removeInterval(next); // Decrement i to recheck the current position after removal i--; } else if (!current.containsMax(next.max)) { const localIntervalToUpdate = this._intervals.find((r) => r.toString() === next.toString()); if (localIntervalToUpdate) { localIntervalToUpdate.min = new IntervalNumber(current.max.number, !current.max.isClosed); } } } } } /** * Returns the interval in the interval set that contain the given number. * @param x - The number to check. * @example * const intervalSet: IntervalSet = new IntervalSet({ intervals: [new Interval({ a: 1, b: 10 }), new Interval({ a: 20, b: 30 })], options: { mergeAddedInterval: true }); * const intervals: Interval[] = intervalSet.getIntervalsContaining(5); * @returns An array of Interval objects that contain the provided number. */ getIntervalsContaining(x: number): Interval[] { return this._intervals.filter((r: Interval): boolean => r.containsNumber(x)); } /** * Returns a string representation of the interval set. * Example: "[1, 5), (10, 15]" */ toString(): string { return this._intervals.map(interval => interval.toString()).join(', '); } } /** * Returns the minimum of two numeric values, handling both numbers and bigints. * @param a The first value to compare. * @param b The second value to compare. * @returns The smaller of the two values. */ export function safeMin(a: NumericValue, b: NumericValue): NumericValue { return a < b ? a : b; } /** * Returns the maximum of two numeric values, handling both numbers and bigints. * @param a The first value to compare. * @param b The second value to compare. * @returns The larger of the two values. */ export function safeMax(a: NumericValue, b: NumericValue): NumericValue { return a > b ? a : b; }