• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brs / #80

03 Apr 2024 10:01PM UTC coverage: 89.636% (+0.007%) from 89.629%
#80

push

web-flow
Merge f7a015966 into 5ea0f3c2f

1997 of 2404 branches covered (83.07%)

Branch coverage included in aggregate %.

5770 of 6261 relevant lines covered (92.16%)

29921.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.48
/src/brsTypes/components/RoArray.ts
1
import { BrsType, isBrsString, isBrsNumber, Int32, Float } from "..";
142✔
2
import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType";
142✔
3
import { BrsComponent, BrsIterable } from "./BrsComponent";
142✔
4
import { Callable, StdlibArgument } from "../Callable";
142✔
5
import { Interpreter } from "../../interpreter";
6
import { RoAssociativeArray } from "./RoAssociativeArray";
142✔
7

8
/**
9
 * Gives the order for sorting different types in a mixed array
10
 *
11
 * Mixed Arrays seem to get sorted in this order (tested with Roku OS 9.4):
12
 *
13
 * numbers in numeric order
14
 * strings in alphabetical order,
15
 * assocArrays in original order
16
 * everything else in original order
17
 */
18
function getTypeSortIndex(a: BrsType): number {
19
    if (isBrsNumber(a)) {
712✔
20
        return 0;
249✔
21
    } else if (isBrsString(a)) {
463✔
22
        return 1;
289✔
23
    } else if (a instanceof RoAssociativeArray) {
174✔
24
        return 2;
119✔
25
    }
26
    return 3;
55✔
27
}
28

29
/**
30
 * Sorts two BrsTypes in the order that Roku would sort them
31
 * @param originalArray A copy of the original array. Used to get the order of items
32
 * @param a
33
 * @param b
34
 * @param caseInsensitive Should strings be compared case insensitively? defaults to false
35
 * @param sortInsideTypes Should two numbers or two strings be sorted? defaults to true
36
 * @return compare value for array.sort()
37
 */
38
function sortCompare(
39
    originalArray: BrsType[],
40
    a: BrsType,
41
    b: BrsType,
42
    caseInsensitive: boolean = false,
×
43
    sortInsideTypes: boolean = true
187✔
44
): number {
45
    let compare = 0;
356✔
46
    if (a !== undefined && b !== undefined) {
356✔
47
        const aSortOrder = getTypeSortIndex(a);
356✔
48
        const bSortOrder = getTypeSortIndex(b);
356✔
49
        if (aSortOrder < bSortOrder) {
356✔
50
            compare = -1;
186✔
51
        } else if (bSortOrder < aSortOrder) {
170✔
52
            compare = 1;
46✔
53
        } else if (sortInsideTypes && isBrsNumber(a)) {
124✔
54
            // two numbers are in numeric order
55
            compare = (a as Comparable).greaterThan(b).toBoolean() ? 1 : -1;
46✔
56
        } else if (sortInsideTypes && isBrsString(a)) {
78✔
57
            // two strings are in alphabetical order
58
            let aStr = a.toString();
46✔
59
            let bStr = b.toString();
46✔
60
            if (caseInsensitive) {
46✔
61
                aStr = aStr.toLowerCase();
18✔
62
                bStr = bStr.toLowerCase();
18✔
63
            }
64
            // roku does not use locale for sorting strings
65
            compare = aStr > bStr ? 1 : -1;
46✔
66
        } else {
67
            // everything else is in the same order as the original
68
            const aOriginalIndex = originalArray.indexOf(a);
32✔
69
            const bOriginalIndex = originalArray.indexOf(b);
32✔
70
            if (aOriginalIndex > -1 && bOriginalIndex > -1) {
32✔
71
                compare = aOriginalIndex - bOriginalIndex;
32✔
72
            }
73
        }
74
    }
75
    return compare;
356✔
76
}
77

78
export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
142✔
79
    readonly kind = ValueKind.Object;
1,628✔
80
    elements: BrsType[];
81
    enumIndex: number;
82

83
    constructor(elements: BrsType[]) {
84
        super("roArray");
1,628✔
85
        this.elements = elements;
1,628✔
86
        this.enumIndex = elements.length ? 0 : -1;
1,628✔
87
        this.registerMethods({
1,628✔
88
            ifArray: [
89
                this.peek,
90
                this.pop,
91
                this.push,
92
                this.shift,
93
                this.unshift,
94
                this.delete,
95
                this.count,
96
                this.clear,
97
                this.append,
98
            ],
99
            ifArrayGet: [this.getEntry],
100
            ifArraySet: [this.setEntry],
101
            ifArrayJoin: [this.join],
102
            ifArraySort: [this.sort, this.sortBy, this.reverse],
103
            ifArraySlice: [this.slice],
104
            ifEnum: [this.isEmpty, this.isNext, this.next, this.reset],
105
        });
106
    }
107

108
    toString(parent?: BrsType): string {
109
        if (parent) {
69✔
110
            return "<Component: roArray>";
62✔
111
        }
112

113
        return [
7✔
114
            "<Component: roArray> =",
115
            "[",
116
            ...Array.from(this.elements, (el: BrsType) =>
117
                el === undefined ? "    invalid" : `    ${el.toString(this)}`
15!
118
            ),
119
            "]",
120
        ].join("\n");
121
    }
122

123
    equalTo(other: BrsType) {
124
        return BrsBoolean.False;
4✔
125
    }
126

127
    getValue() {
128
        return this.elements;
61✔
129
    }
130

131
    getElements() {
132
        return this.elements.slice();
183✔
133
    }
134

135
    get(index: BrsType) {
136
        switch (index.kind) {
164!
137
            case ValueKind.Float:
138
                return this.getElements()[Math.trunc(index.getValue())] ?? BrsInvalid.Instance;
×
139
            case ValueKind.Int32:
140
                return this.getElements()[index.getValue()] ?? BrsInvalid.Instance;
102✔
141
            case ValueKind.String:
142
                return this.getMethod(index.value) ?? BrsInvalid.Instance;
62!
143
            default:
144
                throw new Error(
×
145
                    "Array indexes must be 32-bit integers or Float, or method names must be strings"
146
                );
147
        }
148
    }
149

150
    set(index: BrsType, value: BrsType) {
151
        if (index.kind === ValueKind.Int32 || index.kind === ValueKind.Float) {
13!
152
            this.elements[Math.trunc(index.getValue())] = value;
13✔
153
        } else {
154
            throw new Error("Array indexes must be 32-bit integers or Float");
×
155
        }
156
        return BrsInvalid.Instance;
13✔
157
    }
158

159
    getNext() {
160
        const index = this.enumIndex;
4✔
161
        if (index >= 0) {
4✔
162
            this.enumIndex++;
4✔
163
            if (this.enumIndex >= this.elements.length) {
4!
164
                this.enumIndex = -1;
×
165
            }
166
        }
167
        return this.elements[index];
4✔
168
    }
169

170
    updateNext() {
171
        const hasItems = this.elements.length > 0;
23✔
172
        if (this.enumIndex === -1 && hasItems) {
23✔
173
            this.enumIndex = 0;
2✔
174
        } else if (this.enumIndex >= this.elements.length || !hasItems) {
21✔
175
            this.enumIndex = -1;
2✔
176
        }
177
    }
178

179
    aaCompare(
180
        fieldName: BrsString,
181
        flags: BrsString,
182
        a: RoAssociativeArray,
183
        b: RoAssociativeArray
184
    ) {
185
        let compare = 0;
37✔
186
        const originalArrayCopy = [...this.elements];
37✔
187
        const caseInsensitive = flags.toString().indexOf("i") > -1;
37✔
188
        const aHasField = a.elements.has(fieldName.toString().toLowerCase());
37✔
189
        const bHasField = b.elements.has(fieldName.toString().toLowerCase());
37✔
190
        if (aHasField && bHasField) {
37✔
191
            const valueA = a.get(fieldName);
29✔
192
            const valueB = b.get(fieldName);
29✔
193
            compare = sortCompare(originalArrayCopy, valueA, valueB, caseInsensitive);
29✔
194
        } else if (aHasField) {
8✔
195
            // assocArray with fields come before assocArrays without
196
            compare = -1;
1✔
197
        } else if (bHasField) {
7✔
198
            // assocArray with fields come before assocArrays without
199
            compare = 1;
1✔
200
        }
201
        return compare;
37✔
202
    }
203

204
    // ifArray
205
    private peek = new Callable("peek", {
1,628✔
206
        signature: {
207
            args: [],
208
            returns: ValueKind.Dynamic,
209
        },
210
        impl: (_: Interpreter) => {
211
            return this.elements[this.elements.length - 1] || BrsInvalid.Instance;
2✔
212
        },
213
    });
214

215
    private pop = new Callable("pop", {
1,628✔
216
        signature: {
217
            args: [],
218
            returns: ValueKind.Dynamic,
219
        },
220
        impl: (_: Interpreter) => {
221
            const removed = this.elements.pop();
5✔
222
            this.updateNext();
5✔
223
            return removed || BrsInvalid.Instance;
5✔
224
        },
225
    });
226

227
    private push = new Callable("push", {
1,628✔
228
        signature: {
229
            args: [new StdlibArgument("talue", ValueKind.Dynamic)],
230
            returns: ValueKind.Void,
231
        },
232
        impl: (_: Interpreter, tvalue: BrsType) => {
233
            this.elements.push(tvalue);
7✔
234
            this.updateNext();
7✔
235
            return BrsInvalid.Instance;
7✔
236
        },
237
    });
238

239
    private shift = new Callable("shift", {
1,628✔
240
        signature: {
241
            args: [],
242
            returns: ValueKind.Dynamic,
243
        },
244
        impl: (_: Interpreter) => {
245
            const removed = this.elements.shift();
3✔
246
            this.updateNext();
3✔
247
            return removed || BrsInvalid.Instance;
3✔
248
        },
249
    });
250

251
    private unshift = new Callable("unshift", {
1,628✔
252
        signature: {
253
            args: [new StdlibArgument("tvalue", ValueKind.Dynamic)],
254
            returns: ValueKind.Void,
255
        },
256
        impl: (_: Interpreter, tvalue: BrsType) => {
257
            this.elements.unshift(tvalue);
3✔
258
            this.updateNext();
3✔
259
            return BrsInvalid.Instance;
3✔
260
        },
261
    });
262

263
    private delete = new Callable("delete", {
1,628✔
264
        signature: {
265
            args: [new StdlibArgument("index", ValueKind.Int32)],
266
            returns: ValueKind.Boolean,
267
        },
268
        impl: (_: Interpreter, index: Int32) => {
269
            if (index.lessThan(new Int32(0)).toBoolean()) {
4✔
270
                return BrsBoolean.False;
1✔
271
            }
272
            const deleted = this.elements.splice(index.getValue(), 1);
3✔
273
            this.updateNext();
3✔
274
            return BrsBoolean.from(deleted.length > 0);
3✔
275
        },
276
    });
277

278
    private count = new Callable("count", {
1,628✔
279
        signature: {
280
            args: [],
281
            returns: ValueKind.Int32,
282
        },
283
        impl: (_: Interpreter) => {
284
            return new Int32(this.elements.length);
34✔
285
        },
286
    });
287

288
    private clear = new Callable("clear", {
1,628✔
289
        signature: {
290
            args: [],
291
            returns: ValueKind.Void,
292
        },
293
        impl: (_: Interpreter) => {
294
            this.elements = [];
7✔
295
            this.enumIndex = -1;
7✔
296
            return BrsInvalid.Instance;
7✔
297
        },
298
    });
299

300
    private append = new Callable("append", {
1,628✔
301
        signature: {
302
            args: [new StdlibArgument("array", ValueKind.Object)],
303
            returns: ValueKind.Void,
304
        },
305
        impl: (interpreter: Interpreter, array: BrsComponent) => {
306
            if (!(array instanceof RoArray)) {
2!
307
                let location = `${interpreter.location.file}:(${interpreter.location.start.line})`;
×
308
                interpreter.stderr.write(
×
309
                    `BRIGHTSCRIPT: ERROR: roArray.Append: invalid parameter type ${array.getComponentName()}: ${location}\n`
310
                );
311
                return BrsInvalid.Instance;
×
312
            }
313

314
            this.elements = [
2✔
315
                ...this.elements,
316
                ...array.elements.filter((element) => !!element), // don't copy "holes" where no value exists
10✔
317
            ];
318
            this.updateNext();
2✔
319
            return BrsInvalid.Instance;
2✔
320
        },
321
    });
322

323
    // ifArrayGet
324
    private getEntry = new Callable("getEntry", {
1,628✔
325
        signature: {
326
            args: [new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float)],
327
            returns: ValueKind.Dynamic,
328
        },
329
        impl: (_: Interpreter, index: Int32 | Float) => {
330
            return this.elements[Math.trunc(index.getValue())] || BrsInvalid.Instance;
×
331
        },
332
    });
333

334
    // ifArraySet
335
    private setEntry = new Callable("setEntry", {
1,628✔
336
        signature: {
337
            args: [
338
                new StdlibArgument("index", ValueKind.Int32 | ValueKind.Float),
339
                new StdlibArgument("tvalue", ValueKind.Dynamic),
340
            ],
341
            returns: ValueKind.Void,
342
        },
343
        impl: (_: Interpreter, index: Int32 | Float, tvalue: BrsType) => {
344
            return this.set(index, tvalue);
×
345
        },
346
    });
347

348
    // ifArrayJoin
349
    private join = new Callable("join", {
1,628✔
350
        signature: {
351
            args: [new StdlibArgument("separator", ValueKind.String)],
352
            returns: ValueKind.String,
353
        },
354
        impl: (interpreter: Interpreter, separator: BrsString) => {
355
            if (
9✔
356
                this.elements.some(function (element) {
357
                    return !(element instanceof BrsString);
26✔
358
                })
359
            ) {
360
                interpreter.stderr.write("roArray.Join: Array contains non-string value(s).\n");
2✔
361
                return new BrsString("");
2✔
362
            }
363
            return new BrsString(this.elements.join(separator.value));
7✔
364
        },
365
    });
366
    // ifArraySort
367
    private sort = new Callable("sort", {
1,628✔
368
        signature: {
369
            args: [new StdlibArgument("flags", ValueKind.String, new BrsString(""))],
370
            returns: ValueKind.Void,
371
        },
372
        impl: (interpreter: Interpreter, flags: BrsString) => {
373
            if (flags.toString().match(/([^ir])/g) != null) {
12✔
374
                interpreter.stderr.write("roArray.Sort: Flags contains invalid option(s).\n");
1✔
375
            } else {
376
                const caseInsensitive = flags.toString().indexOf("i") > -1;
11✔
377
                const originalArrayCopy = [...this.elements];
11✔
378
                this.elements = this.elements.sort((a, b) => {
11✔
379
                    return sortCompare(originalArrayCopy, a, b, caseInsensitive);
158✔
380
                });
381
                if (flags.toString().indexOf("r") > -1) {
11✔
382
                    this.elements = this.elements.reverse();
5✔
383
                }
384
            }
385
            return BrsInvalid.Instance;
12✔
386
        },
387
    });
388

389
    private sortBy = new Callable("sortBy", {
1,628✔
390
        signature: {
391
            args: [
392
                new StdlibArgument("fieldName", ValueKind.String),
393
                new StdlibArgument("flags", ValueKind.String, new BrsString("")),
394
            ],
395
            returns: ValueKind.Void,
396
        },
397
        impl: (interpreter: Interpreter, fieldName: BrsString, flags: BrsString) => {
398
            if (flags.toString().match(/([^ir])/g) != null) {
13✔
399
                interpreter.stderr.write("roArray.SortBy: Flags contains invalid option(s).\n");
1✔
400
            } else {
401
                const originalArrayCopy = [...this.elements];
12✔
402
                this.elements = this.elements.sort((a, b) => {
12✔
403
                    let compare = 0;
206✔
404
                    if (a instanceof RoAssociativeArray && b instanceof RoAssociativeArray) {
206✔
405
                        compare = this.aaCompare(fieldName, flags, a, b);
37✔
406
                    } else if (a !== undefined && b !== undefined) {
169✔
407
                        compare = sortCompare(originalArrayCopy, a, b, false, false);
169✔
408
                    }
409
                    return compare;
206✔
410
                });
411
                if (flags.toString().indexOf("r") > -1) {
12✔
412
                    this.elements = this.elements.reverse();
5✔
413
                }
414
            }
415
            return BrsInvalid.Instance;
13✔
416
        },
417
    });
418

419
    private reverse = new Callable("reverse", {
1,628✔
420
        signature: {
421
            args: [],
422
            returns: ValueKind.Void,
423
        },
424
        impl: (_: Interpreter, separator: BrsString) => {
425
            this.elements = this.elements.reverse();
1✔
426
            return BrsInvalid.Instance;
1✔
427
        },
428
    });
429

430
    // ifArraySlice
431

432
    /** Returns a copy of a portion of an array into a new array selected from start to end (end not included) */
433
    private slice = new Callable("slice", {
1,628✔
434
        signature: {
435
            args: [
436
                new StdlibArgument("start", ValueKind.Int32 | ValueKind.Float, new Int32(0)),
437
                new StdlibArgument("end", ValueKind.Int32 | ValueKind.Float, BrsInvalid.Instance),
438
            ],
439
            returns: ValueKind.Object,
440
        },
441
        impl: (_: Interpreter, start: Int32 | Float, end: Int32 | Float | BrsInvalid) => {
442
            if (end instanceof BrsInvalid) {
15✔
443
                return new RoArray(this.elements.slice(start.getValue()));
7✔
444
            } else {
445
                return new RoArray(this.elements.slice(start.getValue(), end.getValue()));
8✔
446
            }
447
        },
448
    });
449

450
    // ifEnum
451

452
    /** Checks whether the array contains no elements. */
453
    private isEmpty = new Callable("isEmpty", {
1,628✔
454
        signature: {
455
            args: [],
456
            returns: ValueKind.Boolean,
457
        },
458
        impl: (_: Interpreter) => {
459
            return BrsBoolean.from(this.elements.length === 0);
2✔
460
        },
461
    });
462

463
    /** Checks whether the current position is not past the end of the enumeration. */
464
    private isNext = new Callable("isNext", {
1,628✔
465
        signature: {
466
            args: [],
467
            returns: ValueKind.Boolean,
468
        },
469
        impl: (_: Interpreter) => {
470
            return BrsBoolean.from(this.enumIndex >= 0);
4✔
471
        },
472
    });
473

474
    /** Resets the current position to the first element of the enumeration. */
475
    private reset = new Callable("reset", {
1,628✔
476
        signature: {
477
            args: [],
478
            returns: ValueKind.Void,
479
        },
480
        impl: (_: Interpreter) => {
481
            this.enumIndex = this.elements.length > 0 ? 0 : -1;
1!
482
            return BrsInvalid.Instance;
1✔
483
        },
484
    });
485

486
    /** Increments the position of an enumeration. */
487
    private next = new Callable("next", {
1,628✔
488
        signature: {
489
            args: [],
490
            returns: ValueKind.Dynamic,
491
        },
492
        impl: (_: Interpreter) => {
493
            return this.getNext() ?? BrsInvalid.Instance;
4!
494
        },
495
    });
496
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc