oakscriptjs 0.10.1 → 0.11.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 (177) hide show
  1. package/dist/cjs/box/index.d.ts.map +1 -1
  2. package/dist/cjs/box/index.js +7 -1
  3. package/dist/cjs/box/index.js.map +2 -2
  4. package/dist/cjs/callsite/index.d.ts +73 -32
  5. package/dist/cjs/callsite/index.d.ts.map +1 -1
  6. package/dist/cjs/callsite/index.js +69 -65
  7. package/dist/cjs/callsite/index.js.map +2 -2
  8. package/dist/cjs/drawing/registry.d.ts +75 -0
  9. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  10. package/dist/cjs/drawing/registry.js +139 -6
  11. package/dist/cjs/drawing/registry.js.map +2 -2
  12. package/dist/cjs/index.d.ts +6 -1
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js +28 -1
  15. package/dist/cjs/index.js.map +2 -2
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +3 -0
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/line/index.d.ts.map +1 -1
  20. package/dist/cjs/line/index.js +7 -1
  21. package/dist/cjs/line/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts.map +1 -1
  23. package/dist/cjs/math/index.js +2 -0
  24. package/dist/cjs/math/index.js.map +2 -2
  25. package/dist/cjs/polyline/index.d.ts.map +1 -1
  26. package/dist/cjs/polyline/index.js +3 -0
  27. package/dist/cjs/polyline/index.js.map +2 -2
  28. package/dist/cjs/script/index.d.ts +30 -13
  29. package/dist/cjs/script/index.d.ts.map +1 -1
  30. package/dist/cjs/script/index.js +48 -9
  31. package/dist/cjs/script/index.js.map +3 -3
  32. package/dist/cjs/session/bars.d.ts +15 -1
  33. package/dist/cjs/session/bars.d.ts.map +1 -1
  34. package/dist/cjs/session/bars.js +41 -6
  35. package/dist/cjs/session/bars.js.map +3 -3
  36. package/dist/cjs/session/calendar.d.ts +9 -1
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +28 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/session/context.d.ts +75 -0
  41. package/dist/cjs/session/context.d.ts.map +1 -0
  42. package/dist/cjs/session/context.js +113 -0
  43. package/dist/cjs/session/context.js.map +7 -0
  44. package/dist/cjs/str/index.d.ts +3 -1
  45. package/dist/cjs/str/index.d.ts.map +1 -1
  46. package/dist/cjs/str/index.js +2 -1
  47. package/dist/cjs/str/index.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -2
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +1 -1
  51. package/dist/cjs/str/numberformat.js.map +2 -2
  52. package/dist/cjs/ta/index.d.ts +33 -17
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +70 -29
  55. package/dist/cjs/ta/index.js.map +2 -2
  56. package/dist/cjs/ta/limits.d.ts +24 -0
  57. package/dist/cjs/ta/limits.d.ts.map +1 -0
  58. package/dist/cjs/ta/limits.js +50 -0
  59. package/dist/cjs/ta/limits.js.map +7 -0
  60. package/dist/cjs/ta/running-sum.d.ts +0 -16
  61. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  62. package/dist/cjs/ta/running-sum.js +2 -0
  63. package/dist/cjs/ta/running-sum.js.map +2 -2
  64. package/dist/cjs/ta-series.d.ts +7 -7
  65. package/dist/cjs/ta-series.d.ts.map +1 -1
  66. package/dist/cjs/ta-series.js +9 -7
  67. package/dist/cjs/ta-series.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts +5 -2
  69. package/dist/cjs/time/index.d.ts.map +1 -1
  70. package/dist/cjs/time/index.js +2 -2
  71. package/dist/cjs/time/index.js.map +2 -2
  72. package/dist/cjs/time/session.d.ts +17 -1
  73. package/dist/cjs/time/session.d.ts.map +1 -1
  74. package/dist/cjs/time/session.js +61 -7
  75. package/dist/cjs/time/session.js.map +2 -2
  76. package/dist/cjs/types/metadata.d.ts +27 -3
  77. package/dist/cjs/types/metadata.d.ts.map +1 -1
  78. package/dist/cjs/types/metadata.js.map +1 -1
  79. package/dist/esm/box/index.d.ts.map +1 -1
  80. package/dist/esm/box/index.js +8 -2
  81. package/dist/esm/box/index.js.map +1 -1
  82. package/dist/esm/callsite/index.d.ts +73 -32
  83. package/dist/esm/callsite/index.d.ts.map +1 -1
  84. package/dist/esm/callsite/index.js +143 -106
  85. package/dist/esm/callsite/index.js.map +1 -1
  86. package/dist/esm/drawing/registry.d.ts +75 -0
  87. package/dist/esm/drawing/registry.d.ts.map +1 -1
  88. package/dist/esm/drawing/registry.js +237 -6
  89. package/dist/esm/drawing/registry.js.map +1 -1
  90. package/dist/esm/index.d.ts +6 -1
  91. package/dist/esm/index.d.ts.map +1 -1
  92. package/dist/esm/index.js +10 -1
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/label/index.d.ts.map +1 -1
  95. package/dist/esm/label/index.js +4 -1
  96. package/dist/esm/label/index.js.map +1 -1
  97. package/dist/esm/line/index.d.ts.map +1 -1
  98. package/dist/esm/line/index.js +8 -2
  99. package/dist/esm/line/index.js.map +1 -1
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +3 -0
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/polyline/index.d.ts.map +1 -1
  104. package/dist/esm/polyline/index.js +5 -1
  105. package/dist/esm/polyline/index.js.map +1 -1
  106. package/dist/esm/script/index.d.ts +30 -13
  107. package/dist/esm/script/index.d.ts.map +1 -1
  108. package/dist/esm/script/index.js +54 -11
  109. package/dist/esm/script/index.js.map +1 -1
  110. package/dist/esm/session/bars.d.ts +15 -1
  111. package/dist/esm/session/bars.d.ts.map +1 -1
  112. package/dist/esm/session/bars.js +50 -6
  113. package/dist/esm/session/bars.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts +9 -1
  115. package/dist/esm/session/calendar.d.ts.map +1 -1
  116. package/dist/esm/session/calendar.js +35 -3
  117. package/dist/esm/session/calendar.js.map +1 -1
  118. package/dist/esm/session/context.d.ts +75 -0
  119. package/dist/esm/session/context.d.ts.map +1 -0
  120. package/dist/esm/session/context.js +122 -0
  121. package/dist/esm/session/context.js.map +1 -0
  122. package/dist/esm/str/index.d.ts +3 -1
  123. package/dist/esm/str/index.d.ts.map +1 -1
  124. package/dist/esm/str/index.js +8 -2
  125. package/dist/esm/str/index.js.map +1 -1
  126. package/dist/esm/str/numberformat.d.ts +4 -2
  127. package/dist/esm/str/numberformat.d.ts.map +1 -1
  128. package/dist/esm/str/numberformat.js +4 -2
  129. package/dist/esm/str/numberformat.js.map +1 -1
  130. package/dist/esm/ta/index.d.ts +33 -17
  131. package/dist/esm/ta/index.d.ts.map +1 -1
  132. package/dist/esm/ta/index.js +130 -56
  133. package/dist/esm/ta/index.js.map +1 -1
  134. package/dist/esm/ta/limits.d.ts +24 -0
  135. package/dist/esm/ta/limits.d.ts.map +1 -0
  136. package/dist/esm/ta/limits.js +42 -0
  137. package/dist/esm/ta/limits.js.map +1 -0
  138. package/dist/esm/ta/running-sum.d.ts +0 -16
  139. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  140. package/dist/esm/ta/running-sum.js +2 -0
  141. package/dist/esm/ta/running-sum.js.map +1 -1
  142. package/dist/esm/ta-series.d.ts +7 -7
  143. package/dist/esm/ta-series.d.ts.map +1 -1
  144. package/dist/esm/ta-series.js +9 -7
  145. package/dist/esm/ta-series.js.map +1 -1
  146. package/dist/esm/time/index.d.ts +5 -2
  147. package/dist/esm/time/index.d.ts.map +1 -1
  148. package/dist/esm/time/index.js +6 -3
  149. package/dist/esm/time/index.js.map +1 -1
  150. package/dist/esm/time/session.d.ts +17 -1
  151. package/dist/esm/time/session.d.ts.map +1 -1
  152. package/dist/esm/time/session.js +97 -10
  153. package/dist/esm/time/session.js.map +1 -1
  154. package/dist/esm/types/metadata.d.ts +27 -3
  155. package/dist/esm/types/metadata.d.ts.map +1 -1
  156. package/package.json +1 -1
  157. package/src/box/index.ts +8 -2
  158. package/src/callsite/index.ts +150 -100
  159. package/src/drawing/registry.ts +255 -5
  160. package/src/index.ts +34 -1
  161. package/src/label/index.ts +4 -1
  162. package/src/line/index.ts +8 -2
  163. package/src/math/index.ts +2 -0
  164. package/src/polyline/index.ts +3 -1
  165. package/src/script/index.ts +77 -18
  166. package/src/session/bars.ts +47 -6
  167. package/src/session/calendar.ts +37 -3
  168. package/src/session/context.ts +160 -0
  169. package/src/str/index.ts +7 -2
  170. package/src/str/numberformat.ts +5 -3
  171. package/src/ta/index.ts +148 -66
  172. package/src/ta/limits.ts +47 -0
  173. package/src/ta/running-sum.ts +2 -0
  174. package/src/ta-series.ts +16 -14
  175. package/src/time/index.ts +6 -3
  176. package/src/time/session.ts +89 -9
  177. package/src/types/metadata.ts +27 -3
@@ -87,7 +87,8 @@ import * as tableCore from '../table/index.js';
87
87
  import * as chartPointCore from '../chartpoint/index.js';
88
88
  import * as textCore from '../text/index.js';
89
89
  import * as mathCore from '../math/index.js';
90
- import { resetDrawings, setDrawingLimits } from '../drawing/registry.js';
90
+ import { resetDrawings, setDrawingBar, setDrawingLimits } from '../drawing/registry.js';
91
+ export { holdDrawing, releaseDrawing, varDrawing, type DrawingVariable } from '../drawing/registry.js';
91
92
  import { drawingOutputs } from '../drawing/output.js';
92
93
  import * as timeframeCore from '../timeframe/index.js';
93
94
  import * as timeCore from '../time/index.js';
@@ -296,6 +297,7 @@ export function executeScript(
296
297
  ): ScriptRunResult {
297
298
  ctxBars.setAll(barsInput);
298
299
  chartCtx = { ...chart };
300
+ runNow = chart.now ?? Date.now();
299
301
  scriptOptions = options;
300
302
  ctx = freshCollector(inputs);
301
303
  resetDrawings();
@@ -547,25 +549,27 @@ export const ta = {
547
549
  ): [Series, Series, Series, Series, Series] =>
548
550
  taSeries.ichimoku(bars(), conversionPeriods, basePeriods, laggingSpan2Periods, displacement),
549
551
  /** `ta.highest(source, length)` or `ta.highest(length)` (source = high). */
550
- highest: (sourceOrLength: Series | number, length?: number): Series =>
552
+ highest: (sourceOrLength: Series | number, length?: number | Series): Series =>
551
553
  typeof sourceOrLength === 'number' ? taSeries.highest(high, sourceOrLength) : taSeries.highest(sourceOrLength, length!),
552
554
  /** `ta.lowest(source, length)` or `ta.lowest(length)` (source = low). */
553
- lowest: (sourceOrLength: Series | number, length?: number): Series =>
555
+ lowest: (sourceOrLength: Series | number, length?: number | Series): Series =>
554
556
  typeof sourceOrLength === 'number' ? taSeries.lowest(low, sourceOrLength) : taSeries.lowest(sourceOrLength, length!),
555
557
  /** `ta.highestbars(source, length)` or `ta.highestbars(length)` (source = high); offsets are 0 or negative. */
556
- highestbars: (sourceOrLength: Series | number, length?: number): Series =>
558
+ highestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
557
559
  typeof sourceOrLength === 'number'
558
560
  ? taSeries.highestbars(high, sourceOrLength)
559
561
  : taSeries.highestbars(sourceOrLength, length!),
560
562
  /** `ta.lowestbars(source, length)` or `ta.lowestbars(length)` (source = low); offsets are 0 or negative. */
561
- lowestbars: (sourceOrLength: Series | number, length?: number): Series =>
563
+ lowestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
562
564
  typeof sourceOrLength === 'number'
563
565
  ? taSeries.lowestbars(low, sourceOrLength)
564
566
  : taSeries.lowestbars(sourceOrLength, length!),
565
567
  /** `ta.vwap(source, anchor?, stdev_mult?)` with the chart volume; the default anchor is a new trading
566
- * day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session). */
568
+ * day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session), and the
569
+ * first bar: as in PineScript, the first loaded bars (a day in progress) have a value. */
567
570
  vwap: ((source: Series, anchor?: Series, stdev_mult?: number): Series | [Series, Series, Series] => {
568
- const a = anchor ?? changeOf('1D'); // PineScript default: timeframe.change("1D")
571
+ // PineScript default: timeframe.change("1D"); the sums also start on the first bar (a day in progress)
572
+ const a = anchor ?? changeOf('1D', true);
569
573
  return stdev_mult === undefined ? taSeries.vwap(source, volume, a) : taSeries.vwapBands(source, volume, a, stdev_mult);
570
574
  }) as {
571
575
  (source: Series, anchor?: Series): Series;
@@ -575,12 +579,12 @@ export const ta = {
575
579
  mfi: (source: Series, length: number): Series => taSeries.mfi(source, length, volume),
576
580
  /** `ta.pivothigh(source, leftbars, rightbars)` or `ta.pivothigh(leftbars, rightbars)` (source = high).
577
581
  * The value appears `rightbars` bars after the pivot bar, as in PineScript. */
578
- pivothigh: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
582
+ pivothigh: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
579
583
  typeof sourceOrLeft === 'number'
580
584
  ? taSeries.pivothigh(high, sourceOrLeft, leftOrRight)
581
585
  : taSeries.pivothigh(sourceOrLeft, leftOrRight, rightbars!),
582
586
  /** `ta.pivotlow(source, leftbars, rightbars)` or `ta.pivotlow(leftbars, rightbars)` (source = low). */
583
- pivotlow: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
587
+ pivotlow: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
584
588
  typeof sourceOrLeft === 'number'
585
589
  ? taSeries.pivotlow(low, sourceOrLeft, leftOrRight)
586
590
  : taSeries.pivotlow(sourceOrLeft, leftOrRight, rightbars!),
@@ -1262,6 +1266,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1262
1266
  for (idx = 0; idx < b.length; idx++) {
1263
1267
  if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
1264
1268
  currentBar = idx;
1269
+ setDrawingBar(idx, idx === b.length - 1);
1265
1270
  const r = fn(ctx);
1266
1271
  out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
1267
1272
  // grow the history buffers from the largest reference of this bar
@@ -1278,6 +1283,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1278
1283
  }
1279
1284
  } finally {
1280
1285
  currentBar = -1;
1286
+ setDrawingBar(null);
1281
1287
  }
1282
1288
  return Series.fromArray(ctxBars, out);
1283
1289
  }
@@ -1547,6 +1553,16 @@ export interface ChartContext {
1547
1553
  pointvalue?: number;
1548
1554
  /** Smallest tradable quantity (PineScript `syminfo.mincontract`); 1 for whole shares */
1549
1555
  mincontract?: number;
1556
+ /** Symbol type (PineScript `syminfo.type`): "stock", "crypto", "forex", "futures", "index", "fund"... */
1557
+ symbolType?: string;
1558
+ /** Currency of the symbol (PineScript `syminfo.currency`), e.g. "USD" */
1559
+ currency?: string;
1560
+ /** Chart foreground colour (PineScript `chart.fg_color`), e.g. "#DBDBDB" on a dark chart */
1561
+ foregroundColor?: string;
1562
+ /** Chart background colour (PineScript `chart.bg_color`), e.g. "#0F0F0F" on a dark chart */
1563
+ backgroundColor?: string;
1564
+ /** Current time in ms (PineScript `timenow`); default: the clock of the computer at the start of the run */
1565
+ now?: number;
1550
1566
  /**
1551
1567
  * The last bar is closed (`barstate.isconfirmed` on the last bar). Default false: PineScript treats the last bar
1552
1568
  * of the data as not confirmed until the data feed closes it.
@@ -1638,6 +1654,16 @@ function symbolProperty(key: 'mintick' | 'pointvalue' | 'mincontract'): number {
1638
1654
  return value;
1639
1655
  }
1640
1656
 
1657
+ /** A text value of the chart or the symbol that the host passes in the chart context. */
1658
+ function chartText(key: 'symbolType' | 'currency' | 'foregroundColor' | 'backgroundColor', name: string): string {
1659
+ collector();
1660
+ const value = chartCtx[key];
1661
+ if (value === undefined) {
1662
+ throw new Error(`${name} is not known: pass it as executeScript(body, bars, inputs, { ${key} }).`);
1663
+ }
1664
+ return value;
1665
+ }
1666
+
1641
1667
  /** `[minmove, pricescale]` of the chart symbol: from the chart context, else from its mintick. */
1642
1668
  function symbolScale(): [number, number] {
1643
1669
  collector();
@@ -1649,7 +1675,8 @@ function symbolScale(): [number, number] {
1649
1675
 
1650
1676
  /**
1651
1677
  * PineScript `syminfo.timezone`, `syminfo.tickerid`, `syminfo.session` (session type), `syminfo.mintick`,
1652
- * `syminfo.minmove`, `syminfo.pricescale`, `syminfo.pointvalue` and `syminfo.mincontract`, from the chart context.
1678
+ * `syminfo.minmove`, `syminfo.pricescale`, `syminfo.pointvalue`, `syminfo.mincontract`, `syminfo.type` and
1679
+ * `syminfo.currency`, from the chart context.
1653
1680
  */
1654
1681
  export const syminfo = {
1655
1682
  get mintick(): number {
@@ -1681,6 +1708,12 @@ export const syminfo = {
1681
1708
  collector();
1682
1709
  return chartCtx.sessionType ?? 'regular';
1683
1710
  },
1711
+ get type(): string {
1712
+ return chartText('symbolType', 'syminfo.type');
1713
+ },
1714
+ get currency(): string {
1715
+ return chartText('currency', 'syminfo.currency');
1716
+ },
1684
1717
  };
1685
1718
 
1686
1719
  /** A calendar function: `(time, timezone?)` on a UNIX time or on a Series of times. */
@@ -1794,7 +1827,10 @@ function sessionBars(): { bars: SessionBars; regular: SessionBars } {
1794
1827
  * session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without
1795
1828
  * corrections and holidays, and an explicit session follows PineScript's session string rules.
1796
1829
  */
1797
- function calendarFor(session?: string, timezone?: string): { bars: SessionBars; test: boolean } {
1830
+ function calendarFor(
1831
+ session?: string,
1832
+ timezone?: string
1833
+ ): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
1798
1834
  const { bars } = sessionBars();
1799
1835
  if (!session && !timezone) return { bars, test: false };
1800
1836
  const tz = timezone ?? exchangeTimezone();
@@ -1806,7 +1842,14 @@ function calendarFor(session?: string, timezone?: string): { bars: SessionBars;
1806
1842
  return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
1807
1843
  }
1808
1844
  const spec = session ? { session } : { session: toSpec(chartCtx.session!).session };
1809
- return { bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period), test: true };
1845
+ return {
1846
+ bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period),
1847
+ test: true,
1848
+ // a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
1849
+ inside: session
1850
+ ? (time) => timeCore.inSession(time, session, tz, timeframeCore.in_seconds(bars.chart.period) * 1000)
1851
+ : undefined,
1852
+ };
1810
1853
  }
1811
1854
 
1812
1855
  function perBar(fn: (t: number, i: number, times: number[]) => number): Series {
@@ -1823,22 +1866,28 @@ function callableSeries<F extends (...args: never[]) => Series>(series: Series,
1823
1866
 
1824
1867
  function timeFunction(tf?: string, session?: string, timezone?: string): Series {
1825
1868
  collector();
1826
- const { bars, test } = calendarFor(session, timezone);
1869
+ const { bars, test, inside } = calendarFor(session, timezone);
1827
1870
  const timeframeString = tf || chartTimeframe().period;
1828
- return perBar((t) => (test && !bars.inside(t) ? NaN : bars.timeOf(t, timeframeString)));
1871
+ return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.timeOf(t, timeframeString)));
1829
1872
  }
1830
1873
 
1831
1874
  function timeCloseFunction(tf?: string, session?: string, timezone?: string): Series {
1832
1875
  collector();
1833
- const { bars, test } = calendarFor(session, timezone);
1876
+ const { bars, test, inside } = calendarFor(session, timezone);
1834
1877
  const timeframeString = tf || chartTimeframe().period;
1835
- return perBar((t) => (test && !bars.inside(t) ? NaN : bars.closeOf(t, timeframeString)));
1878
+ return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.closeOf(t, timeframeString)));
1836
1879
  }
1837
1880
 
1838
1881
  /**
1839
1882
  * PineScript `time`: the bar open time in ms (a Series), and `time(timeframe?, session?, timezone?)`:
1840
1883
  * the open time of the `timeframe` period that contains the bar, `na` outside `session`.
1841
1884
  */
1885
+ /** Current time of the run in ms, see `ChartContext.now`. */
1886
+ let runNow = NaN;
1887
+
1888
+ /** PineScript `timenow`: the current time in ms, the same on every bar of a run (`ChartContext.now`). */
1889
+ export const timenow = new Series(ctxBars, () => runNow);
1890
+
1842
1891
  export const time = callableSeries(new Series(ctxBars, (b) => barTime(b)), timeFunction);
1843
1892
 
1844
1893
  /**
@@ -1853,9 +1902,11 @@ export const time_close = callableSeries(
1853
1902
  /** PineScript `time_tradingday`: 00:00 UTC of the trading day of each bar, in ms. */
1854
1903
  export const time_tradingday = new Series(ctxBars, (b) => sessionBars().bars.tradingDayTime(barTime(b)));
1855
1904
 
1856
- function changeOf(tf: string): Series {
1905
+ function changeOf(tf: string, firstBar = false): Series {
1857
1906
  const { bars } = sessionBars();
1858
- return perBar((t, i, times) => (i > 0 && bars.timeOf(t, tf) !== bars.timeOf(times[i - 1]!, tf) ? 1 : 0));
1907
+ // a bar without a period (after an early close) is no change, and neither is the bar after it
1908
+ const changed = (a: number, b: number): boolean => !Number.isNaN(a) && !Number.isNaN(b) && a !== b;
1909
+ return perBar((t, i, times) => ((i > 0 ? changed(bars.timeOf(t, tf), bars.timeOf(times[i - 1]!, tf)) : firstBar) ? 1 : 0));
1859
1910
  }
1860
1911
 
1861
1912
  function flag(key: keyof SessionFlags): Series {
@@ -2091,6 +2142,14 @@ export const text = { ...textCore };
2091
2142
  * (index, time, and `price`, default the close), so it runs inside eachBar().
2092
2143
  */
2093
2144
  export const chart = {
2145
+ /** Chart foreground colour (PineScript `chart.fg_color`), from the chart context. */
2146
+ get fg_color(): string {
2147
+ return chartText('foregroundColor', 'chart.fg_color');
2148
+ },
2149
+ /** Chart background colour (PineScript `chart.bg_color`), from the chart context. */
2150
+ get bg_color(): string {
2151
+ return chartText('backgroundColor', 'chart.bg_color');
2152
+ },
2094
2153
  point: {
2095
2154
  ...chartPointCore,
2096
2155
  now(price?: number): ChartPoint {
@@ -111,7 +111,10 @@ export class SessionBars {
111
111
  return p[p.length - 1]![1];
112
112
  }
113
113
 
114
- /** PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. */
114
+ /**
115
+ * PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. An intraday bar that
116
+ * opens after an early close has no day: `time("D")` is NaN on it (measured; `time("W")` keeps its value).
117
+ */
115
118
  timeOf(time: number, timeframe: string): number {
116
119
  const tf = info(timeframe);
117
120
  const at = this.calendar.locate(time);
@@ -123,6 +126,7 @@ export class SessionBars {
123
126
  return start + Math.floor((time - start) / ms) * ms;
124
127
  }
125
128
  const { unit, n } = unitOf(tf);
129
+ if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
126
130
  const [from, to] = this.periodRange(at.date, unit, n);
127
131
  const first = this.calendar.firstTradingDay(from, to);
128
132
  return first === null ? NaN : this.dayStart(first);
@@ -140,6 +144,7 @@ export class SessionBars {
140
144
  return Math.min(this.timeOf(time, timeframe) + in_seconds(tf.period) * 1000, end);
141
145
  }
142
146
  const { unit, n } = unitOf(tf);
147
+ if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
143
148
  const [from, to] = this.periodRange(at.date, unit, n);
144
149
  // intraday charts: the start of the next period, except "1D" (the session end)
145
150
  if (this.chart.isintraday && (unit !== 'D' || n > 1)) {
@@ -154,7 +159,7 @@ export class SessionBars {
154
159
  /** Trading day of a bar: on D / W / M charts the last trading day of the chart period. */
155
160
  tradingDay(time: number): DateNum | null {
156
161
  const at = this.calendar.locate(time);
157
- if (!at) return null;
162
+ if (!at || this.stray(time)) return null;
158
163
  if (this.chart.isdwm) {
159
164
  const { unit, n } = unitOf(this.chart);
160
165
  const [from, to] = this.periodRange(at.date, unit, n);
@@ -188,6 +193,35 @@ export class SessionBars {
188
193
  return day === null ? NaN : dateUtcMs(day);
189
194
  }
190
195
 
196
+ /**
197
+ * Open time of the bar after the bar that opens at `time`, on the trading calendar: the next bar of the session
198
+ * period, else the start of the next period or trading day; on D / W / M charts the session start of the first
199
+ * trading day of the next chart period. NaN when none is found within 400 days.
200
+ */
201
+ nextBarTime(time: number): number {
202
+ const at = this.calendar.locate(time);
203
+ if (!at) return NaN;
204
+ if (!this.chart.isdwm) {
205
+ const periods = this.calendar.periods(at.date);
206
+ const [start, end] = periods[at.index]!;
207
+ const ms = in_seconds(this.chart.period) * 1000;
208
+ const next = start + (Math.floor((time - start) / ms) + 1) * ms;
209
+ if (at.inside && next < end) return next;
210
+ if (at.index + 1 < periods.length) return periods[at.index + 1]![0];
211
+ const day = this.calendar.firstTradingDay(addDays(at.date, 1), addDays(at.date, 400));
212
+ return day === null ? NaN : this.dayStart(day);
213
+ }
214
+ const { unit, n } = unitOf(this.chart);
215
+ const [, to] = this.periodRange(at.date, unit, n);
216
+ const day = this.calendar.firstTradingDay(addDays(to, 1), addDays(to, 400));
217
+ return day === null ? NaN : this.dayStart(day);
218
+ }
219
+
220
+ /** An intraday bar that opens after an early close of its day (see TradingCalendar.afterEarlyClose). */
221
+ stray(time: number): boolean {
222
+ return !this.chart.isdwm && this.calendar.afterEarlyClose(time);
223
+ }
224
+
191
225
  /** True when the bar at `time` is inside the session. */
192
226
  inside(time: number): boolean {
193
227
  return this.calendar.locate(time)?.inside ?? false;
@@ -217,6 +251,9 @@ export interface SessionFlags {
217
251
  * time; first and last bars are per session period (a trading day can have two periods, e.g. after a
218
252
  * futures holiday); pre / post market compare with the regular hours of the same trading day; on
219
253
  * D / W / M charts every bar is its own session (first, last, market, first and last regular).
254
+ * The first loaded bar is a first bar only when no earlier bar of its period can exist (it opens less than one chart
255
+ * period after the period start). A bar that opens after an early close is in the market hours (the usual hours of
256
+ * its day) but is no last bar.
220
257
  */
221
258
  export function sessionFlags(times: number[], bars: SessionBars, regular: SessionBars): SessionFlags {
222
259
  const n = times.length;
@@ -242,18 +279,22 @@ export function sessionFlags(times: number[], bars: SessionBars, regular: Sessio
242
279
  const close = bars.closeOf(t, chart.period);
243
280
  const [, end] = bars.calendar.periods(at.date)[at.index]!;
244
281
  const period = `${at.date}/${at.index}`;
245
- out.isfirstbar[i] = period !== previousPeriod;
246
- out.islastbar[i] = close >= end;
282
+ const [start] = bars.calendar.periods(at.date)[at.index]!;
283
+ const barMs = in_seconds(chart.period) * 1000;
284
+ // the first loaded bar: a first bar only when no earlier bar of the period can exist
285
+ out.isfirstbar[i] = i === 0 ? t - start < barMs : period !== previousPeriod;
286
+ const stray = bars.stray(t);
287
+ out.islastbar[i] = !stray && close >= end;
247
288
  const regularPeriods = regular.calendar.periods(at.date);
248
289
  const r = regularPeriods.findIndex(([s, e]) => s <= t && t < e);
249
290
  const market = r !== -1;
250
- out.ismarket[i] = market;
291
+ out.ismarket[i] = market || regular.stray(t);
251
292
  if (!market && regularPeriods.length) {
252
293
  out.ispremarket[i] = t < regularPeriods[0]![0];
253
294
  out.ispostmarket[i] = t >= regularPeriods[regularPeriods.length - 1]![1];
254
295
  }
255
296
  const regularPeriod = market ? `${at.date}/${r}` : '';
256
- out.isfirstbar_regular[i] = market && regularPeriod !== previousRegular;
297
+ out.isfirstbar_regular[i] = market && (i === 0 ? t - regularPeriods[r]![0] < barMs : regularPeriod !== previousRegular);
257
298
  out.islastbar_regular[i] = market && close >= regularPeriods[r]![1];
258
299
  previousPeriod = period;
259
300
  previousRegular = regularPeriod;
@@ -6,7 +6,10 @@
6
6
  * - periods "HHMM-HHMM", several separated by commas; "24x7" is the whole day, every day
7
7
  * - a period whose end is not after its start ("1700-1600", "1700-1700") starts the day before the
8
8
  * trading day: the session that opens Sunday 17:00 is Monday's
9
- * - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1")
9
+ * - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1"); "F" alone is one day ("1700F-1900":
10
+ * 17:00 of the day before to 19:00)
11
+ * - several sections joined by "|", each with its own days ("1900-1900:3456|1700F-1900:2": Monday opens on Sunday
12
+ * 17:00, the other days at 19:00 of the day before)
10
13
  * - optional days after ":" (1 = Sunday ... 7 = Saturday); without days a symbol session trades Monday to
11
14
  * Friday ("24x7": every day), and a PineScript session string follows PineScript's rules (one period:
12
15
  * every day; several periods: Monday to Friday)
@@ -62,9 +65,10 @@ export const weekdayOf = (date: DateNum): number => new Date(utcOf(date)).getUTC
62
65
  export const dateUtcMs = utcOf;
63
66
 
64
67
  function parseTime(token: string, spec: string): { minutes: number; offset?: number } {
65
- const m = /^(\d{2})(\d{2})(?:F(\d+))?$/.exec(token.trim());
68
+ const m = /^(\d{2})(\d{2})(?:F(\d*))?$/.exec(token.trim());
66
69
  if (!m) throw new SyntaxError(`Invalid session "${spec}"`);
67
- return { minutes: Number(m[1]) * 60 + Number(m[2]), offset: m[3] === undefined ? undefined : Number(m[3]) };
70
+ // "F" without a number is one day before the trading day
71
+ return { minutes: Number(m[1]) * 60 + Number(m[2]), offset: m[3] === undefined ? undefined : Number(m[3] || 1) };
68
72
  }
69
73
 
70
74
  function parsePeriods(text: string, spec: string): Period[] {
@@ -102,6 +106,17 @@ function pineVersion(text: string, from: DateNum): Version {
102
106
 
103
107
  function parseVersion(text: string, from: DateNum, spec: string, rule: 'symbol' | 'pine'): Version {
104
108
  if (rule === 'pine') return pineVersion(text, from);
109
+ if (text.includes('|')) {
110
+ // sections with their own days
111
+ const byDay: Period[][] = Array.from({ length: 8 }, () => []);
112
+ for (const section of text.split('|').filter((part) => part.trim())) {
113
+ const one = parseVersion(section, from, spec, rule);
114
+ one.days.forEach((on, d) => {
115
+ if (on) byDay[d] = one.periods;
116
+ });
117
+ }
118
+ return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
119
+ }
105
120
  const [periodText = '', dayText] = text.trim().split(':');
106
121
  const periods = parsePeriods(periodText, spec);
107
122
  const allDays = periodText.trim() === '24x7';
@@ -174,6 +189,25 @@ export class TradingCalendar {
174
189
  return this.periods(date).length > 0;
175
190
  }
176
191
 
192
+ /**
193
+ * True for a bar that opens after the session of its day was cut by a correction (an early close) but inside the
194
+ * usual hours of that day: PineScript has no period for such a bar (`time("D")` is na on it).
195
+ */
196
+ afterEarlyClose(time: number): boolean {
197
+ const at = this.locate(time);
198
+ if (!at || at.inside || !this.corrections.get(at.date)) return false;
199
+ const version = [...this.versions].reverse().find((v) => v.from <= at.date)!;
200
+ const usual = version.byDay ? version.byDay[weekdayOf(at.date)]! : version.days[weekdayOf(at.date)] ? version.periods : [];
201
+ const y = Math.floor(at.date / 10000);
202
+ const m = Math.floor(at.date / 100) % 100;
203
+ const d = at.date % 100;
204
+ return usual.some(
205
+ (p) =>
206
+ zonedToUnix(this.timezone, y, m, d - p.startOffset, Math.floor(p.start / 60), p.start % 60) <= time &&
207
+ time < zonedToUnix(this.timezone, y, m, d - p.endOffset, Math.floor(p.end / 60), p.end % 60)
208
+ );
209
+ }
210
+
177
211
  /**
178
212
  * Trading day and period of a time: the period that contains it, else the last period that started
179
213
  * before it. Null when no trading day is found near the time.
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The session of a chart context, for code that does not run a whole script (a port with its own bar loop).
3
+ *
4
+ * The same values as the script API gives (`time(tf)`, `time_close(tf)`, `time_tradingday`, `session.*`, the periods
5
+ * of `request.security`), from the session, corrections and holidays of the context. Bar times are in the unit of
6
+ * the context (`timeUnit`, seconds by default, as `Bar.time`); PineScript time values are returned in ms.
7
+ *
8
+ * @module session/context
9
+ */
10
+
11
+ import type { Bar } from '../types/index.js';
12
+ import { info, in_seconds } from '../timeframe/index.js';
13
+ import { afterCompletion, periodsOf, resample } from '../security/resample.js';
14
+ import { SessionBars, sessionFlags, type SessionFlags } from './bars.js';
15
+ import { TradingCalendar, type SessionSpec } from './calendar.js';
16
+
17
+ /** The part of a chart context that the session functions read (a `ChartContext` of the script API fits). */
18
+ export interface SessionContext {
19
+ /** Chart timeframe: "5", "60", "1D"... */
20
+ timeframe?: string;
21
+ /** Exchange time zone: "America/New_York", "Etc/UTC"... */
22
+ timezone?: string;
23
+ /** Session of the loaded bars: hours, corrections, holidays */
24
+ session?: SessionSpec | string;
25
+ /** Regular trading hours when the bars include extended hours (default: `session`) */
26
+ regularSession?: SessionSpec | string;
27
+ /** Unit of the bar times: 's' (default) or 'ms' */
28
+ timeUnit?: 's' | 'ms';
29
+ }
30
+
31
+ /** The session calendars of a context: the loaded bars and the regular hours. */
32
+ export interface ChartSessions {
33
+ bars: SessionBars;
34
+ regular: SessionBars;
35
+ /** ms per unit of the bar times */
36
+ unit: number;
37
+ }
38
+
39
+ const toSpec = (s: SessionSpec | string): SessionSpec => (typeof s === 'string' ? { session: s } : s);
40
+ const cache = new WeakMap<object, ChartSessions>();
41
+
42
+ /**
43
+ * Session calendars of a chart context (cached per context object).
44
+ * @throws Error when the context has no timeframe, time zone or session
45
+ */
46
+ export function chartSessions(chart: SessionContext): ChartSessions {
47
+ const cached = cache.get(chart);
48
+ if (cached) return cached;
49
+ const { timeframe, timezone, session } = chart;
50
+ if (!timeframe || !timezone || !session) {
51
+ throw new Error('The chart context needs timeframe, timezone and session for the session functions.');
52
+ }
53
+ const loaded = toSpec(session);
54
+ const regular = chart.regularSession ? toSpec(chart.regularSession) : loaded;
55
+ const out: ChartSessions = {
56
+ bars: new SessionBars(new TradingCalendar(timezone, loaded), timeframe),
57
+ regular: new SessionBars(new TradingCalendar(timezone, { holidays: loaded.holidays, ...regular }), timeframe),
58
+ unit: chart.timeUnit === 'ms' ? 1 : 1000,
59
+ };
60
+ cache.set(chart, out);
61
+ return out;
62
+ }
63
+
64
+ /** PineScript `time(timeframe)` (ms) of a bar that opens at `time`: the start of its period; NaN without one. */
65
+ export function periodTime(time: number, timeframe: string, chart: SessionContext): number {
66
+ const s = chartSessions(chart);
67
+ return s.bars.timeOf(time * s.unit, timeframe);
68
+ }
69
+
70
+ /** PineScript `time_close(timeframe)` (ms) of a bar that opens at `time`. */
71
+ export function periodClose(time: number, timeframe: string, chart: SessionContext): number {
72
+ const s = chartSessions(chart);
73
+ return s.bars.closeOf(time * s.unit, timeframe);
74
+ }
75
+
76
+ /** PineScript `time_tradingday` (ms, 00:00 UTC of the trading day) of a bar that opens at `time`. */
77
+ export function tradingDayTime(time: number, chart: SessionContext): number {
78
+ const s = chartSessions(chart);
79
+ return s.bars.tradingDayTime(time * s.unit);
80
+ }
81
+
82
+ /** PineScript `session.*` flags of bars that open at `times` (in order). */
83
+ export function chartSessionFlags(times: ArrayLike<number>, chart: SessionContext): SessionFlags {
84
+ const s = chartSessions(chart);
85
+ return sessionFlags(Array.from(times, (t) => t * s.unit), s.bars, s.regular);
86
+ }
87
+
88
+ /** Higher-timeframe bars of the chart bars and what `mapToChart` needs to bring values back to the chart. */
89
+ export interface SessionResample {
90
+ /** Bars of `timeframe`, built from the chart bars */
91
+ bars: Bar[];
92
+ /** Period index of each chart bar */
93
+ group: number[];
94
+ /** Latest completed period at each chart bar */
95
+ latest: number[];
96
+ }
97
+
98
+ /**
99
+ * Same-symbol `request.security` on a timeframe equal to or higher than the chart's, with the session of the context:
100
+ * D / W / M periods follow the regular hours, corrections and holidays; intraday periods the loaded session.
101
+ * Compute on `bars`, then bring each value series back with `mapToChart(values, group, latest, lookahead, gaps)`.
102
+ * @throws Error when `timeframe` is lower than the chart timeframe
103
+ */
104
+ export function resampleSession(bars: Bar[], timeframe: string, chart: SessionContext): SessionResample {
105
+ const s = chartSessions(chart);
106
+ const chartTf = s.bars.chart;
107
+ const tf = info(timeframe || chartTf.period);
108
+ if (tf.period === chartTf.period) {
109
+ const group = bars.map((_, i) => i);
110
+ return { bars, group, latest: group };
111
+ }
112
+ if (in_seconds(tf.period) < in_seconds(chartTf.period)) {
113
+ throw new Error(`resampleSession: timeframe "${tf.period}" is lower than the chart timeframe "${chartTf.period}".`);
114
+ }
115
+ const times = bars.map((b) => (b.time as number) * s.unit);
116
+ const calendar = tf.isdwm ? s.regular : s.bars;
117
+ const starts = times.map((t) => calendar.timeOf(t, tf.period));
118
+ const dayPeriods = times.map((t, i) => {
119
+ const at = s.bars.calendar.locate(t);
120
+ const periods = at ? calendar.calendar.periods(at.date) : [];
121
+ return periods.length ? calendar.timeOf(periods[0]![0], tf.period) : starts[i]!;
122
+ });
123
+ const reaches = times.map((t) => s.bars.closeOf(t, chartTf.period) >= calendar.periodEnd(t, tf.period));
124
+ const { group, latest } = periodsOf(starts, dayPeriods, reaches);
125
+ // a higher-timeframe bar ends at the bar that completes it (no value from after the completion)
126
+ const after = afterCompletion(group, latest);
127
+ const kept = (_: unknown, i: number): boolean => !after[i];
128
+ return { bars: resample(bars.filter(kept), starts.map((x) => x / s.unit).filter(kept)).bars, group, latest };
129
+ }
130
+
131
+ /**
132
+ * Open time of the `count`-th bar after the bar that opens at `time`, on the trading calendar of the context: the
133
+ * closed hours, weekends, holidays and days off are skipped. `count` 0 is the bar itself. For a drawing that ends
134
+ * `count` bars after the last bar (PineScript `bar_index + count`).
135
+ */
136
+ export function barTimeAfter(time: number, count: number, chart: SessionContext): number {
137
+ const s = chartSessions(chart);
138
+ let t = time * s.unit;
139
+ for (let k = 0; k < count && !Number.isNaN(t); k++) t = s.bars.nextBarTime(t);
140
+ return t / s.unit;
141
+ }
142
+
143
+ /**
144
+ * Number of bars from the bar that opens at `from` to the bar that contains `to`, on the trading calendar of the
145
+ * context (0 when `to` is in the bar of `from` or before it). For a drawing placed at a time after the last bar
146
+ * (PineScript `time + n days` with `xloc.bar_time`): the chart shows it on the bar of that time, weekends and
147
+ * holidays skipped. NaN when no bar is found within 100,000 bars.
148
+ */
149
+ export function barsUntil(from: number, to: number, chart: SessionContext): number {
150
+ const s = chartSessions(chart);
151
+ const target = to * s.unit;
152
+ let t = from * s.unit;
153
+ for (let k = 0; k < 100_000; k++) {
154
+ const next = s.bars.nextBarTime(t);
155
+ if (Number.isNaN(next)) return NaN;
156
+ if (next > target) return k;
157
+ t = next;
158
+ }
159
+ return NaN;
160
+ }
package/src/str/index.ts CHANGED
@@ -45,7 +45,9 @@ export function length(str: simple_string): int {
45
45
  *
46
46
  * @remarks
47
47
  * As in PineScript:
48
- * - without format, numbers show up to 10 decimals ("0.3333333333"); 1e21 and more use "1E21"
48
+ * - without format, numbers show up to 10 decimals ("0.3333333333"), rounded on the exact value of the double
49
+ * (82325.10000000005 gives "82325.1"; with the format "#.##########" it gives "82325.1000000001"); 1e21 and more
50
+ * use "1E21"
49
51
  * - patterns round half away from zero on the shortest decimal form: 1.005 with "#.##" gives "1.01";
50
52
  * at most 16 decimals from 1e-3 up (0.0012345678901234567 with 20 '#' decimals gives "0.0012345678901235")
51
53
  * - "pos;neg" patterns: a negative number takes the text of the negative subpattern ("#.##;(#.##)": "(3.5)")
@@ -72,6 +74,8 @@ export function length(str: simple_string): int {
72
74
  * ```
73
75
  */
74
76
  export function tostring(value: any, format?: simple_string, mintick?: float, pricescale?: float): string {
77
+ // an array: "[a, b]", each element as a single value
78
+ if (Array.isArray(value)) return '[' + value.map((v) => tostring(v, format, mintick, pricescale)).join(', ') + ']';
75
79
  if (typeof value !== 'number') return String(value);
76
80
  if (Number.isNaN(value)) return format === 'percent' ? 'NaN%' : 'NaN';
77
81
  if (!Number.isFinite(value)) {
@@ -83,7 +87,8 @@ export function tostring(value: any, format?: simple_string, mintick?: float, pr
83
87
  if (Math.abs(value) >= 1e21 && Number.isFinite(value)) {
84
88
  return String(value).replace('e+', 'E').replace(/\.0+E/, 'E');
85
89
  }
86
- return formatNumber(value, '#.##########', 'halfUpShortest');
90
+ // without a format the exact value is rounded (82325.10000000005 is 82325.10000000004947...: "82325.1")
91
+ return formatNumber(value, '#.##########', 'halfUpExact');
87
92
  }
88
93
  switch (format) {
89
94
  case 'percent':
@@ -4,8 +4,10 @@
4
4
  * The pattern syntax is Java's DecimalFormat: `0` required digit, `#` optional digit, `,` grouping,
5
5
  * `.` decimal point, `E0` exponent, `%` multiplies by 100, `'...'` quoted literal text.
6
6
  * PineScript rules:
7
- * - `str.tostring` rounds half away from zero on the shortest decimal form of the number
7
+ * - `str.tostring` with a format rounds half away from zero on the shortest decimal form of the number
8
8
  * (`1.005` with "#.##" gives "1.01", `2.5` with "#" gives "3")
9
+ * - `str.tostring` without a format rounds the exact binary value to 10 decimals (12.34500000005 is
10
+ * 12.34500000004999975...: "12.345", and "12.3450000001" with the format "#.##########")
9
11
  * - `str.format` rounds half to even on the exact binary value (`0.25` with "#.#" gives "0.2")
10
12
  * - a negative number that rounds to zero loses its sign; "#.00" writes `.50` (no leading zero),
11
13
  * but "#.##" writes `0.5` (a pattern without any '0' shows one integer digit)
@@ -18,7 +20,7 @@
18
20
  * 0.1 with 20 '#' decimals gives "0.1" in both rounding modes
19
21
  */
20
22
 
21
- export type Rounding = 'halfUpShortest' | 'halfEvenExact';
23
+ export type Rounding = 'halfUpShortest' | 'halfUpExact' | 'halfEvenExact';
22
24
 
23
25
  interface Pattern {
24
26
  prefix: string;
@@ -161,7 +163,7 @@ function roundDigits(d: Digits, frac: number, mode: Rounding): Digits {
161
163
  if (keep < 0) return { digits: '', intLen: 0 };
162
164
  const first = d.digits.charCodeAt(keep) - 48;
163
165
  let up: boolean;
164
- if (mode === 'halfUpShortest') up = first >= 5;
166
+ if (mode !== 'halfEvenExact') up = first >= 5;
165
167
  else if (first !== 5) up = first > 5;
166
168
  else if (d.digits.length > keep + 1) up = true;
167
169
  else up = keep > 0 && (d.digits.charCodeAt(keep - 1) - 48) % 2 === 1;