@hoox-sh/pynets 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/LICENSE +661 -0
- package/NOTICE +46 -0
- package/README.md +327 -0
- package/dist/browser.js +44 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +41741 -0
- package/package.json +77 -0
- package/src/ast/builder.ts +1002 -0
- package/src/ast/dump.ts +43 -0
- package/src/ast/helper.ts +77 -0
- package/src/ast/nodes.ts +570 -0
- package/src/ast/unparse.ts +360 -0
- package/src/cli/rich.ts +378 -0
- package/src/cli.ts +627 -0
- package/src/generated/PinescriptLexer.interp +267 -0
- package/src/generated/PinescriptLexer.tokens +131 -0
- package/src/generated/PinescriptLexer.ts +673 -0
- package/src/generated/PinescriptLexerBase.ts +395 -0
- package/src/generated/PinescriptParser.interp +267 -0
- package/src/generated/PinescriptParser.tokens +131 -0
- package/src/generated/PinescriptParser.ts +8863 -0
- package/src/generated/PinescriptParserBase.ts +21 -0
- package/src/generated/PinescriptParserVisitor.ts +840 -0
- package/src/index.ts +55 -0
- package/src/parser/LexerBase.ts +395 -0
- package/src/parser/ParserBase.ts +21 -0
- package/src/parser/error.ts +33 -0
- package/src/runtime/array.ts +486 -0
- package/src/runtime/color.ts +164 -0
- package/src/runtime/drawings.ts +510 -0
- package/src/runtime/input.ts +102 -0
- package/src/runtime/interpret.ts +3897 -0
- package/src/runtime/library.ts +282 -0
- package/src/runtime/log.ts +69 -0
- package/src/runtime/map.ts +85 -0
- package/src/runtime/math.ts +231 -0
- package/src/runtime/matrix.ts +1154 -0
- package/src/runtime/provider.ts +232 -0
- package/src/runtime/request.ts +372 -0
- package/src/runtime/series.ts +44 -0
- package/src/runtime/str.ts +433 -0
- package/src/runtime/strategy.ts +1105 -0
- package/src/runtime/ta.ts +2093 -0
- package/src/runtime/ticker.ts +81 -0
- package/src/runtime/time.ts +439 -0
- package/src/runtime/timeframe.ts +85 -0
- package/src/runtime/udt.ts +119 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Tiny Pine `array.*` value. `null` is `na`. Get is na-safe (OOB / non-finite → na).
|
|
6
|
+
* Set OOB is a no-op. Python `ArrayBuiltinsMixin` remains the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Cell = number | null;
|
|
10
|
+
|
|
11
|
+
/** Python `array.set` grows only while index < 1_000_000; same bound here. */
|
|
12
|
+
export const MAX_ARRAY_SIZE = 1_000_000;
|
|
13
|
+
|
|
14
|
+
function resolveIndex(index: number, length: number): number | null {
|
|
15
|
+
if (!Number.isFinite(index)) return null;
|
|
16
|
+
let i = Math.trunc(index);
|
|
17
|
+
if (i < 0) i = length + i;
|
|
18
|
+
if (i < 0 || i >= length) return null;
|
|
19
|
+
return i;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isFiniteCell(v: Cell): v is number {
|
|
23
|
+
return v !== null && Number.isFinite(v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Python `bool` on array cells: only `na` and `0` are false (`NaN` is true). */
|
|
27
|
+
function pineTruthy(v: Cell): boolean {
|
|
28
|
+
return v !== null && v !== 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Binary-search order: comparable values first, `na` last. */
|
|
32
|
+
function keyLt(left: Cell, right: Cell): boolean {
|
|
33
|
+
if (left === null) return false;
|
|
34
|
+
if (right === null) return true;
|
|
35
|
+
return left < right;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function keyEq(left: Cell, right: Cell): boolean {
|
|
39
|
+
if (left === null && right === null) return true;
|
|
40
|
+
if (left === null || right === null) return false;
|
|
41
|
+
return left === right;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function meanOf(nums: number[]): number {
|
|
45
|
+
let acc = 0;
|
|
46
|
+
for (const v of nums) acc += v;
|
|
47
|
+
return acc / nums.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class PineArray {
|
|
51
|
+
private readonly cells: Cell[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(size?: number, initial?: Cell) {
|
|
54
|
+
if (size === undefined) return;
|
|
55
|
+
if (!Number.isFinite(size) || size < 0) return;
|
|
56
|
+
const n = Math.min(Math.trunc(size), MAX_ARRAY_SIZE);
|
|
57
|
+
const fill: Cell = initial === undefined ? null : initial;
|
|
58
|
+
for (let i = 0; i < n; i++) this.cells.push(fill);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
size(): number {
|
|
62
|
+
return this.cells.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `array.get` — OOB / non-finite index → `na`. Negative counts from the end. */
|
|
66
|
+
get(index: number): Cell {
|
|
67
|
+
const i = resolveIndex(index, this.cells.length);
|
|
68
|
+
return i === null ? null : this.cells[i]!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `array.set` — OOB / non-finite index is a no-op. */
|
|
72
|
+
set(index: number, value: Cell): void {
|
|
73
|
+
const i = resolveIndex(index, this.cells.length);
|
|
74
|
+
if (i === null) return;
|
|
75
|
+
this.cells[i] = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
push(value: Cell): void {
|
|
79
|
+
if (this.cells.length >= MAX_ARRAY_SIZE) return;
|
|
80
|
+
this.cells.push(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Last element, or `na` if empty. */
|
|
84
|
+
pop(): Cell {
|
|
85
|
+
if (this.cells.length === 0) return null;
|
|
86
|
+
return this.cells.pop()!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
unshift(value: Cell): void {
|
|
90
|
+
if (this.cells.length >= MAX_ARRAY_SIZE) return;
|
|
91
|
+
this.cells.unshift(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** First element, or `na` if empty. */
|
|
95
|
+
shift(): Cell {
|
|
96
|
+
if (this.cells.length === 0) return null;
|
|
97
|
+
return this.cells.shift()!;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clear(): void {
|
|
101
|
+
this.cells.length = 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
includes(value: Cell): boolean {
|
|
105
|
+
return this.cells.includes(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
first(): Cell {
|
|
109
|
+
return this.cells.length === 0 ? null : this.cells[0]!;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
last(): Cell {
|
|
113
|
+
return this.cells.length === 0 ? null : this.cells[this.cells.length - 1]!;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
insert(index: number, value: Cell): void {
|
|
117
|
+
if (this.cells.length >= MAX_ARRAY_SIZE) return;
|
|
118
|
+
if (!Number.isFinite(index)) return;
|
|
119
|
+
let i = Math.trunc(index);
|
|
120
|
+
if (i < 0) i = this.cells.length + i;
|
|
121
|
+
if (i < 0) i = 0;
|
|
122
|
+
if (i > this.cells.length) i = this.cells.length;
|
|
123
|
+
this.cells.splice(i, 0, value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
remove(index: number): Cell {
|
|
127
|
+
const i = resolveIndex(index, this.cells.length);
|
|
128
|
+
if (i === null) return null;
|
|
129
|
+
return this.cells.splice(i, 1)[0]!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fill(value: Cell): void {
|
|
133
|
+
for (let i = 0; i < this.cells.length; i++) this.cells[i] = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Half-open `[from, to)`. Python clamps `from < 0` to 0; `to < from` → empty. */
|
|
137
|
+
slice(from: number, to?: number): PineArray {
|
|
138
|
+
const n = this.cells.length;
|
|
139
|
+
let start = Number.isFinite(from) ? Math.trunc(from) : 0;
|
|
140
|
+
const end = to === undefined || !Number.isFinite(to) ? n : Math.trunc(to);
|
|
141
|
+
if (start < 0) start = 0;
|
|
142
|
+
const out = new PineArray();
|
|
143
|
+
if (end < start) return out;
|
|
144
|
+
for (const v of this.cells.slice(start, end)) out.push(v);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
copy(): PineArray {
|
|
149
|
+
const out = new PineArray();
|
|
150
|
+
for (const v of this.cells) out.push(v);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
reverse(): void {
|
|
155
|
+
this.cells.reverse();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sort(order: "asc" | "desc" = "asc"): void {
|
|
159
|
+
this.cells.sort((a, b) => {
|
|
160
|
+
if (a === null && b === null) return 0;
|
|
161
|
+
if (a === null) return 1;
|
|
162
|
+
if (b === null) return -1;
|
|
163
|
+
return order === "desc" ? b - a : a - b;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `array.indexof` — miss is `-1` (Python `ArrayBuiltinsMixin`). */
|
|
168
|
+
indexof(value: Cell): Cell {
|
|
169
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
170
|
+
if (Object.is(this.cells[i], value)) return i;
|
|
171
|
+
}
|
|
172
|
+
return -1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
avg(): Cell {
|
|
176
|
+
const nums = this.finiteAll();
|
|
177
|
+
if (nums === null || nums.length === 0) return null;
|
|
178
|
+
let acc = 0;
|
|
179
|
+
for (const v of nums) acc += v;
|
|
180
|
+
return acc / nums.length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
min(): Cell {
|
|
184
|
+
const nums = this.finiteAll();
|
|
185
|
+
if (nums === null || nums.length === 0) return null;
|
|
186
|
+
let best = nums[0]!;
|
|
187
|
+
for (const v of nums) if (v < best) best = v;
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
max(): Cell {
|
|
192
|
+
const nums = this.finiteAll();
|
|
193
|
+
if (nums === null || nums.length === 0) return null;
|
|
194
|
+
let best = nums[0]!;
|
|
195
|
+
for (const v of nums) if (v > best) best = v;
|
|
196
|
+
return best;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sum(): Cell {
|
|
200
|
+
const nums = this.finiteAll();
|
|
201
|
+
if (nums === null || nums.length === 0) return null;
|
|
202
|
+
let acc = 0;
|
|
203
|
+
for (const v of nums) acc += v;
|
|
204
|
+
return acc;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** `na` elements stringify as empty (Python `array.join`). */
|
|
208
|
+
join(sep = ","): string {
|
|
209
|
+
return this.cells.map((v) => (v === null ? "" : String(v))).join(sep);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** `array.from(...)` — collect args; capped at `MAX_ARRAY_SIZE`. */
|
|
213
|
+
static from(...values: Cell[]): PineArray {
|
|
214
|
+
const out = new PineArray();
|
|
215
|
+
const n = Math.min(values.length, MAX_ARRAY_SIZE);
|
|
216
|
+
for (let i = 0; i < n; i++) out.push(values[i]!);
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `array.lastindexof` — miss is `-1`. */
|
|
221
|
+
lastIndexOf(value: Cell): Cell {
|
|
222
|
+
for (let i = this.cells.length - 1; i >= 0; i--) {
|
|
223
|
+
if (Object.is(this.cells[i], value)) return i;
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Append `other` onto this (Pine mutates first) and return this. */
|
|
229
|
+
concat(other: PineArray): PineArray {
|
|
230
|
+
for (const v of other.cells) {
|
|
231
|
+
if (this.cells.length >= MAX_ARRAY_SIZE) break;
|
|
232
|
+
this.cells.push(v);
|
|
233
|
+
}
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Element-wise `abs`; `na` stays `na`. */
|
|
238
|
+
abs(): PineArray | null {
|
|
239
|
+
const out = new PineArray();
|
|
240
|
+
for (const v of this.cells) out.push(v === null ? null : Math.abs(v));
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** All cells truthy (`na` / `0` are false). Empty → true. */
|
|
245
|
+
every(): boolean {
|
|
246
|
+
for (const v of this.cells) {
|
|
247
|
+
if (!pineTruthy(v)) return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Any cell truthy. Empty → false. */
|
|
253
|
+
some(): boolean | null {
|
|
254
|
+
for (const v of this.cells) {
|
|
255
|
+
if (pineTruthy(v)) return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** `na` / non-finite poisons (same `finiteAll` contract as `avg`). */
|
|
261
|
+
median(): Cell {
|
|
262
|
+
const nums = this.finiteAll();
|
|
263
|
+
if (nums === null || nums.length === 0) return null;
|
|
264
|
+
const sorted = nums.slice().sort((a, b) => a - b);
|
|
265
|
+
const mid = sorted.length >> 1;
|
|
266
|
+
if (sorted.length % 2 === 1) return sorted[mid]!;
|
|
267
|
+
return (sorted[mid - 1]! + sorted[mid]!) / 2;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `max - min`; `na` / non-finite poisons. */
|
|
271
|
+
range(): Cell {
|
|
272
|
+
const hi = this.max();
|
|
273
|
+
const lo = this.min();
|
|
274
|
+
if (hi === null || lo === null) return null;
|
|
275
|
+
return hi - lo;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Most frequent finite value; ties keep the first-encountered (Python 3.8+ `mode`). */
|
|
279
|
+
mode(): Cell {
|
|
280
|
+
const counts = new Map<number, number>();
|
|
281
|
+
let best: Cell = null;
|
|
282
|
+
let bestC = 0;
|
|
283
|
+
for (const v of this.cells) {
|
|
284
|
+
if (!isFiniteCell(v)) continue;
|
|
285
|
+
const c = (counts.get(v) ?? 0) + 1;
|
|
286
|
+
counts.set(v, c);
|
|
287
|
+
if (c > bestC) {
|
|
288
|
+
bestC = c;
|
|
289
|
+
best = v;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return best;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Index of `value` in an ascending array (`na` last), or `-1`. */
|
|
296
|
+
binarySearch(value: Cell): number {
|
|
297
|
+
return this.binarySearchBy(value, "any");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
binarySearchLeftmost(value: Cell): number {
|
|
301
|
+
if (value === null || this.cells.some((x) => x === null)) {
|
|
302
|
+
const i = this.indexof(value);
|
|
303
|
+
return typeof i === "number" ? i : -1;
|
|
304
|
+
}
|
|
305
|
+
return this.binarySearchBy(value, "left");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
binarySearchRightmost(value: Cell): number {
|
|
309
|
+
if (value === null || this.cells.some((x) => x === null)) {
|
|
310
|
+
const i = this.lastIndexOf(value);
|
|
311
|
+
return typeof i === "number" ? i : -1;
|
|
312
|
+
}
|
|
313
|
+
return this.binarySearchBy(value, "right");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Default `biased=true` is population (`n`); `false` is sample (`n-1`). Skips `na`. */
|
|
317
|
+
stdev(biased = true): Cell {
|
|
318
|
+
const nums = this.finiteSkip();
|
|
319
|
+
if (nums.length < 2) return null;
|
|
320
|
+
const mean = meanOf(nums);
|
|
321
|
+
let acc = 0;
|
|
322
|
+
for (const x of nums) acc += (x - mean) ** 2;
|
|
323
|
+
const denom = biased ? nums.length : nums.length - 1;
|
|
324
|
+
if (denom <= 0) return null;
|
|
325
|
+
const out = Math.sqrt(acc / denom);
|
|
326
|
+
return Number.isFinite(out) ? out : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
variance(biased = true): Cell {
|
|
330
|
+
const nums = this.finiteSkip();
|
|
331
|
+
if (nums.length < 2) return null;
|
|
332
|
+
const mean = meanOf(nums);
|
|
333
|
+
let acc = 0;
|
|
334
|
+
for (const x of nums) acc += (x - mean) ** 2;
|
|
335
|
+
const denom = biased ? nums.length : nums.length - 1;
|
|
336
|
+
if (denom <= 0) return null;
|
|
337
|
+
const out = acc / denom;
|
|
338
|
+
return Number.isFinite(out) ? out : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
covariance(other: PineArray, biased = true): Cell {
|
|
342
|
+
const left = this.finiteSkip();
|
|
343
|
+
const right = other.finiteSkip();
|
|
344
|
+
const n = Math.min(left.length, right.length);
|
|
345
|
+
if (n < 2) return null;
|
|
346
|
+
const a = left.slice(0, n);
|
|
347
|
+
const b = right.slice(0, n);
|
|
348
|
+
const mean1 = meanOf(a);
|
|
349
|
+
const mean2 = meanOf(b);
|
|
350
|
+
let num = 0;
|
|
351
|
+
for (let i = 0; i < n; i++) num += (a[i]! - mean1) * (b[i]! - mean2);
|
|
352
|
+
const denom = biased ? n : n - 1;
|
|
353
|
+
if (denom <= 0) return null;
|
|
354
|
+
const out = num / denom;
|
|
355
|
+
return Number.isFinite(out) ? out : null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
percentileLinearInterpolation(percentage: number): Cell {
|
|
359
|
+
if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) return null;
|
|
360
|
+
const sorted = this.finiteSkip().sort((a, b) => a - b);
|
|
361
|
+
if (sorted.length === 0) return null;
|
|
362
|
+
const n = sorted.length;
|
|
363
|
+
const h = (percentage / 100) * (n - 1);
|
|
364
|
+
const hFloor = Math.trunc(h);
|
|
365
|
+
const hFrac = h - hFloor;
|
|
366
|
+
if (hFloor >= n - 1) return sorted[n - 1]!;
|
|
367
|
+
if (hFloor < 0) return sorted[0]!;
|
|
368
|
+
return sorted[hFloor]! * (1 - hFrac) + sorted[hFloor + 1]! * hFrac;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
percentileNearestRank(percentage: number): Cell {
|
|
372
|
+
if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) return null;
|
|
373
|
+
const sorted = this.finiteSkip().sort((a, b) => a - b);
|
|
374
|
+
if (sorted.length === 0) return null;
|
|
375
|
+
const n = sorted.length;
|
|
376
|
+
const rank = Math.max(1, Math.trunc((percentage / 100) * n + 0.5));
|
|
377
|
+
return sorted[Math.min(rank, n) - 1]!;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
percentrank(value: Cell): Cell {
|
|
381
|
+
if (!isFiniteCell(value)) return null;
|
|
382
|
+
const nums = this.finiteSkip();
|
|
383
|
+
if (nums.length === 0) return null;
|
|
384
|
+
if (nums.length === 1) return 0;
|
|
385
|
+
let count = 0;
|
|
386
|
+
for (const x of nums) if (x <= value) count += 1;
|
|
387
|
+
return ((count - 1) / (nums.length - 1)) * 100;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Z-score with sample stdev; `na` slots stay `na`. <2 finite or zero stdev → `na`. */
|
|
391
|
+
standardize(): PineArray | null {
|
|
392
|
+
const nums = this.finiteSkip();
|
|
393
|
+
if (nums.length < 2) return null;
|
|
394
|
+
const mean = meanOf(nums);
|
|
395
|
+
let acc = 0;
|
|
396
|
+
for (const x of nums) acc += (x - mean) ** 2;
|
|
397
|
+
const stdev = Math.sqrt(acc / (nums.length - 1));
|
|
398
|
+
if (stdev === 0 || !Number.isFinite(stdev)) return null;
|
|
399
|
+
const out = new PineArray();
|
|
400
|
+
for (const v of this.cells) {
|
|
401
|
+
out.push(isFiniteCell(v) ? (v - mean) / stdev : null);
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Indices that would sort this array; `na` indices last. */
|
|
407
|
+
sortIndices(order: "asc" | "desc" = "asc"): number[] | null {
|
|
408
|
+
const nonNa: { key: number; idx: number }[] = [];
|
|
409
|
+
const naIdx: number[] = [];
|
|
410
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
411
|
+
const v = this.cells[i]!;
|
|
412
|
+
if (!isFiniteCell(v)) {
|
|
413
|
+
naIdx.push(i);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
nonNa.push({ key: v, idx: i });
|
|
417
|
+
}
|
|
418
|
+
const reverse = order === "desc";
|
|
419
|
+
nonNa.sort((a, b) => {
|
|
420
|
+
const cmp = a.key - b.key;
|
|
421
|
+
if (cmp !== 0) return reverse ? -cmp : cmp;
|
|
422
|
+
return a.idx - b.idx;
|
|
423
|
+
});
|
|
424
|
+
return nonNa.map((x) => x.idx).concat(naIdx);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private binarySearchBy(value: Cell, side: "any" | "left" | "right"): number {
|
|
428
|
+
const seq = this.cells;
|
|
429
|
+
const n = seq.length;
|
|
430
|
+
if (side === "left") {
|
|
431
|
+
let left = 0;
|
|
432
|
+
let right = n;
|
|
433
|
+
while (left < right) {
|
|
434
|
+
const mid = Math.trunc((left + right) / 2);
|
|
435
|
+
if (keyLt(seq[mid]!, value)) left = mid + 1;
|
|
436
|
+
else right = mid;
|
|
437
|
+
}
|
|
438
|
+
if (left < n && keyEq(seq[left]!, value)) return left;
|
|
439
|
+
return -1;
|
|
440
|
+
}
|
|
441
|
+
if (side === "right") {
|
|
442
|
+
let left = 0;
|
|
443
|
+
let right = n;
|
|
444
|
+
while (left < right) {
|
|
445
|
+
const mid = Math.trunc((left + right) / 2);
|
|
446
|
+
if (keyLt(value, seq[mid]!)) right = mid;
|
|
447
|
+
else left = mid + 1;
|
|
448
|
+
}
|
|
449
|
+
if (left > 0 && keyEq(seq[left - 1]!, value)) return left - 1;
|
|
450
|
+
return -1;
|
|
451
|
+
}
|
|
452
|
+
let lo = 0;
|
|
453
|
+
let hi = n - 1;
|
|
454
|
+
while (lo <= hi) {
|
|
455
|
+
const mid = Math.trunc((lo + hi) / 2);
|
|
456
|
+
const midK = seq[mid]!;
|
|
457
|
+
if (keyEq(midK, value)) return mid;
|
|
458
|
+
if (keyLt(midK, value)) lo = mid + 1;
|
|
459
|
+
else hi = mid - 1;
|
|
460
|
+
}
|
|
461
|
+
return -1;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Any `na` / non-finite cell poisons aggregates (matrix `finiteAll` contract). */
|
|
465
|
+
private finiteAll(): number[] | null {
|
|
466
|
+
const out: number[] = [];
|
|
467
|
+
for (const v of this.cells) {
|
|
468
|
+
if (!isFiniteCell(v)) return null;
|
|
469
|
+
out.push(v);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Skip `na` / non-finite (Python `_numeric_values` / stdev path). */
|
|
475
|
+
private finiteSkip(): number[] {
|
|
476
|
+
const out: number[] = [];
|
|
477
|
+
for (const v of this.cells) {
|
|
478
|
+
if (isFiniteCell(v)) out.push(v);
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
toValues(): Cell[] {
|
|
484
|
+
return this.cells.slice();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine `color.*` helpers. Transparency is 0–100 (0 = opaque).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Color = { r: number; g: number; b: number; a: number };
|
|
9
|
+
|
|
10
|
+
const HEX = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
11
|
+
|
|
12
|
+
function finite(n: unknown): n is number {
|
|
13
|
+
return typeof n === "number" && Number.isFinite(n);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clampByte(n: number): number {
|
|
17
|
+
return Math.max(0, Math.min(255, Math.round(n)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pack(r: number, g: number, b: number, a: number): Color {
|
|
21
|
+
return { r: clampByte(r), g: clampByte(g), b: clampByte(b), a: clampByte(a) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isColor(c: unknown): c is Color {
|
|
25
|
+
if (typeof c !== "object" || c === null) return false;
|
|
26
|
+
const col = c as Color;
|
|
27
|
+
return finite(col.r) && finite(col.g) && finite(col.b) && finite(col.a);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hexByte(n: number): string {
|
|
31
|
+
return clampByte(n).toString(16).toUpperCase().padStart(2, "0");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function transpToAlpha(t: number): number {
|
|
35
|
+
return 255 * (1 - Math.max(0, Math.min(100, t)) / 100);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function alphaToTransp(a: number): number {
|
|
39
|
+
return Math.max(0, Math.min(100, Math.round((1 - a / 255) * 100)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse `#RRGGBB` / `#RRGGBBAA`. Alpha defaults to 255. Invalid / na → `null` (no throw). */
|
|
43
|
+
export function parseColor(hex: unknown): Color | null {
|
|
44
|
+
if (typeof hex !== "string") return null;
|
|
45
|
+
const m = HEX.exec(hex.trim());
|
|
46
|
+
if (!m) return null;
|
|
47
|
+
const h = m[1]!;
|
|
48
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
49
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
50
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
51
|
+
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255;
|
|
52
|
+
if (![r, g, b, a].every((c) => Number.isFinite(c))) return null;
|
|
53
|
+
return { r, g, b, a };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `color.new(r, g, b, t?)` — `t` is transparency 0–100 → alpha. */
|
|
57
|
+
export function colorNew(
|
|
58
|
+
r: number | null,
|
|
59
|
+
g: number | null,
|
|
60
|
+
b: number | null,
|
|
61
|
+
t?: number | null,
|
|
62
|
+
): Color | null {
|
|
63
|
+
if (!finite(r) || !finite(g) || !finite(b)) return null;
|
|
64
|
+
const a = finite(t) ? transpToAlpha(t) : 255;
|
|
65
|
+
return pack(r, g, b, a);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `color.rgb(r, g, b, t?)` — `t` is transparency 0–100 like `color.new`. */
|
|
69
|
+
export function colorRgb(
|
|
70
|
+
r: number | null,
|
|
71
|
+
g: number | null,
|
|
72
|
+
b: number | null,
|
|
73
|
+
t?: number | null,
|
|
74
|
+
): Color | null {
|
|
75
|
+
return colorNew(r, g, b, t);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Linear RGB(A) lerp. `value` outside `[bottom, top]` is clamped.
|
|
80
|
+
* Any na / non-finite input → `null`.
|
|
81
|
+
*/
|
|
82
|
+
export function colorFromGradient(
|
|
83
|
+
value: number | null,
|
|
84
|
+
bottom: number | null,
|
|
85
|
+
top: number | null,
|
|
86
|
+
c1: Color | null,
|
|
87
|
+
c2: Color | null,
|
|
88
|
+
): Color | null {
|
|
89
|
+
if (!finite(value) || !finite(bottom) || !finite(top)) return null;
|
|
90
|
+
if (!isColor(c1) || !isColor(c2)) return null;
|
|
91
|
+
const span = top - bottom;
|
|
92
|
+
const ratio = span === 0 ? 0 : Math.max(0, Math.min(1, (value - bottom) / span));
|
|
93
|
+
return pack(
|
|
94
|
+
c1.r + (c2.r - c1.r) * ratio,
|
|
95
|
+
c1.g + (c2.g - c1.g) * ratio,
|
|
96
|
+
c1.b + (c2.b - c1.b) * ratio,
|
|
97
|
+
c1.a + (c2.a - c1.a) * ratio,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** CSS / Pine named colors (`color.red` is opaque `(255, 0, 0)`). */
|
|
102
|
+
export const COLOR_BY_NAME: Record<string, Color> = {
|
|
103
|
+
red: { r: 255, g: 0, b: 0, a: 255 },
|
|
104
|
+
green: { r: 0, g: 128, b: 0, a: 255 },
|
|
105
|
+
blue: { r: 0, g: 0, b: 255, a: 255 },
|
|
106
|
+
black: { r: 0, g: 0, b: 0, a: 255 },
|
|
107
|
+
white: { r: 255, g: 255, b: 255, a: 255 },
|
|
108
|
+
gray: { r: 128, g: 128, b: 128, a: 255 },
|
|
109
|
+
grey: { r: 128, g: 128, b: 128, a: 255 },
|
|
110
|
+
silver: { r: 192, g: 192, b: 192, a: 255 },
|
|
111
|
+
yellow: { r: 255, g: 255, b: 0, a: 255 },
|
|
112
|
+
orange: { r: 255, g: 165, b: 0, a: 255 },
|
|
113
|
+
purple: { r: 128, g: 0, b: 128, a: 255 },
|
|
114
|
+
aqua: { r: 0, g: 255, b: 255, a: 255 },
|
|
115
|
+
fuchsia: { r: 255, g: 0, b: 255, a: 255 },
|
|
116
|
+
lime: { r: 0, g: 255, b: 0, a: 255 },
|
|
117
|
+
maroon: { r: 128, g: 0, b: 0, a: 255 },
|
|
118
|
+
navy: { r: 0, g: 0, b: 128, a: 255 },
|
|
119
|
+
olive: { r: 128, g: 128, b: 0, a: 255 },
|
|
120
|
+
teal: { r: 0, g: 128, b: 128, a: 255 },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Case-insensitive lookup; optional `color.` prefix. Unknown → `null`. */
|
|
124
|
+
export function colorByName(name: string): Color | null {
|
|
125
|
+
if (typeof name !== "string") return null;
|
|
126
|
+
let key = name.trim().toLowerCase();
|
|
127
|
+
if (key.startsWith("color.")) key = key.slice("color.".length);
|
|
128
|
+
const found = COLOR_BY_NAME[key];
|
|
129
|
+
return found ? { ...found } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Coerce Color | `#RRGGBB(AA)` | named (`red` / `color.red`). Unknown / na → `null`. */
|
|
133
|
+
export function asColor(c: unknown): Color | null {
|
|
134
|
+
if (isColor(c)) return c;
|
|
135
|
+
if (typeof c !== "string") return null;
|
|
136
|
+
return parseColor(c) ?? colorByName(c);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function colorR(c: unknown): number | null {
|
|
140
|
+
const col = asColor(c);
|
|
141
|
+
return col ? clampByte(col.r) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function colorG(c: unknown): number | null {
|
|
145
|
+
const col = asColor(c);
|
|
146
|
+
return col ? clampByte(col.g) : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function colorB(c: unknown): number | null {
|
|
150
|
+
const col = asColor(c);
|
|
151
|
+
return col ? clampByte(col.b) : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Transparency 0–100 derived from alpha (255 → 0, 0 → 100). */
|
|
155
|
+
export function colorT(c: unknown): number | null {
|
|
156
|
+
const col = asColor(c);
|
|
157
|
+
return col ? alphaToTransp(col.a) : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** `#RRGGBB`, or `#RRGGBBAA` when alpha is not 255. */
|
|
161
|
+
export function colorToHex(c: Color): string {
|
|
162
|
+
const rgb = `#${hexByte(c.r)}${hexByte(c.g)}${hexByte(c.b)}`;
|
|
163
|
+
return c.a !== 255 ? `${rgb}${hexByte(c.a)}` : rgb;
|
|
164
|
+
}
|