generated from obsidianmd/obsidian-sample-plugin
-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
349 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import IntervalCache, { Interval } from './IntervalCache'; | ||
|
||
let intervalCache: IntervalCache; | ||
|
||
describe('parse', () => { | ||
beforeEach(() => { | ||
intervalCache = new IntervalCache(); | ||
}); | ||
|
||
test('Can insert non-overlapping intervals', () => { | ||
intervalCache.insert({ start: 0, end: 2 }); | ||
intervalCache.insert({ start: 3, end: 5 }); | ||
expect(intervalCache.intervals).toHaveLength(2); | ||
expect(intervalCache.intervals).toContainEqual({ start: 0, end: 2 }); | ||
expect(intervalCache.intervals).toContainEqual({ start: 3, end: 5 }); | ||
}); | ||
|
||
test('Can insert overlapping intervals', () => { | ||
intervalCache.insert({ start: 0, end: 2 }); | ||
intervalCache.insert({ start: 1, end: 5 }); | ||
expect(intervalCache.intervals).toHaveLength(1); | ||
expect(intervalCache.intervals).toContainEqual({ start: 0, end: 5 }); | ||
}); | ||
|
||
test('Returns empty array for fully included intervals', () => { | ||
intervalCache.insert({ start: 0, end: 10 }); | ||
expect(intervalCache.check({ start: 2, end: 8 })).toEqual<Interval[]>([]); | ||
expect(intervalCache.check({ start: 0, end: 8 })).toEqual<Interval[]>([]); | ||
expect(intervalCache.check({ start: 2, end: 10 })).toEqual<Interval[]>([]); | ||
expect(intervalCache.check({ start: 0, end: 10 })).toEqual<Interval[]>([]); | ||
}); | ||
|
||
test('Returns same interval for fully excluded intervals', () => { | ||
intervalCache.insert({ start: 0, end: 10 }); | ||
expect(intervalCache.check({ start: -2, end: -1 })).toEqual<Interval[]>([ | ||
{ start: -2, end: -1 } | ||
]); | ||
expect(intervalCache.check({ start: 11, end: 12 })).toEqual<Interval[]>([ | ||
{ start: 11, end: 12 } | ||
]); | ||
}); | ||
|
||
test('Returns Interval for partially excluded intervals (right-overlapping)', () => { | ||
intervalCache.insert({ start: 0, end: 10 }); | ||
expect(intervalCache.check({ start: 8, end: 14 })).toEqual<Interval[]>([ | ||
{ | ||
start: 11, | ||
end: 14 | ||
} | ||
]); | ||
expect(intervalCache.check({ start: 10, end: 14 })).toEqual<Interval[]>([ | ||
{ | ||
start: 11, | ||
end: 14 | ||
} | ||
]); | ||
}); | ||
|
||
test('Returns Interval for partially excluded intervals (left-overlapping)', () => { | ||
intervalCache.insert({ start: 0, end: 10 }); | ||
expect(intervalCache.check({ start: -5, end: 2 })).toEqual<Interval[]>([ | ||
{ | ||
start: -5, | ||
end: -1 | ||
} | ||
]); | ||
expect(intervalCache.check({ start: -5, end: 0 })).toEqual<Interval[]>([ | ||
{ | ||
start: -5, | ||
end: -1 | ||
} | ||
]); | ||
}); | ||
|
||
test('Returns Interval for partially excluded intervals (middle-overlapping)', () => { | ||
intervalCache.insert({ start: 0, end: 2 }); | ||
intervalCache.insert({ start: 5, end: 7 }); | ||
|
||
// single interval | ||
let result = intervalCache.check({ start: 1, end: 5 }); | ||
expect(result).toContainEqual<Interval>({ start: 3, end: 4 }); | ||
|
||
// double interval | ||
result = intervalCache.check({ start: 0, end: 9 }); | ||
expect(result).toContainEqual<Interval>({ start: 3, end: 4 }); | ||
expect(result).toContainEqual<Interval>({ start: 8, end: 9 }); | ||
|
||
// triple interval | ||
result = intervalCache.check({ start: -2, end: 9 }); | ||
expect(result).toContainEqual<Interval>({ start: -2, end: -1 }); | ||
expect(result).toContainEqual<Interval>({ start: 3, end: 4 }); | ||
expect(result).toContainEqual<Interval>({ start: 8, end: 9 }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Datastructure that maintains an interval tree of non-overlapping intervals. | ||
// NOTE: this is not time-complexity optimized because it will not contain enough | ||
// data at any point to require better-than-linear scaling. | ||
|
||
/** Datastructure that maintains an interval list of non-overlapping intervals. | ||
* | ||
* NOTE: this is not time-complexity optimized because it will not contain | ||
* enough data at any point to require better-than-linear scaling. | ||
*/ | ||
|
||
export interface Interval { | ||
start: number; | ||
end: number; | ||
} | ||
|
||
export default class IntervalCache { | ||
private _intervals: Interval[] = []; | ||
|
||
/** | ||
* Inserts a new interval into memory. | ||
*/ | ||
public insert(newInterval: Interval): void { | ||
if (this._intervals.length > 0) { | ||
for (let i = 0; i < this._intervals.length; i++) { | ||
const curr = this._intervals[i]; | ||
// Checking for overlap: | ||
|
||
if (newInterval.end < curr.start) { | ||
// Case A: New interval entirely precedes curr -> insert at position i | ||
this._intervals.splice(i, 0, newInterval); | ||
return; | ||
} else if (doOverlap(curr, newInterval)) { | ||
// Case B: New interval overlaps with curr: | ||
// merge intervals, remove conflicting interval, and insert the new interval. | ||
const mergedInterval = this._mergeIntervals(newInterval, curr); | ||
this._intervals.splice(i, 1); | ||
this.insert(mergedInterval); | ||
return; | ||
} | ||
} | ||
// Case C: the new interval is inserted at the end. | ||
this._intervals.push(newInterval); | ||
} else { | ||
this._intervals.push(newInterval); | ||
} | ||
} | ||
|
||
private _mergeIntervals(a: Interval, b: Interval): Interval { | ||
const start = a.start < b.start ? a.start : b.start; | ||
const end = a.end > b.end ? a.end : b.end; | ||
return { start, end }; | ||
} | ||
|
||
/** | ||
* Checks if the passed interval overlaps with what is already in memory. | ||
* @return empty array if entire interval is contained in cache. Else, | ||
* returns array of missing intervals. | ||
*/ | ||
public check(interval: Interval): Interval[] { | ||
const missingIntervals = [interval]; | ||
for (const curr of this._intervals) { | ||
const remaining = missingIntervals.pop(); | ||
const diff = difference(remaining, curr); | ||
if (diff.length == 0) { | ||
// Case: remaining interval is completely covered. | ||
break; | ||
} else { | ||
missingIntervals.push(...diff); | ||
} | ||
} | ||
return missingIntervals; | ||
} | ||
|
||
public get intervals(): Interval[] { | ||
return this._intervals; | ||
} | ||
} | ||
|
||
function doOverlap(a: Interval, b: Interval): boolean { | ||
// https://stackoverflow.com/questions/13513932/algorithm-to-detect-overlapping-periods | ||
return a.start <= b.end && b.start <= a.end; | ||
} | ||
|
||
/** | ||
* Returns the remainder intervals of a after subtracting b. | ||
* Returns empty array if b entirely overlaps a. | ||
* @param a Interval to subtract from | ||
* @param b Interval to subtract by | ||
*/ | ||
function difference(a: Interval, b: Interval): Interval[] { | ||
if (!doOverlap(a, b)) { | ||
// Case 1: a and b are entirely non-overlapping | ||
return [a]; | ||
} else if (b.start <= a.start && b.end >= a.end) { | ||
// Case 2: b completely spans a | ||
return []; | ||
} else if (b.start <= a.start && b.end < a.end) { | ||
// Case 3: b left-overlaps a | ||
return [{ start: b.end + 1, end: a.end }]; | ||
} else if (b.start >= a.start && b.end >= a.end) { | ||
// Case 4: b right-overlaps a | ||
return [{ start: a.start, end: b.start - 1 }]; | ||
} else if (b.start >= a.start && b.end <= a.end) { | ||
return [ | ||
{ start: a.start, end: b.start - 1 }, | ||
{ start: b.end + 1, end: a.end } | ||
]; | ||
} | ||
throw Error('Unaccounted case in differencing two intervals!'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,80 @@ | ||
export default class TogglCache {} | ||
// Docs for sorted btree: https://www.npmjs.com/package/sorted-btree | ||
import type { Detailed, Report } from 'lib/model/Report'; | ||
import type { ISODate } from 'lib/reports/ReportQuery'; | ||
import moment from 'moment'; | ||
import BTree, { ISortedMap } from 'sorted-btree'; | ||
import IntervalCache, { Interval } from './IntervalCache'; | ||
|
||
/** UNIX-time representation of a date */ | ||
type Date = number; | ||
|
||
export interface TogglCacheResult { | ||
report: Report<Detailed>; | ||
missing: { from: ISODate; to: ISODate }[]; | ||
} | ||
|
||
export default class TogglCache { | ||
private intervals = new IntervalCache(); | ||
private entries: ISortedMap<Date, Detailed> = new BTree(); | ||
|
||
/** | ||
* @param start UNIX-time representation of first day of query range. | ||
* @param end UNIX-time representation of last day of query range (inclusive). | ||
* @returns TogglCacheResult with cached time entries and missing intervals from cache. | ||
*/ | ||
public get(start: ISODate, end: ISODate): TogglCacheResult { | ||
console.debug(`Fetching ${start}-${end} from interval cache.`); | ||
const start_unix = DateToUnixTime(start); | ||
const end_unix = DateToUnixTime(end); | ||
|
||
const data = this.entries | ||
.getRange(start_unix, end_unix + 1000 * 60 * 60 * 24 - 1, true) | ||
.flatMap(([date, detailed]) => detailed); | ||
|
||
const total_grand = data.reduce((count, entry) => count + entry.dur, 0); | ||
const report: Report<Detailed> = { | ||
total_grand, | ||
data, | ||
total_count: data.length | ||
}; | ||
|
||
const missingUnix = this.intervals.check({ | ||
start: start_unix, | ||
end: end_unix | ||
}); | ||
|
||
const missing = missingUnix.map((interval) => { | ||
const from = moment(interval.start * 1000).format('YYYY-MM-DD'); | ||
const to = moment(interval.end * 1000).format('YYYY-MM-DD'); | ||
return { from, to }; | ||
}); | ||
|
||
return { report, missing }; | ||
} | ||
|
||
/** | ||
* Caches an array of detailed report entries. | ||
* NOTE: assuems that the entries are a contiguous range with no missing entries! | ||
*/ | ||
public put(from: ISODate, to: ISODate, entries: Detailed[]): void { | ||
// Add entries to cache | ||
for (const entry of entries) { | ||
const date = moment(entry.start).unix(); | ||
this.entries.set(date, entry, true); | ||
} | ||
// Add new interval to interval cache | ||
const start = DateToUnixTime(from); | ||
const end = DateToUnixTime(to); | ||
|
||
this.intervals.insert({ start, end }); | ||
console.debug(`Added ${from}-${to} to reports cache.`); | ||
} | ||
} | ||
|
||
function DateToUnixTime(dateString: ISODate): Date { | ||
return moment(dateString, 'YYYY-MM-DD') | ||
.set('hour', 0) | ||
.set('minute', 0) | ||
.set('second', 0) | ||
.unix(); | ||
} |
Oops, something went wrong.