All files json-set.ts

100% Statements 107/107
100% Branches 43/43
100% Functions 19/19
100% Lines 107/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209    1x 1x                                               1x 1x   34x 34x 29x 60x 60x 29x 34x         34x   1x 4x 4x 4x         1x 16x 16x         1x 67x 67x 67x         1x 1x 1x         1x 2x 2x         1x 1x 1x 1x 1x         1x 1x 2x 2x 1x         1x 1x 2x 2x 1x         1x 25x 25x         1x 1x 1x 2x 1x 1x 1x         1x 2x 4x 1x 1x 4x 1x 2x         1x 2x 5x 1x 1x 5x 1x 2x         1x 2x 5x 1x 1x 5x 1x 2x         1x 7x 7x         1x 1x   1x 2x 1x 1x 2x   1x 2x 1x 1x 2x 1x 1x         1x 1x 1x         1x 21x 43x 40x 21x         1x 5x 5x 1x  
import { type JsonObject } from 'type-fest';
 
import { jsonDeserialize } from './json-deserialize.ts';
import { jsonSerialize } from './json-serialize.ts';
 
/**
 * A Set-like collection for objects that can be serialized to JSON.
 *
 * `JSONSet` stores objects by serializing them to JSON strings, allowing for deep equality
 * comparison of objects rather than reference equality. This is useful for storing and comparing
 * objects with the same structure and values, regardless of their references.
 * @typeParam T - The type of objects stored in the set. Must extend `JsonObject`.
 * @example
 * ```typescript
 * const set = new JSONSet<{ a: number }>();
 * set.add({ a: 1 });
 * set.has({ a: 1 }); // true
 * set.has({ a: 2 }); // false
 * ```
 * @remarks
 * - All objects are serialized using a `serialize` function and deserialized with a `deserialize` function.
 * - The set supports standard set operations such as union, intersection, difference, and symmetricDifference.
 * - Iteration yields deserialized objects.
 * @see Set
 * @group JSON
 * @category Data Structures
 */
export class JSONSet<T extends JsonObject> implements Set<T> {
  protected set = new Set<string>();
 
  public constructor(values?: Iterable<T> | null) {
    if (values) {
      for (const value of values) {
        this.add(value);
      }
    }
  }
 
  /**
   * The string tag used by Object.prototype.toString for this class.
   */
  public readonly [Symbol.toStringTag] = 'JSONSet';
 
  protected replicate<X = T>(values?: Iterable<X> | null): Set<X> {
    const Maker = this.constructor as new (values?: Iterable<X> | null) => Set<X>;
    return new Maker(values);
  }
 
  /**
   * Gets the number of elements in the set.
   */
  public get size(): number {
    return this.set.size;
  }
 
  /**
   * Adds a serialized value to the set.
   */
  public add(value: T): this {
    this.set.add(jsonSerialize(value));
    return this;
  }
 
  /**
   * Removes all elements from the set.
   */
  public clear(): void {
    this.set.clear();
  }
 
  /**
   * Removes the specified value from the set if it exists.
   */
  public delete(value: T): boolean {
    return this.set.delete(jsonSerialize(value));
  }
 
  /**
   * Returns a new set containing elements present in this set but not in the other set.
   */
  public difference<U>(other: ReadonlySetLike<U>): Set<T> {
    return this.replicate(
      Array.from(this.values()).filter((value) => !other.has(value as unknown as U)),
    );
  }
 
  /**
   * Returns an iterator over the set's values as [value, value] pairs.
   */
  public *entries(): SetIterator<[T, T]> {
    for (const value of this.values()) {
      yield [value, value];
    }
  }
 
  /**
   * Executes a provided function once for each value in the set.
   */
  public forEach(callback: (value: T, key: T, set: Set<T>) => void, thisArg?: unknown): void {
    for (const value of this.values()) {
      callback.call(thisArg, value, value, this);
    }
  }
 
  /**
   * Determines whether the specified value exists in the set.
   */
  public has(value: T): boolean {
    return this.set.has(jsonSerialize(value));
  }
 
  /**
   * Returns a new set containing only the elements present in both this set and the provided set.
   */
  public intersection<U>(other: ReadonlySetLike<U>): Set<T & U> {
    return this.replicate<T & U>(
      Array.from(this.values() as Iterable<T & U>).filter((value) =>
        other.has(value as unknown as U),
      ),
    );
  }
 
  /**
   * Determines whether this set and the specified set have no elements in common.
   */
  public isDisjointFrom(other: ReadonlySetLike<unknown>): boolean {
    for (const value of this.values()) {
      if (other.has(value)) {
        return false;
      }
    }
    return true;
  }
 
  /**
   * Determines whether all elements of this set are contained in another set.
   */
  public isSubsetOf(other: ReadonlySetLike<unknown>): boolean {
    for (const value of this.values()) {
      if (!other.has(value)) {
        return false;
      }
    }
    return true;
  }
 
  /**
   * Determines whether this set contains all elements of the specified set.
   */
  public isSupersetOf(other: ReadonlySetLike<unknown>): boolean {
    for (const value of other.keys() as unknown as Iterable<unknown>) {
      if (!this.has(value as T)) {
        return false;
      }
    }
    return true;
  }
 
  /**
   * Returns an iterator over the keys in the set.
   */
  public *keys(): SetIterator<T> {
    yield* this.values();
  }
 
  /**
   * Returns a new set containing elements that are in either this set or the other set, but not in both.
   */
  public symmetricDifference<U>(other: ReadonlySetLike<U>): Set<T | U> {
    const result = this.replicate<T | U>();
 
    for (const value of this.keys()) {
      if (!other.has(value as unknown as U)) {
        result.add(value);
      }
    }
 
    for (const value of other.keys() as SetIterator<U>) {
      if (!this.has(value as unknown as T)) {
        result.add(value as T | U);
      }
    }
    return result;
  }
 
  /**
   * Returns a new set containing all unique elements from this set and another set.
   */
  public union<U>(other: ReadonlySetLike<U>): Set<T | U> {
    return this.replicate<T | U>([...this.keys(), ...(other.keys() as SetIterator<U>)]);
  }
 
  /**
   * Returns an iterator that yields each value in the set after deserialization.
   */
  public *values(): SetIterator<T> {
    for (const object of this.set) {
      yield jsonDeserialize(object) as T;
    }
  }
 
  /**
   * Returns an iterator over the values in the set.
   */
  public [Symbol.iterator](): SetIterator<T> {
    return this.values();
  }
}