@hoox-sh/pynets 0.1.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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +661 -0
- package/NOTICE +46 -0
- package/README.md +327 -0
- package/dist/browser.js +44 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +41741 -0
- package/package.json +77 -0
- package/src/ast/builder.ts +1002 -0
- package/src/ast/dump.ts +43 -0
- package/src/ast/helper.ts +77 -0
- package/src/ast/nodes.ts +570 -0
- package/src/ast/unparse.ts +360 -0
- package/src/cli/rich.ts +378 -0
- package/src/cli.ts +627 -0
- package/src/generated/PinescriptLexer.interp +267 -0
- package/src/generated/PinescriptLexer.tokens +131 -0
- package/src/generated/PinescriptLexer.ts +673 -0
- package/src/generated/PinescriptLexerBase.ts +395 -0
- package/src/generated/PinescriptParser.interp +267 -0
- package/src/generated/PinescriptParser.tokens +131 -0
- package/src/generated/PinescriptParser.ts +8863 -0
- package/src/generated/PinescriptParserBase.ts +21 -0
- package/src/generated/PinescriptParserVisitor.ts +840 -0
- package/src/index.ts +55 -0
- package/src/parser/LexerBase.ts +395 -0
- package/src/parser/ParserBase.ts +21 -0
- package/src/parser/error.ts +33 -0
- package/src/runtime/array.ts +486 -0
- package/src/runtime/color.ts +164 -0
- package/src/runtime/drawings.ts +510 -0
- package/src/runtime/input.ts +102 -0
- package/src/runtime/interpret.ts +3897 -0
- package/src/runtime/library.ts +282 -0
- package/src/runtime/log.ts +69 -0
- package/src/runtime/map.ts +85 -0
- package/src/runtime/math.ts +231 -0
- package/src/runtime/matrix.ts +1154 -0
- package/src/runtime/provider.ts +232 -0
- package/src/runtime/request.ts +372 -0
- package/src/runtime/series.ts +44 -0
- package/src/runtime/str.ts +433 -0
- package/src/runtime/strategy.ts +1105 -0
- package/src/runtime/ta.ts +2093 -0
- package/src/runtime/ticker.ts +81 -0
- package/src/runtime/time.ts +439 -0
- package/src/runtime/timeframe.ts +85 -0
- package/src/runtime/udt.ts +119 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine `str.*` helpers. `null` is na.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function asStr(x: unknown): string | null {
|
|
9
|
+
if (x == null) return null;
|
|
10
|
+
return typeof x === "string" ? x : String(x);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `str.tostring(x)` — na becomes `""`. */
|
|
14
|
+
export function strTostring(x: unknown): string {
|
|
15
|
+
return asStr(x) ?? "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const strTosring = strTostring;
|
|
19
|
+
|
|
20
|
+
export function strLength(s: unknown): number | null {
|
|
21
|
+
const v = asStr(s);
|
|
22
|
+
return v == null ? null : v.length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function strContains(s: unknown, sub: unknown): boolean | null {
|
|
26
|
+
const hay = asStr(s);
|
|
27
|
+
const needle = asStr(sub);
|
|
28
|
+
if (hay == null || needle == null) return null;
|
|
29
|
+
return hay.includes(needle);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function strUpper(s: unknown): string | null {
|
|
33
|
+
const v = asStr(s);
|
|
34
|
+
return v == null ? null : v.toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function strLower(s: unknown): string | null {
|
|
38
|
+
const v = asStr(s);
|
|
39
|
+
return v == null ? null : v.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `str.replace(s, target, replacement, occurrence?)`.
|
|
44
|
+
*
|
|
45
|
+
* Pine/Python: occurrence is 0-based; default 0 replaces the first match only.
|
|
46
|
+
* `str.replace_all` replaces every match. Negative occurrence is a no-op.
|
|
47
|
+
* Empty target inserts `replacement` at index `occurrence`.
|
|
48
|
+
*/
|
|
49
|
+
export function strReplace(
|
|
50
|
+
s: unknown,
|
|
51
|
+
target: unknown,
|
|
52
|
+
replacement: unknown,
|
|
53
|
+
occurrence?: number | null,
|
|
54
|
+
): string | null {
|
|
55
|
+
if (s == null) return null;
|
|
56
|
+
const src = asStr(s) ?? "";
|
|
57
|
+
const tgt = asStr(target) ?? "";
|
|
58
|
+
const rep = asStr(replacement) ?? "";
|
|
59
|
+
const occ =
|
|
60
|
+
typeof occurrence === "number" && Number.isFinite(occurrence)
|
|
61
|
+
? Math.trunc(occurrence)
|
|
62
|
+
: 0;
|
|
63
|
+
if (occ < 0) return src;
|
|
64
|
+
if (tgt === "") {
|
|
65
|
+
if (occ > src.length) return src;
|
|
66
|
+
return src.slice(0, occ) + rep + src.slice(occ);
|
|
67
|
+
}
|
|
68
|
+
let start = 0;
|
|
69
|
+
for (let n = 0; n <= occ; n++) {
|
|
70
|
+
const idx = src.indexOf(tgt, start);
|
|
71
|
+
if (idx < 0) return src;
|
|
72
|
+
if (n === occ) return src.slice(0, idx) + rep + src.slice(idx + tgt.length);
|
|
73
|
+
start = idx + tgt.length;
|
|
74
|
+
}
|
|
75
|
+
return src;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asInt(x: unknown): number | null {
|
|
79
|
+
if (x == null) return null;
|
|
80
|
+
if (typeof x === "boolean") return x ? 1 : 0;
|
|
81
|
+
if (typeof x === "number") return Number.isFinite(x) ? Math.trunc(x) : null;
|
|
82
|
+
if (typeof x === "string") {
|
|
83
|
+
const n = Number(x.trim());
|
|
84
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Python default `{num:g}` — 6 significant digits. */
|
|
90
|
+
function formatG(n: number): string {
|
|
91
|
+
if (n === 0) return "0";
|
|
92
|
+
const s = n.toPrecision(6);
|
|
93
|
+
if (/[eE]/.test(s)) return s.replace(/\.?0+([eE])/, "$1");
|
|
94
|
+
return String(Number(s));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `str.startswith(source, str)` — either arg na → na. */
|
|
98
|
+
export function strStartsWith(s: unknown, prefix: unknown): boolean | null {
|
|
99
|
+
const hay = asStr(s);
|
|
100
|
+
const pre = asStr(prefix);
|
|
101
|
+
if (hay == null || pre == null) return null;
|
|
102
|
+
return hay.startsWith(pre);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `str.endswith(source, str)` — either arg na → na. */
|
|
106
|
+
export function strEndsWith(s: unknown, suffix: unknown): boolean | null {
|
|
107
|
+
const hay = asStr(s);
|
|
108
|
+
const suf = asStr(suffix);
|
|
109
|
+
if (hay == null || suf == null) return null;
|
|
110
|
+
return hay.endsWith(suf);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `str.substring(source, begin_pos, end_pos?)`.
|
|
115
|
+
* End is exclusive (Python slice). Any na argument → na.
|
|
116
|
+
*/
|
|
117
|
+
export function strSubstring(
|
|
118
|
+
s: unknown,
|
|
119
|
+
begin: unknown,
|
|
120
|
+
end?: unknown,
|
|
121
|
+
): string | null {
|
|
122
|
+
if (s == null) return null;
|
|
123
|
+
const value = asStr(s) ?? "";
|
|
124
|
+
if (begin == null) return null;
|
|
125
|
+
const startI = asInt(begin);
|
|
126
|
+
if (startI == null) return null;
|
|
127
|
+
if (end === undefined) return value.slice(startI);
|
|
128
|
+
if (end == null) return null;
|
|
129
|
+
const endI = asInt(end);
|
|
130
|
+
if (endI == null) return null;
|
|
131
|
+
return value.slice(startI, endI);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Cap `str.repeat` output so a huge count cannot throw / OOM. */
|
|
135
|
+
const MAX_REPEAT_CHARS = 1_000_000;
|
|
136
|
+
|
|
137
|
+
/** `str.repeat(source, num)` — either arg na → na; negative n → `""`. */
|
|
138
|
+
export function strRepeat(s: unknown, n: unknown): string | null {
|
|
139
|
+
if (s == null || n == null) return null;
|
|
140
|
+
const value = asStr(s) ?? "";
|
|
141
|
+
let count = asInt(n);
|
|
142
|
+
if (count == null) return null;
|
|
143
|
+
if (count < 0) count = 0;
|
|
144
|
+
if (value.length > 0 && count > Math.floor(MAX_REPEAT_CHARS / value.length)) return null;
|
|
145
|
+
return value.repeat(count);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `str.trim(string)` — strip whitespace; na → na. */
|
|
149
|
+
export function strTrim(s: unknown): string | null {
|
|
150
|
+
const v = asStr(s);
|
|
151
|
+
return v == null ? null : v.trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `str.split(source, separator?)` → array of substrings.
|
|
156
|
+
* na source → `""` then split (Python never returns na). Empty sep → characters.
|
|
157
|
+
* Omitted / na separator → whitespace split.
|
|
158
|
+
*/
|
|
159
|
+
export function strSplit(s: unknown, separator?: unknown): string[] | null {
|
|
160
|
+
const value = s == null ? "" : (asStr(s) ?? "");
|
|
161
|
+
if (separator === undefined || separator == null) {
|
|
162
|
+
const t = value.trim();
|
|
163
|
+
return t === "" ? [] : t.split(/\s+/);
|
|
164
|
+
}
|
|
165
|
+
const sep = asStr(separator) ?? "";
|
|
166
|
+
if (sep === "") return [...value];
|
|
167
|
+
return value.split(sep);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** `str.tonumber(string)` — unparseable / empty / na → na. */
|
|
171
|
+
export function strToNumber(s: unknown): number | null {
|
|
172
|
+
if (s == null) return null;
|
|
173
|
+
if (typeof s === "boolean") return s ? 1 : 0;
|
|
174
|
+
if (typeof s === "number") return Number.isFinite(s) ? s : null;
|
|
175
|
+
const value = (asStr(s) ?? "").trim();
|
|
176
|
+
if (!value) return null;
|
|
177
|
+
const n = Number(value);
|
|
178
|
+
return Number.isFinite(n) ? n : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `str.pos(source, str)` → first index, or `-1` if missing.
|
|
183
|
+
* Either arg na → na.
|
|
184
|
+
*/
|
|
185
|
+
export function strPos(s: unknown, substr: unknown): number | null {
|
|
186
|
+
const hay = asStr(s);
|
|
187
|
+
const needle = asStr(substr);
|
|
188
|
+
if (hay == null || needle == null) return null;
|
|
189
|
+
return hay.indexOf(needle);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* `str.match(source, regex)` → first matching substring, or na.
|
|
194
|
+
* Either arg na / invalid regex / no match → na.
|
|
195
|
+
*/
|
|
196
|
+
export function strMatch(s: unknown, regex: unknown): string | null {
|
|
197
|
+
const source = asStr(s);
|
|
198
|
+
const pattern = asStr(regex);
|
|
199
|
+
if (source == null || pattern == null) return null;
|
|
200
|
+
try {
|
|
201
|
+
const m = source.match(new RegExp(pattern));
|
|
202
|
+
return m == null ? null : m[0]!;
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const DEFAULT_FORMAT_TIME = "yyyy-MM-dd'T'HH:mm:ssZ";
|
|
209
|
+
|
|
210
|
+
const MONTHS_LONG = [
|
|
211
|
+
"January",
|
|
212
|
+
"February",
|
|
213
|
+
"March",
|
|
214
|
+
"April",
|
|
215
|
+
"May",
|
|
216
|
+
"June",
|
|
217
|
+
"July",
|
|
218
|
+
"August",
|
|
219
|
+
"September",
|
|
220
|
+
"October",
|
|
221
|
+
"November",
|
|
222
|
+
"December",
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const MONTHS_SHORT = [
|
|
226
|
+
"Jan",
|
|
227
|
+
"Feb",
|
|
228
|
+
"Mar",
|
|
229
|
+
"Apr",
|
|
230
|
+
"May",
|
|
231
|
+
"Jun",
|
|
232
|
+
"Jul",
|
|
233
|
+
"Aug",
|
|
234
|
+
"Sep",
|
|
235
|
+
"Oct",
|
|
236
|
+
"Nov",
|
|
237
|
+
"Dec",
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function pad2(n: number): string {
|
|
241
|
+
return n < 10 ? `0${n}` : String(n);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Unix-ms → UTC civil parts. `Date.UTC` getters stay TZ-stable. */
|
|
245
|
+
function utcCivilFromMs(ms: number): {
|
|
246
|
+
year: number;
|
|
247
|
+
month: number;
|
|
248
|
+
day: number;
|
|
249
|
+
hour: number;
|
|
250
|
+
minute: number;
|
|
251
|
+
second: number;
|
|
252
|
+
} {
|
|
253
|
+
const d = new Date(ms);
|
|
254
|
+
return {
|
|
255
|
+
year: d.getUTCFullYear(),
|
|
256
|
+
month: d.getUTCMonth() + 1,
|
|
257
|
+
day: d.getUTCDate(),
|
|
258
|
+
hour: d.getUTCHours(),
|
|
259
|
+
minute: d.getUTCMinutes(),
|
|
260
|
+
second: d.getUTCSeconds(),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Python `_format_time` offset: `UTC` / `GMT` / `UTC-5` / `GMT+10`.
|
|
266
|
+
* Hour integers only (`int("+10")`). Unknown / IANA → 0 (UTC).
|
|
267
|
+
*/
|
|
268
|
+
function parseTimezoneOffsetHours(timezone: unknown): number {
|
|
269
|
+
let spec: unknown = timezone;
|
|
270
|
+
if (Array.isArray(spec)) spec = spec.length ? spec[spec.length - 1] : null;
|
|
271
|
+
if (typeof spec !== "string") return 0;
|
|
272
|
+
const z = spec.trim();
|
|
273
|
+
if (
|
|
274
|
+
!z ||
|
|
275
|
+
z === "syminfo.timezone" ||
|
|
276
|
+
z === "UTC" ||
|
|
277
|
+
z === "utc" ||
|
|
278
|
+
z === "Etc/UTC" ||
|
|
279
|
+
z === "GMT"
|
|
280
|
+
) {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
z.toUpperCase().includes("GMT") ||
|
|
285
|
+
z.startsWith("UTC+") ||
|
|
286
|
+
z.startsWith("UTC-") ||
|
|
287
|
+
z.startsWith("utc+") ||
|
|
288
|
+
z.startsWith("utc-")
|
|
289
|
+
) {
|
|
290
|
+
const offsetStr = z.toUpperCase().replaceAll("GMT", "").replaceAll("UTC", "").trim();
|
|
291
|
+
if (!offsetStr || !/^[+-]?\d+$/.test(offsetStr)) return 0;
|
|
292
|
+
const offset = Number.parseInt(offsetStr, 10);
|
|
293
|
+
return Number.isFinite(offset) ? offset : 0;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function coerceFormatTimeMs(time: unknown): number | null {
|
|
299
|
+
let raw: unknown = time;
|
|
300
|
+
if (raw != null && typeof raw === "object" && "current" in raw) {
|
|
301
|
+
raw = (raw as { current: unknown }).current;
|
|
302
|
+
}
|
|
303
|
+
if (raw == null || typeof raw === "boolean" || typeof raw === "string") return null;
|
|
304
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
305
|
+
let ts = Math.trunc(raw);
|
|
306
|
+
// Python: 0 < timestamp < 10_000_000_000 → Unix seconds, coerce to ms.
|
|
307
|
+
if (ts > 0 && ts < 10_000_000_000) ts *= 1000;
|
|
308
|
+
return ts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatOffsetHours(offsetHours: number): string {
|
|
312
|
+
const sign = offsetHours >= 0 ? "+" : "-";
|
|
313
|
+
return `${sign}${pad2(Math.abs(offsetHours))}00`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatTzName(offsetHours: number): string {
|
|
317
|
+
if (offsetHours === 0) return "UTC";
|
|
318
|
+
const sign = offsetHours >= 0 ? "+" : "-";
|
|
319
|
+
return `UTC${sign}${pad2(Math.abs(offsetHours))}:00`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `str.format_time(time[, format[, timezone]])`.
|
|
324
|
+
* na / non-numeric time → `"NaN"`. Default format is Pine ISO
|
|
325
|
+
* `yyyy-MM-dd'T'HH:mm:ssZ`; `'` markers are stripped (`2020-01-01T00:00:00Z`).
|
|
326
|
+
* `Z` is not a token (literal). `z` → `+0000`.
|
|
327
|
+
*/
|
|
328
|
+
export function strFormatTime(
|
|
329
|
+
time: unknown,
|
|
330
|
+
format?: unknown,
|
|
331
|
+
timezone?: unknown,
|
|
332
|
+
): string {
|
|
333
|
+
const ms = coerceFormatTimeMs(time);
|
|
334
|
+
if (ms == null) return "NaN";
|
|
335
|
+
const formatStr =
|
|
336
|
+
format == null ? DEFAULT_FORMAT_TIME : (asStr(format) ?? DEFAULT_FORMAT_TIME);
|
|
337
|
+
const offsetHours = parseTimezoneOffsetHours(timezone);
|
|
338
|
+
const parts = utcCivilFromMs(ms + offsetHours * 3_600_000);
|
|
339
|
+
const h12 = ((parts.hour - 1) % 12) + 1;
|
|
340
|
+
const replacements: Record<string, string> = {
|
|
341
|
+
yyyy: String(parts.year),
|
|
342
|
+
yy: String(parts.year).slice(-2),
|
|
343
|
+
MMMM: MONTHS_LONG[parts.month - 1] ?? "",
|
|
344
|
+
MMM: MONTHS_SHORT[parts.month - 1] ?? "",
|
|
345
|
+
MM: pad2(parts.month),
|
|
346
|
+
M: String(parts.month),
|
|
347
|
+
dd: pad2(parts.day),
|
|
348
|
+
d: String(parts.day),
|
|
349
|
+
HH: pad2(parts.hour),
|
|
350
|
+
H: String(parts.hour),
|
|
351
|
+
hh: pad2(h12),
|
|
352
|
+
h: String(h12),
|
|
353
|
+
mm: pad2(parts.minute),
|
|
354
|
+
m: String(parts.minute),
|
|
355
|
+
ss: pad2(parts.second),
|
|
356
|
+
s: String(parts.second),
|
|
357
|
+
a: parts.hour < 12 ? "AM" : "PM",
|
|
358
|
+
zzz: formatTzName(offsetHours),
|
|
359
|
+
z: formatOffsetHours(offsetHours),
|
|
360
|
+
};
|
|
361
|
+
let formatted = formatStr;
|
|
362
|
+
for (const key of Object.keys(replacements).sort((a, b) => b.length - a.length)) {
|
|
363
|
+
formatted = formatted.split(key).join(replacements[key]!);
|
|
364
|
+
}
|
|
365
|
+
return formatted.replaceAll("'", "");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* `str.format(fmt, ...)` — Java MessageFormat-ish `{0}` / `{1,number,#.##}`.
|
|
370
|
+
* na format → `"NaN"`; missing index → `""`; na arg → `"NaN"`.
|
|
371
|
+
*/
|
|
372
|
+
export function strFormat(fmt: unknown, ...args: unknown[]): string {
|
|
373
|
+
if (fmt == null) return "NaN";
|
|
374
|
+
const value = typeof fmt === "string" ? fmt : String(fmt);
|
|
375
|
+
return value.replace(/\{([^{}]+)\}/g, (full, body: string) => {
|
|
376
|
+
const parts = body.split(",").map((p) => p.trim());
|
|
377
|
+
const idx = Number.parseInt(parts[0] ?? "", 10);
|
|
378
|
+
if (!Number.isFinite(idx)) return full;
|
|
379
|
+
if (idx < 0 || idx >= args.length) return "";
|
|
380
|
+
const arg = args[idx];
|
|
381
|
+
if (arg == null) return "NaN";
|
|
382
|
+
const kind = (parts[1] ?? "").toLowerCase();
|
|
383
|
+
const pattern = parts[2] ?? "";
|
|
384
|
+
if (kind === "" || kind === "string") return String(arg);
|
|
385
|
+
if (kind === "number") {
|
|
386
|
+
const num = typeof arg === "number" ? arg : Number(arg);
|
|
387
|
+
if (!Number.isFinite(num)) return String(arg);
|
|
388
|
+
if (pattern) {
|
|
389
|
+
if (pattern.includes(".")) {
|
|
390
|
+
const decimals = pattern.split(".", 2)[1]!.length;
|
|
391
|
+
return num.toFixed(decimals);
|
|
392
|
+
}
|
|
393
|
+
return String(Math.trunc(num));
|
|
394
|
+
}
|
|
395
|
+
return formatG(num);
|
|
396
|
+
}
|
|
397
|
+
if (kind === "integer") {
|
|
398
|
+
const num = typeof arg === "number" ? arg : Number(arg);
|
|
399
|
+
if (!Number.isFinite(num)) return String(arg);
|
|
400
|
+
return String(Math.trunc(num));
|
|
401
|
+
}
|
|
402
|
+
return String(arg);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* `str.join(array, separator)` — stringify items.
|
|
408
|
+
* na array → na; na separator → `""`; na elements → empty.
|
|
409
|
+
*/
|
|
410
|
+
export function strJoin(parts: unknown, sep: unknown): string | null {
|
|
411
|
+
if (parts == null) return null;
|
|
412
|
+
if (!Array.isArray(parts)) return null;
|
|
413
|
+
const separator = sep == null ? "" : (asStr(sep) ?? "");
|
|
414
|
+
return parts.map((item) => (item == null ? "" : String(item))).join(separator);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function replaceAll(src: string, tgt: string, rep: string): string {
|
|
418
|
+
if (tgt === "") return src;
|
|
419
|
+
return src.split(tgt).join(rep);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** `str.replace_all(source, target, replacement)`. `na` source → `na`. */
|
|
423
|
+
export function strReplaceAll(
|
|
424
|
+
s: unknown,
|
|
425
|
+
target: unknown,
|
|
426
|
+
replacement: unknown,
|
|
427
|
+
): string | null {
|
|
428
|
+
if (s == null) return null;
|
|
429
|
+
const src = asStr(s) ?? "";
|
|
430
|
+
const tgt = asStr(target) ?? "";
|
|
431
|
+
const rep = asStr(replacement) ?? "";
|
|
432
|
+
return replaceAll(src, tgt, rep);
|
|
433
|
+
}
|