├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── linq-tests.web.js ├── mocha.html ├── package.json ├── src ├── Collections.ts ├── Comparers.ts ├── Enumerables.ts ├── Iterators.ts ├── Linq.ts ├── Types.ts └── Utils.ts ├── test ├── Test.ts ├── TestSuite.ts ├── integration │ └── IEnumerable.test.ts └── unitary │ ├── Dictionary.test.ts │ ├── Enumerable.test.ts │ ├── IQueryable.test.ts │ ├── Iterator.test.ts │ ├── List.test.ts │ ├── Stack.test.ts │ └── Utils.test.ts ├── tsconfig.json └── tslint.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have CRLF line endings on checkout. 5 | * text eol=crlf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | build/ 7 | .vscode/ 8 | assets/ 9 | 10 | *.csproj 11 | *.js 12 | *.map 13 | *.config 14 | *.html 15 | 16 | # User-specific files 17 | *.suo 18 | *.user 19 | *.userosscache 20 | *.sln.docstates 21 | 22 | # User-specific files (MonoDevelop/Xamarin Studio) 23 | *.userprefs 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | 37 | # Visual Studio 2015 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUNIT 47 | *.VisualState.xml 48 | TestResult.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # .NET Core 56 | project.lock.json 57 | project.fragment.lock.json 58 | artifacts/ 59 | **/Properties/launchSettings.json 60 | 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # TFS 2012 Local Workspace 107 | $tf/ 108 | 109 | # Guidance Automation Toolkit 110 | *.gpState 111 | 112 | # ReSharper is a .NET coding add-in 113 | _ReSharper*/ 114 | *.[Rr]e[Ss]harper 115 | *.DotSettings.user 116 | 117 | # JustCode is a .NET coding add-in 118 | .JustCode 119 | 120 | # TeamCity is a build add-in 121 | _TeamCity* 122 | 123 | # DotCover is a Code Coverage Tool 124 | *.dotCover 125 | 126 | # Visual Studio code coverage results 127 | *.coverage 128 | *.coveragexml 129 | 130 | # NCrunch 131 | _NCrunch_* 132 | .*crunch*.local.xml 133 | nCrunchTemp_* 134 | 135 | # MightyMoose 136 | *.mm.* 137 | AutoTest.Net/ 138 | 139 | # Web workbench (sass) 140 | .sass-cache/ 141 | 142 | # Installshield output folder 143 | [Ee]xpress/ 144 | 145 | # DocProject is a documentation generator add-in 146 | DocProject/buildhelp/ 147 | DocProject/Help/*.HxT 148 | DocProject/Help/*.HxC 149 | DocProject/Help/*.hhc 150 | DocProject/Help/*.hhk 151 | DocProject/Help/*.hhp 152 | DocProject/Help/Html2 153 | DocProject/Help/html 154 | 155 | # Click-Once directory 156 | publish/ 157 | 158 | # Publish Web Output 159 | *.[Pp]ublish.xml 160 | *.azurePubxml 161 | # TODO: Comment the next line if you want to checkin your web deploy settings 162 | # but database connection strings (with potential passwords) will be unencrypted 163 | *.pubxml 164 | *.publishproj 165 | 166 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 167 | # checkin your Azure Web App publish settings, but sensitive information contained 168 | # in these scripts will be unencrypted 169 | PublishScripts/ 170 | 171 | # NuGet Packages 172 | *.nupkg 173 | # The packages folder can be ignored because of Package Restore 174 | **/packages/* 175 | # except build/, which is used as an MSBuild target. 176 | !**/packages/build/ 177 | # Uncomment if necessary however generally it will be regenerated when needed 178 | #!**/packages/repositories.config 179 | # NuGet v3's project.json files produces more ignorable files 180 | *.nuget.props 181 | *.nuget.targets 182 | 183 | # Microsoft Azure Build Output 184 | csx/ 185 | *.build.csdef 186 | 187 | # Microsoft Azure Emulator 188 | ecf/ 189 | rcf/ 190 | 191 | # Windows Store app package directories and files 192 | AppPackages/ 193 | BundleArtifacts/ 194 | Package.StoreAssociation.xml 195 | _pkginfo.txt 196 | 197 | # Visual Studio cache files 198 | # files ending in .cache can be ignored 199 | *.[Cc]ache 200 | # but keep track of directories ending in .cache 201 | !*.[Cc]ache/ 202 | 203 | # Others 204 | ClientBin/ 205 | ~$* 206 | *~ 207 | *.dbmdl 208 | *.dbproj.schemaview 209 | *.jfm 210 | *.pfx 211 | *.publishsettings 212 | orleans.codegen.cs 213 | 214 | # Since there are multiple workflows, uncomment next line to ignore bower_components 215 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 216 | #bower_components/ 217 | 218 | # RIA/Silverlight projects 219 | Generated_Code/ 220 | 221 | # Backup & report files from converting an old project file 222 | # to a newer Visual Studio version. Backup files are not needed, 223 | # because we have git ;-) 224 | _UpgradeReport_Files/ 225 | Backup*/ 226 | UpgradeLog*.XML 227 | UpgradeLog*.htm 228 | 229 | # SQL Server files 230 | *.mdf 231 | *.ldf 232 | *.ndf 233 | 234 | # Business Intelligence projects 235 | *.rdl.data 236 | *.bim.layout 237 | *.bim_*.settings 238 | 239 | # Microsoft Fakes 240 | FakesAssemblies/ 241 | 242 | # GhostDoc plugin setting file 243 | *.GhostDoc.xml 244 | 245 | # Node.js Tools for Visual Studio 246 | .ntvs_analysis.dat 247 | node_modules/ 248 | 249 | # Typescript v1 declaration files 250 | typings/ 251 | 252 | # Visual Studio 6 build log 253 | *.plg 254 | 255 | # Visual Studio 6 workspace options file 256 | *.opt 257 | 258 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 259 | *.vbw 260 | 261 | # Visual Studio LightSwitch build output 262 | **/*.HTMLClient/GeneratedArtifacts 263 | **/*.DesktopClient/GeneratedArtifacts 264 | **/*.DesktopClient/ModelManifest.xml 265 | **/*.Server/GeneratedArtifacts 266 | **/*.Server/ModelManifest.xml 267 | _Pvt_Extensions 268 | 269 | # Paket dependency manager 270 | .paket/paket.exe 271 | paket-files/ 272 | 273 | # FAKE - F# Make 274 | .fake/ 275 | 276 | # JetBrains Rider 277 | .idea/ 278 | *.sln.iml 279 | 280 | # CodeRush 281 | .cr/ 282 | 283 | # Python Tools for Visual Studio (PTVS) 284 | __pycache__/ 285 | *.pyc 286 | 287 | # Cake - Uncomment if you are using it 288 | # tools/** 289 | # !tools/packages.config 290 | 291 | # Telerik's JustMock configuration file 292 | *.jmconfig 293 | 294 | # BizTalk build output 295 | *.btp.cs 296 | *.btm.cs 297 | *.odx.cs 298 | *.xsd.cs 299 | coverage/coverage-final.json 300 | .nyc_output/ 301 | boost.bat 302 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "node" 5 | install: 6 | - npm install 7 | script: 8 | - npm test 9 | after_success: 10 | - npm run report-coverage 11 | deploy: 12 | skip_cleanup: true 13 | provider: npm 14 | email: "ivansanzcarasa@gmail.com" 15 | api_key: $NPM_TOKEN 16 | on: 17 | branch: master 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renderWhitespace": "all", 3 | "editor.wordWrap": "on", 4 | "editor.renderControlCharacters": true, 5 | "typescript.format.placeOpenBraceOnNewLineForFunctions": true, 6 | "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true, 7 | "javascript.format.placeOpenBraceOnNewLineForFunctions": true, 8 | "typescript.implementationsCodeLens.enabled": true, 9 | "editor.quickSuggestionsDelay": 0, 10 | "typescript.referencesCodeLens.enabled": true, 11 | "files.exclude": { 12 | "**/.git": true, 13 | "**/*.map": true, 14 | "**/node_modules": true, 15 | "**/.nyc_output": true, 16 | "**/coverage": true, 17 | "**/etc": true 18 | }, 19 | "files.insertFinalNewline": true 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Iván Sanz Carasa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linq-Collections: (IEnumerable, ...) + (List, Dictionary, ...) 2 | 3 | [![npm version](https://img.shields.io/npm/v/linq-collections.svg)](https://npmjs.org/package/linq-collections) 4 | [![npm downloads](https://img.shields.io/npm/dt/linq-collections.svg)](https://npmjs.org/package/linq-collections) 5 | [![build status](https://travis-ci.org/isc30/linq-collections.svg?branch=master)](https://travis-ci.org/isc30/linq-collections) 6 | [![coverage](https://coveralls.io/repos/github/isc30/linq-collections/badge.svg?branch=master&cache=no)](https://coveralls.io/github/isc30/linq-collections?branch=master) 7 | [![](https://img.shields.io/badge/click-run%20browser%20tests-orange.svg)](https://isc30.github.io/linq-collections/mocha) 8 | 9 | 10 | Strongly typed *Linq* implementation for *Javascript* and *TypeScript* (*ES5*, *ES6*, +)
11 | Includes collections (+ readonly versions): List, Dictionary, Stack, ... 12 | 13 | ## Current Stable Version 14 | https://github.com/isc30/linq-collections
15 | This project was developed by Ivan Sanz (isc30)
16 | 17 | The project is already finished, yet some features are missing. If you want to contribute with any of these, please check the [Development status and missing features list](https://github.com/isc30/linq-collections/projects/1)
18 | I will be happy to accept pull requests :D 19 | 20 | [![](https://img.shields.io/badge/click-run%20browser%20tests-orange.svg)](https://isc30.github.io/linq-collections/mocha) 21 | 22 | ## Intellisense friendly 23 | Every single method has **complete** type definitions available.
24 | If you use TypeScript, its purely is based in **generics**.

25 | [Insert motivational GIF with intellisense in action] 26 | 27 | ## Browser compatibility: 100% 28 | Using **ES5**, it has **100% compatibility** with nodejs and all main browsers (+mobile)
29 | Check your browser now if you don't believe it -> [![](https://img.shields.io/badge/click-run%20browser%20tests-orange.svg)](https://isc30.github.io/linq-collections/mocha) 30 | 31 | ## Performance 32 | *Linq-Collections* uses custom **iterators** and **deferred execution** mechanisms that ensure **BLAZING FAST** operations, outperforming any other popular library. Its also optimized to work with **minimal CPU and RAM usage**. 33 | 34 | ## Why use it? 35 | If previous reasons aren't enought, here are few more: 36 | - **Javascript && TypeScript compatible** - You can use it with JS or TypeScript (contains .d.ts definitions) 37 | - **No dependencies** - Pure and lightweight 38 | - **100% browser/nodejs support** - Stop caring about compatibility, it works everywhere! 39 | - **Strongly typed** - Developed in TypeScript, it uses no 'any' or dirty code. Everything is based in generics and strongly typed 40 | - **Best performance** - Deferred execution with custom iterators make the difference. Currently the fastest library. 41 | - **Works out of the box** - *'npm install linq-collections'* is the hardest thing you'll need to do 42 | - **Collections** - Provides many type of collections (list, dictionary, ... + readonly) with linq integrated inside. As in C# 43 | - **Strict standard** - Strictly implementing [microsoft's official linq definition](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/classification-of-standard-query-operators-by-manner-of-execution) (you can check it for exceptions, behavior, etc) 44 | - **Deeply tested** - Each new version is passing tons of quality tests before being released 45 | 46 | ## Using the package 47 | Interfaces for this library are already designed. New versions won't break any old code. 48 | We strongly recommend using `*` for version selector 49 | ```json 50 | dependencies { 51 | "linq-collections": "1.*" 52 | } 53 | ``` 54 | 55 | ## Features 56 | Complete **Linq to Objects** implementation (deferred execution) 57 | > toArray, toList, toDictionary, toLookup, aggregate, all, any, average, concat, contains, count, defaultIfEmpty, distinct, elementAt, elementAtOrDefault, except, first, firstOrDefault, forEach, groupBy, groupJoin, intersect, join, last, lastOrDefault, longCount, max, min, orderBy, orderByDescending, reverse, select, selectMany, sequenceEquals, single, single, singleOrDefault, skip, skipWhile, sum, take, takeWhile, union, where, zip, ... 58 | 59 | Collections (+ readonly versions) 60 | > List, Dictionary, Stack, Queue, ... 61 | 62 | All Collections are **Queryable** 63 | ```typescript 64 | const list = new List([ 65 | "Hello", 66 | "Bye", 67 | "Thanks", 68 | ]); 69 | 70 | const notHello = list.where(e => e !== "Hello"); 71 | ``` 72 | 73 | ## How to run tests 74 | This library uses `mocha` with custom assertion helper for testing.
75 | Use `nyc mocha` to run the tests and coverage. 76 | 77 | ## Hall of fame 78 | * [@nikolalukovic](https://github.com/nikolalukovic) 79 | * [@tholdrim](https://github.com/tholdrim) 80 | -------------------------------------------------------------------------------- /mocha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linq-collections", 3 | "version": "1.0.255", 4 | "description": "Linq-Collections (ES5): [IEnumerable, IQueryable, ...] + [List, Dictionary, Stack, ... + readonly]", 5 | "main": "./build/src/Linq.js", 6 | "files": [ 7 | "build/src", 8 | "README.md", 9 | "LICENSE" 10 | ], 11 | "typescript": { 12 | "definition": "./build/src/Linq.d.ts" 13 | }, 14 | "typings": "./build/src/Linq.d.ts", 15 | "types": "./build/src/Linq.d.ts", 16 | "author": { 17 | "name": "Ivan Sanz", 18 | "email": "ivansanzcarasa@gmail.com", 19 | "url": "https://github.com/isc30" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/isc30/linq-collections/issues" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/isc30/linq-collections" 27 | }, 28 | "homepage": "https://github.com/isc30/linq-collections#readme", 29 | "scripts": { 30 | "node-compile": "tsc", 31 | "web-tests-compile": "browserify ./test/TestSuite.ts -p [tsify] > linq-tests.web.js", 32 | "pretest": "npm run node-compile && npm run web-tests-compile", 33 | "test": "nyc mocha ./build/test/TestSuite.js --slow 0", 34 | "report-coverage": "nyc report --reporter=text-lcov | coveralls", 35 | "publish-node": "npm run node-compile", 36 | "publish-web": "browserify ./src/Linq.ts -p [tsify] > ./build/src/linq.web.js", 37 | "prepublish": "npm run node-compile" 38 | }, 39 | "dependencies": {}, 40 | "devDependencies": { 41 | "typescript": "*", 42 | "@types/mocha": "*", 43 | "mocha": "*", 44 | "coveralls": "*", 45 | "nyc": "*", 46 | "browserify": "*", 47 | "tsify": "*" 48 | }, 49 | "directories": { 50 | "test": "test", 51 | "lib": "src" 52 | }, 53 | "license": "MIT", 54 | "keywords": [ 55 | "linq", 56 | ".net", 57 | "dotnet", 58 | "c#", 59 | "csharp", 60 | "visualbasic", 61 | "collections", 62 | "enumerable", 63 | "ienumerable", 64 | "ts", 65 | "typescript", 66 | "js", 67 | "javascript", 68 | "dictionary", 69 | "idictionary", 70 | "list", 71 | "ilist", 72 | "container", 73 | "hashset", 74 | "objects", 75 | "csharp", 76 | "map", 77 | "iterator", 78 | "iterators", 79 | "array", 80 | "readonly", 81 | "read-only", 82 | "readonlyList", 83 | "readonlydictionary", 84 | "mobile", 85 | "compatible", 86 | "es5", 87 | "ecmascript5", 88 | "es6" 89 | ], 90 | "nyc": { 91 | "exclude": [ 92 | "build/test" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Collections.ts: -------------------------------------------------------------------------------- 1 | // - 2 | // Created by Ivan Sanz (@isc30) 3 | // Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | // - 5 | 6 | // region IMPORTS 7 | // tslint:disable-next-line:max-line-length 8 | 9 | import { Action, Aggregator, Dynamic, Indexer, Predicate, Selector, ZipSelector, Type } from "./Types"; 10 | import 11 | { 12 | ArrayEnumerable, 13 | ConcatEnumerable, 14 | ConditionalEnumerable, 15 | Enumerable, 16 | IEnumerable, 17 | IGrouping, 18 | IKeyValue, 19 | IOrderedEnumerable, 20 | IQueryable, 21 | OrderedEnumerable, 22 | RangeEnumerable, 23 | ReverseEnumerable, 24 | TransformEnumerable, 25 | UniqueEnumerable, 26 | ZippedEnumerable, 27 | } from "./Enumerables"; 28 | import { Comparer, EqualityComparer, strictEqualityComparer, createComparer } from "./Comparers"; 29 | 30 | import { IIterable, ArrayIterator } from "./Iterators"; 31 | 32 | // endregion 33 | 34 | // region EnumerableCollection 35 | export abstract class EnumerableCollection 36 | implements IQueryable 37 | { 38 | public abstract copy(): IQueryable; 39 | public abstract asEnumerable(): IEnumerable; 40 | public abstract toArray(): TElement[]; 41 | 42 | public toList(): IList 43 | { 44 | return new List(this.toArray()); 45 | } 46 | 47 | public toDictionary( 48 | keySelector: Selector, 49 | valueSelector: Selector) 50 | : IDictionary 51 | { 52 | return Dictionary.fromArray(this.toArray(), keySelector, valueSelector); 53 | } 54 | 55 | public reverse(): IEnumerable 56 | { 57 | return new ReverseEnumerable(this.asEnumerable()); 58 | } 59 | 60 | public concat( 61 | other: TElement[] | IQueryable, 62 | ...others: Array>) 63 | : IEnumerable 64 | { 65 | return this.asEnumerable().concat(other, ...others); 66 | } 67 | 68 | public contains(element: TElement): boolean 69 | { 70 | return this.any(e => e === element); 71 | } 72 | 73 | public where(predicate: Predicate): IEnumerable 74 | { 75 | return new ConditionalEnumerable(this.asEnumerable(), predicate); 76 | } 77 | 78 | public select(selector: Selector): IEnumerable 79 | { 80 | return new TransformEnumerable(this.asEnumerable(), selector); 81 | } 82 | 83 | public selectMany( 84 | selector: Selector | IEnumerable>) 85 | : IEnumerable 86 | { 87 | const selectToEnumerable = (e: TElement) => 88 | { 89 | const ie = selector(e); 90 | 91 | return ie instanceof Array 92 | ? new ArrayEnumerable(ie) 93 | : ie.asEnumerable(); 94 | }; 95 | 96 | return this 97 | .select(selectToEnumerable).toArray() 98 | .reduce((p, c) => new ConcatEnumerable(p, c), Enumerable.empty()) as IEnumerable; 99 | } 100 | 101 | public elementAt(index: number): TElement 102 | { 103 | const element = this.elementAtOrDefault(index); 104 | 105 | if (element === undefined) 106 | { 107 | throw new Error("Out of bounds"); 108 | } 109 | 110 | return element; 111 | } 112 | 113 | public except(other: IQueryable): IEnumerable 114 | { 115 | return this.asEnumerable().except(other); 116 | } 117 | 118 | public first(): TElement; 119 | public first(predicate: Predicate): TElement; 120 | public first(predicate?: Predicate): TElement 121 | { 122 | let element: TElement | undefined; 123 | 124 | if (predicate !== undefined) 125 | { 126 | element = this.firstOrDefault(predicate); 127 | } 128 | else 129 | { 130 | element = this.firstOrDefault(); 131 | } 132 | 133 | if (element === undefined) 134 | { 135 | throw new Error("Sequence contains no elements"); 136 | } 137 | 138 | return element; 139 | } 140 | 141 | public groupBy( 142 | keySelector: Selector) 143 | : IEnumerable>; 144 | public groupBy( 145 | keySelector: Selector, 146 | valueSelector: Selector) 147 | : IEnumerable>; 148 | public groupBy( 149 | keySelector: Selector, 150 | valueSelector?: Selector) 151 | : IEnumerable> 152 | { 153 | const array = this.toArray(); 154 | const dictionary = new Dictionary>(); 155 | 156 | for (let i = 0; i < array.length; ++i) 157 | { 158 | const key = keySelector(array[i]); 159 | const value = valueSelector !== undefined 160 | ? valueSelector(array[i]) 161 | : array[i]; 162 | 163 | if (!dictionary.containsKey(key)) 164 | { 165 | dictionary.set(key, new List()); 166 | } 167 | 168 | (dictionary.get(key) as IList).push(value); 169 | } 170 | 171 | return dictionary.asEnumerable(); 172 | } 173 | 174 | public last(): TElement; 175 | public last(predicate: Predicate): TElement; 176 | public last(predicate?: Predicate): TElement 177 | { 178 | let element: TElement | undefined; 179 | 180 | if (predicate !== undefined) 181 | { 182 | element = this.lastOrDefault(predicate); 183 | } 184 | else 185 | { 186 | element = this.lastOrDefault(); 187 | } 188 | 189 | if (element === undefined) 190 | { 191 | throw new Error("Sequence contains no elements"); 192 | } 193 | 194 | return element; 195 | } 196 | 197 | public single(): TElement; 198 | public single(predicate: Predicate): TElement; 199 | public single(predicate?: Predicate): TElement 200 | { 201 | let element: TElement | undefined; 202 | 203 | if (predicate !== undefined) 204 | { 205 | element = this.singleOrDefault(predicate); 206 | } 207 | else 208 | { 209 | element = this.singleOrDefault(); 210 | } 211 | 212 | if (element === undefined) 213 | { 214 | throw new Error("Sequence contains no elements"); 215 | } 216 | 217 | return element; 218 | } 219 | 220 | public singleOrDefault(): TElement | undefined; 221 | public singleOrDefault(predicate: Predicate): TElement | undefined; 222 | public singleOrDefault(predicate?: Predicate): TElement | undefined 223 | { 224 | if (predicate !== undefined) 225 | { 226 | return this.asEnumerable().singleOrDefault(predicate); 227 | } 228 | 229 | return this.asEnumerable().singleOrDefault(); 230 | } 231 | 232 | public skipWhile(predicate: Predicate): IEnumerable 233 | { 234 | return this.asEnumerable().skipWhile(predicate); 235 | } 236 | 237 | public takeWhile(predicate: Predicate): IEnumerable 238 | { 239 | return this.asEnumerable().takeWhile(predicate); 240 | } 241 | 242 | public sequenceEqual(other: IQueryable | TElement[]): boolean 243 | public sequenceEqual(other: IQueryable | TElement[], comparer: EqualityComparer): boolean; 244 | public sequenceEqual(other: IQueryable | TElement[], comparer?: EqualityComparer): boolean 245 | { 246 | if (comparer !== undefined) 247 | { 248 | return this.asEnumerable().sequenceEqual(other, comparer); 249 | } 250 | 251 | return this.asEnumerable().sequenceEqual(other); 252 | } 253 | 254 | public distinct(): IEnumerable; 255 | public distinct(keySelector: Selector): IEnumerable; 256 | public distinct(keySelector?: Selector): IEnumerable 257 | { 258 | return new UniqueEnumerable(this.asEnumerable(), keySelector); 259 | } 260 | 261 | public min(): TElement; 262 | public min(selector: Selector): TSelectorOut; 263 | public min(selector?: Selector): TElement | TSelectorOut 264 | { 265 | if (selector !== undefined) 266 | { 267 | // Don't copy iterators 268 | return new TransformEnumerable(this.asEnumerable(), selector).min(); 269 | } 270 | 271 | return this.aggregate((previous, current) => 272 | (previous !== undefined && previous < current) 273 | ? previous 274 | : current); 275 | } 276 | 277 | public orderBy( 278 | keySelector: Selector): IOrderedEnumerable; 279 | public orderBy( 280 | keySelector: Selector, 281 | comparer: Comparer): IOrderedEnumerable; 282 | public orderBy( 283 | keySelector: Selector, 284 | comparer?: Comparer): IOrderedEnumerable 285 | { 286 | return new OrderedEnumerable(this.asEnumerable(), createComparer(keySelector, true, comparer)); 287 | } 288 | 289 | public orderByDescending( 290 | keySelector: Selector): IOrderedEnumerable 291 | { 292 | return new OrderedEnumerable(this.asEnumerable(), createComparer(keySelector, false, undefined)); 293 | } 294 | 295 | public max(): TElement; 296 | public max(selector: Selector): TSelectorOut; 297 | public max(selector?: Selector): TElement | TSelectorOut 298 | { 299 | if (selector !== undefined) 300 | { 301 | // Don't copy iterators 302 | return new TransformEnumerable(this.asEnumerable(), selector).max(); 303 | } 304 | 305 | return this.aggregate((previous, current) => 306 | (previous !== undefined && previous > current) 307 | ? previous 308 | : current); 309 | } 310 | 311 | public sum(selector: Selector): number 312 | { 313 | return this.aggregate( 314 | (previous: number, current: TElement) => previous + selector(current), 0); 315 | } 316 | 317 | public skip(amount: number): IEnumerable 318 | { 319 | return new RangeEnumerable(this.asEnumerable(), amount, undefined); 320 | } 321 | 322 | public take(amount: number): IEnumerable 323 | { 324 | return new RangeEnumerable(this.asEnumerable(), undefined, amount); 325 | } 326 | 327 | public union(other: IQueryable): IEnumerable 328 | { 329 | return new UniqueEnumerable(this.concat(other)); 330 | } 331 | 332 | public aggregate(aggregator: Aggregator): TElement; 333 | public aggregate(aggregator: Aggregator, initialValue: TValue): TValue; 334 | public aggregate( 335 | aggregator: Aggregator, 336 | initialValue?: TValue): TValue | TElement 337 | { 338 | if (initialValue !== undefined) 339 | { 340 | return this.asEnumerable().aggregate( 341 | aggregator as Aggregator, 342 | initialValue); 343 | } 344 | 345 | return this.asEnumerable().aggregate( 346 | aggregator as Aggregator); 347 | } 348 | 349 | public any(): boolean; 350 | public any(predicate: Predicate): boolean; 351 | public any(predicate?: Predicate): boolean 352 | { 353 | if (predicate !== undefined) 354 | { 355 | return this.asEnumerable().any(predicate); 356 | } 357 | 358 | return this.asEnumerable().any(); 359 | } 360 | 361 | public all(predicate: Predicate): boolean 362 | { 363 | return this.asEnumerable().all(predicate); 364 | } 365 | 366 | public average(selector: Selector): number 367 | { 368 | return this.asEnumerable().average(selector); 369 | } 370 | 371 | public count(): number; 372 | public count(predicate: Predicate): number; 373 | public count(predicate?: Predicate): number 374 | { 375 | if (predicate !== undefined) 376 | { 377 | return this.asEnumerable().count(predicate); 378 | } 379 | 380 | return this.asEnumerable().count(); 381 | } 382 | 383 | public elementAtOrDefault(index: number): TElement | undefined 384 | { 385 | return this.asEnumerable().elementAtOrDefault(index); 386 | } 387 | 388 | public firstOrDefault(): TElement | undefined; 389 | public firstOrDefault(predicate: Predicate): TElement | undefined; 390 | public firstOrDefault(predicate?: Predicate): TElement | undefined 391 | { 392 | if (predicate !== undefined) 393 | { 394 | return this.asEnumerable().firstOrDefault(predicate); 395 | } 396 | 397 | return this.asEnumerable().firstOrDefault(); 398 | } 399 | 400 | public lastOrDefault(): TElement | undefined; 401 | public lastOrDefault(predicate: Predicate): TElement | undefined; 402 | public lastOrDefault(predicate?: Predicate): TElement | undefined 403 | { 404 | if (predicate !== undefined) 405 | { 406 | return this.asEnumerable().lastOrDefault(predicate); 407 | } 408 | 409 | return this.asEnumerable().lastOrDefault(); 410 | } 411 | 412 | public forEach(action: Action): void 413 | { 414 | return this.asEnumerable().forEach(action); 415 | } 416 | 417 | public defaultIfEmpty(): IEnumerable; 418 | public defaultIfEmpty(defaultValue: TElement): IEnumerable; 419 | public defaultIfEmpty(defaultValue?: TElement): IEnumerable 420 | { 421 | if (defaultValue !== undefined) 422 | { 423 | return this.asEnumerable().defaultIfEmpty(defaultValue); 424 | } 425 | 426 | return this.asEnumerable().defaultIfEmpty(); 427 | } 428 | 429 | public zip(other: IQueryable | TOther[], selector: ZipSelector): IEnumerable 430 | { 431 | return this.asEnumerable().zip(other, selector); 432 | } 433 | } 434 | // endregion 435 | // region ArrayQueryable 436 | export abstract class ArrayQueryable 437 | extends EnumerableCollection 438 | { 439 | protected source: TElement[]; 440 | 441 | public abstract copy(): IQueryable; 442 | 443 | public constructor(); 444 | public constructor(elements: TElement[]) 445 | public constructor(elements: TElement[] = []) 446 | { 447 | super(); 448 | this.source = elements; 449 | } 450 | 451 | public asArray(): TElement[] 452 | { 453 | return this.source; 454 | } 455 | 456 | public toArray(): TElement[] 457 | { 458 | return ([] as TElement[]).concat(this.source); 459 | } 460 | 461 | public toList(): IList 462 | { 463 | return new List(this.toArray()); 464 | } 465 | 466 | public asEnumerable(): IEnumerable 467 | { 468 | return new ArrayEnumerable(this.source); 469 | } 470 | 471 | public aggregate(aggregator: Aggregator): TElement; 472 | public aggregate(aggregator: Aggregator, initialValue: TValue): TValue; 473 | public aggregate( 474 | aggregator: Aggregator, 475 | initialValue?: TValue): TValue | TElement 476 | { 477 | if (initialValue !== undefined) 478 | { 479 | return this.source.reduce( 480 | aggregator as Aggregator, 481 | initialValue); 482 | } 483 | 484 | return this.source.reduce(aggregator as Aggregator); 485 | } 486 | 487 | public any(): boolean; 488 | public any(predicate: Predicate): boolean; 489 | public any(predicate?: Predicate): boolean 490 | { 491 | if (predicate !== undefined) 492 | { 493 | return this.source.some(predicate); 494 | } 495 | 496 | return this.source.length > 0; 497 | } 498 | 499 | public all(predicate: Predicate): boolean 500 | { 501 | return this.source.every(predicate); 502 | } 503 | 504 | public average(selector: Selector): number 505 | { 506 | if (this.count() === 0) 507 | { 508 | throw new Error("Sequence contains no elements"); 509 | } 510 | 511 | let sum = 0; 512 | 513 | for (let i = 0, end = this.source.length; i < end; ++i) 514 | { 515 | sum += selector(this.source[i]); 516 | } 517 | 518 | return sum / this.source.length; 519 | } 520 | 521 | public count(): number; 522 | public count(predicate: Predicate): number; 523 | public count(predicate?: Predicate): number 524 | { 525 | if (predicate !== undefined) 526 | { 527 | return this.source.filter(predicate).length; 528 | } 529 | 530 | return this.source.length; 531 | } 532 | 533 | public elementAtOrDefault(index: number): TElement | undefined 534 | { 535 | if (index < 0) 536 | { 537 | throw new Error("Negative index is forbiden"); 538 | } 539 | 540 | return this.source[index]; 541 | } 542 | 543 | public firstOrDefault(): TElement | undefined; 544 | public firstOrDefault(predicate: Predicate): TElement | undefined; 545 | public firstOrDefault(predicate?: Predicate): TElement | undefined 546 | { 547 | if (predicate !== undefined) 548 | { 549 | return this.source.filter(predicate)[0]; 550 | } 551 | 552 | return this.source[0]; 553 | } 554 | 555 | public groupBy( 556 | keySelector: Selector) 557 | : IEnumerable>; 558 | public groupBy( 559 | keySelector: Selector, 560 | valueSelector: Selector) 561 | : IEnumerable>; 562 | public groupBy( 563 | keySelector: Selector, 564 | valueSelector?: Selector) 565 | : IEnumerable> 566 | { 567 | const array = this.asArray(); 568 | const dictionary = new Dictionary>(); 569 | 570 | for (let i = 0; i < array.length; ++i) 571 | { 572 | const key = keySelector(array[i]); 573 | const value = valueSelector !== undefined 574 | ? valueSelector(array[i]) 575 | : array[i]; 576 | 577 | if (!dictionary.containsKey(key)) 578 | { 579 | dictionary.set(key, new List()); 580 | } 581 | 582 | (dictionary.get(key) as IList).push(value); 583 | } 584 | 585 | return dictionary.asEnumerable(); 586 | } 587 | 588 | public lastOrDefault(): TElement | undefined; 589 | public lastOrDefault(predicate: Predicate): TElement | undefined; 590 | public lastOrDefault(predicate?: Predicate): TElement | undefined 591 | { 592 | if (predicate !== undefined) 593 | { 594 | const records = this.source.filter(predicate); 595 | 596 | return records[records.length - 1]; 597 | } 598 | 599 | return this.source[this.source.length - 1]; 600 | } 601 | 602 | public forEach(action: Action): void 603 | { 604 | for (let i = 0, end = this.source.length; i < end; ++i) 605 | { 606 | action(this.source[i], i); 607 | } 608 | } 609 | 610 | public sequenceEqual(other: IQueryable | TElement[]): boolean; 611 | public sequenceEqual(other: IQueryable | TElement[], comparer: EqualityComparer): boolean; 612 | public sequenceEqual(other: IQueryable | TElement[], comparer: EqualityComparer = strictEqualityComparer()): boolean 613 | { 614 | if (other instanceof ArrayQueryable 615 | || other instanceof Array) 616 | { 617 | const thisArray = this.asArray(); 618 | const otherArray = other instanceof ArrayQueryable 619 | ? other.asArray() as TElement[] 620 | : other; 621 | 622 | if (thisArray.length != otherArray.length) 623 | { 624 | return false; 625 | } 626 | 627 | for (let i = 0; i < thisArray.length; ++i) 628 | { 629 | if (!comparer(thisArray[i], otherArray[i])) 630 | { 631 | return false; 632 | } 633 | } 634 | 635 | return true; 636 | } 637 | 638 | return this.asEnumerable().sequenceEqual(other, comparer); 639 | } 640 | } 641 | // endregion 642 | // region List 643 | export interface IReadOnlyList 644 | extends IQueryable 645 | { 646 | copy(): IList; 647 | 648 | get(index: number): TElement | undefined; 649 | 650 | indexOf(element: TElement): number; 651 | } 652 | 653 | export interface IList 654 | extends IReadOnlyList 655 | { 656 | asReadOnly(): IReadOnlyList; 657 | asArray(): TElement[]; 658 | clear(): void; 659 | push(element: TElement): number; 660 | pushRange(elements: TElement[] | IQueryable): number; 661 | pushFront(element: TElement): number; 662 | pop(): TElement | undefined; 663 | popFront(): TElement | undefined; 664 | remove(element: TElement): void; 665 | removeAt(index: number): TElement | undefined; 666 | set(index: number, element: TElement): void; 667 | insert(index: number, element: TElement): void; 668 | } 669 | 670 | export class List 671 | extends ArrayQueryable 672 | implements IList 673 | { 674 | public copy(): IList 675 | { 676 | return new List(this.toArray()); 677 | } 678 | 679 | public asReadOnly(): IReadOnlyList 680 | { 681 | return this; 682 | } 683 | 684 | public clear(): void 685 | { 686 | this.source = []; 687 | } 688 | 689 | public remove(element: TElement): void 690 | { 691 | const newSource: TElement[] = []; 692 | 693 | for (let i = 0, end = this.source.length; i < end; ++i) 694 | { 695 | if (this.source[i] !== element) 696 | { 697 | newSource.push(this.source[i]); 698 | } 699 | } 700 | 701 | this.source = newSource; 702 | } 703 | 704 | public removeAt(index: number): TElement | undefined 705 | { 706 | if (index < 0 || this.source[index] === undefined) 707 | { 708 | throw new Error("Out of bounds"); 709 | } 710 | 711 | return this.source.splice(index, 1)[0]; 712 | } 713 | 714 | public get(index: number): TElement | undefined 715 | { 716 | return this.source[index]; 717 | } 718 | 719 | public push(element: TElement): number 720 | { 721 | return this.source.push(element); 722 | } 723 | 724 | public pushRange(elements: TElement[] | IQueryable): number 725 | { 726 | if (!(elements instanceof Array)) 727 | { 728 | elements = elements.toArray(); 729 | } 730 | 731 | return this.source.push.apply(this.source, elements); 732 | } 733 | 734 | public pushFront(element: TElement): number 735 | { 736 | return this.source.unshift(element); 737 | } 738 | 739 | public pop(): TElement | undefined 740 | { 741 | return this.source.pop(); 742 | } 743 | 744 | public popFront(): TElement | undefined 745 | { 746 | return this.source.shift(); 747 | } 748 | 749 | public set(index: number, element: TElement): void 750 | { 751 | if (index < 0) 752 | { 753 | throw new Error("Out of bounds"); 754 | } 755 | 756 | this.source[index] = element; 757 | } 758 | 759 | public insert(index: number, element: TElement): void 760 | { 761 | if (index < 0 || index > this.source.length) 762 | { 763 | throw new Error("Out of bounds"); 764 | } 765 | 766 | this.source.splice(index, 0, element); 767 | } 768 | 769 | public indexOf(element: TElement): number 770 | { 771 | return this.source.indexOf(element); 772 | } 773 | } 774 | // endregion 775 | // region Stack 776 | export interface IStack 777 | extends IQueryable 778 | { 779 | copy(): IStack; 780 | 781 | asArray(): TElement[]; 782 | clear(): void; 783 | peek(): TElement | undefined; 784 | pop(): TElement | undefined; 785 | push(element: TElement): number; 786 | } 787 | 788 | export class Stack 789 | extends ArrayQueryable 790 | implements IStack 791 | { 792 | public copy(): IStack 793 | { 794 | return new Stack(this.toArray()); 795 | } 796 | 797 | public clear(): void 798 | { 799 | this.source = []; 800 | } 801 | 802 | public peek(): TElement | undefined 803 | { 804 | return this.source[this.source.length - 1]; 805 | } 806 | 807 | public pop(): TElement | undefined 808 | { 809 | return this.source.pop(); 810 | } 811 | 812 | public push(element: TElement): number 813 | { 814 | return this.source.push(element); 815 | } 816 | } 817 | // endregion 818 | // region Dictionary 819 | export interface IReadOnlyDictionary 820 | extends IQueryable> 821 | { 822 | copy(): IDictionary; 823 | 824 | containsKey(key: TKey): boolean; 825 | containsValue(value: TValue): boolean; 826 | getKeys(): IList; 827 | getValues(): IList; 828 | 829 | get(key: TKey): TValue; 830 | } 831 | 832 | export interface IDictionary 833 | extends IReadOnlyDictionary 834 | { 835 | asReadOnly(): IReadOnlyDictionary; 836 | clear(): void; 837 | remove(key: TKey): void; 838 | set(key: TKey, value: TValue): void; 839 | setOrUpdate(key: TKey, value: TValue): void; 840 | } 841 | 842 | export class Dictionary 843 | extends EnumerableCollection> 844 | implements IDictionary 845 | { 846 | public static fromArray( 847 | array: TArray[], 848 | keySelector: Selector, 849 | valueSelector: Selector) 850 | : IDictionary 851 | { 852 | const keyValuePairs = array.map>(v => 853 | { 854 | return { 855 | key: keySelector(v), 856 | value: valueSelector(v), 857 | }; 858 | }); 859 | 860 | return new Dictionary(keyValuePairs); 861 | } 862 | 863 | public static fromJsObject( 864 | object: Dynamic) 865 | : IDictionary 866 | { 867 | const keys = new List(Object.getOwnPropertyNames(object)); 868 | const keyValues = keys.select(k => >{ key: k, value: object[k] }); 869 | 870 | return new Dictionary(keyValues.toArray()); 871 | } 872 | 873 | protected dictionary: Dynamic; 874 | protected keyType: Type; 875 | 876 | public constructor(); 877 | public constructor(keyValuePairs: Array>); 878 | public constructor(keyValuePairs?: Array>) 879 | { 880 | super(); 881 | this.clear(); 882 | 883 | if (keyValuePairs !== undefined) 884 | { 885 | for (let i = 0; i < keyValuePairs.length; ++i) 886 | { 887 | const pair = keyValuePairs[i]; 888 | this.set(pair.key, pair.value); 889 | } 890 | } 891 | } 892 | 893 | public copy(): IDictionary 894 | { 895 | return new Dictionary(this.toArray()); 896 | } 897 | 898 | public asReadOnly(): IReadOnlyDictionary 899 | { 900 | return this; 901 | } 902 | 903 | public asEnumerable(): IEnumerable> 904 | { 905 | return new ArrayEnumerable(this.toArray()); 906 | } 907 | 908 | public toArray(): Array> 909 | { 910 | return this.getKeys().select>(p => 911 | { 912 | return { 913 | key: p, 914 | value: this.dictionary[p], 915 | }; 916 | }).toArray(); 917 | } 918 | 919 | public clear(): void 920 | { 921 | this.dictionary = {}; 922 | } 923 | 924 | public containsKey(key: TKey): boolean 925 | { 926 | return this.dictionary.hasOwnProperty(key); 927 | } 928 | 929 | public containsValue(value: TValue): boolean 930 | { 931 | const keys = this.getKeysFast(); 932 | 933 | for (let i = 0; i < keys.length; ++i) 934 | { 935 | if (this.dictionary[keys[i]] === value) 936 | { 937 | return true; 938 | } 939 | } 940 | 941 | return false; 942 | } 943 | 944 | public getKeys(): IList 945 | { 946 | const keys = this.getKeysFast(); 947 | 948 | return new List(keys.map( 949 | k => this.keyType === "number" 950 | ? parseFloat(k) 951 | : k) as TKey[]); 952 | } 953 | 954 | protected getKeysFast(): string[] 955 | { 956 | return Object.getOwnPropertyNames(this.dictionary); 957 | } 958 | 959 | public getValues(): IList 960 | { 961 | const keys = this.getKeysFast(); 962 | const result = new Array(keys.length); 963 | 964 | for (let i = 0; i < keys.length; ++i) 965 | { 966 | result[i] = this.dictionary[keys[i]]; 967 | } 968 | 969 | return new List(result); 970 | } 971 | 972 | public remove(key: TKey): void 973 | { 974 | if (this.containsKey(key)) 975 | { 976 | delete this.dictionary[key]; 977 | } 978 | } 979 | 980 | public get(key: TKey): TValue 981 | { 982 | if (!this.containsKey(key)) 983 | { 984 | throw new Error(`Key doesn't exist: ${key}`) 985 | } 986 | 987 | return this.dictionary[key]; 988 | } 989 | 990 | public set(key: TKey, value: TValue): void 991 | { 992 | if (this.containsKey(key)) 993 | { 994 | throw new Error(`Key already exists: ${key}`); 995 | } 996 | 997 | this.setOrUpdate(key, value); 998 | } 999 | 1000 | public setOrUpdate(key: TKey, value: TValue): void 1001 | { 1002 | if (this.keyType === undefined) 1003 | { 1004 | this.keyType = typeof key; 1005 | } 1006 | 1007 | this.dictionary[key] = value; 1008 | } 1009 | } 1010 | // endregion 1011 | -------------------------------------------------------------------------------- /src/Comparers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by Ivan Sanz (@isc30) 3 | * Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | */ 5 | 6 | import { Selector } from "./Types"; 7 | 8 | export type ComparerResult = -1 | 0 | 1; 9 | export type Comparer = (left: T, right: T) => ComparerResult; 10 | 11 | export type EqualityComparer = (left: T, right: T) => boolean; 12 | export const strictEqualityComparer = () => (left: T, right: T) => left === right; 13 | 14 | export function combineComparers(left: Comparer, right: Comparer): Comparer 15 | { 16 | return (l: T, r: T) => left(l, r) || right(l, r); 17 | } 18 | 19 | export function createComparer( 20 | keySelector: Selector, 21 | ascending: boolean, 22 | customComparer?: Comparer): Comparer 23 | { 24 | if (customComparer !== undefined) 25 | { 26 | return (l: TElement, r: TElement) => customComparer(keySelector(l), keySelector(r)); 27 | } 28 | 29 | return ascending 30 | ? (l: TElement, r: TElement) => 31 | { 32 | const left = keySelector(l); 33 | const right = keySelector(r); 34 | 35 | return left < right 36 | ? -1 37 | : left > right 38 | ? 1 39 | : 0; 40 | } 41 | : (l: TElement, r: TElement) => 42 | { 43 | const left = keySelector(l); 44 | const right = keySelector(r); 45 | 46 | return left < right 47 | ? 1 48 | : left > right 49 | ? -1 50 | : 0; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/Enumerables.ts: -------------------------------------------------------------------------------- 1 | // - 2 | // Created by Ivan Sanz (@isc30) 3 | // Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | // - 5 | 6 | // region IMPORTS 7 | 8 | import { Action, Aggregator, Dynamic, Indexer, Predicate, Selector, ZipSelector } from "./Types"; 9 | import { ArrayIterator, IIterable } from "./Iterators"; 10 | import { Comparer, EqualityComparer, strictEqualityComparer, combineComparers, createComparer } from "./Comparers"; 11 | import { Dictionary, IDictionary, IList, List } from "./Collections"; 12 | 13 | import { Cached } from "./Utils"; 14 | 15 | // endregion 16 | 17 | // region Interfaces 18 | export interface IKeyValue 19 | { 20 | key: TKey; 21 | value: TValue; 22 | } 23 | 24 | export type IGrouping = IKeyValue>; 25 | 26 | export interface IQueryable 27 | { 28 | copy(): IQueryable; 29 | 30 | asEnumerable(): IEnumerable; 31 | toArray(): TOut[]; 32 | toList(): IList; 33 | toDictionary( 34 | keySelector: Selector, 35 | valueSelector: Selector) 36 | : IDictionary; 37 | // toLookup 38 | 39 | aggregate(aggregator: Aggregator): TOut; 40 | aggregate(aggregator: Aggregator, initialValue: TValue): TValue; 41 | 42 | all(predicate: Predicate): boolean; 43 | 44 | any(): boolean; 45 | any(predicate: Predicate): boolean; 46 | 47 | average(selector: Selector): number; 48 | 49 | concat( 50 | other: TOut[] | IQueryable, 51 | ...others: Array>) 52 | : IEnumerable; 53 | 54 | contains(element: TOut): boolean; 55 | 56 | count(): number; 57 | count(predicate: Predicate): number; 58 | 59 | defaultIfEmpty(): IEnumerable; 60 | defaultIfEmpty(defaultValue: TOut): IEnumerable; 61 | 62 | distinct(): IEnumerable; 63 | distinct(keySelector: Selector): IEnumerable; 64 | 65 | elementAt(index: number): TOut; 66 | 67 | elementAtOrDefault(index: number): TOut | undefined; 68 | 69 | except(other: IQueryable): IEnumerable; 70 | 71 | first(): TOut; 72 | first(predicate: Predicate): TOut; 73 | 74 | firstOrDefault(): TOut | undefined; 75 | firstOrDefault(predicate: Predicate): TOut | undefined; 76 | 77 | forEach(action: Action): void; 78 | 79 | groupBy( 80 | keySelector: Selector) 81 | : IEnumerable>; 82 | groupBy( 83 | keySelector: Selector, 84 | valueSelector: Selector) 85 | : IEnumerable>; 86 | 87 | // groupJoin 88 | 89 | // intersect 90 | 91 | // join 92 | 93 | last(): TOut; 94 | last(predicate: Predicate): TOut; 95 | 96 | lastOrDefault(): TOut | undefined; 97 | lastOrDefault(predicate: Predicate): TOut | undefined; 98 | 99 | // longCount 100 | 101 | max(): TOut; 102 | max(selector: Selector): TSelectorOut; 103 | 104 | min(): TOut; 105 | min(selector: Selector): TSelectorOut; 106 | 107 | orderBy( 108 | keySelector: Selector): IOrderedEnumerable; 109 | orderBy( 110 | keySelector: Selector, 111 | comparer: Comparer): IOrderedEnumerable; 112 | 113 | orderByDescending( 114 | keySelector: Selector): IOrderedEnumerable; 115 | 116 | reverse(): IEnumerable; 117 | 118 | select(selector: Selector): IEnumerable; 119 | 120 | selectMany( 121 | selector: Selector>) 122 | : IEnumerable; 123 | 124 | sequenceEqual(other: IQueryable | TOut[]): boolean; 125 | sequenceEqual(other: IQueryable | TOut[], comparer: EqualityComparer): boolean; 126 | 127 | single(): TOut; 128 | single(predicate: Predicate): TOut; 129 | 130 | singleOrDefault(): TOut | undefined; 131 | singleOrDefault(predicate: Predicate): TOut | undefined; 132 | 133 | skip(amount: number): IEnumerable; 134 | 135 | skipWhile(predicate: Predicate): IEnumerable; 136 | 137 | sum(selector: Selector): number; 138 | 139 | take(amount: number): IEnumerable; 140 | 141 | takeWhile(predicate: Predicate): IEnumerable; 142 | 143 | union(other: IQueryable): IEnumerable; 144 | 145 | where(predicate: Predicate): IEnumerable; 146 | 147 | zip(other: IQueryable | TOther[], selector: ZipSelector): IEnumerable; 148 | } 149 | 150 | export interface IEnumerable extends IQueryable, IIterable 151 | { 152 | copy(): IEnumerable; 153 | } 154 | 155 | export interface IOrderedEnumerable extends IEnumerable 156 | { 157 | copy(): IOrderedEnumerable; 158 | 159 | thenBy( 160 | keySelector: Selector): IOrderedEnumerable; 161 | thenBy( 162 | keySelector: Selector, 163 | comparer: Comparer): IOrderedEnumerable; 164 | 165 | thenByDescending(keySelector: Selector): IOrderedEnumerable; 166 | } 167 | // endregion 168 | // region EnumerableBase 169 | export abstract class EnumerableBase implements IEnumerable 170 | { 171 | 172 | protected readonly source: IIterable | IEnumerable; 173 | 174 | protected constructor(source: IIterable) 175 | { 176 | this.source = source; 177 | } 178 | 179 | public abstract copy(): IEnumerable; 180 | public abstract value(): TOut; 181 | 182 | public reset(): void 183 | { 184 | this.source.reset(); 185 | } 186 | 187 | public next(): boolean 188 | { 189 | return this.source.next(); 190 | } 191 | 192 | public asEnumerable(): IEnumerable 193 | { 194 | return this; 195 | } 196 | 197 | public toArray(): TOut[] 198 | { 199 | const result: TOut[] = []; 200 | this.reset(); 201 | 202 | while (this.next()) 203 | { 204 | result.push(this.value()); 205 | } 206 | 207 | return result; 208 | } 209 | 210 | public toList(): IList 211 | { 212 | return new List(this.toArray()); 213 | } 214 | 215 | public toDictionary( 216 | keySelector: Selector, 217 | valueSelector: Selector) 218 | : IDictionary 219 | { 220 | return Dictionary.fromArray(this.toArray(), keySelector, valueSelector); 221 | } 222 | 223 | public count(): number; 224 | public count(predicate: Predicate): number; 225 | public count(predicate?: Predicate): number 226 | { 227 | if (predicate !== undefined) 228 | { 229 | // Don't copy iterators 230 | return new ConditionalEnumerable(this, predicate).count(); 231 | } 232 | 233 | let result: number = 0; 234 | this.reset(); 235 | 236 | while (this.next()) 237 | { 238 | ++result; 239 | } 240 | 241 | // tslint:disable-next-line:no-bitwise 242 | return result >>> 0; 243 | } 244 | 245 | public any(): boolean; 246 | public any(predicate: Predicate): boolean; 247 | public any(predicate?: Predicate): boolean 248 | { 249 | if (predicate !== undefined) 250 | { 251 | // Don't copy iterators 252 | return new ConditionalEnumerable(this, predicate).any(); 253 | } 254 | 255 | this.reset(); 256 | 257 | return this.next(); 258 | } 259 | 260 | public all(predicate: Predicate): boolean 261 | { 262 | this.reset(); 263 | 264 | while (this.next()) 265 | { 266 | if (!predicate(this.value())) 267 | { 268 | return false; 269 | } 270 | } 271 | 272 | return true; 273 | } 274 | 275 | public reverse(): IEnumerable 276 | { 277 | return new ReverseEnumerable(this.copy()); 278 | } 279 | 280 | public contains(element: TOut): boolean 281 | { 282 | return this.any(e => e === element); 283 | } 284 | 285 | public sequenceEqual(other: IQueryable | TOut[]): boolean; 286 | public sequenceEqual(other: IQueryable | TOut[], comparer: EqualityComparer): boolean; 287 | public sequenceEqual(other: IQueryable | TOut[], comparer: EqualityComparer = strictEqualityComparer()): boolean 288 | { 289 | const otherEnumerable = other instanceof Array 290 | ? new ArrayEnumerable(other) 291 | : other.asEnumerable(); 292 | 293 | this.reset(); 294 | otherEnumerable.reset(); 295 | 296 | while (this.next()) 297 | { 298 | if (!otherEnumerable.next() || !comparer(this.value(), otherEnumerable.value())) 299 | { 300 | return false; 301 | } 302 | } 303 | 304 | return !otherEnumerable.next(); 305 | } 306 | 307 | public where(predicate: Predicate): IEnumerable 308 | { 309 | return new ConditionalEnumerable(this.copy(), predicate); 310 | } 311 | 312 | public select(selector: Selector): IEnumerable 313 | { 314 | return new TransformEnumerable(this.copy(), selector); 315 | } 316 | 317 | public selectMany( 318 | selector: Selector>) 319 | : IEnumerable 320 | { 321 | const selectToEnumerable = (e: TOut) => 322 | { 323 | const ie = selector(e); 324 | 325 | return Array.isArray(ie) 326 | ? new ArrayEnumerable(ie) 327 | : ie.asEnumerable(); 328 | }; 329 | 330 | return this 331 | .select(selectToEnumerable).toArray() 332 | .reduce((p, c) => new ConcatEnumerable(p, c), Enumerable.empty()) as IEnumerable; 333 | } 334 | 335 | public skipWhile(predicate: Selector): IEnumerable 336 | { 337 | return new SkipWhileEnumerable(this.copy(), predicate); 338 | } 339 | 340 | public concat( 341 | other: TOut[] | IQueryable, 342 | ...others: Array>) 343 | : IEnumerable 344 | { 345 | const asEnumerable = (e: TOut[] | IQueryable): IIterable => 346 | { 347 | return e instanceof Array 348 | ? new ArrayEnumerable(e) 349 | : e.asEnumerable(); 350 | }; 351 | 352 | let result = new ConcatEnumerable(this.copy(), asEnumerable(other).copy()); 353 | 354 | for (let i = 0, end = others.length; i < end; ++i) 355 | { 356 | result = new ConcatEnumerable(result, asEnumerable(others[i]).copy()); 357 | } 358 | 359 | return result; 360 | } 361 | 362 | public defaultIfEmpty(): IEnumerable; 363 | public defaultIfEmpty(defaultValue: TOut): IEnumerable; 364 | public defaultIfEmpty(defaultValue?: TOut): IEnumerable 365 | { 366 | return new DefaultIfEmptyEnumerable(this, defaultValue); 367 | } 368 | 369 | public elementAt(index: number): TOut 370 | { 371 | const element = this.elementAtOrDefault(index); 372 | 373 | if (element === undefined) 374 | { 375 | throw new Error("Out of bounds"); 376 | } 377 | 378 | return element; 379 | } 380 | 381 | public elementAtOrDefault(index: number): TOut | undefined 382 | { 383 | if (index < 0) 384 | { 385 | throw new Error("Negative index is forbiden"); 386 | } 387 | 388 | this.reset(); 389 | 390 | let currentIndex = -1; 391 | 392 | while (this.next()) 393 | { 394 | ++currentIndex; 395 | 396 | if (currentIndex === index) 397 | { 398 | break; 399 | } 400 | } 401 | 402 | if (currentIndex !== index) 403 | { 404 | return undefined; 405 | } 406 | 407 | return this.value(); 408 | } 409 | 410 | public except(other: IQueryable): IEnumerable 411 | { 412 | return this.where(e => !other.contains(e)); 413 | } 414 | 415 | public first(): TOut; 416 | public first(predicate: Predicate): TOut; 417 | public first(predicate?: Predicate): TOut 418 | { 419 | let element: TOut | undefined; 420 | 421 | if (predicate !== undefined) 422 | { 423 | element = this.firstOrDefault(predicate); 424 | } 425 | else 426 | { 427 | element = this.firstOrDefault(); 428 | } 429 | 430 | if (element === undefined) 431 | { 432 | throw new Error("Sequence contains no elements"); 433 | } 434 | 435 | return element; 436 | } 437 | 438 | public firstOrDefault(): TOut | undefined; 439 | public firstOrDefault(predicate: Predicate): TOut | undefined; 440 | public firstOrDefault(predicate?: Predicate): TOut | undefined 441 | { 442 | if (predicate !== undefined) 443 | { 444 | // Don't copy iterators 445 | return new ConditionalEnumerable(this, predicate).firstOrDefault(); 446 | } 447 | 448 | this.reset(); 449 | 450 | if (!this.next()) 451 | { 452 | return undefined; 453 | } 454 | 455 | return this.value(); 456 | } 457 | 458 | public forEach(action: Action): void 459 | { 460 | this.reset(); 461 | 462 | for (let i = 0; this.next(); ++i) 463 | { 464 | action(this.value(), i); 465 | } 466 | } 467 | 468 | groupBy( 469 | keySelector: Selector) 470 | : IEnumerable>; 471 | groupBy( 472 | keySelector: Selector, 473 | valueSelector: Selector) 474 | : IEnumerable>; 475 | groupBy( 476 | keySelector: Selector, 477 | valueSelector?: Selector) 478 | : IEnumerable> 479 | { 480 | const array = this.toArray(); 481 | const dictionary = new Dictionary>(); 482 | 483 | for (let i = 0; i < array.length; ++i) 484 | { 485 | const key = keySelector(array[i]); 486 | const value = valueSelector !== undefined 487 | ? valueSelector(array[i]) 488 | : array[i]; 489 | 490 | if (!dictionary.containsKey(key)) 491 | { 492 | dictionary.set(key, new List()); 493 | } 494 | 495 | (dictionary.get(key) as IList).push(value); 496 | } 497 | 498 | return dictionary.asEnumerable(); 499 | } 500 | 501 | public last(): TOut; 502 | public last(predicate: Predicate): TOut; 503 | public last(predicate?: Predicate): TOut 504 | { 505 | let element: TOut | undefined; 506 | 507 | if (predicate !== undefined) 508 | { 509 | element = this.lastOrDefault(predicate); 510 | } 511 | else 512 | { 513 | element = this.lastOrDefault(); 514 | } 515 | 516 | if (element === undefined) 517 | { 518 | throw new Error("Sequence contains no elements"); 519 | } 520 | 521 | return element; 522 | } 523 | 524 | public lastOrDefault(): TOut | undefined; 525 | public lastOrDefault(predicate: Predicate): TOut | undefined; 526 | public lastOrDefault(predicate?: Predicate): TOut | undefined 527 | { 528 | if (predicate !== undefined) 529 | { 530 | // Don't copy iterators 531 | return new ConditionalEnumerable(this, predicate).lastOrDefault(); 532 | } 533 | 534 | const reversed = new ReverseEnumerable(this); 535 | reversed.reset(); 536 | 537 | if (!reversed.next()) 538 | { 539 | return undefined; 540 | } 541 | 542 | return reversed.value(); 543 | } 544 | 545 | public single(): TOut; 546 | public single(predicate: Predicate): TOut; 547 | public single(predicate?: Predicate): TOut 548 | { 549 | let element: TOut | undefined; 550 | 551 | if (predicate !== undefined) 552 | { 553 | element = this.singleOrDefault(predicate); 554 | } 555 | else 556 | { 557 | element = this.singleOrDefault(); 558 | } 559 | 560 | if (element === undefined) 561 | { 562 | throw new Error("Sequence contains no elements"); 563 | } 564 | 565 | return element; 566 | } 567 | 568 | public singleOrDefault(): TOut | undefined; 569 | public singleOrDefault(predicate: Predicate): TOut | undefined; 570 | public singleOrDefault(predicate?: Predicate): TOut | undefined 571 | { 572 | if (predicate !== undefined) 573 | { 574 | // Don't copy iterators 575 | return new ConditionalEnumerable(this, predicate).singleOrDefault(); 576 | } 577 | 578 | this.reset(); 579 | 580 | if (!this.next()) 581 | { 582 | return undefined; 583 | } 584 | 585 | const element = this.value(); 586 | 587 | if (this.next()) 588 | { 589 | throw new Error("Sequence contains more than 1 element"); 590 | } 591 | 592 | return element; 593 | } 594 | 595 | public distinct(): IEnumerable; 596 | public distinct(keySelector: Selector): IEnumerable; 597 | public distinct(keySelector?: Selector): IEnumerable 598 | { 599 | return new UniqueEnumerable(this.copy(), keySelector); 600 | } 601 | 602 | public aggregate(aggregator: Aggregator): TOut; 603 | public aggregate(aggregator: Aggregator, initialValue: TValue): TValue; 604 | public aggregate(aggregator: Aggregator, initialValue?: TValue): TValue 605 | { 606 | let value = initialValue; 607 | 608 | this.reset(); 609 | 610 | if (initialValue === undefined) 611 | { 612 | if (!this.next()) 613 | { 614 | throw new Error("Sequence contains no elements"); 615 | } 616 | 617 | value = aggregator(value as TValue, this.value()); 618 | } 619 | 620 | while (this.next()) 621 | { 622 | value = aggregator(value as TValue, this.value()); 623 | } 624 | 625 | return value as TValue; 626 | } 627 | 628 | public min(): TOut; 629 | public min(selector: Selector): TSelectorOut; 630 | public min(selector?: Selector): TOut | TSelectorOut 631 | { 632 | if (selector !== undefined) 633 | { 634 | // Don't copy iterators 635 | return new TransformEnumerable(this, selector).min(); 636 | } 637 | 638 | return this.aggregate((previous, current) => 639 | (previous !== undefined && previous < current) 640 | ? previous 641 | : current); 642 | } 643 | 644 | public orderBy( 645 | keySelector: Selector): IOrderedEnumerable; 646 | public orderBy( 647 | keySelector: Selector, 648 | comparer: Comparer): IOrderedEnumerable; 649 | public orderBy( 650 | keySelector: Selector, 651 | comparer?: Comparer): IOrderedEnumerable 652 | { 653 | return new OrderedEnumerable(this.copy(), createComparer(keySelector, true, comparer)); 654 | } 655 | 656 | public orderByDescending( 657 | keySelector: Selector): IOrderedEnumerable 658 | { 659 | return new OrderedEnumerable(this.copy(), createComparer(keySelector, false, undefined)); 660 | } 661 | 662 | public max(): TOut; 663 | public max(selector: Selector): TSelectorOut; 664 | public max(selector?: Selector): TOut | TSelectorOut 665 | { 666 | if (selector !== undefined) 667 | { 668 | // Don't copy iterators 669 | return new TransformEnumerable(this, selector).max(); 670 | } 671 | 672 | return this.aggregate((previous, current) => 673 | (previous !== undefined && previous > current) 674 | ? previous 675 | : current); 676 | } 677 | 678 | public sum(selector: Selector): number 679 | { 680 | return this.aggregate( 681 | (previous: number, current: TOut) => previous + selector(current), 0); 682 | } 683 | 684 | public average(selector: Selector): number 685 | { 686 | this.reset(); 687 | 688 | if (!this.next()) 689 | { 690 | throw new Error("Sequence contains no elements"); 691 | } 692 | 693 | let sum = 0; 694 | let count = 0; 695 | 696 | do 697 | { 698 | sum += selector(this.value()); 699 | ++count; 700 | } 701 | while (this.next()); 702 | 703 | return sum / count; 704 | } 705 | 706 | public skip(amount: number): IEnumerable 707 | { 708 | return new RangeEnumerable(this.copy(), amount, undefined); 709 | } 710 | 711 | public take(amount: number): IEnumerable 712 | { 713 | return new RangeEnumerable(this.copy(), undefined, amount); 714 | } 715 | 716 | public takeWhile(predicate: Predicate): IEnumerable 717 | { 718 | return new TakeWhileEnumerable(this.copy(), predicate); 719 | } 720 | 721 | public union(other: IQueryable): IEnumerable 722 | { 723 | return new UniqueEnumerable(this.concat(other)); 724 | } 725 | 726 | public zip(other: IQueryable | TOther[], selector: ZipSelector): IEnumerable 727 | { 728 | const otherAsEnumerable = other instanceof Array 729 | ? new ArrayEnumerable(other) 730 | : other.asEnumerable(); 731 | 732 | return new ZippedEnumerable(this, otherAsEnumerable, selector); 733 | } 734 | } 735 | // endregion 736 | // region Enumerable 737 | export class Enumerable extends EnumerableBase 738 | { 739 | protected currentValue: Cached; 740 | 741 | public static fromSource(source: TElement[] | IIterable): IEnumerable 742 | { 743 | if (source instanceof Array) 744 | { 745 | return new ArrayEnumerable(source); 746 | } 747 | 748 | return new Enumerable(source); 749 | } 750 | 751 | public static empty(): IEnumerable 752 | { 753 | return Enumerable.fromSource([]); 754 | } 755 | 756 | public static range(start: number, count: number): IEnumerable; 757 | public static range(start: number, count: number, ascending: boolean): IEnumerable; 758 | public static range(start: number, count: number, ascending: boolean = true): IEnumerable 759 | { 760 | if (count < 0) 761 | { 762 | throw new Error("Count must be >= 0"); 763 | } 764 | 765 | const source = new Array(count); 766 | 767 | if (ascending) 768 | { 769 | // tslint:disable-next-line:curly 770 | for (let i = 0; i < count; source[i] = start + (i++)); 771 | } 772 | else 773 | { 774 | // tslint:disable-next-line:curly 775 | for (let i = 0; i < count; source[i] = start - (i++)); 776 | } 777 | 778 | return new ArrayEnumerable(source); 779 | } 780 | 781 | public static repeat(element: TElement, count: number): IEnumerable 782 | { 783 | if (count < 0) 784 | { 785 | throw new Error("Count must me >= 0"); 786 | } 787 | 788 | const source = new Array(count); 789 | 790 | for (let i = 0; i < count; ++i) 791 | { 792 | source[i] = element; 793 | } 794 | 795 | return new ArrayEnumerable(source); 796 | } 797 | 798 | public constructor(source: IIterable) 799 | { 800 | super(source); 801 | this.currentValue = new Cached(); 802 | } 803 | 804 | public copy(): IEnumerable 805 | { 806 | return new Enumerable(this.source.copy()); 807 | } 808 | 809 | public value(): TElement 810 | { 811 | if (!this.currentValue.isValid()) 812 | { 813 | this.currentValue.value = this.source.value(); 814 | } 815 | 816 | return this.currentValue.value; 817 | } 818 | 819 | public reset(): void 820 | { 821 | super.reset(); 822 | this.currentValue.invalidate(); 823 | } 824 | 825 | public next(): boolean 826 | { 827 | this.currentValue.invalidate(); 828 | 829 | return super.next(); 830 | } 831 | } 832 | // endregion 833 | // region ConditionalEnumerable 834 | export class ConditionalEnumerable extends Enumerable 835 | { 836 | protected source: IEnumerable; 837 | private _predicate: Predicate; 838 | 839 | public constructor(source: IEnumerable, predicate: Predicate) 840 | { 841 | super(source); 842 | this._predicate = predicate; 843 | } 844 | 845 | public copy(): ConditionalEnumerable 846 | { 847 | return new ConditionalEnumerable(this.source.copy(), this._predicate); 848 | } 849 | 850 | public next(): boolean 851 | { 852 | let hasValue: boolean; 853 | 854 | do 855 | { 856 | hasValue = super.next(); 857 | } 858 | while (hasValue && !this._predicate(this.value())); 859 | 860 | return hasValue; 861 | } 862 | } 863 | // endregion 864 | // region SkipWhileEnumerable 865 | export class SkipWhileEnumerable extends Enumerable 866 | { 867 | protected source: IEnumerable; 868 | private _predicate: Predicate; 869 | private _shouldContinueChecking: boolean; 870 | 871 | public constructor(source: IEnumerable, predicate: Predicate) 872 | { 873 | super(source); 874 | this._predicate = predicate; 875 | this._shouldContinueChecking = true; 876 | } 877 | 878 | public reset(): void 879 | { 880 | super.reset(); 881 | this._shouldContinueChecking = true; 882 | } 883 | 884 | public copy(): SkipWhileEnumerable 885 | { 886 | return new SkipWhileEnumerable(this.source.copy(), this._predicate); 887 | } 888 | 889 | public next(): boolean 890 | { 891 | if (!this._shouldContinueChecking) 892 | { 893 | return super.next(); 894 | } 895 | 896 | let hasValue: boolean; 897 | 898 | do 899 | { 900 | hasValue = super.next(); 901 | } 902 | while (hasValue && this._predicate(this.value())); 903 | 904 | this._shouldContinueChecking = false; 905 | 906 | return hasValue; 907 | } 908 | } 909 | // endregion 910 | // region TakeWhileEnumerable 911 | export class TakeWhileEnumerable extends Enumerable 912 | { 913 | protected source: IEnumerable; 914 | private _predicate: Predicate; 915 | private _shouldContinueTaking: boolean; 916 | 917 | public constructor(source: IEnumerable, predicate: Predicate) 918 | { 919 | super(source); 920 | this._predicate = predicate; 921 | this._shouldContinueTaking = true; 922 | } 923 | 924 | public reset(): void 925 | { 926 | super.reset(); 927 | this._shouldContinueTaking = true; 928 | } 929 | 930 | public copy(): TakeWhileEnumerable 931 | { 932 | return new TakeWhileEnumerable(this.source.copy(), this._predicate); 933 | } 934 | 935 | public next(): boolean 936 | { 937 | if (super.next()) 938 | { 939 | if (this._shouldContinueTaking && this._predicate(this.value())) 940 | { 941 | return true; 942 | } 943 | } 944 | 945 | this._shouldContinueTaking = false; 946 | 947 | return false; 948 | } 949 | } 950 | // endregion 951 | // region ConcatEnumerable 952 | export class ConcatEnumerable extends Enumerable 953 | { 954 | private _otherSource: IIterable; 955 | private _isFirstSourceFinished: boolean; 956 | 957 | public constructor(left: IIterable, right: IIterable) 958 | { 959 | super(left); 960 | this._otherSource = right; 961 | this._isFirstSourceFinished = false; 962 | } 963 | 964 | public copy(): ConcatEnumerable 965 | { 966 | return new ConcatEnumerable(this.source.copy(), this._otherSource.copy()); 967 | } 968 | 969 | public reset(): void 970 | { 971 | this.source.reset(); 972 | this._otherSource.reset(); 973 | this._isFirstSourceFinished = false; 974 | this.currentValue.invalidate(); 975 | } 976 | 977 | public next(): boolean 978 | { 979 | this.currentValue.invalidate(); 980 | 981 | const hasValue = !this._isFirstSourceFinished 982 | ? this.source.next() 983 | : this._otherSource.next(); 984 | 985 | if (!hasValue && !this._isFirstSourceFinished) 986 | { 987 | this._isFirstSourceFinished = true; 988 | 989 | return this.next(); 990 | } 991 | 992 | return hasValue; 993 | } 994 | 995 | public value(): TElement 996 | { 997 | if (!this.currentValue.isValid()) 998 | { 999 | this.currentValue.value = !this._isFirstSourceFinished 1000 | ? this.source.value() 1001 | : this._otherSource.value(); 1002 | } 1003 | 1004 | return this.currentValue.value; 1005 | } 1006 | } 1007 | // endregion 1008 | // region UniqueEnumerable 1009 | export class UniqueEnumerable extends Enumerable 1010 | { 1011 | protected source: IEnumerable; 1012 | private _seen: { primitive: Dynamic, complex: Array }; 1013 | private _keySelector: Selector | undefined; 1014 | 1015 | public constructor(source: IEnumerable, keySelector?: Selector) 1016 | { 1017 | super(source); 1018 | this._keySelector = keySelector; 1019 | this._seen = { primitive: { number: {}, string: {}, boolean: {} }, complex: [] }; 1020 | } 1021 | 1022 | public copy(): UniqueEnumerable 1023 | { 1024 | return new UniqueEnumerable(this.source.copy(), this._keySelector); 1025 | } 1026 | 1027 | public reset(): void 1028 | { 1029 | super.reset(); 1030 | this._seen = { primitive: { number: {}, string: {}, boolean: {} }, complex: [] }; 1031 | } 1032 | 1033 | private isUnique(element: TElement): boolean 1034 | { 1035 | const key = this._keySelector !== undefined 1036 | ? this._keySelector(element) 1037 | : element; 1038 | const type = typeof key; 1039 | 1040 | return (type in this._seen.primitive) 1041 | ? this._seen.primitive[type].hasOwnProperty(key) 1042 | ? false 1043 | : this._seen.primitive[type][key] = true 1044 | : this._seen.complex.indexOf(key) !== -1 1045 | ? false 1046 | : this._seen.complex.push(key) > -1; 1047 | } 1048 | 1049 | public next(): boolean 1050 | { 1051 | let hasValue: boolean; 1052 | 1053 | do 1054 | { 1055 | hasValue = super.next(); 1056 | } 1057 | while (hasValue && !this.isUnique(this.value())); 1058 | 1059 | return hasValue; 1060 | } 1061 | } 1062 | // endregion 1063 | // region RangeEnumerable 1064 | export class RangeEnumerable extends Enumerable 1065 | { 1066 | protected source: IEnumerable; 1067 | private _start: number | undefined; 1068 | private _count: number | undefined; 1069 | private _currentIndex: number; 1070 | 1071 | public constructor(source: IEnumerable, start: number | undefined, count: number | undefined) 1072 | { 1073 | if ((start !== undefined && start < 0) || (count !== undefined && count < 0)) 1074 | { 1075 | throw new Error("Incorrect parameters"); 1076 | } 1077 | 1078 | super(source); 1079 | this._start = start; 1080 | this._count = count; 1081 | this._currentIndex = -1; 1082 | } 1083 | 1084 | public copy(): RangeEnumerable 1085 | { 1086 | return new RangeEnumerable(this.source.copy(), this._start, this._count); 1087 | } 1088 | 1089 | public reset(): void 1090 | { 1091 | super.reset(); 1092 | this._currentIndex = -1; 1093 | } 1094 | 1095 | private isValidIndex(): boolean 1096 | { 1097 | const start = this._start !== undefined ? this._start : 0; 1098 | const end = this._count !== undefined ? start + this._count : undefined; 1099 | 1100 | return this._currentIndex >= start && (end === undefined || this._currentIndex < end); 1101 | } 1102 | 1103 | private performSkip(): boolean 1104 | { 1105 | const start = this._start !== undefined ? this._start : 0; 1106 | let hasValue: boolean = true; 1107 | 1108 | while (hasValue && this._currentIndex + 1 < start) 1109 | { 1110 | hasValue = super.next(); 1111 | ++this._currentIndex; 1112 | } 1113 | 1114 | return hasValue; 1115 | } 1116 | 1117 | public next(): boolean 1118 | { 1119 | if (this._currentIndex < 0 && !this.performSkip()) 1120 | { 1121 | return false; 1122 | } 1123 | 1124 | ++this._currentIndex; 1125 | 1126 | return super.next() && this.isValidIndex(); 1127 | } 1128 | 1129 | public value(): TElement 1130 | { 1131 | if (!this.isValidIndex()) 1132 | { 1133 | throw new Error("Out of bounds"); 1134 | } 1135 | 1136 | return super.value(); 1137 | } 1138 | } 1139 | // endregion 1140 | // region TransformEnumerable 1141 | export class TransformEnumerable extends EnumerableBase 1142 | { 1143 | protected source: IEnumerable; 1144 | private _transform: Selector; 1145 | private _currentValue: Cached; 1146 | 1147 | public constructor(source: IEnumerable, transform: Selector) 1148 | { 1149 | super(source); 1150 | this._transform = transform; 1151 | this._currentValue = new Cached(); 1152 | } 1153 | 1154 | public copy(): TransformEnumerable 1155 | { 1156 | return new TransformEnumerable(this.source.copy(), this._transform); 1157 | } 1158 | 1159 | public value(): TOut 1160 | { 1161 | if (!this._currentValue.isValid()) 1162 | { 1163 | this._currentValue.value = this._transform(this.source.value()); 1164 | } 1165 | 1166 | return this._currentValue.value; 1167 | } 1168 | 1169 | public reset(): void 1170 | { 1171 | super.reset(); 1172 | this._currentValue.invalidate(); 1173 | } 1174 | 1175 | public next(): boolean 1176 | { 1177 | this._currentValue.invalidate(); 1178 | 1179 | return super.next(); 1180 | } 1181 | } 1182 | // endregion 1183 | // region ReverseEnumerable 1184 | export class ReverseEnumerable extends Enumerable 1185 | { 1186 | protected source: IEnumerable; 1187 | private _elements: Cached; 1188 | private _currentIndex: number; 1189 | 1190 | public constructor(source: IEnumerable) 1191 | { 1192 | super(source); 1193 | this._elements = new Cached(); 1194 | this._currentIndex = -1; 1195 | } 1196 | 1197 | public copy(): IEnumerable 1198 | { 1199 | return new ReverseEnumerable(this.source.copy()); 1200 | } 1201 | 1202 | public reset(): void 1203 | { 1204 | this._elements.invalidate(); 1205 | this._currentIndex = -1; 1206 | } 1207 | 1208 | private isValidIndex(): boolean 1209 | { 1210 | return this._currentIndex >= 0 1211 | && this._currentIndex < this._elements.value.length; 1212 | } 1213 | 1214 | public all(predicate: Predicate): boolean 1215 | { 1216 | return this.source.all(predicate); 1217 | } 1218 | 1219 | public any(): boolean; 1220 | public any(predicate: Predicate): boolean; 1221 | public any(predicate?: Predicate): boolean 1222 | { 1223 | if (predicate !== undefined) 1224 | { 1225 | return this.source.any(predicate); 1226 | } 1227 | 1228 | return this.source.any(); 1229 | } 1230 | 1231 | public average(selector: Selector): number 1232 | { 1233 | return this.source.average(selector); 1234 | } 1235 | 1236 | public count(): number; 1237 | public count(predicate: Predicate): number; 1238 | public count(predicate?: Predicate): number 1239 | { 1240 | if (predicate !== undefined) 1241 | { 1242 | return this.source.count(predicate); 1243 | } 1244 | 1245 | return this.source.count(); 1246 | } 1247 | 1248 | public max(): TElement; 1249 | public max(selector: Selector): TSelectorOut; 1250 | public max(selector?: Selector): TElement | TSelectorOut 1251 | { 1252 | if (selector !== undefined) 1253 | { 1254 | return this.source.max(selector); 1255 | } 1256 | 1257 | return this.source.max(); 1258 | } 1259 | 1260 | public min(): TElement; 1261 | public min(selector: Selector): TSelectorOut; 1262 | public min(selector?: Selector): TElement | TSelectorOut 1263 | { 1264 | if (selector !== undefined) 1265 | { 1266 | return this.source.min(selector); 1267 | } 1268 | 1269 | return this.source.min(); 1270 | } 1271 | 1272 | public reverse(): IEnumerable 1273 | { 1274 | return this.source.copy(); // haha so smart 1275 | } 1276 | 1277 | public sum(selector: Selector): number 1278 | { 1279 | return this.source.sum(selector); 1280 | } 1281 | 1282 | public next(): boolean 1283 | { 1284 | if (!this._elements.isValid()) 1285 | { 1286 | this._elements.value = this.source.toArray(); 1287 | } 1288 | 1289 | ++this._currentIndex; 1290 | 1291 | return this.isValidIndex(); 1292 | } 1293 | 1294 | public value(): TElement 1295 | { 1296 | if (!this._elements.isValid() || !this.isValidIndex()) 1297 | { 1298 | throw new Error("Out of bounds"); 1299 | } 1300 | 1301 | return this._elements.value[(this._elements.value.length - 1) - this._currentIndex]; 1302 | } 1303 | } 1304 | // endregion 1305 | // region OrderedEnumerable 1306 | export class OrderedEnumerable 1307 | extends EnumerableBase 1308 | implements IOrderedEnumerable 1309 | { 1310 | protected source: IEnumerable; 1311 | private _comparer: Comparer; 1312 | private _elements: Cached; 1313 | private _currentIndex: number; 1314 | 1315 | public constructor(source: IEnumerable, comparer: Comparer) 1316 | { 1317 | super(source); 1318 | 1319 | this._comparer = comparer; 1320 | this._elements = new Cached(); 1321 | this._currentIndex = -1; 1322 | } 1323 | 1324 | private isValidIndex(): boolean 1325 | { 1326 | return this._currentIndex >= 0 1327 | && this._currentIndex < this._elements.value.length; 1328 | } 1329 | 1330 | public orderBy( 1331 | keySelector: Selector): IOrderedEnumerable; 1332 | public orderBy( 1333 | keySelector: Selector, 1334 | comparer: Comparer): IOrderedEnumerable; 1335 | public orderBy( 1336 | keySelector: Selector, 1337 | comparer?: Comparer): IOrderedEnumerable 1338 | { 1339 | return new OrderedEnumerable(this.source.copy(), createComparer(keySelector, true, comparer)); 1340 | } 1341 | 1342 | public orderByDescending( 1343 | keySelector: Selector): IOrderedEnumerable 1344 | { 1345 | return new OrderedEnumerable(this.source.copy(), createComparer(keySelector, false, undefined)); 1346 | } 1347 | 1348 | public thenBy( 1349 | keySelector: Selector): IOrderedEnumerable; 1350 | public thenBy( 1351 | keySelector: Selector, 1352 | comparer: Comparer): IOrderedEnumerable; 1353 | public thenBy( 1354 | keySelector: Selector, 1355 | comparer?: Comparer): IOrderedEnumerable 1356 | { 1357 | return new OrderedEnumerable( 1358 | this.source.copy(), 1359 | combineComparers(this._comparer, createComparer(keySelector, true, comparer))); 1360 | } 1361 | 1362 | public thenByDescending( 1363 | keySelector: Selector): IOrderedEnumerable 1364 | { 1365 | return new OrderedEnumerable( 1366 | this.source.copy(), 1367 | combineComparers(this._comparer, createComparer(keySelector, false, undefined))); 1368 | } 1369 | 1370 | public reset(): void 1371 | { 1372 | this._elements.invalidate(); 1373 | this._currentIndex = -1; 1374 | } 1375 | 1376 | public copy(): IOrderedEnumerable 1377 | { 1378 | return new OrderedEnumerable(this.source.copy(), this._comparer); 1379 | } 1380 | 1381 | public value(): TElement 1382 | { 1383 | if (!this._elements.isValid() || !this.isValidIndex()) 1384 | { 1385 | throw new Error("Out of bounds"); 1386 | } 1387 | 1388 | return this._elements.value[this._currentIndex]; 1389 | } 1390 | 1391 | public next(): boolean 1392 | { 1393 | if (!this._elements.isValid()) 1394 | { 1395 | this._elements.value = this.toArray(); 1396 | } 1397 | 1398 | ++this._currentIndex; 1399 | 1400 | return this.isValidIndex(); 1401 | } 1402 | 1403 | public toArray(): TElement[] 1404 | { 1405 | // Allocate the array before sorting 1406 | // It's faster than working with anonymous reference 1407 | const result = this.source.toArray(); 1408 | 1409 | return result.sort(this._comparer); 1410 | } 1411 | } 1412 | // endregion 1413 | // region ArrayEnumerable 1414 | export class ArrayEnumerable extends Enumerable 1415 | { 1416 | protected list: List; 1417 | 1418 | public constructor(source: TOut[]) 1419 | { 1420 | super(new ArrayIterator(source)); 1421 | 1422 | this.list = new List(source); 1423 | } 1424 | 1425 | public toArray(): TOut[] 1426 | { 1427 | return this.list.toArray(); 1428 | } 1429 | 1430 | public aggregate(aggregator: Aggregator): TOut; 1431 | public aggregate(aggregator: Aggregator, initialValue: TValue): TValue; 1432 | public aggregate( 1433 | aggregator: Aggregator, 1434 | initialValue?: TValue): TValue | TOut 1435 | { 1436 | if (initialValue !== undefined) 1437 | { 1438 | return this.list.aggregate( 1439 | aggregator as Aggregator, 1440 | initialValue as TValue); 1441 | } 1442 | 1443 | return this.list.aggregate(aggregator as Aggregator); 1444 | } 1445 | 1446 | public any(): boolean; 1447 | public any(predicate: Predicate): boolean; 1448 | public any(predicate?: Predicate): boolean 1449 | { 1450 | if (predicate !== undefined) 1451 | { 1452 | return this.list.any(predicate); 1453 | } 1454 | 1455 | return this.list.any(); 1456 | } 1457 | 1458 | public all(predicate: Predicate): boolean 1459 | { 1460 | return this.list.all(predicate); 1461 | } 1462 | 1463 | public average(selector: Selector): number 1464 | { 1465 | return this.list.average(selector); 1466 | } 1467 | 1468 | public count(): number; 1469 | public count(predicate: Predicate): number; 1470 | public count(predicate?: Predicate): number 1471 | { 1472 | if (predicate !== undefined) 1473 | { 1474 | return this.list.count(predicate); 1475 | } 1476 | 1477 | return this.list.count(); 1478 | } 1479 | 1480 | public copy(): IEnumerable 1481 | { 1482 | return new ArrayEnumerable(this.list.asArray()); 1483 | } 1484 | 1485 | public elementAtOrDefault(index: number): TOut | undefined 1486 | { 1487 | return this.list.elementAtOrDefault(index); 1488 | } 1489 | 1490 | public firstOrDefault(): TOut | undefined; 1491 | public firstOrDefault(predicate: Predicate): TOut | undefined; 1492 | public firstOrDefault(predicate?: Predicate): TOut | undefined 1493 | { 1494 | if (predicate !== undefined) 1495 | { 1496 | return this.list.firstOrDefault(predicate); 1497 | } 1498 | 1499 | return this.list.firstOrDefault(); 1500 | } 1501 | 1502 | public lastOrDefault(): TOut | undefined; 1503 | public lastOrDefault(predicate: Predicate): TOut | undefined; 1504 | public lastOrDefault(predicate?: Predicate): TOut | undefined 1505 | { 1506 | if (predicate !== undefined) 1507 | { 1508 | return this.list.lastOrDefault(predicate); 1509 | } 1510 | 1511 | return this.list.lastOrDefault(); 1512 | } 1513 | } 1514 | // endregion 1515 | // region DefaultIfEmptyEnumerable 1516 | export class DefaultIfEmptyEnumerable extends EnumerableBase 1517 | { 1518 | protected source: IEnumerable; 1519 | private _mustUseDefaultValue: boolean | undefined; 1520 | private _defaultValue: TOut | undefined; 1521 | 1522 | public constructor(source: IEnumerable, defaultValue?: TOut) 1523 | { 1524 | super(source); 1525 | this._mustUseDefaultValue = undefined; 1526 | this._defaultValue = defaultValue; 1527 | } 1528 | 1529 | public copy(): DefaultIfEmptyEnumerable 1530 | { 1531 | return new DefaultIfEmptyEnumerable(this.source.copy(), this._defaultValue); 1532 | } 1533 | 1534 | public value(): TOut | undefined 1535 | { 1536 | if (this._mustUseDefaultValue) 1537 | { 1538 | return this._defaultValue; 1539 | } 1540 | 1541 | return this.source.value(); 1542 | } 1543 | 1544 | public next(): boolean 1545 | { 1546 | const hasNextElement = super.next(); 1547 | 1548 | // single default element 1549 | this._mustUseDefaultValue = this._mustUseDefaultValue === undefined && !hasNextElement; 1550 | 1551 | return this._mustUseDefaultValue || hasNextElement; 1552 | } 1553 | 1554 | public reset(): void 1555 | { 1556 | super.reset(); 1557 | this._mustUseDefaultValue = undefined; 1558 | } 1559 | } 1560 | // endregion 1561 | // region ZippedEnumerable 1562 | export class ZippedEnumerable extends EnumerableBase 1563 | { 1564 | private _otherSource: IIterable; 1565 | private _isOneOfTheSourcesFinished: boolean; 1566 | private _currentValue: Cached; 1567 | private _selector: ZipSelector; 1568 | 1569 | public constructor(source: IIterable, otherSource: IIterable, selector: ZipSelector) 1570 | { 1571 | super(source); 1572 | this._otherSource = otherSource; 1573 | this._isOneOfTheSourcesFinished = false; 1574 | this._currentValue = new Cached(); 1575 | this._selector = selector; 1576 | } 1577 | 1578 | public copy(): ZippedEnumerable 1579 | { 1580 | return new ZippedEnumerable(this.source.copy(), this._otherSource.copy(), this._selector); 1581 | } 1582 | 1583 | public value(): TOut 1584 | { 1585 | if (!this._currentValue.isValid()) 1586 | { 1587 | this._currentValue.value = this._selector(this.source.value(), this._otherSource.value()); 1588 | } 1589 | 1590 | return this._currentValue.value; 1591 | } 1592 | 1593 | public reset(): void 1594 | { 1595 | super.reset(); 1596 | this._otherSource.reset(); 1597 | this._isOneOfTheSourcesFinished = false; 1598 | this._currentValue.invalidate(); 1599 | } 1600 | 1601 | public next(): boolean 1602 | { 1603 | this._currentValue.invalidate(); 1604 | 1605 | if (!this._isOneOfTheSourcesFinished) 1606 | { 1607 | this._isOneOfTheSourcesFinished = !super.next() || !this._otherSource.next(); 1608 | } 1609 | 1610 | return !this._isOneOfTheSourcesFinished; 1611 | } 1612 | } 1613 | // endregion 1614 | -------------------------------------------------------------------------------- /src/Iterators.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by Ivan Sanz (@isc30) 3 | * Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | */ 5 | 6 | export interface IIterable 7 | { 8 | copy(): IIterable; 9 | reset(): void; 10 | next(): boolean; 11 | value(): TElement; 12 | } 13 | 14 | /* ES6 compatibility layer :D 15 | interface IteratorResult 16 | { 17 | done: boolean; 18 | value: T; 19 | } 20 | 21 | interface Iterator 22 | { 23 | next(value?: any): IteratorResult; 24 | return?(value?: any): IteratorResult; 25 | throw?(e?: any): IteratorResult; 26 | }*/ 27 | 28 | export class ArrayIterator implements IIterable 29 | { 30 | protected readonly source: TElement[]; 31 | private _index: number; 32 | 33 | public constructor(source: TElement[]) 34 | { 35 | this.source = source; 36 | this.reset(); 37 | } 38 | 39 | public copy(): IIterable 40 | { 41 | return new ArrayIterator(this.source); 42 | } 43 | 44 | public reset(): void 45 | { 46 | this._index = -1; 47 | } 48 | 49 | private isValidIndex(): boolean 50 | { 51 | return this._index >= 0 && this._index < this.source.length; 52 | } 53 | 54 | public next(): boolean 55 | { 56 | ++this._index; 57 | 58 | return this.isValidIndex(); 59 | } 60 | 61 | public value(): TElement 62 | { 63 | if (!this.isValidIndex()) 64 | { 65 | throw new Error("Out of bounds"); 66 | } 67 | 68 | return this.source[this._index]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Linq.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by Ivan Sanz (@isc30) 3 | * Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | */ 5 | 6 | export { IQueryable, IKeyValue, IGrouping, IEnumerable, IOrderedEnumerable, Enumerable } from "./Enumerables"; 7 | export { IIterable, ArrayIterator } from "./Iterators"; 8 | export { IReadOnlyList, IList, List, IReadOnlyDictionary, IDictionary, Dictionary, IStack, Stack } from "./Collections"; 9 | export { Comparer, ComparerResult, EqualityComparer, strictEqualityComparer } from './Comparers'; 10 | export * from './Types'; 11 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by Ivan Sanz (@isc30) 3 | * Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | */ 5 | 6 | export type Dynamic = any; 7 | export type Type = "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function" | "bigint"; 8 | export type Indexer = number | string; 9 | export type Primitive = number | string | boolean; 10 | export type Selector = (element: TElement) => TOut; 11 | export type ZipSelector = (first: TElement, second: TOther) => TOut; 12 | export type Predicate = Selector; 13 | export type Aggregator = (previous: TValue, current: TElement) => TValue; 14 | export type Action = (element: TElement, index: number) => void; 15 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Created by Ivan Sanz (@isc30) 3 | * Copyright © 2017 Ivan Sanz Carasa. All rights reserved. 4 | */ 5 | 6 | /*export function Lazy(factory: () => T): () => T 7 | { 8 | let instance: T; 9 | 10 | return () => instance !== undefined 11 | ? instance 12 | : (instance = factory()); 13 | }*/ 14 | 15 | export class Cached 16 | { 17 | private _isValid: boolean; 18 | private _value: T; 19 | 20 | public constructor() 21 | { 22 | this._isValid = false; 23 | } 24 | 25 | public invalidate(): void 26 | { 27 | this._isValid = false; 28 | } 29 | 30 | public isValid(): boolean 31 | { 32 | return this._isValid; 33 | } 34 | 35 | public get value(): T 36 | { 37 | if (!this._isValid) 38 | { 39 | throw new Error("Trying to get value of invalid cache"); 40 | } 41 | 42 | return this._value; 43 | } 44 | 45 | public set value(value: T) 46 | { 47 | this._value = value; 48 | this._isValid = true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Test.ts: -------------------------------------------------------------------------------- 1 | export namespace Test 2 | { 3 | export function isTrue(result: boolean): void 4 | { 5 | if (result !== true) 6 | { 7 | throw new Error("Assertion failed"); 8 | } 9 | } 10 | 11 | export function isFalse(result: boolean): void 12 | { 13 | isTrue(result === false); 14 | } 15 | 16 | export function isEqual(first: any, second: any): void 17 | { 18 | isTrue(first === second); 19 | } 20 | 21 | export function isNotEqual(first: any, second: any): void 22 | { 23 | isFalse(first === second); 24 | } 25 | 26 | export function isArrayEqual(left: T[], right: T[]): void 27 | { 28 | isTrue(left.length === right.length 29 | && left.every((e: T, i: number) => e === right[i])); 30 | } 31 | 32 | export function isArrayNotEqual(left: T[], right: T[]): void 33 | { 34 | isFalse(left.length === right.length 35 | && left.every((e: T, i: number) => e === right[i])); 36 | } 37 | 38 | export function throwsException(call: () => void): void 39 | { 40 | try 41 | { 42 | call(); 43 | } 44 | catch (ex) 45 | { 46 | return; 47 | } 48 | 49 | throw new Error("Exception was expected"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/TestSuite.ts: -------------------------------------------------------------------------------- 1 | import { UtilsUnitTest } from "./unitary/Utils.test"; 2 | import { IteratorUnitTest } from "./unitary/Iterator.test"; 3 | import { EnumerableUnitTest } from "./unitary/Enumerable.test"; 4 | import { IQueryableUnitTest } from "./unitary/IQueryable.test"; 5 | import { ListUnitTest } from "./unitary/List.test"; 6 | import { StackUnitTest } from "./unitary/Stack.test"; 7 | import { IEnumerableIntegrationTest } from "./integration/IEnumerable.test"; 8 | import { DictionaryUnitTest } from "./unitary/Dictionary.test"; 9 | 10 | describe("Unit Tests", () => 11 | { 12 | describe("Utils", UtilsUnitTest.run); 13 | describe("Enumerable (static)", EnumerableUnitTest.run); 14 | describe("Iterators", IteratorUnitTest.run); 15 | describe("IQueryable", IQueryableUnitTest.run); 16 | describe("List", ListUnitTest.run); 17 | describe("Stack", StackUnitTest.run); 18 | describe("Dictionary", DictionaryUnitTest.run); 19 | }); 20 | 21 | describe("Integration Tests", () => 22 | { 23 | describe("IEnumerable", IEnumerableIntegrationTest.run); 24 | }); 25 | -------------------------------------------------------------------------------- /test/integration/IEnumerable.test.ts: -------------------------------------------------------------------------------- 1 | import { Enumerable } from "../../src/Enumerables"; 2 | import { Test } from "../Test"; 3 | 4 | export namespace IEnumerableIntegrationTest 5 | { 6 | interface IPerson 7 | { 8 | name: string; 9 | age: number; 10 | aliases: string[]; 11 | } 12 | 13 | export function run(): void 14 | { 15 | it("Where + Select", whereSelect); 16 | it("SelectMany + Where", selectManyWhere); 17 | it("DistinctBy + Select + Max", distinctBySelectMax); 18 | it("Where + OrderBy + Select", whereOrderBySelect); 19 | } 20 | 21 | function whereSelect(): void 22 | { 23 | const persons = Enumerable.fromSource([ 24 | { name: "Ivan", age: 21, aliases: ["isc", "isc30", "ivansanz"] }, 25 | { name: "Antonio", age: 31, aliases: ["tony"] }, 26 | { name: "Ana", age: 17, aliases: ["anita", "ana no se"] }, 27 | { name: "Pedro", age: 8, aliases: ["pica", "piedra"] }, 28 | ]); 29 | 30 | const kids = persons 31 | .where(p => p.age < 18) 32 | .select(p => `${p.name} (${p.age})`) 33 | .toArray(); 34 | 35 | Test.isArrayEqual(kids, ["Ana (17)", "Pedro (8)"]); 36 | } 37 | 38 | function selectManyWhere(): void 39 | { 40 | const persons = Enumerable.fromSource([ 41 | { name: "Ivan", age: 21, aliases: ["isc", "isc30", "ivansanz"] }, 42 | { name: "Antonio", age: 31, aliases: ["tony"] }, 43 | { name: "Ana", age: 17, aliases: ["anita", "ana no se"] }, 44 | { name: "Pedro", age: 8, aliases: ["pica", "piedra"] }, 45 | ]); 46 | 47 | const smallAliases = persons 48 | .selectMany(p => p.aliases) 49 | .where(a => a.length <= 4) 50 | .toArray(); 51 | 52 | Test.isArrayEqual(smallAliases, ["isc", "tony", "pica"]); 53 | } 54 | 55 | function distinctBySelectMax(): void 56 | { 57 | const persons = Enumerable.fromSource([ 58 | { name: "Ivan", age: 21, aliases: ["isc", "isc30", "ivansanz"] }, 59 | { name: "Antonio", age: 31, aliases: ["tony"] }, 60 | { name: "Ana", age: 31, aliases: ["anita", "ana no se"] }, 61 | { name: "Pedro", age: 21, aliases: ["pica", "piedra"] }, 62 | ]); 63 | 64 | const max = persons 65 | .distinct(p => p.age) 66 | .select(p => p.age + 5) 67 | .select(a => a + 2) 68 | .max(); 69 | 70 | Test.isEqual(max, 31 + 5 + 2); 71 | } 72 | 73 | function whereOrderBySelect(): void 74 | { 75 | const persons = Enumerable.fromSource([ 76 | { name: "Ivan", age: 21, aliases: ["isc", "isc30", "ivansanz"] }, 77 | { name: "Antonio", age: 38, aliases: ["tony"] }, 78 | { name: "Ana", age: 18, aliases: ["anita", "ana no se"] }, 79 | { name: "Pedro", age: 9, aliases: ["pica", "piedra"] }, 80 | ]); 81 | 82 | const childToAdult = persons 83 | .where(p => p.age < 30) 84 | .orderBy(p => p.age) 85 | .select(p => p.name) 86 | .select(n => n + "!") 87 | .toArray(); 88 | 89 | Test.isArrayEqual(childToAdult, ["Pedro!", "Ana!", "Ivan!"]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/unitary/Dictionary.test.ts: -------------------------------------------------------------------------------- 1 | import { Test } from "./../Test"; 2 | import { Dictionary } from "./../../src/Collections"; 3 | 4 | export namespace DictionaryUnitTest 5 | { 6 | export function run(): void 7 | { 8 | describe("FromJsObject", fromJsObject); 9 | describe("AsReadOnly", asReadOnly); 10 | describe("Copy", copy); 11 | describe("Clear", clear); 12 | describe("Get", get); 13 | describe("Set", set); 14 | describe("SetOrUpdate", setOrUpdate); 15 | describe("ContainsKey", containsKey); 16 | describe("ContainsValue", containsValue); 17 | describe("GetKeys", getKeys); 18 | describe("GetValues", getValues); 19 | describe("Remove", remove); 20 | } 21 | 22 | function fromJsObject(): void 23 | { 24 | it("Empty object", () => 25 | { 26 | const dic = Dictionary.fromJsObject({}); 27 | 28 | Test.isArrayEqual(dic.getKeys().toArray(), []); 29 | }); 30 | 31 | it("Single property (string)", () => 32 | { 33 | const dic = Dictionary.fromJsObject({ 34 | hello: "hola", 35 | }); 36 | 37 | Test.isArrayEqual(dic.getKeys().toArray(), ["hello"]); 38 | Test.isEqual(dic.get("hello"), "hola"); 39 | }); 40 | 41 | it("Single property (number)", () => 42 | { 43 | const dic = Dictionary.fromJsObject({ 44 | hello: 123, 45 | }); 46 | 47 | Test.isArrayEqual(dic.getKeys().toArray(), ["hello"]); 48 | Test.isEqual(dic.get("hello"), 123); 49 | }); 50 | 51 | it("Multiple properties (string)", () => 52 | { 53 | const dic = Dictionary.fromJsObject({ 54 | hello: "hola", 55 | bye: "adios" 56 | }); 57 | 58 | Test.isArrayEqual(dic.getKeys().toArray(), ["hello", "bye"]); 59 | Test.isArrayEqual(dic.getValues().toArray(), ["hola", "adios"]); 60 | Test.isEqual(dic.get("hello"), "hola"); 61 | Test.isEqual(dic.get("bye"), "adios"); 62 | }); 63 | 64 | it("Multiple properties (number)", () => 65 | { 66 | const dic = Dictionary.fromJsObject({ 67 | hello: 123, 68 | bye: 666, 69 | }); 70 | 71 | Test.isArrayEqual(dic.getKeys().toArray(), ["hello", "bye"]); 72 | Test.isArrayEqual(dic.getValues().toArray(), [123, 666]); 73 | Test.isEqual(dic.get("hello"), 123); 74 | Test.isEqual(dic.get("bye"), 666); 75 | }); 76 | } 77 | 78 | function asReadOnly(): void 79 | { 80 | it("Same object but different interface", () => 81 | { 82 | const dic = new Dictionary([ 83 | { key: "hello", value: "yesssss" } 84 | ]); 85 | 86 | Test.isEqual(dic, dic.asReadOnly()); 87 | }); 88 | } 89 | 90 | function copy(): void 91 | { 92 | it("Type is a Dictionary", () => 93 | { 94 | const dic = new Dictionary(); 95 | Test.isTrue(dic instanceof Dictionary); 96 | }); 97 | 98 | it("Returns a copy, not a reference", () => 99 | { 100 | const dic = new Dictionary(); 101 | const copy = dic.copy(); 102 | 103 | Test.isArrayEqual(dic.toArray(), copy.toArray()); 104 | dic.set("lol", 666); 105 | Test.isArrayNotEqual(dic.toArray(), copy.toArray()); 106 | Test.isArrayEqual(dic.toList().select(t => t.value).toArray(), [666]); 107 | Test.isArrayEqual(copy.toList().select(t => t.value).toArray(), []); 108 | }); 109 | } 110 | 111 | function clear(): void 112 | { 113 | it("Does nothing on empty dictionary", () => 114 | { 115 | const dic = new Dictionary(); 116 | Test.isArrayEqual(dic.toArray(), []); 117 | 118 | dic.clear(); 119 | Test.isArrayEqual(dic.toArray(), []); 120 | 121 | dic.clear(); 122 | Test.isArrayEqual(dic.toArray(), []); 123 | }); 124 | 125 | it("Dictionary is cleared", () => 126 | { 127 | const dic = new Dictionary([ 128 | { key: 1, value: 1 }, 129 | { key: 2, value: 2 }, 130 | ]); 131 | 132 | Test.isArrayEqual(dic.select(p => p.value).toArray(), [1, 2]); 133 | dic.clear(); 134 | Test.isArrayEqual(dic.toArray(), []); 135 | }); 136 | } 137 | 138 | function get(): void 139 | { 140 | it("Value is correct (number)", () => 141 | { 142 | const dic = new Dictionary([ 143 | { key: 1, value: 1 }, 144 | { key: 2, value: 2 }, 145 | ]); 146 | 147 | Test.isEqual(dic.get(1), 1); 148 | Test.isEqual(dic.get(2), 2); 149 | }); 150 | 151 | it("Value is correct (string)", () => 152 | { 153 | const dic = new Dictionary([ 154 | { key: "Hello", value: "Hola" }, 155 | { key: "Bye", value: "Adios" }, 156 | ]); 157 | 158 | Test.isEqual(dic.get("Hello"), "Hola"); 159 | Test.isEqual(dic.get("Bye"), "Adios"); 160 | }); 161 | 162 | it("Exception if invalid key", () => 163 | { 164 | const dic = new Dictionary([ 165 | { key: "Hello", value: "Hola" }, 166 | ]); 167 | 168 | Test.throwsException(() => dic.get("Bye")); 169 | Test.throwsException(() => dic.get(":(")); 170 | }); 171 | } 172 | 173 | function set(): void 174 | { 175 | it("Value is set correctly", () => 176 | { 177 | const dic = new Dictionary(); 178 | 179 | dic.set("hola", "hello"); 180 | Test.isEqual(dic.get("hola"), "hello"); 181 | }); 182 | 183 | it("Throws exception if key already exists", () => 184 | { 185 | const dic = new Dictionary([ 186 | { key: "hola", value: "hello" }, 187 | ]); 188 | 189 | Test.throwsException(() => dic.set("hola", "nope")); 190 | }); 191 | } 192 | 193 | function setOrUpdate(): void 194 | { 195 | it("Value is set correctly", () => 196 | { 197 | const dic = new Dictionary(); 198 | 199 | dic.setOrUpdate("hola", "hello"); 200 | Test.isEqual(dic.get("hola"), "hello"); 201 | }); 202 | 203 | it("Replace value if key already exists", () => 204 | { 205 | const dic = new Dictionary([ 206 | { key: "hola", value: "hello" }, 207 | ]); 208 | 209 | dic.setOrUpdate("hola", "nope"); 210 | Test.isEqual(dic.get("hola"), "nope"); 211 | }); 212 | } 213 | 214 | function containsKey(): void 215 | { 216 | it("Returns correct value", () => 217 | { 218 | const dic = new Dictionary([ 219 | { key: 1, value: 101 }, 220 | { key: 22, value: 122 }, 221 | ]); 222 | 223 | Test.isTrue(dic.containsKey(1)); 224 | Test.isTrue(dic.containsKey(22)); 225 | Test.isFalse(dic.containsKey(-1)); 226 | Test.isFalse(dic.containsKey(0)); 227 | Test.isFalse(dic.containsKey(2)); 228 | Test.isFalse(dic.containsKey(101)); 229 | Test.isFalse(dic.containsKey(122)); 230 | 231 | Test.isFalse(dic.containsKey(5)); 232 | dic.set(5, 105); 233 | Test.isTrue(dic.containsKey(5)); 234 | }); 235 | } 236 | 237 | function containsValue(): void 238 | { 239 | it("Returns correct value", () => 240 | { 241 | const dic = new Dictionary([ 242 | { key: 1, value: 101 }, 243 | { key: 22, value: 122 }, 244 | ]); 245 | 246 | Test.isTrue(dic.containsValue(101)); 247 | Test.isTrue(dic.containsValue(122)); 248 | Test.isFalse(dic.containsValue(-1)); 249 | Test.isFalse(dic.containsValue(0)); 250 | Test.isFalse(dic.containsValue(2)); 251 | Test.isFalse(dic.containsValue(1)); 252 | Test.isFalse(dic.containsValue(22)); 253 | 254 | Test.isFalse(dic.containsValue(105)); 255 | dic.set(5, 105); 256 | Test.isTrue(dic.containsValue(105)); 257 | }); 258 | } 259 | 260 | function getKeys(): void 261 | { 262 | it("Returns correct value (number)", () => 263 | { 264 | const dic = new Dictionary([ 265 | { key: 1, value: 101 }, 266 | { key: 22, value: 122 }, 267 | ]); 268 | 269 | Test.isArrayEqual(dic.getKeys().asArray(), [1, 22]); 270 | }); 271 | 272 | it("Returns correct value (string)", () => 273 | { 274 | const dic = new Dictionary([ 275 | { key: "hola", value: 101 }, 276 | { key: "adios", value: 122 }, 277 | ]); 278 | 279 | Test.isArrayEqual(dic.getKeys().asArray(), ["hola", "adios"]); 280 | }); 281 | } 282 | 283 | function getValues(): void 284 | { 285 | it("Returns correct value (number)", () => 286 | { 287 | const dic = new Dictionary([ 288 | { key: 1, value: 101 }, 289 | { key: 22, value: 122 }, 290 | ]); 291 | 292 | Test.isArrayEqual(dic.getValues().asArray(), [101, 122]); 293 | }); 294 | 295 | it("Returns correct value (string)", () => 296 | { 297 | const dic = new Dictionary([ 298 | { key: "hola", value: "iepe" }, 299 | { key: "adios", value: "talue" }, 300 | ]); 301 | 302 | Test.isArrayEqual(dic.getValues().asArray(), ["iepe", "talue"]); 303 | }); 304 | } 305 | 306 | function remove(): void 307 | { 308 | it("Does nothing if key doesn't exist", () => 309 | { 310 | const dic = new Dictionary([ 311 | { key: 1, value: 101 }, 312 | { key: 22, value: 122 }, 313 | ]); 314 | 315 | Test.isArrayEqual(dic.getKeys().asArray(), [1, 22]); 316 | dic.remove(66); 317 | Test.isArrayEqual(dic.getKeys().asArray(), [1, 22]); 318 | }); 319 | 320 | it("Removes key", () => 321 | { 322 | const dic = new Dictionary([ 323 | { key: 1, value: 101 }, 324 | { key: 22, value: 122 }, 325 | { key: 33, value: 1322 }, 326 | ]); 327 | 328 | Test.isArrayEqual(dic.getKeys().asArray(), [1, 22, 33]); 329 | dic.remove(22); 330 | Test.isArrayEqual(dic.getKeys().asArray(), [1, 33]); 331 | dic.remove(1); 332 | Test.isArrayEqual(dic.getKeys().asArray(), [33]); 333 | dic.remove(33); 334 | Test.isArrayEqual(dic.getKeys().asArray(), []); 335 | }); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /test/unitary/Enumerable.test.ts: -------------------------------------------------------------------------------- 1 | import { Enumerable } from "../../src/Enumerables"; 2 | import { ArrayIterator } from "../../src/Iterators"; 3 | import { Test } from "../Test"; 4 | 5 | export namespace EnumerableUnitTest 6 | { 7 | export function run(): void 8 | { 9 | it("FromSource", fromSource); 10 | it("Empty", empty); 11 | describe("Range", range); 12 | it("Repeat", repeat); 13 | } 14 | 15 | function fromSource(): void 16 | { 17 | let e = Enumerable.fromSource(new ArrayIterator([])); 18 | Test.isFalse(e.next()); 19 | Test.throwsException(() => e.value()); 20 | 21 | e = Enumerable.fromSource(new ArrayIterator([2, 4, 6])); 22 | Test.isTrue(e.next()); 23 | Test.isEqual(e.value(), 2); 24 | Test.isTrue(e.next()); 25 | Test.isEqual(e.value(), 4); 26 | Test.isTrue(e.next()); 27 | Test.isEqual(e.value(), 6); 28 | Test.isFalse(e.next()); 29 | Test.throwsException(() => e.value()); 30 | 31 | e = Enumerable.fromSource(Enumerable.fromSource(new ArrayIterator([]))); 32 | Test.isFalse(e.next()); 33 | Test.throwsException(() => e.value()); 34 | 35 | e = Enumerable.fromSource(Enumerable.fromSource([2, 4, 6])); 36 | Test.isTrue(e.next()); 37 | Test.isEqual(e.value(), 2); 38 | Test.isTrue(e.next()); 39 | Test.isEqual(e.value(), 4); 40 | Test.isTrue(e.next()); 41 | Test.isEqual(e.value(), 6); 42 | Test.isFalse(e.next()); 43 | Test.throwsException(() => e.value()); 44 | } 45 | 46 | function empty(): void 47 | { 48 | const base = Enumerable.empty(); 49 | Test.isArrayEqual(base.toArray(), [] as number[]); 50 | } 51 | 52 | function range(): void 53 | { 54 | it("Negative count throws exception", () => 55 | { 56 | Test.throwsException(() => Enumerable.range(0, -1)); 57 | Test.throwsException(() => Enumerable.range(5, -666)); 58 | }); 59 | 60 | it("Zero count returns empty", () => 61 | { 62 | let base = Enumerable.range(0, 0); 63 | Test.isArrayEqual(base.toArray(), [] as number[]); 64 | 65 | base = Enumerable.range(4, 0); 66 | Test.isArrayEqual(base.toArray(), [] as number[]); 67 | }); 68 | 69 | it("Value is correct", () => 70 | { 71 | let base = Enumerable.range(2, 3); 72 | Test.isArrayEqual(base.toArray(), [2, 3, 4]); 73 | 74 | base = Enumerable.range(-2, 4); 75 | Test.isArrayEqual(base.toArray(), [-2, -1, 0, 1]); 76 | 77 | base = Enumerable.range(0, 6); 78 | Test.isArrayEqual(base.toArray(), [0, 1, 2, 3, 4, 5]); 79 | 80 | base = Enumerable.range(0, 1000000); 81 | Test.isArrayEqual(base.toArray(), base.toArray()); 82 | }); 83 | 84 | it("Negative count throws exception (descending)", () => 85 | { 86 | Test.throwsException(() => Enumerable.range(0, -1, false)); 87 | Test.throwsException(() => Enumerable.range(5, -666, false)); 88 | }); 89 | 90 | it("Zero count returns empty (descending)", () => 91 | { 92 | let base = Enumerable.range(0, 0, false); 93 | Test.isArrayEqual(base.toArray(), [] as number[]); 94 | 95 | base = Enumerable.range(4, 0, false); 96 | Test.isArrayEqual(base.toArray(), [] as number[]); 97 | }); 98 | 99 | it("Value is correct (descending)", () => 100 | { 101 | let base = Enumerable.range(2, 3, false); 102 | Test.isArrayEqual(base.toArray(), [2, 1, 0]); 103 | 104 | base = Enumerable.range(-2, 4, false); 105 | Test.isArrayEqual(base.toArray(), [-2, -3, -4, -5]); 106 | 107 | base = Enumerable.range(0, 6, false); 108 | Test.isArrayEqual(base.toArray(), [0, -1, -2, -3, -4, -5]); 109 | 110 | base = Enumerable.range(0, 1000000, false); 111 | Test.isArrayEqual(base.toArray(), base.toArray()); 112 | }); 113 | } 114 | 115 | function repeat(): void 116 | { 117 | Test.throwsException(() => Enumerable.repeat(0, -1)); 118 | Test.throwsException(() => Enumerable.repeat(5, -60)); 119 | Test.throwsException(() => Enumerable.repeat(-5, -1)); 120 | 121 | let base = Enumerable.repeat(3, 0); 122 | Test.isArrayEqual(base.toArray(), [] as number[]); 123 | 124 | base = Enumerable.repeat(3, 4); 125 | Test.isArrayEqual(base.toArray(), [3, 3, 3, 3]); 126 | 127 | let baseString = Enumerable.repeat("a", 0); 128 | Test.isArrayEqual(baseString.toArray(), [] as string[]); 129 | 130 | baseString = Enumerable.repeat("a", 2); 131 | Test.isArrayEqual(baseString.toArray(), ["a", "a"]); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/unitary/Iterator.test.ts: -------------------------------------------------------------------------------- 1 | import { Test } from "../Test"; 2 | import { ArrayIterator, IIterable } from "../../src/Iterators"; 3 | import { Enumerable, ArrayEnumerable, ConditionalEnumerable, 4 | ConcatEnumerable, UniqueEnumerable, RangeEnumerable, 5 | TransformEnumerable, ReverseEnumerable, OrderedEnumerable, 6 | DefaultIfEmptyEnumerable, TakeWhileEnumerable, SkipWhileEnumerable, 7 | ZippedEnumerable } from "../../src/Enumerables"; 8 | 9 | export namespace IteratorUnitTest 10 | { 11 | type Instancer = (elements: T[]) => IIterable; 12 | 13 | function runTest(name: string, test: (instancer: Instancer) => void) 14 | { 15 | describe(`${name} (ArrayIterator)`, () => test( 16 | (e: T[]) => new ArrayIterator(e))); 17 | 18 | describe(`${name} (Enumerable)`, () => test( 19 | (e: T[]) => new Enumerable(new ArrayIterator(e)))); 20 | 21 | describe(`${name} (ConditionalEnumerable)`, () => test( 22 | (e: T[]) => new ConditionalEnumerable(Enumerable.fromSource(e), x => true))); 23 | 24 | describe(`${name} (ConcatEnumerable)`, () => test( 25 | (e: T[]) => e.length > 1 26 | ? new ConcatEnumerable( 27 | Enumerable.fromSource([e[0]]), 28 | Enumerable.fromSource(e.slice(1))) 29 | : new ConcatEnumerable( 30 | Enumerable.fromSource(e), 31 | Enumerable.fromSource([])))); 32 | 33 | describe(`${name} (OrderedEnumerable)`, () => test( 34 | (e: T[]) => new OrderedEnumerable(Enumerable.fromSource(e), (l, r) => 0))); 35 | 36 | describe(`${name} (RangeEnumerable)`, () => test( 37 | (e: T[]) => new RangeEnumerable(Enumerable.fromSource(e), undefined, undefined))); 38 | 39 | describe(`${name} (TransformEnumerable)`, () => test( 40 | (e: T[]) => new TransformEnumerable(Enumerable.fromSource(e), x => x))); 41 | 42 | describe(`${name} (ReverseEnumerable)`, () => test( 43 | (e: T[]) => new ReverseEnumerable(new ReverseEnumerable(Enumerable.fromSource(e))))); 44 | 45 | describe(`${name} (OrderedEnumerable)`, () => test( 46 | (e: T[]) => new OrderedEnumerable(Enumerable.fromSource(e), (x, y) => 0))); 47 | 48 | describe(`${name} (ArrayEnumerable)`, () => test( 49 | (e: T[]) => new ArrayEnumerable(e))); 50 | 51 | describe(`${name} (DefaultIfEmptyEnumerable)`, () => test( 52 | (e: T[]) => new DefaultIfEmptyEnumerable(Enumerable.fromSource(e)) 53 | .where(p => p !== undefined) as IIterable)); 54 | 55 | describe(`${name} (TakeWhileEnumerable)`, () => test( 56 | (e: T[]) => new TakeWhileEnumerable(Enumerable.fromSource(e), e => true))); 57 | 58 | describe(`${name} (SkipWhileEnumerable)`, () => test( 59 | (e: T[]) => new SkipWhileEnumerable(Enumerable.fromSource(e), e => false))); 60 | 61 | describe(`${name} (ZippedEnumerable)`, () => test( 62 | (e: T[]) => new ZippedEnumerable(Enumerable.fromSource(e), Enumerable.fromSource(e), (x, y) => x))); 63 | } 64 | 65 | export function run(): void 66 | { 67 | runTest("Next", next); 68 | runTest("Reset", reset); 69 | runTest("Value", value); 70 | runTest("Clone", clone); 71 | } 72 | 73 | function next(instancer: Instancer): void 74 | { 75 | it("Return false for empty collection", () => 76 | { 77 | const it = instancer([]); 78 | Test.isFalse(it.next()); 79 | }); 80 | 81 | it("Iterate over elements + return false in the end", () => 82 | { 83 | const it = instancer([1, 2, 3]); 84 | Test.isTrue(it.next()); // 1 85 | Test.isTrue(it.next()); // 2 86 | Test.isTrue(it.next()); // 3 87 | Test.isFalse(it.next()); 88 | }); 89 | } 90 | 91 | function reset(instancer: Instancer): void 92 | { 93 | it("Iterator is resetted correctly", () => 94 | { 95 | const it = instancer([1, 2]); 96 | 97 | Test.isTrue(it.next()); // 1 98 | Test.isTrue(it.next()); // 2 99 | Test.isFalse(it.next()); 100 | it.reset(); 101 | 102 | Test.isTrue(it.next()); // 1 103 | Test.isTrue(it.next()); // 2 104 | Test.isFalse(it.next()); 105 | it.reset(); 106 | 107 | Test.isTrue(it.next()); // 1 108 | it.reset(); 109 | 110 | Test.isTrue(it.next()); // 1 111 | Test.isTrue(it.next()); // 2 112 | Test.isFalse(it.next()); 113 | }); 114 | 115 | it("Multiple reset in a row act like single one", () => 116 | { 117 | const it = instancer([1, 2]); 118 | 119 | it.reset(); 120 | it.reset(); 121 | it.reset(); 122 | 123 | Test.isTrue(it.next()); // 1 124 | Test.isTrue(it.next()); // 2 125 | Test.isFalse(it.next()); 126 | 127 | it.reset(); 128 | it.reset(); 129 | it.reset(); 130 | 131 | Test.isTrue(it.next()); // 1 132 | Test.isTrue(it.next()); // 2 133 | Test.isFalse(it.next()); 134 | 135 | it.reset(); 136 | it.reset(); 137 | it.reset(); 138 | }); 139 | } 140 | 141 | function value(instancer: Instancer): void 142 | { 143 | it("Exception if getting an out of bounds value", () => 144 | { 145 | const it = instancer([]); 146 | Test.isFalse(it.next()); 147 | Test.throwsException(() => it.value()); 148 | }); 149 | 150 | it("Get values", () => 151 | { 152 | const it = instancer([2, 4, 6]); 153 | 154 | Test.isTrue(it.next()); Test.isEqual(it.value(), 2); 155 | Test.isTrue(it.next()); Test.isEqual(it.value(), 4); 156 | Test.isTrue(it.next()); Test.isEqual(it.value(), 6); 157 | Test.isFalse(it.next()); 158 | }); 159 | 160 | it("Get values + exception in the end (out of bounds)", () => 161 | { 162 | const it = instancer([2, 4]); 163 | 164 | Test.isTrue(it.next()); Test.isEqual(it.value(), 2); 165 | Test.isTrue(it.next()); Test.isEqual(it.value(), 4); 166 | Test.isFalse(it.next()); Test.throwsException(() => it.value()); 167 | }); 168 | } 169 | 170 | function clone(instancer: Instancer): void 171 | { 172 | it("Cloned iterator is resetted by default", () => 173 | { 174 | const original = instancer([2, 4, 6]); 175 | Test.isTrue(original.next()); Test.isEqual(original.value(), 2); 176 | 177 | const clone = original.copy(); 178 | Test.isTrue(clone.next()); Test.isEqual(clone.value(), 2); 179 | }); 180 | 181 | it("Cloned iterator doesn't affect original one", () => 182 | { 183 | const original = instancer([2, 4, 6]); 184 | Test.isTrue(original.next()); Test.isEqual(original.value(), 2); 185 | 186 | const cloned = original.copy(); 187 | cloned.next(); // 2 188 | cloned.next(); // 4 189 | 190 | Test.isTrue(original.next()); Test.isEqual(original.value(), 4); 191 | 192 | cloned.reset(); 193 | 194 | Test.isTrue(original.next()); Test.isEqual(original.value(), 6); 195 | Test.isFalse(original.next()); 196 | }); 197 | 198 | it("Cloned iterator is identical to original", () => 199 | { 200 | const original = instancer([2, 4, 6]); 201 | const cloned = original.copy(); 202 | 203 | Test.isTrue(original.next()); Test.isEqual(original.value(), 2); 204 | Test.isTrue(cloned.next()); Test.isEqual(cloned.value(), 2); 205 | 206 | Test.isTrue(original.next()); Test.isEqual(original.value(), 4); 207 | Test.isTrue(cloned.next()); Test.isEqual(cloned.value(), 4); 208 | 209 | Test.isTrue(original.next()); Test.isEqual(original.value(), 6); 210 | Test.isTrue(cloned.next()); Test.isEqual(cloned.value(), 6); 211 | 212 | Test.isFalse(original.next()); 213 | Test.isFalse(cloned.next()); 214 | }); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /test/unitary/List.test.ts: -------------------------------------------------------------------------------- 1 | import { Test } from "./../Test"; 2 | import { List } from "./../../src/Collections"; 3 | 4 | export namespace ListUnitTest 5 | { 6 | export function run(): void 7 | { 8 | describe("AsArray", asArray); 9 | describe("AsReadOnly", asReadOnly); 10 | describe("Copy", copy); 11 | describe("Clear", clear); 12 | describe("Get", get); 13 | describe("Push", push); 14 | describe("PushFront", pushFront); 15 | describe("PushRange", pushRange); 16 | describe("Pop", pop); 17 | describe("PopFront", popFront); 18 | describe("Remove", remove); 19 | describe("RemoveAt", removeAt); 20 | describe("Set", set); 21 | describe("IndexOf", indexOf); 22 | describe("Insert", insert); 23 | } 24 | 25 | function asArray(): void 26 | { 27 | it("Returns a reference, not a copy", () => 28 | { 29 | const array = [1, 2, 3]; 30 | const list = new List(array); 31 | 32 | Test.isArrayEqual(list.asArray(), array); 33 | array.push(245); 34 | Test.isArrayEqual(list.asArray(), array); 35 | }); 36 | } 37 | 38 | function asReadOnly(): void 39 | { 40 | it("Same object but different interface", () => 41 | { 42 | const list = new List(["hi", "yess"]); 43 | 44 | Test.isEqual(list, list.asReadOnly()); 45 | }); 46 | } 47 | 48 | function copy(): void 49 | { 50 | it("Type is a List", () => 51 | { 52 | const list = new List([1, 2, 3]); 53 | Test.isTrue(list instanceof List); 54 | }); 55 | 56 | it("Returns a copy, not a reference", () => 57 | { 58 | const array = [1, 2, 3]; 59 | const list = new List(array); 60 | const copy = list.copy(); 61 | 62 | Test.isArrayEqual(list.asArray(), copy.asArray()); 63 | list.asArray().push(245); 64 | Test.isArrayNotEqual(list.asArray(), copy.asArray()); 65 | Test.isArrayEqual(list.asArray(), [1, 2, 3, 245]); 66 | Test.isArrayEqual(copy.asArray(), [1, 2, 3]); 67 | }); 68 | } 69 | 70 | function clear(): void 71 | { 72 | it("Does nothing on empty list", () => 73 | { 74 | const list = new List(); 75 | Test.isArrayEqual(list.asArray(), []); 76 | 77 | list.clear(); 78 | Test.isArrayEqual(list.asArray(), []); 79 | 80 | list.clear(); 81 | Test.isArrayEqual(list.asArray(), []); 82 | }); 83 | 84 | it("List is cleared", () => 85 | { 86 | const list = new List([1, 2, 3]); 87 | Test.isArrayEqual(list.asArray(), [1, 2, 3]); 88 | 89 | list.clear(); 90 | Test.isArrayEqual(list.asArray(), []); 91 | }); 92 | } 93 | 94 | function get(): void 95 | { 96 | it("Value is correct", () => 97 | { 98 | const list = new List([1, 2, 3]); 99 | Test.isEqual(list.get(0), 1); 100 | Test.isEqual(list.get(1), 2); 101 | Test.isEqual(list.get(2), 3); 102 | }); 103 | 104 | it("Undefined if invalid index", () => 105 | { 106 | const list = new List([1, 2, 3]); 107 | Test.isEqual(list.get(-999), undefined); 108 | Test.isEqual(list.get(-1), undefined); 109 | Test.isEqual(list.get(3), undefined); 110 | Test.isEqual(list.get(999), undefined); 111 | }); 112 | } 113 | 114 | function push(): void 115 | { 116 | it("Adds element in back of the list", () => 117 | { 118 | const list = new List(); 119 | list.push(2); Test.isArrayEqual(list.asArray(), [2]); 120 | list.push(22); Test.isArrayEqual(list.asArray(), [2, 22]); 121 | list.push(1); Test.isArrayEqual(list.asArray(), [2, 22, 1]); 122 | list.push(4); Test.isArrayEqual(list.asArray(), [2, 22, 1, 4]); 123 | }); 124 | } 125 | 126 | function pushFront(): void 127 | { 128 | it("Adds element in front of the list", () => 129 | { 130 | const list = new List(); 131 | list.pushFront(2); Test.isArrayEqual(list.asArray(), [2]); 132 | list.pushFront(22); Test.isArrayEqual(list.asArray(), [22, 2]); 133 | list.pushFront(1); Test.isArrayEqual(list.asArray(), [1, 22, 2]); 134 | list.pushFront(4); Test.isArrayEqual(list.asArray(), [4, 1, 22, 2]); 135 | }); 136 | } 137 | 138 | function pushRange(): void 139 | { 140 | it("Empty range doesn't modify the original (array)", () => 141 | { 142 | const list = new List([1, 2, 3]); 143 | 144 | Test.isEqual(list.pushRange([]), 3); 145 | Test.isArrayEqual(list.asArray(), [1, 2, 3]); 146 | }); 147 | 148 | it("Empty range doesn't modify the original (IQueryable)", () => 149 | { 150 | const list = new List([1, 2, 3]); 151 | 152 | Test.isEqual(list.pushRange(new List()), 3); 153 | Test.isArrayEqual(list.asArray(), [1, 2, 3]); 154 | }); 155 | 156 | it("Value is correct (array)", () => 157 | { 158 | const list = new List([1, 2, 3]); 159 | 160 | Test.isEqual(list.pushRange([22]), 4); 161 | Test.isArrayEqual(list.asArray(), [1, 2, 3, 22]); 162 | 163 | Test.isEqual(list.pushRange([24, 67]), 6); 164 | Test.isArrayEqual(list.asArray(), [1, 2, 3, 22, 24, 67]); 165 | }); 166 | 167 | it("Value is correct (IQueryable)", () => 168 | { 169 | const list = new List([1, 2, 3]); 170 | 171 | Test.isEqual(list.pushRange(new List([22])), 4); 172 | Test.isArrayEqual(list.asArray(), [1, 2, 3, 22]); 173 | 174 | Test.isEqual(list.pushRange(new List([24, 67])), 6); 175 | Test.isArrayEqual(list.asArray(), [1, 2, 3, 22, 24, 67]); 176 | }); 177 | } 178 | 179 | function pop(): void 180 | { 181 | it("Removes and returns the element in back of the list", () => 182 | { 183 | const list = new List([1, 2, 3]); 184 | 185 | let element = list.pop(); 186 | Test.isEqual(element, 3); 187 | Test.isArrayEqual(list.asArray(), [1, 2]); 188 | 189 | element = list.pop(); 190 | Test.isEqual(element, 2); 191 | Test.isArrayEqual(list.asArray(), [1]); 192 | 193 | element = list.pop(); 194 | Test.isEqual(element, 1); 195 | Test.isArrayEqual(list.asArray(), []); 196 | }); 197 | 198 | it("Returns undefined in empty list", () => 199 | { 200 | const list = new List(); 201 | 202 | const element = list.pop(); 203 | Test.isEqual(element, undefined); 204 | Test.isArrayEqual(list.asArray(), []); 205 | }); 206 | } 207 | 208 | function popFront(): void 209 | { 210 | it("Removes and returns the element in back of the list", () => 211 | { 212 | const list = new List([1, 2, 3]); 213 | 214 | let element = list.popFront(); 215 | Test.isEqual(element, 1); 216 | Test.isArrayEqual(list.asArray(), [2, 3]); 217 | 218 | element = list.popFront(); 219 | Test.isEqual(element, 2); 220 | Test.isArrayEqual(list.asArray(), [3]); 221 | 222 | element = list.popFront(); 223 | Test.isEqual(element, 3); 224 | Test.isArrayEqual(list.asArray(), []); 225 | }); 226 | 227 | it("Returns undefined in empty list", () => 228 | { 229 | const list = new List(); 230 | 231 | const element = list.popFront(); 232 | Test.isEqual(element, undefined); 233 | Test.isArrayEqual(list.asArray(), []); 234 | }); 235 | } 236 | 237 | function remove(): void 238 | { 239 | it("Does nothing on empty list", () => 240 | { 241 | const list = new List(); 242 | list.remove(6); 243 | list.remove(-6); 244 | Test.isArrayEqual(list.asArray(), []); 245 | }); 246 | 247 | it("Remove single element", () => 248 | { 249 | const list = new List([2, 3, 4, 5]); 250 | list.remove(3); 251 | Test.isArrayEqual(list.asArray(), [2, 4, 5]); 252 | 253 | list.remove(6); 254 | Test.isArrayEqual(list.asArray(), [2, 4, 5]); 255 | 256 | list.remove(4); 257 | Test.isArrayEqual(list.asArray(), [2, 5]); 258 | 259 | list.remove(2); 260 | Test.isArrayEqual(list.asArray(), [5]); 261 | 262 | list.remove(5); 263 | Test.isArrayEqual(list.asArray(), []); 264 | }); 265 | 266 | it("Remove element multiple times", () => 267 | { 268 | const list = new List([1, 1, 2, 3, 2, 5, 6, 6]); 269 | list.remove(1); 270 | Test.isArrayEqual(list.asArray(), [2, 3, 2, 5, 6, 6]); 271 | 272 | list.remove(2); 273 | Test.isArrayEqual(list.asArray(), [3, 5, 6, 6]); 274 | 275 | list.remove(6); 276 | Test.isArrayEqual(list.asArray(), [3, 5]); 277 | }); 278 | } 279 | 280 | function removeAt(): void 281 | { 282 | it("Exception if negative index", () => 283 | { 284 | const list = new List([6, 6, 6]); 285 | Test.throwsException(() => list.removeAt(-1)); 286 | Test.throwsException(() => list.removeAt(-50)); 287 | Test.throwsException(() => list.removeAt(-9999)); 288 | }); 289 | 290 | it("Exception if invalid index", () => 291 | { 292 | const list = new List([6, 6, 6]); 293 | Test.throwsException(() => list.removeAt(3)); 294 | Test.throwsException(() => list.removeAt(50)); 295 | Test.throwsException(() => list.removeAt(9999)); 296 | }); 297 | 298 | it("Deletes element by index", () => 299 | { 300 | const list = new List([2, 3, 4, 5]); 301 | list.removeAt(3); 302 | Test.isArrayEqual(list.asArray(), [2, 3, 4]); 303 | 304 | list.removeAt(0); 305 | Test.isArrayEqual(list.asArray(), [3, 4]); 306 | 307 | list.removeAt(1); 308 | Test.isArrayEqual(list.asArray(), [3]); 309 | 310 | list.removeAt(0); 311 | Test.isArrayEqual(list.asArray(), []); 312 | }); 313 | } 314 | 315 | function set(): void 316 | { 317 | it("Exception if negative index", () => 318 | { 319 | const list = new List([6, 6, 6]); 320 | Test.throwsException(() => list.set(-1, 666)); 321 | Test.throwsException(() => list.set(-50, 666)); 322 | Test.throwsException(() => list.set(-9999, 666)); 323 | }); 324 | 325 | it("Initializes list if empty", () => 326 | { 327 | const list = new List(); 328 | 329 | list.set(0, 33); 330 | Test.isArrayEqual(list.asArray(), [33]); 331 | }); 332 | 333 | it("Sets the correct index", () => 334 | { 335 | const list = new List([2, 4, 6, 8, 7]); 336 | 337 | list.set(0, 33); 338 | Test.isArrayEqual(list.asArray(), [33, 4, 6, 8, 7]); 339 | 340 | list.set(3, 22); 341 | Test.isArrayEqual(list.asArray(), [33, 4, 6, 22, 7]); 342 | }); 343 | 344 | it("Resizes list if necessary", () => 345 | { 346 | const list = new List([2, 4, 6, 8, 7]); 347 | 348 | list.set(7, 33); 349 | Test.isArrayEqual(list.asArray(), [2, 4, 6, 8, 7, undefined, undefined, 33]); 350 | }); 351 | } 352 | 353 | function indexOf(): void 354 | { 355 | it("-1 in empty list", () => 356 | { 357 | const list = new List(); 358 | Test.isEqual(list.indexOf(666), -1); 359 | Test.isEqual(list.indexOf(0), -1); 360 | Test.isEqual(list.indexOf(-666), -1); 361 | }); 362 | 363 | it("-1 if element not found", () => 364 | { 365 | const list = new List([1, 2, 3]); 366 | Test.isEqual(list.indexOf(-999), -1); 367 | Test.isEqual(list.indexOf(-1), -1); 368 | Test.isEqual(list.indexOf(4), -1); 369 | Test.isEqual(list.indexOf(999), -1); 370 | }); 371 | 372 | it("Value is correct", () => 373 | { 374 | const list = new List([1, 2, 3]); 375 | Test.isEqual(list.indexOf(2), 1); 376 | Test.isEqual(list.indexOf(1), 0); 377 | Test.isEqual(list.indexOf(3), 2); 378 | }); 379 | } 380 | 381 | function insert(): void 382 | { 383 | it("Throws exception if index < 0", () => 384 | { 385 | const list = new List([1, 2, 3]); 386 | Test.throwsException(() => list.insert(-3, 55)); 387 | }); 388 | 389 | it("Throws exception if index > length", () => 390 | { 391 | const list = new List([1, 2, 3]); 392 | Test.throwsException(() => list.insert(4, 55)); 393 | }); 394 | 395 | it("Initializes empty list", () => 396 | { 397 | const list = new List(); 398 | list.insert(0, 4); 399 | Test.isArrayEqual(list.asArray(), [4]); 400 | }); 401 | 402 | it("Moves other elements to fit the new one", () => 403 | { 404 | const list = new List([1, 2, 3]); 405 | list.insert(1, 55); 406 | Test.isArrayEqual(list.asArray(), [1, 55, 2, 3]); 407 | }); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /test/unitary/Stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Test } from "./../Test"; 2 | import { Stack } from "./../../src/Collections"; 3 | 4 | export namespace StackUnitTest 5 | { 6 | export function run(): void 7 | { 8 | describe("Copy", copy); 9 | describe("Clear", clear); 10 | describe("Peek", peek); 11 | describe("Push", push); 12 | describe("Pop", pop); 13 | } 14 | 15 | function copy(): void 16 | { 17 | it("Type is a Stack", () => 18 | { 19 | const list = new Stack([1, 2, 3]); 20 | Test.isTrue(list instanceof Stack); 21 | }); 22 | 23 | it("Returns a copy, not a reference", () => 24 | { 25 | const array = [1, 2, 3]; 26 | const stack = new Stack(array); 27 | const copy = stack.copy(); 28 | 29 | Test.isArrayEqual(stack.asArray(), copy.asArray()); 30 | stack.asArray().push(245); 31 | Test.isArrayNotEqual(stack.asArray(), copy.asArray()); 32 | Test.isArrayEqual(stack.asArray(), [1, 2, 3, 245]); 33 | Test.isArrayEqual(copy.asArray(), [1, 2, 3]); 34 | }); 35 | } 36 | 37 | function clear(): void 38 | { 39 | it("Does nothing on empty stack", () => 40 | { 41 | const stack = new Stack(); 42 | Test.isArrayEqual(stack.asArray(), []); 43 | 44 | stack.clear(); 45 | Test.isArrayEqual(stack.asArray(), []); 46 | 47 | stack.clear(); 48 | Test.isArrayEqual(stack.asArray(), []); 49 | }); 50 | 51 | it("Stack is cleared", () => 52 | { 53 | const stack = new Stack([1, 2, 3]); 54 | Test.isArrayEqual(stack.asArray(), [1, 2, 3]); 55 | 56 | stack.clear(); 57 | Test.isArrayEqual(stack.asArray(), []); 58 | }); 59 | } 60 | 61 | function peek(): void 62 | { 63 | it("Returns the element in back of the stack", () => 64 | { 65 | const stack = new Stack([1, 2, 3]); 66 | 67 | let element = stack.peek(); 68 | Test.isEqual(element, 3); 69 | Test.isArrayEqual(stack.asArray(), [1, 2, 3]); 70 | 71 | stack.pop(); 72 | 73 | element = stack.peek(); 74 | Test.isEqual(element, 2); 75 | Test.isArrayEqual(stack.asArray(), [1, 2]); 76 | 77 | element = stack.peek(); 78 | Test.isEqual(element, 2); 79 | Test.isArrayEqual(stack.asArray(), [1, 2]); 80 | 81 | stack.pop(); 82 | 83 | element = stack.peek(); 84 | Test.isEqual(element, 1); 85 | Test.isArrayEqual(stack.asArray(), [1]); 86 | }); 87 | 88 | it("Returns undefined in empty stack", () => 89 | { 90 | const stack = new Stack(); 91 | 92 | const element = stack.peek(); 93 | Test.isEqual(element, undefined); 94 | Test.isArrayEqual(stack.asArray(), []); 95 | }); 96 | } 97 | 98 | function push(): void 99 | { 100 | it("Adds element in back of the stack", () => 101 | { 102 | const stack = new Stack(); 103 | stack.push(2); Test.isArrayEqual(stack.asArray(), [2]); 104 | stack.push(22); Test.isArrayEqual(stack.asArray(), [2, 22]); 105 | stack.push(1); Test.isArrayEqual(stack.asArray(), [2, 22, 1]); 106 | stack.push(4); Test.isArrayEqual(stack.asArray(), [2, 22, 1, 4]); 107 | }); 108 | } 109 | 110 | function pop(): void 111 | { 112 | it("Removes and returns the element in back of the stack", () => 113 | { 114 | const stack = new Stack([1, 2, 3]); 115 | 116 | let element = stack.pop(); 117 | Test.isEqual(element, 3); 118 | Test.isArrayEqual(stack.asArray(), [1, 2]); 119 | 120 | element = stack.pop(); 121 | Test.isEqual(element, 2); 122 | Test.isArrayEqual(stack.asArray(), [1]); 123 | 124 | element = stack.pop(); 125 | Test.isEqual(element, 1); 126 | Test.isArrayEqual(stack.asArray(), []); 127 | }); 128 | 129 | it("Returns undefined in empty stack", () => 130 | { 131 | const stack = new Stack(); 132 | 133 | const element = stack.pop(); 134 | Test.isEqual(element, undefined); 135 | Test.isArrayEqual(stack.asArray(), []); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/unitary/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Cached } from "../../src/Utils"; 2 | import { Test } from "./../Test"; 3 | 4 | export namespace UtilsUnitTest 5 | { 6 | export function run(): void 7 | { 8 | describe("Constructor", constructor); 9 | describe("Value", value); 10 | describe("Invalidate", invalidate); 11 | } 12 | 13 | function constructor(): void 14 | { 15 | it("Starts invalidated", () => 16 | { 17 | const c = new Cached(); 18 | Test.isFalse(c.isValid()); 19 | }); 20 | } 21 | 22 | function value(): void 23 | { 24 | it("Throws exception if invalid", () => 25 | { 26 | const c = new Cached(); 27 | Test.isFalse(c.isValid()); 28 | Test.throwsException(() => { const x = c.value; }); 29 | }); 30 | 31 | it("Validate on value set", () => 32 | { 33 | const c = new Cached(); 34 | Test.isFalse(c.isValid()); 35 | c.value = 33; 36 | Test.isTrue(c.isValid()); 37 | }); 38 | 39 | it("Get value", () => 40 | { 41 | const c = new Cached(); 42 | c.value = 33; 43 | Test.isEqual(c.value, 33); 44 | }); 45 | } 46 | 47 | function invalidate(): void 48 | { 49 | it("Do nothing if already invalid", () => 50 | { 51 | const c = new Cached(); 52 | Test.isFalse(c.isValid()); 53 | 54 | c.invalidate(); 55 | c.invalidate(); 56 | c.invalidate(); 57 | 58 | Test.isFalse(c.isValid()); 59 | }); 60 | 61 | it("Make invalid if it was valid", () => 62 | { 63 | const c = new Cached(); 64 | Test.isFalse(c.isValid()); 65 | c.value = 33; 66 | Test.isTrue(c.isValid()); 67 | c.invalidate(); 68 | Test.isFalse(c.isValid()); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": 4 | { 5 | "target": "es5", 6 | "module": "commonjs", 7 | "lib": ["esnext"], 8 | "declaration": true, 9 | "alwaysStrict": true, 10 | "allowSyntheticDefaultImports": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "removeComments": false, 14 | "sourceMap": true, 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "strictNullChecks": true, 20 | "allowUnreachableCode": false, 21 | "strictFunctionTypes": true, 22 | "types": ["mocha"], 23 | "outDir": "build", 24 | "strictPropertyInitialization": false 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "!node_modules/@types", 29 | "build" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [true, 8 | "check-space" 9 | ], 10 | "indent": [true, 11 | "spaces" 12 | ], 13 | "linebreak-style": [true, "CRLF"], 14 | "one-line": [true, 15 | "check-whitespace" 16 | ], 17 | "no-var-keyword": true, 18 | "quotemark": [true, 19 | "double", 20 | "avoid-escape" 21 | ], 22 | "semicolon": [true, "always", "ignore-bound-class-methods"], 23 | "whitespace": [true, 24 | "check-branch", 25 | "check-decl", 26 | "check-operator", 27 | "check-module", 28 | "check-separator", 29 | "check-type" 30 | ], 31 | "typedef-whitespace": [ 32 | true, 33 | { 34 | //"call-signature": "nospace", 35 | "index-signature": "nospace", 36 | "parameter": "nospace", 37 | "property-declaration": "nospace", 38 | "variable-declaration": "nospace" 39 | }, 40 | { 41 | "call-signature": "onespace", 42 | "index-signature": "onespace", 43 | "parameter": "onespace", 44 | "property-declaration": "onespace", 45 | "variable-declaration": "onespace" 46 | } 47 | ], 48 | "next-line": [true, 49 | "check-catch", 50 | "check-else", 51 | "check-open-brace" 52 | ], 53 | "return-undefined": true, 54 | "prefer-for-of": false, // performance vs readability 55 | "no-empty-interface": false, 56 | "unified-signatures": false, 57 | "newline-before-return": true, 58 | "no-parameter-properties": true, 59 | "no-unused-variable": true, 60 | "no-internal-module": true, 61 | "no-trailing-whitespace": true, 62 | "object-literal-sort-keys": false, 63 | "no-null-keyword": true, 64 | "boolean-trivia": true, 65 | "type-operator-spacing": true, 66 | "prefer-const": true, 67 | "no-increment-decrement": true, 68 | "object-literal-surrounding-space": true, 69 | "no-type-assertion-whitespace": true, 70 | "no-in-operator": true, 71 | "no-switch-case-fall-through": true, 72 | "triple-equals": true, 73 | "jsdoc-format": true, 74 | "no-unused-expression": true, 75 | "no-unused-new": true, 76 | "no-unreachable": true, 77 | "arrow-parens": false, 78 | "max-classes-per-file": [ 79 | false 80 | ], 81 | "variable-name": [ true, "allow-leading-underscore" ], 82 | "member-ordering": [ 83 | false 84 | ], 85 | "no-namespace": [ 86 | false 87 | ], 88 | "no-any": false, 89 | "no-console": [ 90 | false 91 | ], 92 | "radix": false, 93 | "no-bitwise": false, 94 | "no-reference": false, 95 | "ordered-imports": false 96 | } 97 | } 98 | --------------------------------------------------------------------------------