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.
- package/dist/cjs/array/index.d.ts +88 -27
- package/dist/cjs/array/index.d.ts.map +1 -1
- package/dist/cjs/array/index.js +257 -117
- package/dist/cjs/array/index.js.map +3 -3
- package/dist/cjs/callsite/index.d.ts +122 -4
- package/dist/cjs/callsite/index.d.ts.map +1 -1
- package/dist/cjs/callsite/index.js +102 -3
- package/dist/cjs/callsite/index.js.map +2 -2
- package/dist/cjs/color/index.d.ts +7 -3
- package/dist/cjs/color/index.d.ts.map +1 -1
- package/dist/cjs/color/index.js +6 -1
- package/dist/cjs/color/index.js.map +2 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/label/index.d.ts.map +1 -1
- package/dist/cjs/label/index.js +2 -1
- package/dist/cjs/label/index.js.map +2 -2
- package/dist/cjs/linefill/index.d.ts.map +1 -1
- package/dist/cjs/linefill/index.js +1 -0
- package/dist/cjs/linefill/index.js.map +2 -2
- package/dist/cjs/math/index.d.ts +79 -22
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +157 -43
- package/dist/cjs/math/index.js.map +3 -3
- package/dist/cjs/matrix/index.d.ts.map +1 -1
- package/dist/cjs/matrix/index.js +42 -36
- package/dist/cjs/matrix/index.js.map +2 -2
- package/dist/cjs/runtime/series.d.ts +9 -0
- package/dist/cjs/runtime/series.d.ts.map +1 -1
- package/dist/cjs/runtime/series.js +16 -2
- package/dist/cjs/runtime/series.js.map +2 -2
- package/dist/cjs/script/index.d.ts +15 -31
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +32 -0
- package/dist/cjs/script/index.js.map +2 -2
- package/dist/cjs/session/calendar.d.ts.map +1 -1
- package/dist/cjs/session/calendar.js +15 -2
- package/dist/cjs/session/calendar.js.map +2 -2
- package/dist/cjs/str/index.d.ts +50 -38
- package/dist/cjs/str/index.d.ts.map +1 -1
- package/dist/cjs/str/index.js +43 -15
- package/dist/cjs/str/index.js.map +2 -2
- package/dist/cjs/str/messageformat.d.ts +2 -1
- package/dist/cjs/str/messageformat.d.ts.map +1 -1
- package/dist/cjs/str/messageformat.js +4 -1
- package/dist/cjs/str/messageformat.js.map +2 -2
- package/dist/cjs/str/numberformat.d.ts +4 -0
- package/dist/cjs/str/numberformat.d.ts.map +1 -1
- package/dist/cjs/str/numberformat.js +26 -4
- package/dist/cjs/str/numberformat.js.map +3 -3
- package/dist/cjs/ta/index.d.ts +95 -46
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +193 -318
- package/dist/cjs/ta/index.js.map +3 -3
- package/dist/cjs/ta/running-sum.d.ts +54 -1
- package/dist/cjs/ta/running-sum.d.ts.map +1 -1
- package/dist/cjs/ta/running-sum.js +132 -47
- package/dist/cjs/ta/running-sum.js.map +2 -2
- package/dist/cjs/ta-series.d.ts +4 -4
- package/dist/cjs/ta-series.d.ts.map +1 -1
- package/dist/cjs/ta-series.js +4 -3
- package/dist/cjs/ta-series.js.map +2 -2
- package/dist/cjs/time/datestring.d.ts +1 -0
- package/dist/cjs/time/datestring.d.ts.map +1 -1
- package/dist/cjs/time/datestring.js +7 -0
- package/dist/cjs/time/datestring.js.map +2 -2
- package/dist/cjs/time/index.d.ts.map +1 -1
- package/dist/cjs/time/index.js +2 -1
- package/dist/cjs/time/index.js.map +2 -2
- package/dist/cjs/time/session.d.ts +28 -9
- package/dist/cjs/time/session.d.ts.map +1 -1
- package/dist/cjs/time/session.js +74 -28
- package/dist/cjs/time/session.js.map +2 -2
- package/dist/cjs/time/timezone.d.ts +5 -0
- package/dist/cjs/time/timezone.d.ts.map +1 -1
- package/dist/cjs/time/timezone.js +15 -1
- package/dist/cjs/time/timezone.js.map +2 -2
- package/dist/esm/array/index.d.ts +88 -27
- package/dist/esm/array/index.d.ts.map +1 -1
- package/dist/esm/array/index.js +408 -157
- package/dist/esm/array/index.js.map +1 -1
- package/dist/esm/callsite/index.d.ts +122 -4
- package/dist/esm/callsite/index.d.ts.map +1 -1
- package/dist/esm/callsite/index.js +228 -8
- package/dist/esm/callsite/index.js.map +1 -1
- package/dist/esm/color/index.d.ts +7 -3
- package/dist/esm/color/index.d.ts.map +1 -1
- package/dist/esm/color/index.js +15 -3
- package/dist/esm/color/index.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/label/index.d.ts.map +1 -1
- package/dist/esm/label/index.js +2 -1
- package/dist/esm/label/index.js.map +1 -1
- package/dist/esm/linefill/index.d.ts.map +1 -1
- package/dist/esm/linefill/index.js +3 -0
- package/dist/esm/linefill/index.js.map +1 -1
- package/dist/esm/math/index.d.ts +79 -22
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +229 -63
- package/dist/esm/math/index.js.map +1 -1
- package/dist/esm/matrix/index.d.ts.map +1 -1
- package/dist/esm/matrix/index.js +57 -43
- package/dist/esm/matrix/index.js.map +1 -1
- package/dist/esm/runtime/series.d.ts +9 -0
- package/dist/esm/runtime/series.d.ts.map +1 -1
- package/dist/esm/runtime/series.js +18 -1
- package/dist/esm/runtime/series.js.map +1 -1
- package/dist/esm/script/index.d.ts +15 -31
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +44 -1
- package/dist/esm/script/index.js.map +1 -1
- package/dist/esm/session/calendar.d.ts.map +1 -1
- package/dist/esm/session/calendar.js +22 -2
- package/dist/esm/session/calendar.js.map +1 -1
- package/dist/esm/str/index.d.ts +50 -38
- package/dist/esm/str/index.d.ts.map +1 -1
- package/dist/esm/str/index.js +103 -52
- package/dist/esm/str/index.js.map +1 -1
- package/dist/esm/str/messageformat.d.ts +2 -1
- package/dist/esm/str/messageformat.d.ts.map +1 -1
- package/dist/esm/str/messageformat.js +7 -2
- package/dist/esm/str/messageformat.js.map +1 -1
- package/dist/esm/str/numberformat.d.ts +4 -0
- package/dist/esm/str/numberformat.d.ts.map +1 -1
- package/dist/esm/str/numberformat.js +38 -4
- package/dist/esm/str/numberformat.js.map +1 -1
- package/dist/esm/ta/index.d.ts +95 -46
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +341 -414
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/running-sum.d.ts +54 -1
- package/dist/esm/ta/running-sum.d.ts.map +1 -1
- package/dist/esm/ta/running-sum.js +166 -60
- package/dist/esm/ta/running-sum.js.map +1 -1
- package/dist/esm/ta-series.d.ts +4 -4
- package/dist/esm/ta-series.d.ts.map +1 -1
- package/dist/esm/ta-series.js +5 -4
- package/dist/esm/ta-series.js.map +1 -1
- package/dist/esm/time/datestring.d.ts +1 -0
- package/dist/esm/time/datestring.d.ts.map +1 -1
- package/dist/esm/time/datestring.js +12 -0
- package/dist/esm/time/datestring.js.map +1 -1
- package/dist/esm/time/index.d.ts.map +1 -1
- package/dist/esm/time/index.js +3 -1
- package/dist/esm/time/index.js.map +1 -1
- package/dist/esm/time/session.d.ts +28 -9
- package/dist/esm/time/session.d.ts.map +1 -1
- package/dist/esm/time/session.js +113 -42
- package/dist/esm/time/session.js.map +1 -1
- package/dist/esm/time/timezone.d.ts +5 -0
- package/dist/esm/time/timezone.d.ts.map +1 -1
- package/dist/esm/time/timezone.js +21 -1
- package/dist/esm/time/timezone.js.map +1 -1
- package/package.json +2 -2
- package/src/array/index.ts +382 -174
- package/src/callsite/index.ts +232 -8
- package/src/color/index.ts +15 -5
- package/src/index.ts +1 -1
- package/src/label/index.ts +2 -1
- package/src/linefill/index.ts +2 -0
- package/src/math/index.ts +261 -77
- package/src/matrix/index.ts +52 -52
- package/src/runtime/series.ts +22 -1
- package/src/script/index.ts +53 -1
- package/src/session/calendar.ts +22 -2
- package/src/str/index.ts +95 -51
- package/src/str/messageformat.ts +7 -2
- package/src/str/numberformat.ts +39 -4
- package/src/ta/index.ts +356 -458
- package/src/ta/running-sum.ts +187 -62
- package/src/ta-series.ts +8 -7
- package/src/time/datestring.ts +10 -0
- package/src/time/index.ts +3 -1
- package/src/time/session.ts +97 -47
- package/src/time/timezone.ts +21 -1
package/src/runtime/series.ts
CHANGED
|
@@ -174,6 +174,17 @@ export class BarData {
|
|
|
174
174
|
* const materialized = complex.materialize(); // Breaks closure chain
|
|
175
175
|
* ```
|
|
176
176
|
*/
|
|
177
|
+
/** Largest history reference of a script series (PineScript max_bars_back limit). */
|
|
178
|
+
export const MAX_BARS_BACK = 5000;
|
|
179
|
+
|
|
180
|
+
/** PineScript's runtime error RE10008 text. */
|
|
181
|
+
export function historyError(bar: number, barsBack: number): string {
|
|
182
|
+
return (
|
|
183
|
+
`Error on bar ${bar}: The script attempts to reference historical data that is too far from the current bar ` +
|
|
184
|
+
`(${barsBack} bars back). The history-referencing length for the expression must be a value between 0 and ${MAX_BARS_BACK}.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
export class Series {
|
|
178
189
|
private extractor: SeriesExtractor;
|
|
179
190
|
private dataSource: BarData;
|
|
@@ -186,6 +197,12 @@ export class Series {
|
|
|
186
197
|
* @param data - Bar data (Bar[] or BarData)
|
|
187
198
|
* @param extractor - Function to extract/compute value for each bar
|
|
188
199
|
*/
|
|
200
|
+
/**
|
|
201
|
+
* True for PineScript's built-in price variables (open, high, low, close, volume, hl2...): their history covers
|
|
202
|
+
* every bar, so references further than 5000 bars back are allowed (measured: close[bar_index] runs).
|
|
203
|
+
*/
|
|
204
|
+
fullHistory = false;
|
|
205
|
+
|
|
189
206
|
constructor(data: Bar[] | BarData, extractor: SeriesExtractor) {
|
|
190
207
|
// Support both Bar[] and BarData for backward compatibility
|
|
191
208
|
this.dataSource = data instanceof BarData ? data : new BarData(data);
|
|
@@ -219,7 +236,9 @@ export class Series {
|
|
|
219
236
|
* @returns Series with extracted field values
|
|
220
237
|
*/
|
|
221
238
|
static fromBars(bars: Bar[] | BarData, field: 'open' | 'high' | 'low' | 'close' | 'volume'): Series {
|
|
222
|
-
|
|
239
|
+
const series = new Series(bars, (bar) => bar[field] ?? NaN);
|
|
240
|
+
series.fullHistory = true;
|
|
241
|
+
return series;
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
/**
|
|
@@ -516,6 +535,8 @@ export class Series {
|
|
|
516
535
|
*/
|
|
517
536
|
offset(offset: number): Series {
|
|
518
537
|
if (offset < 0) throw new RangeError(`Series.offset(${offset}): the offset cannot be negative (it would read a future bar).`);
|
|
538
|
+
// PineScript runtime error RE10008: a script series keeps at most 5000 bars of history
|
|
539
|
+
if (offset > MAX_BARS_BACK && !this.fullHistory) throw new Error(historyError(0, Math.floor(offset)));
|
|
519
540
|
return new Series(this.dataSource, (_bar, i, data) => {
|
|
520
541
|
const targetIndex = i - offset;
|
|
521
542
|
if (targetIndex < 0 || targetIndex >= data.length) {
|
package/src/script/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import type {
|
|
|
55
55
|
PlotConfig,
|
|
56
56
|
ShapeConfig,
|
|
57
57
|
} from '../runtime/types.js';
|
|
58
|
-
import { BarData, Series } from '../runtime/series.js';
|
|
58
|
+
import { BarData, Series, MAX_BARS_BACK, historyError } from '../runtime/series.js';
|
|
59
59
|
import * as taSeries from '../ta-series.js';
|
|
60
60
|
import * as colorCore from '../color/index.js';
|
|
61
61
|
import { inputColorDefault } from '../color/parse.js';
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
type StrategyTrade,
|
|
99
99
|
type StrategyVariable,
|
|
100
100
|
} from '../strategy/index.js';
|
|
101
|
+
import { parsePineSession } from '../time/session.js';
|
|
101
102
|
|
|
102
103
|
/** Everything one run of a script produced. */
|
|
103
104
|
export interface ScriptRunResult {
|
|
@@ -264,6 +265,7 @@ export function executeScript(
|
|
|
264
265
|
scriptOptions = options;
|
|
265
266
|
ctx = freshCollector(inputs);
|
|
266
267
|
resetDrawings();
|
|
268
|
+
mathCore.resetRandom();
|
|
267
269
|
try {
|
|
268
270
|
body();
|
|
269
271
|
const c = ctx;
|
|
@@ -449,6 +451,8 @@ export const hl2 = new Series(ctxBars, (b) => (b.high + b.low) / 2);
|
|
|
449
451
|
export const hlc3 = new Series(ctxBars, (b) => (b.high + b.low + b.close) / 3);
|
|
450
452
|
export const ohlc4 = new Series(ctxBars, (b) => (b.open + b.high + b.low + b.close) / 4);
|
|
451
453
|
export const hlcc4 = new Series(ctxBars, (b) => (b.high + b.low + b.close + b.close) / 4);
|
|
454
|
+
// built-in price variables keep the whole history (no 5000-bar limit)
|
|
455
|
+
for (const s of [hl2, hlc3, ohlc4, hlcc4]) s.fullHistory = true;
|
|
452
456
|
|
|
453
457
|
const SOURCES: Record<string, Series> = { open, high, low, close, volume, hl2, hlc3, ohlc4, hlcc4 };
|
|
454
458
|
|
|
@@ -975,11 +979,30 @@ export interface BarContext {
|
|
|
975
979
|
* });
|
|
976
980
|
* ```
|
|
977
981
|
*/
|
|
982
|
+
/** Initial history buffer of a script series and its growth factor (measured, issue #149). */
|
|
983
|
+
const HISTORY_START = 242;
|
|
984
|
+
const HISTORY_GROWTH = 1.618034;
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* History buffer of one script series in eachBar(), as PineScript grows it (measured on PineScript, issue #149):
|
|
988
|
+
* the buffer starts with 242 bars; when the largest reference of a bar goes past it, a jump (the reference grew by
|
|
989
|
+
* more than 1 since the previous bar) sets the buffer to the reference, and a step of 1 grows it to
|
|
990
|
+
* round(size * 1.618034); a buffer above 5000 stops the script on the next bar (RE10008), with that size as
|
|
991
|
+
* "bars back". A reference above 5000 stops it at once.
|
|
992
|
+
*/
|
|
993
|
+
interface HistoryBuffer {
|
|
994
|
+
size: number;
|
|
995
|
+
previous: number;
|
|
996
|
+
barMax: number;
|
|
997
|
+
}
|
|
998
|
+
|
|
978
999
|
export function eachBar(fn: (c: BarContext) => number | boolean | void): Series {
|
|
979
1000
|
collector(); // same lifecycle guard (and error message) as the declaration calls
|
|
980
1001
|
const b = bars();
|
|
981
1002
|
const out: number[] = new Array(b.length).fill(NaN);
|
|
982
1003
|
const cache = new Map<Series, number[]>();
|
|
1004
|
+
const buffers = new Map<Series, HistoryBuffer>();
|
|
1005
|
+
let pendingError: { bar: number; barsBack: number } | null = null;
|
|
983
1006
|
let idx = 0;
|
|
984
1007
|
const ctx: BarContext = {
|
|
985
1008
|
get i() {
|
|
@@ -1006,6 +1029,13 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
|
|
|
1006
1029
|
},
|
|
1007
1030
|
get(src: Series, offset: number = 0): number {
|
|
1008
1031
|
if (offset < 0) throw new RangeError(`c.get(): the offset cannot be negative (${offset} would read a future bar).`);
|
|
1032
|
+
if (!src.fullHistory && offset > 0) {
|
|
1033
|
+
const k = Math.floor(offset);
|
|
1034
|
+
if (k > MAX_BARS_BACK) throw new Error(historyError(idx, k));
|
|
1035
|
+
let buffer = buffers.get(src);
|
|
1036
|
+
if (!buffer) buffers.set(src, (buffer = { size: HISTORY_START, previous: 0, barMax: 0 }));
|
|
1037
|
+
if (k > buffer.barMax) buffer.barMax = k;
|
|
1038
|
+
}
|
|
1009
1039
|
let vals = cache.get(src);
|
|
1010
1040
|
if (!vals) {
|
|
1011
1041
|
vals = src.toArray();
|
|
@@ -1021,9 +1051,21 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
|
|
|
1021
1051
|
};
|
|
1022
1052
|
try {
|
|
1023
1053
|
for (idx = 0; idx < b.length; idx++) {
|
|
1054
|
+
if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
|
|
1024
1055
|
currentBar = idx;
|
|
1025
1056
|
const r = fn(ctx);
|
|
1026
1057
|
out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
|
|
1058
|
+
// grow the history buffers from the largest reference of this bar
|
|
1059
|
+
for (const buffer of buffers.values()) {
|
|
1060
|
+
const k = buffer.barMax;
|
|
1061
|
+
if (k > buffer.size) {
|
|
1062
|
+
const next = k - buffer.previous > 1 ? k : Math.round(buffer.size * HISTORY_GROWTH);
|
|
1063
|
+
if (next > MAX_BARS_BACK && !pendingError) pendingError = { bar: idx + 1, barsBack: next };
|
|
1064
|
+
buffer.size = next;
|
|
1065
|
+
}
|
|
1066
|
+
buffer.previous = k;
|
|
1067
|
+
buffer.barMax = 0;
|
|
1068
|
+
}
|
|
1027
1069
|
}
|
|
1028
1070
|
} finally {
|
|
1029
1071
|
currentBar = -1;
|
|
@@ -1467,6 +1509,9 @@ export function inSession(time: number, session: string, timezone?: string): boo
|
|
|
1467
1509
|
/** PineScript `str.*`; `format_time` and the dates of `format` use the exchange time zone by default. */
|
|
1468
1510
|
export const str = {
|
|
1469
1511
|
...strCore,
|
|
1512
|
+
// format.mintick rounds to the chart symbol's tick (syminfo.mintick)
|
|
1513
|
+
tostring: ((value: unknown, format?: string, mintick?: number) =>
|
|
1514
|
+
strCore.tostring(value as never, format, format === 'mintick' ? (mintick ?? syminfo.mintick) : mintick)) as typeof strCore.tostring,
|
|
1470
1515
|
format_time(time: number, format: string = "yyyy-MM-dd'T'HH:mm:ssZ", timezone?: string): string {
|
|
1471
1516
|
return strCore.format_time(time, format, timezone ?? exchangeTimezone());
|
|
1472
1517
|
},
|
|
@@ -1514,6 +1559,13 @@ function calendarFor(session?: string, timezone?: string): { bars: SessionBars;
|
|
|
1514
1559
|
const { bars } = sessionBars();
|
|
1515
1560
|
if (!session && !timezone) return { bars, test: false };
|
|
1516
1561
|
const tz = timezone ?? exchangeTimezone();
|
|
1562
|
+
// a session string that is empty or does not start with a digit is the symbol session: "regular" the regular
|
|
1563
|
+
// hours, "extended" and any other name the loaded hours (PineScript)
|
|
1564
|
+
if (session !== undefined && parsePineSession(session) === null) {
|
|
1565
|
+
const name = session.trim();
|
|
1566
|
+
const symbol = name === 'regular' && chartCtx.regularSession ? chartCtx.regularSession : chartCtx.session!;
|
|
1567
|
+
return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
|
|
1568
|
+
}
|
|
1517
1569
|
const spec = session ? { session } : { session: toSpec(chartCtx.session!).session };
|
|
1518
1570
|
return { bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period), test: true };
|
|
1519
1571
|
}
|
package/src/session/calendar.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { zonedFields, zonedToUnix } from '../time/timezone.js';
|
|
19
|
+
import { parsePineSession } from '../time/session.js';
|
|
19
20
|
|
|
20
21
|
/** Session information of a symbol (hours, corrections, holidays). */
|
|
21
22
|
export interface SessionSpec {
|
|
@@ -44,6 +45,8 @@ interface Version {
|
|
|
44
45
|
periods: Period[];
|
|
45
46
|
/** Index 1 (Sunday) to 7 (Saturday) */
|
|
46
47
|
days: boolean[];
|
|
48
|
+
/** PineScript session strings: the periods of each weekday (index 1 to 7), from the "|" sections */
|
|
49
|
+
byDay?: Period[][];
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export const toDateNum = (y: number, m: number, d: number): DateNum => y * 10000 + m * 100 + d;
|
|
@@ -82,10 +85,26 @@ function parsePeriods(text: string, spec: string): Period[] {
|
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/** A PineScript session string as periods per weekday (rules in time/session.ts). */
|
|
89
|
+
function pineVersion(text: string, from: DateNum): Version {
|
|
90
|
+
const ranges = parsePineSession(text);
|
|
91
|
+
if (ranges === null) throw new SyntaxError(`Session "${text}" is the symbol session`);
|
|
92
|
+
const byDay: Period[][] = Array.from({ length: 8 }, () => []);
|
|
93
|
+
for (const { start, end, days } of ranges) {
|
|
94
|
+
const period: Period =
|
|
95
|
+
start === 0 && end === 0
|
|
96
|
+
? { start: 0, end: 1440, startOffset: 0, endOffset: 0 }
|
|
97
|
+
: { start, end, startOffset: end <= start ? 1 : 0, endOffset: 0 };
|
|
98
|
+
for (const d of days) byDay[d]!.push(period);
|
|
99
|
+
}
|
|
100
|
+
return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
function parseVersion(text: string, from: DateNum, spec: string, rule: 'symbol' | 'pine'): Version {
|
|
104
|
+
if (rule === 'pine') return pineVersion(text, from);
|
|
86
105
|
const [periodText = '', dayText] = text.trim().split(':');
|
|
87
106
|
const periods = parsePeriods(periodText, spec);
|
|
88
|
-
const allDays = periodText.trim() === '24x7'
|
|
107
|
+
const allDays = periodText.trim() === '24x7';
|
|
89
108
|
const digits = dayText ?? (allDays ? '1234567' : '23456');
|
|
90
109
|
if (!/^[1-7]+$/.test(digits)) throw new SyntaxError(`Invalid session days in "${spec}"`);
|
|
91
110
|
const days = new Array<boolean>(8).fill(false);
|
|
@@ -137,7 +156,8 @@ export class TradingCalendar {
|
|
|
137
156
|
periods = correction ?? [];
|
|
138
157
|
} else if (!this.holidays.has(date)) {
|
|
139
158
|
const version = [...this.versions].reverse().find((v) => v.from <= date)!;
|
|
140
|
-
if (version.
|
|
159
|
+
if (version.byDay) periods = version.byDay[weekdayOf(date)]!;
|
|
160
|
+
else if (version.days[weekdayOf(date)]) periods = version.periods;
|
|
141
161
|
}
|
|
142
162
|
const y = Math.floor(date / 10000);
|
|
143
163
|
const m = Math.floor(date / 100) % 100;
|
package/src/str/index.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { formatNumber } from './numberformat.js';
|
|
13
13
|
import { formatMessage } from './messageformat.js';
|
|
14
14
|
import { formatDate } from './dateformat.js';
|
|
15
|
+
import { round_to_mintick } from '../math/index.js';
|
|
15
16
|
import type { int, bool, float, simple_int, simple_string } from '../types/index.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -34,20 +35,28 @@ export function length(str: simple_string): int {
|
|
|
34
35
|
* Converts a value to its string representation (PineScript `str.tostring`).
|
|
35
36
|
*
|
|
36
37
|
* @param value - The value to convert (number, boolean, string, etc.)
|
|
37
|
-
* @param format - Optional number format: a pattern ("#.##", "#,###.00", "0.00%", "0.00E0") or
|
|
38
|
-
* `format.percent` ("percent"), `format.volume` ("volume"), `format.
|
|
38
|
+
* @param format - Optional number format: a pattern ("#.##", "#,###.00", "0.00%", "0.00E0", "#.##;(#.##)") or
|
|
39
|
+
* `format.percent` ("percent"), `format.volume` ("volume"), `format.mintick` ("mintick"),
|
|
40
|
+
* `format.price` / `format.inherit`
|
|
41
|
+
* @param mintick - Tick size for `format.mintick` (PineScript `syminfo.mintick`, implicit there). Not part of the
|
|
42
|
+
* PineScript signature: the core function cannot see the chart, so pass it, as for `math.round_to_mintick`
|
|
39
43
|
* @returns String representation of the value
|
|
40
44
|
*
|
|
41
45
|
* @remarks
|
|
42
46
|
* As in PineScript:
|
|
43
47
|
* - without format, numbers show up to 10 decimals ("0.3333333333"); 1e21 and more use "1E21"
|
|
44
|
-
* - patterns round half away from zero on the shortest decimal form: 1.005 with "#.##" gives "1.01"
|
|
48
|
+
* - patterns round half away from zero on the shortest decimal form: 1.005 with "#.##" gives "1.01";
|
|
49
|
+
* at most 16 decimals from 1e-3 up (0.0012345678901234567 with 20 '#' decimals gives "0.0012345678901235")
|
|
50
|
+
* - "pos;neg" patterns: a negative number takes the text of the negative subpattern ("#.##;(#.##)": "(3.5)")
|
|
45
51
|
* - "#" is an optional digit ("1.5" with "#.##"), "0" a required digit ("1.50" with "0.00"),
|
|
46
52
|
* "," groups thousands, "%" multiplies by 100, 'text' is literal text
|
|
47
53
|
* - `format.percent` shows 2 decimals and "%" without multiplying; `format.volume` uses K, M, B, T
|
|
48
54
|
* - `format.price` and `format.inherit` give "price1" / "inherit1" in PineScript: they are patterns
|
|
49
55
|
* without digit characters, and oakscriptjs does the same
|
|
50
|
-
* - `format.mintick`
|
|
56
|
+
* - `format.mintick` rounds to the tick (`math.round_to_mintick`) and shows the tick's decimals ("#.##" for 0.01);
|
|
57
|
+
* it throws without `mintick`
|
|
58
|
+
* - na gives "NaN" ("NaN%" with `format.percent`); an infinity gives "NaN" too ("NaN%" for `format.percent`,
|
|
59
|
+
* "NaNT" for `format.volume`), except `format.mintick`, which gives "Infinity" / "-Infinity"
|
|
51
60
|
*
|
|
52
61
|
* @example
|
|
53
62
|
* ```typescript
|
|
@@ -57,9 +66,14 @@ export function length(str: simple_string): int {
|
|
|
57
66
|
* str.tostring(12345.678, format.volume) // "12.346K"
|
|
58
67
|
* ```
|
|
59
68
|
*/
|
|
60
|
-
export function tostring(value: any, format?: simple_string): string {
|
|
69
|
+
export function tostring(value: any, format?: simple_string, mintick?: float): string {
|
|
61
70
|
if (typeof value !== 'number') return String(value);
|
|
62
|
-
if (
|
|
71
|
+
if (Number.isNaN(value)) return format === 'percent' ? 'NaN%' : 'NaN';
|
|
72
|
+
if (!Number.isFinite(value)) {
|
|
73
|
+
// the chart formatter has no infinity: NaN with the format's suffix
|
|
74
|
+
if (format === 'mintick') return String(value);
|
|
75
|
+
return format === 'percent' ? 'NaN%' : format === 'volume' ? 'NaNT' : 'NaN';
|
|
76
|
+
}
|
|
63
77
|
if (format === undefined || format === '') {
|
|
64
78
|
if (Math.abs(value) >= 1e21 && Number.isFinite(value)) {
|
|
65
79
|
return String(value).replace('e+', 'E').replace(/\.0+E/, 'E');
|
|
@@ -76,8 +90,16 @@ export function tostring(value: any, format?: simple_string): string {
|
|
|
76
90
|
}
|
|
77
91
|
return formatNumber(value, '#', 'halfUpShortest');
|
|
78
92
|
}
|
|
79
|
-
case 'mintick':
|
|
80
|
-
|
|
93
|
+
case 'mintick': {
|
|
94
|
+
if (mintick === undefined) {
|
|
95
|
+
throw new Error('str.tostring(x, format.mintick) needs the tick size: pass it as str.tostring(x, format.mintick, syminfo.mintick)');
|
|
96
|
+
}
|
|
97
|
+
// mask: one '#' per decimal of the tick ("#.##" for 0.01, "#" for 1)
|
|
98
|
+
const m = /^\d+(?:\.(\d+))?(?:e-(\d+))?$/.exec(String(mintick));
|
|
99
|
+
const decimals = m ? (m[1]?.length ?? 0) + Number(m[2] ?? 0) : 0;
|
|
100
|
+
const mask = decimals ? '#.' + '#'.repeat(decimals) : '#';
|
|
101
|
+
return formatNumber(round_to_mintick(value, mintick), mask, 'halfUpShortest');
|
|
102
|
+
}
|
|
81
103
|
default:
|
|
82
104
|
return formatNumber(value, format, 'halfUpShortest');
|
|
83
105
|
}
|
|
@@ -87,26 +109,33 @@ export function tostring(value: any, format?: simple_string): string {
|
|
|
87
109
|
* Converts a string to a number (float).
|
|
88
110
|
*
|
|
89
111
|
* @param str - The string to convert
|
|
90
|
-
* @returns The numeric value, or null if conversion fails
|
|
112
|
+
* @returns The numeric value, or null (na) if conversion fails
|
|
91
113
|
*
|
|
92
114
|
* @remarks
|
|
93
|
-
*
|
|
94
|
-
* -
|
|
95
|
-
* -
|
|
96
|
-
* -
|
|
97
|
-
*
|
|
115
|
+
* As in PineScript:
|
|
116
|
+
* - The whole text must be a number: a sign, digits and one optional "." ("0.5", ".5", "5.", "-12"); "12abc" is na
|
|
117
|
+
* - Scientific notation ("1e3"), "Infinity", "NaN", hexadecimal, "1,000" give na
|
|
118
|
+
* - The characters U+0000 to U+0020 (space, tab, line feed...) around the number are ignored; other spaces
|
|
119
|
+
* (no-break space U+00A0, U+3000) give na
|
|
98
120
|
*
|
|
99
121
|
* @example
|
|
100
122
|
* ```typescript
|
|
101
123
|
* str.tonumber("123") // Returns: 123
|
|
102
124
|
* str.tonumber("45.67") // Returns: 45.67
|
|
103
125
|
* str.tonumber("abc") // Returns: null
|
|
104
|
-
* str.tonumber("
|
|
126
|
+
* str.tonumber("12abc") // Returns: null
|
|
127
|
+
* str.tonumber("1e3") // Returns: null
|
|
105
128
|
* ```
|
|
106
129
|
*/
|
|
107
130
|
export function tonumber(str: simple_string): float | null {
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
// remove the characters U+0000..U+0020 at both ends
|
|
132
|
+
let start = 0;
|
|
133
|
+
let end = str.length;
|
|
134
|
+
while (start < end && str.charCodeAt(start) <= 0x20) start++;
|
|
135
|
+
while (end > start && str.charCodeAt(end - 1) <= 0x20) end--;
|
|
136
|
+
const text = str.slice(start, end);
|
|
137
|
+
if (!/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(text)) return null;
|
|
138
|
+
return parseFloat(text);
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
/**
|
|
@@ -118,6 +147,7 @@ export function tonumber(str: simple_string): float | null {
|
|
|
118
147
|
* @returns The extracted substring
|
|
119
148
|
*
|
|
120
149
|
* @remarks
|
|
150
|
+
* - An na `end` (NaN / null) is a runtime error in PineScript: it throws
|
|
121
151
|
* - If begin > end, indices are swapped (JavaScript behavior)
|
|
122
152
|
* - Negative indices are treated as 0
|
|
123
153
|
* - Indices beyond string length return empty string
|
|
@@ -130,6 +160,7 @@ export function tonumber(str: simple_string): float | null {
|
|
|
130
160
|
* ```
|
|
131
161
|
*/
|
|
132
162
|
export function substring(str: simple_string, begin: simple_int, end?: simple_int): string {
|
|
163
|
+
if (end === null || Number.isNaN(end)) throw new Error('str.substring: the end position is na');
|
|
133
164
|
return str.substring(begin, end);
|
|
134
165
|
}
|
|
135
166
|
|
|
@@ -194,51 +225,63 @@ export function contains(source: simple_string, str: simple_string): bool {
|
|
|
194
225
|
*
|
|
195
226
|
* @param source - The string to search in
|
|
196
227
|
* @param str - The substring to search for
|
|
197
|
-
* @returns The 0-based index of the first occurrence, or
|
|
228
|
+
* @returns The 0-based index of the first occurrence, or na (NaN) if not found
|
|
198
229
|
*
|
|
199
230
|
* @remarks
|
|
200
231
|
* - Search is case-sensitive
|
|
201
232
|
* - Returns 0 for empty search string
|
|
202
|
-
* - Returns
|
|
233
|
+
* - Returns na (NaN) if substring is not found, as in PineScript
|
|
203
234
|
*
|
|
204
235
|
* @example
|
|
205
236
|
* ```typescript
|
|
206
237
|
* str.pos("hello world", "world") // Returns: 6
|
|
207
238
|
* str.pos("hello world", "o") // Returns: 4 (first occurrence)
|
|
208
|
-
* str.pos("hello", "xyz") // Returns:
|
|
239
|
+
* str.pos("hello", "xyz") // Returns: NaN (na)
|
|
209
240
|
* ```
|
|
210
241
|
*/
|
|
211
242
|
export function pos(source: simple_string, str: simple_string): int {
|
|
212
|
-
|
|
243
|
+
const index = source.indexOf(str);
|
|
244
|
+
return index === -1 ? NaN : index;
|
|
213
245
|
}
|
|
214
246
|
|
|
215
247
|
/**
|
|
216
|
-
* Replaces
|
|
248
|
+
* Replaces the Nth occurrence of a substring (PineScript `str.replace`).
|
|
217
249
|
*
|
|
218
250
|
* @param source - The source string
|
|
219
251
|
* @param target - The substring to replace
|
|
220
252
|
* @param replacement - The string to replace with
|
|
221
|
-
* @param occurrence -
|
|
222
|
-
* @returns The string with
|
|
253
|
+
* @param occurrence - 0-based occurrence to replace (default 0, the first one)
|
|
254
|
+
* @returns The string with one occurrence replaced
|
|
223
255
|
*
|
|
224
256
|
* @remarks
|
|
225
|
-
*
|
|
226
|
-
* -
|
|
227
|
-
* -
|
|
257
|
+
* PineScript rules:
|
|
258
|
+
* - only the Nth occurrence is replaced; an na occurrence is 0 (`str.replace("aaa", "a", "-", na)` is "-aa")
|
|
259
|
+
* - occurrences overlap: "aa" occurs at 0 and 1 in "aaa" (`str.replace("aaa", "aa", "-", 1)` is "a-")
|
|
260
|
+
* - an empty target inserts the replacement at character N, at most at the end
|
|
261
|
+
* (`str.replace("abc", "", "-", 2)` is "ab-c")
|
|
262
|
+
* - fewer than N + 1 occurrences (or a negative N) leave the source unchanged
|
|
228
263
|
*
|
|
229
264
|
* @example
|
|
230
265
|
* ```typescript
|
|
231
|
-
* str.replace("hello world hello", "hello", "hi"
|
|
232
|
-
* str.replace("hello world hello", "hello", "hi") // Returns: "
|
|
266
|
+
* str.replace("hello world hello", "hello", "hi") // Returns: "hi world hello"
|
|
267
|
+
* str.replace("hello world hello", "hello", "hi", 1) // Returns: "hello world hi"
|
|
233
268
|
* str.replace("test", "xyz", "abc") // Returns: "test"
|
|
234
269
|
* ```
|
|
235
270
|
*/
|
|
236
|
-
export function replace(source: simple_string, target: simple_string, replacement: simple_string, occurrence
|
|
237
|
-
|
|
238
|
-
|
|
271
|
+
export function replace(source: simple_string, target: simple_string, replacement: simple_string, occurrence: simple_int = 0): string {
|
|
272
|
+
const n = occurrence === null || Number.isNaN(occurrence) ? 0 : Math.trunc(occurrence);
|
|
273
|
+
if (n < 0) return source;
|
|
274
|
+
let index: number;
|
|
275
|
+
if (target === '') {
|
|
276
|
+
index = Math.min(n, source.length);
|
|
239
277
|
} else {
|
|
240
|
-
|
|
278
|
+
index = -1;
|
|
279
|
+
for (let k = 0; k <= n; k++) {
|
|
280
|
+
index = source.indexOf(target, index + 1);
|
|
281
|
+
if (index === -1) return source;
|
|
282
|
+
}
|
|
241
283
|
}
|
|
284
|
+
return source.slice(0, index) + replacement + source.slice(index + target.length);
|
|
242
285
|
}
|
|
243
286
|
|
|
244
287
|
/**
|
|
@@ -274,7 +317,7 @@ export function replace_all(source: simple_string, target: simple_string, replac
|
|
|
274
317
|
* @returns Array of substrings
|
|
275
318
|
*
|
|
276
319
|
* @remarks
|
|
277
|
-
* - Empty separator splits into individual characters
|
|
320
|
+
* - Empty separator splits into individual characters; an empty string gives [""] (also with "")
|
|
278
321
|
* - If separator is not found, returns array with original string
|
|
279
322
|
* - Consecutive separators create empty strings in result
|
|
280
323
|
*
|
|
@@ -286,7 +329,8 @@ export function replace_all(source: simple_string, target: simple_string, replac
|
|
|
286
329
|
* ```
|
|
287
330
|
*/
|
|
288
331
|
export function split(str: simple_string, separator: simple_string): string[] {
|
|
289
|
-
|
|
332
|
+
// JavaScript gives [] for "".split(""); PineScript gives [""]
|
|
333
|
+
return str === '' ? [''] : str.split(separator);
|
|
290
334
|
}
|
|
291
335
|
|
|
292
336
|
/**
|
|
@@ -316,7 +360,8 @@ export function concat(...strings: simple_string[]): string {
|
|
|
316
360
|
* @remarks
|
|
317
361
|
* As in PineScript: numbers in `{n}` use
|
|
318
362
|
* "#,##0.###" (1234.5678 gives "1,234.568"); number patterns round half to even on the exact
|
|
319
|
-
* value; `'...'` is literal text and `''` a quote;
|
|
363
|
+
* value (16 decimals at most from 1e-3 up); an array gives "[a, b]"; `'...'` is literal text and `''` a quote;
|
|
364
|
+
* a missing argument leaves `{n}` in the text.
|
|
320
365
|
* Dates are formatted in UTC here; PineScript uses the exchange time zone.
|
|
321
366
|
*
|
|
322
367
|
* @example
|
|
@@ -459,26 +504,25 @@ export function trimRight(str: simple_string): string {
|
|
|
459
504
|
}
|
|
460
505
|
|
|
461
506
|
/**
|
|
462
|
-
*
|
|
507
|
+
* Returns the first substring of `source` that matches the `regex` (PineScript `str.match`).
|
|
463
508
|
*
|
|
464
|
-
* @param source -
|
|
465
|
-
* @param regex -
|
|
466
|
-
* @returns
|
|
509
|
+
* @param source - Source string
|
|
510
|
+
* @param regex - Regular expression
|
|
511
|
+
* @returns The first match (searched anywhere in the string), or na (null) when nothing matches or the match is empty
|
|
467
512
|
*
|
|
468
513
|
* @remarks
|
|
514
|
+
* - As PineScript: "Hello World!" with "o" gives "o", with "[a-z]+" gives "ello"; no match gives na
|
|
469
515
|
* - Uses JavaScript regex syntax
|
|
470
|
-
* - Match is case-sensitive by default
|
|
471
|
-
* - Pattern is tested against entire string
|
|
472
516
|
*
|
|
473
517
|
* @example
|
|
474
518
|
* ```typescript
|
|
475
|
-
* str.match("
|
|
476
|
-
* str.match("
|
|
477
|
-
* str.match("abc", "[A-Z]+") // Returns: false
|
|
519
|
+
* str.match("abc123def", "[0-9]+") // Returns: "123"
|
|
520
|
+
* str.match("abc", "[0-9]+") // Returns: null (na)
|
|
478
521
|
* ```
|
|
479
522
|
*/
|
|
480
|
-
export function match(source: simple_string, regex: simple_string):
|
|
481
|
-
|
|
523
|
+
export function match(source: simple_string, regex: simple_string): simple_string | null {
|
|
524
|
+
const found = new RegExp(regex).exec(source);
|
|
525
|
+
return found && found[0] !== '' ? found[0] : null;
|
|
482
526
|
}
|
|
483
527
|
|
|
484
528
|
/**
|
|
@@ -521,21 +565,21 @@ export function match(source: simple_string, regex: simple_string): bool {
|
|
|
521
565
|
* Constructs a new string containing the source string repeated N times with an optional separator.
|
|
522
566
|
*
|
|
523
567
|
* @param source - The string to repeat
|
|
524
|
-
* @param count - Number of times to repeat
|
|
568
|
+
* @param count - Number of times to repeat
|
|
525
569
|
* @param separator - String to inject between repeated instances. Optional, defaults to empty string.
|
|
526
|
-
* @returns The repeated string, or null if source is
|
|
570
|
+
* @returns The repeated string, or null (na) if source is na or count is 0 / negative / na, as in PineScript
|
|
527
571
|
*
|
|
528
572
|
* @example
|
|
529
573
|
* ```typescript
|
|
530
574
|
* str.repeat("ab", 3) // Returns: "ababab"
|
|
531
575
|
* str.repeat("?", 3, ",") // Returns: "?,?,?"
|
|
532
|
-
* str.repeat("hello", 0) // Returns:
|
|
576
|
+
* str.repeat("hello", 0) // Returns: null (na)
|
|
533
577
|
* str.repeat("x", 1) // Returns: "x"
|
|
534
578
|
* ```
|
|
535
579
|
*/
|
|
536
580
|
export function repeat(source: simple_string, count: simple_int, separator: simple_string = ''): string | null {
|
|
537
581
|
if (source == null) return null;
|
|
538
|
-
if (count
|
|
582
|
+
if (!(count > 0)) return null; // 0, negative or na: na
|
|
539
583
|
return Array(count).fill(source).join(separator);
|
|
540
584
|
}
|
|
541
585
|
|
package/src/str/messageformat.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `str.format` patterns as PineScript formats them (Java MessageFormat):
|
|
3
3
|
*
|
|
4
|
-
* - `{n}` inserts argument n; numbers use "#,##0.###" (1234.5678 gives "1,234.568")
|
|
4
|
+
* - `{n}` inserts argument n; numbers use "#,##0.###" (1234.5678 gives "1,234.568"); an array gives
|
|
5
|
+
* "[Hello, World!]"
|
|
5
6
|
* - `{n,number}`, `{n,number,integer}` ("#,##0"), `{n,number,percent}` ("#,##0%"),
|
|
6
7
|
* `{n,number,currency}` ("$1,234.57", "-$7.00"), `{n,number,<pattern>}`
|
|
7
8
|
* - `{n,date}`, `{n,time}`, `{n,date,<pattern>}`, `{n,time,<pattern>}` (styles short, medium, long, full)
|
|
@@ -44,7 +45,11 @@ function formatArgument(
|
|
|
44
45
|
timezone: string | (() => string)
|
|
45
46
|
): string {
|
|
46
47
|
if (value === undefined) return '';
|
|
47
|
-
if (!type)
|
|
48
|
+
if (!type) {
|
|
49
|
+
// an array gives "[a, b]" (PineScript); the element format of numbers is not measured: plain JS text
|
|
50
|
+
if (Array.isArray(value)) return '[' + value.map((v) => (v == null ? 'NaN' : String(v))).join(', ') + ']';
|
|
51
|
+
return typeof value === 'number' ? number(value, '#,##0.###') : String(value);
|
|
52
|
+
}
|
|
48
53
|
switch (type.trim()) {
|
|
49
54
|
case 'number': {
|
|
50
55
|
const s = style?.trim();
|
package/src/str/numberformat.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* - with an exponent, the value is first rounded to the fraction digits ("0.00E0": 0.125 gives "1.30E-1")
|
|
13
13
|
* - a pattern without digit characters (`format.price` is "price") formats like "#" after its text,
|
|
14
14
|
* and keeps the sign: -0.004 gives "-price0"
|
|
15
|
+
* - "pos;neg": a negative number uses the text around the digits of the negative subpattern instead of "-"
|
|
16
|
+
* ("#.##;(#.##)" gives "(3.5)" for -3.5); the digits follow the positive subpattern, as in Java
|
|
17
|
+
* - at most 16 decimals when |x| >= 1e-3 (the shortest digits of a double never have more there):
|
|
18
|
+
* 0.1 with 20 '#' decimals gives "0.1" in both rounding modes
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
export type Rounding = 'halfUpShortest' | 'halfEvenExact';
|
|
@@ -19,6 +23,9 @@ export type Rounding = 'halfUpShortest' | 'halfEvenExact';
|
|
|
19
23
|
interface Pattern {
|
|
20
24
|
prefix: string;
|
|
21
25
|
suffix: string;
|
|
26
|
+
/** Text around the digits of the negative subpattern ("pos;neg"), or null without one */
|
|
27
|
+
negPrefix: string | null;
|
|
28
|
+
negSuffix: string | null;
|
|
22
29
|
minInt: number;
|
|
23
30
|
minFrac: number;
|
|
24
31
|
maxFrac: number;
|
|
@@ -32,9 +39,27 @@ interface Pattern {
|
|
|
32
39
|
|
|
33
40
|
const patterns = new Map<string, Pattern>();
|
|
34
41
|
|
|
42
|
+
/** Index of the first ';' outside quotes, or -1. */
|
|
43
|
+
function subpatternSplit(pattern: string): number {
|
|
44
|
+
let quoted = false;
|
|
45
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
46
|
+
if (pattern[i] === "'") quoted = !quoted;
|
|
47
|
+
else if (pattern[i] === ';' && !quoted) return i;
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
function parsePattern(pattern: string): Pattern {
|
|
36
53
|
const cached = patterns.get(pattern);
|
|
37
54
|
if (cached) return cached;
|
|
55
|
+
const semi = subpatternSplit(pattern);
|
|
56
|
+
if (semi !== -1) {
|
|
57
|
+
// only the text around the digits of the negative subpattern is used
|
|
58
|
+
const neg = parsePattern(pattern.slice(semi + 1));
|
|
59
|
+
const parsed = { ...parsePattern(pattern.slice(0, semi)), negPrefix: neg.prefix, negSuffix: neg.suffix };
|
|
60
|
+
patterns.set(pattern, parsed);
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
38
63
|
|
|
39
64
|
let prefix = '';
|
|
40
65
|
let suffix = '';
|
|
@@ -72,6 +97,8 @@ function parsePattern(pattern: string): Pattern {
|
|
|
72
97
|
const parsed: Pattern = {
|
|
73
98
|
prefix,
|
|
74
99
|
suffix,
|
|
100
|
+
negPrefix: null,
|
|
101
|
+
negSuffix: null,
|
|
75
102
|
// as in Java, a pattern with a decimal point and no '0' at all ("#.##") shows one integer digit
|
|
76
103
|
minInt: !mantissa.includes('0') && mantissa.includes('.') ? 1 : [...intPart].filter((c) => c === '0').length,
|
|
77
104
|
minFrac: [...fracPart].filter((c) => c === '0').length,
|
|
@@ -169,7 +196,7 @@ function layout(d: Digits, p: Pattern): string {
|
|
|
169
196
|
export function formatNumber(value: number, pattern: string, mode: Rounding): string {
|
|
170
197
|
if (Number.isNaN(value)) return 'NaN';
|
|
171
198
|
const p = parsePattern(pattern);
|
|
172
|
-
if (!Number.isFinite(value)) return (value < 0
|
|
199
|
+
if (!Number.isFinite(value)) return affixes(p, value < 0, '∞');
|
|
173
200
|
const x = p.percent ? value * 100 : value;
|
|
174
201
|
const toDigits = mode === 'halfUpShortest' ? shortestDigits : exactDigits;
|
|
175
202
|
|
|
@@ -188,10 +215,18 @@ export function formatNumber(value: number, pattern: string, mode: Rounding): st
|
|
|
188
215
|
const e = String(Math.abs(exponent)).padStart(p.exponent, '0');
|
|
189
216
|
body = layout(mant, p) + 'E' + (exponent < 0 ? '-' : '') + e;
|
|
190
217
|
} else {
|
|
191
|
-
|
|
218
|
+
// 16 decimals at most from 1e-3 up (the shortest digits of a double have no more there)
|
|
219
|
+
const frac = !p.hasDigits ? 0 : Math.abs(x) >= 1e-3 ? Math.min(p.maxFrac, 16) : p.maxFrac;
|
|
220
|
+
const rounded = roundDigits(toDigits(x), frac, mode);
|
|
192
221
|
isZero = rounded.digits === '';
|
|
193
222
|
body = p.hasDigits ? layout(rounded, p) : layout(rounded, { ...p, minInt: 0, maxFrac: 0, minFrac: 0 });
|
|
194
223
|
}
|
|
195
|
-
|
|
196
|
-
|
|
224
|
+
return affixes(p, x < 0 && (!isZero || !p.hasDigits), body);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** `body` with the text of the positive pattern, or of the negative one ("-" + prefix without "pos;neg"). */
|
|
228
|
+
function affixes(p: Pattern, negative: boolean, body: string): string {
|
|
229
|
+
if (!negative) return p.prefix + body + p.suffix;
|
|
230
|
+
if (p.negPrefix === null) return '-' + p.prefix + body + p.suffix;
|
|
231
|
+
return p.negPrefix + body + p.negSuffix!;
|
|
197
232
|
}
|