oakscriptjs 0.9.5 → 0.10.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 (256) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/array/index.d.ts +93 -50
  3. package/dist/cjs/array/index.d.ts.map +1 -1
  4. package/dist/cjs/array/index.js +239 -78
  5. package/dist/cjs/array/index.js.map +3 -3
  6. package/dist/cjs/callsite/index.d.ts +81 -2
  7. package/dist/cjs/callsite/index.d.ts.map +1 -1
  8. package/dist/cjs/callsite/index.js +62 -1
  9. package/dist/cjs/callsite/index.js.map +2 -2
  10. package/dist/cjs/color/index.d.ts +7 -3
  11. package/dist/cjs/color/index.d.ts.map +1 -1
  12. package/dist/cjs/color/index.js +6 -1
  13. package/dist/cjs/color/index.js.map +2 -2
  14. package/dist/cjs/drawing/bar-time.d.ts +24 -0
  15. package/dist/cjs/drawing/bar-time.d.ts.map +1 -0
  16. package/dist/cjs/drawing/bar-time.js +47 -0
  17. package/dist/cjs/drawing/bar-time.js.map +7 -0
  18. package/dist/cjs/drawing/output.d.ts +32 -0
  19. package/dist/cjs/drawing/output.d.ts.map +1 -0
  20. package/dist/cjs/drawing/output.js +184 -0
  21. package/dist/cjs/drawing/output.js.map +7 -0
  22. package/dist/cjs/drawing/registry.d.ts +5 -4
  23. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  24. package/dist/cjs/drawing/registry.js +11 -2
  25. package/dist/cjs/drawing/registry.js.map +2 -2
  26. package/dist/cjs/index.d.ts +6 -3
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/cjs/index.js +9 -2
  29. package/dist/cjs/index.js.map +2 -2
  30. package/dist/cjs/label/index.d.ts.map +1 -1
  31. package/dist/cjs/label/index.js +2 -1
  32. package/dist/cjs/label/index.js.map +2 -2
  33. package/dist/cjs/linefill/index.d.ts.map +1 -1
  34. package/dist/cjs/linefill/index.js +1 -0
  35. package/dist/cjs/linefill/index.js.map +2 -2
  36. package/dist/cjs/math/index.d.ts +76 -21
  37. package/dist/cjs/math/index.d.ts.map +1 -1
  38. package/dist/cjs/math/index.js +155 -41
  39. package/dist/cjs/math/index.js.map +3 -3
  40. package/dist/cjs/matrix/index.d.ts.map +1 -1
  41. package/dist/cjs/matrix/index.js +42 -36
  42. package/dist/cjs/matrix/index.js.map +2 -2
  43. package/dist/cjs/polyline/index.d.ts +3 -5
  44. package/dist/cjs/polyline/index.d.ts.map +1 -1
  45. package/dist/cjs/polyline/index.js +0 -5
  46. package/dist/cjs/polyline/index.js.map +2 -2
  47. package/dist/cjs/runtime/series.d.ts +9 -0
  48. package/dist/cjs/runtime/series.d.ts.map +1 -1
  49. package/dist/cjs/runtime/series.js +16 -2
  50. package/dist/cjs/runtime/series.js.map +2 -2
  51. package/dist/cjs/runtime/types.d.ts +22 -0
  52. package/dist/cjs/runtime/types.d.ts.map +1 -1
  53. package/dist/cjs/runtime/types.js.map +1 -1
  54. package/dist/cjs/script/index.d.ts +97 -36
  55. package/dist/cjs/script/index.d.ts.map +1 -1
  56. package/dist/cjs/script/index.js +173 -26
  57. package/dist/cjs/script/index.js.map +3 -3
  58. package/dist/cjs/session/calendar.d.ts.map +1 -1
  59. package/dist/cjs/session/calendar.js +15 -2
  60. package/dist/cjs/session/calendar.js.map +2 -2
  61. package/dist/cjs/str/index.d.ts +50 -38
  62. package/dist/cjs/str/index.d.ts.map +1 -1
  63. package/dist/cjs/str/index.js +43 -15
  64. package/dist/cjs/str/index.js.map +2 -2
  65. package/dist/cjs/str/messageformat.d.ts +2 -1
  66. package/dist/cjs/str/messageformat.d.ts.map +1 -1
  67. package/dist/cjs/str/messageformat.js +4 -1
  68. package/dist/cjs/str/messageformat.js.map +2 -2
  69. package/dist/cjs/str/numberformat.d.ts +4 -0
  70. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  71. package/dist/cjs/str/numberformat.js +26 -4
  72. package/dist/cjs/str/numberformat.js.map +3 -3
  73. package/dist/cjs/ta/index.d.ts +86 -43
  74. package/dist/cjs/ta/index.d.ts.map +1 -1
  75. package/dist/cjs/ta/index.js +173 -267
  76. package/dist/cjs/ta/index.js.map +3 -3
  77. package/dist/cjs/ta/running-sum.d.ts +54 -2
  78. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  79. package/dist/cjs/ta/running-sum.js +132 -52
  80. package/dist/cjs/ta/running-sum.js.map +2 -2
  81. package/dist/cjs/ta-series.d.ts +4 -4
  82. package/dist/cjs/ta-series.d.ts.map +1 -1
  83. package/dist/cjs/ta-series.js +4 -3
  84. package/dist/cjs/ta-series.js.map +2 -2
  85. package/dist/cjs/table/index.d.ts +87 -0
  86. package/dist/cjs/table/index.d.ts.map +1 -0
  87. package/dist/cjs/table/index.js +150 -0
  88. package/dist/cjs/table/index.js.map +7 -0
  89. package/dist/cjs/text/index.d.ts +17 -2
  90. package/dist/cjs/text/index.d.ts.map +1 -1
  91. package/dist/cjs/text/index.js +15 -1
  92. package/dist/cjs/text/index.js.map +2 -2
  93. package/dist/cjs/time/datestring.d.ts +1 -0
  94. package/dist/cjs/time/datestring.d.ts.map +1 -1
  95. package/dist/cjs/time/datestring.js +7 -0
  96. package/dist/cjs/time/datestring.js.map +2 -2
  97. package/dist/cjs/time/index.d.ts.map +1 -1
  98. package/dist/cjs/time/index.js +2 -1
  99. package/dist/cjs/time/index.js.map +2 -2
  100. package/dist/cjs/time/session.d.ts +28 -9
  101. package/dist/cjs/time/session.d.ts.map +1 -1
  102. package/dist/cjs/time/session.js +74 -28
  103. package/dist/cjs/time/session.js.map +2 -2
  104. package/dist/cjs/time/timezone.d.ts +5 -0
  105. package/dist/cjs/time/timezone.d.ts.map +1 -1
  106. package/dist/cjs/time/timezone.js +15 -1
  107. package/dist/cjs/time/timezone.js.map +2 -2
  108. package/dist/cjs/types/index.d.ts +30 -1
  109. package/dist/cjs/types/index.d.ts.map +1 -1
  110. package/dist/cjs/types/index.js +12 -0
  111. package/dist/cjs/types/index.js.map +2 -2
  112. package/dist/cjs/types/metadata.d.ts +258 -26
  113. package/dist/cjs/types/metadata.d.ts.map +1 -1
  114. package/dist/cjs/types/metadata.js.map +1 -1
  115. package/dist/esm/array/index.d.ts +93 -50
  116. package/dist/esm/array/index.d.ts.map +1 -1
  117. package/dist/esm/array/index.js +383 -133
  118. package/dist/esm/array/index.js.map +1 -1
  119. package/dist/esm/callsite/index.d.ts +81 -2
  120. package/dist/esm/callsite/index.d.ts.map +1 -1
  121. package/dist/esm/callsite/index.js +145 -3
  122. package/dist/esm/callsite/index.js.map +1 -1
  123. package/dist/esm/color/index.d.ts +7 -3
  124. package/dist/esm/color/index.d.ts.map +1 -1
  125. package/dist/esm/color/index.js +15 -3
  126. package/dist/esm/color/index.js.map +1 -1
  127. package/dist/esm/drawing/bar-time.d.ts +24 -0
  128. package/dist/esm/drawing/bar-time.d.ts.map +1 -0
  129. package/dist/esm/drawing/bar-time.js +44 -0
  130. package/dist/esm/drawing/bar-time.js.map +1 -0
  131. package/dist/esm/drawing/output.d.ts +32 -0
  132. package/dist/esm/drawing/output.d.ts.map +1 -0
  133. package/dist/esm/drawing/output.js +190 -0
  134. package/dist/esm/drawing/output.js.map +1 -0
  135. package/dist/esm/drawing/registry.d.ts +5 -4
  136. package/dist/esm/drawing/registry.d.ts.map +1 -1
  137. package/dist/esm/drawing/registry.js +12 -2
  138. package/dist/esm/drawing/registry.js.map +1 -1
  139. package/dist/esm/index.d.ts +6 -3
  140. package/dist/esm/index.d.ts.map +1 -1
  141. package/dist/esm/index.js +7 -3
  142. package/dist/esm/index.js.map +1 -1
  143. package/dist/esm/label/index.d.ts.map +1 -1
  144. package/dist/esm/label/index.js +2 -1
  145. package/dist/esm/label/index.js.map +1 -1
  146. package/dist/esm/linefill/index.d.ts.map +1 -1
  147. package/dist/esm/linefill/index.js +3 -0
  148. package/dist/esm/linefill/index.js.map +1 -1
  149. package/dist/esm/math/index.d.ts +76 -21
  150. package/dist/esm/math/index.d.ts.map +1 -1
  151. package/dist/esm/math/index.js +227 -61
  152. package/dist/esm/math/index.js.map +1 -1
  153. package/dist/esm/matrix/index.d.ts.map +1 -1
  154. package/dist/esm/matrix/index.js +57 -43
  155. package/dist/esm/matrix/index.js.map +1 -1
  156. package/dist/esm/polyline/index.d.ts +3 -5
  157. package/dist/esm/polyline/index.d.ts.map +1 -1
  158. package/dist/esm/polyline/index.js +3 -9
  159. package/dist/esm/polyline/index.js.map +1 -1
  160. package/dist/esm/runtime/series.d.ts +9 -0
  161. package/dist/esm/runtime/series.d.ts.map +1 -1
  162. package/dist/esm/runtime/series.js +18 -1
  163. package/dist/esm/runtime/series.js.map +1 -1
  164. package/dist/esm/runtime/types.d.ts +22 -0
  165. package/dist/esm/runtime/types.d.ts.map +1 -1
  166. package/dist/esm/script/index.d.ts +97 -36
  167. package/dist/esm/script/index.d.ts.map +1 -1
  168. package/dist/esm/script/index.js +211 -22
  169. package/dist/esm/script/index.js.map +1 -1
  170. package/dist/esm/session/calendar.d.ts.map +1 -1
  171. package/dist/esm/session/calendar.js +22 -2
  172. package/dist/esm/session/calendar.js.map +1 -1
  173. package/dist/esm/str/index.d.ts +50 -38
  174. package/dist/esm/str/index.d.ts.map +1 -1
  175. package/dist/esm/str/index.js +103 -52
  176. package/dist/esm/str/index.js.map +1 -1
  177. package/dist/esm/str/messageformat.d.ts +2 -1
  178. package/dist/esm/str/messageformat.d.ts.map +1 -1
  179. package/dist/esm/str/messageformat.js +7 -2
  180. package/dist/esm/str/messageformat.js.map +1 -1
  181. package/dist/esm/str/numberformat.d.ts +4 -0
  182. package/dist/esm/str/numberformat.d.ts.map +1 -1
  183. package/dist/esm/str/numberformat.js +38 -4
  184. package/dist/esm/str/numberformat.js.map +1 -1
  185. package/dist/esm/ta/index.d.ts +86 -43
  186. package/dist/esm/ta/index.d.ts.map +1 -1
  187. package/dist/esm/ta/index.js +296 -348
  188. package/dist/esm/ta/index.js.map +1 -1
  189. package/dist/esm/ta/running-sum.d.ts +54 -2
  190. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  191. package/dist/esm/ta/running-sum.js +166 -71
  192. package/dist/esm/ta/running-sum.js.map +1 -1
  193. package/dist/esm/ta-series.d.ts +4 -4
  194. package/dist/esm/ta-series.d.ts.map +1 -1
  195. package/dist/esm/ta-series.js +5 -4
  196. package/dist/esm/ta-series.js.map +1 -1
  197. package/dist/esm/table/index.d.ts +87 -0
  198. package/dist/esm/table/index.d.ts.map +1 -0
  199. package/dist/esm/table/index.js +169 -0
  200. package/dist/esm/table/index.js.map +1 -0
  201. package/dist/esm/text/index.d.ts +17 -2
  202. package/dist/esm/text/index.d.ts.map +1 -1
  203. package/dist/esm/text/index.js +17 -2
  204. package/dist/esm/text/index.js.map +1 -1
  205. package/dist/esm/time/datestring.d.ts +1 -0
  206. package/dist/esm/time/datestring.d.ts.map +1 -1
  207. package/dist/esm/time/datestring.js +12 -0
  208. package/dist/esm/time/datestring.js.map +1 -1
  209. package/dist/esm/time/index.d.ts.map +1 -1
  210. package/dist/esm/time/index.js +3 -1
  211. package/dist/esm/time/index.js.map +1 -1
  212. package/dist/esm/time/session.d.ts +28 -9
  213. package/dist/esm/time/session.d.ts.map +1 -1
  214. package/dist/esm/time/session.js +113 -42
  215. package/dist/esm/time/session.js.map +1 -1
  216. package/dist/esm/time/timezone.d.ts +5 -0
  217. package/dist/esm/time/timezone.d.ts.map +1 -1
  218. package/dist/esm/time/timezone.js +21 -1
  219. package/dist/esm/time/timezone.js.map +1 -1
  220. package/dist/esm/types/index.d.ts +30 -1
  221. package/dist/esm/types/index.d.ts.map +1 -1
  222. package/dist/esm/types/index.js +14 -0
  223. package/dist/esm/types/index.js.map +1 -1
  224. package/dist/esm/types/metadata.d.ts +258 -26
  225. package/dist/esm/types/metadata.d.ts.map +1 -1
  226. package/package.json +1 -1
  227. package/src/array/index.ts +364 -150
  228. package/src/callsite/index.ts +147 -3
  229. package/src/color/index.ts +15 -5
  230. package/src/drawing/bar-time.ts +42 -0
  231. package/src/drawing/output.ts +214 -0
  232. package/src/drawing/registry.ts +16 -6
  233. package/src/index.ts +9 -3
  234. package/src/label/index.ts +2 -1
  235. package/src/linefill/index.ts +2 -0
  236. package/src/math/index.ts +256 -74
  237. package/src/matrix/index.ts +52 -52
  238. package/src/polyline/index.ts +3 -12
  239. package/src/runtime/series.ts +22 -1
  240. package/src/runtime/types.ts +23 -0
  241. package/src/script/index.ts +307 -25
  242. package/src/session/calendar.ts +22 -2
  243. package/src/str/index.ts +95 -51
  244. package/src/str/messageformat.ts +7 -2
  245. package/src/str/numberformat.ts +39 -4
  246. package/src/ta/index.ts +306 -386
  247. package/src/ta/running-sum.ts +187 -74
  248. package/src/ta-series.ts +8 -7
  249. package/src/table/index.ts +221 -0
  250. package/src/text/index.ts +18 -2
  251. package/src/time/datestring.ts +10 -0
  252. package/src/time/index.ts +3 -1
  253. package/src/time/session.ts +97 -47
  254. package/src/time/timezone.ts +21 -1
  255. package/src/types/index.ts +32 -1
  256. package/src/types/metadata.ts +325 -26
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.price` / `format.inherit`
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` needs the symbol's tick size and throws here
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 (!Number.isFinite(value)) return String(value); // NaN (na), Infinity
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
- throw new Error('str.tostring(x, format.mintick) needs the symbol tick size (syminfo.mintick), which is not known here');
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
- * - Returns null for invalid number strings
94
- * - Supports scientific notation (e.g., "1e3")
95
- * - Supports decimal numbers (e.g., "0.5", ".5")
96
- * - Whitespace is ignored
97
- * - "NaN" string returns null
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("1e3") // Returns: 1000
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
- const num = parseFloat(str);
109
- return isNaN(num) ? null : num;
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 -1 if not found
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 -1 if substring is not found
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: -1
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
- return source.indexOf(str);
243
+ const index = source.indexOf(str);
244
+ return index === -1 ? NaN : index;
213
245
  }
214
246
 
215
247
  /**
216
- * Replaces occurrences of a substring with another string.
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 - If 0, replaces first occurrence only; otherwise replaces all occurrences (default: all)
222
- * @returns The string with replacements made
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
- * - Search is case-sensitive
226
- * - If target is not found, returns source unchanged
227
- * - occurrence parameter: 0 = first only, any other value = all occurrences
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", 0) // Returns: "hi world hello"
232
- * str.replace("hello world hello", "hello", "hi") // Returns: "hi world hi"
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?: simple_int): string {
237
- if (occurrence === 0) {
238
- return source.replace(target, replacement);
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
- return source.replaceAll(target, replacement);
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
- return str.split(separator);
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; a missing argument leaves `{n}` in the text.
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
- * Tests if a string matches a regular expression pattern.
507
+ * Returns the first substring of `source` that matches the `regex` (PineScript `str.match`).
463
508
  *
464
- * @param source - The string to test
465
- * @param regex - The regular expression pattern
466
- * @returns True if the pattern matches, false otherwise
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("hello123", "\\d+") // Returns: true
476
- * str.match("test@example.com", "^[a-z]+@[a-z]+\\.[a-z]+$") // Returns: true
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): bool {
481
- return new RegExp(regex).test(source);
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 (must be >= 0)
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 null/undefined
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 <= 0) return '';
582
+ if (!(count > 0)) return null; // 0, negative or na: na
539
583
  return Array(count).fill(source).join(separator);
540
584
  }
541
585
 
@@ -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) return typeof value === 'number' ? number(value, '#,##0.###') : String(value);
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();
@@ -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 ? '-' : '') + p.prefix + '∞' + p.suffix;
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
- const rounded = roundDigits(toDigits(x), p.hasDigits ? p.maxFrac : 0, mode);
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
- const negative = x < 0 && (!isZero || !p.hasDigits);
196
- return (negative ? '-' : '') + p.prefix + body + p.suffix;
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
  }