@georgevie/period-sequence
Version:
High-performance TypeScript library for time period manipulation with immutable design and enterprise-grade performance
561 lines • 19.6 kB
JavaScript
"use strict";
/**
* High-performance Sequence class for managing collections of Period instances
* Optimized for large datasets with efficient gap analysis and set operations
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sequence = void 0;
const Period_1 = require("../core/Period");
class Sequence {
constructor(periodsOrFirst, preserveOrderOrSecond, ...rest) {
// Handle overloaded constructor
let periods;
let shouldSort = true;
if (Array.isArray(periodsOrFirst) && typeof preserveOrderOrSecond === 'boolean') {
// First overload: constructor(periods: Period[], preserveOrder: boolean)
periods = periodsOrFirst;
shouldSort = !preserveOrderOrSecond;
}
else if (periodsOrFirst && !Array.isArray(periodsOrFirst)) {
// Second overload: constructor(...periods: Period[])
const allPeriods = [periodsOrFirst];
if (typeof preserveOrderOrSecond !== 'boolean') {
allPeriods.push(preserveOrderOrSecond);
}
allPeriods.push(...rest);
periods = allPeriods.filter(p => p !== undefined);
}
else if (Array.isArray(periodsOrFirst)) {
// Array passed without preserve flag - default to sorting
periods = periodsOrFirst;
}
else {
periods = [];
}
if (periods.length === 0) {
this._periods = [];
this._sorted = true;
return;
}
if (shouldSort) {
// Sort periods by start time for performance optimization
const sortedPeriods = [...periods].sort((a, b) => a.startTime - b.startTime);
this._periods = Object.freeze(sortedPeriods);
this._sorted = true;
}
else {
// Preserve the order passed in (for custom sorting)
this._periods = Object.freeze([...periods]);
this._sorted = false;
}
}
/**
* Create sequence from array of periods
* Optimized constructor for better performance with large arrays
*/
static fromArray(periods) {
return new Sequence(periods, false); // Use array constructor directly - no spread operator limits!
}
/**
* Create empty sequence
* Optimized for common use case
*/
static empty() {
return new Sequence();
}
/**
* Number of periods in the sequence (cached)
* O(1) operation with memoization
*/
count() {
if (this._count === undefined) {
this._count = this._periods.length;
}
return this._count;
}
/**
* Check if sequence is empty (cached)
* O(1) operation with memoization
*/
isEmpty() {
if (this._isEmpty === undefined) {
this._isEmpty = this._periods.length === 0;
}
return this._isEmpty;
}
/**
* Get period by index
* O(1) operation with bounds checking
*/
get(index) {
if (index < 0 || index >= this._periods.length) {
throw new Error(`Index ${index} out of bounds. Sequence has ${this._periods.length} periods.`);
}
return this._periods[index];
}
/**
* Get first period
* O(1) operation
*/
first() {
return this._periods[0];
}
/**
* Get last period
* O(1) operation
*/
last() {
return this._periods[this._periods.length - 1];
}
/**
* Convert to array
* Returns a copy to maintain immutability
*/
toArray() {
return [...this._periods];
}
/**
* Iterator implementation for for...of loops
* Enables: for (const period of sequence) { ... }
*/
[Symbol.iterator]() {
return this._periods[Symbol.iterator]();
}
/**
* Get boundaries of the entire sequence
* Returns a period that spans from the earliest start to the latest end
* Cached for performance
*/
boundaries() {
if (this.isEmpty()) {
return undefined;
}
if (!this._boundaries) {
const first = this.first();
const last = this.last();
// Find the actual earliest start and latest end
let earliestStart = first.startTime;
let latestEnd = last.endTime;
// Since periods are sorted by start time, we only need to check end times
for (const period of this._periods) {
if (period.endTime > latestEnd) {
latestEnd = period.endTime;
}
}
this._boundaries = new Period_1.Period(earliestStart, latestEnd, first.bounds);
}
return this._boundaries;
}
/**
* Find gaps between non-overlapping periods
* Returns a new Sequence containing the gap periods
* Highly optimized algorithm with O(n) complexity
*/
gaps() {
if (this._gaps) {
return this._gaps;
}
if (this.isEmpty() || this.count() === 1) {
this._gaps = Sequence.empty();
return this._gaps;
}
const gaps = [];
// Iterate through adjacent periods to find gaps
for (let i = 0; i < this._periods.length - 1; i++) {
const current = this._periods[i];
const next = this._periods[i + 1];
// Check if there's a gap between current and next period
if (!current.overlaps(next) && !current.touches(next)) {
// Create gap period using the end of current and start of next
const gap = new Period_1.Period(current.endTime, next.startTime, current.bounds);
gaps.push(gap);
}
}
this._gaps = new Sequence(...gaps);
return this._gaps;
}
/**
* Filter periods based on predicate
* Returns new Sequence with matching periods
*/
filter(predicate) {
const filtered = this._periods.filter(predicate);
return new Sequence(...filtered);
}
/**
* Map periods to new values
* Returns array of mapped values
*/
map(mapper) {
return this._periods.map(mapper);
}
/**
* Check if any period matches the predicate
*/
some(predicate) {
return this._periods.some(predicate);
}
/**
* Check if all periods match the predicate
*/
every(predicate) {
return this._periods.every(predicate);
}
/**
* Find first period matching predicate
*/
find(predicate) {
return this._periods.find(predicate);
}
/**
* Reduce sequence to a single value
* Standard array reduce functionality
*/
reduce(reducer, initialValue) {
return this._periods.reduce(reducer, initialValue);
}
/**
* Sort sequence by custom comparator
* Returns new Sequence with sorted periods
*/
sort(compareFn) {
const sorted = [...this._periods].sort(compareFn);
return new Sequence(sorted, true); // preserveOrder = true to avoid re-sorting
}
/**
* Sort by start date (already optimized as default)
* Returns this sequence if already sorted by start date
*/
sortByStartDate() {
return this._sorted ? this : this.sort((a, b) => a.startTime - b.startTime);
}
/**
* Sort by duration (shortest first)
* Returns new Sequence sorted by duration
*/
sortByDuration() {
return this.sort((a, b) => {
const durA = a.endTime - a.startTime;
const durB = b.endTime - b.startTime;
return durA - durB;
});
}
/**
* Check if this sequence equals another sequence
* Compares all periods for equality
*/
equals(other) {
if (this.count() !== other.count()) {
return false;
}
for (let i = 0; i < this._periods.length; i++) {
if (!this._periods[i].equals(other._periods[i])) {
return false;
}
}
return true;
}
/**
* String representation of the sequence
* Shows count and boundaries for debugging
*/
toString() {
if (this.isEmpty()) {
return 'Sequence(empty)';
}
const boundaries = this.boundaries();
return `Sequence(${this.count()} periods, ${boundaries?.toString()})`;
}
/**
* Create a union of this sequence with another sequence
* Combines all periods from both sequences, removing duplicates
* @param other - The sequence to union with
* @returns A new sequence containing all unique periods from both sequences
*/
union(other) {
if (this.isEmpty())
return other;
if (other.isEmpty())
return this;
const maxSize = this._periods.length + other._periods.length;
const combined = new Array(maxSize);
let writeIndex = 0;
const seenPeriods = new Set();
// Add periods from first sequence
const thisPeriods = this._periods;
for (let i = 0; i < thisPeriods.length; i++) {
const period = thisPeriods[i];
if (!seenPeriods.has(period)) {
seenPeriods.add(period);
combined[writeIndex++] = period;
}
}
// Add periods from second sequence
const otherPeriods = other._periods;
for (let i = 0; i < otherPeriods.length; i++) {
const period = otherPeriods[i];
if (!seenPeriods.has(period)) {
seenPeriods.add(period);
combined[writeIndex++] = period;
}
}
// Trim array to actual size and create sequence
combined.length = writeIndex;
return new Sequence(combined, false); // preserveOrder = false for sorting
}
/**
* Find intersections with another sequence
* Returns periods that overlap between this sequence and another
* @param other - The sequence to intersect with
* @returns A new sequence containing only overlapping periods
*/
intersect(other) {
// Fast path for empty sequences
if (this.isEmpty() || other.isEmpty()) {
return Sequence.empty();
}
const intersections = [];
// Use two-pointer technique on sorted sequences for O(n + m) complexity
let i = 0, j = 0;
const thisArray = this._periods;
const otherArray = other._periods;
while (i < thisArray.length && j < otherArray.length) {
const thisPeriod = thisArray[i];
const otherPeriod = otherArray[j];
// Check for overlap using optimized timestamp comparison
const maxStart = Math.max(thisPeriod.startTime, otherPeriod.startTime);
const minEnd = Math.min(thisPeriod.endTime, otherPeriod.endTime);
if (maxStart < minEnd) {
// Periods overlap - create intersection
intersections.push(new Period_1.Period(maxStart, minEnd, thisPeriod.bounds));
}
// Advance the pointer of the period that ends first
if (thisPeriod.endTime <= otherPeriod.endTime) {
i++;
}
else {
j++;
}
}
return new Sequence(intersections, true); // preserveOrder = true (already sorted)
}
/**
* Subtract operation - remove overlapping periods
* High-performance implementation with early termination
* Complexity: O(n * m) but optimized with early breaks
*/
subtract(other) {
// Fast path for empty sequences
if (this.isEmpty())
return Sequence.empty();
if (other.isEmpty())
return this;
const remaining = [];
// Pre-filter: only check periods that could potentially overlap
const otherBoundaries = other.boundaries();
if (!otherBoundaries)
return this;
for (const thisPeriod of this._periods) {
// Fast boundary check before expensive overlap tests
if (thisPeriod.endTime <= otherBoundaries.startTime ||
thisPeriod.startTime >= otherBoundaries.endTime) {
// Period is completely outside other sequence bounds
remaining.push(thisPeriod);
continue;
}
// Check for overlaps with early termination
let hasOverlap = false;
for (const otherPeriod of other._periods) {
// Early termination if other period is past current period
if (otherPeriod.startTime >= thisPeriod.endTime)
break;
if (thisPeriod.overlaps(otherPeriod)) {
hasOverlap = true;
break;
}
}
if (!hasOverlap) {
remaining.push(thisPeriod);
}
}
return new Sequence(remaining, true); // preserveOrder = true (maintaining order)
}
/**
* Merge overlapping and consecutive day periods within this sequence
* Optimized for date-only operations with enhanced boundary logic
* Complexity: O(n) since periods are already sorted
*/
merge() {
if (this.isEmpty() || this.count() === 1) {
return this;
}
const merged = [];
let current = this._periods[0];
for (let i = 1; i < this._periods.length; i++) {
const next = this._periods[i];
// Use the enhanced date-only merging logic
if (current.overlaps(next) || current.canMergeConsecutiveDays(next)) {
// Merge periods using the union method which handles date-only logic
const mergedPeriod = current.union(next);
if (mergedPeriod) {
current = mergedPeriod;
}
else {
// Fallback: shouldn't happen with proper logic, but safety net
merged.push(current);
current = next;
}
}
else {
// No overlap or consecutive merger possible, add current and move to next
merged.push(current);
current = next;
}
}
// Add the last period
merged.push(current);
return new Sequence(merged, true); // preserveOrder = true
}
/**
* Get the total duration of all periods in the sequence (cached)
* Ultra-optimized calculation with memoization
*/
totalDuration() {
if (this._totalDuration === undefined) {
let totalMs = 0;
const periods = this._periods;
const len = periods.length;
// Unrolled loop for better performance
let i = 0;
while (i < len - 3) {
totalMs += periods[i].endTime - periods[i].startTime;
totalMs += periods[i + 1].endTime - periods[i + 1].startTime;
totalMs += periods[i + 2].endTime - periods[i + 2].startTime;
totalMs += periods[i + 3].endTime - periods[i + 3].startTime;
i += 4;
}
// Handle remaining elements
while (i < len) {
totalMs += periods[i].endTime - periods[i].startTime;
i++;
}
this._totalDuration = totalMs;
}
return this._totalDuration;
}
// Mutable-style methods (immutable behind the scenes)
/**
* Add a period to the end of the sequence
* Returns a new Sequence instance (immutable design)
*/
push(period) {
const newPeriods = [...this._periods, period];
return new Sequence(newPeriods, true); // preserveOrder = true
}
/**
* Add a period to the beginning of the sequence
* Returns a new Sequence instance (immutable design)
*/
unshift(period) {
const newPeriods = [period, ...this._periods];
return new Sequence(newPeriods, true); // preserveOrder = true
}
/**
* Insert a period at the specified index
* Returns a new Sequence instance (immutable design)
* @param index - Zero-based index (supports negative indexing)
* @param period - Period to insert
*/
insert(index, period) {
const len = this._periods.length;
// Handle negative indexing: -1 means insert before last element
if (index < 0) {
index = len + index;
}
// Validate index bounds
if (index < 0 || index > len) {
throw new Error(`Index ${index} is out of bounds for sequence of length ${len}`);
}
const newPeriods = [
...this._periods.slice(0, index),
period,
...this._periods.slice(index)
];
return new Sequence(newPeriods, true); // preserveOrder = true
}
/**
* Remove and return the period at the specified index
* Returns the removed period, and updates this sequence to a new instance
* @param index - Zero-based index (supports negative indexing)
*/
remove(index) {
const len = this._periods.length;
// Handle negative indexing
if (index < 0) {
index = len + index;
}
// Validate index bounds
if (index < 0 || index >= len) {
throw new Error(`Index ${index} is out of bounds for sequence of length ${len}`);
}
const removedPeriod = this._periods[index];
const newPeriods = [
...this._periods.slice(0, index),
...this._periods.slice(index + 1)
];
// Update this instance to point to new sequence
Object.assign(this, new Sequence(newPeriods, true));
return removedPeriod;
}
/**
* Replace the period at the specified index
* Returns a new Sequence instance (immutable design)
* @param index - Zero-based index (supports negative indexing)
* @param period - New period to set
*/
set(index, period) {
const len = this._periods.length;
// Handle negative indexing
if (index < 0) {
index = len + index;
}
// Validate index bounds
if (index < 0 || index >= len) {
throw new Error(`Index ${index} is out of bounds for sequence of length ${len}`);
}
const newPeriods = [...this._periods];
newPeriods[index] = period;
return new Sequence(newPeriods, true); // preserveOrder = true
}
/**
* Remove all periods from the sequence
* Returns a new empty Sequence instance (immutable design)
*/
clear() {
return Sequence.empty();
}
/**
* Check if the sequence contains a specific period
* @param period - Period to search for
*/
contains(period) {
return this._periods.some(p => p.equals(period));
}
/**
* Find the index of a specific period in the sequence
* @param period - Period to search for
* @returns Index of the period, or false if not found
*/
indexOf(period) {
const index = this._periods.findIndex(p => p.equals(period));
return index === -1 ? false : index;
}
/**
* Convert sequence to array (alias for toArray for compatibility)
*/
toList() {
return this.toArray();
}
}
exports.Sequence = Sequence;
//# sourceMappingURL=Sequence.js.map