@hoox-sh/pynets 0.1.0 → 0.3.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +20 -9
  3. package/dist/browser.js +95 -34
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +23002 -12427
  6. package/package.json +1 -1
  7. package/src/ast/builder.ts +235 -12
  8. package/src/ast/nodes.ts +114 -4
  9. package/src/ast/unparse.ts +192 -17
  10. package/src/cli/rich.ts +4 -3
  11. package/src/cli.ts +29 -7
  12. package/src/generated/PinescriptLexer.interp +5 -1
  13. package/src/generated/PinescriptLexer.tokens +97 -95
  14. package/src/generated/PinescriptLexer.ts +512 -502
  15. package/src/generated/PinescriptParser.interp +8 -1
  16. package/src/generated/PinescriptParser.tokens +97 -95
  17. package/src/generated/PinescriptParser.ts +1907 -1287
  18. package/src/generated/PinescriptParserVisitor.ts +36 -1
  19. package/src/index.ts +22 -0
  20. package/src/runtime/array.ts +238 -68
  21. package/src/runtime/compile/collections.ts +492 -0
  22. package/src/runtime/compile/draw.ts +181 -0
  23. package/src/runtime/compile/emit.ts +1421 -0
  24. package/src/runtime/compile/emit_call.ts +1796 -0
  25. package/src/runtime/compile/emit_udf.ts +326 -0
  26. package/src/runtime/compile/engine.ts +265 -0
  27. package/src/runtime/compile/helpers.ts +140 -0
  28. package/src/runtime/compile/index.ts +33 -0
  29. package/src/runtime/compile/runtime.ts +357 -0
  30. package/src/runtime/compile/strategy.ts +250 -0
  31. package/src/runtime/compile/types.ts +127 -0
  32. package/src/runtime/compile/udt_runtime.ts +50 -0
  33. package/src/runtime/drawings.ts +405 -8
  34. package/src/runtime/interpret.ts +2387 -281
  35. package/src/runtime/matrix.ts +371 -42
  36. package/src/runtime/strategy.ts +695 -33
  37. package/src/runtime/ta.ts +654 -20
  38. package/src/runtime/timeframe.ts +199 -0
@@ -1,4 +1,4 @@
1
- // Generated from /mnt/data/home/jango/Git/pynescript/src/pynescript/ast/grammar/antlr4/resource/PinescriptParser.g4 by ANTLR 4.13.2
1
+ // Generated from /home/jango/Git/pynescript/src/pynescript/ast/grammar/antlr4/resource/PinescriptParser.g4 by ANTLR 4.13.2
2
2
 
3
3
  import {ParseTreeVisitor} from 'antlr4';
4
4
 
@@ -31,6 +31,7 @@ import { Field_definitionContext } from "./PinescriptParser.ts";
31
31
  import { Enum_declarationContext } from "./PinescriptParser.ts";
32
32
  import { Enum_definitionsContext } from "./PinescriptParser.ts";
33
33
  import { Enum_definitionContext } from "./PinescriptParser.ts";
34
+ import { Value_structureContext } from "./PinescriptParser.ts";
34
35
  import { StructureContext } from "./PinescriptParser.ts";
35
36
  import { Structure_statementContext } from "./PinescriptParser.ts";
36
37
  import { Structure_expressionContext } from "./PinescriptParser.ts";
@@ -44,8 +45,11 @@ import { For_structure_inContext } from "./PinescriptParser.ts";
44
45
  import { For_iteratorContext } from "./PinescriptParser.ts";
45
46
  import { While_structureContext } from "./PinescriptParser.ts";
46
47
  import { Switch_structureContext } from "./PinescriptParser.ts";
48
+ import { Once_structureContext } from "./PinescriptParser.ts";
49
+ import { Switch_caseContext } from "./PinescriptParser.ts";
47
50
  import { Switch_casesContext } from "./PinescriptParser.ts";
48
51
  import { Switch_pattern_caseContext } from "./PinescriptParser.ts";
52
+ import { Switch_patternsContext } from "./PinescriptParser.ts";
49
53
  import { Switch_default_caseContext } from "./PinescriptParser.ts";
50
54
  import { Local_blockContext } from "./PinescriptParser.ts";
51
55
  import { Indented_local_blockContext } from "./PinescriptParser.ts";
@@ -113,6 +117,7 @@ import { Type_specificationContext } from "./PinescriptParser.ts";
113
117
  import { Type_qualifierContext } from "./PinescriptParser.ts";
114
118
  import { Attributed_type_nameContext } from "./PinescriptParser.ts";
115
119
  import { Template_spec_suffixContext } from "./PinescriptParser.ts";
120
+ import { Rshift_closed_type_argsContext } from "./PinescriptParser.ts";
116
121
  import { Array_type_suffixContext } from "./PinescriptParser.ts";
117
122
  import { Type_argument_listContext } from "./PinescriptParser.ts";
118
123
  import { NameContext } from "./PinescriptParser.ts";
@@ -298,6 +303,12 @@ export default class PinescriptParserVisitor<Result> extends ParseTreeVisitor<Re
298
303
  * @return the visitor result
299
304
  */
300
305
  visitEnum_definition?: (ctx: Enum_definitionContext) => Result;
306
+ /**
307
+ * Visit a parse tree produced by `PinescriptParser.value_structure`.
308
+ * @param ctx the parse tree
309
+ * @return the visitor result
310
+ */
311
+ visitValue_structure?: (ctx: Value_structureContext) => Result;
301
312
  /**
302
313
  * Visit a parse tree produced by `PinescriptParser.structure`.
303
314
  * @param ctx the parse tree
@@ -376,6 +387,18 @@ export default class PinescriptParserVisitor<Result> extends ParseTreeVisitor<Re
376
387
  * @return the visitor result
377
388
  */
378
389
  visitSwitch_structure?: (ctx: Switch_structureContext) => Result;
390
+ /**
391
+ * Visit a parse tree produced by `PinescriptParser.once_structure`.
392
+ * @param ctx the parse tree
393
+ * @return the visitor result
394
+ */
395
+ visitOnce_structure?: (ctx: Once_structureContext) => Result;
396
+ /**
397
+ * Visit a parse tree produced by `PinescriptParser.switch_case`.
398
+ * @param ctx the parse tree
399
+ * @return the visitor result
400
+ */
401
+ visitSwitch_case?: (ctx: Switch_caseContext) => Result;
379
402
  /**
380
403
  * Visit a parse tree produced by `PinescriptParser.switch_cases`.
381
404
  * @param ctx the parse tree
@@ -388,6 +411,12 @@ export default class PinescriptParserVisitor<Result> extends ParseTreeVisitor<Re
388
411
  * @return the visitor result
389
412
  */
390
413
  visitSwitch_pattern_case?: (ctx: Switch_pattern_caseContext) => Result;
414
+ /**
415
+ * Visit a parse tree produced by `PinescriptParser.switch_patterns`.
416
+ * @param ctx the parse tree
417
+ * @return the visitor result
418
+ */
419
+ visitSwitch_patterns?: (ctx: Switch_patternsContext) => Result;
391
420
  /**
392
421
  * Visit a parse tree produced by `PinescriptParser.switch_default_case`.
393
422
  * @param ctx the parse tree
@@ -794,6 +823,12 @@ export default class PinescriptParserVisitor<Result> extends ParseTreeVisitor<Re
794
823
  * @return the visitor result
795
824
  */
796
825
  visitTemplate_spec_suffix?: (ctx: Template_spec_suffixContext) => Result;
826
+ /**
827
+ * Visit a parse tree produced by `PinescriptParser.rshift_closed_type_args`.
828
+ * @param ctx the parse tree
829
+ * @return the visitor result
830
+ */
831
+ visitRshift_closed_type_args?: (ctx: Rshift_closed_type_argsContext) => Result;
797
832
  /**
798
833
  * Visit a parse tree produced by `PinescriptParser.array_type_suffix`.
799
834
  * @param ctx the parse tree
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export { Runtime, RuntimeStream, interpret, interpretTree } from "./runtime/inte
13
13
  export type {
14
14
  DrawingEvent,
15
15
  InputOverrides,
16
+ LibrarySourceSpec,
16
17
  OHLCVBar,
17
18
  RuntimeFill,
18
19
  RuntimeOptions,
@@ -20,6 +21,27 @@ export type {
20
21
  StrategyEvent,
21
22
  StreamEvent,
22
23
  } from "./runtime/interpret.ts";
24
+ export {
25
+ compileScript,
26
+ transpile,
27
+ compileEligible,
28
+ clearCompileCache,
29
+ compileCacheStats,
30
+ runScript,
31
+ compileToResult,
32
+ CompileError,
33
+ CompileEmitError,
34
+ CompileIneligibleError,
35
+ CompileLoadError,
36
+ } from "./runtime/compile/index.ts";
37
+ export type {
38
+ CompileBackend,
39
+ CompileCacheStats,
40
+ CompileEligibility,
41
+ CompilePlotMeta,
42
+ CompiledScript,
43
+ RuntimeMode,
44
+ } from "./runtime/compile/index.ts";
23
45
  export { NA, PineSeries } from "./runtime/series.ts";
24
46
  export { TaEngine } from "./runtime/ta.ts";
25
47
  export type { BrokerSettings, StrategySummary } from "./runtime/strategy.ts";
@@ -3,9 +3,13 @@
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  *
5
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.
6
+ * Set OOB is a no-op. Slots may hold numbers, na, UDT instances, strings, or bools.
7
+ * Python `ArrayBuiltinsMixin` remains the source of truth.
7
8
  */
8
9
 
10
+ import { UdtInstance } from "./udt.ts";
11
+
12
+ /** Numeric/na cell. Arrays may also store UDT instances, strings, and bools. */
9
13
  export type Cell = number | null;
10
14
 
11
15
  /** Python `array.set` grows only while index < 1_000_000; same bound here. */
@@ -19,28 +23,118 @@ function resolveIndex(index: number, length: number): number | null {
19
23
  return i;
20
24
  }
21
25
 
22
- function isFiniteCell(v: Cell): v is number {
23
- return v !== null && Number.isFinite(v);
26
+ function isFiniteCell(v: unknown): v is number {
27
+ return typeof v === "number" && Number.isFinite(v);
24
28
  }
25
29
 
26
30
  /** Python `bool` on array cells: only `na` and `0` are false (`NaN` is true). */
27
- function pineTruthy(v: Cell): boolean {
31
+ function pineTruthy(v: unknown): boolean {
28
32
  return v !== null && v !== 0;
29
33
  }
30
34
 
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;
35
+ function isSortNa(v: unknown): boolean {
36
+ if (v == null) return true;
37
+ if (typeof v === "number") return !Number.isFinite(v);
38
+ return false;
39
+ }
40
+
41
+ /** Python `_looks_like_udt` — ObjectInstance or compile-path `{__type__}` dict. */
42
+ function looksLikeUdt(item: unknown): boolean {
43
+ if (item == null || typeof item !== "object") return false;
44
+ if (item instanceof UdtInstance) return true;
45
+ return !Array.isArray(item) && Object.prototype.hasOwnProperty.call(item, "__type__");
46
+ }
47
+
48
+ function udtFieldNames(item: object): string[] | null {
49
+ if (item instanceof UdtInstance) return item.type.fields.map((f) => f.name);
50
+ const rec = item as Record<string, unknown>;
51
+ if (Object.prototype.hasOwnProperty.call(rec, "__type__")) {
52
+ return Object.keys(rec).filter((k) => k !== "__type__");
53
+ }
54
+ const udt = (item as { udt?: { fields?: Record<string, unknown> | Array<{ name: string }> } }).udt;
55
+ const fields = udt?.fields;
56
+ if (fields == null) return null;
57
+ if (Array.isArray(fields)) return fields.map((f) => f.name);
58
+ return Object.keys(fields);
59
+ }
60
+
61
+ function readUdtField(item: object, name: string): unknown {
62
+ if (item instanceof UdtInstance) return item.get(name);
63
+ const getField = (item as { get_field?: (n: string) => unknown }).get_field;
64
+ if (typeof getField === "function") {
65
+ try {
66
+ return getField.call(item, name);
67
+ } catch {
68
+ return item;
69
+ }
70
+ }
71
+ return (item as Record<string, unknown>)[name];
72
+ }
73
+
74
+ /** Python `_udt_field_key` — field name or int index; missing field → item. */
75
+ function udtFieldKey(item: unknown, sortField: unknown): unknown {
76
+ if (item == null || sortField == null) return item;
77
+ if (typeof item !== "object") return item;
78
+ if (typeof sortField === "string") {
79
+ const names = udtFieldNames(item);
80
+ if (names != null && !names.includes(sortField)) return item;
81
+ return readUdtField(item, sortField);
82
+ }
83
+ if (typeof sortField === "number" && Number.isFinite(sortField)) {
84
+ const names = udtFieldNames(item);
85
+ if (names == null) return item;
86
+ const idx = Math.trunc(sortField);
87
+ if (idx < 0 || idx >= names.length) return item;
88
+ return readUdtField(item, names[idx]!);
89
+ }
90
+ return item;
91
+ }
92
+
93
+ /** Python `_resolve_search_field` — default `0` (first field) on UDT arrays. */
94
+ function resolveSearchField(seq: readonly unknown[], sortField: unknown): unknown {
95
+ if (sortField != null) return sortField;
96
+ for (const item of seq) {
97
+ if (item == null) continue;
98
+ if (looksLikeUdt(item)) return 0;
99
+ break;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function searchElemKey(item: unknown, sortField: unknown): unknown {
105
+ if (sortField == null) return item;
106
+ return udtFieldKey(item, sortField);
107
+ }
108
+
109
+ function searchTargetKey(value: unknown, sortField: unknown): unknown {
110
+ if (sortField == null) return value;
111
+ if (looksLikeUdt(value)) return udtFieldKey(value, sortField);
112
+ return value;
113
+ }
114
+
115
+ /** Python `_key_lt`: `na` sorts last (never less). */
116
+ function keyLt(left: unknown, right: unknown): boolean {
117
+ if (left == null) return false;
118
+ if (right == null) return true;
119
+ try {
120
+ return (left as never) < (right as never);
121
+ } catch {
122
+ return `${String(Object.prototype.toString.call(left))}${String(left)}` <
123
+ `${String(Object.prototype.toString.call(right))}${String(right)}`;
124
+ }
36
125
  }
37
126
 
38
- function keyEq(left: Cell, right: Cell): boolean {
39
- if (left === null && right === null) return true;
40
- if (left === null || right === null) return false;
127
+ function keyEq(left: unknown, right: unknown): boolean {
128
+ if (left == null && right == null) return true;
129
+ if (left == null || right == null) return false;
41
130
  return left === right;
42
131
  }
43
132
 
133
+ function cmpKeys(a: unknown, b: unknown): number {
134
+ if (keyEq(a, b)) return 0;
135
+ return keyLt(a, b) ? -1 : 1;
136
+ }
137
+
44
138
  function meanOf(nums: number[]): number {
45
139
  let acc = 0;
46
140
  for (const v of nums) acc += v;
@@ -48,13 +142,13 @@ function meanOf(nums: number[]): number {
48
142
  }
49
143
 
50
144
  export class PineArray {
51
- private readonly cells: Cell[] = [];
145
+ private readonly cells: unknown[] = [];
52
146
 
53
- constructor(size?: number, initial?: Cell) {
147
+ constructor(size?: number, initial?: unknown) {
54
148
  if (size === undefined) return;
55
149
  if (!Number.isFinite(size) || size < 0) return;
56
150
  const n = Math.min(Math.trunc(size), MAX_ARRAY_SIZE);
57
- const fill: Cell = initial === undefined ? null : initial;
151
+ const fill = initial === undefined ? null : initial;
58
152
  for (let i = 0; i < n; i++) this.cells.push(fill);
59
153
  }
60
154
 
@@ -65,17 +159,17 @@ export class PineArray {
65
159
  /** `array.get` — OOB / non-finite index → `na`. Negative counts from the end. */
66
160
  get(index: number): Cell {
67
161
  const i = resolveIndex(index, this.cells.length);
68
- return i === null ? null : this.cells[i]!;
162
+ return (i === null ? null : this.cells[i]) as Cell;
69
163
  }
70
164
 
71
165
  /** `array.set` — OOB / non-finite index is a no-op. */
72
- set(index: number, value: Cell): void {
166
+ set(index: number, value: unknown): void {
73
167
  const i = resolveIndex(index, this.cells.length);
74
168
  if (i === null) return;
75
169
  this.cells[i] = value;
76
170
  }
77
171
 
78
- push(value: Cell): void {
172
+ push(value: unknown): void {
79
173
  if (this.cells.length >= MAX_ARRAY_SIZE) return;
80
174
  this.cells.push(value);
81
175
  }
@@ -83,10 +177,10 @@ export class PineArray {
83
177
  /** Last element, or `na` if empty. */
84
178
  pop(): Cell {
85
179
  if (this.cells.length === 0) return null;
86
- return this.cells.pop()!;
180
+ return this.cells.pop() as Cell;
87
181
  }
88
182
 
89
- unshift(value: Cell): void {
183
+ unshift(value: unknown): void {
90
184
  if (this.cells.length >= MAX_ARRAY_SIZE) return;
91
185
  this.cells.unshift(value);
92
186
  }
@@ -94,26 +188,26 @@ export class PineArray {
94
188
  /** First element, or `na` if empty. */
95
189
  shift(): Cell {
96
190
  if (this.cells.length === 0) return null;
97
- return this.cells.shift()!;
191
+ return this.cells.shift() as Cell;
98
192
  }
99
193
 
100
194
  clear(): void {
101
195
  this.cells.length = 0;
102
196
  }
103
197
 
104
- includes(value: Cell): boolean {
198
+ includes(value: unknown): boolean {
105
199
  return this.cells.includes(value);
106
200
  }
107
201
 
108
202
  first(): Cell {
109
- return this.cells.length === 0 ? null : this.cells[0]!;
203
+ return (this.cells.length === 0 ? null : this.cells[0]) as Cell;
110
204
  }
111
205
 
112
206
  last(): Cell {
113
- return this.cells.length === 0 ? null : this.cells[this.cells.length - 1]!;
207
+ return (this.cells.length === 0 ? null : this.cells[this.cells.length - 1]) as Cell;
114
208
  }
115
209
 
116
- insert(index: number, value: Cell): void {
210
+ insert(index: number, value: unknown): void {
117
211
  if (this.cells.length >= MAX_ARRAY_SIZE) return;
118
212
  if (!Number.isFinite(index)) return;
119
213
  let i = Math.trunc(index);
@@ -126,10 +220,10 @@ export class PineArray {
126
220
  remove(index: number): Cell {
127
221
  const i = resolveIndex(index, this.cells.length);
128
222
  if (i === null) return null;
129
- return this.cells.splice(i, 1)[0]!;
223
+ return this.cells.splice(i, 1)[0] as Cell;
130
224
  }
131
225
 
132
- fill(value: Cell): void {
226
+ fill(value: unknown): void {
133
227
  for (let i = 0; i < this.cells.length; i++) this.cells[i] = value;
134
228
  }
135
229
 
@@ -155,17 +249,14 @@ export class PineArray {
155
249
  this.cells.reverse();
156
250
  }
157
251
 
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
- });
252
+ /** In-place; `na` last. Optional *sortField* keys UDT cells (name or index). */
253
+ sort(order: "asc" | "desc" = "asc", sortField?: unknown): void {
254
+ const reverse = order === "desc";
255
+ this.cells.splice(0, this.cells.length, ...sortWithNaLast(this.cells, reverse, sortField));
165
256
  }
166
257
 
167
258
  /** `array.indexof` — miss is `-1` (Python `ArrayBuiltinsMixin`). */
168
- indexof(value: Cell): Cell {
259
+ indexof(value: unknown): Cell {
169
260
  for (let i = 0; i < this.cells.length; i++) {
170
261
  if (Object.is(this.cells[i], value)) return i;
171
262
  }
@@ -210,7 +301,7 @@ export class PineArray {
210
301
  }
211
302
 
212
303
  /** `array.from(...)` — collect args; capped at `MAX_ARRAY_SIZE`. */
213
- static from(...values: Cell[]): PineArray {
304
+ static from(...values: unknown[]): PineArray {
214
305
  const out = new PineArray();
215
306
  const n = Math.min(values.length, MAX_ARRAY_SIZE);
216
307
  for (let i = 0; i < n; i++) out.push(values[i]!);
@@ -218,7 +309,7 @@ export class PineArray {
218
309
  }
219
310
 
220
311
  /** `array.lastindexof` — miss is `-1`. */
221
- lastIndexOf(value: Cell): Cell {
312
+ lastIndexOf(value: unknown): Cell {
222
313
  for (let i = this.cells.length - 1; i >= 0; i--) {
223
314
  if (Object.is(this.cells[i], value)) return i;
224
315
  }
@@ -234,10 +325,14 @@ export class PineArray {
234
325
  return this;
235
326
  }
236
327
 
237
- /** Element-wise `abs`; `na` stays `na`. */
328
+ /** Element-wise `abs`; `na` stays `na`. Non-numeric slots become `na`. */
238
329
  abs(): PineArray | null {
239
330
  const out = new PineArray();
240
- for (const v of this.cells) out.push(v === null ? null : Math.abs(v));
331
+ for (const v of this.cells) {
332
+ if (v == null) out.push(null);
333
+ else if (typeof v === "number") out.push(Math.abs(v));
334
+ else out.push(null);
335
+ }
241
336
  return out;
242
337
  }
243
338
 
@@ -293,24 +388,32 @@ export class PineArray {
293
388
  }
294
389
 
295
390
  /** Index of `value` in an ascending array (`na` last), or `-1`. */
296
- binarySearch(value: Cell): number {
297
- return this.binarySearchBy(value, "any");
391
+ binarySearch(value: unknown, sortField?: unknown): number {
392
+ return this.binarySearchBy(value, "any", sortField);
298
393
  }
299
394
 
300
- binarySearchLeftmost(value: Cell): number {
301
- if (value === null || this.cells.some((x) => x === null)) {
395
+ binarySearchLeftmost(value: unknown, sortField?: unknown): number {
396
+ if (
397
+ sortField == null &&
398
+ !this.cells.some((x) => x != null && looksLikeUdt(x)) &&
399
+ (value == null || this.cells.some((x) => x == null))
400
+ ) {
302
401
  const i = this.indexof(value);
303
402
  return typeof i === "number" ? i : -1;
304
403
  }
305
- return this.binarySearchBy(value, "left");
404
+ return this.binarySearchBy(value, "left", sortField);
306
405
  }
307
406
 
308
- binarySearchRightmost(value: Cell): number {
309
- if (value === null || this.cells.some((x) => x === null)) {
407
+ binarySearchRightmost(value: unknown, sortField?: unknown): number {
408
+ if (
409
+ sortField == null &&
410
+ !this.cells.some((x) => x != null && looksLikeUdt(x)) &&
411
+ (value == null || this.cells.some((x) => x == null))
412
+ ) {
310
413
  const i = this.lastIndexOf(value);
311
414
  return typeof i === "number" ? i : -1;
312
415
  }
313
- return this.binarySearchBy(value, "right");
416
+ return this.binarySearchBy(value, "right", sortField);
314
417
  }
315
418
 
316
419
  /** Default `biased=true` is population (`n`); `false` is sample (`n-1`). Skips `na`. */
@@ -377,7 +480,7 @@ export class PineArray {
377
480
  return sorted[Math.min(rank, n) - 1]!;
378
481
  }
379
482
 
380
- percentrank(value: Cell): Cell {
483
+ percentrank(value: unknown): Cell {
381
484
  if (!isFiniteCell(value)) return null;
382
485
  const nums = this.finiteSkip();
383
486
  if (nums.length === 0) return null;
@@ -404,38 +507,52 @@ export class PineArray {
404
507
  }
405
508
 
406
509
  /** Indices that would sort this array; `na` indices last. */
407
- sortIndices(order: "asc" | "desc" = "asc"): number[] | null {
408
- const nonNa: { key: number; idx: number }[] = [];
510
+ sortIndices(order: "asc" | "desc" = "asc", sortField?: unknown): number[] | null {
511
+ const reverse = order === "desc";
512
+ const nonNa: { key: unknown; idx: number }[] = [];
409
513
  const naIdx: number[] = [];
410
514
  for (let i = 0; i < this.cells.length; i++) {
411
- const v = this.cells[i]!;
412
- if (!isFiniteCell(v)) {
515
+ const v = this.cells[i];
516
+ if (isSortNa(v)) {
413
517
  naIdx.push(i);
414
518
  continue;
415
519
  }
416
- nonNa.push({ key: v, idx: i });
520
+ const key = sortField != null ? udtFieldKey(v, sortField) : v;
521
+ if (isSortNa(key)) naIdx.push(i);
522
+ else nonNa.push({ key, idx: i });
523
+ }
524
+ try {
525
+ nonNa.sort((a, b) => {
526
+ const cmp = cmpKeys(a.key, b.key);
527
+ if (cmp !== 0) return reverse ? -cmp : cmp;
528
+ return a.idx - b.idx;
529
+ });
530
+ } catch {
531
+ nonNa.sort((a, b) => {
532
+ const ka = `${String(Object.prototype.toString.call(a.key))}${String(a.key)}`;
533
+ const kb = `${String(Object.prototype.toString.call(b.key))}${String(b.key)}`;
534
+ const cmp = ka < kb ? -1 : ka > kb ? 1 : 0;
535
+ if (cmp !== 0) return reverse ? -cmp : cmp;
536
+ return a.idx - b.idx;
537
+ });
417
538
  }
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
539
  return nonNa.map((x) => x.idx).concat(naIdx);
425
540
  }
426
541
 
427
- private binarySearchBy(value: Cell, side: "any" | "left" | "right"): number {
542
+ private binarySearchBy(value: unknown, side: "any" | "left" | "right", sortField?: unknown): number {
428
543
  const seq = this.cells;
544
+ const field = resolveSearchField(seq, sortField ?? null);
545
+ const target = searchTargetKey(value, field);
429
546
  const n = seq.length;
430
547
  if (side === "left") {
431
548
  let left = 0;
432
549
  let right = n;
433
550
  while (left < right) {
434
551
  const mid = Math.trunc((left + right) / 2);
435
- if (keyLt(seq[mid]!, value)) left = mid + 1;
552
+ if (keyLt(searchElemKey(seq[mid], field), target)) left = mid + 1;
436
553
  else right = mid;
437
554
  }
438
- if (left < n && keyEq(seq[left]!, value)) return left;
555
+ if (left < n && keyEq(searchElemKey(seq[left], field), target)) return left;
439
556
  return -1;
440
557
  }
441
558
  if (side === "right") {
@@ -443,19 +560,19 @@ export class PineArray {
443
560
  let right = n;
444
561
  while (left < right) {
445
562
  const mid = Math.trunc((left + right) / 2);
446
- if (keyLt(value, seq[mid]!)) right = mid;
563
+ if (keyLt(target, searchElemKey(seq[mid], field))) right = mid;
447
564
  else left = mid + 1;
448
565
  }
449
- if (left > 0 && keyEq(seq[left - 1]!, value)) return left - 1;
566
+ if (left > 0 && keyEq(searchElemKey(seq[left - 1], field), target)) return left - 1;
450
567
  return -1;
451
568
  }
452
569
  let lo = 0;
453
570
  let hi = n - 1;
454
571
  while (lo <= hi) {
455
572
  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;
573
+ const midK = searchElemKey(seq[mid], field);
574
+ if (keyEq(midK, target)) return mid;
575
+ if (keyLt(midK, target)) lo = mid + 1;
459
576
  else hi = mid - 1;
460
577
  }
461
578
  return -1;
@@ -481,6 +598,59 @@ export class PineArray {
481
598
  }
482
599
 
483
600
  toValues(): Cell[] {
484
- return this.cells.slice();
601
+ return this.cells.slice() as Cell[];
602
+ }
603
+ }
604
+
605
+ /** Python `_sort_with_na_last`. */
606
+ function sortWithNaLast(sequence: readonly unknown[], reverse: boolean, sortField?: unknown): unknown[] {
607
+ if (sortField == null) {
608
+ const nonNa: unknown[] = [];
609
+ let naCount = 0;
610
+ for (const x of sequence) {
611
+ if (isSortNa(x)) naCount += 1;
612
+ else nonNa.push(x);
613
+ }
614
+ try {
615
+ nonNa.sort((a, b) => {
616
+ const cmp = cmpKeys(a, b);
617
+ return reverse ? -cmp : cmp;
618
+ });
619
+ } catch {
620
+ nonNa.sort((a, b) => {
621
+ const ka = `${String(Object.prototype.toString.call(a))}${String(a)}`;
622
+ const kb = `${String(Object.prototype.toString.call(b))}${String(b)}`;
623
+ const cmp = ka < kb ? -1 : ka > kb ? 1 : 0;
624
+ return reverse ? -cmp : cmp;
625
+ });
626
+ }
627
+ const out = nonNa.slice();
628
+ for (let i = 0; i < naCount; i++) out.push(null);
629
+ return out;
630
+ }
631
+ const keyed: { key: unknown; item: unknown }[] = [];
632
+ const naItems: unknown[] = [];
633
+ for (const item of sequence) {
634
+ if (isSortNa(item)) {
635
+ naItems.push(item);
636
+ continue;
637
+ }
638
+ const key = udtFieldKey(item, sortField);
639
+ if (isSortNa(key)) naItems.push(item);
640
+ else keyed.push({ key, item });
641
+ }
642
+ try {
643
+ keyed.sort((a, b) => {
644
+ const cmp = cmpKeys(a.key, b.key);
645
+ return reverse ? -cmp : cmp;
646
+ });
647
+ } catch {
648
+ keyed.sort((a, b) => {
649
+ const ka = `${String(Object.prototype.toString.call(a.key))}${String(a.key)}`;
650
+ const kb = `${String(Object.prototype.toString.call(b.key))}${String(b.key)}`;
651
+ const cmp = ka < kb ? -1 : ka > kb ? 1 : 0;
652
+ return reverse ? -cmp : cmp;
653
+ });
485
654
  }
655
+ return keyed.map((x) => x.item).concat(naItems);
486
656
  }