@abaplint/runtime 2.13.93 → 2.13.95

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.
@@ -0,0 +1,9 @@
1
+ import { DecFloat34, Float, Integer, Integer8, Packed } from "../types";
2
+ import { INumeric } from "../types/_numeric";
3
+ export type ExtremumArgument = number | INumeric | undefined;
4
+ /** nmax( ) and nmin( ): the result has the calculation type of the arguments,
5
+ * as an arithmetic expression would determine it - decfloat34 before f before
6
+ * p before int8 before i. So nmin( val1 = i1 val2 = i2 ) is an i, which DO
7
+ * nmin( ... ) TIMES and every other integer position rely on, and a p result
8
+ * keeps the most decimals of its arguments. */
9
+ export declare function extremum(args: ExtremumArgument[], isBetter: (candidate: number, best: number) => boolean): Float | Integer8 | Integer | DecFloat34 | Packed;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extremum = extremum;
4
+ const _parse_1 = require("../operators/_parse");
5
+ const types_1 = require("../types");
6
+ // the calculation types in the order an arithmetic expression ranks them
7
+ const INT = 0;
8
+ const INT8 = 1;
9
+ const PACKED = 2;
10
+ const FLOAT = 3;
11
+ const DECFLOAT = 4;
12
+ /** nmax( ) and nmin( ): the result has the calculation type of the arguments,
13
+ * as an arithmetic expression would determine it - decfloat34 before f before
14
+ * p before int8 before i. So nmin( val1 = i1 val2 = i2 ) is an i, which DO
15
+ * nmin( ... ) TIMES and every other integer position rely on, and a p result
16
+ * keeps the most decimals of its arguments. */
17
+ function extremum(args, isBetter) {
18
+ let best = undefined;
19
+ let bestValue = 0;
20
+ let type = INT;
21
+ let decimals = 0;
22
+ for (let arg of args) {
23
+ if (arg === undefined) {
24
+ continue;
25
+ }
26
+ if (arg instanceof types_1.FieldSymbol) {
27
+ arg = arg.getPointer();
28
+ }
29
+ if (arg instanceof types_1.DecFloat34) {
30
+ type = Math.max(type, DECFLOAT);
31
+ }
32
+ else if (arg instanceof types_1.Float) {
33
+ type = Math.max(type, FLOAT);
34
+ }
35
+ else if (arg instanceof types_1.Packed) {
36
+ type = Math.max(type, PACKED);
37
+ decimals = Math.max(decimals, arg.getDecimals());
38
+ }
39
+ else if (arg instanceof types_1.Integer8) {
40
+ type = Math.max(type, INT8);
41
+ }
42
+ else if (arg instanceof types_1.Integer || (typeof arg === "number" && Number.isInteger(arg))) {
43
+ // i, the lowest type
44
+ }
45
+ else {
46
+ // character-like and other arguments, as before: f
47
+ type = Math.max(type, FLOAT);
48
+ }
49
+ const value = (0, _parse_1.parse)(arg);
50
+ if (best === undefined || isBetter(value, bestValue)) {
51
+ best = arg;
52
+ bestValue = value;
53
+ }
54
+ }
55
+ switch (type) {
56
+ case DECFLOAT:
57
+ return new types_1.DecFloat34().set(bestValue);
58
+ case FLOAT:
59
+ return new types_1.Float().set(bestValue);
60
+ case PACKED:
61
+ if (best instanceof types_1.Packed) {
62
+ // through the digits, not through a binary float
63
+ return new types_1.Packed({ length: 16, decimals }).set(best.toFixed(best.getDecimals()));
64
+ }
65
+ return new types_1.Packed({ length: 16, decimals }).set(bestValue);
66
+ case INT8:
67
+ if (best instanceof types_1.Integer8) {
68
+ return new types_1.Integer8().set(best.get());
69
+ }
70
+ return new types_1.Integer8().set(bestValue);
71
+ default:
72
+ return new types_1.Integer().set(bestValue);
73
+ }
74
+ }
75
+ //# sourceMappingURL=_extremum.js.map
@@ -32,7 +32,7 @@ function find(input) {
32
32
  }
33
33
  }
34
34
  else {
35
- const sub = typeof input.sub === "string" ? input.sub : input.sub?.get();
35
+ let sub = typeof input.sub === "string" ? input.sub : input.sub?.get() || "";
36
36
  let off = (0, _position_1.position)(input.off) || 0;
37
37
  let occ = (0, _position_1.position)(input.occ);
38
38
  if (occ === 0) {
@@ -41,6 +41,26 @@ function find(input) {
41
41
  else if (occ === undefined) {
42
42
  occ = 1;
43
43
  }
44
+ const caseInput = typeof input.case === "string" ? input.case : input.case?.get();
45
+ if (caseInput !== undefined && caseInput !== "X") {
46
+ val = foldCase(val);
47
+ sub = foldCase(sub);
48
+ }
49
+ if (occ < 0 && sub !== "" && off === 0) {
50
+ // search from the right, on the string as it is, so a "sub" of any length
51
+ // matches and the offset needs no mapping back
52
+ let from = val.length - sub.length;
53
+ let found = -1;
54
+ for (let i = 0; i < Math.abs(occ); i++) {
55
+ found = from < 0 ? -1 : val.lastIndexOf(sub, from);
56
+ if (found < 0) {
57
+ return new types_1.Integer().set(-1);
58
+ }
59
+ from = found - 1;
60
+ }
61
+ return new types_1.Integer().set(found);
62
+ }
63
+ // a negative "occ" with an empty "sub" or an "off" still comes here, unchanged
44
64
  let negative = false;
45
65
  if (occ < 0) {
46
66
  negative = true;
@@ -54,7 +74,7 @@ function find(input) {
54
74
  }
55
75
  let found = -1;
56
76
  for (let i = 0; i < occ; i++) {
57
- found = val.indexOf(sub || "", off);
77
+ found = val.indexOf(sub, off);
58
78
  if (found >= 0) {
59
79
  off = found + 1;
60
80
  }
@@ -65,4 +85,15 @@ function find(input) {
65
85
  return new types_1.Integer().set(found);
66
86
  }
67
87
  }
88
+ // lower case one character at a time and keep a character whose lower case form
89
+ // has another length, so every offset stays valid - a plain toLowerCase( )
90
+ // turns e.g. "\u0130" into two code units
91
+ function foldCase(str) {
92
+ let ret = "";
93
+ for (const character of str) {
94
+ const lower = character.toLowerCase();
95
+ ret += lower.length === character.length ? lower : character;
96
+ }
97
+ return ret;
98
+ }
68
99
  //# sourceMappingURL=find.js.map
@@ -1,5 +1,4 @@
1
1
  import { INumeric } from "../types/_numeric";
2
- import { Float } from "../types";
3
2
  export interface INmaxInput {
4
3
  val1: number | INumeric;
5
4
  val2: number | INumeric;
@@ -11,4 +10,4 @@ export interface INmaxInput {
11
10
  val8?: number | INumeric;
12
11
  val9?: number | INumeric;
13
12
  }
14
- export declare function nmax(input: INmaxInput): Float;
13
+ export declare function nmax(input: INmaxInput): import("../types").Float | import("../types").Integer8 | import("../types").Integer | import("../types").DecFloat34 | import("../types").Packed;
@@ -1,34 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nmax = nmax;
4
- const _parse_1 = require("../operators/_parse");
5
- const types_1 = require("../types");
4
+ const _extremum_1 = require("./_extremum");
6
5
  function nmax(input) {
7
- const values = [];
8
- values.push((0, _parse_1.parse)(input.val1));
9
- values.push((0, _parse_1.parse)(input.val2));
10
- if (input.val3) {
11
- values.push((0, _parse_1.parse)(input.val3));
12
- }
13
- if (input.val4) {
14
- values.push((0, _parse_1.parse)(input.val4));
15
- }
16
- if (input.val5) {
17
- values.push((0, _parse_1.parse)(input.val5));
18
- }
19
- if (input.val6) {
20
- values.push((0, _parse_1.parse)(input.val6));
21
- }
22
- if (input.val7) {
23
- values.push((0, _parse_1.parse)(input.val7));
24
- }
25
- if (input.val8) {
26
- values.push((0, _parse_1.parse)(input.val8));
27
- }
28
- if (input.val9) {
29
- values.push((0, _parse_1.parse)(input.val9));
30
- }
31
- values.sort((a, b) => (b - a));
32
- return new types_1.Float().set(values[0]);
6
+ return (0, _extremum_1.extremum)([input.val1, input.val2, input.val3, input.val4, input.val5,
7
+ input.val6, input.val7, input.val8, input.val9], (candidate, best) => candidate > best);
33
8
  }
34
9
  //# sourceMappingURL=nmax.js.map
@@ -1,5 +1,4 @@
1
1
  import { INumeric } from "../types/_numeric";
2
- import { Float } from "../types";
3
2
  export interface INminInput {
4
3
  val1: number | INumeric;
5
4
  val2: number | INumeric;
@@ -11,4 +10,4 @@ export interface INminInput {
11
10
  val8?: number | INumeric;
12
11
  val9?: number | INumeric;
13
12
  }
14
- export declare function nmin(input: INminInput): Float;
13
+ export declare function nmin(input: INminInput): import("../types").Float | import("../types").Integer8 | import("../types").Integer | import("../types").DecFloat34 | import("../types").Packed;
@@ -1,34 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nmin = nmin;
4
- const _parse_1 = require("../operators/_parse");
5
- const types_1 = require("../types");
4
+ const _extremum_1 = require("./_extremum");
6
5
  function nmin(input) {
7
- const values = [];
8
- values.push((0, _parse_1.parse)(input.val1));
9
- values.push((0, _parse_1.parse)(input.val2));
10
- if (input.val3) {
11
- values.push((0, _parse_1.parse)(input.val3));
12
- }
13
- if (input.val4) {
14
- values.push((0, _parse_1.parse)(input.val4));
15
- }
16
- if (input.val5) {
17
- values.push((0, _parse_1.parse)(input.val5));
18
- }
19
- if (input.val6) {
20
- values.push((0, _parse_1.parse)(input.val6));
21
- }
22
- if (input.val7) {
23
- values.push((0, _parse_1.parse)(input.val7));
24
- }
25
- if (input.val8) {
26
- values.push((0, _parse_1.parse)(input.val8));
27
- }
28
- if (input.val9) {
29
- values.push((0, _parse_1.parse)(input.val9));
30
- }
31
- values.sort((a, b) => (a - b));
32
- return new types_1.Float().set(values[0]);
6
+ return (0, _extremum_1.extremum)([input.val1, input.val2, input.val3, input.val4, input.val5,
7
+ input.val6, input.val7, input.val8, input.val9], (candidate, best) => candidate < best);
33
8
  }
34
9
  //# sourceMappingURL=nmin.js.map
@@ -1,8 +1,17 @@
1
- import { Float } from "../types";
1
+ import { DecFloat34 } from "../types";
2
2
  import { ICharacter } from "../types/_character";
3
3
  import { INumeric } from "../types/_numeric";
4
+ type Input = INumeric | ICharacter | string | number;
5
+ /** round( val = arg {dec = n}|{prec = n} [mode = m] )
6
+ *
7
+ * The argument is converted to decfloat34 and rounded as a decimal number, so
8
+ * round( val = '2.675' dec = 2 ) is 2.68 - not 2.67, which is what scaling the
9
+ * nearest binary float would give. A binary float argument brings its full
10
+ * binary expansion along, the way converting f to decfloat34 does. */
4
11
  export declare function round(input: {
5
- val: ICharacter | string;
6
- dec: INumeric | number;
7
- mode?: INumeric | number;
8
- }): Float;
12
+ val: Input;
13
+ dec?: Input;
14
+ prec?: Input;
15
+ mode?: Input;
16
+ }): DecFloat34;
17
+ export {};
@@ -3,39 +3,87 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.round = round;
4
4
  const _parse_1 = require("../operators/_parse");
5
5
  const types_1 = require("../types");
6
+ // the rounding modes of CL_ABAP_MATH, the decNumber enumeration
7
+ const ROUND_CEILING = 0;
8
+ const ROUND_UP = 1;
9
+ const ROUND_HALF_UP = 2;
10
+ const ROUND_HALF_EVEN = 3;
11
+ const ROUND_HALF_DOWN = 4;
12
+ const ROUND_DOWN = 5;
13
+ const ROUND_FLOOR = 6;
14
+ /** round( val = arg {dec = n}|{prec = n} [mode = m] )
15
+ *
16
+ * The argument is converted to decfloat34 and rounded as a decimal number, so
17
+ * round( val = '2.675' dec = 2 ) is 2.68 - not 2.67, which is what scaling the
18
+ * nearest binary float would give. A binary float argument brings its full
19
+ * binary expansion along, the way converting f to decfloat34 does. */
6
20
  function round(input) {
7
- let mode = input.mode;
8
- if (mode === undefined) {
9
- mode = 2;
21
+ const mode = input.mode === undefined ? ROUND_HALF_UP : (0, _parse_1.parse)(input.mode);
22
+ let arg = input.val;
23
+ if (arg instanceof types_1.FieldSymbol) {
24
+ arg = arg.getPointer();
10
25
  }
11
- else if (typeof mode !== "number") {
12
- mode = mode?.get();
26
+ const val = (0, _parse_1.parse)(arg);
27
+ const text = arg instanceof types_1.Float ? val.toPrecision(34) : String(val);
28
+ const match = /^(-?)(\d*)\.?(\d*)(?:e([+-]?\d+))?$/i.exec(text);
29
+ if (match === null || !Number.isFinite(val)) {
30
+ return new types_1.DecFloat34().set(val);
13
31
  }
14
- const val = (0, _parse_1.parse)(input.val);
15
- const dec = (0, _parse_1.parse)(input.dec);
16
- if (dec !== 0) {
17
- throw new Error("round(), todo, handle decimals");
32
+ const negative = match[1] === "-";
33
+ const digits = BigInt((match[2] + match[3]).replace(/^0+/, "") || "0");
34
+ // the value is digits * 10^exponent
35
+ const exponent = parseInt(match[4] || "0", 10) - match[3].length;
36
+ let dec;
37
+ if (input.prec !== undefined) {
38
+ const prec = (0, _parse_1.parse)(input.prec);
39
+ if (prec <= 0) {
40
+ throw new Error("CX_SY_ARITHMETIC_ERROR, round( ) prec must be positive");
41
+ }
42
+ // position of the most significant digit
43
+ const top = digits.toString().length - 1 + exponent;
44
+ dec = prec - 1 - top;
18
45
  }
19
- const ret = new types_1.Float();
46
+ else {
47
+ dec = input.dec === undefined ? 0 : (0, _parse_1.parse)(input.dec);
48
+ }
49
+ const drop = -exponent - dec;
50
+ if (drop <= 0 || digits === 0n) {
51
+ return new types_1.DecFloat34().set(val);
52
+ }
53
+ const divisor = 10n ** BigInt(drop);
54
+ let kept = digits / divisor;
55
+ const rest = digits % divisor;
56
+ // compare the dropped part with one half of the last kept digit
57
+ const half = rest * 2n === divisor ? 0 : (rest * 2n > divisor ? 1 : -1);
58
+ let away;
20
59
  switch (mode) {
21
- case 1:
22
- ret.set(Math.ceil(val));
60
+ case ROUND_CEILING:
61
+ away = rest > 0n && negative === false;
62
+ break;
63
+ case ROUND_UP:
64
+ away = rest > 0n;
23
65
  break;
24
- case 2:
25
- ret.set(Math.round(val));
66
+ case ROUND_HALF_UP:
67
+ away = half >= 0;
26
68
  break;
27
- case 4:
28
- ret.set(-Math.round(-val));
69
+ case ROUND_HALF_EVEN:
70
+ away = half > 0 || (half === 0 && kept % 2n === 1n);
29
71
  break;
30
- case 5:
31
- ret.set(Math.trunc(val));
72
+ case ROUND_HALF_DOWN:
73
+ away = half > 0;
32
74
  break;
33
- case 6:
34
- ret.set(Math.floor(val));
75
+ case ROUND_DOWN:
76
+ away = false;
77
+ break;
78
+ case ROUND_FLOOR:
79
+ away = rest > 0n && negative === true;
35
80
  break;
36
81
  default:
37
82
  throw new Error("round(), unknown mode: " + mode);
38
83
  }
39
- return ret;
84
+ if (away) {
85
+ kept += 1n;
86
+ }
87
+ return new types_1.DecFloat34().set(Number((negative ? "-" : "") + kept.toString() + "e" + (-dec)));
40
88
  }
41
89
  //# sourceMappingURL=round.js.map
@@ -5,6 +5,41 @@ const types_1 = require("../types");
5
5
  function escapeRegExpCharacter(input) {
6
6
  return input.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
7
7
  }
8
+ const CACHE_SIZE = 1000;
9
+ const cache = new Map();
10
+ /** `end` is the pattern's length after the last token that is not a `*`: what
11
+ * follows is a run of trailing `[\s\S]*`, which matches any rest of the
12
+ * string - so it goes, and so does the `$` that made the engine walk there */
13
+ function compile(r) {
14
+ let pattern = "";
15
+ let end = 0;
16
+ for (let i = 0; i < r.length; i++) {
17
+ const current = r[i];
18
+ if (current === "*") {
19
+ pattern += "[\\s\\S]*";
20
+ continue;
21
+ }
22
+ if (current === "#") {
23
+ if (i + 1 < r.length) {
24
+ const next = r[i + 1];
25
+ pattern += next === "#" ? "#" : escapeRegExpCharacter(next);
26
+ i++;
27
+ }
28
+ else {
29
+ pattern += "#";
30
+ }
31
+ }
32
+ else if (current === "+") {
33
+ pattern += "[\\s\\S]";
34
+ }
35
+ else {
36
+ pattern += escapeRegExpCharacter(current);
37
+ }
38
+ end = pattern.length;
39
+ }
40
+ const open = end < pattern.length;
41
+ return new RegExp("^" + pattern.slice(0, end) + (open ? "" : "$"), "iu");
42
+ }
8
43
  function cp(left, right) {
9
44
  let l = "";
10
45
  if (typeof left === "number" || typeof left === "string") {
@@ -35,36 +70,14 @@ function cp(left, right) {
35
70
  else {
36
71
  r = right.get().toString().trimEnd();
37
72
  }
38
- let pattern = "";
39
- for (let i = 0; i < r.length; i++) {
40
- const current = r[i];
41
- if (current === "#") {
42
- if (i + 1 < r.length) {
43
- const next = r[i + 1];
44
- if (next === "#") {
45
- pattern += "#";
46
- i++;
47
- }
48
- else {
49
- pattern += escapeRegExpCharacter(next);
50
- i++;
51
- }
52
- }
53
- else {
54
- pattern += "#";
55
- }
56
- }
57
- else if (current === "*") {
58
- pattern += "[\\s\\S]*";
59
- }
60
- else if (current === "+") {
61
- pattern += "[\\s\\S]";
62
- }
63
- else {
64
- pattern += escapeRegExpCharacter(current);
73
+ let reg = cache.get(r);
74
+ if (reg === undefined) {
75
+ reg = compile(r);
76
+ if (cache.size >= CACHE_SIZE) {
77
+ cache.delete(cache.keys().next().value);
65
78
  }
79
+ cache.set(r, reg);
66
80
  }
67
- const reg = new RegExp("^" + pattern + "$", "iu");
68
- return l.match(reg) !== null;
81
+ return reg.test(l);
69
82
  }
70
83
  //# sourceMappingURL=cp.js.map
@@ -347,6 +347,16 @@ function eq(left, right) {
347
347
  else if (right instanceof types_1.Float && typeof l === "number") {
348
348
  r = right.getRaw();
349
349
  }
350
+ else if ((right instanceof types_1.Float || right instanceof types_1.DecFloat34) && typeof l === "string") {
351
+ // a character-like operand is compared as a number: get( ) of a float
352
+ // writes 1,4800000000000000E+02, which no text compares equal with
353
+ r = right.getRaw();
354
+ l = Number(l);
355
+ }
356
+ else if (left instanceof types_1.DecFloat34 && typeof r === "string") {
357
+ l = left.getRaw();
358
+ r = Number(r);
359
+ }
350
360
  else if (left instanceof types_1.Float) {
351
361
  if (typeof r === "number") {
352
362
  l = left.getRaw();
@@ -1,4 +1,6 @@
1
1
  import { Table } from "../types";
2
2
  import { ICharacter } from "../types/_character";
3
3
  import { INumeric } from "../types/_numeric";
4
- export declare function compareIn(left: number | string | ICharacter | INumeric, right: Table): boolean;
4
+ type Operand = number | string | ICharacter | INumeric;
5
+ export declare function compareIn(left: Operand, right: Table): boolean;
6
+ export {};
@@ -3,32 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compareIn = compareIn;
4
4
  const cp_1 = require("./cp");
5
5
  const eq_1 = require("./eq");
6
+ const ge_1 = require("./ge");
7
+ const gt_1 = require("./gt");
8
+ const le_1 = require("./le");
9
+ const lt_1 = require("./lt");
6
10
  const ne_1 = require("./ne");
7
- function compareIn(left, right) {
8
- if (right.array().length === 0) {
9
- return true;
11
+ function matches(left, option, low, high) {
12
+ switch (option) {
13
+ case "EQ": return (0, eq_1.eq)(left, low);
14
+ case "NE": return (0, ne_1.ne)(left, low);
15
+ case "GT": return (0, gt_1.gt)(left, low);
16
+ case "GE": return (0, ge_1.ge)(left, low);
17
+ case "LT": return (0, lt_1.lt)(left, low);
18
+ case "LE": return (0, le_1.le)(left, low);
19
+ case "BT": return (0, ge_1.ge)(left, low) && (0, le_1.le)(left, high);
20
+ case "NB": return !((0, ge_1.ge)(left, low) && (0, le_1.le)(left, high));
21
+ case "CP": return (0, cp_1.cp)(left, low);
22
+ case "NP": return !(0, cp_1.cp)(left, low);
23
+ default: throw new Error("compareIn, unknown option " + option);
10
24
  }
25
+ }
26
+ // A value is in a range table when some I row admits it, or there is no I
27
+ // row at all, and no E row admits it. An empty table admits everything.
28
+ function compareIn(left, right) {
29
+ let included = false;
30
+ let hasInclude = false;
11
31
  for (const row of right.array()) {
12
- if ((0, eq_1.eq)(row.get()["sign"], "I") && (0, eq_1.eq)(row.get()["option"], "EQ")) {
13
- if ((0, eq_1.eq)(row.get()["low"], left)) {
14
- return true;
15
- }
16
- }
17
- else if ((0, eq_1.eq)(row.get()["sign"], "E") && (0, eq_1.eq)(row.get()["option"], "EQ")) {
18
- if ((0, ne_1.ne)(row.get()["low"], left)) {
19
- return true;
20
- }
32
+ const r = row.get();
33
+ const sign = r["sign"].get().toString().toUpperCase().trim();
34
+ const option = r["option"].get().toString().toUpperCase().trim();
35
+ const hit = matches(left, option, r["low"], r["high"]);
36
+ if (sign === "I") {
37
+ hasInclude = true;
38
+ included = included || hit;
21
39
  }
22
- else if ((0, eq_1.eq)(row.get()["sign"], "I") && (0, eq_1.eq)(row.get()["option"], "CP")) {
23
- if ((0, cp_1.cp)(left, row.get()["low"])) {
24
- return true;
40
+ else if (sign === "E") {
41
+ if (hit) {
42
+ return false;
25
43
  }
26
44
  }
27
45
  else {
28
- console.dir(row);
29
- throw new Error("compareIn todo");
46
+ throw new Error("compareIn, unknown sign " + sign);
30
47
  }
31
48
  }
32
- return false;
49
+ return hasInclude === false || included;
33
50
  }
34
51
  //# sourceMappingURL=in.js.map
@@ -38,8 +38,10 @@ function concatenate(input) {
38
38
  if (hasSeparator === true && i > 0) {
39
39
  result += sep;
40
40
  }
41
- const value = array[i].get();
42
- result += respectingBlanks === true ? value : value.trimEnd();
41
+ const row = array[i];
42
+ const value = row.get();
43
+ // Preserve trailing blanks in string rows even without RESPECTING BLANKS.
44
+ result += respectingBlanks === true || row instanceof types_1.String ? value : value.trimEnd();
43
45
  }
44
46
  }
45
47
  setTarget(input.target, result);
@@ -21,6 +21,18 @@ function createDataRefTo(target, name) {
21
21
  }
22
22
  target.assign(new abap.types.ABAPObject({ qualifiedName: name, RTTIName: name }));
23
23
  }
24
+ /** CREATE DATA ... LIKE dobj takes the type of dobj, never its content: the
25
+ * created data object starts initial. A table is created empty right away
26
+ * instead of copying its rows first. */
27
+ function initialLike(like) {
28
+ if (like instanceof types_1.Table || like instanceof types_1.HashedTable) {
29
+ const TableType = like.constructor;
30
+ return new TableType(like.getRowType(), like.getOptions(), like.getQualifiedName());
31
+ }
32
+ const copy = like.clone();
33
+ copy.clear();
34
+ return copy;
35
+ }
24
36
  function createData(target, options) {
25
37
  // console.dir(options);
26
38
  if (target instanceof types_1.FieldSymbol) {
@@ -208,10 +220,10 @@ function createData(target, options) {
208
220
  options.like = options.like.getPointer();
209
221
  }
210
222
  if (options.table === true) {
211
- target.assign(new abap.types.Table(options.like.clone(), tableOptions));
223
+ target.assign(new abap.types.Table(initialLike(options.like), tableOptions));
212
224
  }
213
225
  else {
214
- target.assign(options.like.clone());
226
+ target.assign(initialLike(options.like));
215
227
  }
216
228
  }
217
229
  else {
@@ -8,13 +8,17 @@ function determineFromTo(array, topEquals, key) {
8
8
  // if there is no WHERE supplied, its using the sorting of the secondary key
9
9
  return { from: 1, to: array.length };
10
10
  }
11
- let from = 0;
11
+ // 1-based, as the branch without topEquals above answers: when the key's
12
+ // first field is not in topEquals (a key over a substructure component,
13
+ // s-x, is never put there) nothing is narrowed, and the loop must not start
14
+ // at row index -1
15
+ let from = 1;
12
16
  let to = array.length;
13
17
  // todo: multi field
14
18
  const keyField = key.keyFields[0].toLowerCase();
15
19
  const keyValue = topEquals[keyField];
16
20
  if (keyField && keyValue) {
17
- from = (0, binary_search_1.binarySearchFrom)(array, from, to, keyField, keyValue);
21
+ from = (0, binary_search_1.binarySearchFrom)(array, 0, to, keyField, keyValue);
18
22
  to = (0, binary_search_1.binarySearchTo)(array, from, to, keyField, keyValue);
19
23
  // console.dir("from: " + from + ", to: " + to);
20
24
  }
@@ -99,7 +103,9 @@ async function* loop(table, options) {
99
103
  let loopFrom = options?.from && options?.from.get() > 0 ? options.from.get() - 1 : 0;
100
104
  let loopTo = options?.to && options.to.get() < length ? options.to.get() : length;
101
105
  let array = [];
102
- if (options?.usingKey && options.usingKey !== undefined && options.usingKey !== "primary_key") {
106
+ // a dynamic key name may come in any case, USING KEY ('PRIMARY_KEY') is the primary key too
107
+ const isPrimaryKey = options?.usingKey === undefined || options.usingKey.toLowerCase() === "primary_key";
108
+ if (options?.usingKey && isPrimaryKey === false) {
103
109
  array = table.getSecondaryIndex(options.usingKey);
104
110
  const { from, to } = determineFromTo(array, options.topEquals, table.getKeyByName(options.usingKey));
105
111
  loopFrom = Math.max(loopFrom, from) - 1;
@@ -122,7 +128,7 @@ async function* loop(table, options) {
122
128
  // The same goes for a loop that reads an index table through a hash
123
129
  // secondary key. Handing out 1, 2, 3 there looks helpful and is a lie the
124
130
  // caller cannot tell from the truth.
125
- const usedKey = options?.usingKey === undefined || options.usingKey === "primary_key"
131
+ const usedKey = options?.usingKey === undefined || isPrimaryKey
126
132
  ? undefined
127
133
  : table.getKeyByName(options.usingKey);
128
134
  const hasRowNumber = usedKey !== undefined
@@ -131,7 +137,9 @@ async function* loop(table, options) {
131
137
  try {
132
138
  const isStructured = array[0] instanceof types_1.Structure;
133
139
  while (loopController.index < loopController.loopTo) {
134
- if (loopController.index > array.length) {
140
+ // the body may have deleted rows: never read past the end, where
141
+ // array[array.length] is undefined
142
+ if (loopController.index >= array.length) {
135
143
  break;
136
144
  }
137
145
  const current = array[loopController.index];
@@ -90,6 +90,73 @@ async function searchWithKeyPromise(arr, withKey, startIndex = 0, usesTableLine)
90
90
  };
91
91
  }
92
92
  /////////////////
93
+ function toRow(a, isStructured, usesTableLine) {
94
+ if (usesTableLine === false && isStructured === true) {
95
+ return a.get();
96
+ }
97
+ return isStructured ? { table_line: a, ...a.get() } : { table_line: a };
98
+ }
99
+ /** READ with a key on a SORTED table: binary search over the leading key components that are given,
100
+ * a miss gives sy-subrc 4 and sy-tabix of the row the key would be inserted before,
101
+ * or sy-subrc 8 and sy-tabix lines + 1 when it would be inserted after the last row */
102
+ /** true when every component of the condition is a component of the key */
103
+ function onlyKeyComponents(keyFields, options) {
104
+ const key = keyFields.map(k => k.toLowerCase());
105
+ return Object.keys(options.withKeySimple || {}).every(n => key.includes(n.toLowerCase()));
106
+ }
107
+ function readSortedWithKey(arr, keyFields, options) {
108
+ const names = Object.keys(options.withKeySimple).map(n => n.toLowerCase());
109
+ const prefix = [];
110
+ for (const keyField of keyFields) {
111
+ const index = names.indexOf(keyField.toLowerCase());
112
+ if (index < 0) {
113
+ break;
114
+ }
115
+ prefix.push(options.withKeyValue[index]);
116
+ }
117
+ if (prefix.length === 0) {
118
+ const searchResult = searchWithKey(arr, options.withKey, 0, options.usesTableLine);
119
+ return { found: searchResult.found, foundIndex: searchResult.foundIndex, subrc: undefined };
120
+ }
121
+ const isStructured = arr[0] instanceof types_1.Structure;
122
+ const comparePrefix = (row) => {
123
+ for (const p of prefix) {
124
+ const value = p.key(row);
125
+ // eq first, lt and gt compare a c operand with its trailing blanks against a string
126
+ if ((0, compare_1.eq)(value, p.value)) {
127
+ continue;
128
+ }
129
+ else if ((0, compare_1.lt)(value, p.value)) {
130
+ return -1;
131
+ }
132
+ else if ((0, compare_1.gt)(value, p.value)) {
133
+ return 1;
134
+ }
135
+ }
136
+ return 0;
137
+ };
138
+ let left = 0;
139
+ let right = arr.length;
140
+ while (left < right) {
141
+ const middle = Math.floor((left + right) / 2);
142
+ if (comparePrefix(toRow(arr[middle], isStructured, options.usesTableLine)) < 0) {
143
+ left = middle + 1;
144
+ }
145
+ else {
146
+ right = middle;
147
+ }
148
+ }
149
+ for (let index = left; index < arr.length; index++) {
150
+ const row = toRow(arr[index], isStructured, options.usesTableLine);
151
+ if (comparePrefix(row) !== 0) {
152
+ break;
153
+ }
154
+ if (options.withKey(row) === true) {
155
+ return { found: arr[index], foundIndex: index + 1, subrc: undefined };
156
+ }
157
+ }
158
+ return { found: undefined, foundIndex: left + 1, subrc: left >= arr.length ? 8 : 4 };
159
+ }
93
160
  function readTable(table, options) {
94
161
  let found = undefined;
95
162
  let foundIndex = 0;
@@ -187,15 +254,34 @@ function readTable(table, options) {
187
254
  // fallback
188
255
  return readTable(table, { ...options, withKeySimple: undefined });
189
256
  }
190
- const startIndex = (0, binary_search_1.binarySearchFrom)(arr, 0, arr.length, firstKeyName.toLowerCase(), firstValue) - 1;
191
- // console.dir("startindex: " + startIndex);
192
- if (startIndex >= 0) {
193
- const searchResult = searchWithKeyEarlyExit(arr, options.withKey, startIndex, options.usesTableLine, firstKeyName, firstValue);
194
- // console.dir(searchResult);
195
- found = searchResult.found;
196
- foundIndex = searchResult.foundIndex;
257
+ if (keyInformation?.type === types_1.TableAccessType.sorted
258
+ && options.withKeyValue
259
+ && onlyKeyComponents(keyInformation.keyFields, options)) {
260
+ const result = readSortedWithKey(arr, keyInformation.keyFields, options);
261
+ found = result.found;
262
+ foundIndex = result.foundIndex;
263
+ binarySubrc = result.subrc;
264
+ }
265
+ else {
266
+ const startIndex = (0, binary_search_1.binarySearchFrom)(arr, 0, arr.length, firstKeyName.toLowerCase(), firstValue) - 1;
267
+ if (startIndex >= 0) {
268
+ const searchResult = searchWithKeyEarlyExit(arr, options.withKey, startIndex, options.usesTableLine, firstKeyName, firstValue);
269
+ found = searchResult.found;
270
+ foundIndex = searchResult.foundIndex;
271
+ }
197
272
  }
198
273
  }
274
+ else if (options?.withTableKey === true
275
+ && options.withKeyValue
276
+ && options.withKeySimple
277
+ && options.withKey
278
+ && table.getOptions().primaryKey?.type === types_1.TableAccessType.sorted
279
+ && onlyKeyComponents(table.getOptions().primaryKey?.keyFields || [], options)) {
280
+ const result = readSortedWithKey(table.array(), table.getOptions().primaryKey?.keyFields || [], options);
281
+ found = result.found;
282
+ foundIndex = result.foundIndex;
283
+ binarySubrc = result.subrc;
284
+ }
199
285
  else if ((options?.binarySearch === true || options?.withTableKey === true)
200
286
  && options.withKeyValue
201
287
  && (options?.binarySearch === true || table.getOptions().primaryKey?.type !== types_1.TableAccessType.standard)
@@ -15,6 +15,20 @@ async function updateDatabase(table, options, context) {
15
15
  if (typeof table !== "string") {
16
16
  table = table.get();
17
17
  }
18
+ if (options.table !== undefined) {
19
+ // row by row, as a work area each: sy-subrc 4 when a row found nothing
20
+ // to update, sy-dbcnt the rows updated
21
+ let subrc = 0;
22
+ let dbcnt = 0;
23
+ for (const row of options.table.array()) {
24
+ await updateDatabase(table, { from: row }, context);
25
+ subrc = Math.max(subrc, abap.builtin.sy.get().subrc.get());
26
+ dbcnt += abap.builtin.sy.get().dbcnt.get();
27
+ }
28
+ abap.builtin.sy.get().subrc.set(subrc);
29
+ abap.builtin.sy.get().dbcnt.set(dbcnt);
30
+ return subrc;
31
+ }
18
32
  const tabl = abap.DDIC[table.toUpperCase()];
19
33
  if (tabl === undefined) {
20
34
  await (0, throw_error_1.throwErrorWithParameters)("CX_SY_DYNAMIC_OSQL_SEMANTICS", { sqlmsg: new types_1.String().set(`Table ${table} not found`) });
@@ -3,6 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.templateFormatting = templateFormatting;
4
4
  const alpha_1 = require("./alpha");
5
5
  const types_1 = require("./types");
6
+ /** DECIMALS for a decimal number: its decimal digits are rounded, half away
7
+ * from zero, the way ABAP rounds commercially - 2.675 gives 2.68, where
8
+ * toFixed( ) on the nearest binary float gives 2.67 */
9
+ function decimalsOfDecimal(value, decimals) {
10
+ const shift = (x, places) => {
11
+ const [mantissa, exponent] = (x + "").split("e");
12
+ return Number(mantissa + "e" + (Number(exponent || 0) + places));
13
+ };
14
+ const rounded = shift(Math.round(shift(Math.abs(value), decimals)), -decimals);
15
+ return (value < 0 && rounded !== 0 ? "-" : "") + rounded.toFixed(decimals);
16
+ }
6
17
  function templateFormatting(source, options) {
7
18
  let text = "";
8
19
  if (source instanceof types_1.FieldSymbol) {
@@ -22,7 +33,10 @@ function templateFormatting(source, options) {
22
33
  }
23
34
  else if (source instanceof types_1.DecFloat34) {
24
35
  const raw = source.getRaw();
25
- if (Number.isInteger(raw)) {
36
+ if (options?.decimals !== undefined) {
37
+ text = decimalsOfDecimal(raw, options.decimals);
38
+ }
39
+ else if (Number.isInteger(raw)) {
26
40
  text = raw.toFixed(0);
27
41
  }
28
42
  else {
@@ -43,7 +57,7 @@ function templateFormatting(source, options) {
43
57
  }
44
58
  }
45
59
  else if (source instanceof types_1.Packed) {
46
- if (options?.decimals) {
60
+ if (options?.decimals !== undefined) {
47
61
  text = source.toFixed(options.decimals);
48
62
  }
49
63
  else {
@@ -127,10 +141,10 @@ function templateFormatting(source, options) {
127
141
  else if (options.width) {
128
142
  text = text.trimEnd().padEnd(options.width, " ");
129
143
  }
130
- else if (options.decimals && source instanceof types_1.Integer) {
144
+ else if (options.decimals !== undefined && source instanceof types_1.Integer) {
131
145
  text = source.get().toFixed(options.decimals);
132
146
  }
133
- else if (options.decimals && source instanceof types_1.Float) {
147
+ else if (options.decimals !== undefined && source instanceof types_1.Float) {
134
148
  text = source.getRaw().toFixed(options.decimals);
135
149
  }
136
150
  }
@@ -97,7 +97,7 @@ export declare class Table implements ITable {
97
97
  clone(): Table;
98
98
  getArrayLength(): number;
99
99
  getKeyByName(name: string): ITableKey | undefined;
100
- getSecondaryIndex(name: string): TableRowType[];
100
+ getSecondaryIndex(name: string): any[];
101
101
  getQualifiedName(): string | undefined;
102
102
  getOptions(): ITableOptions;
103
103
  startLoop(from: number, to: number, array: any[]): LoopController;
@@ -27,6 +27,15 @@ var TableKeyType;
27
27
  TableKeyType["user"] = "USER";
28
28
  TableKeyType["empty"] = "EMPTY";
29
29
  })(TableKeyType || (exports.TableKeyType = TableKeyType = {}));
30
+ /* rows are handed to a sorted secondary key in table order, and a duplicate of a
31
+ * non-unique key goes in front of its equals, as INSERT ... INTO TABLE does: reversed
32
+ * before the stable sort, the later of two equal rows comes first */
33
+ function secondaryDuplicatesFirst(rows, key) {
34
+ if (key.type === TableAccessType.sorted && key.isUnique !== true) {
35
+ rows.reverse();
36
+ }
37
+ return rows;
38
+ }
30
39
  class LoopController {
31
40
  index;
32
41
  loopTo;
@@ -109,7 +118,7 @@ class HashedTable {
109
118
  throw `Table, secondary key "${name}" not found`;
110
119
  }
111
120
  // note, array() already is a copy, so it can be used,
112
- const copy = this.array();
121
+ const copy = secondaryDuplicatesFirst(this.array(), secondary);
113
122
  (0, sort_1.sort)(copy, { by: secondary.keyFields.map(k => { return { component: k.toLowerCase() }; }) });
114
123
  this.secondaryIndexes[name.toUpperCase()] = copy;
115
124
  return copy;
@@ -365,7 +374,7 @@ class Table {
365
374
  if (secondary === undefined) {
366
375
  throw `Table, secondary key "${name}" not found`;
367
376
  }
368
- const copy = [...this.value];
377
+ const copy = secondaryDuplicatesFirst([...this.value], secondary);
369
378
  (0, sort_1.sort)(copy, { by: secondary.keyFields.map(k => { return { component: k.toLowerCase() }; }), skipSortedCheck: true });
370
379
  this.secondaryIndexes[name.toUpperCase()] = copy;
371
380
  return copy;
@@ -482,24 +491,25 @@ class Table {
482
491
  return { value: this.insertIndex(val, 0, true), subrc: 0 };
483
492
  }
484
493
  const lastComparison = compare(this.value[lastIndex], val);
485
- if (lastComparison < 0 || (lastComparison === 0 && unique === false)) {
494
+ if (lastComparison < 0) {
486
495
  return { value: this.insertIndex(val, this.value.length, true), subrc: 0 };
487
496
  }
488
- else if (lastComparison === 0) {
497
+ else if (lastComparison === 0 && unique === true) {
489
498
  return { value: undefined, subrc: 4 };
490
499
  }
491
500
  let low = 0;
492
501
  let high = this.value.length;
493
502
  while (low < high) {
494
503
  const middle = Math.floor((low + high) / 2);
495
- if (compare(this.value[middle], val) <= 0) {
504
+ // a duplicate of a non-unique key goes in front of the rows it duplicates
505
+ if (compare(this.value[middle], val) < 0) {
496
506
  low = middle + 1;
497
507
  }
498
508
  else {
499
509
  high = middle;
500
510
  }
501
511
  }
502
- if (unique === true && low > 0 && compare(this.value[low - 1], val) === 0) {
512
+ if (unique === true && low < this.value.length && compare(this.value[low], val) === 0) {
503
513
  return { value: undefined, subrc: 4 };
504
514
  }
505
515
  return { value: this.insertIndex(val, low, true), subrc: 0 };
@@ -550,8 +560,18 @@ class Table {
550
560
  return val;
551
561
  }
552
562
  }
553
- /* appends and returns this */
563
+ /* appends and returns this - how VALUE #( ) builds a table, row by row.
564
+ * A sorted table gets each row at its sorted place, as INSERT ... INTO
565
+ * TABLE puts it: pushed, it was in insertion order until something
566
+ * assigned it, so a VALUE #( ) handed straight to a method or looped over
567
+ * was a SORTED table unsorted, and every binary search on it could miss.
568
+ * A duplicate of a unique key is kept, as before */
554
569
  appendThis(item) {
570
+ const primary = this.getOptions()?.primaryKey;
571
+ if (primary?.type === TableAccessType.sorted && !(item instanceof data_reference_1.DataReference)) {
572
+ this.insertSorted(item, (a, b) => (0, sort_1.compareRows)(a, b, primary.keyFields), false);
573
+ return this;
574
+ }
555
575
  this.append(item);
556
576
  return this;
557
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abaplint/runtime",
3
- "version": "2.13.93",
3
+ "version": "2.13.95",
4
4
  "description": "Transpiler - Runtime",
5
5
  "main": "build/src/index.js",
6
6
  "typings": "build/src/index.d.ts",
@@ -33,11 +33,11 @@
33
33
  "@types/mocha": "^10.0.10",
34
34
  "@types/node": "^26.4.1",
35
35
  "chai": "^6.2.2",
36
- "mocha": "^12.0.2",
36
+ "mocha": "^12.0.3",
37
37
  "source-map-support": "^0.5.21",
38
38
  "typescript": "^6.0.3"
39
39
  },
40
40
  "dependencies": {
41
- "temporal-polyfill": "^1.0.4"
41
+ "temporal-polyfill": "^1.0.5"
42
42
  }
43
43
  }