{"id":21259463,"url":"https://github.com/ori88c/non-replacement-random-item-sampler","last_synced_at":"2026-01-26T21:52:38.210Z","repository":{"id":262149125,"uuid":"886053796","full_name":"ori88c/non-replacement-random-item-sampler","owner":"ori88c","description":"An efficient random item sampler that ensures O(1) sampling complexity, and equal selection probability for all items across cycles. Each cycle ensures unique, non-repeating item selections, with each item sampled only once per cycle. Upon cycle completion, the sampler automatically refreshes, initiating a new cycle to repeat the process.","archived":false,"fork":false,"pushed_at":"2024-11-10T04:13:03.000Z","size":48,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-12T09:43:46.980Z","etag":null,"topics":["constant-time","cycles","efficient-sampling","no-repetitions","non-replacement","random","random-element","random-item","random-permutations","random-sample","random-samples","random-select","random-selection","random-selector","random-shuffle","rounds","sampler","sampling","shuffle"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ori88c.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":"2024-11-10T04:09:01.000Z","updated_at":"2024-11-10T20:36:31.000Z","dependencies_parsed_at":"2024-11-10T20:47:05.711Z","dependency_job_id":"2d306655-b5c3-4843-b7b4-acee5b6b8809","html_url":"https://github.com/ori88c/non-replacement-random-item-sampler","commit_stats":null,"previous_names":["ori88c/non-replacement-random-item-sampler"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ori88c/non-replacement-random-item-sampler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ori88c%2Fnon-replacement-random-item-sampler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ori88c%2Fnon-replacement-random-item-sampler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ori88c%2Fnon-replacement-random-item-sampler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ori88c%2Fnon-replacement-random-item-sampler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ori88c","download_url":"https://codeload.github.com/ori88c/non-replacement-random-item-sampler/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ori88c%2Fnon-replacement-random-item-sampler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28789224,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-26T21:49:50.245Z","status":"ssl_error","status_checked_at":"2026-01-26T21:48:29.455Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["constant-time","cycles","efficient-sampling","no-repetitions","non-replacement","random","random-element","random-item","random-permutations","random-sample","random-samples","random-select","random-selection","random-selector","random-shuffle","rounds","sampler","sampling","shuffle"],"created_at":"2024-11-21T04:14:11.705Z","updated_at":"2026-01-26T21:52:38.183Z","avatar_url":"https://github.com/ori88c.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch2 align=\"middle\"\u003eNon-Replacement Random Item Sampler\u003c/h2\u003e\n\nThe `NonReplacementRandomSampler` class implements a random item sampler that ensures equal selection probability for all items across cycles. Each cycle guarantees unique, non-repeating item selections, with each item sampled only **once per cycle**. Upon cycle completion — when all items have been selected — the sampler automatically refreshes, seamlessly initiating a new cycle to repeat the process.\n\nUse case examples include:\n* __Customer Feedback Surveys with Limited Fatigue__: Suppose a company has a pool of 1000 customers and wants to conduct feedback surveys over time without exhausting the same group of customers repeatedly. Using `NonReplacementRandomSampler`, each survey cycle ensures that each customer is sampled only once, minimizing survey fatigue within the cycle. When the cycle is exhausted (after surveying all 1000 customers), a new cycle begins, allowing the company to start another round of sampling while still ensuring each customer is only surveyed once per cycle. This approach **maintains variety** and minimizes the risk of \"survey burnout\" in any particular customer.\n* __Randomized Load Testing of Distributed Servers__: In a distributed server environment with 50 servers, a company wants to run randomized load tests to simulate production traffic without overloading the same servers repeatedly. Using `NonReplacementRandomSampler`, each server is selected once per testing cycle, ensuring balanced load testing across all servers. When all servers have been tested, a new cycle automatically begins, allowing for continued load testing across the server pool without disproportionate load on any individual server within a cycle.\n\n## Table of Contents :bookmark_tabs:\n\n* [Key Features](#key-features)\n* [API](#api)\n* [Getter Methods](#getter-methods)\n* [Use Case Example: Randomized Playlist Generator](#use-case-example)\n* [Algorithm](#algorithm)\n* [License](#license)\n\n## Key Features :sparkles:\u003ca id=\"key-features\"\u003e\u003c/a\u003e\n\n- __Random Sampling without Repetitions__: The sampler’s internal state is tracked by a cycle number, starting at 1. Each cycle guarantees unique, non-repeating item selections, with each item sampled only **once per cycle**. Upon cycle completion — when all items have been selected — the sampler automatically refreshes, seamlessly initiating a new cycle to repeat the process.\n- __Manually Trigger a New Cycle__: The `startNewCycle` method initiates a new cycle, making all input items available again for sampling. This is useful in scenarios where the non-replacement attribute is not required for extended periods, allowing a new cycle to begin without waiting for full cycle exhaustion.\n- __Sample Multiple Unique Items at Once__: The `sampleGroupFromCycle` method samples multiple **unique** items from the current cycle in a single operation.\n- __Efficiency :gear:__: All methods have time and space complexities of O(1), except for `sampleGroupFromCycle`, where both time and space complexities are O(groupSize).\n- __Comprehensive documentation :books:__: The class is thoroughly documented, enabling IDEs to provide helpful tooltips that enhance the coding experience.\n- __Tests :test_tube:__: **Fully covered** by comprehensive unit tests.\n- **TypeScript** support.\n- No external runtime dependencies: Only development dependencies are used.\n- ES2020 Compatibility: The `tsconfig` target is set to ES2020, ensuring compatibility with ES2020 environments.\n\n## API :globe_with_meridians:\u003ca id=\"api\"\u003e\u003c/a\u003e\n\nThe `NonReplacementRandomSampler` class provides the following methods:\n\n* __sample__: Returns a random item from the remaining items in the current cycle. In other words, an item that has not been sampled yet in the current cycle.\n* __sampleGroupFromCycle__: Returns an array containing `groupSize` unique items from the current cycle, i.e., items that have not been previously sampled in the current cycle. To avoid ambiguity, these items will also *not* be sampled again in the same cycle. It is equivalent to executing the `sample` method multiple times, with the difference that the group size is limited by the number of remaining items in the cycle. This distinction is necessary to ensure a *unique* group of items.\n* __startNewCycle__: Manually initiates a new cycle, making all input items available again for sampling.\n\nIf needed, refer to the code documentation for a more comprehensive description.\n\n## Getter Methods :mag:\u003ca id=\"getter-methods\"\u003e\u003c/a\u003e\n\nThe `NonReplacementRandomSampler` class provides the following getter methods to reflect the current state:\n\n* __size__: The total number of items available for sampling. Remains constant throughout the instance’s lifespan.\n* __currentCycle__: The current cycle number. The cycle number increments in either of the following cases: Either all items in the current cycle have been sampled (cycle exhaustion), **or** the `startNewCycle` method is manually triggered.\n* __remainingItemsInCurrentCycle__: The number of items not yet sampled in the current cycle.\n\nTo eliminate any ambiguity, all getter methods have **O(1)** time and space complexity.\n\n## Use Case Example: Randomized Playlist Generator :man_technologist:\u003ca id=\"use-case-example\"\u003e\u003c/a\u003e\n\nConsider a component responsible for selecting songs randomly for a playlist. Once all songs in the list have been played, the component automatically begins a new cycle, reshuffling the songs for fresh playback.\n\n```ts\nimport { NonReplacementRandomSampler } from 'non-replacement-random-item-sampler';\n\ninterface Song {\n  id: string;\n  title: string;\n  artist: string;\n  durationInSeconds: number;\n  genre: string;\n  releaseYear: number;\n  azureStorageUri: string; // URI to access the song file in Azure Storage\n}\n\nclass RandomizedPlaylistGenerator {\n  private readonly _songSampler: NonReplacementRandomSampler\u003cSong\u003e;\n\n  constructor(songs: Song[]) {\n    // Note: Refer to the Ownership Transfer section in the NonReplacementRandomSampler\n    // constructor documentation.\n    this._songSampler = new NonReplacementRandomSampler(songs);\n  }\n\n  public get remainingSongsUntilNextShuffle(): number {\n    return this._songSampler.remainingItemsInCurrentCycle;\n  }\n\n  public sampleNext(): Song {\n    return this._songSampler.sample();\n  }\n\n  public sampleGroupFromPlaylist(groupSize: number): Song[] {\n    return this._songSampler.sampleGroupFromCycle(groupSize);\n  }\n\n  public shuffle(): void {\n    this._songSampler.startNewCycle();\n  }\n}\n```\n\n## Algorithm :gear:\u003ca id=\"algorithm\"\u003e\u003c/a\u003e\n\nThe sampler’s non-replacement requirement presents a complexity challenge, as using array-mutating methods like `Array.splice` would result in an undesirable O(n) complexity per sample.\n\nTo address this, we conceptually divide the internal items array into two distinct parts:\n* __Available Prefix__: A prefix of items that are still available for sampling in the current cycle.\n* __Unavailable Suffix__: A suffix of items that have already been sampled within the current cycle.\n\nThe algorithm samples a random index from the Available Prefix and **swaps** it with the **new** start of the Unavailable Suffix. Each sample operation therefore **decreases** the length of the prefix by 1 and **increases** the length of the suffix by 1. As a result, each sampled item is excluded from further selections during the current cycle.\n\nFor example, consider an array `[A, B, C, D]`. Initially, the Available Prefix length is 4, allowing us to sample a random index within [0, 3]. Suppose we sample index 1 (item `B`). We then swap `B` with `D`, transforming the array to `[A, D, C, B]` and reducing the Available Prefix length to 3. Now, `B` is in the Unavailable Suffix, preventing it from being selected again within this cycle.\n\nOn the next sampling attempt, a random index within [0, 2] is chosen, say 0 (item `A`). We swap `A` with `C`, resulting in `[C, D, A, B]`. Now both `A` and `B` are in the Unavailable Suffix, ensuring neither is sampled again during the current cycle. This process continues until all items are exhausted from the Available Prefix, completing the cycle.\n\n## License :scroll:\u003ca id=\"license\"\u003e\u003c/a\u003e\n\n[Apache 2.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fori88c%2Fnon-replacement-random-item-sampler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fori88c%2Fnon-replacement-random-item-sampler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fori88c%2Fnon-replacement-random-item-sampler/lists"}