{"id":15436907,"url":"https://github.com/tangentlin/indexed-collection","last_synced_at":"2025-04-19T18:25:59.104Z","repository":{"id":65200772,"uuid":"576994734","full_name":"tangentlin/indexed-collection","owner":"tangentlin","description":"A zero-dependency library of classes that make filtering, sorting and observing changes to arrays easier and more efficient.","archived":false,"fork":false,"pushed_at":"2024-03-22T15:46:21.000Z","size":521,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-30T11:03:21.449Z","etag":null,"topics":["collectionview","data","data-indexing","index","javascript","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tangentlin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-11T16:46:43.000Z","updated_at":"2023-01-31T09:40:51.000Z","dependencies_parsed_at":"2025-03-02T20:41:09.521Z","dependency_job_id":null,"html_url":"https://github.com/tangentlin/indexed-collection","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tangentlin%2Findexed-collection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tangentlin%2Findexed-collection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tangentlin%2Findexed-collection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tangentlin%2Findexed-collection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tangentlin","download_url":"https://codeload.github.com/tangentlin/indexed-collection/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249762642,"owners_count":21321968,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collectionview","data","data-indexing","index","javascript","typescript"],"created_at":"2024-10-01T18:54:01.799Z","updated_at":"2025-04-19T18:25:59.074Z","avatar_url":"https://github.com/tangentlin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![NPM version](https://img.shields.io/npm/v/indexed-collection.svg?style=flat)](https://www.npmjs.com/package/indexed-collection)\n![NPM license](https://img.shields.io/npm/l/indexed-collection.svg?style=flat)\n[![codecov](https://codecov.io/gh/tangentlin/indexed-collection/graph/badge.svg?token=87S67J5BLV)](https://codecov.io/gh/tangentlin/indexed-collection)\n\n# indexed-collection\n\nA zero-dependency library of classes that make filtering, sorting and observing changes to arrays easier and more efficient.\n\n## Getting Started\n\nInstallation\n\n```shell\n#npm\nnpm install indexed-collection\n\n# yarn\nyarn add indexed-collection\n```\n\n### Example\n\nThe following contrived example that walks through some basic features of Collect, CollectionView and Index.\n\nAssuming we have a community of people that consists of the following fields\n\n- name\n- age\n- gender\n- hobbies\n\nThe community want to keep statistics by gender initially for everybody. Let's build a collection\nthat indexes on gender\n\n```JavaScript\nimport { IndexedCollectionBase, CollectionIndex, CollectionViewBase } from 'indexed-colection';\n\nclass PeopleCollection extends IndexedCollectionBase {\n  construtor() {\n    super();\n    this.genderIndex = new CollectionIndex([(person) =\u003e person.gender]);\n    this.buildIndexes([\n      this.genderIndex,\n    ]);\n  }\n\n  byGender(gender) {\n    return this.genderIndex.getValue(gender);\n  }\n}\n```\n\nNext, let's add some people\n\n```JavaScript\n\nconst people = new PeopleCollection();\npeople.add({ name: 'John', gender: 'male', age: 70, hobbies: ['fishing', 'hiking'] });\npeople.add({ name: 'Mary', gender: 'female', age: 50, hobbies: ['hiking', 'bowling'] });\npeople.add({ name: 'Chris', gender: 'male', age: 35, hobbies: ['hiking', 'kayaking'] });\npeople.add({ name: 'Ana', gender: 'femal', age: 32, hobbies: ['bowling', 'kayaking'] });\n\n// let's report\npeople.count; // 4\npeople.items; // [John, Mary, Chris, Ana]\npeople.byGender('male');  // [John, Chris]\npeople.byGender('female'); // [Mary, Ana]\n```\n\nBesides tracking all the people in the community, the community also want to track all the seniors (age 65 or above). Given\nthis collection is a subset of the `PeopleCollect`, we can utilize the CollectionView, which is a readonly collection that\nderives values from a base collection.\n\n```JavaScript\n\nclass SeniorCollection {\n  constructor(baseCollection) {\n    super(baseCollection, {\n      filter: (person) =\u003e person.age \u003e= 65\n    })\n  }\n\n  byGender(gender) {\n    return super.applyFilterAndSort(\n      this.source.byGender(gender)\n    );\n  }\n}\n\n\nconst seniors = new SeniorCollection(people);\nseniors.count; // 1\nseniors.byGender('male');  // [John]\nseniors.byGender('female'); // [] No-one\n\n```\n\n#### Handling changes\n\nThe beauty of Collection and CollectionView is it can handle data changes. For example, let's add a new people to the community\n\n```JavaScript\npeople.add({ name: 'Betty', age: 68, gender: 'female', hobbies: ['fishing'] });\n\npeople.count // 5\npeople.byGender('female'); // Mary, Ana, Betty\n\n// The collection view is also updated because it stays in sync with its base collection\nseniors.count // 2\nseniors.byGender('female'); // [Betty]\n```\n\n#### More advanced indexes\n\nThe community want to report on people's hobbies, and also want to report on hobbies \u0026 gender as well.\n\nTo do so, we need to add two indexes, one for hobbies, and one for both hobbies and gender.\n\n```JavaScript\n\n// Revise within PeopleCollection's constructor\nconst getHobby = (person) =\u003e person.hobbies;\ngetHobby.isMultiple = true;  // isMultiple = true indicates the value extracted is an array or a set of values\n\nthis.hobbyIndex = new CollectionIndex([getBobby]);\nthis.genderAndHobbyIndex = new CollectionIndex( [person =\u003e person.gender, getHobby] );\n\nthis.buildIndexes([\n  this.genderIndex,\n  this.hobbyIndex,\n  this.genderAndHobbyIndex,\n]);\n```\n\n```JavaScript\n// Add byHobby and byGenderAndHobby methods to PeopleCollection for ease of access\nbyHobby(hobby) {\n  return this.hobbyIndex.getValue(hobby);\n}\n\nbyGenderAndHobby(gender, hobby) {\n  return this.hobbyIndex.getValue(gender, hobby);\n}\n```\n\n```JavaScript\npeople.byHobby('fishing'); // [John, Betty]\npeople.byHooby('hiking'); // [John, Mary, Chris]\npeople.byGenderAndHobby('male', 'hiking'); // [John, Chris]\n```\n\nNext, let's propagate the byHobby and byGenderAndHobby to the collection SeniorCollection class\n\n```JavaScript\nbyHobby(hobby) {\n  return super.applyFilterAndSort(\n    this.source.byHobby(hobby)\n  );\n}\n\nbyGenderAndHobby(gender, hobby) {\n  return super.applyFilterAndSort(\n    this.source.byGenderAndHobby(gender, hobby)\n  );\n}\n```\n\n```JavaScript\nseniors.byHobby('finshing'); // [John, Betty]\nseniors.byGenderAndHobby('hiking'); // [John]\n```\n\n## In-depth\n\n### Concepts\n\nIndexed collection consists of three key parts: index, collection, and view.\n\n#### Index\n\nIndex defines how a collection should be indexed for each retrieval. A common use case of index\nwould be indexing on a field of each line item, thus the index would group items by the value of\nthe field. To create an index, use the CollectionIndex class.\n\nFor example\n\n```typescript\n// JavaScript\nconst byGenderIndex = new CollectionIndex( [ (person) =\u003e person.gender ]);\n\n// TypeScript\nconst byGenderIndex: \u003cIPerson, [string]\u003e = new CollectionIndex( [ (person) =\u003e person.gender ]);\n```\n\n`CollectionIndex` supports multiple level of indexes. Multiple level\nindexes are useful when values need to be further grouped into levels of subgroups.\n\nFor example, if we wish to group people by gender then by age, the index can look like\n\n```typescript\n// JavaScript\nconst byGenderAndAgeIndex = new CollectionIndex( [ (person) =\u003e person.gender, (person) =\u003e person.age ]);\n\n// TypeScript\nconst byGenderAndAgeIndex: \u003cIPerson, [string, number]\u003e = new CollectionIndex( [ (person) =\u003e person.gender, (person) =\u003e person.age ]);\n```\n\n##### Many-to-many relationship\n\nSometimes, and field in each item may consist of an array or set of values, we can annotate\nthe index with `isMultiple=true`, so each value of the field become a key in the index, thus it\ncreates a many-to-many relationship.\n\nFor example, in the [example](#example) above, each person has multiple hobbies, and multiple people\ncan have the same hobby, therefore hobby and person has many-to-many relationship, so to index\nthe hobby, `isMultiple=true` is needed.\n\n```JavaScript\n// JavaScript\nconst getHobbies = (person) =\u003e person.hobbies;\ngetHobbies.isMultiple = true;\nconst byHobbyIndex = new CollectionIndex( getHobbies );\n\n// TypeScript\nconst getHobbies: MultipleKeyExtract\u003cIPerson, string\u003e = (person) =\u003e person.hobbies;\ngetHobbies.isMultiple = true;\nconst byHobbyIndex = new CollectionIndex\u003cIPerson, [string]\u003e( getHobbies );\n```\n\nValue of index is not limited to number or string, it can be anything in JavaScript. Keep\nin mind value comparison is index is strict equal.\n\nAdditionally, one can also use index to do advanced indexing such as value bucketing, for example\npeople can be indexed/grouped by age range (10-19, 20-19, ... etc), please see [Advanced Topics](#advanced-topics)\nfor more details.\n\n### Collection\n\nCollection provides way to add, remove and retrieve items. Each collection can consist of\nzero to many indexes. Underneath the hood, the collection would orchestrate all the indexes\nwhen items are added or removed.\n\nTo create a collection, one would need to create a class that extends either `IndexedCollectionBase`\nor `PrimaryKeyCollection`. `IndexedCollectionBase` identifies duplicates by performing\nstrict equality of each item added to the collection; `PrimaryKeyCollection` identifies duplicates by\nperforming strict equality of each item's primary key value such as the ID value of an item.\n\nA typical Collection class would consist of the following elements\n\n- Constructor\n  - Initial values (optional)\n  - Define indexes as fields\n  - Call super.buildIndexes() with the defined indexes\n  - Call addRange() to add initial values\n- Helper methods to extract values (optional but recommended) from indexes\n\nFor example, the PeopleCollection class in the [example](#example) provides a typical JavaScript example,\na TypeScript equivalent would be as the following,\n\n```typescript\nimport { CollectionIndex } from './CollectionIndex';\nimport { MultipleKeyExtract } from './KeyExtract';\nimport { IndexedCollectionBase } from './IndexedCollectionBase';\n\nclass PeopleCollection extends IndexedCollectionBase\u003cIPerson\u003e {\n  private readonly byGenderIndex: CollectionIndex\u003cIPerson, [string]\u003e;\n  private readonly byHobbyIndex: CollectionIndex\u003cIPerson, [string]\u003e;\n  private readonly byGenderAndHobbyIndex: CollectionIndex\u003c\n    IPerson,\n    [string, string]\n  \u003e;\n\n  constructor(initialValues?: readonly IPerson[]) {\n    super();\n\n    const getGender = (person: IPerson) =\u003e person.gender;\n    const getHobbies: MultipleKeyExtract\u003cIPeson, string\u003e = (person: IPerson) =\u003e\n      person.hobbies;\n    getHobbies.isMultiple = true;\n\n    this.byGenderIndex = new CollectionIndex\u003cIPerson, [string]\u003e([getGender]);\n    this.byHobbyIndex = new CollectionIndex\u003cIPerson, [string]\u003e([getHobbies]);\n    this.byGenderAndHobbyIndex = new CollectionIndex\u003cIPerson, [string]\u003e([\n      getGender,\n      getHobbies,\n    ]);\n\n    this.buildIndexes([\n      this.byGenderIndex,\n      this.byHobbyIndex,\n      this.byGenderAndHobbyIndex,\n    ]);\n\n    if (initialValues) {\n      this.addRange(initialValues);\n    }\n  }\n\n  // Helper methods to help extract values from indexes\n  byGender(gender: string): readonly IPerson[] {\n    return this.byGenderIndex.getValues(gender);\n  }\n\n  byHobby(hobby: string): readonly IPerson[] {\n    return this.byHobbyIndex.getValues(gender);\n  }\n\n  byGenderAndHobby(gender: string, hobby: string): readonly IPerson[] {\n    return this.byGenderAndHobbyIndex.getValues(gender, hobby);\n  }\n}\n```\n\nTo create collection based on PrimaryKeyCollection, a function that extracts the id from each item would\nneed to be provided in the constructor. For example\n\n```typescript\n// Assuming each IPerson is identified by a unique SSN which is a number\nclass PeopleCollection extends PrimaryKeyCollection\u003cIPerson, number\u003e {\n  constructor(initialValues?: readonly IPerson[]) {\n    super((person: IPerson) =\u003e person.ssn);\n\n    // Additional indexes similar to IndexedCollectionBased example above\n  }\n}\n```\n\nCollection also support other features such as change observation etc, please see [Advanced Topics](#advanced-topics)\nfor more details.\n\n### View\n\nIf a Collection is seen as a table in a relational database, a CollectionView is similar to View in the relational\ndatabase as well. A view is a read-only version of a collection with values filtered and sorted.\n\nA view has to depend on a source collection or CollectionView, and any changes on sourced collection would immediately\nimpact the views output.\n\nTo define a view, one would need to create a class based on `CollectionViewBase` class with the following\nelements:\n\n- Constructor\n  - Pass in an instance of collection as the data source of the view\n  - Define filter, sort or both for the video\n    - Filter has the same signature as [Array.filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)\n    - Sort has the same signature as [Array.sort compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)\n- Helper methods to extract values\n  - These helper methods may mirror source collection's helper methods\n  - Each helper method involves calling `this.applyFilterandSort( parent.helperMethod )`\n\nFor example\n\n```typescript\nclass SeniorPeopleView extends CollectionViewBase\u003cIPerson, PeopleCollection\u003e {\n  constructor(source: PeopleCollection) {\n    super(source, {\n      filter: (person: IPerson) =\u003e person.age \u003e= 65,\n\n      // Always sort by age in ascending order\n      sort: (a: IPerson, b: IPerson) =\u003e a.age - b.age,\n    });\n  }\n\n  byGender(gender: string): readonly IPerson[] {\n    return this.applyFilterAndSort(this.source.byGender(gender));\n  }\n\n  // byHobby, byGenderAndHobby are similar to byGender\n}\n```\n\n#### Nested Collection view\n\nA collection view can be nested from another view. This can be used for representing further\ndata subset. For example, `SeniorFemalePeopleView` can be sourced from `SeniorPeopleView`, for example\n\n```typescript\nclass SeniorPeopleView extends CollectionViewBase\u003cIPerson, SeniorPeopleView\u003e {\n  constructor(source: SeniorPeopleView) {\n    super(source, {\n      // Note that filter does not need to define the age constrain\n      filter: (person: IPerson) =\u003e person.gender === 'female',\n    });\n  }\n\n  byGender(gender: string): readonly IPerson[] {\n    return this.applyFilterAndSort(this.source.byGender(gender));\n  }\n\n  // byHobby, byGenderAndHobby are similar to byGender\n}\n```\n\nNote that filter is nested when a view is sourced from another view. However, sorting is not nested,\neach view has to manage its own sort order. If sort is undefined, the view would inherit the natural\norder from its source.\n\n## Advanced Topics\n\nComing soon.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftangentlin%2Findexed-collection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftangentlin%2Findexed-collection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftangentlin%2Findexed-collection/lists"}