oakscriptjs 0.9.4 → 0.9.5

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 (52) hide show
  1. package/dist/cjs/array/index.d.ts +4 -0
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +22 -39
  4. package/dist/cjs/array/index.js.map +2 -2
  5. package/dist/cjs/callsite/index.d.ts +43 -4
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +40 -2
  8. package/dist/cjs/callsite/index.js.map +2 -2
  9. package/dist/cjs/index.d.ts +1 -1
  10. package/dist/cjs/index.js +1 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/math/index.d.ts +3 -1
  13. package/dist/cjs/math/index.d.ts.map +1 -1
  14. package/dist/cjs/math/index.js +2 -2
  15. package/dist/cjs/math/index.js.map +2 -2
  16. package/dist/cjs/ta/index.d.ts +12 -6
  17. package/dist/cjs/ta/index.d.ts.map +1 -1
  18. package/dist/cjs/ta/index.js +27 -58
  19. package/dist/cjs/ta/index.js.map +3 -3
  20. package/dist/cjs/ta/running-sum.d.ts +2 -1
  21. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  22. package/dist/cjs/ta/running-sum.js +7 -2
  23. package/dist/cjs/ta/running-sum.js.map +2 -2
  24. package/dist/esm/array/index.d.ts +4 -0
  25. package/dist/esm/array/index.d.ts.map +1 -1
  26. package/dist/esm/array/index.js +35 -46
  27. package/dist/esm/array/index.js.map +1 -1
  28. package/dist/esm/callsite/index.d.ts +43 -4
  29. package/dist/esm/callsite/index.d.ts.map +1 -1
  30. package/dist/esm/callsite/index.js +85 -7
  31. package/dist/esm/callsite/index.js.map +1 -1
  32. package/dist/esm/index.d.ts +1 -1
  33. package/dist/esm/index.js +1 -1
  34. package/dist/esm/math/index.d.ts +3 -1
  35. package/dist/esm/math/index.d.ts.map +1 -1
  36. package/dist/esm/math/index.js +2 -2
  37. package/dist/esm/math/index.js.map +1 -1
  38. package/dist/esm/ta/index.d.ts +12 -6
  39. package/dist/esm/ta/index.d.ts.map +1 -1
  40. package/dist/esm/ta/index.js +56 -77
  41. package/dist/esm/ta/index.js.map +1 -1
  42. package/dist/esm/ta/running-sum.d.ts +2 -1
  43. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  44. package/dist/esm/ta/running-sum.js +15 -4
  45. package/dist/esm/ta/running-sum.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/array/index.ts +30 -47
  48. package/src/callsite/index.ts +87 -7
  49. package/src/index.ts +1 -1
  50. package/src/math/index.ts +5 -3
  51. package/src/ta/index.ts +61 -83
  52. package/src/ta/running-sum.ts +16 -4
@@ -11,7 +11,8 @@
11
11
  * loops, called only on the bars where the Pine call runs.
12
12
  * - {@link lowest} / {@link highest}: `ta.lowest` / `ta.highest` with a series length (a different length on each
13
13
  * call), which PineScript computes with a state kept between calls.
14
- * - {@link linreg}: `ta.linreg` in a conditional block, whose window is kept by bar (not by call).
14
+ * - {@link lowestByBar} / {@link highestByBar} / {@link linreg}: `ta.lowest` / `ta.highest` / `ta.linreg` in a
15
+ * conditional block, whose history is kept by bar (not by call).
15
16
  *
16
17
  * Since PineScript v6, `and` / `or` are lazy: the right operand runs only when the left operand does not decide
17
18
  * the result. A ta.* call in the right operand of `a or b` runs on the bars where `a` is false, in `a and b` on the
@@ -67,24 +68,35 @@ export function whenCalled<T extends number | boolean>(
67
68
  return out;
68
69
  }
69
70
 
70
- /** A call site that keeps the two previous arguments (na before the first call). */
71
+ /**
72
+ * A call site that keeps the arguments of the last call where both were not na (na before such a call), as
73
+ * ta.crossover / ta.crossunder / ta.cross.
74
+ */
71
75
  function twoSeriesSite(test: (a: number, b: number, prevA: number, prevB: number) => boolean) {
72
76
  let prevA = NaN;
73
77
  let prevB = NaN;
74
78
  return (a: number, b: number): boolean => {
75
79
  const result = test(a, b, prevA, prevB);
76
- prevA = a;
77
- prevB = b;
80
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
81
+ prevA = a;
82
+ prevB = b;
83
+ }
78
84
  return result;
79
85
  };
80
86
  }
81
87
 
82
- /** One `ta.crossover(a, b)` call site: false on its first call, as its history is na. */
88
+ /**
89
+ * One `ta.crossover(a, b)` call site: compared with the last call where both arguments were not na; false before
90
+ * such a call.
91
+ */
83
92
  export function crossover(): (a: number, b: number) => boolean {
84
93
  return twoSeriesSite((a, b, pa, pb) => a > b && pa <= pb);
85
94
  }
86
95
 
87
- /** One `ta.crossunder(a, b)` call site: false on its first call, as its history is na. */
96
+ /**
97
+ * One `ta.crossunder(a, b)` call site: compared with the last call where both arguments were not na; false before
98
+ * such a call.
99
+ */
88
100
  export function crossunder(): (a: number, b: number) => boolean {
89
101
  return twoSeriesSite((a, b, pa, pb) => a < b && pa >= pb);
90
102
  }
@@ -102,7 +114,10 @@ export function barssince(): (condition: boolean | number) => number {
102
114
  };
103
115
  }
104
116
 
105
- /** One `ta.cross(a, b)` call site: false on its first call, as its history is na. */
117
+ /**
118
+ * One `ta.cross(a, b)` call site: compared with the last call where both arguments were not na; false before such
119
+ * a call.
120
+ */
106
121
  export function cross(): (a: number, b: number) => boolean {
107
122
  return twoSeriesSite((a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
108
123
  }
@@ -179,6 +194,71 @@ export function highest(): (value: number, length: number) => number {
179
194
  return extremeSite(false);
180
195
  }
181
196
 
197
+ /** One `ta.lowest` / `ta.highest` call site whose history is kept by bar; rules in {@link lowestByBar}. */
198
+ function extremeByBarSite(isLowest: boolean): (barIndex: number, value: number, length: number) => number {
199
+ const better = isLowest ? (a: number, b: number) => a < b : (a: number, b: number) => a > b;
200
+ let ring: number[] = [];
201
+ let extreme = NaN;
202
+ let extremeAt = -1;
203
+ return (barIndex, value, length) => {
204
+ const len = Math.floor(length);
205
+ if (ring.length !== len + 1) {
206
+ ring = new Array<number>(len + 1).fill(0);
207
+ extreme = NaN;
208
+ }
209
+ ring[barIndex % (len + 1)] = value;
210
+ if (Number.isNaN(extreme) || better(value, extreme)) {
211
+ extreme = value;
212
+ extremeAt = barIndex;
213
+ } else if (barIndex - extremeAt >= len) {
214
+ extreme = NaN;
215
+ for (let b = barIndex - len + 1; b <= barIndex; b++) {
216
+ const v = ring[b % (len + 1)]!;
217
+ if (!Number.isNaN(v) && (Number.isNaN(extreme) || better(v, extreme))) {
218
+ extreme = v;
219
+ extremeAt = b;
220
+ }
221
+ }
222
+ }
223
+ return barIndex < len - 1 ? NaN : extreme;
224
+ };
225
+ }
226
+
227
+ /**
228
+ * One `ta.lowest(source, length)` call site in a conditional block whose history is kept by bar (not by call), called
229
+ * with `(barIndex, value, length)` on the bars where the Pine call runs.
230
+ *
231
+ * PineScript rules (measured on a call in a ternary branch, length 6):
232
+ * - the values are kept in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start and
233
+ * written only on the bars where the call runs: a bar before the first call counts as 0, and a bar without a call
234
+ * holds the value of the call `length + 1` bars earlier (0 if there was none)
235
+ * - on a call, the value replaces the kept extreme when there is none or when it is strictly lower; only otherwise,
236
+ * when the kept extreme is `length` bars old, the last `length` bars are read again from the ring (the oldest bar
237
+ * wins a tie)
238
+ * - na before bar `length - 1` (by bar, not by call)
239
+ *
240
+ * {@link lowest} keeps the history by call instead; both rules were measured, on different scripts.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * // Pine: lo = cond ? ta.lowest(low, 6) : na
245
+ * const site = callsite.lowestByBar();
246
+ * const lo = lows.map((v, i) => (cond[i] ? site(i, v, 6) : NaN));
247
+ * ```
248
+ */
249
+ export function lowestByBar(): (barIndex: number, value: number, length: number) => number {
250
+ return extremeByBarSite(true);
251
+ }
252
+
253
+ /**
254
+ * One `ta.highest(source, length)` call site in a conditional block whose history is kept by bar, called with
255
+ * `(barIndex, value, length)`. Same rules as {@link lowestByBar} (a value replaces the kept extreme when it is
256
+ * strictly higher).
257
+ */
258
+ export function highestByBar(): (barIndex: number, value: number, length: number) => number {
259
+ return extremeByBarSite(false);
260
+ }
261
+
182
262
  /**
183
263
  * One `ta.linreg(source, length, offset)` call site in a conditional block, called with
184
264
  * `(barIndex, value, length, offset)` on the bars where the Pine call runs.
package/src/index.ts CHANGED
@@ -164,7 +164,7 @@ export type { LightweightSeriesDefinitions } from './runtime/adapters/Lightweigh
164
164
  export { SimpleInputAdapter } from './runtime/adapters/SimpleInputAdapter.js';
165
165
 
166
166
  // Version
167
- export const VERSION = '0.9.4';
167
+ export const VERSION = '0.9.5';
168
168
 
169
169
  /**
170
170
  * Library information
package/src/math/index.ts CHANGED
@@ -952,12 +952,13 @@ export function random(min?: float, max?: float, _seed?: int): float {
952
952
  * Returns the sign of a number.
953
953
  *
954
954
  * @param value - The number to check or Series
955
- * @returns 1 for positive, -1 for negative, 0 for zero
955
+ * @returns 1 for positive, -1 for negative, 0 for zero, na for na
956
956
  *
957
957
  * @remarks
958
958
  * - Returns 1 if value > 0
959
959
  * - Returns -1 if value < 0
960
960
  * - Returns 0 if value = 0
961
+ * - Returns na if value is na (as PineScript)
961
962
  * - Useful for determining direction or polarity
962
963
  * - If value is a Series, returns a Series; if value is a number, returns a number
963
964
  *
@@ -967,6 +968,7 @@ export function random(min?: float, max?: float, _seed?: int): float {
967
968
  * math.sign(-3) // Returns: -1
968
969
  * math.sign(0) // Returns: 0
969
970
  * math.sign(0.001) // Returns: 1
971
+ * math.sign(NaN) // Returns: NaN
970
972
  * ```
971
973
  */
972
974
  export function sign(value: Series): Series;
@@ -980,13 +982,13 @@ export function sign(value: float | Series): int | Series {
980
982
  const result: number[] = [];
981
983
  for (let i = 0; i < length; i++) {
982
984
  const v = valueArray[i] ?? NaN;
983
- result.push(v > 0 ? 1 : v < 0 ? -1 : 0);
985
+ result.push(v > 0 ? 1 : v < 0 ? -1 : v === 0 ? 0 : NaN);
984
986
  }
985
987
 
986
988
  return Series.fromArray(bars, result);
987
989
  }
988
990
 
989
- return value > 0 ? 1 : value < 0 ? -1 : 0;
991
+ return value > 0 ? 1 : value < 0 ? -1 : value === 0 ? 0 : NaN;
990
992
  }
991
993
 
992
994
  /**
package/src/ta/index.ts CHANGED
@@ -74,26 +74,21 @@ export function ema(source: Source, length: simple_int): series_float {
74
74
  const len = Math.floor(length);
75
75
  const multiplier = 2 / (len + 1);
76
76
 
77
- // SMA seed: slide past leading NaN, then collect `len` valid values.
78
- // An infinite value is na, as in PineScript.
77
+ // SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
79
78
  let firstValidIndex = -1;
80
79
  let validCount = 0;
81
- let initSum = 0;
82
80
 
83
81
  for (let i = 0; i < source.length; i++) {
84
82
  const val = source[i];
85
- if (val !== undefined && Number.isFinite(val)) {
86
- initSum += val;
87
- validCount++;
88
- if (validCount === len) {
89
- firstValidIndex = i;
90
- break;
91
- }
83
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
84
+ firstValidIndex = i;
85
+ break;
92
86
  }
93
87
  }
94
88
 
95
- let emaValue = validCount > 0 ? initSum / validCount : NaN;
96
89
  const emaInitialized = firstValidIndex >= 0;
90
+ // The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
91
+ let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
97
92
 
98
93
  for (let i = 0; i < source.length; i++) {
99
94
  if (!emaInitialized || i < firstValidIndex) {
@@ -350,6 +345,32 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
350
345
  });
351
346
  }
352
347
 
348
+ /**
349
+ * Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
350
+ * ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
351
+ *
352
+ * @internal
353
+ */
354
+ function crossTest(
355
+ series1: Source,
356
+ series2: Source,
357
+ test: (a: number, b: number, prevA: number, prevB: number) => boolean,
358
+ ): series_bool {
359
+ const result: series_bool = [];
360
+ let prevA = NaN;
361
+ let prevB = NaN;
362
+ for (let i = 0; i < series1.length; i++) {
363
+ const a = series1[i]!;
364
+ const b = series2[i]!;
365
+ result.push(test(a, b, prevA, prevB));
366
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
367
+ prevA = a;
368
+ prevB = b;
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+
353
374
  /**
354
375
  * Crossover - returns true when series1 crosses over series2 (moves from below to above).
355
376
  *
@@ -358,8 +379,10 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
358
379
  * @returns Boolean series (true at crossover points)
359
380
  *
360
381
  * @remarks
361
- * - True when: series1[i]! > series2[i]! AND series1[i-1] <= series2[i-1]
382
+ * - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
383
+ * values were not na (PineScript rule: a crossing right after an na bar is not missed)
362
384
  * - First value is always false (no previous value to compare)
385
+ * - Comparisons are exact (no tolerance)
363
386
  * - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
364
387
  *
365
388
  * @example
@@ -368,17 +391,7 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
368
391
  * ```
369
392
  */
370
393
  export function crossover(series1: Source, series2: Source): series_bool {
371
- const result: series_bool = [];
372
-
373
- for (let i = 0; i < series1.length; i++) {
374
- if (i === 0) {
375
- result.push(false);
376
- } else {
377
- result.push(series1[i]! > series2[i]! && series1[i - 1]! <= series2[i - 1]!);
378
- }
379
- }
380
-
381
- return result;
394
+ return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
382
395
  }
383
396
 
384
397
  /**
@@ -389,8 +402,10 @@ export function crossover(series1: Source, series2: Source): series_bool {
389
402
  * @returns Boolean series (true at crossunder points)
390
403
  *
391
404
  * @remarks
392
- * - True when: series1[i]! < series2[i]! AND series1[i-1] >= series2[i-1]
405
+ * - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
406
+ * values were not na
393
407
  * - First value is always false (no previous value to compare)
408
+ * - Comparisons are exact (no tolerance)
394
409
  * - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
395
410
  *
396
411
  * @example
@@ -399,17 +414,7 @@ export function crossover(series1: Source, series2: Source): series_bool {
399
414
  * ```
400
415
  */
401
416
  export function crossunder(series1: Source, series2: Source): series_bool {
402
- const result: series_bool = [];
403
-
404
- for (let i = 0; i < series1.length; i++) {
405
- if (i === 0) {
406
- result.push(false);
407
- } else {
408
- result.push(series1[i]! < series2[i]! && series1[i - 1]! >= series2[i - 1]!);
409
- }
410
- }
411
-
412
- return result;
417
+ return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
413
418
  }
414
419
 
415
420
  /**
@@ -859,8 +864,9 @@ export function cum(source: Source): series_float {
859
864
  * @returns Boolean series (true at cross points)
860
865
  *
861
866
  * @remarks
862
- * - True when: (source1[i]! > source2[i]! AND source1[i-1] <= source2[i-1]) OR
863
- * (source1[i]! < source2[i]! AND source1[i-1] >= source2[i-1])
867
+ * - True when: (source1[i] > source2[i] AND source1[j] <= source2[j]) OR
868
+ * (source1[i] < source2[i] AND source1[j] >= source2[j]),
869
+ * where j is the last bar before i where both values were not na
864
870
  * - First value is always false (no previous value to compare)
865
871
  * - Detects any crossing (either over or under)
866
872
  * - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
@@ -872,19 +878,7 @@ export function cum(source: Source): series_float {
872
878
  * ```
873
879
  */
874
880
  export function cross(source1: Source, source2: Source): series_bool {
875
- const result: series_bool = [];
876
-
877
- for (let i = 0; i < source1.length; i++) {
878
- if (i === 0) {
879
- result.push(false);
880
- } else {
881
- const crossedUp = source1[i]! > source2[i]! && source1[i - 1]! <= source2[i - 1]!;
882
- const crossedDown = source1[i]! < source2[i]! && source1[i - 1]! >= source2[i - 1]!;
883
- result.push(crossedUp || crossedDown);
884
- }
885
- }
886
-
887
- return result;
881
+ return crossTest(source1, source2, (a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
888
882
  }
889
883
 
890
884
  /**
@@ -1428,7 +1422,7 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1428
1422
  * - Useful for identifying relative strength within a period
1429
1423
  *
1430
1424
  * PineScript rules:
1431
- * - percentrank = count(source[k] <= source, k = 1..length) / length * 100
1425
+ * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1432
1426
  * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1433
1427
  * `<=` the current value
1434
1428
  * - the comparison uses the 1e-10 tolerance
@@ -1454,7 +1448,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
1454
1448
  for (let j = 1; j <= length; j++) {
1455
1449
  if (le(source[i - j]!, currentValue)) countLessOrEqual++;
1456
1450
  }
1457
- result.push((countLessOrEqual / length) * 100);
1451
+ result.push((countLessOrEqual * 100) / length);
1458
1452
  }
1459
1453
 
1460
1454
  return result;
@@ -2078,7 +2072,8 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2078
2072
  * - CMO > +50: overbought conditions
2079
2073
  * - CMO < -50: oversold conditions
2080
2074
  * - Unlike RSI, CMO uses sum of gains/losses instead of averages
2081
- * - na on a flat window (every change 0), as in PineScript
2075
+ * - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
2076
+ * is `0 / 0` = na
2082
2077
  * - More volatile than RSI
2083
2078
  *
2084
2079
  * @example
@@ -2089,37 +2084,20 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2089
2084
  * ```
2090
2085
  */
2091
2086
  export function cmo(source: Source, length: simple_int): series_float {
2092
- const result: series_float = [];
2093
-
2087
+ // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
2088
+ // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
2089
+ // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
2090
+ const up: number[] = [];
2091
+ const down: number[] = [];
2094
2092
  for (let i = 0; i < source.length; i++) {
2095
- if (i < length) {
2096
- result.push(NaN);
2097
- continue;
2098
- }
2099
-
2100
- let sumGains = 0;
2101
- let sumLosses = 0;
2102
-
2103
- for (let j = 0; j < length; j++) {
2104
- const change = source[i - j]! - source[i - j - 1]!;
2105
- if (change > 0) {
2106
- sumGains += change;
2107
- } else {
2108
- sumLosses += Math.abs(change);
2109
- }
2110
- }
2111
-
2112
- // PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
2113
- const totalMovement = sumGains + sumLosses;
2114
- if (totalMovement === 0) {
2115
- result.push(NaN);
2116
- } else {
2117
- const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
2118
- result.push(cmoValue);
2119
- }
2120
- }
2121
-
2122
- return result;
2093
+ const mom = i > 0 ? source[i]! - source[i - 1]! : NaN;
2094
+ up.push(mom >= 0 ? mom : 0);
2095
+ down.push(mom >= 0 ? 0 : -mom);
2096
+ }
2097
+ const sm1 = runningSum(up, length);
2098
+ const sm2 = runningSum(down, length);
2099
+ // a flat window is 0 / 0 = na
2100
+ return sm1.map((s1, i) => (100 * (s1 - sm2[i]!)) / (s1 + sm2[i]!));
2123
2101
  }
2124
2102
 
2125
2103
  /**
@@ -6,7 +6,8 @@
6
6
  * - each bar first removes, with a Kahan step, the compensated value that was added for the value that leaves the
7
7
  * window, then adds the new value with a Kahan step and keeps its compensated value;
8
8
  * - when the compensation of the previous step would be lost when applied to the new value, the sum is computed
9
- * again from the window values (newest first) and the compensation is reset.
9
+ * again from the window values (newest first) and the compensation is reset. The test rounds on the grid of the
10
+ * new value toward +infinity: for a negative power of two, the finer grid of the binade below (#136).
10
11
  *
11
12
  * So the result can differ from the exact window sum in the last bits (e.g. a small residue on a window of zeros),
12
13
  * as in PineScript.
@@ -23,18 +24,29 @@ function exponentOf(x: number): number {
23
24
  return e === 0 ? -1074 : e - 1023;
24
25
  }
25
26
 
27
+ /**
28
+ * Distance from a finite non-zero `x` to the next number toward +infinity: the spacing of the binade of `x`, or half
29
+ * of it when `x` is a negative power of two (the next number up is in the binade below).
30
+ */
31
+ function spacingUp(x: number): number {
32
+ const e = exponentOf(x);
33
+ const powerOfTwo = (view.getUint16(0) & 0xf) === 0 && view.getUint16(2) === 0 && view.getUint32(4) === 0;
34
+ return 2 ** (e - (x < 0 && powerOfTwo && e > -1022 ? 53 : 52));
35
+ }
36
+
26
37
  /**
27
38
  * Whether the compensation `c` is (partly) lost when it is applied to `x`: `x - c` rounded on the grid of `x`
28
- * (spacing of the binade of `x`, ties to an even mantissa) applies less than `c`.
39
+ * (spacing {@link spacingUp}, ties to an even mantissa) applies less than `c`.
29
40
  */
30
41
  function compensationLost(x: number, c: number): boolean {
31
42
  if (c === 0 || x === 0) return false;
32
- const ulp = 2 ** (exponentOf(x) - 52);
43
+ const ulp = spacingUp(x);
33
44
  const q = c / ulp;
34
45
  const mantissa = x / ulp;
35
46
  const f = Math.floor(q);
36
47
  const d = q - f;
37
- const applied = d > 0.5 ? f + 1 : d < 0.5 ? f : (mantissa - f) % 2 === 0 ? f : f + 1;
48
+ // parity of mantissa - f from each part: the mantissa can be 2^53 (half spacing), where mantissa - f is not exact
49
+ const applied = d > 0.5 ? f + 1 : d < 0.5 ? f : Math.abs(mantissa % 2) === Math.abs(f % 2) ? f : f + 1;
38
50
  return Math.abs(applied) < Math.abs(q);
39
51
  }
40
52