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

rokucommunity / brs / #294

24 Apr 2023 07:40PM UTC coverage: 91.705% (+5.4%) from 86.275%
#294

push

web-flow
Merge pull request #1 from rokucommunity/adoption

refactor in preparation for adoption the project

1736 of 2013 branches covered (86.24%)

Branch coverage included in aggregate %.

5152 of 5498 relevant lines covered (93.71%)

8882.84 hits per line

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

82.61
/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,843✔
19
    elements = new Map<string, BrsType>();
11,843✔
20
    /** Maps lowercased element name to original name used in this.elements.
21
     * Main benefit of it is fast, case-insensitive access.
22
     */
23
    keyMap = new Map<string, Set<string>>();
11,843✔
24
    private modeCaseSensitive: boolean = false;
11,843✔
25

26
    constructor(elements: AAMember[]) {
27
        super("roAssociativeArray");
11,843✔
28
        elements.forEach((member) => this.set(member.name, member.value, true));
11,843✔
29

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

48
    toString(parent?: BrsType): string {
49
        if (parent) {
33✔
50
            return "<Component: roAssociativeArray>";
28✔
51
        }
52

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

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

67
    getValue() {
68
        return this.elements;
41✔
69
    }
70

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

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

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

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

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

117
        let indexValue = isCaseSensitive ? index.value : index.value.toLowerCase();
3,104✔
118
        this.elements.set(indexValue, value);
3,104✔
119

120
        let lkey = index.value.toLowerCase();
3,104✔
121
        if (!this.keyMap.has(lkey)) {
3,104✔
122
            this.keyMap.set(lkey, new Set());
3,055✔
123
        }
124
        this.keyMap.get(lkey)?.add(indexValue);
3,104!
125

126
        return BrsInvalid.Instance;
3,104✔
127
    }
128

129
    /** if AA is in insensitive mode, it means that we should do insensitive search of real key */
130
    private findElementKey(elementKey: string, isCaseSensitiveFind = false) {
3,104✔
131
        if (this.modeCaseSensitive && isCaseSensitiveFind) {
3,793✔
132
            return elementKey;
7✔
133
        } else {
134
            return this.keyMap.get(elementKey.toLowerCase())?.values().next().value;
3,786✔
135
        }
136
    }
137

138
    private findElement(elementKey: string, isCaseSensitiveFind = false) {
×
139
        let realKey = this.findElementKey(elementKey, isCaseSensitiveFind);
678✔
140
        return realKey !== undefined ? this.elements.get(realKey) : undefined;
678✔
141
    }
142

143
    /** Removes all elements from the associative array */
144
    private clear = new Callable("clear", {
11,843✔
145
        signature: {
146
            args: [],
147
            returns: ValueKind.Void,
148
        },
149
        impl: (interpreter: Interpreter) => {
150
            this.elements.clear();
2✔
151
            this.keyMap.clear();
2✔
152
            return BrsInvalid.Instance;
2✔
153
        },
154
    });
155

156
    /** Removes a given item from the associative array */
157
    private delete = new Callable("delete", {
11,843✔
158
        signature: {
159
            args: [new StdlibArgument("str", ValueKind.String)],
160
            returns: ValueKind.Boolean,
161
        },
162
        impl: (interpreter: Interpreter, str: BrsString) => {
163
            let key = this.findElementKey(str.value, this.modeCaseSensitive);
3✔
164
            let deleted = key ? this.elements.delete(key) : false;
3✔
165

166
            let lKey = str.value.toLowerCase();
3✔
167
            if (this.modeCaseSensitive) {
3!
168
                let keySet = this.keyMap.get(lKey);
×
169
                keySet?.delete(key);
×
170
                if (keySet?.size === 0) {
×
171
                    this.keyMap.delete(lKey);
×
172
                }
173
            } else {
174
                this.keyMap.delete(lKey);
3✔
175
            }
176
            return BrsBoolean.from(deleted);
3✔
177
        },
178
    });
179

180
    /** Given a key and value, adds an item to the associative array if it doesn't exist
181
     * Or replaces the value of a key that already exists in the associative array
182
     */
183
    private addreplace = new Callable("addreplace", {
11,843✔
184
        signature: {
185
            args: [
186
                new StdlibArgument("key", ValueKind.String),
187
                new StdlibArgument("value", ValueKind.Dynamic),
188
            ],
189
            returns: ValueKind.Void,
190
        },
191
        impl: (interpreter: Interpreter, key: BrsString, value: BrsType) => {
192
            this.set(key, value, /* isCaseSensitive */ true);
6✔
193
            return BrsInvalid.Instance;
6✔
194
        },
195
    });
196

197
    /** Returns the number of items in the associative array */
198
    private count = new Callable("count", {
11,843✔
199
        signature: {
200
            args: [],
201
            returns: ValueKind.Int32,
202
        },
203
        impl: (interpreter: Interpreter) => {
204
            return new Int32(this.elements.size);
14✔
205
        },
206
    });
207

208
    /** Returns a boolean indicating whether or not a given key exists in the associative array */
209
    private doesexist = new Callable("doesexist", {
11,843✔
210
        signature: {
211
            args: [new StdlibArgument("str", ValueKind.String)],
212
            returns: ValueKind.Boolean,
213
        },
214
        impl: (interpreter: Interpreter, str: BrsString) => {
215
            let strValue = this.modeCaseSensitive ? str.value : str.value.toLowerCase();
8✔
216
            let key = this.findElementKey(str.value, this.modeCaseSensitive);
8✔
217
            return key && this.elements.has(key) ? BrsBoolean.True : BrsBoolean.False;
8✔
218
        },
219
    });
220

221
    /** 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. */
222
    private append = new Callable("append", {
11,843✔
223
        signature: {
224
            args: [new StdlibArgument("obj", ValueKind.Object)],
225
            returns: ValueKind.Void,
226
        },
227
        impl: (interpreter: Interpreter, obj: BrsType) => {
228
            if (!(obj instanceof RoAssociativeArray)) {
2!
229
                // TODO: validate against RBI
230
                return BrsInvalid.Instance;
×
231
            }
232

233
            obj.elements.forEach((value, key) => {
2✔
234
                this.set(new BrsString(key), value, true);
6✔
235
            });
236

237
            return BrsInvalid.Instance;
2✔
238
        },
239
    });
240

241
    /** Returns an array of keys from the associative array in lexicographical order */
242
    private keys = new Callable("keys", {
11,843✔
243
        signature: {
244
            args: [],
245
            returns: ValueKind.Object,
246
        },
247
        impl: (interpreter: Interpreter) => {
248
            return new RoArray(this.getElements());
9✔
249
        },
250
    });
251

252
    /** Returns an array of values from the associative array in lexicographical order */
253
    private items = new Callable("items", {
11,843✔
254
        signature: {
255
            args: [],
256
            returns: ValueKind.Object,
257
        },
258
        impl: (interpreter: Interpreter) => {
259
            return new RoArray(
18✔
260
                this.getElements().map((key: BrsString) => {
261
                    return new RoAssociativeArray([
21✔
262
                        {
263
                            name: new BrsString("key"),
264
                            value: key,
265
                        },
266
                        {
267
                            name: new BrsString("value"),
268
                            value: this.get(key),
269
                        },
270
                    ]);
271
                })
272
            );
273
        },
274
    });
275

276
    /** Given a key, returns the value associated with that key.
277
     * This method is case insensitive either-or case sensitive, depends on whether `setModeCasesensitive` was called or not.
278
     */
279
    private lookup = new Callable("lookup", {
11,843✔
280
        signature: {
281
            args: [new StdlibArgument("key", ValueKind.String)],
282
            returns: ValueKind.Dynamic,
283
        },
284
        impl: (interpreter: Interpreter, key: BrsString) => {
285
            return this.get(key, true);
7✔
286
        },
287
    });
288

289
    /** Given a key, returns the value associated with that key. This method always is case insensitive. */
290
    private lookupCI = new Callable("lookupCI", {
11,843✔
291
        signature: {
292
            args: [new StdlibArgument("key", ValueKind.String)],
293
            returns: ValueKind.Dynamic,
294
        },
295
        impl: (interpreter: Interpreter, key: BrsString) => {
296
            return this.get(key);
4✔
297
        },
298
    });
299

300
    /** Changes the sensitive case method for lookups */
301
    private setmodecasesensitive = new Callable("setModeCaseSensitive", {
11,843✔
302
        signature: {
303
            args: [],
304
            returns: ValueKind.Void,
305
        },
306
        impl: (interpreter: Interpreter) => {
307
            this.modeCaseSensitive = true;
3✔
308
            return BrsInvalid.Instance;
3✔
309
        },
310
    });
311

312
    //--------------------------------- ifEnum ---------------------------------
313

314
    /** Returns true if enumeration contains no elements, false otherwise         */
315
    private isEmpty = new Callable("isEmpty", {
11,843✔
316
        signature: {
317
            args: [],
318
            returns: ValueKind.Boolean,
319
        },
320
        impl: (interpreter: Interpreter) => {
321
            return BrsBoolean.from(this.elements.size === 0);
1✔
322
        },
323
    });
324
}
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

© 2026 Coveralls, Inc