UNPKG

ts-data-forge

Version:

[![npm version](https://img.shields.io/npm/v/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![npm downloads](https://img.shields.io/npm/dm/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge) [![License](https://img.shields.

978 lines (808 loc) 21.7 kB
import { Optional } from '../functional/index.mjs'; import { IMapMapped } from './imap-mapped.mjs'; const toKey = (a: Readonly<{ v: number }>): number => a.v; const fromKey = (k: number): Readonly<{ v: number }> => ({ v: k }); type TestKey = { id: number; type: string }; const testKeyToString = (key: Readonly<TestKey>): string => `${key.type}_${key.id}`; const stringToTestKey = (str: string): TestKey => { const [type, idStr] = str.split('_'); return { type: type ?? '', id: Number(idStr ?? '0') }; }; describe('IMapMapped[Symbol.iterator]', () => { test('case 1', () => { const s0 = IMapMapped.create( IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), toKey, fromKey, ); assert.deepStrictEqual( s0, IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); }); }); describe('IMapMapped.create', () => { test('should create empty map', () => { const map = IMapMapped.create<TestKey, string, string>( [], testKeyToString, stringToTestKey, ); expect(map.size).toBe(0); }); test('should create map with initial entries', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); expect(map.size).toBe(2); expect(Optional.unwrap(map.get({ id: 1, type: 'user' }))).toBe('Alice'); expect(Optional.unwrap(map.get({ id: 2, type: 'admin' }))).toBe('Bob'); }); test('should create map from another IMapMapped', () => { const original = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const copy = IMapMapped.create(original, testKeyToString, stringToTestKey); expect(copy.size).toBe(2); expect(Optional.unwrap(copy.get({ id: 1, type: 'user' }))).toBe('Alice'); expect(Optional.unwrap(copy.get({ id: 2, type: 'admin' }))).toBe('Bob'); }); test('should handle complex key transformations', () => { type ComplexKey = { nested: { id: number }; arr: number[] }; const complexKeyToString = ( key: DeepReadonly<{ nested: { id: number }; arr: number[]; }>, ): string => `${key.nested.id}_${key.arr.join(',')}`; const stringToComplexKey = (str: string): ComplexKey => { const [idStr, arrStr] = str.split('_'); return { nested: { id: Number(idStr ?? '0') }, arr: (arrStr ?? '').split(',').map(Number), }; }; const map = IMapMapped.create<ComplexKey, string, string>( [[{ nested: { id: 1 }, arr: [1, 2, 3] }, 'test']], complexKeyToString, stringToComplexKey, ); expect(map.size).toBe(1); expect( Optional.unwrap(map.get({ nested: { id: 1 }, arr: [1, 2, 3] })), ).toBe('test'); }); }); describe('IMapMapped.equal', () => { test('should return true for equal maps', () => { const map1 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const map2 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); // Test structural equality, not reference equality expect(map1.size).toBe(map2.size); assert.isTrue(map1.has({ id: 1, type: 'user' })); assert.isTrue(map2.has({ id: 1, type: 'user' })); expect(Optional.unwrap(map1.get({ id: 1, type: 'user' }))).toBe( Optional.unwrap(map2.get({ id: 1, type: 'user' })), ); }); test('should return false for maps with different values', () => { const map1 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const map2 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Charlie'], ], testKeyToString, stringToTestKey, ); assert.isFalse(IMapMapped.equal(map1, map2)); }); test('should return false for maps with different keys', () => { const map1 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const map2 = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 3, type: 'guest' }, 'Bob'], ], testKeyToString, stringToTestKey, ); assert.isFalse(IMapMapped.equal(map1, map2)); }); test('should return true for empty maps', () => { const map1 = IMapMapped.create<TestKey, string, string>( [], testKeyToString, stringToTestKey, ); const map2 = IMapMapped.create<TestKey, string, string>( [], testKeyToString, stringToTestKey, ); assert.isTrue(IMapMapped.equal(map1, map2)); }); }); describe('IMapMapped.size', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); expect(s0.size).toBe(3); }); }); describe('IMapMapped.has', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.isTrue(s0.has({ v: 3 })); }); test('case 2', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.isFalse(s0.has({ v: 4 })); }); test('case 3', () => { const s0 = IMapMapped.create<Readonly<{ v: number }>, string, number>( [], toKey, fromKey, ); assert.isFalse(s0.has({ v: 3 })); }); }); describe('IMapMapped.get', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual(s0.get({ v: 3 }), Optional.some('3')); }); test('case 2', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual(s0.get({ v: 4 }), Optional.none); }); test('case 3', () => { const s0 = IMapMapped.create<Readonly<{ v: number }>, string, number>( [], toKey, fromKey, ); assert.deepStrictEqual(s0.get({ v: 3 }), Optional.none); }); }); describe('IMapMapped.set', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual( s0.set({ v: 5 }, '5'), IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], [{ v: 5 }, '5'], ], toKey, fromKey, ), ); assert.deepStrictEqual( s0, IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); }); test('case 2', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual( s0.set({ v: 3 }, '3'), IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); assert.deepStrictEqual( s0, IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); }); test('case 3', () => { const s0 = IMapMapped.create<Readonly<{ v: number }>, string, number>( [], toKey, fromKey, ); assert.deepStrictEqual( s0.set({ v: 1 }, '1'), IMapMapped.create([[{ v: 1 }, '1']], toKey, fromKey), ); assert.deepStrictEqual(s0, IMapMapped.create([], toKey, fromKey)); }); }); describe('IMapMapped.update', () => { test('should update existing key', () => { const map = IMapMapped.create( [[{ id: 1, type: 'user' }, 'Alice']], testKeyToString, stringToTestKey, ); const updated = map.update({ id: 1, type: 'user' }, (name) => name.toUpperCase(), ); expect(Optional.unwrap(updated.get({ id: 1, type: 'user' }))).toBe('ALICE'); }); test('should not update non-existent key', () => { const map = IMapMapped.create( [[{ id: 1, type: 'user' }, 'Alice']], testKeyToString, stringToTestKey, ); const updated = map.update({ id: 2, type: 'user' }, (name) => name.toUpperCase(), ); expect(updated).toBe(map); assert.isTrue(Optional.isNone(updated.get({ id: 2, type: 'user' }))); }); }); describe('IMapMapped.delete', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual( s0.delete({ v: 10 }), IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); assert.deepStrictEqual( s0, IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); }); test('case 2', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); assert.deepStrictEqual( s0.delete({ v: 3 }), IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], ], toKey, fromKey, ), ); assert.deepStrictEqual( s0, IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ), ); }); test('case 3', () => { const s0 = IMapMapped.create([], toKey, fromKey); assert.deepStrictEqual( s0.delete({ v: 1 }), IMapMapped.create([], toKey, fromKey), ); assert.deepStrictEqual(s0, IMapMapped.create([], toKey, fromKey)); }); test('should delete entry if it exists', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const updated = map.delete({ id: 2, type: 'admin' }); expect(updated).not.toBe(map); // Should return new instance expect(updated.size).toBe(1); expect(map.size).toBe(2); // Original unchanged }); }); describe('IMapMapped.every', () => { test('should return true when all values satisfy predicate', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); assert.isTrue(map.every((value) => typeof value === 'string')); }); test('should return false when some values do not satisfy predicate', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); assert.isFalse(map.every((value) => value.length > 5)); }); test('should return true for empty map', () => { const map = IMapMapped.create<TestKey, string, string>( [], testKeyToString, stringToTestKey, ); assert.isTrue(map.every((value) => value.length > 0)); }); }); describe('IMapMapped.some', () => { test('should return true when at least one value satisfies predicate', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); // eslint-disable-next-line unicorn/prefer-includes assert.isTrue(map.some((value) => value === 'Alice')); }); test('should return false when no values satisfy predicate', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); // eslint-disable-next-line unicorn/prefer-includes assert.isFalse(map.some((value) => value === 'Charlie')); }); test('should return false for empty map', () => { const map = IMapMapped.create<TestKey, string, string>( [], testKeyToString, stringToTestKey, ); assert.isFalse(map.some((value) => value.length > 0)); }); }); describe('IMapMapped.withMutations', () => { test('should apply multiple mutations', () => { const map = IMapMapped.create<TestKey, string, string>( [[{ id: 1, type: 'user' }, 'Alice']], testKeyToString, stringToTestKey, ); const updated = map.withMutations([ { type: 'set', key: { id: 2, type: 'admin' }, value: 'Bob' }, { type: 'update', key: { id: 1, type: 'user' }, updater: (x) => x.toUpperCase(), }, { type: 'delete', key: { id: 3, type: 'guest' } }, ]); expect(updated.size).toBe(2); expect(Optional.unwrap(updated.get({ id: 1, type: 'user' }))).toBe('ALICE'); expect(Optional.unwrap(updated.get({ id: 2, type: 'admin' }))).toBe('Bob'); }); test('should handle empty mutations array', () => { const map = IMapMapped.create( [[{ id: 1, type: 'user' }, 'Alice']], testKeyToString, stringToTestKey, ); const updated = map.withMutations([]); expect(updated.size).toBe(1); expect(Optional.unwrap(updated.get({ id: 1, type: 'user' }))).toBe('Alice'); }); }); describe('IMapMapped.map', () => { test('should transform values', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const transformed = map.map((value) => value.toUpperCase()); expect(Optional.unwrap(transformed.get({ id: 1, type: 'user' }))).toBe( 'ALICE', ); expect(Optional.unwrap(transformed.get({ id: 2, type: 'admin' }))).toBe( 'BOB', ); }); test('should work with key parameter', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const transformed = map.map((value, key) => `${key.type}:${value}`); expect(Optional.unwrap(transformed.get({ id: 1, type: 'user' }))).toBe( 'user:Alice', ); expect(Optional.unwrap(transformed.get({ id: 2, type: 'admin' }))).toBe( 'admin:Bob', ); }); }); describe('IMapMapped.mapKeys', () => { test('should transform keys', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const transformed = map.mapKeys((key) => ({ ...key, id: key.id * 10 })); assert.isTrue(Optional.isNone(transformed.get({ id: 1, type: 'user' }))); expect(Optional.unwrap(transformed.get({ id: 10, type: 'user' }))).toBe( 'Alice', ); expect(Optional.unwrap(transformed.get({ id: 20, type: 'admin' }))).toBe( 'Bob', ); }); }); describe('IMapMapped.mapEntries', () => { test('should transform both keys and values', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const transformed = map.mapEntries(([key, value]) => [ { ...key, id: key.id * 10 }, value.toUpperCase(), ]); expect(Optional.unwrap(transformed.get({ id: 10, type: 'user' }))).toBe( 'ALICE', ); expect(Optional.unwrap(transformed.get({ id: 20, type: 'admin' }))).toBe( 'BOB', ); }); }); describe('IMapMapped.forEach', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); const keys = [{ v: 1 }, { v: 2 }, { v: 3 }]; const values = ['1', '2', '3']; for (const [key, value] of s0.entries()) { expect(keys).toContainEqual(key); expect(values).toContainEqual(value); } }); test('should execute callback for each element', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const mut_collected: [TestKey, string][] = []; for (const [key, value] of map.entries()) { mut_collected.push([key, value]); } expect(mut_collected).toHaveLength(2); expect(mut_collected).toContainEqual([{ id: 1, type: 'user' }, 'Alice']); expect(mut_collected).toContainEqual([{ id: 2, type: 'admin' }, 'Bob']); }); }); describe('IMapMapped.keys', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); const keys = [{ v: 1 }, { v: 2 }, { v: 3 }]; for (const k of s0.keys()) { expect(keys).toContainEqual(k); } }); }); describe('IMapMapped.values', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); const values = ['1', '2', '3']; for (const v of s0.values()) { expect(values).toContainEqual(v); } }); }); describe('IMapMapped.entries', () => { test('case 1', () => { const s0 = IMapMapped.create( [ [{ v: 1 }, '1'], [{ v: 2 }, '2'], [{ v: 3 }, '3'], ], toKey, fromKey, ); const keys = [{ v: 1 }, { v: 2 }, { v: 3 }]; const values = ['1', '2', '3']; for (const [k, v] of s0.entries()) { expect(keys).toContainEqual(k); expect(values).toContainEqual(v); } }); }); describe('IMapMapped.toKeysArray', () => { test('should return array of original keys', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const keys = map.toKeysArray(); expect(keys).toHaveLength(2); expect(keys).toContainEqual({ id: 1, type: 'user' }); expect(keys).toContainEqual({ id: 2, type: 'admin' }); }); }); describe('IMapMapped.toValuesArray', () => { test('should return array of values', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const values = map.toValuesArray(); expect(values).toHaveLength(2); expect(values).toContain('Alice'); expect(values).toContain('Bob'); }); }); describe('IMapMapped.toEntriesArray', () => { test('should return array of entries', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const entries = map.toEntriesArray(); expect(entries).toHaveLength(2); expect(entries).toContainEqual([{ id: 1, type: 'user' }, 'Alice']); expect(entries).toContainEqual([{ id: 2, type: 'admin' }, 'Bob']); }); }); describe('IMapMapped.toArray', () => { test('should be alias for toEntriesArray', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const entries = map.toArray(); const entriesArray = map.toEntriesArray(); assert.deepStrictEqual(entries, entriesArray); }); }); describe('IMapMapped.toRawMap', () => { test('should return underlying map with transformed keys', () => { const map = IMapMapped.create( [ [{ id: 1, type: 'user' }, 'Alice'], [{ id: 2, type: 'admin' }, 'Bob'], ], testKeyToString, stringToTestKey, ); const rawMap = map.toRawMap(); expect(rawMap.size).toBe(2); expect(rawMap.get('user_1')).toBe('Alice'); expect(rawMap.get('admin_2')).toBe('Bob'); }); });