oakscriptjs 0.9.4 → 0.9.6

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 (177) hide show
  1. package/dist/cjs/array/index.d.ts +88 -27
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +257 -117
  4. package/dist/cjs/array/index.js.map +3 -3
  5. package/dist/cjs/callsite/index.d.ts +122 -4
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +102 -3
  8. package/dist/cjs/callsite/index.js.map +2 -2
  9. package/dist/cjs/color/index.d.ts +7 -3
  10. package/dist/cjs/color/index.d.ts.map +1 -1
  11. package/dist/cjs/color/index.js +6 -1
  12. package/dist/cjs/color/index.js.map +2 -2
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +2 -1
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/linefill/index.d.ts.map +1 -1
  20. package/dist/cjs/linefill/index.js +1 -0
  21. package/dist/cjs/linefill/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts +79 -22
  23. package/dist/cjs/math/index.d.ts.map +1 -1
  24. package/dist/cjs/math/index.js +157 -43
  25. package/dist/cjs/math/index.js.map +3 -3
  26. package/dist/cjs/matrix/index.d.ts.map +1 -1
  27. package/dist/cjs/matrix/index.js +42 -36
  28. package/dist/cjs/matrix/index.js.map +2 -2
  29. package/dist/cjs/runtime/series.d.ts +9 -0
  30. package/dist/cjs/runtime/series.d.ts.map +1 -1
  31. package/dist/cjs/runtime/series.js +16 -2
  32. package/dist/cjs/runtime/series.js.map +2 -2
  33. package/dist/cjs/script/index.d.ts +15 -31
  34. package/dist/cjs/script/index.d.ts.map +1 -1
  35. package/dist/cjs/script/index.js +32 -0
  36. package/dist/cjs/script/index.js.map +2 -2
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +15 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/str/index.d.ts +50 -38
  41. package/dist/cjs/str/index.d.ts.map +1 -1
  42. package/dist/cjs/str/index.js +43 -15
  43. package/dist/cjs/str/index.js.map +2 -2
  44. package/dist/cjs/str/messageformat.d.ts +2 -1
  45. package/dist/cjs/str/messageformat.d.ts.map +1 -1
  46. package/dist/cjs/str/messageformat.js +4 -1
  47. package/dist/cjs/str/messageformat.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -0
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +26 -4
  51. package/dist/cjs/str/numberformat.js.map +3 -3
  52. package/dist/cjs/ta/index.d.ts +95 -46
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +193 -318
  55. package/dist/cjs/ta/index.js.map +3 -3
  56. package/dist/cjs/ta/running-sum.d.ts +54 -1
  57. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  58. package/dist/cjs/ta/running-sum.js +132 -47
  59. package/dist/cjs/ta/running-sum.js.map +2 -2
  60. package/dist/cjs/ta-series.d.ts +4 -4
  61. package/dist/cjs/ta-series.d.ts.map +1 -1
  62. package/dist/cjs/ta-series.js +4 -3
  63. package/dist/cjs/ta-series.js.map +2 -2
  64. package/dist/cjs/time/datestring.d.ts +1 -0
  65. package/dist/cjs/time/datestring.d.ts.map +1 -1
  66. package/dist/cjs/time/datestring.js +7 -0
  67. package/dist/cjs/time/datestring.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts.map +1 -1
  69. package/dist/cjs/time/index.js +2 -1
  70. package/dist/cjs/time/index.js.map +2 -2
  71. package/dist/cjs/time/session.d.ts +28 -9
  72. package/dist/cjs/time/session.d.ts.map +1 -1
  73. package/dist/cjs/time/session.js +74 -28
  74. package/dist/cjs/time/session.js.map +2 -2
  75. package/dist/cjs/time/timezone.d.ts +5 -0
  76. package/dist/cjs/time/timezone.d.ts.map +1 -1
  77. package/dist/cjs/time/timezone.js +15 -1
  78. package/dist/cjs/time/timezone.js.map +2 -2
  79. package/dist/esm/array/index.d.ts +88 -27
  80. package/dist/esm/array/index.d.ts.map +1 -1
  81. package/dist/esm/array/index.js +408 -157
  82. package/dist/esm/array/index.js.map +1 -1
  83. package/dist/esm/callsite/index.d.ts +122 -4
  84. package/dist/esm/callsite/index.d.ts.map +1 -1
  85. package/dist/esm/callsite/index.js +228 -8
  86. package/dist/esm/callsite/index.js.map +1 -1
  87. package/dist/esm/color/index.d.ts +7 -3
  88. package/dist/esm/color/index.d.ts.map +1 -1
  89. package/dist/esm/color/index.js +15 -3
  90. package/dist/esm/color/index.js.map +1 -1
  91. package/dist/esm/index.d.ts +1 -1
  92. package/dist/esm/index.js +1 -1
  93. package/dist/esm/label/index.d.ts.map +1 -1
  94. package/dist/esm/label/index.js +2 -1
  95. package/dist/esm/label/index.js.map +1 -1
  96. package/dist/esm/linefill/index.d.ts.map +1 -1
  97. package/dist/esm/linefill/index.js +3 -0
  98. package/dist/esm/linefill/index.js.map +1 -1
  99. package/dist/esm/math/index.d.ts +79 -22
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +229 -63
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/matrix/index.d.ts.map +1 -1
  104. package/dist/esm/matrix/index.js +57 -43
  105. package/dist/esm/matrix/index.js.map +1 -1
  106. package/dist/esm/runtime/series.d.ts +9 -0
  107. package/dist/esm/runtime/series.d.ts.map +1 -1
  108. package/dist/esm/runtime/series.js +18 -1
  109. package/dist/esm/runtime/series.js.map +1 -1
  110. package/dist/esm/script/index.d.ts +15 -31
  111. package/dist/esm/script/index.d.ts.map +1 -1
  112. package/dist/esm/script/index.js +44 -1
  113. package/dist/esm/script/index.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts.map +1 -1
  115. package/dist/esm/session/calendar.js +22 -2
  116. package/dist/esm/session/calendar.js.map +1 -1
  117. package/dist/esm/str/index.d.ts +50 -38
  118. package/dist/esm/str/index.d.ts.map +1 -1
  119. package/dist/esm/str/index.js +103 -52
  120. package/dist/esm/str/index.js.map +1 -1
  121. package/dist/esm/str/messageformat.d.ts +2 -1
  122. package/dist/esm/str/messageformat.d.ts.map +1 -1
  123. package/dist/esm/str/messageformat.js +7 -2
  124. package/dist/esm/str/messageformat.js.map +1 -1
  125. package/dist/esm/str/numberformat.d.ts +4 -0
  126. package/dist/esm/str/numberformat.d.ts.map +1 -1
  127. package/dist/esm/str/numberformat.js +38 -4
  128. package/dist/esm/str/numberformat.js.map +1 -1
  129. package/dist/esm/ta/index.d.ts +95 -46
  130. package/dist/esm/ta/index.d.ts.map +1 -1
  131. package/dist/esm/ta/index.js +341 -414
  132. package/dist/esm/ta/index.js.map +1 -1
  133. package/dist/esm/ta/running-sum.d.ts +54 -1
  134. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  135. package/dist/esm/ta/running-sum.js +166 -60
  136. package/dist/esm/ta/running-sum.js.map +1 -1
  137. package/dist/esm/ta-series.d.ts +4 -4
  138. package/dist/esm/ta-series.d.ts.map +1 -1
  139. package/dist/esm/ta-series.js +5 -4
  140. package/dist/esm/ta-series.js.map +1 -1
  141. package/dist/esm/time/datestring.d.ts +1 -0
  142. package/dist/esm/time/datestring.d.ts.map +1 -1
  143. package/dist/esm/time/datestring.js +12 -0
  144. package/dist/esm/time/datestring.js.map +1 -1
  145. package/dist/esm/time/index.d.ts.map +1 -1
  146. package/dist/esm/time/index.js +3 -1
  147. package/dist/esm/time/index.js.map +1 -1
  148. package/dist/esm/time/session.d.ts +28 -9
  149. package/dist/esm/time/session.d.ts.map +1 -1
  150. package/dist/esm/time/session.js +113 -42
  151. package/dist/esm/time/session.js.map +1 -1
  152. package/dist/esm/time/timezone.d.ts +5 -0
  153. package/dist/esm/time/timezone.d.ts.map +1 -1
  154. package/dist/esm/time/timezone.js +21 -1
  155. package/dist/esm/time/timezone.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/array/index.ts +382 -174
  158. package/src/callsite/index.ts +232 -8
  159. package/src/color/index.ts +15 -5
  160. package/src/index.ts +1 -1
  161. package/src/label/index.ts +2 -1
  162. package/src/linefill/index.ts +2 -0
  163. package/src/math/index.ts +261 -77
  164. package/src/matrix/index.ts +52 -52
  165. package/src/runtime/series.ts +22 -1
  166. package/src/script/index.ts +53 -1
  167. package/src/session/calendar.ts +22 -2
  168. package/src/str/index.ts +95 -51
  169. package/src/str/messageformat.ts +7 -2
  170. package/src/str/numberformat.ts +39 -4
  171. package/src/ta/index.ts +356 -458
  172. package/src/ta/running-sum.ts +187 -62
  173. package/src/ta-series.ts +8 -7
  174. package/src/time/datestring.ts +10 -0
  175. package/src/time/index.ts +3 -1
  176. package/src/time/session.ts +97 -47
  177. package/src/time/timezone.ts +21 -1
@@ -18,17 +18,47 @@ export function new_array(size = 0, initial_value) {
18
18
  export function size(id) {
19
19
  return id.length;
20
20
  }
21
+ /** True for a PineScript `na` element: NaN, null or undefined. */
22
+ function isNa(v) {
23
+ return v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v));
24
+ }
25
+ /** The `na` of the array's element type: NaN for a numeric (or empty) array, undefined otherwise. */
26
+ function naOf(id) {
27
+ return (id.length === 0 || typeof id[0] === 'number' ? NaN : undefined);
28
+ }
21
29
  /**
22
- * Returns the element at the specified index
30
+ * Position addressed by a PineScript index: truncated, a negative index counts from the end. An index outside
31
+ * `-size .. size - 1 + extra` is a runtime error.
32
+ */
33
+ function position(name, id, index, extra = 0) {
34
+ const i = Math.trunc(index);
35
+ const n = id.length;
36
+ if (i < -n || i >= n + extra) {
37
+ throw new Error(`array.${name}: index ${i} is out of bounds, array size is ${n}`);
38
+ }
39
+ return i < 0 ? i + n : i;
40
+ }
41
+ /**
42
+ * Returns the element at the specified index.
43
+ *
44
+ * PineScript rules: a negative index counts from the end (`get(a, -1)` is the last element); an index outside
45
+ * `-size .. size - 1` is a runtime error (throws); an `na` index gives `na`.
23
46
  */
24
47
  export function get(id, index) {
25
- return id[index];
48
+ if (Number.isNaN(index))
49
+ return naOf(id);
50
+ return id[position('get', id, index)];
26
51
  }
27
52
  /**
28
- * Sets the value of the element at the specified index
53
+ * Sets the value of the element at the specified index.
54
+ *
55
+ * PineScript rules: a negative index counts from the end; an index outside `-size .. size - 1` is a runtime error
56
+ * (throws); an `na` index does nothing.
29
57
  */
30
58
  export function set(id, index, value) {
31
- id[index] = value;
59
+ if (Number.isNaN(index))
60
+ return;
61
+ id[position('set', id, index)] = value;
32
62
  }
33
63
  /**
34
64
  * Appends a value to the end of the array
@@ -61,34 +91,72 @@ export function clear(id) {
61
91
  id.length = 0;
62
92
  }
63
93
  /**
64
- * Inserts a value at the specified index
94
+ * Inserts a value at the specified index.
95
+ *
96
+ * PineScript rules: a negative index counts from the end and `size` appends, so `-size .. size` is valid; any other
97
+ * index is a runtime error (throws); an `na` index appends.
65
98
  */
66
99
  export function insert(id, index, value) {
67
- id.splice(index, 0, value);
100
+ if (Number.isNaN(index)) {
101
+ id.push(value);
102
+ return;
103
+ }
104
+ id.splice(position('insert', id, index, 1), 0, value);
68
105
  }
69
106
  /**
70
- * Removes the element at the specified index
107
+ * Removes the element at the specified index and returns it.
108
+ *
109
+ * PineScript rules: a negative index counts from the end; an index outside `-size .. size - 1` is a runtime error
110
+ * (throws); an `na` index removes nothing and gives `na`.
71
111
  */
72
112
  export function remove(id, index) {
73
- return id.splice(index, 1)[0];
113
+ if (Number.isNaN(index))
114
+ return naOf(id);
115
+ return id.splice(position('remove', id, index), 1)[0];
116
+ }
117
+ /** PineScript `==` of two elements: numbers are equal when |a - b| <= 1e-10; `na` never equals anything. */
118
+ function pineEqual(a, b) {
119
+ if (isNa(a) || isNa(b))
120
+ return false;
121
+ if (a === b)
122
+ return true;
123
+ if (typeof a === 'number' && typeof b === 'number') {
124
+ const d = a - b;
125
+ return -1e-10 <= d && d <= 1e-10;
126
+ }
127
+ return false;
74
128
  }
75
129
  /**
76
- * Returns true if the array contains the value
130
+ * Returns true if the array contains the value.
131
+ *
132
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
77
133
  */
78
134
  export function includes(id, value) {
79
- return id.includes(value);
135
+ return indexof(id, value) >= 0;
80
136
  }
81
137
  /**
82
- * Returns the index of the first occurrence of the value
138
+ * Returns the index of the first occurrence of the value, -1 when not found.
139
+ *
140
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
83
141
  */
84
142
  export function indexof(id, value) {
85
- return id.indexOf(value);
143
+ for (let i = 0; i < id.length; i++) {
144
+ if (pineEqual(id[i], value))
145
+ return i;
146
+ }
147
+ return -1;
86
148
  }
87
149
  /**
88
- * Returns the index of the last occurrence of the value
150
+ * Returns the index of the last occurrence of the value, -1 when not found.
151
+ *
152
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
89
153
  */
90
154
  export function lastindexof(id, value) {
91
- return id.lastIndexOf(value);
155
+ for (let i = id.length - 1; i >= 0; i--) {
156
+ if (pineEqual(id[i], value))
157
+ return i;
158
+ }
159
+ return -1;
92
160
  }
93
161
  /**
94
162
  * Returns a shallow copy of the array
@@ -97,10 +165,12 @@ export function copy(id) {
97
165
  return [...id];
98
166
  }
99
167
  /**
100
- * Concatenates two arrays
168
+ * Appends the elements of `id2` to `id1` and returns `id1` (PineScript `array.concat` changes `id1`).
101
169
  */
102
170
  export function concat(id1, id2) {
103
- return [...id1, ...id2];
171
+ for (const v of Array.from(id2))
172
+ id1.push(v);
173
+ return id1;
104
174
  }
105
175
  /**
106
176
  * Joins all elements into a string
@@ -115,29 +185,139 @@ export function reverse(id) {
115
185
  id.reverse();
116
186
  }
117
187
  /**
118
- * Returns a slice of the array
188
+ * Returns the elements from `index_from` (included) to `index_to` (excluded, default: the size) as a live view of
189
+ * the array (PineScript `array.slice`).
190
+ *
191
+ * PineScript rules:
192
+ * - the slice shares the elements of the original array: a change of an element of the slice changes the original
193
+ * array, and adding / removing elements of the slice adds / removes them in the original array, inside the slice
194
+ * bounds (a push to the slice inserts at the end of the slice)
195
+ * - `index_from` must address an element (`0 .. size - 1`) and `index_to` may be `size`; other bounds, or
196
+ * `index_from > index_to`, are a runtime error (throws); an `na` bound means the start / the end of the array
119
197
  */
120
198
  export function slice(id, index_from, index_to) {
121
- return id.slice(index_from, index_to);
199
+ const n = id.length;
200
+ const start = Number.isNaN(index_from) ? 0 : Math.trunc(index_from);
201
+ const stop = index_to === undefined || Number.isNaN(index_to) ? n : Math.trunc(index_to);
202
+ if (start < 0 || start >= n)
203
+ throw new Error(`array.slice: index ${start} is out of bounds, array size is ${n}`);
204
+ if (stop < 0 || stop > n)
205
+ throw new Error(`array.slice: index ${stop} is out of bounds, array size is ${n}`);
206
+ if (start > stop)
207
+ throw new Error(`array.slice: index_from ${start} is greater than index_to ${stop}`);
208
+ return sliceView(id, start, stop - start);
209
+ }
210
+ /** Index of a property key that is an array index, -1 otherwise. */
211
+ function indexKey(p) {
212
+ if (typeof p !== 'string')
213
+ return -1;
214
+ const i = Number(p);
215
+ return Number.isInteger(i) && i >= 0 && String(i) === p ? i : -1;
122
216
  }
123
217
  /**
124
- * Sorts array elements (numeric)
218
+ * Live view of `size` elements of `parent` from `start`: an array (a Proxy) whose element reads and writes go to
219
+ * `parent`, and whose size changes insert / remove elements in `parent` at the end of the view.
125
220
  */
126
- export function sort(id, order = 'asc') {
127
- id.sort((a, b) => {
128
- if (order === 'asc') {
129
- return a - b;
130
- }
131
- else {
132
- return b - a;
133
- }
221
+ function sliceView(parent, start, size) {
222
+ const resize = (target) => {
223
+ if (target > size)
224
+ parent.splice(start + size, 0, ...Array(target - size).fill(undefined));
225
+ else if (target < size)
226
+ parent.splice(start + target, size - target);
227
+ size = target;
228
+ };
229
+ return new Proxy([], {
230
+ get(target, p, receiver) {
231
+ const i = indexKey(p);
232
+ if (i >= 0)
233
+ return i < size ? parent[start + i] : undefined;
234
+ if (p === 'length')
235
+ return size;
236
+ return Reflect.get(target, p, receiver);
237
+ },
238
+ set(target, p, value, receiver) {
239
+ const i = indexKey(p);
240
+ if (i >= 0) {
241
+ if (i >= size)
242
+ resize(i + 1);
243
+ parent[start + i] = value;
244
+ return true;
245
+ }
246
+ if (p === 'length') {
247
+ resize(Number(value));
248
+ return true;
249
+ }
250
+ return Reflect.set(target, p, value, receiver);
251
+ },
252
+ has(target, p) {
253
+ const i = indexKey(p);
254
+ return i >= 0 ? i < size : Reflect.has(target, p);
255
+ },
256
+ // Array methods delete the slots past the new end before they set the length, which does the removal.
257
+ deleteProperty(target, p) {
258
+ return indexKey(p) >= 0 ? true : Reflect.deleteProperty(target, p);
259
+ },
260
+ ownKeys(target) {
261
+ return [...Array.from({ length: size }, (_, i) => String(i)), ...Reflect.ownKeys(target)];
262
+ },
263
+ getOwnPropertyDescriptor(target, p) {
264
+ const i = indexKey(p);
265
+ if (i >= 0) {
266
+ return i < size ? { value: parent[start + i], writable: true, enumerable: true, configurable: true } : undefined;
267
+ }
268
+ if (p === 'length')
269
+ return { value: size, writable: true, enumerable: false, configurable: false };
270
+ return Reflect.getOwnPropertyDescriptor(target, p);
271
+ },
134
272
  });
135
273
  }
136
274
  /**
137
- * Returns the sum of all elements (numeric arrays only)
275
+ * Positions of the elements in PineScript sorted order: ascending values, `na` after the numbers (before the
276
+ * strings of a string array), stable; descending is the ascending result reversed.
277
+ */
278
+ function sortedPositions(id, order) {
279
+ const values = [];
280
+ const nas = [];
281
+ for (let i = 0; i < id.length; i++)
282
+ (isNa(id[i]) ? nas : values).push(i);
283
+ values.sort((a, b) => {
284
+ const x = id[a];
285
+ const y = id[b];
286
+ return x < y ? -1 : x > y ? 1 : 0;
287
+ });
288
+ const naFirst = values.length > 0 && typeof id[values[0]] === 'string';
289
+ const positions = naFirst ? [...nas, ...values] : [...values, ...nas];
290
+ if (order === 'desc' || order === 'descending')
291
+ positions.reverse();
292
+ return positions;
293
+ }
294
+ /**
295
+ * Sorts the array in place.
296
+ *
297
+ * PineScript rules: `na` goes after the numbers in ascending order and before them in descending order (the
298
+ * ascending result reversed); in a string array `na` goes first in ascending order.
299
+ */
300
+ export function sort(id, order = 'asc') {
301
+ const sorted = sortedPositions(id, order).map((p) => id[p]);
302
+ for (let i = 0; i < sorted.length; i++)
303
+ id[i] = sorted[i];
304
+ }
305
+ /**
306
+ * Sum of the non-na elements (PineScript `array.sum(id)`), added from left to right; an array without values gives NaN.
307
+ * As PineScript, the running sum is set to 0 after a step when its absolute value is <= 1e-10.
138
308
  */
139
309
  export function sum(id) {
140
- return id.reduce((acc, val) => acc + val, 0);
310
+ let total = 0;
311
+ let count = 0;
312
+ for (const v of id) {
313
+ if (Number.isNaN(v))
314
+ continue;
315
+ count++;
316
+ total += v;
317
+ if (Math.abs(total) <= 1e-10)
318
+ total = 0;
319
+ }
320
+ return count ? total : NaN;
141
321
  }
142
322
  /**
143
323
  * Average of the non-na elements (PineScript `array.avg(id)`); an array without values gives NaN.
@@ -146,72 +326,115 @@ export function avg(id) {
146
326
  const values = id.filter((v) => !Number.isNaN(v));
147
327
  return values.length ? values.reduce((a, b) => a + b, 0) / values.length : NaN;
148
328
  }
329
+ /** The `nth` value of the non-na elements sorted with `compare` (array.min / array.max). */
330
+ function nthValue(name, id, nth, compare) {
331
+ const rank = Number.isNaN(nth) ? 0 : Math.trunc(nth);
332
+ const values = id.filter((v) => !Number.isNaN(v));
333
+ if (!values.length)
334
+ return NaN;
335
+ if (rank < 0 || rank >= id.length) {
336
+ throw new Error(`array.${name}: nth ${rank} is out of bounds, array size is ${id.length}`);
337
+ }
338
+ values.sort(compare);
339
+ return values[Math.min(rank, values.length - 1)];
340
+ }
149
341
  /**
150
342
  * The `nth` smallest value (0 = the smallest) of the non-na elements (PineScript `array.min(id, nth)`).
151
- * An `nth` past the end gives the greatest value; an array without values gives NaN.
343
+ *
344
+ * PineScript rules: an `nth` past the non-na values but inside the array size gives the greatest value; an `nth`
345
+ * outside `0 .. size - 1` is a runtime error (throws); an `na` nth counts as 0; an array without values gives NaN.
152
346
  */
153
347
  export function min(id, nth = 0) {
154
- const values = id.filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
155
- return values.length ? values[Math.min(nth, values.length - 1)] : NaN;
348
+ return nthValue('min', id, nth, (a, b) => a - b);
156
349
  }
157
350
  /**
158
351
  * The `nth` greatest value (0 = the greatest) of the non-na elements (PineScript `array.max(id, nth)`).
159
- * An `nth` past the end gives the smallest value; an array without values gives NaN.
352
+ *
353
+ * PineScript rules: an `nth` past the non-na values but inside the array size gives the smallest value; an `nth`
354
+ * outside `0 .. size - 1` is a runtime error (throws); an `na` nth counts as 0; an array without values gives NaN.
160
355
  */
161
356
  export function max(id, nth = 0) {
162
- const values = id.filter((v) => !Number.isNaN(v)).sort((a, b) => b - a);
163
- return values.length ? values[Math.min(nth, values.length - 1)] : NaN;
357
+ return nthValue('max', id, nth, (a, b) => b - a);
164
358
  }
165
359
  /**
166
- * Returns the median value (numeric arrays only)
360
+ * Median of the non-na elements (PineScript `array.median(id)`): the middle value, or the mean of the two middle
361
+ * values for an even count; an array without values gives NaN.
167
362
  */
168
363
  export function median(id) {
169
- const sorted = [...id].sort((a, b) => a - b);
364
+ const sorted = id.filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
365
+ if (!sorted.length)
366
+ return NaN;
170
367
  const mid = Math.floor(sorted.length / 2);
171
368
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
172
369
  }
173
370
  /**
174
- * Returns the mode (most frequent value)
371
+ * Most frequent non-na element (PineScript `array.mode(id)`), compared exactly.
372
+ *
373
+ * PineScript rules: with several values of the same frequency, the smallest one; an array without values gives NaN.
175
374
  */
176
375
  export function mode(id) {
177
376
  const frequency = new Map();
178
- let maxFreq = 0;
179
- let mode = id[0];
180
- for (const val of id) {
181
- const freq = (frequency.get(val) || 0) + 1;
182
- frequency.set(val, freq);
183
- if (freq > maxFreq) {
184
- maxFreq = freq;
185
- mode = val;
377
+ for (const v of id) {
378
+ if (!Number.isNaN(v))
379
+ frequency.set(v, (frequency.get(v) ?? 0) + 1);
380
+ }
381
+ let best = NaN;
382
+ let bestCount = 0;
383
+ for (const [v, count] of frequency) {
384
+ if (count > bestCount || (count === bestCount && v < best)) {
385
+ best = v;
386
+ bestCount = count;
186
387
  }
187
388
  }
188
- return mode;
389
+ return best;
189
390
  }
190
391
  /**
191
- * Standard deviation of the non-na elements (PineScript `array.stdev(id, biased)`).
192
- * `biased` true (default) divides by the count, false by the count - 1.
392
+ * Standard deviation of the non-na elements (PineScript `array.stdev(id, biased)`): the square root of
393
+ * `array.variance(id, biased)`.
193
394
  */
194
395
  export function stdev(id, biased = true) {
195
396
  return Math.sqrt(variance(id, biased));
196
397
  }
398
+ /** Sum of the values and sum of their squares, both added from the first to the last value. */
399
+ function moments(values) {
400
+ let p = 0;
401
+ let q = 0;
402
+ for (const v of values)
403
+ p += v;
404
+ for (const v of values)
405
+ q += v * v;
406
+ return [p, q];
407
+ }
197
408
  /**
198
409
  * Variance of the non-na elements (PineScript `array.variance(id, biased)`).
199
- * `biased` true (default) divides by the count, false by the count - 1.
410
+ *
411
+ * PineScript rule (moment form): with `p` the sum of the values, `q` the sum of their squares, `n` the count and
412
+ * `m = p / n`: biased (default) `max(0, q / n - m * m)`, unbiased `max(0, q / (n - 1) - (p / (n - 1)) * m)`.
413
+ * One value gives 0 biased and NaN unbiased; an array without values gives NaN.
200
414
  */
201
415
  export function variance(id, biased = true) {
202
416
  const values = id.filter((v) => !Number.isNaN(v));
203
- if (!values.length)
417
+ const n = values.length;
418
+ if (!n)
204
419
  return NaN;
205
- const mean = values.reduce((a, b) => a + b, 0) / values.length;
206
- const squares = values.reduce((a, b) => a + (b - mean) * (b - mean), 0);
207
- return squares / (biased ? values.length : values.length - 1);
420
+ if (n < 2)
421
+ return biased ? 0 : NaN;
422
+ const [p, q] = moments(values);
423
+ const mean = p / n;
424
+ return biased ? Math.max(0, q / n - mean * mean) : Math.max(0, q / (n - 1) - (p / (n - 1)) * mean);
208
425
  }
209
426
  /**
210
427
  * Fills the array with the specified value
211
428
  */
212
429
  export function fill(id, value, index_from = 0, index_to) {
213
- const end = index_to ?? id.length;
214
- for (let i = index_from; i < end; i++) {
430
+ // PineScript: an na bound is the start / the end; a bound past the end is a runtime error
431
+ const start = index_from === undefined || index_from === null || Number.isNaN(index_from) ? 0 : index_from;
432
+ const end = index_to === undefined || index_to === null || Number.isNaN(index_to) ? id.length : index_to;
433
+ for (const i of [start, end]) {
434
+ if (i < 0 || i > id.length)
435
+ throw new Error(`array.fill: index ${i} is out of bounds, array size is ${id.length}`);
436
+ }
437
+ for (let i = start; i < end; i++) {
215
438
  id[i] = value;
216
439
  }
217
440
  }
@@ -244,6 +467,9 @@ export function from(...values) {
244
467
  * ```
245
468
  */
246
469
  export function first(id) {
470
+ // PineScript: a runtime error on an empty array
471
+ if (id.length === 0)
472
+ throw new Error('Cannot call array.first() if array is empty');
247
473
  return id[0];
248
474
  }
249
475
  /**
@@ -263,6 +489,9 @@ export function first(id) {
263
489
  * ```
264
490
  */
265
491
  export function last(id) {
492
+ // PineScript: a runtime error on an empty array (as array.first)
493
+ if (id.length === 0)
494
+ throw new Error('Cannot call array.last() if array is empty');
266
495
  return id[id.length - 1];
267
496
  }
268
497
  /**
@@ -608,32 +837,38 @@ export function binary_search(id, val) {
608
837
  * - Array must be sorted in ascending order
609
838
  * - For duplicate values, returns leftmost occurrence
610
839
  * - When value not found, returns position of next smaller element
840
+ * - PineScript rules: a value below every element gives 0; `na` counts as greater than every number (an array
841
+ * without na gives `size - 1`); the comparison is exact (no 1e-10 tolerance)
611
842
  */
612
843
  export function binary_search_leftmost(id, val) {
613
- let left = 0;
614
- let right = id.length - 1;
615
- let result = -1;
616
- let found = false;
617
- while (left <= right) {
618
- const mid = Math.floor((left + right) / 2);
619
- if (id[mid] < val) {
620
- if (!found) {
621
- result = mid; // Track the last element smaller than val (only if not found)
622
- }
623
- left = mid + 1;
624
- }
625
- else if (id[mid] === val) {
626
- result = mid;
627
- found = true;
628
- // Continue searching to the left for leftmost occurrence
629
- right = mid - 1;
630
- }
631
- else {
632
- // id[mid] > val
633
- right = mid - 1;
634
- }
844
+ if (id.length === 0)
845
+ return -1;
846
+ const at = searchBound(id, val, false);
847
+ return at < id.length && compareSorted(id[at], val) === 0 ? at : Math.max(0, at - 1);
848
+ }
849
+ /**
850
+ * Order of the PineScript binary searches: numbers in ascending order, na above every number (equal to na).
851
+ */
852
+ function compareSorted(a, b) {
853
+ const an = Number.isNaN(a);
854
+ const bn = Number.isNaN(b);
855
+ if (an || bn)
856
+ return an === bn ? 0 : an ? 1 : -1;
857
+ return a < b ? -1 : a > b ? 1 : 0;
858
+ }
859
+ /** First index whose element is >= `val` (`after` false) or > `val` (`after` true); `id.length` when none. */
860
+ function searchBound(id, val, after) {
861
+ let lo = 0;
862
+ let hi = id.length;
863
+ while (lo < hi) {
864
+ const mid = (lo + hi) >>> 1;
865
+ const c = compareSorted(id[mid], val);
866
+ if (c < 0 || (after && c === 0))
867
+ lo = mid + 1;
868
+ else
869
+ hi = mid;
635
870
  }
636
- return result;
871
+ return lo;
637
872
  }
638
873
  /**
639
874
  * Binary Search Rightmost
@@ -663,31 +898,14 @@ export function binary_search_leftmost(id, val) {
663
898
  * - Array must be sorted in ascending order
664
899
  * - For duplicate values, returns rightmost occurrence
665
900
  * - When value not found, returns position of next larger element
901
+ * - PineScript rules: a value above every element gives `size` (the position after the last element); `na` counts
902
+ * as greater than every number (an array without na gives `size`); the comparison is exact (no 1e-10 tolerance)
666
903
  */
667
904
  export function binary_search_rightmost(id, val) {
668
- let left = 0;
669
- let right = id.length - 1;
670
- let result = -1;
671
- let found = false;
672
- while (left <= right) {
673
- const mid = Math.floor((left + right) / 2);
674
- if (id[mid] <= val) {
675
- if (id[mid] === val) {
676
- result = mid;
677
- found = true;
678
- }
679
- // Continue searching to the right for rightmost occurrence
680
- left = mid + 1;
681
- }
682
- else {
683
- // id[mid] > val
684
- if (!found) {
685
- result = mid; // Track the first element greater than val (only if not found)
686
- }
687
- right = mid - 1;
688
- }
689
- }
690
- return result;
905
+ if (id.length === 0)
906
+ return -1;
907
+ const at = searchBound(id, val, true);
908
+ return at > 0 && compareSorted(id[at - 1], val) === 0 ? at - 1 : at;
691
909
  }
692
910
  /**
693
911
  * Covariance
@@ -712,24 +930,38 @@ export function binary_search_rightmost(id, val) {
712
930
  * - Unbiased (false): divides by n-1 (sample covariance)
713
931
  * - Returns NaN if arrays are empty or have different lengths
714
932
  * - Formula: Cov(X,Y) = E[(X - μX)(Y - μY)]
933
+ *
934
+ * PineScript rules: a pair where either value is `na` is skipped and the count is the number of pairs kept; both
935
+ * means first, then the sum of the products from the first to the last pair. One pair gives 0 biased, NaN unbiased.
715
936
  */
716
937
  export function covariance(id1, id2, biased = true) {
717
- if (id1.length === 0 || id2.length === 0) {
938
+ if (id1.length !== id2.length) {
718
939
  return NaN;
719
940
  }
720
- if (id1.length !== id2.length) {
941
+ const xs = [];
942
+ const ys = [];
943
+ for (let i = 0; i < id1.length; i++) {
944
+ if (Number.isNaN(id1[i]) || Number.isNaN(id2[i]))
945
+ continue;
946
+ xs.push(id1[i]);
947
+ ys.push(id2[i]);
948
+ }
949
+ const n = xs.length;
950
+ if (!n || (!biased && n < 2))
721
951
  return NaN;
952
+ let mean1 = 0;
953
+ let mean2 = 0;
954
+ for (let i = 0; i < n; i++) {
955
+ mean1 += xs[i];
956
+ mean2 += ys[i];
722
957
  }
723
- const n = id1.length;
724
- const mean1 = avg(id1);
725
- const mean2 = avg(id2);
958
+ mean1 /= n;
959
+ mean2 /= n;
726
960
  let sum = 0;
727
961
  for (let i = 0; i < n; i++) {
728
- sum += (id1[i] - mean1) * (id2[i] - mean2);
962
+ sum += (xs[i] - mean1) * (ys[i] - mean2);
729
963
  }
730
- // Biased: divide by n, Unbiased: divide by n-1
731
- const divisor = biased ? n : n - 1;
732
- return sum / divisor;
964
+ return sum / (biased ? n : n - 1);
733
965
  }
734
966
  /**
735
967
  * Percentile Linear Interpolation
@@ -754,32 +986,45 @@ export function covariance(id1, id2, biased = true) {
754
986
  * - Returns NaN if array is empty
755
987
  *
756
988
  * PineScript rules:
757
- * - position in the sorted array: `percentage / 100 * size - 0.5`, clamped to the first / last value, linear
758
- * interpolation between the two neighbours
989
+ * - 1-based position in the sorted array: `size * percentage / 100 + 0.5` (snapped to an integer within 1e-9),
990
+ * clamped to the first / last value; between two values `lo * (1 - frac) + hi * frac`
759
991
  * - `na` values are sorted after all numbers; with an `na` in the array, a position between two values gives `na`
992
+ * - an `na` percentage gives `na`; a percentage outside 0 .. 100 is a runtime error (throws)
760
993
  */
761
994
  export function percentile_linear_interpolation(id, percentage) {
762
- if (id.length === 0) {
995
+ if (id.length === 0 || Number.isNaN(percentage)) {
763
996
  return NaN;
764
997
  }
998
+ checkPercentage('percentile_linear_interpolation', percentage);
765
999
  const s = sortedNaLast(id);
766
1000
  const n = s.length;
767
- const pos = (percentage / 100) * n - 0.5;
768
- if (pos <= 0)
1001
+ let pos = (n * percentage) / 100 + 0.5;
1002
+ const nearest = Math.round(pos);
1003
+ if (Math.abs(pos - nearest) < 1e-9)
1004
+ pos = nearest;
1005
+ if (pos <= 1)
769
1006
  return s[0];
770
- if (pos >= n - 1)
1007
+ if (pos >= n)
771
1008
  return s[n - 1];
772
- const lo = Math.floor(pos);
773
- const f = pos - lo;
774
- if (f === 0)
775
- return s[lo];
776
- return Number.isNaN(s[n - 1]) ? NaN : s[lo] + f * (s[lo + 1] - s[lo]);
1009
+ const lower = Math.floor(pos);
1010
+ const frac = pos - lower;
1011
+ if (frac === 0)
1012
+ return s[lower - 1];
1013
+ if (Number.isNaN(s[n - 1]))
1014
+ return NaN;
1015
+ return s[lower - 1] * (1 - frac) + s[lower] * frac;
777
1016
  }
778
1017
  /** Array values sorted ascending, `na` values after the numbers (array.percentile_*). */
779
1018
  function sortedNaLast(id) {
780
1019
  const numbers = id.filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
781
1020
  return [...numbers, ...Array(id.length - numbers.length).fill(NaN)];
782
1021
  }
1022
+ /** A percentage outside 0 .. 100 is a PineScript runtime error. */
1023
+ function checkPercentage(name, percentage) {
1024
+ if (!(percentage >= 0 && percentage <= 100)) {
1025
+ throw new Error(`array.${name}: percentage ${percentage} must be between 0 and 100`);
1026
+ }
1027
+ }
783
1028
  /**
784
1029
  * Percentile Nearest Rank
785
1030
  *
@@ -803,15 +1048,18 @@ function sortedNaLast(id) {
803
1048
  * - Returns NaN if array is empty
804
1049
  *
805
1050
  * PineScript rules:
806
- * - the value at rank `ceil(percentage / 100 * size)` of the sorted array
1051
+ * - the value at rank `ceil(percentage * size / 100)` (clamped to 1 .. size) of the sorted array
807
1052
  * - `na` values are sorted after all numbers (na when that rank holds an `na`)
1053
+ * - an `na` percentage counts as 0; a percentage outside 0 .. 100 is a runtime error (throws)
808
1054
  */
809
1055
  export function percentile_nearest_rank(id, percentage) {
810
1056
  if (id.length === 0) {
811
1057
  return NaN;
812
1058
  }
1059
+ const pct = Number.isNaN(percentage) ? 0 : percentage;
1060
+ checkPercentage('percentile_nearest_rank', pct);
813
1061
  const s = sortedNaLast(id);
814
- const rank = Math.min(s.length, Math.max(1, Math.ceil((percentage / 100) * s.length)));
1062
+ const rank = Math.min(s.length, Math.max(1, Math.ceil((pct * s.length) / 100)));
815
1063
  return s[rank - 1];
816
1064
  }
817
1065
  /**
@@ -829,31 +1077,39 @@ export function percentile_nearest_rank(id, percentage) {
829
1077
  * ```typescript
830
1078
  * const arr = [1, 2, 3, 4, 5];
831
1079
  * const rank = array.percentrank(arr, 2); // Value at index 2 is 3
832
- * // Returns: 40 (40% of values <= 3)
1080
+ * // Returns: 50 ((3 - 1) * 100 / (5 - 1))
833
1081
  * ```
834
1082
  *
835
1083
  * @remarks
836
- * - Returns percentage of elements <= the value at given index
837
1084
  * - Range: 0 to 100
838
- * - Returns NaN if array is empty or index out of bounds
1085
+ *
1086
+ * PineScript rules:
1087
+ * - `(count - 1) * 100 / (size - 1)`, with `count` the number of non-na elements <= the element (exact
1088
+ * comparison, no 1e-10 tolerance); `na` elements count in the size
1089
+ * - an `na` element, a one-element array or an empty array gives NaN
1090
+ * - an `na` index counts as 0; an index outside `0 .. size - 1` is a runtime error (throws)
839
1091
  */
840
1092
  export function percentrank(id, index) {
841
- if (id.length === 0 || index < 0 || index >= id.length) {
1093
+ const i = Number.isNaN(index) ? 0 : Math.trunc(index);
1094
+ if (id.length === 0) {
842
1095
  return NaN;
843
1096
  }
844
- const value = id[index];
845
- if (isNaN(value)) {
1097
+ if (i < 0 || i >= id.length) {
1098
+ throw new Error(`array.percentrank: index ${i} is out of bounds, array size is ${id.length}`);
1099
+ }
1100
+ if (id.length === 1) {
1101
+ return NaN;
1102
+ }
1103
+ const value = id[i];
1104
+ if (Number.isNaN(value)) {
846
1105
  return NaN;
847
1106
  }
848
- // Count elements less than or equal to the value
849
1107
  let count = 0;
850
- for (let i = 0; i < id.length; i++) {
851
- if (!isNaN(id[i]) && id[i] <= value) {
1108
+ for (const v of id) {
1109
+ if (v <= value)
852
1110
  count++;
853
- }
854
1111
  }
855
- // Calculate percentage
856
- return (count / id.length) * 100;
1112
+ return ((count - 1) * 100) / (id.length - 1);
857
1113
  }
858
1114
  /**
859
1115
  * Sort Indices
@@ -880,20 +1136,11 @@ export function percentrank(id, index) {
880
1136
  * - Original array is not modified
881
1137
  * - Returns indices that would sort the array
882
1138
  * - Useful for maintaining correspondence with other arrays
1139
+ * - PineScript rule: the indices of `na` elements go where `array.sort` puts them (after the numbers in ascending
1140
+ * order, in their original order: `sort_indices([na, na, 5, 1])` is `[3, 2, 0, 1]`)
883
1141
  */
884
1142
  export function sort_indices(id, order = 'asc') {
885
- // Create array of indices
886
- const indices = Array.from({ length: id.length }, (_, i) => i);
887
- // Sort indices based on values in original array
888
- indices.sort((a, b) => {
889
- if (order === 'asc') {
890
- return id[a] - id[b];
891
- }
892
- else {
893
- return id[b] - id[a];
894
- }
895
- });
896
- return indices;
1143
+ return sortedPositions(id, order);
897
1144
  }
898
1145
  /**
899
1146
  * Standardize
@@ -915,21 +1162,25 @@ export function sort_indices(id, order = 'asc') {
915
1162
  * @remarks
916
1163
  * - Formula: z = (x - μ) / σ
917
1164
  * - Result has mean of 0 and standard deviation of 1
918
- * - Returns array of NaN if standard deviation is 0
919
1165
  * - Useful for comparing values on different scales
1166
+ *
1167
+ * PineScript rules:
1168
+ * - μ and σ are the population mean and standard deviation of the non-na elements, as `array.variance` (moment
1169
+ * form); `na` elements stay `na`
1170
+ * - when σ is 0 (all values equal) every non-na element gives 1
920
1171
  */
921
1172
  export function standardize(id) {
922
- if (id.length === 0) {
923
- return [];
1173
+ const values = id.filter((v) => !Number.isNaN(v));
1174
+ if (!values.length) {
1175
+ return id.map(() => NaN);
924
1176
  }
925
- const mean = avg(id);
926
- const stdDev = stdev(id);
927
- // If stddev is 0, all values are the same - return array of 0s or NaN
1177
+ const [p, q] = moments(values);
1178
+ const mean = p / values.length;
1179
+ const stdDev = Math.sqrt(Math.max(0, q / values.length - mean * mean));
928
1180
  if (stdDev === 0) {
929
- return id.map(() => NaN);
1181
+ return id.map((v) => (Number.isNaN(v) ? NaN : 1));
930
1182
  }
931
- // Calculate z-score for each element
932
- return id.map(value => (value - mean) / stdDev);
1183
+ return id.map((v) => (Number.isNaN(v) ? NaN : (v - mean) / stdDev));
933
1184
  }
934
1185
  /**
935
1186
  * New Type (User-Defined Type Array)