-
Notifications
You must be signed in to change notification settings - Fork 547
/
Copy pathrangeTracker.ts
226 lines (204 loc) · 6.63 KB
/
rangeTracker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
// eslint-disable-next-line import/no-internal-modules
import cloneDeep from "lodash/cloneDeep";
import { assert } from "./assert";
/**
* A range in the {@link RangeTracker}
*
* @deprecated No replacement.
* @internal
*/
export interface IRange {
primary: number;
secondary: number | undefined;
length: number;
}
/**
* A serialized version of the {@link RangeTracker}
*
* @deprecated No replacement.
* @internal
*/
export interface IRangeTrackerSnapshot {
ranges: IRange[];
lastPrimary: number;
lastSecondary: number | undefined;
}
/**
* Helper class that keeps track of the relation between two ranges in a 1:N fashion. Primary
* is continuous and always maps to a single value in secondary above the base value. The range
* defines an increasing step function.
*
* Used by deli to keep track of the branch map
*
* @deprecated No replacement.
* @internal
*/
export class RangeTracker {
private ranges: IRange[];
private lastPrimary: number;
private lastSecondary: number | undefined;
get base(): number {
return this.ranges[0].primary;
}
/**
* Getter for the last primary that was added
*
* @returns last primary that was added
*/
get primaryHead(): number {
return this.lastPrimary;
}
/**
* Getter for the last secondary that was added
*
* @returns last secondary that was added
*/
get secondaryHead(): number | undefined {
return this.lastSecondary;
}
constructor(primary: IRangeTrackerSnapshot);
constructor(primary: number, secondary: number);
constructor(primary: IRangeTrackerSnapshot | number, secondary?: number) {
if (typeof primary === "number") {
this.ranges = [{ length: 0, primary, secondary }];
this.lastPrimary = primary;
this.lastSecondary = secondary;
} else {
this.ranges = cloneDeep(primary.ranges);
this.lastPrimary = primary.lastPrimary;
this.lastSecondary = primary.lastSecondary;
}
}
/**
* Returns a serialized form of the RangeTracker
*/
public serialize(): IRangeTrackerSnapshot {
return {
lastPrimary: this.lastPrimary,
lastSecondary: this.lastSecondary,
ranges: cloneDeep(this.ranges),
};
}
/**
* Add a primary, secondary pair to the range
*
* @param primary - the primary number in the range
* @param secondary - the secondary number in the range
*/
public add(primary: number, secondary: number): void {
// Both values must continuously be increasing - we won't always track the last value we saw so we do so
// below to check invariants
assert(primary >= this.lastPrimary, 0x003 /* "Primary to add to range < last primary!" */);
if (this.lastSecondary !== undefined) {
assert(
secondary >= this.lastSecondary,
0x004 /* "Secondary to add to range < last secondary!" */,
);
}
this.lastPrimary = primary;
this.lastSecondary = secondary;
// Get quicker references to the head of the range
const head = this.ranges[this.ranges.length - 1];
const primaryHead = head.primary + head.length;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const secondaryHead = head.secondary! + head.length;
// Same secondary indicates this is not a true inflection point - we can ignore it
if (secondary === secondaryHead) {
return;
}
// New secondary - need to update the ranges
if (primary === primaryHead) {
// Technically this code path has us supporting N:N ranges. But we simply overwrite duplicate values to
// preserve 1:N since you can only lookup from the primary to a secondary
if (head.length === 0) {
// No range represented - we can simply update secondary with the overwritten value
head.secondary = secondary;
} else {
// The values in the range before this one are valid - but we need to create a new one for this update
head.length--;
this.ranges.push({ length: 0, primary, secondary });
}
} else {
if (primaryHead + 1 === primary && secondaryHead + 1 === secondary) {
// Extend the length if both increase by the same amount
head.length++;
} else {
// Insert a new node
this.ranges.push({ length: 0, primary, secondary });
}
}
}
/**
* Get the closest range to the primary
*
* @param primary - the primary value to look for
* @returns the closest range to the primary
*/
public get(primary: number): number {
assert(
primary >= this.ranges[0].primary,
0x005 /* "Target primary to retrieve < first range's primary!" */,
);
// Find the first range where the starting position is greater than the primary. Our target range is
// the one before it.
let index = 1;
for (; index < this.ranges.length; index++) {
if (primary < this.ranges[index].primary) {
break;
}
}
assert(
primary >= this.ranges[index - 1].primary,
0x006 /* "Target primary to retrieve < last range's primary!" */,
);
// If the difference is within the stored range use it - otherwise add in the length - 1 as the highest
// stored secondary value to use.
const closestRange = this.ranges[index - 1];
return (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Math.min(primary - closestRange.primary, closestRange.length) + closestRange.secondary!
);
}
/**
* Update the range of primary
*
* @param primary - the primary value to update
*/
public updateBase(primary: number): void {
assert(
primary >= this.ranges[0].primary,
0x007 /* "Target primary to update < first range's primary!" */,
);
// Walk the ranges looking for the first one that is greater than the primary. Primary is then within the
// previous index by definition (since it's less than the current index's primary but greather than the
// previous index's primary) and we know primary must be greater than the base.
let index = 1;
for (; index < this.ranges.length; index++) {
if (primary < this.ranges[index].primary) {
break;
}
}
assert(
primary >= this.ranges[index - 1].primary,
0x008 /* "Target primary to update < last range's primary!" */,
);
// Update the last range values
const range = this.ranges[index - 1];
const delta = primary - range.primary;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
range.secondary = range.secondary! + Math.min(delta, range.length);
range.length = Math.max(range.length - delta, 0);
range.primary = primary;
// And remove unnecessary ranges
this.ranges = index - 1 > 0 ? this.ranges.slice(index - 1) : this.ranges;
// Assert that the lowest value is now the input to this method
assert(
primary === this.ranges[0].primary,
0x009 /* "After update, target primary is not first range's primary!" */,
);
}
}