Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript Map key-value pairs case insensitive search

Tags:

typescript

I am using the Map dictionary in TypeScript and I want to have the get and has properties to work case insensitive. How can I make this work ?

    let envVariables = new Map<string, string>();
    envVariables.set('OS', 'Windows_NT');
    envVariables.set('USERNAME', 'SYSTEM');
    if (this.envVariables.has('UserName')) {
      // this should work with case insensitive search
    }

In C# the Dictionary constructor just needs the StringComparer.OrdinalIgnoreCase and then the dictionary will be case insensitive.

like image 746
Devid Avatar asked Apr 25 '18 10:04

Devid


2 Answers

Map doesn't support this behaviour. It can be extended in order for keys to be stored and looked up in case-insensitive fashion. Since Map internally uses set on construction, the constructor doesn't need to be augmented.

It's straightforward with TypeScript es6 or higher target because ES6 built-in classes support extends:

class CaseInsensitiveMap<T, U> extends Map<T, U> {    
  set(key: T, value: U): this {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }
    return super.set(key, value);
  }

  get(key: T): U | undefined {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }

    return super.get(key);
  }

  has(key: T): boolean {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }

    return super.has(key);
  }
}

Native classes should be treated in a special way when being extended in TypeScript with es5 target:

interface CaseInsensitiveMap<T, U> extends Map<T, U> {} 
class CaseInsensitiveMap<T, U> {
  constructor(entries?: Array<[T, U]> | Iterable<[T, U]>) {
    return Reflect.construct(Map, arguments, CaseInsensitiveMap);
  }

  set (key: T, value: U): this {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }
    return Map.prototype.set.call(this, key, value) as this;
  }

  get (key: T): U | undefined {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }
    return Map.prototype.get.call(this, key) as U;
  }

  has(key: T): boolean {
    if (typeof key === 'string') {
      key = key.toLowerCase() as any as T;
    }
    return Map.prototype.has.call(this, key) as boolean;
  }
}
like image 121
Estus Flask Avatar answered Sep 21 '22 12:09

Estus Flask


This is my variant of a "true" Case Insensitive Map created using TypeScript: it contains a private map that stores original keys.

export class CaseInsensitiveMap<TKey, TVal> extends Map<TKey, TVal> {
    private keysMap = new Map<TKey, TKey>();

    constructor(iterable?: Iterable<[TKey, TVal]>){
        super();
        if (iterable) {
            for (const [key, value] of iterable) {
                this.set(key, value);
            }
        }
    }

    set(key: TKey, value: TVal): this {
        const keyLowerCase = typeof key === 'string'
            ? key.toLowerCase() as any as TKey
            : key;
        this.keysMap.set(keyLowerCase, key);

        return super.set(keyLowerCase, value);
    }

    get(key: TKey): TVal | undefined {
        return typeof key === 'string'
            ? super.get(key.toLowerCase() as any as TKey)
            : super.get(key);
    }

    has(key: TKey): boolean {
        return typeof key === 'string'
            ? super.has(key.toLowerCase() as any as TKey)
            : super.has(key);
    }

    delete(key: TKey): boolean {
        const keyLowerCase = typeof key === 'string'
            ? key.toLowerCase() as any as TKey
            : key;
        this.keysMap.delete(keyLowerCase);

        return super.delete(keyLowerCase);
    }
    
    clear(): void {
        this.keysMap.clear();
        super.clear();
    }

    keys(): IterableIterator<TKey> {
        return this.keysMap.values();
    }

    *entries(): IterableIterator<[TKey, TVal]> {
        const keys = this.keysMap.values();
        const values = super.values();
        for (let i = 0; i < super.size; i++) {
            yield [keys.next().value, values.next().value];
        }
    }

    forEach(callbackfn: (value: TVal, key: TKey, map: Map<TKey, TVal>) => void): void {
        const keys = this.keysMap.values();
        const values = super.values();
        for (let i = 0; i < super.size; i++) {
            callbackfn(values.next().value, keys.next().value, this);
        }
    }

    [Symbol.iterator](): IterableIterator<[TKey, TVal]> {
        return this.entries();
    }
}
like image 34
Kseniia Ann Avatar answered Sep 20 '22 12:09

Kseniia Ann