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

rokucommunity / brs / #26

01 Dec 2023 12:59PM UTC coverage: 91.487% (-0.2%) from 91.648%
#26

push

web-flow
Merge 73f2d2beb into e98151c1d

1772 of 2065 branches covered (0.0%)

Branch coverage included in aggregate %.

78 of 85 new or added lines in 2 files covered. (91.76%)

2 existing lines in 1 file now uncovered.

5235 of 5594 relevant lines covered (93.58%)

8975.08 hits per line

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

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

9
/** A member of an `AssociativeArray` in BrightScript. */
10
export interface AAMember {
11
    /** The member's name. */
12
    name: BrsString;
13
    /** The value associated with `name`. */
14
    value: BrsType;
15
}
16

17
export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIterable {
138✔
18
    readonly kind = ValueKind.Object;
11,935✔
19
    elements = new Map<string, BrsType>();
11,935✔
20
    enumIndex: number;
21
    /** Maps lowercased element name to original name used in this.elements.
22
     * Main benefit of it is fast, case-insensitive access.
23
     */
24
    keyMap = new Map<string, Set<string>>();
11,935✔
25
    private modeCaseSensitive: boolean = false;
11,935✔
26

27
    constructor(elements: AAMember[]) {
28
        super("roAssociativeArray");
11,935✔
29
        elements.forEach((member) => this.set(member.name, member.value, true));
11,935✔
30
        this.enumIndex = elements.length ? 0 : -1;
11,935✔
31

32
        this.registerMethods({
11,935✔
33
            ifAssociativeArray: [
34
                this.clear,
35
                this.delete,
36
                this.addreplace,
37
                this.count,
38
                this.doesexist,
39
                this.append,
40
                this.keys,
41
                this.items,
42
                this.lookup,
43
                this.lookupCI,
44
                this.setmodecasesensitive,
45
            ],
46
            ifEnum: [this.isEmpty, this.isNext, this.next, this.reset],
47
        });
48
    }
49

50
    toString(parent?: BrsType): string {
51
        if (parent) {
49✔
52
            return "<Component: roAssociativeArray>";
34✔
53
        }
54

55
        return [
15✔
56
            "<Component: roAssociativeArray> =",
57
            "{",
58
            ...Array.from(this.elements.entries())
59
                .sort()
60
                .map(([key, value]) => `    ${key}: ${value.toString(this)}`),
27✔
61
            "}",
62
        ].join("\n");
63
    }
64

65
    equalTo(other: BrsType) {
66
        return BrsBoolean.False;
3✔
67
    }
68

69
    getValue() {
70
        return this.elements;
41✔
71
    }
72

73
    getElements() {
74
        return Array.from(this.elements.keys())
33✔
75
            .sort()
76
            .map((key) => new BrsString(key));
72✔
77
    }
78

79
    getValues() {
80
        return Array.from(this.elements.values())
×
81
            .sort()
82
            .map((value: BrsType) => value);
×
83
    }
84

85
    get(index: BrsType, isCaseSensitive = false) {
692✔
86
        if (index.kind !== ValueKind.String) {
709!
87
            throw new Error("Associative array indexes must be strings");
×
88
        }
89

90
        // TODO: this works for now, in that a property with the same name as a method essentially
91
        // overwrites the method. The only reason this doesn't work is that getting a method from an
92
        // associative array and _not_ calling it returns `invalid`, but calling it returns the
93
        // function itself. I'm not entirely sure why yet, but it's gotta have something to do with
94
        // how methods are implemented within RBI.
95
        //
96
        // Are they stored separately from elements, like they are here? Or does
97
        // `Interpreter#visitCall` need to check for `invalid` in its callable, then try to find a
98
        // method with the desired name separately? That last bit would work but it's pretty gross.
99
        // That'd allow roArrays to have methods with the methods not accessible via `arr["count"]`.
100
        // Same with RoAssociativeArrays I guess.
101
        return (
709✔
102
            this.findElement(index.value, isCaseSensitive) ||
796✔
103
            this.getMethod(index.value) ||
104
            BrsInvalid.Instance
105
        );
106
    }
107

108
    set(index: BrsType, value: BrsType, isCaseSensitive = false) {
350✔
109
        if (index.kind !== ValueKind.String) {
3,096!
110
            throw new Error("Associative array indexes must be strings");
×
111
        }
112
        // override old key with new one
113
        let oldKey = this.findElementKey(index.value);
3,096✔
114
        if (!this.modeCaseSensitive && oldKey) {
3,096✔
115
            this.elements.delete(oldKey);
47✔
116
            this.keyMap.set(oldKey.toLowerCase(), new Set()); // clear key set cuz in insensitive mode we should have 1 key in set
47✔
117
        }
118

119
        let indexValue = isCaseSensitive ? index.value : index.value.toLowerCase();
3,096✔
120
        this.elements.set(indexValue, value);
3,096✔
121

122
        let lkey = index.value.toLowerCase();
3,096✔
123
        if (!this.keyMap.has(lkey)) {
3,096✔
124
            this.keyMap.set(lkey, new Set());
3,047✔
125
        }
126
        this.keyMap.get(lkey)?.add(indexValue);
3,096!
127

128
        return BrsInvalid.Instance;
3,096✔
129
    }
130

131
    getNext() {
132
        const keys = Array.from(this.elements.keys());
17✔
133
        const index = this.enumIndex;
17✔
134
        if (index >= 0) {
17✔
135
            this.enumIndex++;
15✔
136
            if (this.enumIndex >= keys.length) {
15✔
137
                this.enumIndex = -1;
2✔
138
            }
139
        }
140
        return keys[index];
17✔
141
    }
142

143
    updateNext() {
144
        const keys = Array.from(this.elements.keys());
19✔
145
        const hasItems = keys.length > 0;
19✔
146
        if (this.enumIndex === -1 && hasItems) {
19✔
147
            this.enumIndex = 0;
2✔
148
        } else if (this.enumIndex >= keys.length || !hasItems) {
17!
NEW
149
            this.enumIndex = -1;
×
150
        }
151
    }
152

153
    /** if AA is in insensitive mode, it means that we should do insensitive search of real key */
154
    private findElementKey(elementKey: string, isCaseSensitiveFind = false) {
3,096✔
155
        if (this.modeCaseSensitive && isCaseSensitiveFind) {
3,816✔
156
            return elementKey;
7✔
157
        } else {
158
            return this.keyMap.get(elementKey.toLowerCase())?.values().next().value;
3,809✔
159
        }
160
    }
161

162
    private findElement(elementKey: string, isCaseSensitiveFind = false) {
×
163
        let realKey = this.findElementKey(elementKey, isCaseSensitiveFind);
709✔
164
        return realKey !== undefined ? this.elements.get(realKey) : undefined;
709✔
165
    }
166

167
    /** Removes all elements from the associative array */
168
    private clear = new Callable("clear", {
11,935✔
169
        signature: {
170
            args: [],
171
            returns: ValueKind.Void,
172
        },
173
        impl: (_: Interpreter) => {
174
            this.elements.clear();
2✔
175
            this.keyMap.clear();
2✔
176
            this.enumIndex = -1;
2✔
177
            return BrsInvalid.Instance;
2✔
178
        },
179
    });
180

181
    /** Removes a given item from the associative array */
182
    private delete = new Callable("delete", {
11,935✔
183
        signature: {
184
            args: [new StdlibArgument("str", ValueKind.String)],
185
            returns: ValueKind.Boolean,
186
        },
187
        impl: (_: Interpreter, str: BrsString) => {
188
            let key = this.findElementKey(str.value, this.modeCaseSensitive);
3✔
189
            let deleted = key ? this.elements.delete(key) : false;
3✔
190

191
            let lKey = str.value.toLowerCase();
3✔
192
            if (this.modeCaseSensitive) {
3!
193
                let keySet = this.keyMap.get(lKey);
×
194
                keySet?.delete(key);
×
195
                if (keySet?.size === 0) {
×
196
                    this.keyMap.delete(lKey);
×
197
                }
198
            } else {
199
                this.keyMap.delete(lKey);
3✔
200
            }
201
            this.updateNext();
3✔
202
            return BrsBoolean.from(deleted);
3✔
203
        },
204
    });
205

206
    /** Given a key and value, adds an item to the associative array if it doesn't exist
207
     * Or replaces the value of a key that already exists in the associative array
208
     */
209
    private addreplace = new Callable("addreplace", {
11,935✔
210
        signature: {
211
            args: [
212
                new StdlibArgument("key", ValueKind.String),
213
                new StdlibArgument("value", ValueKind.Dynamic),
214
            ],
215
            returns: ValueKind.Void,
216
        },
217
        impl: (_: Interpreter, key: BrsString, value: BrsType) => {
218
            this.set(key, value, /* isCaseSensitive */ true);
14✔
219
            this.updateNext();
14✔
220
            return BrsInvalid.Instance;
14✔
221
        },
222
    });
223

224
    /** Returns the number of items in the associative array */
225
    private count = new Callable("count", {
11,935✔
226
        signature: {
227
            args: [],
228
            returns: ValueKind.Int32,
229
        },
230
        impl: (_: Interpreter) => {
231
            return new Int32(this.elements.size);
14✔
232
        },
233
    });
234

235
    /** Returns a boolean indicating whether or not a given key exists in the associative array */
236
    private doesexist = new Callable("doesexist", {
11,935✔
237
        signature: {
238
            args: [new StdlibArgument("str", ValueKind.String)],
239
            returns: ValueKind.Boolean,
240
        },
241
        impl: (_: Interpreter, str: BrsString) => {
242
            let key = this.findElementKey(str.value, this.modeCaseSensitive);
8✔
243
            return key && this.elements.has(key) ? BrsBoolean.True : BrsBoolean.False;
8✔
244
        },
245
    });
246

247
    /** Appends a new associative array to another. If two keys are the same, the value of the original AA is replaced with the new one. */
248
    private append = new Callable("append", {
11,935✔
249
        signature: {
250
            args: [new StdlibArgument("obj", ValueKind.Object)],
251
            returns: ValueKind.Void,
252
        },
253
        impl: (_: Interpreter, obj: BrsType) => {
254
            if (!(obj instanceof RoAssociativeArray)) {
2!
255
                // TODO: validate against RBI
256
                return BrsInvalid.Instance;
×
257
            }
258

259
            obj.elements.forEach((value, key) => {
2✔
260
                this.set(new BrsString(key), value, true);
6✔
261
            });
262
            this.updateNext();
2✔
263

264
            return BrsInvalid.Instance;
2✔
265
        },
266
    });
267

268
    /** Returns an array of keys from the associative array in lexicographical order */
269
    private keys = new Callable("keys", {
11,935✔
270
        signature: {
271
            args: [],
272
            returns: ValueKind.Object,
273
        },
274
        impl: (_: Interpreter) => {
275
            return new RoArray(this.getElements());
7✔
276
        },
277
    });
278

279
    /** Returns an array of values from the associative array in lexicographical order */
280
    private items = new Callable("items", {
11,935✔
281
        signature: {
282
            args: [],
283
            returns: ValueKind.Object,
284
        },
285
        impl: (_: Interpreter) => {
286
            return new RoArray(
17✔
287
                this.getElements().map((key: BrsString) => {
288
                    return new RoAssociativeArray([
18✔
289
                        {
290
                            name: new BrsString("key"),
291
                            value: key,
292
                        },
293
                        {
294
                            name: new BrsString("value"),
295
                            value: this.get(key),
296
                        },
297
                    ]);
298
                })
299
            );
300
        },
301
    });
302

303
    /** Given a key, returns the value associated with that key.
304
     * This method is case insensitive either-or case sensitive, depends on whether `setModeCasesensitive` was called or not.
305
     */
306
    private lookup = new Callable("lookup", {
11,935✔
307
        signature: {
308
            args: [new StdlibArgument("key", ValueKind.String)],
309
            returns: ValueKind.Dynamic,
310
        },
311
        impl: (_: Interpreter, key: BrsString) => {
312
            return this.get(key, true);
7✔
313
        },
314
    });
315

316
    /** Given a key, returns the value associated with that key. This method always is case insensitive. */
317
    private lookupCI = new Callable("lookupCI", {
11,935✔
318
        signature: {
319
            args: [new StdlibArgument("key", ValueKind.String)],
320
            returns: ValueKind.Dynamic,
321
        },
322
        impl: (_: Interpreter, key: BrsString) => {
323
            return this.get(key);
4✔
324
        },
325
    });
326

327
    /** Changes the sensitive case method for lookups */
328
    private setmodecasesensitive = new Callable("setModeCaseSensitive", {
11,935✔
329
        signature: {
330
            args: [],
331
            returns: ValueKind.Void,
332
        },
333
        impl: (_: Interpreter) => {
334
            this.modeCaseSensitive = true;
3✔
335
            return BrsInvalid.Instance;
3✔
336
        },
337
    });
338

339
    //--------------------------------- ifEnum ---------------------------------
340

341
    /** Returns true if enumeration contains no elements, false otherwise         */
342
    private isEmpty = new Callable("isEmpty", {
11,935✔
343
        signature: {
344
            args: [],
345
            returns: ValueKind.Boolean,
346
        },
347
        impl: (_: Interpreter) => {
348
            return BrsBoolean.from(this.elements.size === 0);
1✔
349
        },
350
    });
351

352
    /** Checks whether the current position is not past the end of the enumeration. */
353
    private isNext = new Callable("isNext", {
11,935✔
354
        signature: {
355
            args: [],
356
            returns: ValueKind.Boolean,
357
        },
358
        impl: (_: Interpreter) => {
359
            return BrsBoolean.from(this.enumIndex >= 0);
6✔
360
        },
361
    });
362

363
    /** Resets the current position to the first element of the enumeration. */
364
    private reset = new Callable("reset", {
11,935✔
365
        signature: {
366
            args: [],
367
            returns: ValueKind.Void,
368
        },
369
        impl: (_: Interpreter) => {
370
            this.enumIndex = this.elements.size > 0 ? 0 : -1;
3!
371
            return BrsInvalid.Instance;
3✔
372
        },
373
    });
374

375
    /** Increments the position of an enumeration. */
376
    private next = new Callable("next", {
11,935✔
377
        signature: {
378
            args: [],
379
            returns: ValueKind.Dynamic,
380
        },
381
        impl: (_: Interpreter) => {
382
            const item = this.getNext();
17✔
383
            return item ? new BrsString(item) : BrsInvalid.Instance;
17✔
384
        },
385
    });
386
}
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