├── .eslintrc.cjs ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── 01-intro ├── README.md ├── exercises │ ├── champions.js │ ├── champions.solution.js │ ├── champions.test.js │ ├── data │ │ └── athletes.js │ ├── fizzbuzz.js │ ├── fizzbuzz.solution.js │ └── fizzbuzz.test.js ├── for-in-debug.js ├── for-in-object.js ├── for-in.js ├── for-of-map.js ├── for-of-object-entries.js ├── for-of-object.js ├── for-of-set.js ├── for-of-string.js ├── for-of.js ├── for.js ├── spread-array.js ├── spread-function.js └── spread-object.js ├── 02-generators ├── README.md ├── cycle.js ├── exercises │ ├── take.js │ ├── take.solution.js │ ├── take.test.js │ ├── zip.js │ ├── zip.solution.js │ └── zip.test.js ├── fruit-generator-iterable.js ├── fruit-generator-return.js ├── fruit-generator.js └── range.js ├── 03-iterator-protocol ├── README.md ├── countdown-class.js ├── countdown-generator.js ├── countdown.js ├── exercises │ ├── emoji.js │ ├── emoji.solution.js │ └── emoji.test.js └── images │ ├── iterable.png │ └── iterator.png ├── 04-iterable-protocol ├── README.md ├── countdown-class.js ├── countdown-generator.js ├── countdown-iterator-iterable.js ├── countdown.js ├── exercises │ ├── binarytree.js │ ├── binarytree.solution.js │ ├── binarytree.test.js │ ├── emoji.js │ ├── emoji.solution.js │ ├── emoji.test.js │ ├── entries.js │ ├── entries.solution.js │ └── entries.test.js └── hello-iterator-iterable.js ├── 05-async-iterator-protocol ├── README.md ├── countdown-async-iterator-generator.js ├── countdown-async-iterator.js ├── exercises │ ├── __mocks__ │ │ └── rickmortydata.js │ ├── rickmorty.js │ ├── rickmorty.solution.js │ └── rickmorty.test.js └── images │ └── countdown-async-iterator.gif ├── 06-async-iterable-protocol ├── README.md ├── countdown-async-iterable-generator.js ├── countdown-async-iterable.js ├── exercises │ ├── __mocks__ │ │ └── rickmortydata.js │ ├── rickmorty.js │ ├── rickmorty.solution.js │ └── rickmorty.test.js └── images │ ├── countdown-async-iterable-generator.gif │ └── countdown-async-iterable.gif ├── 07-tips-and-pitfalls ├── README.md ├── assets │ └── bigdata.csv ├── copy-stdin-backpressure.js ├── copy-stdin-compress.js ├── copy-stdin.js ├── count-bytes-stdin-async-iterable.js ├── count-bytes-stdin.js ├── find-js-files-abort.js ├── find-js-files-listeners.js ├── find-js-files.js ├── http-delay-traditional.js ├── http-for-await-delay-fixed.js ├── http-for-await-delay.js ├── http-for-await.js └── iterable-check.js ├── 08-exercises └── README.md ├── LICENSE ├── README.md ├── package-lock.json └── package.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | es2022: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 13, 12 | sourceType: 'module' 13 | }, 14 | rules: { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run build --if-present 25 | - run: npm test 26 | env: 27 | TEST_SOLUTIONS: "true" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # OS files 133 | .DS_Store 134 | 135 | # Project files 136 | data.bin 137 | data.bin.br -------------------------------------------------------------------------------- /01-intro/README.md: -------------------------------------------------------------------------------- 1 | # 01 - Introduction 2 | 3 | There are many ways to do iteration in JavaScript, just to name a few: 4 | 5 | - `while` 6 | - `do ... while` 7 | - `for` 8 | - `for ... in` 9 | - `for ... of` 10 | - Array methods like `forEach`, `map`, `flatMap`, `reduce`, etc... 11 | - Generators 12 | - ... 13 | 14 | > JavaScript even supports loop labels which can allow you to do crazy things that remind me of the infamous `GOTO` in Basic... but we are not going near this stuff here 🙈 (ok, ok... if you are really curious, check out this [example](https://gist.github.com/lmammino/868076ff5d863f88298a86eb4792fdd5)). 15 | 16 | So, if we have all these different ways of doing iteration, why do we need **iteration protocols**? 🤔 17 | 18 | Iteration protocols define **Iterators**, **Iterables**, **Async iterators** & **Async iterables**. 19 | 20 | It's an attempt at standardising "iteration" behaviors by providing a consistent and interoperable API. 21 | 22 | This standardisation is what allows us to use constructs such as `for...of`, `for await...of` and the spread operator on a variety of objects. 23 | 24 | Knowing iteration protocols allows us: 25 | 26 | - Understand JavaScript better 27 | - Write more modern and idiomatic code 28 | - Be able to write our own custom iterators 29 | 30 | 31 | ## Syntax review 32 | 33 | But, before we get into the weeds of iteration protocols, let's recap a little bit of JavaScript syntax. 34 | 35 | Let's say that we have the following array: 36 | 37 | ```javascript 38 | const judokas = [ 39 | 'Driulis Gonzalez Morales', 40 | 'Ilias Iliadis', 41 | 'Tadahiro Nomura', 42 | 'Anton Geesink', 43 | 'Teddy Riner', 44 | 'Ryoko Tani' 45 | ] 46 | ``` 47 | 48 | and that we want to print all the items in the array. 49 | 50 | One classic way to do this is: 51 | 52 | ```javascript 53 | // for.js 54 | for (let i = 0; i < judokas.length; i++) { 55 | console.log(judokas[i]) 56 | } 57 | ``` 58 | 59 | As expected, this will print: 60 | 61 | ```plain 62 | Driulis Gonzalez Morales 63 | Ilias Iliadis 64 | Tadahiro Nomura 65 | Anton Geesink 66 | Teddy Riner 67 | Ryoko Tani 68 | ``` 69 | 70 | There is nothing wrong with this approach, but let's admit that it requires us to specify a bunch of unnecessary details for the particular use cases: 71 | 72 | - how many times do we have to loop: `i < judokas.length` 73 | - how to increment the counter after every iteration: `i++` 74 | - how to explicitely access the current item: `judokas[i]` 75 | 76 | We can do better! 77 | 78 | > **Note**: "regular" `for` loop is not bad per se, but it makes sense to use it only with more advanced use cases (e.g. you need to traverse multi-dimensional arrays, you want to go throgh an array in reverse order, or pick only odd elements, or simply when you want to repeat an operation for a certain amount of times and you are not even dealing with a collection). 79 | 80 | 81 | ### `for ... of` 82 | 83 | So, let's do better... with `for ... of`: 84 | 85 | ```javascript 86 | // for-of.js 87 | for (const judoka of judokas) { 88 | console.log(judoka) 89 | } 90 | ``` 91 | 92 | This will produce the same output as with the previous snippet. 93 | 94 | Isn't this much nicer to write (and read!)? 95 | 96 | The intent here is definitely more clear: do something for every judoka *in* the collection. Well, I know here I said **in** but our code says **of**... bear with me for a second, we'll get there! 97 | 98 | Before we get there, keep in mind that `for ... of` works with any **iterable** object. 99 | 100 | 101 | #### `for ... of` with `String` 102 | 103 | A `string` is an iterable object, so we can use `for ... of` with a string: 104 | 105 | ```javascript 106 | // for-of-string.js 107 | const judoka = 'Ryoko Tani' 108 | 109 | for (const char of judoka) { 110 | console.log(char) 111 | } 112 | ``` 113 | 114 | This prints: 115 | 116 | ```plain 117 | R 118 | y 119 | o 120 | k 121 | o 122 | 123 | T 124 | a 125 | n 126 | i 127 | ``` 128 | 129 | We are printing every single character. It makes sense, right? A string is just a collection of caracters so it implements the iterator protocol to give us a nice way to iterate over all the characters! 130 | 131 | Other iterable objects are `Set` and `Map`. 132 | 133 | 134 | #### `for ... of` with `Set` 135 | 136 | Let's see an example using a `Set`: 137 | 138 | ```javascript 139 | // for-of-set.js 140 | const medals = new Set([ 141 | 'gold', 142 | 'silver', 143 | 'bronze' 144 | ]) 145 | 146 | for (const medal of medals) { 147 | console.log(medal) 148 | } 149 | ``` 150 | 151 | As you might expect, this prints: 152 | 153 | ```plain 154 | gold 155 | silver 156 | bronze 157 | ``` 158 | 159 | #### `for ... of` with `Map` 160 | 161 | Now let's see an example with a `Map`: 162 | 163 | ```javascript 164 | // for-of-map.js 165 | const medallists = new Map([ 166 | ['Teddy Riner', 33], 167 | ['Driulis Gonzalez Morales', 16], 168 | ['Ryoko Tani', 16], 169 | ['Ilias Iliadis', 15] 170 | ]) 171 | 172 | for (const [judoka, medals] of medallists) { 173 | console.log(`${judoka} has won ${medals} medals`) 174 | } 175 | ``` 176 | 177 | This prints: 178 | 179 | ```plain 180 | Teddy Riner has won 33 medals 181 | Driulis Gonzalez Morales has won 16 medals 182 | Ryoko Tani has won 16 medals 183 | Ilias Iliadis has won 15 medals 184 | ``` 185 | 186 | In this particular case it's interesting to see that the *item* returned in every iteration is an array with 2 elements containing the **current key** and the **current value**. We used array destructuring here to make things more concise and readable! 187 | 188 | 189 | #### `for ... of` with `Object` 190 | 191 | What happens if we use `for ... of` on an object? Let's find out: 192 | 193 | ```js 194 | // for-of-object.js 195 | const medallists = { 196 | 'Teddy Riner': 33, 197 | 'Driulis Gonzalez Morales': 16, 198 | 'Ryoko Tani': 16, 199 | 'Ilias Iliadis': 15 200 | } 201 | 202 | for (const [judoka, medals] of medallists) { 203 | console.log(`${judoka} has won ${medals} medals`) 204 | } 205 | ``` 206 | 207 | Boom! 💣💥 208 | 209 | If we run this code we get the following error: 210 | 211 | ```plain 212 | for (const [judoka, medals] of medallists) { 213 | ^ 214 | 215 | TypeError: medallists is not iterable 216 | ``` 217 | 218 | And here's our first lesson learned! 219 | 220 | > **Note**: We can use `for ... of` only on iterable objects! 221 | 222 | And it turns out that plain objects are not iterable by default. 223 | 224 | So, what if we want to iterate over key/value pairs of a plain object? 225 | 226 | We can do that with `Object.entries()`: 227 | 228 | ```js 229 | // for-of-object-entries.js 230 | 231 | const medallists = { 232 | 'Teddy Riner': 33, 233 | 'Driulis Gonzalez Morales': 16, 234 | 'Ryoko Tani': 16, 235 | 'Ilias Iliadis': 15 236 | } 237 | 238 | for (const [judoka, medals] of Object.entries(medallists)) { 239 | console.log(`${judoka} has won ${medals} medals`) 240 | } 241 | ``` 242 | 243 | This will work and it will print: 244 | 245 | ```plain 246 | Teddy Riner has won 33 medals 247 | Driulis Gonzalez Morales has won 16 medals 248 | Ryoko Tani has won 16 medals 249 | Ilias Iliadis has won 15 medals 250 | ``` 251 | 252 | The reason why this works is because `Object.entries()` returns an **iterable** object which will produce items with the shape `[key, value]` for every iteration. Similar to what we saw for `Map`. 253 | 254 | 255 | ### `for ... in` 256 | 257 | You might have heard about `for ... in` as well. Let's see what that does with an array: 258 | 259 | ```js 260 | // for-in.js 261 | const judokas = [ 262 | 'Driulis Gonzalez Morales', 263 | 'Ilias Iliadis', 264 | 'Tadahiro Nomura', 265 | 'Anton Geesink', 266 | 'Teddy Riner', 267 | 'Ryoko Tani' 268 | ] 269 | 270 | for (const judoka in judokas) { 271 | console.log(judoka) 272 | } 273 | ``` 274 | 275 | Can you already guess what's going to be the output? 276 | 277 |
278 | 🔎 Let's find out! 279 | 280 | ```plain 281 | 0 282 | 1 283 | 2 284 | 3 285 | 4 286 | 5 287 | ``` 288 | 289 | Not what you might have expected, right?! 290 | 291 |
292 | 293 | The way `for ... in` works is that it **iterates over all the enumerable properties of an object**. 294 | 295 | In the case of an array the enumerable properties are the array indices. 296 | 297 | Let's see what happens with a plain object: 298 | 299 | ```js 300 | // for-in-object.js 301 | const medallists = { 302 | 'Teddy Riner': 33, 303 | 'Driulis Gonzalez Morales': 16, 304 | 'Ryoko Tani': 16, 305 | 'Ilias Iliadis': 15 306 | } 307 | 308 | for (const judoka in medallists) { 309 | console.log(`${judoka} has won ${medallists[judoka]} medals`) 310 | } 311 | ``` 312 | 313 | Note how `judoka` will contain the current key, so we need to use `medallist[judoka]` if we want to access the current value for that key. 314 | 315 | `for ... in` doesn't really have too many practical use cases. 316 | 317 | One case where it could be useful is when you want to debug the properties of an object: 318 | 319 | ```js 320 | // for-in-debug.js 321 | const medallists = { 322 | 'Teddy Riner': 33, 323 | 'Driulis Gonzalez Morales': 16, 324 | 'Ryoko Tani': 16, 325 | 'Ilias Iliadis': 15 326 | } 327 | 328 | for (const prop in medallists) { 329 | console.log(`medallists[${prop}] = ${medallists[prop]}`); 330 | } 331 | ``` 332 | 333 | This prints: 334 | 335 | ```plain 336 | medallists[Teddy Riner] = 33 337 | medallists[Driulis Gonzalez Morales] = 16 338 | medallists[Ryoko Tani] = 16 339 | medallists[Ilias Iliadis] = 15 340 | ``` 341 | 342 | Check out the [`for ... in` page on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) if you want to learn more about this statement. 343 | 344 | 345 | ### The spread syntax 346 | 347 | The spread syntax is another interesting concept that applies to iterable objects. 348 | 349 | This syntax is identified by `...iterableObj`: 3 dots and the name of the iterable object. 350 | 351 | It allows us to "spread" all the elements of an iterable into an array, or to pass them as individual arguments to a function. 352 | 353 | Let's see an example with an array: 354 | 355 | ```js 356 | // spread-array.js 357 | const countdown = [3, 2, 1, 0] 358 | const from5to0 = [5, 4, ...countdown] 359 | console.log(from5to0) 360 | ``` 361 | 362 | This prints: 363 | 364 | ```plain 365 | [ 5, 4, 3, 2, 1, 0 ] 366 | ``` 367 | 368 | As we said, we can also use this syntax to pass the elements of an iterable as distinct arguments to a function call: 369 | 370 | ```js 371 | // spread-function.js 372 | const countdown = [3, 2, 1, 0] 373 | console.log('countdown data:', ...countdown) 374 | ``` 375 | 376 | This is equivalent to calling: 377 | 378 | ```js 379 | console.log('countdown data:', 3, 2, 1, 0) 380 | ``` 381 | 382 | and it produces the following output: 383 | 384 | ```plain 385 | countdown data: 3 2 1 0 386 | ``` 387 | 388 | **Note:** `console.log()` accepts an arbitrary number of arguments and it will print all of them separated by a space. 389 | 390 | Let's now see how we can use the spread syntax with objects: 391 | 392 | ```js 393 | // spread-object.js 394 | const judoInfo = { 395 | creator: 'Jigoro Kano', 396 | creationYear: 1882 397 | } 398 | 399 | const ranks = { 400 | belts: ['white', 'yellow', 'orange', 'green', 'blue', 'brown', 'black', 'red-white', 'red'], 401 | dan: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 402 | } 403 | 404 | const judo = { ...judoInfo, ...ranks } 405 | 406 | console.log(judo) 407 | ``` 408 | 409 | This will output: 410 | 411 | ```plain 412 | { 413 | creator: 'Jigoro Kano', 414 | creationYear: 1882, 415 | belts: [ 416 | 'white', 'yellow', 417 | 'orange', 'green', 418 | 'blue', 'brown', 419 | 'black', 'red-white', 420 | 'red' 421 | ], 422 | dan: [ 423 | 1, 2, 3, 4, 5, 424 | 6, 7, 8, 9, 10, 425 | 11, 12 426 | ] 427 | } 428 | ``` 429 | 430 | It's a nice way to merge key-value pairs from different objects into one object. 431 | 432 | **Note:** The spread syntax with objects has been introduced in EcmaScript 2018, so it's still relatively new and it might not be available in older browsers or Node.js runtimes. If you want to write code that is more broadly compatible you can use `Object.assign()` as an alternative way to merge object key-value pairs. 433 | 434 | 435 | ## Warm up your keyboard 436 | 437 | Let's try a quick exercise to warm up a little: 438 | 439 | > **🏹 Exercise** ([fizzbuzz.js](/01-intro/exercises/fizzbuzz.js)) 440 | > 441 | > Write a function that implements the fizzBuzz game. 442 | > 443 | > A skeleton of the file is available at `01-intro/exercises/fizzbuzz.js`. 444 | > 445 | > You can edit the file and run an interactive test session to validate your implementation with (from the root folder of the project): 446 | > 447 | > ```bash 448 | > npm run ex -- 01-intro/exercises/fizzbuzz.test.js 449 | > ``` 450 | > 451 | > If you really struggle with this, you can have a look at [`fizzbuzz.solution.js`](/01-intro/exercises/fizzbuzz.solution.js) for a possible solution. 452 | 453 | Was that too easy? Let's try something else: 454 | 455 | > **🏹 Exercise** ([champions.js](/01-intro/exercises/champions.js)) 456 | > 457 | > Let's do some data analysis and let's find the best Judo Olympics champions: athletes who have won at least 3 olympic medals in their career. 458 | > 459 | > A skeleton of the file is available at `01-intro/exercises/champions.js`. 460 | > 461 | > You can edit the file and run an interactive test session to validate your implementation with: 462 | > 463 | > ```bash 464 | > npm run ex -- 01-intro/exercises/champions.test.js 465 | > ``` 466 | > 467 | > If you really struggle with this, you can have a look at [`champions.solution.js`](/01-intro/exercises/champions.solution.js) for a possible solution. 468 | 469 | 470 | ## Summary 471 | 472 | Let's summarise what we have learned so far: 473 | 474 | - There are many (many many...) ways to do iteration in JavaScript. 475 | - Iteration protocols try to standardise how to make different types of objects iterable. 476 | - Iterable objects can be iterated over with `for ... of`. 477 | - You can also use the **spread syntax** to "spread" all the elements of an iterable into an array, or to pass them as individual arguments to a function. 478 | - The spread syntax can also be used to merge key-value pairs from different objects. 479 | - `for ... in` also exists and it allows us to iterate over all the enumerable properties of an object. 480 | 481 | 482 | 483 | That's all for now, congratulations on finishing the first chapter! 🎉 484 | 485 | Take a little break and get ready to move to the [Next section](/02-generators/README.md). 486 | 487 | --- 488 | 489 | | [⬅️ 00 - README](/README.md) | [🏠](/README.md)| [02 - Generators ➡️](/02-generators/README.md)| 490 | |:--------------|:------:|------------------------------------------------:| 491 | -------------------------------------------------------------------------------- /01-intro/exercises/champions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's find the best Judo Olympics champions: athletes who have won at 4 | least 3 olympic medals in their career. 5 | 6 | Write a function that receives an object that contains all the Judo 7 | olympic athletes who have won at least a medal. 8 | The object has athlete names for keys and a collection of medals as 9 | values as in the following example: 10 | 11 | ``` 12 | { 13 | 'athlete1': {gold: 1, silver: 0, bronze: 1}, 14 | 'athlete2': {gold: 0, silver: 0, bronze: 2}, 15 | 'athlete3': {gold: 1, silver: 1, bronze: 1}, 16 | 'athlete4': {gold: 1, silver: 2, bronze: 1}, 17 | } 18 | ``` 19 | 20 | Return the names of the athletes who have won at least 3 medals. 21 | For the above example the result should be: 22 | 23 | ``` 24 | [ 25 | 'athlete3', 26 | 'athlete4' 27 | ] 28 | ``` 29 | 30 | */ 31 | 32 | /** 33 | * @param {Object.} athletes 34 | * @returns {[string]} 35 | */ 36 | export default function champions (athletes) { 37 | const wonAtLeast3Medals = [] 38 | // write your logic here 39 | return wonAtLeast3Medals 40 | } 41 | -------------------------------------------------------------------------------- /01-intro/exercises/champions.solution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object.} athletes 3 | * @returns {[string]} 4 | */ 5 | export default function champions (athletes) { 6 | const wonAtLeast3Medals = [] 7 | // Using for...of in combination with Object.entries to iterate over all the key-value pairs 8 | for (const [athlete, medals] of Object.entries(athletes)) { 9 | // Using reduce as a shortcut. Could have used another for...of and Object.values 10 | const countMedals = Object.values(medals).reduce((acc, curr) => acc + curr) 11 | if (countMedals >= 3) { 12 | wonAtLeast3Medals.push(athlete) 13 | } 14 | } 15 | return wonAtLeast3Medals 16 | } 17 | -------------------------------------------------------------------------------- /01-intro/exercises/champions.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import athletes from './data/athletes.js' 3 | 4 | import championsSolution from './champions.solution.js' 5 | import championsTpl from './champions.js' 6 | 7 | const champions = process.env.TEST_SOLUTIONS ? championsSolution : championsTpl 8 | 9 | tap.test('matches expected champions', async function (t) { 10 | const result = champions(athletes) 11 | t.same(result, [ 12 | 'Tadahiro Nomura', 13 | 'Rishod Sobirov', 14 | 'Lasha Shavdatuashvili', 15 | 'Mark Huizinga', 16 | 'Angelo Parisi', 17 | 'David Douillet', 18 | 'Teddy Riner', 19 | 'Ryoko Tamura', 20 | 'Amarilis Savón', 21 | 'Kye Sun-hui', 22 | 'Driulis González', 23 | 'Edith Bosch', 24 | 'Mayra Aguiar', 25 | 'Idalys Ortiz' 26 | ]) 27 | }) 28 | -------------------------------------------------------------------------------- /01-intro/exercises/data/athletes.js: -------------------------------------------------------------------------------- 1 | const athletes = { 2 | 'Thierry Rey': { 3 | gold: 1, 4 | silver: 0, 5 | bronze: 0 6 | }, 7 | 'José Rodríguez': { 8 | gold: 0, 9 | silver: 1, 10 | bronze: 0 11 | }, 12 | 'Tibor Kincses': { 13 | gold: 0, 14 | silver: 0, 15 | bronze: 1 16 | }, 17 | 'Aramby Emizh': { 18 | gold: 0, 19 | silver: 0, 20 | bronze: 1 21 | }, 22 | 'Shinji Hosokawa': { 23 | gold: 1, 24 | silver: 0, 25 | bronze: 1 26 | }, 27 | 'Kim Jae-yup': { 28 | gold: 1, 29 | silver: 1, 30 | bronze: 0 31 | }, 32 | 'Neil Eckersley': { 33 | gold: 0, 34 | silver: 0, 35 | bronze: 1 36 | }, 37 | 'Edward Liddie': { 38 | gold: 0, 39 | silver: 0, 40 | bronze: 1 41 | }, 42 | 'Kevin Asano': { 43 | gold: 0, 44 | silver: 1, 45 | bronze: 0 46 | }, 47 | 'Amiran Totikashvili': { 48 | gold: 0, 49 | silver: 0, 50 | bronze: 1 51 | }, 52 | 'Nazim Huseynov': { 53 | gold: 1, 54 | silver: 0, 55 | bronze: 0 56 | }, 57 | 'Yoon Hyun': { 58 | gold: 0, 59 | silver: 1, 60 | bronze: 0 61 | }, 62 | 'Tadanori Koshino': { 63 | gold: 0, 64 | silver: 0, 65 | bronze: 1 66 | }, 67 | 'Richard Trautmann': { 68 | gold: 0, 69 | silver: 0, 70 | bronze: 2 71 | }, 72 | 'Tadahiro Nomura': { 73 | gold: 3, 74 | silver: 0, 75 | bronze: 0 76 | }, 77 | 'Girolamo Giovinazzo': { 78 | gold: 0, 79 | silver: 1, 80 | bronze: 1 81 | }, 82 | 'Dorjpalamyn Narmandakh': { 83 | gold: 0, 84 | silver: 0, 85 | bronze: 1 86 | }, 87 | 'Jung Bu-kyung': { 88 | gold: 0, 89 | silver: 1, 90 | bronze: 0 91 | }, 92 | 'Manolo Poulot': { 93 | gold: 0, 94 | silver: 0, 95 | bronze: 1 96 | }, 97 | 'Aidyn Smagulov': { 98 | gold: 0, 99 | silver: 0, 100 | bronze: 1 101 | }, 102 | 'Nestor Khergiani': { 103 | gold: 0, 104 | silver: 1, 105 | bronze: 0 106 | }, 107 | 'Khashbaataryn Tsagaanbaatar': { 108 | gold: 0, 109 | silver: 0, 110 | bronze: 1 111 | }, 112 | 'Choi Min-ho': { 113 | gold: 0, 114 | silver: 0, 115 | bronze: 1 116 | }, 117 | 'Choi Min-Ho': { 118 | gold: 1, 119 | silver: 0, 120 | bronze: 0 121 | }, 122 | 'Ludwig Paischer': { 123 | gold: 0, 124 | silver: 1, 125 | bronze: 0 126 | }, 127 | 'Rishod Sobirov': { 128 | gold: 0, 129 | silver: 0, 130 | bronze: 3 131 | }, 132 | 'Ruben Houkes': { 133 | gold: 0, 134 | silver: 0, 135 | bronze: 1 136 | }, 137 | 'Arsen Galstyan': { 138 | gold: 1, 139 | silver: 0, 140 | bronze: 0 141 | }, 142 | 'Hiroaki Hiraoka': { 143 | gold: 0, 144 | silver: 1, 145 | bronze: 0 146 | }, 147 | 'Felipe Kitadai': { 148 | gold: 0, 149 | silver: 0, 150 | bronze: 1 151 | }, 152 | 'Beslan Mudranov': { 153 | gold: 1, 154 | silver: 0, 155 | bronze: 0 156 | }, 157 | 'Yeldos Smetov': { 158 | gold: 0, 159 | silver: 1, 160 | bronze: 1 161 | }, 162 | 'Naohisa Takato': { 163 | gold: 1, 164 | silver: 0, 165 | bronze: 1 166 | }, 167 | 'Diyorbek Urozboev': { 168 | gold: 0, 169 | silver: 0, 170 | bronze: 1 171 | }, 172 | 'Yang Yung-wei': { 173 | gold: 0, 174 | silver: 1, 175 | bronze: 0 176 | }, 177 | 'Luka Mkheidze': { 178 | gold: 0, 179 | silver: 0, 180 | bronze: 1 181 | }, 182 | 'Nikolay Solodukhin': { 183 | gold: 1, 184 | silver: 0, 185 | bronze: 0 186 | }, 187 | 'Tsendiin Damdin': { 188 | gold: 0, 189 | silver: 1, 190 | bronze: 0 191 | }, 192 | 'Iliyan Nedkov': { 193 | gold: 0, 194 | silver: 0, 195 | bronze: 1 196 | }, 197 | 'Janusz Pawłowski': { 198 | gold: 0, 199 | silver: 1, 200 | bronze: 1 201 | }, 202 | 'Yoshiyuki Matsuoka': { 203 | gold: 1, 204 | silver: 0, 205 | bronze: 0 206 | }, 207 | 'Hwang Jung-oh': { 208 | gold: 0, 209 | silver: 1, 210 | bronze: 0 211 | }, 212 | 'Marc Alexandre': { 213 | gold: 1, 214 | silver: 0, 215 | bronze: 1 216 | }, 217 | 'Josef Reiter': { 218 | gold: 0, 219 | silver: 0, 220 | bronze: 1 221 | }, 222 | 'Lee Kyung-keun': { 223 | gold: 1, 224 | silver: 0, 225 | bronze: 0 226 | }, 227 | 'Bruno Carabetta': { 228 | gold: 0, 229 | silver: 0, 230 | bronze: 1 231 | }, 232 | 'Yosuke Yamamoto': { 233 | gold: 0, 234 | silver: 0, 235 | bronze: 1 236 | }, 237 | 'Rogério Sampaio': { 238 | gold: 1, 239 | silver: 0, 240 | bronze: 0 241 | }, 242 | 'József Csák': { 243 | gold: 0, 244 | silver: 1, 245 | bronze: 0 246 | }, 247 | 'Israel Hernández': { 248 | gold: 0, 249 | silver: 0, 250 | bronze: 2 251 | }, 252 | 'Udo Quellmalz': { 253 | gold: 1, 254 | silver: 0, 255 | bronze: 1 256 | }, 257 | 'Yukimasa Nakamura': { 258 | gold: 0, 259 | silver: 1, 260 | bronze: 0 261 | }, 262 | 'Henrique Guimarães': { 263 | gold: 0, 264 | silver: 0, 265 | bronze: 1 266 | }, 267 | 'Hüseyin Özkan': { 268 | gold: 1, 269 | silver: 0, 270 | bronze: 0 271 | }, 272 | 'Larbi Benboudaoud': { 273 | gold: 0, 274 | silver: 1, 275 | bronze: 0 276 | }, 277 | 'Giorgi Vazagashvili': { 278 | gold: 0, 279 | silver: 0, 280 | bronze: 1 281 | }, 282 | 'Masato Uchishiba': { 283 | gold: 2, 284 | silver: 0, 285 | bronze: 0 286 | }, 287 | 'Jozef Krnáč': { 288 | gold: 0, 289 | silver: 1, 290 | bronze: 0 291 | }, 292 | 'Yordanis Arencibia': { 293 | gold: 0, 294 | silver: 0, 295 | bronze: 2 296 | }, 297 | 'Georgi Georgiev': { 298 | gold: 0, 299 | silver: 0, 300 | bronze: 1 301 | }, 302 | 'Benjamin Darbelet': { 303 | gold: 0, 304 | silver: 1, 305 | bronze: 0 306 | }, 307 | 'Pak Chol-Min': { 308 | gold: 0, 309 | silver: 0, 310 | bronze: 1 311 | }, 312 | 'Lasha Shavdatuashvili': { 313 | gold: 1, 314 | silver: 1, 315 | bronze: 1 316 | }, 317 | 'Miklós Ungvári': { 318 | gold: 0, 319 | silver: 1, 320 | bronze: 0 321 | }, 322 | 'Masashi Ebinuma': { 323 | gold: 0, 324 | silver: 0, 325 | bronze: 2 326 | }, 327 | 'Cho Jun-ho': { 328 | gold: 0, 329 | silver: 0, 330 | bronze: 1 331 | }, 332 | 'Fabio Basile': { 333 | gold: 1, 334 | silver: 0, 335 | bronze: 0 336 | }, 337 | 'An Baul': { 338 | gold: 0, 339 | silver: 1, 340 | bronze: 1 341 | }, 342 | 'Hifumi Abe': { 343 | gold: 1, 344 | silver: 0, 345 | bronze: 0 346 | }, 347 | 'Vazha Margvelashvili': { 348 | gold: 0, 349 | silver: 1, 350 | bronze: 0 351 | }, 352 | 'Daniel Cargnin': { 353 | gold: 0, 354 | silver: 0, 355 | bronze: 1 356 | }, 357 | 'Takehide Nakatani': { 358 | gold: 1, 359 | silver: 0, 360 | bronze: 0 361 | }, 362 | 'Eric Hänni': { 363 | gold: 0, 364 | silver: 1, 365 | bronze: 0 366 | }, 367 | 'Ārons Bogoļubovs': { 368 | gold: 0, 369 | silver: 0, 370 | bronze: 1 371 | }, 372 | 'Oleg Stepanov': { 373 | gold: 0, 374 | silver: 0, 375 | bronze: 1 376 | }, 377 | 'Takao Kawaguchi': { 378 | gold: 1, 379 | silver: 0, 380 | bronze: 0 381 | }, 382 | 'Kim Yong-ik': { 383 | gold: 0, 384 | silver: 0, 385 | bronze: 1 386 | }, 387 | 'Jean-Jacques Mounier': { 388 | gold: 0, 389 | silver: 0, 390 | bronze: 1 391 | }, 392 | 'Hector Rodriguez': { 393 | gold: 1, 394 | silver: 0, 395 | bronze: 0 396 | }, 397 | 'Chang Eun-kyung': { 398 | gold: 0, 399 | silver: 1, 400 | bronze: 0 401 | }, 402 | 'Felice Mariani': { 403 | gold: 0, 404 | silver: 0, 405 | bronze: 1 406 | }, 407 | 'József Tuncsik': { 408 | gold: 0, 409 | silver: 0, 410 | bronze: 1 411 | }, 412 | 'Ezio Gamba': { 413 | gold: 1, 414 | silver: 1, 415 | bronze: 0 416 | }, 417 | 'Neil Adams': { 418 | gold: 0, 419 | silver: 2, 420 | bronze: 0 421 | }, 422 | 'Ravdangiin Davaadalai': { 423 | gold: 0, 424 | silver: 0, 425 | bronze: 1 426 | }, 427 | 'Karl-Heinz Lehmann': { 428 | gold: 0, 429 | silver: 0, 430 | bronze: 1 431 | }, 432 | 'Ahn Byeong-keun': { 433 | gold: 1, 434 | silver: 0, 435 | bronze: 0 436 | }, 437 | 'Kerrith Brown': { 438 | gold: 0, 439 | silver: 0, 440 | bronze: 1 441 | }, 442 | 'Luís Onmura': { 443 | gold: 0, 444 | silver: 0, 445 | bronze: 1 446 | }, 447 | 'Sven Loll': { 448 | gold: 0, 449 | silver: 1, 450 | bronze: 0 451 | }, 452 | 'Mike Swain': { 453 | gold: 0, 454 | silver: 0, 455 | bronze: 1 456 | }, 457 | 'Georgy Tenadze': { 458 | gold: 0, 459 | silver: 0, 460 | bronze: 1 461 | }, 462 | 'Toshihiko Koga': { 463 | gold: 1, 464 | silver: 1, 465 | bronze: 0 466 | }, 467 | 'Bertalan Hajtós': { 468 | gold: 0, 469 | silver: 1, 470 | bronze: 0 471 | }, 472 | 'Hoon Chung': { 473 | gold: 0, 474 | silver: 0, 475 | bronze: 1 476 | }, 477 | 'Oren Smadja': { 478 | gold: 0, 479 | silver: 0, 480 | bronze: 1 481 | }, 482 | 'Kenzo Nakamura': { 483 | gold: 1, 484 | silver: 0, 485 | bronze: 0 486 | }, 487 | 'Kwak Dae-sung': { 488 | gold: 0, 489 | silver: 1, 490 | bronze: 0 491 | }, 492 | 'Christophe Gagliano': { 493 | gold: 0, 494 | silver: 0, 495 | bronze: 1 496 | }, 497 | 'Jimmy Pedro': { 498 | gold: 0, 499 | silver: 0, 500 | bronze: 2 501 | }, 502 | 'Giuseppe Maddaloni': { 503 | gold: 1, 504 | silver: 0, 505 | bronze: 0 506 | }, 507 | 'Tiago Camilo': { 508 | gold: 0, 509 | silver: 1, 510 | bronze: 1 511 | }, 512 | 'Anatoly Laryukov': { 513 | gold: 0, 514 | silver: 0, 515 | bronze: 1 516 | }, 517 | 'Vsevolods Zeļonijs': { 518 | gold: 0, 519 | silver: 0, 520 | bronze: 1 521 | }, 522 | 'Lee Won-hee': { 523 | gold: 1, 524 | silver: 0, 525 | bronze: 0 526 | }, 527 | 'Vitaliy Makarov': { 528 | gold: 0, 529 | silver: 1, 530 | bronze: 0 531 | }, 532 | 'Leandro Guilheiro': { 533 | gold: 0, 534 | silver: 0, 535 | bronze: 2 536 | }, 537 | 'Elnur Mammadli': { 538 | gold: 1, 539 | silver: 0, 540 | bronze: 0 541 | }, 542 | 'Wang Ki-chun': { 543 | gold: 0, 544 | silver: 1, 545 | bronze: 0 546 | }, 547 | 'Rasul Boqiev': { 548 | gold: 0, 549 | silver: 0, 550 | bronze: 1 551 | }, 552 | 'Mansur Isaev': { 553 | gold: 1, 554 | silver: 0, 555 | bronze: 0 556 | }, 557 | 'Riki Nakaya': { 558 | gold: 0, 559 | silver: 1, 560 | bronze: 0 561 | }, 562 | 'Nyam-Ochir Sainjargal': { 563 | gold: 0, 564 | silver: 0, 565 | bronze: 1 566 | }, 567 | 'Ugo Legrand': { 568 | gold: 0, 569 | silver: 0, 570 | bronze: 1 571 | }, 572 | 'Shohei Ono': { 573 | gold: 2, 574 | silver: 0, 575 | bronze: 0 576 | }, 577 | 'Rustam Orujov': { 578 | gold: 0, 579 | silver: 1, 580 | bronze: 0 581 | }, 582 | 'Dirk Van Tichelt': { 583 | gold: 0, 584 | silver: 0, 585 | bronze: 1 586 | }, 587 | 'An Chang-rim': { 588 | gold: 0, 589 | silver: 0, 590 | bronze: 1 591 | }, 592 | 'Tsend-Ochiryn Tsogtbaatar': { 593 | gold: 0, 594 | silver: 0, 595 | bronze: 1 596 | }, 597 | 'Toyokazu Nomura': { 598 | gold: 1, 599 | silver: 0, 600 | bronze: 0 601 | }, 602 | 'Antoni Zajkowski': { 603 | gold: 0, 604 | silver: 1, 605 | bronze: 0 606 | }, 607 | 'Dietmar Hotger': { 608 | gold: 0, 609 | silver: 0, 610 | bronze: 1 611 | }, 612 | 'Anatoliy Novikov': { 613 | gold: 0, 614 | silver: 0, 615 | bronze: 1 616 | }, 617 | 'Vladimir Nevzorov': { 618 | gold: 1, 619 | silver: 0, 620 | bronze: 0 621 | }, 622 | 'Koji Kuramoto': { 623 | gold: 0, 624 | silver: 1, 625 | bronze: 0 626 | }, 627 | 'Marian Tałaj': { 628 | gold: 0, 629 | silver: 0, 630 | bronze: 1 631 | }, 632 | 'Patrick Vial': { 633 | gold: 0, 634 | silver: 0, 635 | bronze: 1 636 | }, 637 | 'Shota Khabareli': { 638 | gold: 1, 639 | silver: 0, 640 | bronze: 0 641 | }, 642 | 'Juan Ferrer': { 643 | gold: 0, 644 | silver: 1, 645 | bronze: 0 646 | }, 647 | 'Harald Heinke': { 648 | gold: 0, 649 | silver: 0, 650 | bronze: 1 651 | }, 652 | 'Bernard Tchoullouyan': { 653 | gold: 0, 654 | silver: 0, 655 | bronze: 1 656 | }, 657 | 'Frank Wieneke': { 658 | gold: 1, 659 | silver: 1, 660 | bronze: 0 661 | }, 662 | 'Mircea Frăţică': { 663 | gold: 0, 664 | silver: 0, 665 | bronze: 1 666 | }, 667 | 'Michel Nowak': { 668 | gold: 0, 669 | silver: 0, 670 | bronze: 1 671 | }, 672 | 'Waldemar Legień': { 673 | gold: 2, 674 | silver: 0, 675 | bronze: 0 676 | }, 677 | 'Torsten Bréchôt': { 678 | gold: 0, 679 | silver: 0, 680 | bronze: 1 681 | }, 682 | 'Bashir Varaev': { 683 | gold: 0, 684 | silver: 0, 685 | bronze: 1 686 | }, 687 | 'Hidehiko Yoshida': { 688 | gold: 1, 689 | silver: 0, 690 | bronze: 0 691 | }, 692 | 'Jason Morris': { 693 | gold: 0, 694 | silver: 1, 695 | bronze: 0 696 | }, 697 | 'Kim Byung-joo': { 698 | gold: 0, 699 | silver: 0, 700 | bronze: 1 701 | }, 702 | 'Bertrand Damaisin': { 703 | gold: 0, 704 | silver: 0, 705 | bronze: 1 706 | }, 707 | 'Djamel Bouras': { 708 | gold: 1, 709 | silver: 0, 710 | bronze: 0 711 | }, 712 | 'Cho In-chul': { 713 | gold: 0, 714 | silver: 1, 715 | bronze: 1 716 | }, 717 | 'Soso Liparteliani': { 718 | gold: 0, 719 | silver: 0, 720 | bronze: 1 721 | }, 722 | 'Makoto Takimoto': { 723 | gold: 1, 724 | silver: 0, 725 | bronze: 0 726 | }, 727 | 'Aleksei Budõlin': { 728 | gold: 0, 729 | silver: 0, 730 | bronze: 1 731 | }, 732 | 'Nuno Delgado': { 733 | gold: 0, 734 | silver: 0, 735 | bronze: 1 736 | }, 737 | 'Ilias Iliadis': { 738 | gold: 1, 739 | silver: 0, 740 | bronze: 1 741 | }, 742 | 'Roman Gontiuk': { 743 | gold: 0, 744 | silver: 1, 745 | bronze: 1 746 | }, 747 | 'Flávio Canto': { 748 | gold: 0, 749 | silver: 0, 750 | bronze: 1 751 | }, 752 | 'Dmitri Nossov': { 753 | gold: 0, 754 | silver: 0, 755 | bronze: 1 756 | }, 757 | 'Ole Bischof': { 758 | gold: 1, 759 | silver: 1, 760 | bronze: 0 761 | }, 762 | 'Kim Jae-bum': { 763 | gold: 1, 764 | silver: 1, 765 | bronze: 0 766 | }, 767 | 'Ivan Nifontov': { 768 | gold: 0, 769 | silver: 0, 770 | bronze: 1 771 | }, 772 | 'Antoine Valois-Fortier': { 773 | gold: 0, 774 | silver: 0, 775 | bronze: 1 776 | }, 777 | 'Khasan Khalmurzaev': { 778 | gold: 1, 779 | silver: 0, 780 | bronze: 0 781 | }, 782 | 'Travis Stevens': { 783 | gold: 0, 784 | silver: 1, 785 | bronze: 0 786 | }, 787 | 'Sergiu Toma': { 788 | gold: 0, 789 | silver: 0, 790 | bronze: 1 791 | }, 792 | 'Takanori Nagase': { 793 | gold: 1, 794 | silver: 0, 795 | bronze: 1 796 | }, 797 | 'Saeid Mollaei': { 798 | gold: 0, 799 | silver: 1, 800 | bronze: 0 801 | }, 802 | 'Shamil Borchashvili': { 803 | gold: 0, 804 | silver: 0, 805 | bronze: 1 806 | }, 807 | 'Matthias Casse': { 808 | gold: 0, 809 | silver: 0, 810 | bronze: 1 811 | }, 812 | 'Isao Okano': { 813 | gold: 1, 814 | silver: 0, 815 | bronze: 0 816 | }, 817 | 'Wolfgang Hofmann': { 818 | gold: 0, 819 | silver: 1, 820 | bronze: 0 821 | }, 822 | 'James Bregman': { 823 | gold: 0, 824 | silver: 0, 825 | bronze: 1 826 | }, 827 | 'Kim Eui-tae': { 828 | gold: 0, 829 | silver: 0, 830 | bronze: 1 831 | }, 832 | 'Shinobu Sekine': { 833 | gold: 1, 834 | silver: 0, 835 | bronze: 0 836 | }, 837 | 'Oh Seung-lip': { 838 | gold: 0, 839 | silver: 1, 840 | bronze: 0 841 | }, 842 | 'Jean-Paul Coche': { 843 | gold: 0, 844 | silver: 0, 845 | bronze: 1 846 | }, 847 | 'Brian Jacks': { 848 | gold: 0, 849 | silver: 0, 850 | bronze: 1 851 | }, 852 | 'Isamu Sonoda': { 853 | gold: 1, 854 | silver: 0, 855 | bronze: 0 856 | }, 857 | 'Valeriy Dvoynikov': { 858 | gold: 0, 859 | silver: 1, 860 | bronze: 0 861 | }, 862 | 'Slavko Obadov': { 863 | gold: 0, 864 | silver: 0, 865 | bronze: 1 866 | }, 867 | 'Park Young-chul': { 868 | gold: 0, 869 | silver: 0, 870 | bronze: 1 871 | }, 872 | 'Jürg Röthlisberger': { 873 | gold: 1, 874 | silver: 0, 875 | bronze: 1 876 | }, 877 | 'Isaac Azcuy': { 878 | gold: 0, 879 | silver: 1, 880 | bronze: 0 881 | }, 882 | 'Aleksandrs Jackevičs': { 883 | gold: 0, 884 | silver: 0, 885 | bronze: 1 886 | }, 887 | 'Detlef Ultsch': { 888 | gold: 0, 889 | silver: 0, 890 | bronze: 1 891 | }, 892 | 'Peter Seisenbacher': { 893 | gold: 2, 894 | silver: 0, 895 | bronze: 0 896 | }, 897 | 'Robert Berland': { 898 | gold: 0, 899 | silver: 1, 900 | bronze: 0 901 | }, 902 | 'Walter Carmona': { 903 | gold: 0, 904 | silver: 0, 905 | bronze: 1 906 | }, 907 | 'Seiki Nose': { 908 | gold: 0, 909 | silver: 0, 910 | bronze: 1 911 | }, 912 | 'Vladimir Shestakov': { 913 | gold: 0, 914 | silver: 1, 915 | bronze: 0 916 | }, 917 | 'Akinobu Osako': { 918 | gold: 0, 919 | silver: 0, 920 | bronze: 1 921 | }, 922 | 'Ben Spijkers': { 923 | gold: 0, 924 | silver: 0, 925 | bronze: 1 926 | }, 927 | 'Pascal Tayot': { 928 | gold: 0, 929 | silver: 1, 930 | bronze: 0 931 | }, 932 | 'Nicolas Gill': { 933 | gold: 0, 934 | silver: 1, 935 | bronze: 1 936 | }, 937 | 'Hirotaka Okada': { 938 | gold: 0, 939 | silver: 0, 940 | bronze: 1 941 | }, 942 | 'Jeon Ki-young': { 943 | gold: 1, 944 | silver: 0, 945 | bronze: 0 946 | }, 947 | 'Armen Bagdasarov': { 948 | gold: 0, 949 | silver: 1, 950 | bronze: 0 951 | }, 952 | 'Mark Huizinga': { 953 | gold: 1, 954 | silver: 0, 955 | bronze: 2 956 | }, 957 | 'Marko Spittka': { 958 | gold: 0, 959 | silver: 0, 960 | bronze: 1 961 | }, 962 | 'Carlos Honorato': { 963 | gold: 0, 964 | silver: 1, 965 | bronze: 0 966 | }, 967 | 'Frédéric Demontfaucon': { 968 | gold: 0, 969 | silver: 0, 970 | bronze: 1 971 | }, 972 | 'Ruslan Mashurenko': { 973 | gold: 0, 974 | silver: 0, 975 | bronze: 1 976 | }, 977 | 'Zurab Zviadauri': { 978 | gold: 1, 979 | silver: 0, 980 | bronze: 0 981 | }, 982 | 'Hiroshi Izumi': { 983 | gold: 0, 984 | silver: 1, 985 | bronze: 0 986 | }, 987 | 'Khasanbi Taov': { 988 | gold: 0, 989 | silver: 0, 990 | bronze: 1 991 | }, 992 | 'Irakli Tsirekidze': { 993 | gold: 1, 994 | silver: 0, 995 | bronze: 0 996 | }, 997 | 'Amar Benikhlef': { 998 | gold: 0, 999 | silver: 1, 1000 | bronze: 0 1001 | }, 1002 | 'Hesham Mesbah': { 1003 | gold: 0, 1004 | silver: 0, 1005 | bronze: 1 1006 | }, 1007 | 'Sergei Aschwanden': { 1008 | gold: 0, 1009 | silver: 0, 1010 | bronze: 1 1011 | }, 1012 | 'Song Dae-nam': { 1013 | gold: 1, 1014 | silver: 0, 1015 | bronze: 0 1016 | }, 1017 | 'Asley Gonzalez': { 1018 | gold: 0, 1019 | silver: 1, 1020 | bronze: 0 1021 | }, 1022 | 'Masashi Nishiyama': { 1023 | gold: 0, 1024 | silver: 0, 1025 | bronze: 1 1026 | }, 1027 | 'Mashu Baker': { 1028 | gold: 1, 1029 | silver: 0, 1030 | bronze: 0 1031 | }, 1032 | 'Varlam Liparteliani': { 1033 | gold: 0, 1034 | silver: 1, 1035 | bronze: 0 1036 | }, 1037 | 'Gwak Dong-han': { 1038 | gold: 0, 1039 | silver: 0, 1040 | bronze: 1 1041 | }, 1042 | 'Cheng Xunzhao': { 1043 | gold: 0, 1044 | silver: 0, 1045 | bronze: 1 1046 | }, 1047 | 'Lasha Bekauri': { 1048 | gold: 1, 1049 | silver: 0, 1050 | bronze: 0 1051 | }, 1052 | 'Eduard Trippel': { 1053 | gold: 0, 1054 | silver: 1, 1055 | bronze: 0 1056 | }, 1057 | 'Davlat Bobonov': { 1058 | gold: 0, 1059 | silver: 0, 1060 | bronze: 1 1061 | }, 1062 | 'Krisztián Tóth': { 1063 | gold: 0, 1064 | silver: 0, 1065 | bronze: 1 1066 | }, 1067 | 'Shota Chochishvili': { 1068 | gold: 1, 1069 | silver: 0, 1070 | bronze: 1 1071 | }, 1072 | 'David Starbrook': { 1073 | gold: 0, 1074 | silver: 1, 1075 | bronze: 1 1076 | }, 1077 | 'Paul Barth': { 1078 | gold: 0, 1079 | silver: 0, 1080 | bronze: 1 1081 | }, 1082 | 'Chiaki Ishii': { 1083 | gold: 0, 1084 | silver: 0, 1085 | bronze: 1 1086 | }, 1087 | 'Kazuhiro Ninomiya': { 1088 | gold: 1, 1089 | silver: 0, 1090 | bronze: 0 1091 | }, 1092 | 'Ramaz Kharshiladze': { 1093 | gold: 0, 1094 | silver: 1, 1095 | bronze: 0 1096 | }, 1097 | 'Robert Van de Walle': { 1098 | gold: 1, 1099 | silver: 0, 1100 | bronze: 0 1101 | }, 1102 | 'Tengiz Khubuluri': { 1103 | gold: 0, 1104 | silver: 1, 1105 | bronze: 0 1106 | }, 1107 | 'Dietmar Lorenz': { 1108 | gold: 1, 1109 | silver: 0, 1110 | bronze: 1 1111 | }, 1112 | 'Henk Numan': { 1113 | gold: 0, 1114 | silver: 0, 1115 | bronze: 1 1116 | }, 1117 | 'Ha Hyung-joo': { 1118 | gold: 1, 1119 | silver: 0, 1120 | bronze: 0 1121 | }, 1122 | 'Douglas Vieria': { 1123 | gold: 0, 1124 | silver: 1, 1125 | bronze: 0 1126 | }, 1127 | 'Bjarni Friðriksson': { 1128 | gold: 0, 1129 | silver: 0, 1130 | bronze: 1 1131 | }, 1132 | 'Günther Neureuther': { 1133 | gold: 0, 1134 | silver: 1, 1135 | bronze: 1 1136 | }, 1137 | 'Aurélio Miguel': { 1138 | gold: 1, 1139 | silver: 0, 1140 | bronze: 1 1141 | }, 1142 | 'Marc Meiling': { 1143 | gold: 0, 1144 | silver: 1, 1145 | bronze: 0 1146 | }, 1147 | 'Dennis Stewart': { 1148 | gold: 0, 1149 | silver: 0, 1150 | bronze: 1 1151 | }, 1152 | 'Robert Van De Walle': { 1153 | gold: 0, 1154 | silver: 0, 1155 | bronze: 1 1156 | }, 1157 | 'Antal Kovács': { 1158 | gold: 1, 1159 | silver: 0, 1160 | bronze: 0 1161 | }, 1162 | 'Raymond Stevens': { 1163 | gold: 0, 1164 | silver: 1, 1165 | bronze: 0 1166 | }, 1167 | 'Theo Meijer': { 1168 | gold: 0, 1169 | silver: 0, 1170 | bronze: 1 1171 | }, 1172 | 'Dmitri Sergeyev': { 1173 | gold: 0, 1174 | silver: 0, 1175 | bronze: 1 1176 | }, 1177 | 'Paweł Nastula': { 1178 | gold: 1, 1179 | silver: 0, 1180 | bronze: 0 1181 | }, 1182 | 'Kim Min-soo': { 1183 | gold: 0, 1184 | silver: 1, 1185 | bronze: 0 1186 | }, 1187 | 'Stéphane Traineau': { 1188 | gold: 0, 1189 | silver: 0, 1190 | bronze: 2 1191 | }, 1192 | 'Kōsei Inoue': { 1193 | gold: 1, 1194 | silver: 0, 1195 | bronze: 0 1196 | }, 1197 | 'Youri Stepkine': { 1198 | gold: 0, 1199 | silver: 0, 1200 | bronze: 1 1201 | }, 1202 | 'Ihar Makarau': { 1203 | gold: 1, 1204 | silver: 0, 1205 | bronze: 0 1206 | }, 1207 | 'Jang Sung-ho': { 1208 | gold: 0, 1209 | silver: 1, 1210 | bronze: 0 1211 | }, 1212 | 'Michael Jurack': { 1213 | gold: 0, 1214 | silver: 0, 1215 | bronze: 1 1216 | }, 1217 | "Ariel Ze'evi": { 1218 | gold: 0, 1219 | silver: 0, 1220 | bronze: 1 1221 | }, 1222 | 'Naidangiin Tüvshinbayar': { 1223 | gold: 1, 1224 | silver: 1, 1225 | bronze: 0 1226 | }, 1227 | 'Askhat Zhitkeyev': { 1228 | gold: 0, 1229 | silver: 1, 1230 | bronze: 0 1231 | }, 1232 | 'Movlud Miraliyev': { 1233 | gold: 0, 1234 | silver: 0, 1235 | bronze: 1 1236 | }, 1237 | 'Henk Grol': { 1238 | gold: 0, 1239 | silver: 0, 1240 | bronze: 2 1241 | }, 1242 | 'Tagir Khaybulaev': { 1243 | gold: 1, 1244 | silver: 0, 1245 | bronze: 0 1246 | }, 1247 | 'Dimitri Peters': { 1248 | gold: 0, 1249 | silver: 0, 1250 | bronze: 1 1251 | }, 1252 | 'Lukáš Krpálek': { 1253 | gold: 2, 1254 | silver: 0, 1255 | bronze: 0 1256 | }, 1257 | 'Elmar Gasimov': { 1258 | gold: 0, 1259 | silver: 1, 1260 | bronze: 0 1261 | }, 1262 | 'Cyrille Maret': { 1263 | gold: 0, 1264 | silver: 0, 1265 | bronze: 1 1266 | }, 1267 | 'Ryunosuke Haga': { 1268 | gold: 0, 1269 | silver: 0, 1270 | bronze: 1 1271 | }, 1272 | 'Aaron Wolf': { 1273 | gold: 1, 1274 | silver: 0, 1275 | bronze: 0 1276 | }, 1277 | 'Cho Gu-ham': { 1278 | gold: 0, 1279 | silver: 1, 1280 | bronze: 0 1281 | }, 1282 | 'Jorge Fonseca': { 1283 | gold: 0, 1284 | silver: 0, 1285 | bronze: 1 1286 | }, 1287 | 'Niyaz Ilyasov': { 1288 | gold: 0, 1289 | silver: 0, 1290 | bronze: 1 1291 | }, 1292 | 'Isao Inokuma': { 1293 | gold: 1, 1294 | silver: 0, 1295 | bronze: 0 1296 | }, 1297 | 'Doug Rogers': { 1298 | gold: 0, 1299 | silver: 1, 1300 | bronze: 0 1301 | }, 1302 | 'Parnaoz Chikviladze': { 1303 | gold: 0, 1304 | silver: 0, 1305 | bronze: 1 1306 | }, 1307 | 'Anzor Kiknadze': { 1308 | gold: 0, 1309 | silver: 0, 1310 | bronze: 1 1311 | }, 1312 | 'Wim Ruska': { 1313 | gold: 2, 1314 | silver: 0, 1315 | bronze: 0 1316 | }, 1317 | 'Klaus Glahn': { 1318 | gold: 0, 1319 | silver: 1, 1320 | bronze: 1 1321 | }, 1322 | 'Motoki Nishimura': { 1323 | gold: 0, 1324 | silver: 0, 1325 | bronze: 1 1326 | }, 1327 | 'Givi Onashvili': { 1328 | gold: 0, 1329 | silver: 0, 1330 | bronze: 1 1331 | }, 1332 | 'Serhiy Novikov': { 1333 | gold: 1, 1334 | silver: 0, 1335 | bronze: 0 1336 | }, 1337 | 'Allen Coage': { 1338 | gold: 0, 1339 | silver: 0, 1340 | bronze: 1 1341 | }, 1342 | 'Sumio Endo': { 1343 | gold: 0, 1344 | silver: 0, 1345 | bronze: 1 1346 | }, 1347 | 'Angelo Parisi': { 1348 | gold: 1, 1349 | silver: 2, 1350 | bronze: 1 1351 | }, 1352 | 'Dimitar Zaprianov': { 1353 | gold: 0, 1354 | silver: 1, 1355 | bronze: 0 1356 | }, 1357 | 'Radomir Kovačević': { 1358 | gold: 0, 1359 | silver: 0, 1360 | bronze: 1 1361 | }, 1362 | 'Vladimír Kocman': { 1363 | gold: 0, 1364 | silver: 0, 1365 | bronze: 1 1366 | }, 1367 | 'Hitoshi Saito': { 1368 | gold: 2, 1369 | silver: 0, 1370 | bronze: 0 1371 | }, 1372 | 'Mark Berger': { 1373 | gold: 0, 1374 | silver: 0, 1375 | bronze: 1 1376 | }, 1377 | 'Cho Yong-chul': { 1378 | gold: 0, 1379 | silver: 0, 1380 | bronze: 2 1381 | }, 1382 | 'Henry Stöhr': { 1383 | gold: 0, 1384 | silver: 1, 1385 | bronze: 0 1386 | }, 1387 | 'Grigory Verichev': { 1388 | gold: 0, 1389 | silver: 0, 1390 | bronze: 1 1391 | }, 1392 | 'David Khakhaleishvili': { 1393 | gold: 1, 1394 | silver: 0, 1395 | bronze: 0 1396 | }, 1397 | 'Naoya Ogawa': { 1398 | gold: 0, 1399 | silver: 1, 1400 | bronze: 0 1401 | }, 1402 | 'Imre Csösz': { 1403 | gold: 0, 1404 | silver: 0, 1405 | bronze: 1 1406 | }, 1407 | 'David Douillet': { 1408 | gold: 2, 1409 | silver: 0, 1410 | bronze: 1 1411 | }, 1412 | 'Ernesto Pérez': { 1413 | gold: 0, 1414 | silver: 1, 1415 | bronze: 0 1416 | }, 1417 | 'Frank Möller': { 1418 | gold: 0, 1419 | silver: 0, 1420 | bronze: 1 1421 | }, 1422 | 'Harry van Barneveld': { 1423 | gold: 0, 1424 | silver: 0, 1425 | bronze: 1 1426 | }, 1427 | 'Shinichi Shinohara': { 1428 | gold: 0, 1429 | silver: 1, 1430 | bronze: 0 1431 | }, 1432 | 'Indrek Pertelson': { 1433 | gold: 0, 1434 | silver: 0, 1435 | bronze: 2 1436 | }, 1437 | 'Tamerlan Tmenov': { 1438 | gold: 0, 1439 | silver: 1, 1440 | bronze: 1 1441 | }, 1442 | 'Keiji Suzuki': { 1443 | gold: 1, 1444 | silver: 0, 1445 | bronze: 0 1446 | }, 1447 | 'Dennis van der Geest': { 1448 | gold: 0, 1449 | silver: 0, 1450 | bronze: 1 1451 | }, 1452 | 'Satoshi Ishii': { 1453 | gold: 1, 1454 | silver: 0, 1455 | bronze: 0 1456 | }, 1457 | 'Abdullo Tangriev': { 1458 | gold: 0, 1459 | silver: 1, 1460 | bronze: 0 1461 | }, 1462 | 'Oscar Braison': { 1463 | gold: 0, 1464 | silver: 0, 1465 | bronze: 1 1466 | }, 1467 | 'Teddy Riner': { 1468 | gold: 2, 1469 | silver: 0, 1470 | bronze: 2 1471 | }, 1472 | 'Aleksandr Mikhailine': { 1473 | gold: 0, 1474 | silver: 1, 1475 | bronze: 0 1476 | }, 1477 | 'Rafael Silva': { 1478 | gold: 0, 1479 | silver: 0, 1480 | bronze: 2 1481 | }, 1482 | 'Andreas Tölzer': { 1483 | gold: 0, 1484 | silver: 0, 1485 | bronze: 1 1486 | }, 1487 | 'Hisayoshi Harasawa': { 1488 | gold: 0, 1489 | silver: 1, 1490 | bronze: 0 1491 | }, 1492 | 'Or Sasson': { 1493 | gold: 0, 1494 | silver: 0, 1495 | bronze: 1 1496 | }, 1497 | 'Guram Tushishvili': { 1498 | gold: 0, 1499 | silver: 1, 1500 | bronze: 0 1501 | }, 1502 | 'Tamerlan Bashaev': { 1503 | gold: 0, 1504 | silver: 0, 1505 | bronze: 1 1506 | }, 1507 | 'Anton Geesink': { 1508 | gold: 1, 1509 | silver: 0, 1510 | bronze: 0 1511 | }, 1512 | 'Akio Kaminaga': { 1513 | gold: 0, 1514 | silver: 1, 1515 | bronze: 0 1516 | }, 1517 | 'Theodore Boronovskis': { 1518 | gold: 0, 1519 | silver: 0, 1520 | bronze: 1 1521 | }, 1522 | 'Vitali Kuznetsov': { 1523 | gold: 0, 1524 | silver: 1, 1525 | bronze: 0 1526 | }, 1527 | 'Jean-Claude Brondani': { 1528 | gold: 0, 1529 | silver: 0, 1530 | bronze: 1 1531 | }, 1532 | 'Haruki Uemura': { 1533 | gold: 1, 1534 | silver: 0, 1535 | bronze: 0 1536 | }, 1537 | 'Keith Remfry': { 1538 | gold: 0, 1539 | silver: 1, 1540 | bronze: 0 1541 | }, 1542 | 'Cho Jea-Ki': { 1543 | gold: 0, 1544 | silver: 0, 1545 | bronze: 1 1546 | }, 1547 | 'András Ozsvár': { 1548 | gold: 0, 1549 | silver: 0, 1550 | bronze: 1 1551 | }, 1552 | 'Arthur Mapp': { 1553 | gold: 0, 1554 | silver: 0, 1555 | bronze: 1 1556 | }, 1557 | 'Yasuhiro Yamashita': { 1558 | gold: 1, 1559 | silver: 0, 1560 | bronze: 0 1561 | }, 1562 | 'Mohamed Rashwan': { 1563 | gold: 0, 1564 | silver: 1, 1565 | bronze: 0 1566 | }, 1567 | 'Arthur Schnabel': { 1568 | gold: 0, 1569 | silver: 0, 1570 | bronze: 1 1571 | }, 1572 | 'Mihai Cioc': { 1573 | gold: 0, 1574 | silver: 0, 1575 | bronze: 1 1576 | }, 1577 | 'Cécile Nowak': { 1578 | gold: 1, 1579 | silver: 0, 1580 | bronze: 0 1581 | }, 1582 | 'Ryoko Tamura': { 1583 | gold: 1, 1584 | silver: 2, 1585 | bronze: 0 1586 | }, 1587 | 'Amarilis Savón': { 1588 | gold: 0, 1589 | silver: 0, 1590 | bronze: 3 1591 | }, 1592 | 'Hülya Şenyurt': { 1593 | gold: 0, 1594 | silver: 0, 1595 | bronze: 1 1596 | }, 1597 | 'Kye Sun-hui': { 1598 | gold: 1, 1599 | silver: 1, 1600 | bronze: 1 1601 | }, 1602 | 'Yolanda Soler': { 1603 | gold: 0, 1604 | silver: 0, 1605 | bronze: 1 1606 | }, 1607 | 'Lyubov Bruletova': { 1608 | gold: 0, 1609 | silver: 1, 1610 | bronze: 0 1611 | }, 1612 | 'Anna-Maria Gradante': { 1613 | gold: 0, 1614 | silver: 0, 1615 | bronze: 1 1616 | }, 1617 | 'Ann Simons': { 1618 | gold: 0, 1619 | silver: 0, 1620 | bronze: 1 1621 | }, 1622 | 'Ryoko Tani': { 1623 | gold: 1, 1624 | silver: 0, 1625 | bronze: 1 1626 | }, 1627 | 'Frédérique Jossinet': { 1628 | gold: 0, 1629 | silver: 1, 1630 | bronze: 0 1631 | }, 1632 | 'Julia Matijass': { 1633 | gold: 0, 1634 | silver: 0, 1635 | bronze: 1 1636 | }, 1637 | 'Gao Feng': { 1638 | gold: 0, 1639 | silver: 0, 1640 | bronze: 1 1641 | }, 1642 | 'Alina Dumitru': { 1643 | gold: 1, 1644 | silver: 1, 1645 | bronze: 0 1646 | }, 1647 | 'Yanet Bermoy': { 1648 | gold: 0, 1649 | silver: 2, 1650 | bronze: 0 1651 | }, 1652 | 'Paula Pareto': { 1653 | gold: 1, 1654 | silver: 0, 1655 | bronze: 1 1656 | }, 1657 | 'Sarah Menezes': { 1658 | gold: 1, 1659 | silver: 0, 1660 | bronze: 0 1661 | }, 1662 | 'Eva Csernoviczki': { 1663 | gold: 0, 1664 | silver: 0, 1665 | bronze: 1 1666 | }, 1667 | 'Charline Van Snick': { 1668 | gold: 0, 1669 | silver: 0, 1670 | bronze: 1 1671 | }, 1672 | 'Jeong Bo-kyeong': { 1673 | gold: 0, 1674 | silver: 1, 1675 | bronze: 0 1676 | }, 1677 | 'Ami Kondo': { 1678 | gold: 0, 1679 | silver: 0, 1680 | bronze: 1 1681 | }, 1682 | 'Galbadrakhyn Otgontsetseg': { 1683 | gold: 0, 1684 | silver: 0, 1685 | bronze: 1 1686 | }, 1687 | 'Distria Krasniqi': { 1688 | gold: 1, 1689 | silver: 0, 1690 | bronze: 0 1691 | }, 1692 | 'Funa Tonaki': { 1693 | gold: 0, 1694 | silver: 1, 1695 | bronze: 0 1696 | }, 1697 | 'Daria Bilodid': { 1698 | gold: 0, 1699 | silver: 0, 1700 | bronze: 1 1701 | }, 1702 | 'Urantsetseg Munkhbat': { 1703 | gold: 0, 1704 | silver: 0, 1705 | bronze: 1 1706 | }, 1707 | 'Almudena Muñoz': { 1708 | gold: 1, 1709 | silver: 0, 1710 | bronze: 0 1711 | }, 1712 | 'Noriko Mizoguchi': { 1713 | gold: 0, 1714 | silver: 1, 1715 | bronze: 0 1716 | }, 1717 | 'Li Zhongyun': { 1718 | gold: 0, 1719 | silver: 0, 1720 | bronze: 1 1721 | }, 1722 | 'Sharon Rendle': { 1723 | gold: 0, 1724 | silver: 0, 1725 | bronze: 1 1726 | }, 1727 | 'Marie-Claire Restoux': { 1728 | gold: 1, 1729 | silver: 0, 1730 | bronze: 0 1731 | }, 1732 | 'Hyun Sook-hee': { 1733 | gold: 0, 1734 | silver: 1, 1735 | bronze: 0 1736 | }, 1737 | 'Noriko Sugawara': { 1738 | gold: 0, 1739 | silver: 0, 1740 | bronze: 1 1741 | }, 1742 | 'Legna Verdecia': { 1743 | gold: 1, 1744 | silver: 0, 1745 | bronze: 1 1746 | }, 1747 | 'Noriko Narazaki': { 1748 | gold: 0, 1749 | silver: 1, 1750 | bronze: 0 1751 | }, 1752 | 'Liu Yuxiang': { 1753 | gold: 0, 1754 | silver: 0, 1755 | bronze: 1 1756 | }, 1757 | 'Xian Dongmei': { 1758 | gold: 2, 1759 | silver: 0, 1760 | bronze: 0 1761 | }, 1762 | 'Yuki Yokosawa': { 1763 | gold: 0, 1764 | silver: 1, 1765 | bronze: 0 1766 | }, 1767 | 'Ilse Heylen': { 1768 | gold: 0, 1769 | silver: 0, 1770 | bronze: 1 1771 | }, 1772 | 'An Kum-ae': { 1773 | gold: 1, 1774 | silver: 1, 1775 | bronze: 0 1776 | }, 1777 | 'Soraya Haddad': { 1778 | gold: 0, 1779 | silver: 0, 1780 | bronze: 1 1781 | }, 1782 | 'Misato Nakamura': { 1783 | gold: 0, 1784 | silver: 0, 1785 | bronze: 2 1786 | }, 1787 | 'Rosalba Forciniti': { 1788 | gold: 0, 1789 | silver: 0, 1790 | bronze: 1 1791 | }, 1792 | 'Priscilla Gneto': { 1793 | gold: 0, 1794 | silver: 0, 1795 | bronze: 1 1796 | }, 1797 | 'Majlinda Kelmendi': { 1798 | gold: 1, 1799 | silver: 0, 1800 | bronze: 0 1801 | }, 1802 | 'Odette Giuffrida': { 1803 | gold: 0, 1804 | silver: 1, 1805 | bronze: 1 1806 | }, 1807 | 'Natalia Kuziutina': { 1808 | gold: 0, 1809 | silver: 0, 1810 | bronze: 1 1811 | }, 1812 | 'Uta Abe': { 1813 | gold: 1, 1814 | silver: 0, 1815 | bronze: 0 1816 | }, 1817 | 'Amandine Buchard': { 1818 | gold: 0, 1819 | silver: 1, 1820 | bronze: 0 1821 | }, 1822 | 'Chelsie Giles': { 1823 | gold: 0, 1824 | silver: 0, 1825 | bronze: 1 1826 | }, 1827 | 'Miriam Blasco Soto': { 1828 | gold: 1, 1829 | silver: 0, 1830 | bronze: 0 1831 | }, 1832 | 'Nicola Fairbrother': { 1833 | gold: 0, 1834 | silver: 1, 1835 | bronze: 0 1836 | }, 1837 | 'Driulis González': { 1838 | gold: 1, 1839 | silver: 1, 1840 | bronze: 2 1841 | }, 1842 | 'Chiyori Tateno': { 1843 | gold: 0, 1844 | silver: 0, 1845 | bronze: 1 1846 | }, 1847 | 'Jung Sun-yong': { 1848 | gold: 0, 1849 | silver: 1, 1850 | bronze: 0 1851 | }, 1852 | 'Isabel Fernández': { 1853 | gold: 1, 1854 | silver: 0, 1855 | bronze: 1 1856 | }, 1857 | 'Marisabel Lomba': { 1858 | gold: 0, 1859 | silver: 0, 1860 | bronze: 1 1861 | }, 1862 | 'Kie Kusakabe': { 1863 | gold: 0, 1864 | silver: 0, 1865 | bronze: 1 1866 | }, 1867 | 'Maria Pekli': { 1868 | gold: 0, 1869 | silver: 0, 1870 | bronze: 1 1871 | }, 1872 | 'Yvonne Bönisch': { 1873 | gold: 1, 1874 | silver: 0, 1875 | bronze: 0 1876 | }, 1877 | 'Deborah Gravenstijn': { 1878 | gold: 0, 1879 | silver: 1, 1880 | bronze: 1 1881 | }, 1882 | 'Yurisleidy Lupetey': { 1883 | gold: 0, 1884 | silver: 0, 1885 | bronze: 1 1886 | }, 1887 | 'Giulia Quintavalle': { 1888 | gold: 1, 1889 | silver: 0, 1890 | bronze: 0 1891 | }, 1892 | 'Ketleyn Quadros': { 1893 | gold: 0, 1894 | silver: 0, 1895 | bronze: 1 1896 | }, 1897 | 'Xu Yan': { 1898 | gold: 0, 1899 | silver: 0, 1900 | bronze: 1 1901 | }, 1902 | 'Kaori Matsumoto': { 1903 | gold: 1, 1904 | silver: 0, 1905 | bronze: 1 1906 | }, 1907 | 'Corina Căprioriu': { 1908 | gold: 0, 1909 | silver: 1, 1910 | bronze: 0 1911 | }, 1912 | 'Marti Malloy': { 1913 | gold: 0, 1914 | silver: 0, 1915 | bronze: 1 1916 | }, 1917 | 'Automne Pavia': { 1918 | gold: 0, 1919 | silver: 0, 1920 | bronze: 1 1921 | }, 1922 | 'Rafaela Silva': { 1923 | gold: 1, 1924 | silver: 0, 1925 | bronze: 0 1926 | }, 1927 | 'Dorjsürengiin Sumiyaa': { 1928 | gold: 0, 1929 | silver: 1, 1930 | bronze: 0 1931 | }, 1932 | 'Telma Monteiro': { 1933 | gold: 0, 1934 | silver: 0, 1935 | bronze: 1 1936 | }, 1937 | 'Nora Gjakova': { 1938 | gold: 1, 1939 | silver: 0, 1940 | bronze: 0 1941 | }, 1942 | 'Sarah-Léonie Cysique': { 1943 | gold: 0, 1944 | silver: 1, 1945 | bronze: 0 1946 | }, 1947 | 'Jessica Klimkait': { 1948 | gold: 0, 1949 | silver: 0, 1950 | bronze: 1 1951 | }, 1952 | 'Tsukasa Yoshida': { 1953 | gold: 0, 1954 | silver: 0, 1955 | bronze: 1 1956 | }, 1957 | 'Catherine Fleury': { 1958 | gold: 1, 1959 | silver: 0, 1960 | bronze: 0 1961 | }, 1962 | 'Yael Arad': { 1963 | gold: 0, 1964 | silver: 1, 1965 | bronze: 0 1966 | }, 1967 | 'Yelena Petrova': { 1968 | gold: 0, 1969 | silver: 0, 1970 | bronze: 1 1971 | }, 1972 | 'Zhang Di': { 1973 | gold: 0, 1974 | silver: 0, 1975 | bronze: 1 1976 | }, 1977 | 'Yuko Emoto': { 1978 | gold: 1, 1979 | silver: 0, 1980 | bronze: 0 1981 | }, 1982 | 'Gella Vandecaveye': { 1983 | gold: 0, 1984 | silver: 1, 1985 | bronze: 1 1986 | }, 1987 | 'Jenny Gal': { 1988 | gold: 0, 1989 | silver: 0, 1990 | bronze: 1 1991 | }, 1992 | 'Jung Sung-sook': { 1993 | gold: 0, 1994 | silver: 0, 1995 | bronze: 2 1996 | }, 1997 | 'Séverine Vandenhende': { 1998 | gold: 1, 1999 | silver: 0, 2000 | bronze: 0 2001 | }, 2002 | 'Li Shufang': { 2003 | gold: 0, 2004 | silver: 1, 2005 | bronze: 0 2006 | }, 2007 | 'Ayumi Tanimoto': { 2008 | gold: 2, 2009 | silver: 0, 2010 | bronze: 0 2011 | }, 2012 | 'Claudia Heill': { 2013 | gold: 0, 2014 | silver: 1, 2015 | bronze: 0 2016 | }, 2017 | 'Urška Žolnir': { 2018 | gold: 1, 2019 | silver: 0, 2020 | bronze: 1 2021 | }, 2022 | 'Lucie Décosse': { 2023 | gold: 1, 2024 | silver: 1, 2025 | bronze: 0 2026 | }, 2027 | 'Elisabeth Willeboordse': { 2028 | gold: 0, 2029 | silver: 0, 2030 | bronze: 1 2031 | }, 2032 | 'Won Ok-im': { 2033 | gold: 0, 2034 | silver: 0, 2035 | bronze: 1 2036 | }, 2037 | 'Xu Lili': { 2038 | gold: 0, 2039 | silver: 1, 2040 | bronze: 0 2041 | }, 2042 | 'Yoshie Ueno': { 2043 | gold: 0, 2044 | silver: 0, 2045 | bronze: 1 2046 | }, 2047 | 'Gévrise Émane': { 2048 | gold: 0, 2049 | silver: 0, 2050 | bronze: 1 2051 | }, 2052 | 'Tina Trstenjak': { 2053 | gold: 1, 2054 | silver: 1, 2055 | bronze: 0 2056 | }, 2057 | 'Clarisse Agbegnenou': { 2058 | gold: 1, 2059 | silver: 1, 2060 | bronze: 0 2061 | }, 2062 | 'Yarden Gerbi': { 2063 | gold: 0, 2064 | silver: 0, 2065 | bronze: 1 2066 | }, 2067 | 'Anicka van Emden': { 2068 | gold: 0, 2069 | silver: 0, 2070 | bronze: 1 2071 | }, 2072 | 'Maria Centracchio': { 2073 | gold: 0, 2074 | silver: 0, 2075 | bronze: 1 2076 | }, 2077 | 'Catherine Beauchemin-Pinard': { 2078 | gold: 0, 2079 | silver: 0, 2080 | bronze: 1 2081 | }, 2082 | 'Odalis Revé': { 2083 | gold: 1, 2084 | silver: 0, 2085 | bronze: 0 2086 | }, 2087 | 'Emanuela Pierantozzi': { 2088 | gold: 0, 2089 | silver: 1, 2090 | bronze: 1 2091 | }, 2092 | 'Kate Howey': { 2093 | gold: 0, 2094 | silver: 1, 2095 | bronze: 1 2096 | }, 2097 | 'Heidi Rakels': { 2098 | gold: 0, 2099 | silver: 0, 2100 | bronze: 1 2101 | }, 2102 | 'Cho Min-sun': { 2103 | gold: 1, 2104 | silver: 0, 2105 | bronze: 1 2106 | }, 2107 | 'Aneta Szczepańska': { 2108 | gold: 0, 2109 | silver: 1, 2110 | bronze: 0 2111 | }, 2112 | 'Wang Xianbo': { 2113 | gold: 0, 2114 | silver: 0, 2115 | bronze: 1 2116 | }, 2117 | 'Claudia Zwiers': { 2118 | gold: 0, 2119 | silver: 0, 2120 | bronze: 1 2121 | }, 2122 | 'Sibelis Veranes': { 2123 | gold: 1, 2124 | silver: 0, 2125 | bronze: 0 2126 | }, 2127 | 'Ylenia Scapin': { 2128 | gold: 0, 2129 | silver: 0, 2130 | bronze: 2 2131 | }, 2132 | 'Masae Ueno': { 2133 | gold: 2, 2134 | silver: 0, 2135 | bronze: 0 2136 | }, 2137 | 'Edith Bosch': { 2138 | gold: 0, 2139 | silver: 1, 2140 | bronze: 2 2141 | }, 2142 | 'Qin Dongya': { 2143 | gold: 0, 2144 | silver: 0, 2145 | bronze: 1 2146 | }, 2147 | 'Annett Böhm': { 2148 | gold: 0, 2149 | silver: 0, 2150 | bronze: 1 2151 | }, 2152 | 'Anaysi Hernández': { 2153 | gold: 0, 2154 | silver: 1, 2155 | bronze: 0 2156 | }, 2157 | 'Ronda Rousey': { 2158 | gold: 0, 2159 | silver: 0, 2160 | bronze: 1 2161 | }, 2162 | 'Kerstin Thiele': { 2163 | gold: 0, 2164 | silver: 1, 2165 | bronze: 0 2166 | }, 2167 | 'Yuri Alvear': { 2168 | gold: 0, 2169 | silver: 1, 2170 | bronze: 1 2171 | }, 2172 | 'Haruka Tachimoto': { 2173 | gold: 1, 2174 | silver: 0, 2175 | bronze: 0 2176 | }, 2177 | 'Sally Conway': { 2178 | gold: 0, 2179 | silver: 0, 2180 | bronze: 1 2181 | }, 2182 | 'Laura Vargas Koch': { 2183 | gold: 0, 2184 | silver: 0, 2185 | bronze: 1 2186 | }, 2187 | 'Chizuru Arai': { 2188 | gold: 1, 2189 | silver: 0, 2190 | bronze: 0 2191 | }, 2192 | 'Michaela Polleres': { 2193 | gold: 0, 2194 | silver: 1, 2195 | bronze: 0 2196 | }, 2197 | 'Madina Taimazova': { 2198 | gold: 0, 2199 | silver: 0, 2200 | bronze: 1 2201 | }, 2202 | 'Sanne van Dijke': { 2203 | gold: 0, 2204 | silver: 0, 2205 | bronze: 1 2206 | }, 2207 | 'Kim Mi-jung': { 2208 | gold: 1, 2209 | silver: 0, 2210 | bronze: 0 2211 | }, 2212 | 'Yoko Tanabe': { 2213 | gold: 0, 2214 | silver: 2, 2215 | bronze: 0 2216 | }, 2217 | 'Irene de Kok': { 2218 | gold: 0, 2219 | silver: 0, 2220 | bronze: 1 2221 | }, 2222 | 'Laetitia Meignan': { 2223 | gold: 0, 2224 | silver: 0, 2225 | bronze: 1 2226 | }, 2227 | 'Ulla Werbrouck': { 2228 | gold: 1, 2229 | silver: 0, 2230 | bronze: 0 2231 | }, 2232 | 'Diadenis Luna': { 2233 | gold: 0, 2234 | silver: 0, 2235 | bronze: 1 2236 | }, 2237 | 'Tang Lin': { 2238 | gold: 1, 2239 | silver: 0, 2240 | bronze: 0 2241 | }, 2242 | 'Céline Lebrun': { 2243 | gold: 0, 2244 | silver: 1, 2245 | bronze: 0 2246 | }, 2247 | 'Simona Richter': { 2248 | gold: 0, 2249 | silver: 0, 2250 | bronze: 1 2251 | }, 2252 | 'Noriko Anno': { 2253 | gold: 1, 2254 | silver: 0, 2255 | bronze: 0 2256 | }, 2257 | 'Liu Xia': { 2258 | gold: 0, 2259 | silver: 1, 2260 | bronze: 0 2261 | }, 2262 | 'Lucia Morico': { 2263 | gold: 0, 2264 | silver: 0, 2265 | bronze: 1 2266 | }, 2267 | 'Yurisel Laborde': { 2268 | gold: 0, 2269 | silver: 0, 2270 | bronze: 1 2271 | }, 2272 | 'Yang Xiuli': { 2273 | gold: 1, 2274 | silver: 0, 2275 | bronze: 0 2276 | }, 2277 | 'Yalennis Castillo': { 2278 | gold: 0, 2279 | silver: 1, 2280 | bronze: 0 2281 | }, 2282 | 'Jeong Gyeong-mi': { 2283 | gold: 0, 2284 | silver: 0, 2285 | bronze: 1 2286 | }, 2287 | 'Stéphanie Possamaï': { 2288 | gold: 0, 2289 | silver: 0, 2290 | bronze: 1 2291 | }, 2292 | 'Kayla Harrison': { 2293 | gold: 2, 2294 | silver: 0, 2295 | bronze: 0 2296 | }, 2297 | 'Gemma Gibbons': { 2298 | gold: 0, 2299 | silver: 1, 2300 | bronze: 0 2301 | }, 2302 | 'Audrey Tcheuméo': { 2303 | gold: 0, 2304 | silver: 1, 2305 | bronze: 1 2306 | }, 2307 | 'Mayra Aguiar': { 2308 | gold: 0, 2309 | silver: 0, 2310 | bronze: 3 2311 | }, 2312 | 'Anamari Velenšek': { 2313 | gold: 0, 2314 | silver: 0, 2315 | bronze: 1 2316 | }, 2317 | 'Shori Hamada': { 2318 | gold: 1, 2319 | silver: 0, 2320 | bronze: 0 2321 | }, 2322 | 'Madeleine Malonga': { 2323 | gold: 0, 2324 | silver: 1, 2325 | bronze: 0 2326 | }, 2327 | 'Anna-Maria Wagner': { 2328 | gold: 0, 2329 | silver: 0, 2330 | bronze: 1 2331 | }, 2332 | 'Zhuang Xiaoyan': { 2333 | gold: 1, 2334 | silver: 0, 2335 | bronze: 0 2336 | }, 2337 | 'Estela Rodríguez': { 2338 | gold: 0, 2339 | silver: 2, 2340 | bronze: 0 2341 | }, 2342 | 'Natalia Lupino': { 2343 | gold: 0, 2344 | silver: 0, 2345 | bronze: 1 2346 | }, 2347 | 'Yoko Sakaue': { 2348 | gold: 0, 2349 | silver: 0, 2350 | bronze: 1 2351 | }, 2352 | 'Sun Fuming': { 2353 | gold: 1, 2354 | silver: 0, 2355 | bronze: 1 2356 | }, 2357 | 'Christine Cicot': { 2358 | gold: 0, 2359 | silver: 0, 2360 | bronze: 1 2361 | }, 2362 | 'Johanna Hagn': { 2363 | gold: 0, 2364 | silver: 0, 2365 | bronze: 1 2366 | }, 2367 | 'Yuan Hua': { 2368 | gold: 1, 2369 | silver: 0, 2370 | bronze: 0 2371 | }, 2372 | 'Daima Beltrán': { 2373 | gold: 0, 2374 | silver: 2, 2375 | bronze: 0 2376 | }, 2377 | 'Kim Seon-Young': { 2378 | gold: 0, 2379 | silver: 0, 2380 | bronze: 1 2381 | }, 2382 | 'Mayumi Yamashita': { 2383 | gold: 0, 2384 | silver: 0, 2385 | bronze: 1 2386 | }, 2387 | 'Maki Tsukada': { 2388 | gold: 1, 2389 | silver: 1, 2390 | bronze: 0 2391 | }, 2392 | 'Tea Donguzashvili': { 2393 | gold: 0, 2394 | silver: 0, 2395 | bronze: 1 2396 | }, 2397 | 'Tong Wen': { 2398 | gold: 1, 2399 | silver: 0, 2400 | bronze: 1 2401 | }, 2402 | 'Lucija Polavder': { 2403 | gold: 0, 2404 | silver: 0, 2405 | bronze: 1 2406 | }, 2407 | 'Idalys Ortiz': { 2408 | gold: 1, 2409 | silver: 2, 2410 | bronze: 1 2411 | }, 2412 | 'Mika Sugimoto': { 2413 | gold: 0, 2414 | silver: 1, 2415 | bronze: 0 2416 | }, 2417 | 'Karina Bryant': { 2418 | gold: 0, 2419 | silver: 0, 2420 | bronze: 1 2421 | }, 2422 | 'Émilie Andéol': { 2423 | gold: 1, 2424 | silver: 0, 2425 | bronze: 0 2426 | }, 2427 | 'Kanae Yamabe': { 2428 | gold: 0, 2429 | silver: 0, 2430 | bronze: 1 2431 | }, 2432 | 'Yu Song': { 2433 | gold: 0, 2434 | silver: 0, 2435 | bronze: 1 2436 | }, 2437 | 'Akira Sone': { 2438 | gold: 1, 2439 | silver: 0, 2440 | bronze: 0 2441 | }, 2442 | 'Iryna Kindzerska': { 2443 | gold: 0, 2444 | silver: 0, 2445 | bronze: 1 2446 | }, 2447 | 'Romane Dicko': { 2448 | gold: 0, 2449 | silver: 0, 2450 | bronze: 1 2451 | } 2452 | } 2453 | 2454 | export default athletes 2455 | -------------------------------------------------------------------------------- /01-intro/exercises/fizzbuzz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Write a function that implements the fizzBuzz game. 4 | 5 | In fizzBuzz you need to enumerate all the numbers from 1 to `limit`. 6 | If a number is divisible by 3, rather than enumerating the number itself, you need to use 'Fizz'. 7 | If a number is divisible by 5, rather than enumerating the number itself, you need to use 'Buzz'. 8 | If a number is divisible both by 3 and 5, rather than enumerating the number itself, you need to use 'Fizz Buzz'. 9 | 10 | Your function receives the `limit` as an argument and needs to return an array with the fizz buzz sequence from 1 to `limit` (included). 11 | 12 | For instance if `limit` is `15` you should return: 13 | 14 | [ 15 | 1, 16 | 2, 17 | 'Fizz', // 3 is divisible by 3 18 | 4, 19 | 'Buzz', // 5 is divisible by 5 20 | 'Fizz', // 6 is dibisible by 3 21 | 7, 22 | 8, 23 | 'Fizz', // 9 is divisible by 3 24 | 'Buzz', // 10 is divisible by 5 25 | 11, 26 | 'Fizz', // 12 is divisible by 3 27 | 13, 28 | 14, 29 | 'Fizz Buzz' // 15 is divisible both by 3 and 5 30 | ] 31 | */ 32 | 33 | /** 34 | * @param {number} limit 35 | * @returns {[(string|number)]} 36 | */ 37 | export default function fizzBuzz (limit) { 38 | const seq = [] 39 | // Write your implementation here. Fill `seq` based on limit 40 | return seq 41 | } 42 | -------------------------------------------------------------------------------- /01-intro/exercises/fizzbuzz.solution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} limit 3 | * @returns {[(string|number)]} 4 | */ 5 | export default function fizzBuzz (limit) { 6 | const seq = [] 7 | // using a simple for loop because we don't have items to iterate on 8 | for (let i = 1; i <= limit; i++) { 9 | const divBy3 = i % 3 === 0 10 | const divBy5 = i % 5 === 0 11 | 12 | if (divBy3 && divBy5) { 13 | seq.push('Fizz Buzz') 14 | } else if (divBy3) { 15 | seq.push('Fizz') 16 | } else if (divBy5) { 17 | seq.push('Buzz') 18 | } else { 19 | seq.push(i) 20 | } 21 | } 22 | return seq 23 | } 24 | -------------------------------------------------------------------------------- /01-intro/exercises/fizzbuzz.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import fizzBuzzSolution from './fizzbuzz.solution.js' 4 | import fizzBuzzTpl from './fizzbuzz.js' 5 | 6 | const fizzBuzz = process.env.TEST_SOLUTIONS ? fizzBuzzSolution : fizzBuzzTpl 7 | 8 | tap.test('fizzBuzz(15)', async function (t) { 9 | const seq = fizzBuzz(15) 10 | t.same(seq, [ 11 | 1, 12 | 2, 13 | 'Fizz', // 3 is divisible by 3 14 | 4, 15 | 'Buzz', // 5 is divisible by 5 16 | 'Fizz', // 6 is dibisible by 3 17 | 7, 18 | 8, 19 | 'Fizz', // 9 is divisible by 3 20 | 'Buzz', // 10 is divisible by 5 21 | 11, 22 | 'Fizz', // 12 is divisible by 3 23 | 13, 24 | 14, 25 | 'Fizz Buzz' // 15 is divisible both by 3 and 5 26 | ]) 27 | }) 28 | 29 | tap.test('fizzBuzz(30)', async function (t) { 30 | const seq = fizzBuzz(30) 31 | t.same(seq, [ 32 | 1, 33 | 2, 34 | 'Fizz', // 3 is divisible by 3 35 | 4, 36 | 'Buzz', // 5 is divisible by 5 37 | 'Fizz', // 6 is dibisible by 3 38 | 7, 39 | 8, 40 | 'Fizz', // 9 is divisible by 3 41 | 'Buzz', // 10 is divisible by 5 42 | 11, 43 | 'Fizz', // 12 is divisible by 3 44 | 13, 45 | 14, 46 | 'Fizz Buzz', // 15 is divisible both by 3 and 5 47 | 16, 48 | 17, 49 | 'Fizz', // 18 is divisible by 3 50 | 19, 51 | 'Buzz', // 20 is divisible by 5 52 | 'Fizz', // 21 is divisible by 3 53 | 22, 54 | 23, 55 | 'Fizz', // 24 is divisible by 3 56 | 'Buzz', // 25 is divisible by 5 57 | 26, 58 | 'Fizz', // 27 is divisible by 3 59 | 28, 60 | 29, 61 | 'Fizz Buzz' // 30 is divisible both by 3 and 5 62 | ]) 63 | }) 64 | 65 | tap.test('fizzBuzz(1)', async function (t) { 66 | const seq = fizzBuzz(1) 67 | t.same(seq, [1]) 68 | }) 69 | 70 | tap.test('fizzBuzz(0)', async function (t) { 71 | const seq = fizzBuzz(0) 72 | t.same(seq, []) 73 | }) 74 | -------------------------------------------------------------------------------- /01-intro/for-in-debug.js: -------------------------------------------------------------------------------- 1 | const medallists = { 2 | 'Teddy Riner': 33, 3 | 'Driulis Gonzalez Morales': 16, 4 | 'Ryoko Tani': 16, 5 | 'Ilias Iliadis': 15 6 | } 7 | 8 | for (const prop in medallists) { 9 | console.log(`medallists[${prop}] = ${medallists[prop]}`) 10 | } 11 | -------------------------------------------------------------------------------- /01-intro/for-in-object.js: -------------------------------------------------------------------------------- 1 | const medallists = { 2 | 'Teddy Riner': 33, 3 | 'Driulis Gonzalez Morales': 16, 4 | 'Ryoko Tani': 16, 5 | 'Ilias Iliadis': 15 6 | } 7 | 8 | for (const judoka in medallists) { 9 | console.log(`${judoka} has won ${medallists[judoka]} medals`) 10 | } 11 | -------------------------------------------------------------------------------- /01-intro/for-in.js: -------------------------------------------------------------------------------- 1 | const judokas = [ 2 | 'Driulis Gonzalez Morales', 3 | 'Ilias Iliadis', 4 | 'Tadahiro Nomura', 5 | 'Anton Geesink', 6 | 'Teddy Riner', 7 | 'Ryoko Tani' 8 | ] 9 | 10 | // ⚠️ Careful, this can be misleading! 11 | // It's not going to print the judokas, but their index in the array! 12 | for (const judoka in judokas) { 13 | console.log(judoka) 14 | } 15 | -------------------------------------------------------------------------------- /01-intro/for-of-map.js: -------------------------------------------------------------------------------- 1 | const medallists = new Map([ 2 | ['Teddy Riner', 33], 3 | ['Driulis Gonzalez Morales', 16], 4 | ['Ryoko Tani', 16], 5 | ['Ilias Iliadis', 15] 6 | ]) 7 | 8 | for (const [judoka, medals] of medallists) { 9 | console.log(`${judoka} has won ${medals} medals`) 10 | } 11 | -------------------------------------------------------------------------------- /01-intro/for-of-object-entries.js: -------------------------------------------------------------------------------- 1 | const medallists = { 2 | 'Teddy Riner': 33, 3 | 'Driulis Gonzalez Morales': 16, 4 | 'Ryoko Tani': 16, 5 | 'Ilias Iliadis': 15 6 | } 7 | 8 | for (const [judoka, medals] of Object.entries(medallists)) { 9 | console.log(`${judoka} has won ${medals} medals`) 10 | } 11 | -------------------------------------------------------------------------------- /01-intro/for-of-object.js: -------------------------------------------------------------------------------- 1 | const medallists = { 2 | 'Teddy Riner': 33, 3 | 'Driulis Gonzalez Morales': 16, 4 | 'Ryoko Tani': 16, 5 | 'Ilias Iliadis': 15 6 | } 7 | 8 | // ⚠️ This will fail: medallist is not iterable! 9 | for (const [judoka, medals] of medallists) { 10 | console.log(`${judoka} has won ${medals} medals`) 11 | } 12 | -------------------------------------------------------------------------------- /01-intro/for-of-set.js: -------------------------------------------------------------------------------- 1 | const medals = new Set([ 2 | 'gold', 3 | 'silver', 4 | 'bronze' 5 | ]) 6 | 7 | for (const medal of medals) { 8 | console.log(medal) 9 | } 10 | -------------------------------------------------------------------------------- /01-intro/for-of-string.js: -------------------------------------------------------------------------------- 1 | const judoka = 'Ryoko Tani' 2 | 3 | for (const char of judoka) { 4 | console.log(char) 5 | } 6 | -------------------------------------------------------------------------------- /01-intro/for-of.js: -------------------------------------------------------------------------------- 1 | const judokas = [ 2 | 'Driulis Gonzalez Morales', 3 | 'Ilias Iliadis', 4 | 'Tadahiro Nomura', 5 | 'Anton Geesink', 6 | 'Teddy Riner', 7 | 'Ryoko Tani' 8 | ] 9 | 10 | for (const judoka of judokas) { 11 | console.log(judoka) 12 | } 13 | -------------------------------------------------------------------------------- /01-intro/for.js: -------------------------------------------------------------------------------- 1 | const judokas = [ 2 | 'Driulis Gonzalez Morales', 3 | 'Ilias Iliadis', 4 | 'Tadahiro Nomura', 5 | 'Anton Geesink', 6 | 'Teddy Riner', 7 | 'Ryoko Tani' 8 | ] 9 | 10 | for (let i = 0; i < judokas.length; i++) { 11 | console.log(judokas[i]) 12 | } 13 | -------------------------------------------------------------------------------- /01-intro/spread-array.js: -------------------------------------------------------------------------------- 1 | const countdown = [3, 2, 1, 0] 2 | 3 | // spread into array 4 | const from5to0 = [5, 4, ...countdown] 5 | console.log(from5to0) 6 | 7 | // [ 5, 4, 3, 2, 1, 0 ] 8 | -------------------------------------------------------------------------------- /01-intro/spread-function.js: -------------------------------------------------------------------------------- 1 | const countdown = [3, 2, 1, 0] 2 | console.log('countdown data:', ...countdown) 3 | 4 | // countdown data: 3 2 1 0 5 | -------------------------------------------------------------------------------- /01-intro/spread-object.js: -------------------------------------------------------------------------------- 1 | const judoInfo = { 2 | creator: 'Jigoro Kano', 3 | creationYear: 1882 4 | } 5 | 6 | const ranks = { 7 | belts: ['white', 'yellow', 'orange', 'green', 'blue', 'brown', 'black', 'red-white', 'red'], 8 | dan: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 9 | } 10 | 11 | const judo = { ...judoInfo, ...ranks } 12 | 13 | console.log(judo) 14 | -------------------------------------------------------------------------------- /02-generators/README.md: -------------------------------------------------------------------------------- 1 | # 02 - Generators 2 | 3 | JavaScript offers an interesting feature that can be very useful in the context of iteration: **generator functions**. 4 | 5 | We can define a generator function as a function with a super power: a generator **can be suspended** and then **resumed at a later time**. 6 | 7 | Generators are well suited to implement iterators and (little spoiler) they produce objects that implement both the **iterator** and the **iterable** protocol. 8 | 9 | ## Syntax 10 | 11 | To define a generator function we need to use the `function*` declaration (`function` keyword followed by an asterisk): 12 | 13 | ```js 14 | function * myGenerator () { 15 | // generator body 16 | } 17 | ``` 18 | 19 | Invoking a generator will not execute its body immediately. It will instead return a **generator object**. 20 | 21 | A generator object exposes the `next()` method which can be used to start or resume the execution of the generator function. 22 | 23 | A generator function can decide to suspend the execution by using the keyword `yield` inside the function body. 24 | 25 | `yield` works in a similar way to a `return` statement, meaning that it allows to return a value back to the caller. 26 | 27 | When we invoke `next()` on a generator object for the first time, it will execute its body until the very first `yield` statement. At this point the execution is supended and control is given back to the caller. The caller receives an object with the following shape: 28 | 29 | ```js 30 | { 31 | done: false, 32 | value: 'someValue' 33 | } 34 | ``` 35 | 36 | - **`done`** is a boolean value that indicates wheter the generator is exhausted (completed) or not 37 | - **`value`** represents the value _returned_ by the `yield` statement that suspended the execution. 38 | 39 | If the caller invokes `next()` again, the generator function will resume the execution. This means that the generator function keeps track of its internal state: it remembers the value of all the variables and where the execution was stopped. This allows the generator function to continue the execution from where it was left off. 40 | 41 | Let's clarify all these concepts with a simple example: 42 | 43 | ```js 44 | // fruit-generator.js 45 | function * fruitGenerator () { 46 | yield '🍑' 47 | yield '🍉' 48 | yield '🍋' 49 | yield '🥭' 50 | } 51 | 52 | const fruitGeneratorObj = fruitGenerator() 53 | console.log(fruitGeneratorObj.next()) 54 | console.log(fruitGeneratorObj.next()) 55 | console.log(fruitGeneratorObj.next()) 56 | console.log(fruitGeneratorObj.next()) 57 | console.log(fruitGeneratorObj.next()) 58 | ``` 59 | 60 | This snippet will print: 61 | 62 | ```plain 63 | { value: '🍑', done: false } 64 | { value: '🍉', done: false } 65 | { value: '🍋', done: false } 66 | { value: '🥭', done: false } 67 | { value: undefined, done: true } 68 | ``` 69 | 70 | Can you see now how the generator object can _resume_ its execution when we call `next()`? 71 | 72 | A generator can also use the `return` statement to _yield_ one last value and stop the iteration: 73 | 74 | ```js 75 | // fruit-generator-return.js 76 | function * fruitGenerator () { 77 | yield '🍑' 78 | yield '🍉' 79 | yield '🍋' 80 | return '🥭' 81 | } 82 | 83 | const fruitGeneratorObj = fruitGenerator() 84 | console.log(fruitGeneratorObj.next()) 85 | console.log(fruitGeneratorObj.next()) 86 | console.log(fruitGeneratorObj.next()) 87 | console.log(fruitGeneratorObj.next()) 88 | ``` 89 | 90 | The code above outputs: 91 | 92 | ```plain 93 | { value: '🍑', done: false } 94 | { value: '🍉', done: false } 95 | { value: '🍋', done: false } 96 | { value: '🥭', done: true } 97 | ``` 98 | 99 | Note how the latest value has already `done: true`. 100 | 101 | We said that generator objects are also iterable, this means that we can use `for ... of` with them! 102 | 103 | So we could rewrite the previous example as follow: 104 | 105 | ```js 106 | // fruit-generator-iterable.js 107 | function * fruitGenerator () { 108 | yield '🍑' 109 | yield '🍉' 110 | yield '🍋' 111 | yield '🥭' 112 | } 113 | 114 | const fruitGeneratorObj = fruitGenerator() 115 | for (const fruit of fruitGeneratorObj) { 116 | console.log(fruit) 117 | } 118 | ``` 119 | 120 | This code produces the following output: 121 | 122 | ```plain 123 | 🍑 124 | 🍉 125 | 🍋 126 | 🥭 127 | ``` 128 | 129 | With the `for ... of` syntax we don't have to worry about checking the `done` field and extracting the `value` field by ourselves. 130 | 131 | Generators support a bunch of additional features like [delegation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*), [two way data passing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next#sending_values_to_the_generator) and [error generation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw), but for the sake of this workshop we are going to focus only on the basics and learn how to use generator functions to create iterators and iterable objects. 132 | 133 | Let's see then a few interesting examples that we can build with generators. 134 | 135 | ## A `range` utility 136 | 137 | Many languageas (e.g. Python) have a utility that allows you to easily generate a range of values. 138 | 139 | This utility can be used for to simplify iteration and avoid the classic `for(var, cond, incr)` loop. 140 | 141 | For instance, assuming we had this range utility, if we wanted to print the first 10 positive numbers we could write something like this: 142 | 143 | ```js 144 | for (const i of range(0, 11)) { 145 | console.log(i) 146 | } 147 | ``` 148 | 149 | This would be pretty much equivalent to: 150 | 151 | ```js 152 | for (let i = 0; i < 11; i++) { 153 | console.log(i) 154 | } 155 | ``` 156 | 157 | Which one do you think reads better? 158 | 159 | I vote for the `range` one. So let's see how we could implement that with generator functions: 160 | 161 | ```js 162 | // range.js 163 | function * range (start, end) { 164 | for (let i = start; i < end; i++) { 165 | yield i 166 | } 167 | } 168 | ``` 169 | 170 | The interesting part is that we are effectively abstracting a _classic_ `for` loop and leveraging the resumability of generators to yield a value per every iteration. 171 | 172 | 173 | > **🎭 PLAY** 174 | > Can you modify this `range` function to support an arbitrary step (e.g. increment by 2 rather than by 1)? 175 | 176 | 177 | > **🎭 PLAY** 178 | > Can you also modify this `range` function to work with decreasing values (e.g. go from 10 to 0)? 179 | 180 | 181 | ## A `cycle` utility 182 | 183 | Another classic iterator utility available in other languages (hello again, Python 🐍) is the `cycle` utility. 184 | 185 | It allows you to pass a sequence of values and it will yield one of these value per every iteration. Once the values are finished it will restart from the beginning. When is this going to finish? Never! 186 | 187 | Yes, generators are **lazy**, meaning that they produce (_yield_) values on demand and they can also produce and **endless** sequence of values. 188 | 189 | Let's see how we could implement this cycle utility: 190 | 191 | ```js 192 | // cycle.js 193 | function * cycle (values) { 194 | let current = 0 195 | while (true) { 196 | yield values[current] 197 | current = (current + 1) % values.length 198 | } 199 | } 200 | ``` 201 | 202 | See that `while (true)`? That should immediately give you the idea that this iterator could go on forever! 203 | 204 | So what happens if we use `cycle` with `for ... of`? Let's find out: 205 | 206 | ```js 207 | for (const value of cycle(['even', 'odd'])) { 208 | console.log(value) 209 | } 210 | ``` 211 | 212 | This will print: 213 | 214 | ```plain 215 | even 216 | odd 217 | even 218 | odd 219 | even 220 | odd 221 | even 222 | odd 223 | even 224 | ... 225 | ``` 226 | 227 | and, yes... it will never finish! 228 | 229 | 230 | ## Exercises 231 | 232 | But how to deal with endless iterators and avoid endless loops? One way is to avoid `for ... of` and use `next()` explicitly. 233 | 234 | Another approach might be to create another utility function called `take(n, iterable)` that gives us another iterable that will stop when the iterable is completed or when `n` items have been yielded. 235 | 236 | Let's implement `take` as an exercise: 237 | 238 | > **🏹 Exercise** ([take.js](/02-generators/exercises/take.js)) 239 | > 240 | > Let's implement the `take(n, iterable)` utility: 241 | > 242 | > A skeleton of the file is available at `02-generators/exercises/take.js`. 243 | > 244 | > You can edit the file and run an interactive test session to validate your implementation with: 245 | > 246 | > ```bash 247 | > npm run ex -- 02-generators/exercises/take.test.js 248 | > ``` 249 | > 250 | > If you really struggle with this, you can have a look at [`take.solution.js`](/02-generators/exercises/take.solution.js) for a possible solution. 251 | 252 | Ok, if you want to push yourself a bit more, let's try another one! 253 | 254 | > **🏹 Exercise** ([zip.js](/02-generators/exercises/zip.js)) 255 | > 256 | > Let's implement the `zip(iterable1, iterable2)` utility: 257 | > 258 | > A skeleton of the file is available at `02-generators/exercises/zip.js`. 259 | > 260 | > You can edit the file and run an interactive test session to validate your implementation with: 261 | > 262 | > ```bash 263 | > npm run ex -- 02-generators/exercises/zip.test.js 264 | > ``` 265 | > 266 | > If you really struggle with this, you can have a look at [`zip.solution.js`](/02-generators/exercises/zip.solution.js) for a possible solution. 267 | 268 | > **🎭 PLAY** 269 | > Did you manage to implement `zip`? Well done you! Now can you implement a more generic version of `zip` that accepts an arbitrary number of iterables as arguments? If you pass N iterables it should create a new iterable that yields an array of N items (1 from every iterable). 270 | 271 | 272 | ## Summary 273 | 274 | - Generator functions are a special type of function. 275 | - A generator function returns a **generator object** which is both an **iterator** and an **iterable**. 276 | - A generator function uses `yield` to _yield_ a value and pause its execution. The generator object is used to make progress on an instance of the generator (by calling `next()`) 277 | - Generator functions are a great way to create custom iterable objects. 278 | - Generator objects are **lazy** and they can be **endless**. 279 | 280 | 281 | That's all for now, congratulations on finishing the second chapter! 🎉 282 | 283 | Take a little break and get ready to move to the [Next section](/03-iterator-protocol/README.md). 284 | 285 | --- 286 | 287 | | [⬅️ 01 - Introduction](/01-intro/README.md) | [🏠](/README.md)| [03 - Iterator protocol ➡️](/03-iterator-protocol/README.md)| 288 | |:--------------|:------:|------------------------------------------------:| 289 | -------------------------------------------------------------------------------- /02-generators/cycle.js: -------------------------------------------------------------------------------- 1 | function * cycle (values) { 2 | let current = 0 3 | while (true) { 4 | yield values[current] 5 | current = (current + 1) % values.length 6 | } 7 | } 8 | 9 | for (const value of cycle(['even', 'odd'])) { 10 | console.log(value) 11 | } 12 | -------------------------------------------------------------------------------- /02-generators/exercises/take.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's implement a `take(n, iterable)` utility. 4 | 5 | This function receives a positive integer and an iterable. 6 | 7 | It returns a new iterable that will stop when the source iterable is 8 | completed or when `n` items have been already yielded. 9 | 10 | For example: 11 | 12 | ```js 13 | for (const item of take(4, cycle(['even', 'odd']))) { 14 | console.log(item) 15 | } 16 | ``` 17 | 18 | Should print: 19 | 20 | ```plain 21 | even 22 | odd 23 | even 24 | odd 25 | ``` 26 | 27 | Run the tests with: 28 | 29 | > npm run ex -- 02-generators/exercises/take.test.js 30 | */ 31 | 32 | /** 33 | * @template T 34 | * @param {number} n 35 | * @param {Iterable.} iterable 36 | * @returns {Iterable.} 37 | */ 38 | export default function * take (n, iterable) { 39 | // Note that this is already a generator function, 40 | // so it already returns an Iterable. You "just" need to use 41 | // `yield` correctly... 42 | // 43 | // Write your code here! 44 | } 45 | -------------------------------------------------------------------------------- /02-generators/exercises/take.solution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @param {number} n 4 | * @param {Iterable.} iterable 5 | * @returns {Iterable.} 6 | */ 7 | export default function * take (n, iterable) { 8 | let yielded = 0 9 | for (const value of iterable) { 10 | if (yielded >= n) { break } 11 | yield value 12 | yielded += 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /02-generators/exercises/take.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import takeSolution from './take.solution.js' 4 | import takeTpl from './take.js' 5 | 6 | const take = process.env.TEST_SOLUTIONS ? takeSolution : takeTpl 7 | 8 | /** 9 | * @template T 10 | * @param {[T]} values 11 | * @returns {Iterable.} 12 | */ 13 | function * cycle (values) { 14 | let current = 0 15 | while (true) { 16 | yield values[current % values.length] 17 | current += 1 18 | } 19 | } 20 | 21 | tap.test('take(4, cycle([\'even\', \'odd\']))', async function (t) { 22 | const seq = [...take(4, cycle(['even', 'odd']))] 23 | t.same(seq, [ 24 | 'even', 25 | 'odd', 26 | 'even', 27 | 'odd' 28 | ]) 29 | }) 30 | 31 | tap.test('take(1, cycle([\'even\', \'odd\']))', async function (t) { 32 | const seq = [...take(1, cycle(['even', 'odd']))] 33 | t.same(seq, [ 34 | 'even' 35 | ]) 36 | }) 37 | 38 | tap.test('take(0, [])', async function (t) { 39 | const seq = take(0, []) 40 | t.same(seq, []) 41 | }) 42 | -------------------------------------------------------------------------------- /02-generators/exercises/zip.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's implement a `zip(iterable1, iterable2)` utility. 4 | 5 | This function receives two iterable objects. 6 | 7 | It returns a new iterable that will stop when one of source iterable is 8 | exhausted and it yields a pair of items respectively from iterable1 and iterable2. 9 | 10 | For example: 11 | 12 | ```js 13 | for (const item of zip(range(0,10), cycle(['even', 'odd']))) { 14 | console.log(item) 15 | } 16 | ``` 17 | 18 | Should print: 19 | 20 | ```plain 21 | [0, 'even'], 22 | [1, 'odd'], 23 | [2, 'even'], 24 | [3, 'odd'], 25 | [4, 'even'], 26 | [5, 'odd'], 27 | [6, 'even'], 28 | [7, 'odd'], 29 | [8, 'even'], 30 | [9, 'odd'] 31 | ``` 32 | 33 | Run the tests with: 34 | 35 | > npm run ex -- 02-generators/exercises/zip.test.js 36 | */ 37 | 38 | /** 39 | * @template T 40 | * @template S 41 | * @param {Iterable.} iterable1 42 | * @param {Iterable.} iterable2 43 | * @returns {Iterable.<[T,S]}>} 44 | */ 45 | export default function * zip (iterable1, iterable2) { 46 | // write your code here 47 | // Tip: if you want to support generic iterable objects (like arrays) 48 | // you can get an iterator from them with `obj[Symbol.iterator]()`. 49 | } 50 | -------------------------------------------------------------------------------- /02-generators/exercises/zip.solution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @template S 4 | * @param {Iterable.} iterable1 5 | * @param {Iterable.} iterable2 6 | * @returns {Iterable.<[T,S]}>} 7 | */ 8 | export default function * zip (iterable1, iterable2) { 9 | // Supports arbitraty iterable objects like arrays, maps or sets 10 | const [it1, it2] = [iterable1[Symbol.iterator](), iterable2[Symbol.iterator]()] 11 | while (true) { 12 | const [next1, next2] = [it1.next(), it2.next()] 13 | if (next1.done || next2.done) { break } 14 | yield [next1.value, next2.value] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /02-generators/exercises/zip.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import zipSolution from './zip.solution.js' 4 | import zipTpl from './zip.js' 5 | 6 | const zip = process.env.TEST_SOLUTIONS ? zipSolution : zipTpl 7 | 8 | /** 9 | * @template T 10 | * @param {[T]} values 11 | * @returns {Iterable.} 12 | */ 13 | function * cycle (values) { 14 | let current = 0 15 | while (true) { 16 | yield values[current % values.length] 17 | current += 1 18 | } 19 | } 20 | 21 | /** 22 | * @param {number} start 23 | * @param {number} end 24 | * @returns {Iterable.} 25 | */ 26 | function * range (start, end) { 27 | for (let i = start; i < end; i++) { 28 | yield i 29 | } 30 | } 31 | 32 | tap.test('zip(range(0,10), cycle([\'even\', \'odd\']))', async function (t) { 33 | const seq = [...zip(range(0, 10), cycle(['even', 'odd']))] 34 | t.same(seq, [ 35 | [0, 'even'], 36 | [1, 'odd'], 37 | [2, 'even'], 38 | [3, 'odd'], 39 | [4, 'even'], 40 | [5, 'odd'], 41 | [6, 'even'], 42 | [7, 'odd'], 43 | [8, 'even'], 44 | [9, 'odd'] 45 | ]) 46 | }) 47 | 48 | tap.test('zip([\'a\', \'b\', \'c\', \'d\'], [1, 2, 3]))', async function (t) { 49 | const seq = [...zip(['a', 'b', 'c'], [1, 2, 3])] 50 | t.same(seq, [ 51 | ['a', 1], 52 | ['b', 2], 53 | ['c', 3] 54 | ]) 55 | }) 56 | 57 | tap.test('zip([], []))', async function (t) { 58 | const seq = [...zip([], [])] 59 | t.same(seq, []) 60 | }) 61 | 62 | /* 63 | Uncomment the following test for testing the advanced version of zip function 64 | */ 65 | /* 66 | tap.test('zip(range(0,10), cycle([\'even\', \'odd\']), [\'zero\', \'one\', \'two\'])', async function (t) { 67 | const seq = [...zip(range(0, 10), cycle(['even', 'odd']), ['zero', 'one', 'two'])] 68 | t.same(seq, [ 69 | [0, 'even', 'zero'], 70 | [1, 'odd', 'one'], 71 | [2, 'even', 'two'] 72 | ]) 73 | }) 74 | */ 75 | -------------------------------------------------------------------------------- /02-generators/fruit-generator-iterable.js: -------------------------------------------------------------------------------- 1 | function * fruitGenerator () { 2 | yield '🍑' 3 | yield '🍉' 4 | yield '🍋' 5 | yield '🥭' 6 | } 7 | 8 | const fruitGeneratorObj = fruitGenerator() 9 | for (const fruit of fruitGeneratorObj) { 10 | console.log(fruit) 11 | } 12 | -------------------------------------------------------------------------------- /02-generators/fruit-generator-return.js: -------------------------------------------------------------------------------- 1 | function * fruitGenerator () { 2 | yield '🍑' 3 | yield '🍉' 4 | yield '🍋' 5 | return '🥭' 6 | } 7 | 8 | const fruitGeneratorObj = fruitGenerator() 9 | console.log(fruitGeneratorObj.next()) // { value: '🍑', done: false } 10 | console.log(fruitGeneratorObj.next()) // { value: '🍉', done: false } 11 | console.log(fruitGeneratorObj.next()) // { value: '🍋', done: false } 12 | console.log(fruitGeneratorObj.next()) // { value: '🥭', done: true } 13 | -------------------------------------------------------------------------------- /02-generators/fruit-generator.js: -------------------------------------------------------------------------------- 1 | function * fruitGenerator () { 2 | yield '🍑' 3 | yield '🍉' 4 | yield '🍋' 5 | yield '🥭' 6 | } 7 | 8 | const fruitGeneratorObj = fruitGenerator() 9 | console.log(fruitGeneratorObj.next()) // { value: '🍑', done: false } 10 | console.log(fruitGeneratorObj.next()) // { value: '🍉', done: false } 11 | console.log(fruitGeneratorObj.next()) // { value: '🍋', done: false } 12 | console.log(fruitGeneratorObj.next()) // { value: '🥭', done: false } 13 | console.log(fruitGeneratorObj.next()) // { value: undefined, done: true } 14 | -------------------------------------------------------------------------------- /02-generators/range.js: -------------------------------------------------------------------------------- 1 | function * range (start, end) { 2 | for (let i = start; i < end; i++) { 3 | yield i 4 | } 5 | } 6 | 7 | for (const i of range(0, 11)) { 8 | console.log(i) 9 | } 10 | -------------------------------------------------------------------------------- /03-iterator-protocol/README.md: -------------------------------------------------------------------------------- 1 | # 03 - Iterator Protocol 2 | 3 | Before continuing let's try to distinguish between **iterator** and **iterable**. Those two concepts have very similar names and sometimes they are used interchangeably. 4 | 5 | In JavaScript is important to understand the difference to appreciate the iteration protocols we are about to discuss. 6 | 7 | ## Iterator 8 | 9 | ![Iterator](./images/iterator.png) 10 | 11 | An **iterator** is an object that acts like a cursor to iterate over blocks of data sequentially. 12 | 13 | The iterator itself doesn't know much about the collection, it just knows how to move to the next element. 14 | 15 | 16 | ## Iterable 17 | 18 | ![Iterable](./images/iterable.png) 19 | 20 | An **iterable** is an object that contains data that can be iterated over sequentially. 21 | 22 | It's a higher level concept. You can consider an iterable as a generic collection of items. If you want to go through all the items in order, **the iterable can give you an iterator** to do that. 23 | 24 | If you want a simple and easy definition, an iterable is a collection, an iterator is a cursor over that collection. 25 | 26 | 27 | ## Iterator protocol in JavaScript 28 | 29 | In JavaScript, an object is considered **an iterator** if it has a `next()` method. Every time you call it, it returns an object with the keys `done` (boolean) and `value`. 30 | 31 | What are the `done` and `value` keys for? 32 | 33 | One way we could think about them is that they help answering the following questions: 34 | 35 | - Is there another value left (is it `done` or not)? 36 | - If there is a `value`, what it is? 37 | 38 | Sounds familiar, right? 39 | 40 | > **Note**: this protocol is implicit, in fact, we don't have to explicitly tell JavaScript that the object implements some sort of iterator interface. This is an application of **duck typing** ([_"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck"_](https://en.wikipedia.org/wiki/Duck_test) 🦆). This means that if an iterator behaves like an iterator than it can be considered an iterator. 41 | 42 | Ok, let's see an example. Let's say we want to implement a generic _countdown_ utility that gives us all the numbers from a positive `n` to 0: 43 | 44 | ```js 45 | // countdown.js 46 | function createCountdown (start) { 47 | let nextVal = start 48 | return { 49 | next () { 50 | if (nextVal < 0) { 51 | return { done: true } 52 | } 53 | return { 54 | done: false, 55 | value: nextVal-- 56 | } 57 | } 58 | } 59 | } 60 | 61 | const countdown = createCountdown(3) 62 | console.log(countdown.next()) 63 | console.log(countdown.next()) 64 | console.log(countdown.next()) 65 | console.log(countdown.next()) 66 | console.log(countdown.next()) 67 | ``` 68 | 69 | This example will print: 70 | 71 | ```plain 72 | { done: false, value: 3 } 73 | { done: false, value: 2 } 74 | { done: false, value: 1 } 75 | { done: false, value: 0 } 76 | { done: true } 77 | ``` 78 | 79 | We have basically implemented a countdown from 3 to 0. 80 | 81 | Note how `createCountdown` is just a **factory function**, it's not the iterator, but it creates one. In this implementation the iterator is just an **anonymous object**, but if we wanted we could have implemented the iterator using a **class**: 82 | 83 | ```js 84 | // countdown-class.js 85 | class Countdown { 86 | constructor (start) { 87 | this.nextVal = start 88 | } 89 | 90 | next () { 91 | if (this.nextVal < 0) { 92 | return { done: true } 93 | } 94 | return { 95 | done: false, 96 | value: this.nextVal-- 97 | } 98 | } 99 | } 100 | 101 | const countdown = new Countdown(3) 102 | console.log(countdown.next()) 103 | console.log(countdown.next()) 104 | console.log(countdown.next()) 105 | console.log(countdown.next()) 106 | console.log(countdown.next()) 107 | ``` 108 | 109 | This example is perfectly equivalent to the previous one. The important part is that our iterator object exposes a `next()` method that conforms with the **iterator protocol**. 110 | 111 | 112 | ## Iterators with generators 113 | 114 | Do you remember that we said that a generator object (an object returned by a generator function) is also an iterator? You do, right?! 😇 115 | 116 | Ok... so that means that we could reimplement our countdown example using a generator function! 117 | 118 | Let's see how that looks like: 119 | 120 | ```js 121 | // countdown-generator.js 122 | function * countdownGen (start) { 123 | for (let i = start; i >= 0; i--) { 124 | yield i 125 | } 126 | } 127 | 128 | const countdown = countdownGen(3) 129 | console.log(countdown.next()) 130 | console.log(countdown.next()) 131 | console.log(countdown.next()) 132 | console.log(countdown.next()) 133 | console.log(countdown.next()) 134 | ``` 135 | 136 | If you run this code, you'll see the same result as before. 137 | 138 | Note how with generators we don't have to think about keeping track of the state by ourselves (we are not storing `nextVal`). We can simply use a _classic_ `for` loop and rely on the _resumability_ of generators. 139 | 140 | Now, I hope you are appreciating all the time we spent talking about generators! Maybe you are also realising that this countdown example isn't that different from our previous `range` example! 😜 141 | 142 | 143 | ## Exercises 144 | 145 | That's all we have to know about iterators and the iterator protocol. Let's exercise a bit 💪. 146 | 147 | > **🏹 Exercise** ([emoji.js](/03-iterator-protocol/exercises/emoji.js)) 148 | > 149 | > Let's implement an iterator for the emojis in a text. 150 | > 151 | > A skeleton of the file is available at `03-iterator-protocol/exercises/emoji.js`. 152 | > 153 | > You can edit the file and run an interactive test session to validate your implementation with: 154 | > 155 | > ```bash 156 | > npm run ex -- 03-iterator-protocol/exercises/emoji.test.js 157 | > ``` 158 | > 159 | > If you really struggle with this, you can have a look at [`emoji.solution.js`](/03-iterator-protocol/exercises/emoji.solution.js) for a possible solution. 160 | 161 | 162 | ## Summary 163 | 164 | - An **iterator** is an object that allows us to traverse a collection 165 | - The **iterator protocol** specifies that an object is an iterator if it has a `next()` method that returns an object with the shape `{done, value}`. `done` (a boolean) tells us if the iteration is completed, `value` represents the value from the current iteration. 166 | - You can write an iterator as an anonymous object (e.g. returned by a factory function), using classes or using generators. 167 | 168 | 169 | That's all for now, congratulations on finishing the third chapter! 🎉 170 | 171 | Take a little break and get ready to move to the [Next section](/04-iterable-protocol/README.md). 172 | 173 | --- 174 | 175 | 176 | | [⬅️ 02 - Generators](/02-generators/README.md) | [🏠](/README.md)| [04 - Iterable protocol ➡️](/04-iterable-protocol/README.md)| 177 | |:--------------|:------:|------------------------------------------------:| 178 | -------------------------------------------------------------------------------- /03-iterator-protocol/countdown-class.js: -------------------------------------------------------------------------------- 1 | class Countdown { 2 | constructor (start) { 3 | this.nextVal = start 4 | } 5 | 6 | next () { 7 | if (this.nextVal < 0) { 8 | return { done: true } 9 | } 10 | return { 11 | done: false, 12 | value: this.nextVal-- 13 | } 14 | } 15 | } 16 | 17 | const countdown = new Countdown(3) 18 | console.log(countdown.next()) // { done: false, value: 3 } 19 | console.log(countdown.next()) // { done: false, value: 2 } 20 | console.log(countdown.next()) // { done: false, value: 1 } 21 | console.log(countdown.next()) // { done: false, value: 0 } 22 | console.log(countdown.next()) // { done: true } 23 | -------------------------------------------------------------------------------- /03-iterator-protocol/countdown-generator.js: -------------------------------------------------------------------------------- 1 | function * countdownGen (start) { 2 | for (let i = start; i >= 0; i--) { 3 | yield i 4 | } 5 | } 6 | 7 | const countdown = countdownGen(3) 8 | console.log(countdown.next()) // { done: false, value: 3 } 9 | console.log(countdown.next()) // { done: false, value: 2 } 10 | console.log(countdown.next()) // { done: false, value: 1 } 11 | console.log(countdown.next()) // { done: false, value: 0 } 12 | console.log(countdown.next()) // { done: true, value: undefined } 13 | -------------------------------------------------------------------------------- /03-iterator-protocol/countdown.js: -------------------------------------------------------------------------------- 1 | function createCountdown (start) { 2 | let nextVal = start 3 | return { 4 | next () { 5 | if (nextVal < 0) { 6 | return { done: true } 7 | } 8 | return { 9 | done: false, 10 | value: nextVal-- 11 | } 12 | } 13 | } 14 | } 15 | 16 | const countdown = createCountdown(3) 17 | console.log(countdown.next()) // { done: false, value: 3 } 18 | console.log(countdown.next()) // { done: false, value: 2 } 19 | console.log(countdown.next()) // { done: false, value: 1 } 20 | console.log(countdown.next()) // { done: false, value: 0 } 21 | console.log(countdown.next()) // { done: true } 22 | -------------------------------------------------------------------------------- /03-iterator-protocol/exercises/emoji.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's implement an iterator over all the emojis of a given text. 4 | 5 | This iterator should behave as follows: 6 | 7 | - If you have the `text`: "Hello 👋 World 🌎" 8 | - The first time you call `iter.next()` you should get: `{done: false, value: '👋'}` 9 | - The second time you call `iter.next()` you should get: `{done: false, value: '🌎'}` 10 | - The third time you call `iter.next()` you should get: `{done: true, value: undefined}` 11 | 12 | Try to implement this iterator using 3 different styles: 13 | 14 | - With a factory function 15 | - With a class 16 | - With a generator 17 | 18 | TIPS: 19 | 20 | - You can convert a string into an array of unicode characters with `Array.from(str)` 21 | - If you use a `for ... of` over a string, every element will be a unicode character 22 | - A simple way to test if a given unicode character is an emoji is: `char.match(/\p{Emoji}/u) !== null` 23 | 24 | Run the tests with: 25 | 26 | > npm run ex -- 03-iterator-protocol/exercises/emoji.test.js 27 | */ 28 | 29 | export function createEmojiIter (text) { 30 | // write your code here 31 | } 32 | 33 | export class EmojiIter { 34 | constructor (text) { 35 | this.chars = Array.from(text) 36 | // write your code here 37 | } 38 | 39 | next () { 40 | // write your code here 41 | } 42 | } 43 | 44 | export function * emojiIterGen (text) { 45 | // write your code here 46 | } 47 | -------------------------------------------------------------------------------- /03-iterator-protocol/exercises/emoji.solution.js: -------------------------------------------------------------------------------- 1 | export function createEmojiIter (text) { 2 | let index = 0 3 | const chars = Array.from(text) 4 | 5 | return { 6 | next () { 7 | while (index < chars.length) { 8 | const char = chars[index++] 9 | if (char.match(/\p{Emoji}/u) !== null) { 10 | return { done: false, value: char } 11 | } 12 | } 13 | 14 | return { done: true, value: undefined } 15 | } 16 | } 17 | } 18 | 19 | export class EmojiIter { 20 | constructor (text) { 21 | this.chars = Array.from(text) 22 | this.index = 0 23 | } 24 | 25 | next () { 26 | while (this.index < this.chars.length) { 27 | const char = this.chars[this.index++] 28 | if (char.match(/\p{Emoji}/u) !== null) { 29 | return { done: false, value: char } 30 | } 31 | } 32 | 33 | return { done: true, value: undefined } 34 | } 35 | } 36 | 37 | export function * emojiIterGen (text) { 38 | for (const char of text) { 39 | if (char.match(/\p{Emoji}/u) !== null) { 40 | yield char 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /03-iterator-protocol/exercises/emoji.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import * as emojiSolution from './emoji.solution.js' 4 | import * as emojiTpl from './emoji.js' 5 | 6 | const solution = process.env.TEST_SOLUTIONS ? emojiSolution : emojiTpl 7 | 8 | const text = 'the quick ⚡ brown 🟫 fox 🦊 jumps over 🔼 the lazy 💤 dog 🐶' 9 | const expected = ['⚡', '🟫', '🦊', '🔼', '💤', '🐶'] 10 | 11 | function collect (iter) { 12 | const data = [] 13 | while (true) { 14 | const res = iter.next() 15 | if (typeof res.value !== 'undefined') { 16 | data.push(res.value) 17 | } 18 | if (res.done) { 19 | break 20 | } 21 | } 22 | return data 23 | } 24 | 25 | tap.test('createEmojiIter', async function (t) { 26 | const iter = solution.createEmojiIter(text) 27 | const emojis = collect(iter) 28 | t.same(emojis, expected) 29 | }) 30 | 31 | tap.test('EmojiIter', async function (t) { 32 | const iter = new solution.EmojiIter(text) 33 | const emojis = collect(iter) 34 | t.same(emojis, expected) 35 | }) 36 | 37 | tap.test('emojiIterGen', async function (t) { 38 | const iter = solution.emojiIterGen(text) 39 | const emojis = collect(iter) 40 | t.same(emojis, expected) 41 | }) 42 | -------------------------------------------------------------------------------- /03-iterator-protocol/images/iterable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/iteration-protocols-workshop/e0a53bcea2f5bcc587a4543c27e25eafa7b39f4f/03-iterator-protocol/images/iterable.png -------------------------------------------------------------------------------- /03-iterator-protocol/images/iterator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/iteration-protocols-workshop/e0a53bcea2f5bcc587a4543c27e25eafa7b39f4f/03-iterator-protocol/images/iterator.png -------------------------------------------------------------------------------- /04-iterable-protocol/README.md: -------------------------------------------------------------------------------- 1 | # 04 - Iterable Protocol 2 | 3 | In the previous chapter we said that an **iterable** is an object that abstracts a collection of items that we can iterate on sequentially. 4 | 5 | Before getting into the **iterable protocol**, let review some interesting use cases where we can use iterable objects in JavaScript. 6 | 7 | We already mentioned the `for ... of` loop and the Spread operator. But there's more: 8 | 9 | - Destructuring via an Array pattern (e.g. `const [head, ...tail] = iterable`) 10 | - `Array.from()` 11 | - Constructors of `Map`, `WeakMap`, `Set` and `WeakSet` 12 | - An iterable where the items are promises can be passed to `Promise.all()` or `Promise.race()` 13 | 14 | I hope this is enough for you to be excited about the prospect of learning more about iterables! 😛 15 | 16 | 17 | ## The iterable protocol 18 | 19 | In JavaScript an object is **iterable** if it has a special **property called `Symbol.iterator`**. This property needs to be a zero-argument function that **returns an iterator**. 20 | 21 | We already know how to create an iterator, so to make an object iterable we just need to return an iterator from its `Symbol.iterator` function. 22 | 23 | Let's see how that looks like for our countdown example: 24 | 25 | ```js 26 | // countdown.js 27 | function createCountdown (start) { 28 | let nextVal = start 29 | return { 30 | [Symbol.iterator]: () => ({ 31 | next () { 32 | if (nextVal < 0) { 33 | return { done: true } 34 | } 35 | return { 36 | done: false, 37 | value: nextVal-- 38 | } 39 | } 40 | }) 41 | } 42 | } 43 | ``` 44 | 45 | Note, how the code of the `Symbol.iterator` function is pretty much the same as our iterator example from the previous chapter... 46 | 47 | Because now `createCountdown` produces an iterable, that means that we can do cool things like using `for ... of` loops with it: 48 | 49 | ```js 50 | const countdown = createCountdown(3) 51 | for (const value of countdown) { 52 | console.log(value) 53 | } 54 | ``` 55 | 56 | As you might expect, this prints: 57 | 58 | ```plain 59 | 3 60 | 2 61 | 1 62 | 0 63 | ``` 64 | 65 | In this example we used a factory function to create an anonymous object that is an iterable. As we did for the iterator example, also for an iterable we could use a class based approach or generator functions. 66 | 67 | Let's see how to rewrite our iterable countdown using a class: 68 | 69 | ```js 70 | // countdown-class.js 71 | class Countdown { 72 | constructor (start) { 73 | this.nextVal = start 74 | } 75 | 76 | [Symbol.iterator] () { 77 | let nextVal = this.nextVal 78 | return ({ 79 | next () { 80 | if (nextVal < 0) { 81 | return { done: true } 82 | } 83 | return { 84 | done: false, 85 | value: nextVal-- 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | ``` 92 | 93 | As per the protocol, everytime we call `Symbol.iterator` we get back an iterator object. 94 | 95 | One important note here is that every time we call this method we get a **new** iterator object. This means that we can instantiate the iterable once and do as many `for ... of` as we want and everytime we will restart the countdown from the beginning: 96 | 97 | ```js 98 | const countdown = new Countdown(3) 99 | 100 | for (const value of countdown) { 101 | console.log(value) 102 | } 103 | 104 | console.log('\n--- trying again ---\n') 105 | 106 | for (const value of countdown) { 107 | console.log(value) 108 | } 109 | ``` 110 | 111 | This produces the following output: 112 | 113 | ```plain 114 | 3 115 | 2 116 | 1 117 | 0 118 | 119 | --- trying again --- 120 | 121 | 3 122 | 2 123 | 1 124 | 0 125 | ``` 126 | 127 | > **🎭 PLAY** 128 | > What do you think is going to happen if we do this with the factory function implementation? Why don't you try? 129 | 130 | > **Note**: Making iterable objects resumable is a design choice that needs to be considered case by case. Sometimes it can make sense (like with out countdown example), sometimes it doesn't. Sometimes it is not even possible. 131 | > 132 | > If you are using third party iterables, make no assumption and check their documentation. Or, if you really want to try to make an assumption, stay on the safe side and assume that the given iterable is not resumable. 133 | 134 | Let's now see how to implement our countdown iterable using generator functions: 135 | 136 | ```js 137 | // countdown-generator.js 138 | function * countdownGen (start) { 139 | for (let i = start; i >= 0; i--) { 140 | yield i 141 | } 142 | } 143 | ``` 144 | 145 | Wait, isn't this exactly the same code as per our iterator example from the previous chapter? 😨 146 | 147 | How is it possible that the same code is both an iterator and an iterable? 148 | 149 | Let's find out! 👇 150 | 151 | 152 | ## Iterator and Iterable protocols together! 🤝 153 | 154 | I don't know if you realised already, but the iterator and the iterable protocol are not mutually exclusive: we can create objects that implement both! 155 | 156 | Let's see a very simple example, an iterator (and iterable) that always yields the string `"hello"`: 157 | 158 | ```js 159 | // hello-iterator-iterable.js 160 | const iterableIterator = { 161 | next () { 162 | return { done: false, value: 'hello' } 163 | }, 164 | [Symbol.iterator] () { 165 | return this 166 | } 167 | } 168 | ``` 169 | 170 | Ok, `iterableIterator` implements the iterator protocol, in fact it has a `next()` method that always returns `{ done: false, value: 'hello' }`! 171 | 172 | But it also has a `Symbol.iterator` method which returns... well itself, an iterator! So it's also an iterable! 173 | 174 | If we call `next()` we will get the following result: 175 | 176 | ```js 177 | iterableIterator.next() 178 | // { done: false, value: "hello" } 179 | ``` 180 | 181 | If we do a `for ... of`, instead: 182 | 183 | ```js 184 | for (const value of iterableIterator) { 185 | console.log(value) 186 | } 187 | 188 | // hello 189 | // hello 190 | // hello 191 | // ... 192 | ``` 193 | 194 | This is exactly the kind of objects that generator functions return and that's why we can use `next()` and `for ... of` on them! 195 | 196 | Can we make our countdown example an iterator that is also an iterable without using generators? Let's try: 197 | 198 | ```js 199 | // countdown-iterator-iterable.js 200 | class Countdown { 201 | constructor (start) { 202 | this.nextVal = start 203 | } 204 | 205 | // iterator protocol 206 | next () { 207 | if (this.nextVal < 0) { 208 | return { done: true } 209 | } 210 | return { 211 | done: false, 212 | value: this.nextVal-- 213 | } 214 | } 215 | 216 | // iterable protocol 217 | [Symbol.iterator] () { 218 | return this 219 | } 220 | } 221 | ``` 222 | 223 | Note that this implementation is not _resumable_, so once the iterator is exhausted you cannot reset it, you'll need to create a new `Countdown` object if you want to iterate again. 224 | 225 | 226 | ## Exercises 227 | 228 | Time to put into practice what we learned in this chapter! 229 | 230 | > **🏹 Exercise** ([emoji.js](/04-iterable-protocol/exercises/emoji.js)) 231 | > 232 | > Let's re-implement the emoji iterator from the previous chapter but this time let's make iterable. 233 | > 234 | > A skeleton of the file is available at `04-iterable-protocol/exercises/emoji.js`. 235 | > 236 | > You can edit the file and run an interactive test session to validate your implementation with: 237 | > 238 | > ```bash 239 | > npm run ex -- 04-iterable-protocol/exercises/emoji.test.js 240 | > ``` 241 | > 242 | > If you really struggle with this, you can have a look at [`emoji.solution.js`](/04-iterable-protocol/exercises/emoji.solution.js) for a possible solution. 243 | 244 | 245 | > **🏹 Exercise** ([binarytree.js](/04-iterable-protocol/exercises/binarytree.js)) 246 | > 247 | > You are given a simple implementation of a binary tree data structure. Can you make it iterable? 248 | > 249 | > A skeleton of the file is available at `04-iterable-protocol/exercises/binarytree.js`. 250 | > 251 | > You can edit the file and run an interactive test session to validate your implementation with: 252 | > 253 | > ```bash 254 | > npm run ex -- 04-iterable-protocol/exercises/binarytree.test.js 255 | > ``` 256 | > 257 | > If you really struggle with this, you can have a look at [`binarytree.solution.js`](/04-iterable-protocol/exercises/binarytree.solution.js) for a possible solution. 258 | 259 | 260 | > **🏹 Exercise** ([entries.js](/04-iterable-protocol/exercises/entries.js)) 261 | > 262 | > Let's re-implement `Object.entries` by ourselves! 263 | > 264 | > A skeleton of the file is available at `04-iterable-protocol/exercises/entries.js`. 265 | > 266 | > You can edit the file and run an interactive test session to validate your implementation with: 267 | > 268 | > ```bash 269 | > npm run ex -- 04-iterable-protocol/exercises/entries.test.js 270 | > ``` 271 | > 272 | > If you really struggle with this, you can have a look at [`entries.solution.js`](/04-iterable-protocol/exercises/entries.solution.js) for a possible solution. 273 | 274 | 275 | ## Summary 276 | 277 | Here's a summary of what we learned in this chapter: 278 | 279 | - The **iterable protocol** defines what's expected for a JavaScript object to be considered **iterable**. That is an object that holds a collection of data on which you can iterate on sequentially. 280 | - An object is **iterable** if it implements a **_special_ method called** `Symbol.iterator` which **returns an iterator**. 281 | - In other words, an object is iterable if you can get an iterator from it! 282 | - Generator functions produce objects that are iterable. 283 | - We saw that generators produce objects that are also iterators. 284 | - It is possible to have objects that are both iterator and iterable! 285 | - The trick is to create the object as an iterator and to implement a `Symbol.iterator` that returns the object itself (`this`). 286 | 287 | That's all for now, congratulations on finishing the fourth chapter! 🎉 288 | 289 | Take a little break and get ready to move to the [Next section](/05-async-iterator-protocol/README.md). 290 | 291 | --- 292 | 293 | | [⬅️ 03 - Iterator protocol](/03-iterator-protocol/README.md) | [🏠](/README.md)| [05 - Async Iterator protocol ➡️](/05-async-iterator-protocol/README.md)| 294 | |:--------------|:------:|------------------------------------------------:| -------------------------------------------------------------------------------- /04-iterable-protocol/countdown-class.js: -------------------------------------------------------------------------------- 1 | class Countdown { 2 | constructor (start) { 3 | this.start = start 4 | } 5 | 6 | [Symbol.iterator] () { 7 | let nextVal = this.start 8 | return ({ 9 | next () { 10 | if (nextVal < 0) { 11 | return { done: true } 12 | } 13 | return { 14 | done: false, 15 | value: nextVal-- 16 | } 17 | } 18 | }) 19 | } 20 | } 21 | 22 | const countdown = new Countdown(3) 23 | 24 | for (const value of countdown) { 25 | console.log(value) 26 | } 27 | 28 | // Note that Symbol.iterator gives us a new iterator each time, so we can do this again: 29 | console.log('\n--- trying again ---\n') 30 | 31 | for (const value of countdown) { 32 | console.log(value) 33 | } 34 | 35 | // 3 36 | // 2 37 | // 1 38 | // 0 39 | // 40 | // --- trying again --- 41 | // 42 | // 3 43 | // 2 44 | // 1 45 | // 0 46 | -------------------------------------------------------------------------------- /04-iterable-protocol/countdown-generator.js: -------------------------------------------------------------------------------- 1 | function * countdownGen (start) { 2 | for (let i = start; i >= 0; i--) { 3 | yield i 4 | } 5 | } 6 | 7 | const countdown = countdownGen(3) 8 | for (const value of countdown) { 9 | console.log(value) 10 | } 11 | -------------------------------------------------------------------------------- /04-iterable-protocol/countdown-iterator-iterable.js: -------------------------------------------------------------------------------- 1 | class Countdown { 2 | constructor (start) { 3 | this.nextVal = start 4 | } 5 | 6 | // iterator protocol 7 | next () { 8 | if (this.nextVal < 0) { 9 | return { done: true } 10 | } 11 | return { 12 | done: false, 13 | value: this.nextVal-- 14 | } 15 | } 16 | 17 | // iterable protocol 18 | [Symbol.iterator] () { 19 | return this 20 | } 21 | } 22 | 23 | const countdown = new Countdown(3) 24 | 25 | console.log(countdown.next()) // { done: false, value: 3 } 26 | 27 | for (const value of countdown) { 28 | console.log(value) 29 | } 30 | 31 | // 2 32 | // 1 33 | // 0 34 | 35 | console.log(countdown.next()) // { done: true } 36 | -------------------------------------------------------------------------------- /04-iterable-protocol/countdown.js: -------------------------------------------------------------------------------- 1 | function createCountdown (start) { 2 | let nextVal = start 3 | return { 4 | [Symbol.iterator]: () => ({ 5 | next () { 6 | if (nextVal < 0) { 7 | return { done: true } 8 | } 9 | return { 10 | done: false, 11 | value: nextVal-- 12 | } 13 | } 14 | }) 15 | } 16 | } 17 | 18 | const countdown = createCountdown(3) 19 | for (const value of countdown) { 20 | console.log(value) 21 | } 22 | 23 | // 3 24 | // 2 25 | // 1 26 | // 0 27 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/binarytree.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | 4 | Complete the binary tree implementation and make it iterable. 5 | The given iterator should produce the values in the tree in order. 6 | 7 | Hint: you can rely on the provided `inOrderTraversal` function to get the 8 | items in order. 9 | 10 | Run the tests with: 11 | 12 | > npm run ex -- 04-iterable-protocol/exercises/binarytree.test.js 13 | */ 14 | 15 | class Node { 16 | constructor (value, left, right) { 17 | this.value = value 18 | this.left = left 19 | this.right = right 20 | } 21 | } 22 | 23 | export class BinaryTree { 24 | constructor () { 25 | this.root = null 26 | } 27 | 28 | insert (value) { 29 | const node = new Node(value) 30 | if (!this.root) { 31 | this.root = node 32 | return 33 | } 34 | 35 | let current = this.root 36 | while (current) { 37 | if (value < current.value) { 38 | if (!current.left) { 39 | current.left = node 40 | break 41 | } 42 | current = current.left 43 | } else { 44 | if (!current.right) { 45 | current.right = node 46 | break 47 | } 48 | current = current.right 49 | } 50 | } 51 | } 52 | 53 | * inOrderTraversal (node = this.root) { 54 | // NOTE: yield* is a special syntax to delegate to another generator 55 | if (node.left) yield * this.inOrderTraversal(node.left) 56 | yield node.value 57 | if (node.right) yield * this.inOrderTraversal(node.right) 58 | } 59 | 60 | // What's missing here to make objects of this class iterable?! 61 | } 62 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/binarytree.solution.js: -------------------------------------------------------------------------------- 1 | class Node { 2 | constructor (value, left, right) { 3 | this.value = value 4 | this.left = left 5 | this.right = right 6 | } 7 | } 8 | 9 | export class BinaryTree { 10 | constructor () { 11 | this.root = null 12 | } 13 | 14 | insert (value) { 15 | const node = new Node(value) 16 | if (!this.root) { 17 | this.root = node 18 | return 19 | } 20 | 21 | let current = this.root 22 | while (current) { 23 | if (value < current.value) { 24 | if (!current.left) { 25 | current.left = node 26 | break 27 | } 28 | current = current.left 29 | } else { 30 | if (!current.right) { 31 | current.right = node 32 | break 33 | } 34 | current = current.right 35 | } 36 | } 37 | } 38 | 39 | * inOrderTraversal (node = this.root) { 40 | if (node.left) yield * this.inOrderTraversal(node.left) 41 | yield node.value 42 | if (node.right) yield * this.inOrderTraversal(node.right) 43 | } 44 | 45 | [Symbol.iterator] () { 46 | return this.inOrderTraversal() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/binarytree.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import { BinaryTree as BinaryTreeSolution } from './binarytree.solution.js' 4 | import { BinaryTree as BinaryTreeTpl } from './binarytree.js' 5 | 6 | const BinaryTree = process.env.TEST_SOLUTIONS ? BinaryTreeSolution : BinaryTreeTpl 7 | 8 | const values = [5, 3, 7, 2, 4, 6, 8, 1, 9] 9 | const expected = [...values].sort() 10 | 11 | tap.test('iterating over the binary tree give the numbers in order', async function (t) { 12 | const tree = new BinaryTree() 13 | for (const value of values) { 14 | tree.insert(value) 15 | } 16 | t.same([...tree], expected) 17 | }) 18 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/emoji.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's implement an iterable over all the emojis of a given text. 4 | 5 | This iterator should behave as follows: 6 | 7 | - If you create the iterable with the `text`: "Hello 👋 World 🌎" 8 | - You should be able to do a `for ... of` loop over the iterable and get '👋' 9 | for the first iteration and '🌎' for the second. 10 | 11 | Try to implement this iterable using 3 different styles: 12 | 13 | - With a factory function 14 | - With a class 15 | - With a generator 16 | 17 | TIPS: 18 | 19 | - You can convert a string into an array of unicode characters with `Array.from(str)` 20 | - If you use a `for ... of` over a string, every element will be a unicode character 21 | - A simple way to test if a given unicode character is an emoji is: `char.match(/\p{Emoji}/u) !== null` 22 | 23 | Run the tests with: 24 | 25 | > npm run ex -- 04-iterable-protocol/exercises/emoji.test.js 26 | */ 27 | 28 | export function createEmojiIter (text) { 29 | // write your code here 30 | } 31 | 32 | export class EmojiIter { 33 | constructor (text) { 34 | this.chars = Array.from(text) 35 | // write your code here 36 | } 37 | 38 | next () { 39 | // write your code here 40 | } 41 | } 42 | 43 | export function * emojiIterGen (text) { 44 | // write your code here 45 | } 46 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/emoji.solution.js: -------------------------------------------------------------------------------- 1 | export function createEmojiIter (text) { 2 | let index = 0 3 | const chars = Array.from(text) 4 | 5 | return { 6 | next () { 7 | while (index < chars.length) { 8 | const char = chars[index++] 9 | if (char.match(/\p{Emoji}/u) !== null) { 10 | return { done: false, value: char } 11 | } 12 | } 13 | 14 | return { done: true, value: undefined } 15 | }, 16 | 17 | // Note: this is the only addition from the previous chapter 18 | // This makes it both an iterator and an iterable 19 | [Symbol.iterator] () { 20 | return this 21 | } 22 | } 23 | } 24 | 25 | export class EmojiIter { 26 | constructor (text) { 27 | this.chars = Array.from(text) 28 | this.index = 0 29 | } 30 | 31 | next () { 32 | while (this.index < this.chars.length) { 33 | const char = this.chars[this.index++] 34 | if (char.match(/\p{Emoji}/u) !== null) { 35 | return { done: false, value: char } 36 | } 37 | } 38 | 39 | return { done: true, value: undefined } 40 | } 41 | 42 | // Note: this is the only addition from the previous chapter 43 | // This makes it both an iterator and an iterable 44 | [Symbol.iterator] () { 45 | return this 46 | } 47 | } 48 | 49 | // Note: this one doesn't need to change. 50 | // Generator functions create objects that are both iterable and iterator. 51 | export function * emojiIterGen (text) { 52 | for (const char of text) { 53 | if (char.match(/\p{Emoji}/u) !== null) { 54 | yield char 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/emoji.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import * as emojiSolution from './emoji.solution.js' 4 | import * as emojiTpl from './emoji.js' 5 | 6 | const solution = process.env.TEST_SOLUTIONS ? emojiSolution : emojiTpl 7 | 8 | const text = 'the quick ⚡ brown 🟫 fox 🦊 jumps over 🔼 the lazy 💤 dog 🐶' 9 | const expected = ['⚡', '🟫', '🦊', '🔼', '💤', '🐶'] 10 | 11 | tap.test('createEmojiIter', async function (t) { 12 | const iter = solution.createEmojiIter(text) 13 | const emojis = [...iter] 14 | t.same(emojis, expected) 15 | }) 16 | 17 | tap.test('EmojiIter', async function (t) { 18 | const iter = new solution.EmojiIter(text) 19 | const emojis = [...iter] 20 | t.same(emojis, expected) 21 | }) 22 | 23 | tap.test('emojiIterGen', async function (t) { 24 | const iter = solution.emojiIterGen(text) 25 | const emojis = [...iter] 26 | t.same(emojis, expected) 27 | }) 28 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/entries.js: -------------------------------------------------------------------------------- 1 | /* 2 | Do you remember when we saw that using for...of over an object would 3 | result into an error (objects are not inherently iterable)? 4 | 5 | We also saw that you can use `Object.entries` to create an iterable over the 6 | key/value pairs of an object. 7 | 8 | Let's pretend for a moment that `Object.entries` does not exist and let's 9 | reimplement something similar by ourselves. 10 | 11 | Write a function that takes an object as an argument and returns an iterable 12 | object that will produce all the key/value pairs of the original object. 13 | 14 | Note: using `Object.entries` is considered cheating. 15 | 16 | Run the tests with: 17 | 18 | > npm run ex -- 04-iterable-protocol/exercises/entries.test.js 19 | 20 | */ 21 | 22 | export function entriesIterable (obj) { 23 | // Your code here 24 | } 25 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/entries.solution.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Run the tests with: 4 | 5 | > npm run ex -- 04-iterable-protocol/exercises/entries.test.js 6 | 7 | */ 8 | 9 | export function * entriesIterable (obj) { 10 | for (const key of Object.keys(obj)) { 11 | yield [key, obj[key]] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /04-iterable-protocol/exercises/entries.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | 3 | import * as entriesIterableSolution from './entries.solution.js' 4 | import * as entriesIterableTpl from './entries.js' 5 | 6 | const solution = process.env.TEST_SOLUTIONS ? entriesIterableSolution : entriesIterableTpl 7 | 8 | tap.test('produce key value pairs', async function (t) { 9 | const obj = { 10 | foo: 'bar', 11 | baz: 'quz', 12 | qux: 'quux', 13 | corge: 'grault', 14 | garply: 'waldo', 15 | fred: 'plugh', 16 | xyzzy: 'thud' 17 | } 18 | const expected = [ 19 | ['foo', 'bar'], 20 | ['baz', 'quz'], 21 | ['qux', 'quux'], 22 | ['corge', 'grault'], 23 | ['garply', 'waldo'], 24 | ['fred', 'plugh'], 25 | ['xyzzy', 'thud'] 26 | ] 27 | 28 | const iter = solution.entriesIterable(obj) 29 | const pairs = [...iter] 30 | t.same(pairs, expected) 31 | }) 32 | 33 | tap.test('an empty obj does not produce anything', async function (t) { 34 | const iter = solution.entriesIterable({}) 35 | const pairs = [...iter] 36 | t.same(pairs, []) 37 | }) 38 | -------------------------------------------------------------------------------- /04-iterable-protocol/hello-iterator-iterable.js: -------------------------------------------------------------------------------- 1 | const iterableIterator = { 2 | next () { 3 | return { done: false, value: 'hello' } 4 | }, 5 | [Symbol.iterator] () { 6 | return this 7 | } 8 | } 9 | 10 | iterableIterator.next() 11 | // { done: false, value: "hello" } 12 | 13 | for (const value of iterableIterator) { 14 | console.log(value) 15 | } 16 | 17 | // hello 18 | // hello 19 | // hello 20 | // ... 21 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/README.md: -------------------------------------------------------------------------------- 1 | # 05 - Async Iterator Protocol 2 | 3 | Everything we have discussed so far is **synchronous**. 4 | 5 | This means that we can use the iterator and the iterable protocols and the techniques that we learned so far to iterate over data that is already available in memory or that it can be _produced_ synchronously. 6 | 7 | This is great, but more often than not, JavaScript code tends to be **asynchronous**. In fact, most of the time, our code needs to use data from files, network sockets, HTTP servers (APIs), databases, etc. 8 | 9 | Can we apply the same principles that we learned so far in **a**synchronous situations? 10 | 11 | What if we could define asynchronous iterators (and iterable objects) to implement sequential iteration patterns with data arriving in order over time? 12 | 13 | Great use cases would be traversing paginated datasets or consuming tasks from a remote queue. 14 | 15 | But let's focus more on the **paginated dataset** example for now. 16 | 17 | We need to consume a significant amount of data, so that data is exposed to us in chunks (pages). Every page contains a portion of the whole dataset and we need to explicitly request the next page to keep going. 18 | 19 | Traversing a paginated dataset generally looks like this: 20 | 21 | 1. Get the first page 22 | 2. Use the data from the first page 23 | 3. Request the next page 24 | 4. Use the data from the next page 25 | 5. ... repeat from **3.** until there are no more pages left 26 | 27 | Do you recall this pattern from dealing with databases and APIs? 28 | 29 | Wouldn't it be nice if we could handle this kind of situations by doing something like this? 👇 30 | 31 | ```js 32 | for (const currentPage of somePaginatedDataset) { 33 | // process data from `currentPage` 34 | } 35 | ``` 36 | 37 | Well, yes, but this `for ... of` loop is synchronous! 🤔 38 | 39 | We need an equivalent **a**synchronous version of this loop, something that can wait for data to be available asynchronously before triggering the next iteration. 40 | 41 | It turns out that what we want actually exists and it's called [`for await ... of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of): 42 | 43 | ```js 44 | for await (const currentPage of somePaginatedDataset) { 45 | // process data from `currentPage` 46 | } 47 | ``` 48 | 49 | Note the use of `await` after `for`! 50 | 51 | We can use this kind of syntax with **async iterables**, but before we get there, we need to learn about **async iterators**! 52 | 53 | 54 | ## The async iterator protocol 55 | 56 | An object is an **async iterator** if it has a `next()` method. Every time you call it, it returns **a promise that resolves** to an object with the keys `done` (boolean) and `value`. 57 | 58 | This is _quite similar_ to its synchronous counterpart! 59 | 60 | The main difference here is that the `next()` method this time returns **a `Promise` object**! 61 | 62 | The promise is used to capture the asynchronicity of the iterator. The returned promise indicates that data is being retrieved and that it will eventually be available. Once the promise is settled, the data is available and it is represented exactly as with synchronous iterators: an object with the shape `{ done, value }`. 63 | 64 | So, let's see an example for an async iterator. 65 | 66 | We could stick with our countdown example, except that this time we actually want some time to pass between one item and the next are emitted: 67 | 68 | ```js 69 | // countdown-async-iterator.js 70 | import { setTimeout } from 'node:timers/promises' 71 | 72 | function createAsyncCountdown (from, delay = 1000) { 73 | let nextVal = from 74 | return { 75 | async next () { 76 | await setTimeout(delay) 77 | if (nextVal < 0) { 78 | return { done: true } 79 | } 80 | 81 | return { done: false, value: nextVal-- } 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | A few things worth reviewing here: 88 | 89 | 1. We are importing `setTimeout` from the core `timers/promises` module. This function is a _promisified_ version of the classic `setTimeout`. It allows us to create a promise that resolves after a given delay (specified in milliseconds). 90 | 2. We define a factory function called `createAsyncCountdown`. This function receives the starting number and the delay between numbers (in milliseconds) as arguments. 91 | 3. The factory function returns an anonymous object (our async iterator) 92 | 4. The `next()` function has to return a `Promise` object. Here we are making the function `async` to do that for us! In fact, an async function always returns a promise under the hood. The promise resolves when a `return` statement is reached in the function body (and it resolves exactly to the value that is returned). 93 | 5. Finally note how are we are _awaiting_ the promise returned by `setTimeout` to wait for a given amount of time before letting our function code continue. 94 | 95 | I hope that all of that seems quite clear to you at this point! 96 | 97 | So now, how do we use this factory function and the async iterator that it returns? 98 | 99 | ```js 100 | const countdown = createAsyncCountdown(3) 101 | console.log(await countdown.next()) 102 | console.log(await countdown.next()) 103 | console.log(await countdown.next()) 104 | console.log(await countdown.next()) 105 | console.log(await countdown.next()) 106 | ``` 107 | 108 | Simply, we just call the `next()` method! 109 | 110 | But remember that this time, for every call, we get back a promise, so we need to `await` that promise before making another call to `next()`! 111 | 112 | We can see what will happen if we execute the script above with the following image: 113 | 114 | ![Async Iterator Countdown example running in the terminal](./images/countdown-async-iterator.gif) 115 | 116 | Note how it takes roughly 1 second for an item to become available and be printed! 117 | 118 | It's also worth mentioning that here we went for a factory function based approach, but we could have used a class as well! 119 | 120 | > **🎭 PLAY** 121 | > Try to re-implement our async countdown using a class. 122 | 123 | Now this doesn't look extremely useful, but imagine that you could implement an iterator that every time we call `next()` fetches data from some remote resource! 124 | 125 | 126 | ## Async iterators with async generators 127 | 128 | We saw how convenient generator functions are to create iterator and iterable objects. 129 | 130 | Can we use them also for async iterators? 131 | 132 | YES we can! 💪 133 | 134 | So, let's see how to rewrite our async countdown iterator using a generator: 135 | 136 | ```js 137 | // countdown-async-iterator-generator.js 138 | import { setTimeout } from 'node:timers/promises' 139 | 140 | async function * createAsyncCountdown (start, delay = 1000) { 141 | for (let i = start; i >= 0; i--) { 142 | await setTimeout(delay) 143 | yield i 144 | } 145 | } 146 | ``` 147 | 148 | Note how this function is both an `async` function but also a generator... do you see that asterisk?! 149 | 150 | This kind of functions have 2 super powers: you can use both `yield` and `await` in their body! 🤯 151 | 152 | - `await` works like with any other async function and it allows you to await for promises to settle before continuing the execution of the code 153 | - `yield` works like with any other generator function and it allows you to _produce_ a value and suspend the execution until `next()` is called again on the underlying generator object. 154 | 155 | A little spoiler, this function returns an object that is also an **async iterable**, but we'll talk more about that in the next chapter! 156 | 157 | 158 | ## Exercises 159 | 160 | Ok, get ready because in this chapter we are going to have some serious piece of exercise... 😏 161 | 162 | 163 | > **🏹 Exercise** ([rickmorty.js](/05-async-iterator-protocol/exercises/rickmorty.js)) 164 | > 165 | > I hope you like Rick and Morty... because we have to implement an iterator that allows us to go through 166 | > a paginated API that returns all Rick and Morty characters! Yes, there's [an API](https://rickandmortyapi.com) for that! 167 | > 168 | > Let's do this! 💪 169 | > 170 | > A skeleton of the file is available at `05-async-iterator-protocol/exercises/rickmorty.js`. 171 | > 172 | > You can edit the file and run an interactive test session to validate your implementation with: 173 | > 174 | > ```bash 175 | > npm run ex -- 05-async-iterator-protocol/exercises/rickmorty.test.js 176 | > ``` 177 | > 178 | > If you really struggle with this, you can have a look at [`rickmorty.solution.js`](/05-async-iterator-protocol/exercises/rickmorty.solution.js) for a possible solution. 179 | 180 | 181 | ## Summary 182 | 183 | That's what we learned in this chapter: 184 | 185 | - Async iterators are the asynchronous counterpart of iterators. 186 | - They are useful to iterate over data that becomes available asynchronously (e.g. coming from a database or a REST API). 187 | - A good example is a paginated API, we could build an async iterator that gives a new page for every iteration. 188 | - An object is an async iterator if it has a `next()` method which returns a `Promise` that resolves to an object with the shape: `{done, value}`. 189 | - The main difference with the iterator protocol is that this time `next()` returns a promise. 190 | - When we call next we need to make sure we `await` the returned promise. 191 | 192 | 193 | That's all for now, congratulations on finishing the fifth chapter! 🎉 194 | 195 | Take a little break and get ready to move to the [Next section](/06-async-iterable-protocol/README.md). 196 | 197 | --- 198 | 199 | | [⬅️ 04 - Iterable protocol](/04-iterable-protocol/README.md) | [🏠](/README.md)| [06 - Async Iterable protocol ➡️](/06-async-iterable-protocol/README.md)| 200 | |:--------------|:------:|------------------------------------------------:| -------------------------------------------------------------------------------- /05-async-iterator-protocol/countdown-async-iterator-generator.js: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'node:timers/promises' 2 | 3 | async function * createAsyncCountdown (start, delay = 1000) { 4 | for (let i = start; i >= 0; i--) { 5 | await setTimeout(delay) 6 | yield i 7 | } 8 | } 9 | 10 | const countdown = createAsyncCountdown(3) 11 | console.log(await countdown.next()) 12 | // { done: false, value: 3 } 13 | 14 | console.log(await countdown.next()) 15 | // { done: false, value: 2 } 16 | 17 | console.log(await countdown.next()) 18 | // { done: false, value: 1 } 19 | 20 | console.log(await countdown.next()) 21 | // { done: false, value: 0 } 22 | 23 | console.log(await countdown.next()) 24 | // { value: undefined, done: true } 25 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/countdown-async-iterator.js: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'node:timers/promises' 2 | 3 | function createAsyncCountdown (from, delay = 1000) { 4 | let nextVal = from 5 | return { 6 | async next () { 7 | await setTimeout(delay) 8 | if (nextVal < 0) { 9 | return { done: true } 10 | } 11 | 12 | return { done: false, value: nextVal-- } 13 | } 14 | } 15 | } 16 | 17 | const countdown = createAsyncCountdown(3) 18 | console.log(await countdown.next()) 19 | // { done: false, value: 3 } 20 | 21 | console.log(await countdown.next()) 22 | // { done: false, value: 2 } 23 | 24 | console.log(await countdown.next()) 25 | // { done: false, value: 1 } 26 | 27 | console.log(await countdown.next()) 28 | // { done: false, value: 0 } 29 | 30 | console.log(await countdown.next()) 31 | // { done: true } 32 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/exercises/rickmorty.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's create a utility function to retrieve all the Rick and Morty characters! 4 | 5 | Yes, there's an API for that. Check out https://rickandmortyapi.com/ 6 | 7 | We want to implement a `createCharactersPaginator` factory function that returns 8 | an iterator of pages. Every page contains multiple characters. 9 | 10 | You can get the first page by calling: 11 | 12 | ``` 13 | GET https://rickandmortyapi.com/api/character 14 | ``` 15 | 16 | The iterator should emit values that look like this: 17 | 18 | ```json 19 | [ 20 | "character1", 21 | "character2", 22 | "character3", 23 | ... 24 | "character20" 25 | ] 26 | ``` 27 | 28 | An array with a maximum of 20 character names. 29 | 30 | You can use `axios`, `node-fetch` or `undici`, they are all already available in your `node_modules`! 31 | 32 | Run the tests with: 33 | 34 | npm run ex -- 05-async-iterator-protocol/exercises/rickmorty.test.js 35 | */ 36 | 37 | export default function createCharactersPaginator () { 38 | // return an iterator that returns pages of characters 39 | } 40 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/exercises/rickmorty.solution.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default function createCharactersPaginator () { 4 | let nextPage = 'https://rickandmortyapi.com/api/character' 5 | return { 6 | async next () { 7 | if (nextPage === null) { 8 | return { done: true, value: undefined } 9 | } 10 | 11 | const resp = await axios.get(nextPage) 12 | nextPage = resp.data.info.next 13 | 14 | const pageData = resp.data.results.map((char) => char.name) 15 | return { done: false, value: pageData } 16 | } 17 | } 18 | } 19 | 20 | // const paginator = createCharactersPaginator() 21 | // console.log(await paginator.next()) 22 | // console.log(await paginator.next()) 23 | // console.log(await paginator.next()) 24 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/exercises/rickmorty.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import nock from 'nock' 3 | 4 | import rickmortydata from './__mocks__/rickmortydata.js' 5 | import createCharactersPaginatorSolution from './rickmorty.solution.js' 6 | import createCharactersPaginatorTpl from './rickmorty.js' 7 | 8 | const createCharactersPaginator = process.env.TEST_SOLUTIONS ? createCharactersPaginatorSolution : createCharactersPaginatorTpl 9 | 10 | tap.test('createCharactersPaginator', async function (t) { 11 | const scope = nock('https://rickandmortyapi.com') 12 | 13 | const expected = [] 14 | 15 | for (let i = 0; i < rickmortydata.length; i++) { 16 | const pageNum = i + 1 17 | const page = rickmortydata[i] 18 | scope.get(`/api/character${pageNum > 1 ? `?page=${pageNum}` : ''}`) 19 | .reply(200, page) 20 | 21 | expected.push(page.results.map(c => c.name)) 22 | } 23 | 24 | const iter = createCharactersPaginator() 25 | const received = [] 26 | 27 | let resp = await iter.next() 28 | while (!resp.done) { 29 | received.push(resp.value) 30 | resp = await iter.next() 31 | } 32 | 33 | t.same(received, expected, 'Results match') 34 | t.ok(scope.isDone, 'Called all the pages') 35 | }) 36 | -------------------------------------------------------------------------------- /05-async-iterator-protocol/images/countdown-async-iterator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/iteration-protocols-workshop/e0a53bcea2f5bcc587a4543c27e25eafa7b39f4f/05-async-iterator-protocol/images/countdown-async-iterator.gif -------------------------------------------------------------------------------- /06-async-iterable-protocol/README.md: -------------------------------------------------------------------------------- 1 | # 06 - Async Iterable Protocol 2 | 3 | In the previous chapters we already mention that **async iterable** objects would allow us to do something like this: 4 | 5 | ```js 6 | for await (const currentPage of somePaginatedDataset) { 7 | // process data from `currentPage` 8 | } 9 | ``` 10 | 11 | This is a very convenient way to implement an asynchronous iteration, like, for instance, going over a paginated dataset. 12 | 13 | So, without further ado, let's see how we can create async iterables. 14 | 15 | 16 | ## The async iterable protocol 17 | 18 | An object is an **async iterable** if it implements a special method called `Symbol.asyncIterator`, which is a zero-argument function that returns an **async iterator**. 19 | 20 | Again this is very close to the definition of **iterable** objects that we discussed in [chapter 04](/04-iterable-protocol/README.md). 21 | 22 | The following table summarises the differences between the 2 iterable protocols: 23 | 24 | | | **Iterable** | **Async Iterable** | 25 | |-------------|-------------------|------------------------| 26 | | **method** | `Symbol.iterator` | `Symbol.asyncIterator` | 27 | | **returns** | an iterator | an async iterator | 28 | 29 | Let's take a moment to remember that an iterable is an object that holds or represents a collection. We can iterate over that collection by asking the iterable to give us an iterator. For synchronous iterables we need to call `Symbol.iterator` to get a synchronous iterator, while for asynchronous iterables we need to call `Symbol.asyncIterator` to get an async iterator. 30 | 31 | Makes sense, right? 🤗 32 | 33 | OK... Now, let's see an example. Let's convert our asynchronous countdown example into an async iterable: 34 | 35 | ```js 36 | // countdown-async-iterable.js 37 | import { setTimeout } from 'node:timers/promises' 38 | 39 | function createAsyncCountdown (start, delay = 1000) { 40 | let nextVal = start 41 | return { 42 | [Symbol.asyncIterator]: function () { 43 | return { 44 | async next () { 45 | await setTimeout(delay) 46 | if (nextVal < 0) { 47 | return { done: true } 48 | } 49 | return { done: false, value: nextVal-- } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | This is very similar to the code we presented to implement an async countdown iterator. 58 | 59 | The only difference now is that our factory function returns an iterable object. In fact, it has a `Symbol.asyncIterator` method and this method returns an async iterator. If we take a closer look at the code inside the `Symbol.asyncIterator` function, we can see that it is exactly the same code we already wrote for the async iterator example. 60 | 61 | Because objects returned by `createAsyncCountdown` are async iterable, we can use the `for await ... of` syntax with them: 62 | 63 | ```js 64 | const countdown = createAsyncCountdown(3) 65 | 66 | for await (const value of countdown) { 67 | console.log(value) 68 | } 69 | ``` 70 | 71 | The following image illustrates what we should see on our terminal when executing this code: 72 | 73 | ![Terminal showing what happens when we execute our async countdown iterable script](./images/countdown-async-iterable.gif) 74 | 75 | Can you see that it takes roughly one second for every number to appear? 76 | 77 | As with synchronous iterables, `for await ... of` gives us the convenience of not having to deal with `done` and `value`. We just get the value objects straight away inside our loop and the loop will automatically stop when `done` is `true`! 78 | 79 | How convenient is that? 😉 80 | 81 | > **🎭 PLAY** 82 | > Did you realise that our implementation of the async iterable countdown is not resumable? That means that if we use `for await ... of` for a second time on a `countdown` object it simply won't do anything... What could we change to make our implementation resumable? 83 | 84 | 85 | ## Async iterables with async generators 86 | 87 | We already mentioned a few times that we can use **async generators** to create async iterable objects. Let's just do that: 88 | 89 | 90 | ```js 91 | // countdown-async-iterable-generator.js 92 | import { setTimeout } from 'node:timers/promises' 93 | 94 | async function * createAsyncCountdown (start, delay = 1000) { 95 | for (let i = start; i >= 0; i--) { 96 | await setTimeout(delay) 97 | yield i 98 | } 99 | } 100 | ``` 101 | 102 | It shouldn't come as a big surprise at this point to see that that code is exactly as the one we used in the previous chapter to create our countdown async iterator example! 103 | 104 | Yes, async generators create objects that are both async iterators and async iterable! 105 | 106 | This means that we can use `await ... next()` and `for await ... of`: 107 | 108 | ```js 109 | const countdown = createAsyncCountdown(3) 110 | 111 | console.log(await countdown.next()) // using it as an async iterator 112 | 113 | // using it as an async iterable 114 | for await (const value of countdown) { 115 | console.log(value) 116 | } 117 | ``` 118 | 119 | Can you guess what is this going to output? 120 | 121 |
👀 You can have a peek if you want! 122 | 123 | ![The output of our async generator example](./images/countdown-async-iterable-generator.gif) 124 | 125 |
126 | 127 | > **🎭 PLAY** 128 | > Do you think this version of the iterable countdown is resumable? What if we repeat the `for await ... of` a second time? Should anything be printed? If you can't take a guess, why don't you change the code and see for yourself? 😇 129 | 130 | 131 | ## Exercises 132 | 133 | I am sure you already guessed what I am going to ask you here as an exercise... 😜 134 | 135 | > **🏹 Exercise** ([rickmorty.js](/06-async-iterable-protocol/exercises/rickmorty.js)) 136 | > 137 | > Let's convert our Rick and Morty paginator from an async iterator to an async iterable! 138 | > 139 | > A skeleton of the file is available at `06-async-iterable-protocol/exercises/rickmorty.js`. 140 | > 141 | > You can edit the file and run an interactive test session to validate your implementation with: 142 | > 143 | > ```bash 144 | > npm run ex -- 06-async-iterable-protocol/exercises/rickmorty.test.js 145 | > ``` 146 | > 147 | > If you really struggle with this, you can have a look at [`rickmorty.solution.js`](/06-async-iterable-protocol/exercises/rickmorty.solution.js) for a possible solution. 148 | 149 | 150 | ## Summary 151 | 152 | Let's review what we learned in this chapter: 153 | 154 | - The async iterable protocol defines what it means for an object to be an async iterable. 155 | - Once you have an async iterable you can use the `for await ... of` syntax on it. 156 | - An object is an async iterable if it has a special method called `Symbol.asyncIterator` that returns an async iterator. 157 | - Async iterables are a great way to abstract paginated data that is available asynchronously or similar operations like pulling jobs from a remote queue. 158 | - A small spoiler, async iterables can also be used with Node.js streams... 159 | 160 | That's all for now, congratulations on finishing the sixth chapter! 🎉 161 | 162 | Pat yourself on the back, take a little break, and get ready to explore some interesting [Tips and Pitfalls](/07-tips-and-pitfalls/README.md). 163 | 164 | --- 165 | 166 | | [⬅️ 05 - Async Iterator protocol](/05-async-iterator-protocol/README.md) | [🏠](/README.md)| [07 - Tips and Pitfalls ➡️](/07-tips-and-pitfalls/README.md)| 167 | |:--------------|:------:|------------------------------------------------:| 168 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/countdown-async-iterable-generator.js: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'node:timers/promises' 2 | 3 | async function * createAsyncCountdown (start, delay = 1000) { 4 | for (let i = start; i >= 0; i--) { 5 | await setTimeout(delay) 6 | yield i 7 | } 8 | } 9 | 10 | const countdown = createAsyncCountdown(3) 11 | 12 | console.log(await countdown.next()) // { value: 3, done: false } 13 | 14 | for await (const value of countdown) { 15 | console.log(value) // 2 ... 1 ... 0 16 | } 17 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/countdown-async-iterable.js: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'node:timers/promises' 2 | 3 | function createAsyncCountdown (start, delay = 1000) { 4 | let nextVal = start 5 | return { 6 | [Symbol.asyncIterator]: function () { 7 | return { 8 | async next () { 9 | await setTimeout(delay) 10 | if (nextVal < 0) { 11 | return { done: true } 12 | } 13 | return { done: false, value: nextVal-- } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | 20 | const countdown = createAsyncCountdown(3) 21 | 22 | for await (const value of countdown) { 23 | console.log(value) // 3 ... 2 ... 1 ... 0 24 | } 25 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/exercises/rickmorty.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | Let's create a utility function to retrieve all the Rick and Morty characters! 4 | 5 | Yes, there's an API for that. Check out https://rickandmortyapi.com/ 6 | 7 | We want to implement a `createCharactersPaginator` factory function that returns 8 | an iterable objects that can be used to go through all of the pages. 9 | Every page contains multiple characters. 10 | 11 | You can get the first page by calling: 12 | 13 | ``` 14 | GET https://rickandmortyapi.com/api/character 15 | ``` 16 | 17 | The iterator should emit values that look like this: 18 | 19 | ```json 20 | [ 21 | "character1", 22 | "character2", 23 | "character3", 24 | ... 25 | "character20" 26 | ] 27 | ``` 28 | 29 | An array with a maximum of 20 character names. 30 | 31 | You can use `axios`, `node-fetch` or `undici`, they are all already available in your `node_modules`! 32 | 33 | Run the tests with: 34 | 35 | npm run ex -- 06-async-iterable-protocol/exercises/rickmorty.test.js 36 | */ 37 | 38 | export default function createCharactersPaginator () { 39 | // return an iterator that returns pages of characters 40 | } 41 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/exercises/rickmorty.solution.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default function createCharactersPaginator () { 4 | let nextPage = 'https://rickandmortyapi.com/api/character' 5 | return { 6 | [Symbol.asyncIterator] () { return this }, 7 | async next () { 8 | if (nextPage === null) { 9 | return { done: true, value: undefined } 10 | } 11 | 12 | const resp = await axios.get(nextPage) 13 | nextPage = resp.data.info.next 14 | 15 | const pageData = resp.data.results.map((char) => char.name) 16 | return { done: false, value: pageData } 17 | } 18 | } 19 | } 20 | 21 | // const paginator = createCharactersPaginator() 22 | // for await (const page of paginator) { 23 | // console.log(page) 24 | // } 25 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/exercises/rickmorty.test.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import nock from 'nock' 3 | 4 | import rickmortydata from './__mocks__/rickmortydata.js' 5 | import createCharactersPaginatorSolution from './rickmorty.solution.js' 6 | import createCharactersPaginatorTpl from './rickmorty.js' 7 | 8 | const createCharactersPaginator = process.env.TEST_SOLUTIONS ? createCharactersPaginatorSolution : createCharactersPaginatorTpl 9 | 10 | tap.test('createCharactersPaginator', async function (t) { 11 | const scope = nock('https://rickandmortyapi.com') 12 | 13 | const expected = [] 14 | 15 | for (let i = 0; i < rickmortydata.length; i++) { 16 | const pageNum = i + 1 17 | const page = rickmortydata[i] 18 | scope.get(`/api/character${pageNum > 1 ? `?page=${pageNum}` : ''}`) 19 | .reply(200, page) 20 | 21 | expected.push(page.results.map(c => c.name)) 22 | } 23 | 24 | const iter = createCharactersPaginator() 25 | const received = [] 26 | 27 | for await (const page of iter) { 28 | received.push(page) 29 | } 30 | 31 | t.same(received, expected, 'Results match') 32 | t.ok(scope.isDone, 'Called all the pages') 33 | }) 34 | -------------------------------------------------------------------------------- /06-async-iterable-protocol/images/countdown-async-iterable-generator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/iteration-protocols-workshop/e0a53bcea2f5bcc587a4543c27e25eafa7b39f4f/06-async-iterable-protocol/images/countdown-async-iterable-generator.gif -------------------------------------------------------------------------------- /06-async-iterable-protocol/images/countdown-async-iterable.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmammino/iteration-protocols-workshop/e0a53bcea2f5bcc587a4543c27e25eafa7b39f4f/06-async-iterable-protocol/images/countdown-async-iterable.gif -------------------------------------------------------------------------------- /07-tips-and-pitfalls/README.md: -------------------------------------------------------------------------------- 1 | # 07 - Tips and Pitfalls 2 | 3 | At this point, you acquired some pretty solid basics when it comes to iterable protocols. Well done for getting so far! 👏 4 | 5 | If you are willing to go a tiny bit deeper, in this chapter we are going to explore some interesting tips and some scary pitfalls (and how to avoid them)! 6 | 7 | 8 | ## Check if an object is Iterable or Async Iterable 9 | 10 | We said that iteration protocols are just _protocols_ (touché). So there is no way to really enforce them, so how can we know if an object is iterable? 11 | 12 | We can do some light checks to see if the protocol is respected: `typeof obj[Symbol.iterator] === 'function'` 13 | 14 | For instance: 15 | 16 | ```js 17 | // iterable-check.js 18 | function isIterable (obj) { 19 | return typeof obj[Symbol.iterator] === 'function' 20 | } 21 | 22 | const array = [1, 2, 3] 23 | console.log(array, isIterable(array)) // true 24 | 25 | const genericObj = { foo: 'bar' } 26 | console.log(genericObj, isIterable(genericObj)) // false 27 | console.log(Object.entries(genericObj), isIterable(Object.entries(genericObj))) // true 28 | 29 | const fakeIterable = { 30 | [Symbol.iterator] () { return 'notAnIterator' } 31 | } 32 | console.log(fakeIterable, isIterable(fakeIterable)) // true 😡 33 | ``` 34 | 35 | Note that this works... for the most part! The problem is that we are only checking that an object has a property called `Symbol.asyncIterator` and that the property is a function. We are not really checking that the function is really returning an iterator. 36 | 37 | Checking that the returned object is actually a _well implemented_ iterator is not easy. We would need to check that the returned object has a `next()` method and that this method returns an object with the shape `{done, value}`. How can we be sure that **every single time** we call `next()` it conforms to this specification (without actually calling `next()`)? And what if suddenly calling `next()` returns `null` or `NaN` or an object with a different shape than the one defined by the protocol? 38 | 39 | This is not an easy problem to solve, so I recommend to stick with the light check if you need to distinguish between iterable and non iterable objects in your business logic. 40 | 41 | If you don't trust that something that looks like an iterable is not implementing the iterable protocol correctly, you'd be better off reading the code of that particular implementation. 42 | 43 | Similarly you can check if an object is an **async** iterable with `typeof obj[Symbol.asyncIterator] === 'function'`: 44 | 45 | ```js 46 | function isAsyncIterable (obj) { 47 | return typeof obj[Symbol.asyncIterator] === 'function' 48 | } 49 | ``` 50 | 51 | 52 | ## Node.js Readable streams are async iterables 53 | 54 | I don't know if you know about Node.js streams... Well, if you don't I strongly suggest you look into them because they are awesome! Did I already tell you I have [a workshop like this one entirely dedicated to Node.js streams](https://github.com/lmammino/streams-workshop)? 😇 55 | 56 | Ok, back to my point... If you know about Node.js streams, you might have realised already that streams (in particular Readable streams) kinda fit the bill of async iterables... I mean they are another abstraction that allows us to consume data that is ordered and becomes available asynchronously. We just tend to refers to the unit of streams as **chunks** while we generally talk about **items** or **pages** with async iterables. 57 | 58 | Well, the point that I want to get to is... 🥁 ... Redable streams are also async iterables! 59 | 60 | To see what all of that implies, let's discuss an example. 61 | 62 | Let's say we want to use Node.js streams to count the number of bytes from standard input. 63 | 64 | A quite standard way to do that would look like this: 65 | 66 | ```js 67 | // count-bytes-stdin.js 68 | let bytes = 0 69 | process.stdin.on('data', (chunk) => { 70 | bytes += chunk.length 71 | }) 72 | process.stdin.on('end', () => { 73 | console.log(`${bytes} bytes read from stdin`) 74 | }) 75 | ``` 76 | 77 | Note that `process.stdin` is a way to access standard input for a process in Node.js. This object is a Redable stream, so that's why we can listen to the `data` and the `end` events. 78 | 79 | I have prepared a little example dataset, so we could use it to run the code above: 80 | 81 | ```bash 82 | node 07-tips-and-pitfalls/count-bytes-stdin.js < 07-tips-and-pitfalls/assets/bigdata.csv 83 | ``` 84 | 85 | If you run this command you should see: 86 | 87 | ```plain 88 | 332637 bytes read from stdin 89 | ``` 90 | 91 | Which is not really _"big data"_ but it should be enough to convince you that our stream-based implementation works. 92 | 93 | Ok, but we said that Readable streams are also async iterable objects, so we could write our count bytes example in a much nicer way (if you think that avoiding explicit events is nicer 😜): 94 | 95 | ```js 96 | // count-bytes-stdin-async-iterable.js 97 | let bytes = 0 98 | 99 | for await (const chunk of process.stdin) { 100 | bytes += chunk.length 101 | } 102 | 103 | console.log(`${bytes} bytes read from stdin`) 104 | ``` 105 | 106 | Let's test this: 107 | 108 | ```bash 109 | node 07-tips-and-pitfalls/count-bytes-stdin-async-iterable.js < 07-tips-and-pitfalls/assets/bigdata.csv 110 | ``` 111 | 112 | This prints: 113 | 114 | ```plain 115 | 332637 bytes read from stdin 116 | ``` 117 | 118 | So it works! 🎉 119 | 120 | Isn't it much nicer that our code reads more sequentially and that we didn't have to worry about remembering and wiring very specific events? We just wanted a simple and convenient way to _loop over_ the chunks as they became available and that's what we get with `for await ... of`. 121 | 122 | > **Note**: you can use `for await ... of` even with Transform streams. This is because transform streams have a both a Writable and a Readable part. When we try to use a Transform stream as an async iterable we are effectively reading data from the Readable part of the Transform stream. 123 | 124 | 125 | ## Streaming pipelines and handling backpressure 126 | 127 | Now let's say that we want to modify our count bytes example a little. This time while we count the bytes we also want to write the data coming from standard input to a file called `data.bin`. 128 | 129 | That doesn't seem too hard, so let's have a quick look at a possible implementation: 130 | 131 | ```js 132 | // copy-stdin.js 133 | import { createWriteStream } from 'node:fs' 134 | 135 | const dest = createWriteStream('data.bin') 136 | 137 | let bytes = 0 138 | for await (const chunk of process.stdin) { 139 | dest.write(chunk) 140 | bytes += chunk.length 141 | } 142 | dest.end() 143 | 144 | console.log(`${bytes} written into data.bin`) 145 | ``` 146 | 147 | We can test this code with our lovely `bigdata.csv`: 148 | 149 | ```bash 150 | node 07-tips-and-pitfalls/copy-stdin.js < 07-tips-and-pitfalls/assets/bigdata.csv 151 | ``` 152 | 153 | If all went according to plan, we should see the following output: 154 | 155 | ```plain 156 | 332637 written into data.bin 157 | ``` 158 | 159 | And we should have a new file called `data.bin` which contains a small (and fake) _bigdata_ treasure! 🤑 160 | 161 | If we have a second look at the code, the only thing to report is that every time we get a new chunk of data from standard input, we write it down to a file using a Writable file stream. We do that by calling `write()` on the stream object. 162 | 163 | If you know anything about Writable streams you should already be thinking about **backpressure**... 164 | 165 | Backpressure happens when the data source and the destination work at very different throughputs. If we can read data much faster than we can write, we might end up in trouble. If we are not careful we will accumulate all the data (pending write) in memory and we might just blow the process out of memory. 166 | 167 | Can this happen with our current example? Yes it can! Let's imagine we are saving `data.bin` to a very slow disk and that there is a lot of data (more than 2GB) coming through standard input. 168 | 169 | So how do we know if we are accumulating too much data in memory and how do we stop the processing until all the writes have been flushed? 170 | 171 | If we care to listen, the Writable stream can actually tell us if we are overloading it with too much data too fast! 172 | 173 | How does it do that? It's actually simple: every time we call `write()`, the method returns a boolean value. We can interpret that value as answering the question _is it ok to send you more data?_ If the returned value is `true` it's ok to do another call to `write()`. If the value is `false` then the stream is already _under pressure_ and we should stop until further notice. 174 | 175 | Ok, so if we see `false` we should stop, but how do we know when it's ok to start writing again? 176 | 177 | Again, the writable stream will tell us. But this is an event that will happen at some point in the future, once the stream has flushed all the data pending write and the process has reclaimed all the accumulated memory. So the way that the stream can inform that it is safe to restart writing is through an event, specifically an event called `drain`. 178 | 179 | So here's how we should update our script to handle backpressure correctly: 180 | 181 | ```js 182 | // copy-stdin-backpressure.js 183 | import { createWriteStream } from 'node:fs' 184 | import { once } from 'node:events' 185 | 186 | const dest = createWriteStream('data.bin') 187 | 188 | let bytes = 0 189 | for await (const chunk of process.stdin) { 190 | const canContinue = dest.write(chunk) 191 | bytes += chunk.length 192 | if (!canContinue) { 193 | // backpressure, now we stop and we need to wait for drain 194 | await once(dest, 'drain') 195 | // ok now it's safe to resume writing 196 | } 197 | } 198 | dest.end() 199 | 200 | console.log(`${bytes} written into data.bin`) 201 | ``` 202 | 203 | The main change here is that we capture the return statement from the call to `write()` in a variable called `canContinue`. If we cannot continue we have to _suspend_ the loop until it's safe to continue, that is until the stream emits a `drain` event. 204 | 205 | An interesting way to do that in a `for await ... of` loop is to use the [`once`](https://nodejs.org/api/events.html#emitteronceeventname-listener) utility from the core `events` module. `once` returns a promise that will resolve only when the specified event is emitted by the given event emitter. As the name suggests, `once` will listen only once for the event. Then it will detach its internal listener and resolve the `Promise` object that was originally returned. 206 | 207 | `once` allows us to simply `await` inside the `for await ... of` loop, which has the effect of _suspending_ the iteration until the `drain` event is emitted. 208 | 209 | Now that we know how to use async iterables and handle backpressure, let me tell you that for _complex enough_ pipelines (e.g. when you have more than 2 steps) it's much better (and simpler) to avoid async iterables and use the [`pipeline()`](https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback) function from the `stream/promise` module instead. 210 | 211 | Just to illustrate my point, let me show you an example. In the following example we are still counting bytes and saving the data to a file, but this time we also want to compress the data while saving it. 212 | 213 | 214 | 215 | ```js 216 | // copy-stdin-compress.js 217 | import { Transform } from 'node:stream' 218 | import { pipeline } from 'node:stream/promises' 219 | import { createWriteStream } from 'node:fs' 220 | import { createBrotliCompress } from 'node:zlib' 221 | 222 | class CountBytes extends Transform { 223 | // ... omitted for brevity 224 | } 225 | 226 | const compress = createBrotliCompress() 227 | const beforeCompression = new CountBytes() 228 | const afterCompression = new CountBytes() 229 | const destStream = createWriteStream('data.bin.br') 230 | 231 | await pipeline( 232 | process.stdin, 233 | beforeCompression, 234 | compress, 235 | afterCompression, 236 | destStream 237 | ) 238 | 239 | console.log(`Read ${beforeCompression.bytes} bytes and written ${afterCompression.bytes} bytes into "data.bin.br"`) 240 | ``` 241 | 242 | You can see how pipeline just connects all the different streams together and make the data flow. It will also handle backpressure correctly for us and return a `Promise` object that we can await to continue only when all the data has been consumed. 243 | 244 | We can run this example with: 245 | 246 | ```bash 247 | node 07-tips-and-pitfalls/copy-stdin-compress.js < 07-tips-and-pitfalls/assets/bigdata.csv 248 | ``` 249 | 250 | And this should output: 251 | 252 | ```plain 253 | Read 332637 bytes and written 79956 bytes into "data.bin.br" 254 | ``` 255 | 256 | Sweet, Brotli compression seems to work quite well with our awesome bigdata file! 🙂 257 | 258 | 259 | > **Note**: If streams are confusing you (yes, they can do that at first), you should really check out my [open source workshop about Node.js Streams](https://github.com/lmammino/streams-workshop) and take a deep dive on them. 260 | 261 | 262 | ### Converting Node.js event emitters to Async Iterable 263 | 264 | We can generalise what we have just discussed for Node.js streams to event emitters. 265 | 266 | Streams are event emitters after all: every time a new chunk is available the stream abstraction emits a `data` event. 267 | 268 | Node.js offers a great utility function to build async iterators on top of any event emitter that might emit the same type of event over time. 269 | 270 | This utility is the `on` function from the core module `events`. 271 | 272 | To illustrate how `on` works, we can use the example of the `glob` library (from npm). 273 | 274 | This library allows us to search for all files (and folders) matching a specific pattern. This library will traverse the file system recursively to find matches to our patterns. Every time there is a new match the `matcher` instance will emit a `match` event, which contains the path of the matched file. 275 | 276 | 277 | ```js 278 | // find-js-files.js 279 | import { on } from 'node:events' 280 | import glob from 'glob' // from npm 281 | 282 | const matcher = glob('**/*.js') 283 | 284 | for await (const [filePath] of on(matcher, 'match')) { 285 | console.log(filePath) 286 | } 287 | ``` 288 | 289 | If we run this code we should see a lot of files from this project (and the relative `node_modules` folder): 290 | 291 | ```plain 292 | 01-intro/for-in-debug.js 293 | 01-intro/for-in-object.js 294 | 01-intro/for-in.js 295 | 01-intro/for-of-map.js 296 | 01-intro/for-of-object-entries.js 297 | // ... 298 | node_modules/acorn-jsx/index.js 299 | node_modules/acorn-jsx/xhtml.js 300 | node_modules/aggregate-error/index.js 301 | node_modules/ansi-regex/index.js 302 | node_modules/ansi-styles/index.js 303 | // ... 304 | ``` 305 | 306 | > **🎭 PLAY** 307 | > Do you want to know how many JavaScript files do you have in this folder alone? Just run: `node 07-tips-and-pitfalls/find-js-files.js | wc -l` and try not to be shocked by the result! 😅 308 | 309 | 310 | Using `on` and `for await ... of` might look cool and very convenient, but be careful because this code will never exit from the loop. 311 | 312 | In fact, the following code might now behave as you might expect: 313 | 314 | ```js 315 | import { on } from 'node:events' 316 | import glob from 'glob' // from npm 317 | 318 | const matcher = glob('**/*.js') 319 | 320 | for await (const [filePath] of on(matcher, 'match')) { 321 | console.log(filePath) 322 | } 323 | 324 | // ⚠️ DANGER, DANGER (high voltage ⚡️): We'll never get here! 325 | console.log('ALL DONE! :)') 326 | ``` 327 | 328 | To handle termination of the loop correctly we would need to have a way to _signal_ that `match` events won't happen anymore. 329 | 330 | We could do that as follows: 331 | 332 | ```js 333 | // find-js-files-abort.js 334 | import { on } from 'node:events' 335 | import glob from 'glob' 336 | 337 | const matcher = glob('**/*.js') 338 | const ac = new global.AbortController() 339 | 340 | matcher.once('end', () => ac.abort()) 341 | 342 | try { 343 | for await (const [filePath] of on(matcher, 'match', { signal: ac.signal })) { 344 | console.log(filePath) 345 | } 346 | } catch (err) { 347 | if (!ac.signal.aborted) { 348 | console.error(err) 349 | process.exit(1) 350 | } 351 | // we ignore the AbortError 352 | } 353 | 354 | console.log('NOW WE ARE GETTING HERE! :)') // YAY! 😻 355 | ``` 356 | 357 | As you can see, `on` allows us to pass a _signal_ from an `AbortController`. When `on` receives the _abort_ signal from the controller, it just throws an `AbortError` which effectively stops the loop. We need to make sure we have a `try/catch` block and handle the error accordingly. 358 | 359 | If this seems like a lot of work... well, I won't argue, it is a lot of work! So consider this carefully, sometimes you might just better off using event listeners directly: 360 | 361 | ```js 362 | // find-js-files-listeners.js 363 | import glob from 'glob' 364 | 365 | const matcher = glob('**/*.js') 366 | matcher.on('match', console.log) 367 | matcher.on('end', () => console.log('All completed!')) 368 | ``` 369 | 370 | Hard truth: `async/await` doesn't always lead to the nicest code! It's up to you to pick the best abstaction and the best coding style for the problem at hand when it comes to JavaScript! 371 | 372 | 373 | > **Note**: If you know ahead of time how many events you need to process you can also use a `break` in the `for ... await` loop. 374 | 375 | 376 | ### Using async iterators to handle web requests 377 | 378 | If you have ever done any web server with [Deno](https://deno.land) you might have seen something like this: 379 | 380 | ```js 381 | const server = Deno.listen({ port: 8080 }) 382 | 383 | for await (const conn of server) { 384 | // ...handle the connection... 385 | } 386 | ``` 387 | 388 | > **Warning**: the code above works only with Deno, not with Node.js 389 | 390 | Yes, a `for await ... of` to handle incoming requests. At this point of the workshop, you should know that this means that a `server` is an Async Iterator in Deno. 391 | 392 | If that sounds somewhat nice, you might be wondering whether we could do the same with Node.js... 393 | 394 | Well, hold my drink! 🍺 395 | 396 | ```js 397 | // http-for-await.js 398 | import { createServer } from 'node:http' 399 | import { on } from 'node:events' 400 | 401 | const server = createServer() 402 | server.listen(8000) 403 | 404 | for await (const [req, res] of on(server, 'request')) { 405 | res.end('hello dear friend') 406 | } 407 | ``` 408 | 409 | So, in Node.js a `server` is not an async iterator, but we know that a `server` emits a `request` every time there is an incoming request. 410 | 411 | We know that we can use the `on` utility from the `events` module to create an async iterator from an event emitter (in this case for `server` using the `request` event). 412 | 413 | This allows us to use a `for await ... of` to handle incoming requests. 414 | 415 | Ok, does this actually work? 416 | 417 | To try that out we just need to run: 418 | 419 | ```bash 420 | node 07-tips-and-pitfalls/http-for-await.js 421 | ``` 422 | 423 | And then (in another terminal): 424 | 425 | ```bash 426 | curl -v http://localhost:8000 427 | ``` 428 | 429 | And, if all went well, we should see something like this: 430 | 431 | ```plain 432 | * Trying 127.0.0.1:8000... 433 | * Connected to localhost (127.0.0.1) port 8000 (#0) 434 | > GET / HTTP/1.1 435 | > Host: localhost:8000 436 | > User-Agent: curl/7.79.1 437 | > Accept: */* 438 | > 439 | * Mark bundle as not supporting multiuse 440 | < HTTP/1.1 200 OK 441 | < Date: Fri, 20 May 2022 10:47:12 GMT 442 | < Connection: keep-alive 443 | < Keep-Alive: timeout=5 444 | < Content-Length: 17 445 | < 446 | * Connection #0 to host localhost left intact 447 | hello dear friend 448 | ``` 449 | 450 | So, this seems to work as expected! 💪 451 | 452 | 453 | But... wait, aren't we processing all requests in series, now? 🤔 454 | 455 | If we are actually doing that we are basically killing the performance of our server because we are only serving 1 request at the time! 😱 456 | 457 | That's scary, so let's do something to test our web server. 458 | 459 | One easy thing we can do is to add an artificial delay of 1 second before we send back our response to the user. 460 | 461 | If we are processing request in series that means we won't be able to serve more than 1 request per second, so once we do that we can use [`autocannon`](https://github.com/mcollina/autocannon) to verify our server throughput and draw some conclusions: 462 | 463 | ```js 464 | // http-for-await-delay.js 465 | import { createServer } from 'node:http' 466 | import { on } from 'node:events' 467 | import { setTimeout } from 'node:timers/promises' 468 | 469 | const server = createServer() 470 | server.listen(8000) 471 | 472 | for await (const [req, res] of on(server, 'request')) { 473 | await setTimeout(1000) 474 | res.end('hello dear friend') 475 | } 476 | ``` 477 | 478 | Nothing too crazy here, so let's benchmark this implementation with: 479 | 480 | ```bash 481 | npx autocannon --on-port / -- node 07-tips-and-pitfalls/http-for-await-delay.js 482 | ``` 483 | 484 | This command launches our server application and once it's up and running it starts to fire requests at it. 485 | 486 | By default `autocannon` creates 10 different clients trying to send as many requests as possible within 10 seconds. 487 | 488 | Once this is done we should see a table like this: 489 | 490 | ```plain 491 | Running 10s test @ http://localhost:8000/ 492 | 10 connections 493 | 494 | 495 | ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ 496 | │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ 497 | ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ 498 | │ Latency │ 1044 ms │ 5045 ms │ 9057 ms │ 9057 ms │ 5046.78 ms │ 2586.69 ms │ 9057 ms │ 499 | └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ 500 | ┌───────────┬─────┬──────┬───────┬───────┬───────┬───────┬───────┐ 501 | │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ 502 | ├───────────┼─────┼──────┼───────┼───────┼───────┼───────┼───────┤ 503 | │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.9 │ 0.3 │ 1 │ 504 | ├───────────┼─────┼──────┼───────┼───────┼───────┼───────┼───────┤ 505 | │ Bytes/Sec │ 0 B │ 0 B │ 140 B │ 140 B │ 126 B │ 42 B │ 140 B │ 506 | └───────────┴─────┴──────┴───────┴───────┴───────┴───────┴───────┘ 507 | 508 | Req/Bytes counts sampled once per second. 509 | # of samples: 10 510 | 511 | 20 requests in 10.06s, 1.26 kB read 512 | 1 errors (1 timeouts) 513 | ``` 514 | 515 | We are processing an average of 1 req/sec... YES THIS IS BAD! 🙈 516 | 517 | Just for the sake of comparison, let's see a more traditional implementation of a web server and how it performs against the same benchmark: 518 | 519 | ```js 520 | // http-delay-traditional.js 521 | import { createServer } from 'node:http' 522 | import { setTimeout } from 'node:timers/promises' 523 | 524 | const server = createServer(async function (req, res) { 525 | await setTimeout(1000) 526 | res.end('hello dear friend') 527 | }) 528 | 529 | server.listen(8000) 530 | ``` 531 | 532 | No `for await ... of` here, we just pass a good hold handler function to `createServer`. Let's run the benchmarks: 533 | 534 | ```bash 535 | npx autocannon --on-port / -- node 07-tips-and-pitfalls/http-delay-traditional.js 536 | ``` 537 | 538 | This benchmark should print something similar to this: 539 | 540 | ```plain 541 | Running 10s test @ http://localhost:8000/ 542 | 10 connections 543 | 544 | 545 | ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬──────────┬─────────┐ 546 | │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ 547 | ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼──────────┼─────────┤ 548 | │ Latency │ 1009 ms │ 1018 ms │ 1056 ms │ 1059 ms │ 1026.42 ms │ 14.38 ms │ 1059 ms │ 549 | └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴──────────┴─────────┘ 550 | ┌───────────┬─────┬──────┬────────┬────────┬─────────┬───────┬────────┐ 551 | │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ 552 | ├───────────┼─────┼──────┼────────┼────────┼─────────┼───────┼────────┤ 553 | │ Req/Sec │ 0 │ 0 │ 10 │ 10 │ 9 │ 3 │ 10 │ 554 | ├───────────┼─────┼──────┼────────┼────────┼─────────┼───────┼────────┤ 555 | │ Bytes/Sec │ 0 B │ 0 B │ 1.4 kB │ 1.4 kB │ 1.26 kB │ 420 B │ 1.4 kB │ 556 | └───────────┴─────┴──────┴────────┴────────┴─────────┴───────┴────────┘ 557 | 558 | Req/Bytes counts sampled once per second. 559 | # of samples: 10 560 | 561 | 100 requests in 10.06s, 12.6 kB read 562 | ``` 563 | 564 | We can see here that we have a much better situation now. 565 | 566 | We have 10 clients and we are artificially forcing every request to take 1 second. The maximum number of requests we could achieve in this scenario is 10 req/sec. 567 | Here we are very close to that amount in average! 568 | 569 | > **🎭 PLAY** 570 | > If you want to see what happens with more clients, you can run `autocannon` with the option `-c X` (where X is the number of clients). 571 | > Why don't you try to run the benchmarks for both implementation with many clients. Do you see any interesting pattern? 572 | 573 | Does this mean that we cannot use this `for await ... of` approach for web servers? 574 | 575 | Well not really... 576 | 577 | The trick is not to use `await` inside the loop: 578 | 579 | 580 | ```js 581 | import { createServer } from 'node:http' 582 | import { on } from 'node:events' 583 | 584 | const server = createServer() 585 | server.listen(8000) 586 | 587 | for await (const [req, res] of on(server, 'request')) { 588 | // ⚠️ ... AS LONG AS WE DON'T USE await HERE, WE ARE FINE! 589 | } 590 | ``` 591 | 592 | If you really want to do something `async` in the handling logic, you could call another async function from within the loop and pass `req` and `res` or you can do something like this: 593 | 594 | ```js 595 | // http-for-await-delay-fixed.js 596 | import { createServer } from 'node:http' 597 | import { on } from 'node:events' 598 | import { setTimeout } from 'node:timers/promises' 599 | 600 | const server = createServer() 601 | server.listen(8000) 602 | 603 | for await (const [req, res] of on(server, 'request')) { 604 | (async function () { 605 | await setTimeout(1000) 606 | res.end('hello dear friend') 607 | })() 608 | } 609 | ``` 610 | 611 | With this approach we are not waiting for the promise representing the current request to settle before we start processing the next one, which means we are not processing requests in series anymore! Requests being processed concurrently is exactly what you want in a web server! 😌 612 | 613 | > **🎭 PLAY** 614 | > What numbers do we expect to see if we benchmark this new implementation? You should try that out by running: `npx autocannon --on-port / -- node 07-tips-and-pitfalls/http-for-await-delay-fixed.js`. 615 | 616 | Ok, problem solved! 🍻 617 | 618 | But... if you want my 2 cents, this is not necessarily pretty and we have seen that it can be quite dangerous: a careless handling of `async` can easily destroy the performance of you server, so it's probably better to stick with handler functions when it comes to web servers! 619 | 620 | **Let's not be tempted by the patterns of the dinosaur!** 🦕 (Sorry, Deno... 😇) 621 | 622 | 623 | ## Exercises 624 | 625 | I'll spare you from exercises in this chapter. But, if you really want to keep coding, check out some suggestions in the [next and final chapter](/08-exercises/README.md)! 626 | 627 | 628 | ## Summary 629 | 630 | In this chapter we learned a few interesting tips, tricks and common gotchas! Here's a summary: 631 | 632 | - You can check if an object is an iterable by checking `typeof obj[Symbol.iterator] === 'function'` 633 | - Similarly you can check if an object is an **async** iterable with `typeof obj[Symbol.asyncIterator] === 'function'` 634 | - In both cases, there's no guarantee that the iterable protocol is implemented correctly (the function might not return an iterator 😥) 635 | - Node.js Readable streams are also async iterable objects, so you could use `for await ... of` to consume the data in chunks 636 | - If you do that and you end up writing data somewhere else, you'll need to handle backpressure yourself. It might be better to use `pipeline()` instead. 637 | - You can convert Node.js event emitters to async iterable objects by using the `on` function from the module `events`. 638 | - You can use this trick to handle requests to a web server using `for await ... of`, but that's not necessarily a great idea! 639 | 640 | That's all for now, congratulations on finishing the sixt and last chapter! 🎉 641 | 642 | Take a little break and get ready to explore what you could learn next and maybe attempt cracking some more challenging [exercises](/08-exercises/README.md). 643 | 644 | --- 645 | 646 | | [⬅️ 06 - Async Iterable protocol](/06-async-iterable-protocol/README.md) | [🏠](/README.md)| [08 - Exercises ➡️](/08-exercises/README.md)| 647 | |:--------------|:------:|------------------------------------------------:| 648 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/copy-stdin-backpressure.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'node:fs' 2 | 3 | const dest = createWriteStream('data.bin') 4 | 5 | let bytes = 0 6 | for await (const chunk of process.stdin) { 7 | dest.write(chunk) 8 | bytes += chunk.length 9 | } 10 | dest.end() 11 | 12 | console.log(`${bytes} written into data.bin`) 13 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/copy-stdin-compress.js: -------------------------------------------------------------------------------- 1 | import { Transform } from 'node:stream' 2 | import { pipeline } from 'node:stream/promises' 3 | import { createWriteStream } from 'node:fs' 4 | import { createBrotliCompress } from 'node:zlib' 5 | 6 | class CountBytes extends Transform { 7 | constructor (options) { 8 | super(options) 9 | this.bytes = 0 10 | } 11 | 12 | _transform (chunk, _enc, done) { 13 | this.bytes += chunk.length 14 | this.push(chunk) 15 | done() 16 | } 17 | } 18 | 19 | const compress = createBrotliCompress() 20 | const beforeCompression = new CountBytes() 21 | const afterCompression = new CountBytes() 22 | const destStream = createWriteStream('data.bin.br') 23 | 24 | await pipeline( 25 | process.stdin, 26 | beforeCompression, 27 | compress, 28 | afterCompression, 29 | destStream 30 | ) 31 | 32 | console.log(`Read ${beforeCompression.bytes} bytes and written ${afterCompression.bytes} bytes into "data.bin.br"`) 33 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/copy-stdin.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'node:fs' 2 | import { once } from 'node:events' 3 | 4 | const dest = createWriteStream('data.bin') 5 | 6 | let bytes = 0 7 | for await (const chunk of process.stdin) { 8 | const canContinue = dest.write(chunk) 9 | bytes += chunk.length 10 | if (!canContinue) { 11 | // backpressure, now we stop and we need to wait for drain 12 | await once(dest, 'drain') 13 | // ok now it's safe to resume writing 14 | } 15 | } 16 | dest.end() 17 | 18 | console.log(`${bytes} written into data.bin`) 19 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/count-bytes-stdin-async-iterable.js: -------------------------------------------------------------------------------- 1 | let bytes = 0 2 | 3 | for await (const chunk of process.stdin) { 4 | bytes += chunk.length 5 | } 6 | 7 | console.log(`${bytes} bytes read from stdin`) 8 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/count-bytes-stdin.js: -------------------------------------------------------------------------------- 1 | let bytes = 0 2 | // process.stdin is a Readable stream! 3 | process.stdin.on('data', (chunk) => { 4 | bytes += chunk.length 5 | }) 6 | process.stdin.on('end', () => { 7 | console.log(`${bytes} bytes read from stdin`) 8 | }) 9 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/find-js-files-abort.js: -------------------------------------------------------------------------------- 1 | import { on } from 'node:events' 2 | import glob from 'glob' 3 | 4 | const matcher = glob('**/*.js') 5 | const ac = new global.AbortController() 6 | 7 | matcher.once('end', () => ac.abort()) 8 | 9 | try { 10 | for await (const [filePath] of on(matcher, 'match', { signal: ac.signal })) { 11 | console.log(filePath) 12 | } 13 | } catch (err) { 14 | if (!ac.signal.aborted) { 15 | console.error(err) 16 | process.exit(1) 17 | } 18 | // we ignore the AbortError 19 | } 20 | 21 | console.log('NOW WE ARE GETTING HERE! :)') // YAY! 😻 22 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/find-js-files-listeners.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | 3 | const matcher = glob('**/*.js') 4 | matcher.on('match', console.log) 5 | matcher.on('end', () => console.log('All completed!')) 6 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/find-js-files.js: -------------------------------------------------------------------------------- 1 | import { on } from 'node:events' 2 | import glob from 'glob' 3 | 4 | const matcher = glob('**/*.js') 5 | 6 | for await (const [filePath] of on(matcher, 'match')) { 7 | console.log(filePath) 8 | } 9 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/http-delay-traditional.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { setTimeout } from 'node:timers/promises' 3 | 4 | const server = createServer(async function (req, res) { 5 | await setTimeout(1000) 6 | res.end('hello dear friend') 7 | }) 8 | 9 | server.listen(8000) 10 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/http-for-await-delay-fixed.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { on } from 'node:events' 3 | import { setTimeout } from 'node:timers/promises' 4 | 5 | const server = createServer() 6 | server.listen(8000) 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | for await (const [req, res] of on(server, 'request')) { 10 | (async function () { 11 | await setTimeout(1000) 12 | res.end('hello dear friend') 13 | })() 14 | } 15 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/http-for-await-delay.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { on } from 'node:events' 3 | import { setTimeout } from 'node:timers/promises' 4 | 5 | const server = createServer() 6 | server.listen(8000) 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | for await (const [req, res] of on(server, 'request')) { 10 | await setTimeout(1000) 11 | res.end('hello dear friend') 12 | } 13 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/http-for-await.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http' 2 | import { on } from 'node:events' 3 | 4 | const server = createServer() 5 | server.listen(8000) 6 | 7 | // eslint-disable-next-line no-unused-vars 8 | for await (const [req, res] of on(server, 'request')) { 9 | res.end('hello dear friend') 10 | } 11 | -------------------------------------------------------------------------------- /07-tips-and-pitfalls/iterable-check.js: -------------------------------------------------------------------------------- 1 | function isIterable (obj) { 2 | return typeof obj[Symbol.iterator] === 'function' 3 | } 4 | 5 | const array = [1, 2, 3] 6 | console.log(array, isIterable(array)) // true 7 | 8 | const genericObj = { foo: 'bar' } 9 | console.log(genericObj, isIterable(genericObj)) // false 10 | console.log(Object.entries(genericObj), isIterable(Object.entries(genericObj))) // true 11 | 12 | const fakeIterable = { 13 | [Symbol.iterator] () { return 'notAnIterator' } 14 | } 15 | console.log(fakeIterable, isIterable(fakeIterable)) // true 😡 16 | -------------------------------------------------------------------------------- /08-exercises/README.md: -------------------------------------------------------------------------------- 1 | # 08 - Exercises 2 | 3 | In this last section you will find some ideas of small projects and exercises that you can build to practice your acquired knowledge of JavaScript iteration protocols. 4 | 5 | ## Exercises 6 | 7 | For the following exercises you don't get a testing suite nor a solution, so be creative and have fun! 8 | 9 | Feel free to share your solutions on GitHub and let me know on [Twitter](https://twitter.com/loige). 10 | 11 | 12 | ### 08.01 Lines iterator 13 | 14 | Implement an iterable object that, given a text, will yield a line of that input text for every iteration. 15 | 16 | For instance: 17 | 18 | ```js 19 | const text = `I've got another confession to make 20 | I'm your fool 21 | Everyone's got their chains to break 22 | Holding you` 23 | 24 | const lines = linesIter(text) 25 | lines.next() // { done: false, value: 'I\'ve got another confession to make' } 26 | lines.next() // { done: false, value: 'I\'m your fool' } 27 | lines.next() // { done: false, value: 'Everyone\'s got their chains to break' } 28 | lines.next() // { done: false, value: 'Holding you' } 29 | lines.next() // { done: true, value: undefined } 30 | ``` 31 | 32 | 33 | ### 08.02 Incremental find 34 | 35 | Create an iterable object that allows you to search an occurrence of a certain keyword in a given text. The search should be lazy and give you one occurrence per every iteration. Every occurrence should be represented by the line number and the initial position of the match in that line (column index). 36 | 37 | For instance, if you have the following text: 38 | 39 | ```js 40 | const text = `Were you born to resist or be abused 41 | Is someone getting the best, the best, the best, the best of you 42 | Is someone getting the best, the best, the best, the best of you` 43 | ``` 44 | 45 | We might want to search for the word `best` as follows: 46 | 47 | ```js 48 | const finder = find(text, 'best') 49 | finder.next() // { done: false, value: { line: 1, col: 24 } } 50 | finder.next() // { done: false, value: { line: 1, col: 34 } } 51 | finder.next() // { done: false, value: { line: 1, col: 44 } } 52 | finder.next() // { done: false, value: { line: 1, col: 54 } } 53 | finder.next() // { done: false, value: { line: 2, col: 24 } } 54 | finder.next() // { done: false, value: { line: 2, col: 34 } } 55 | finder.next() // { done: false, value: { line: 2, col: 44 } } 56 | finder.next() // { done: false, value: { line: 2, col: 54 } } 57 | finder.next() // { done: true, value: undefined } 58 | ``` 59 | 60 | Can you implement the `find` function to work as described in this example? 61 | 62 | 63 | ### 08.03 Enumerate utility 64 | 65 | Python, Rust and other languages have an interesting utility when it comes to iterators: the `enumerate` utility. 66 | 67 | Let's a Python example to illustrate how `enumerate()` works: 68 | 69 | ```python 70 | values = ['a','b','c'] 71 | for count, value in enumerate(values): 72 | print(count, value) 73 | ``` 74 | 75 | This code prints: 76 | 77 | ```plain 78 | 0 a 79 | 1 b 80 | 2 c 81 | ``` 82 | 83 | Unfortunately `enumerate` does not exist in JavaScript, but based on what we learned in this workshop we should be able to implement it ourselves, right? 84 | 85 | And yes, ideally our implementation of `enumerate` should be lazy: it should take an iterable object as input and produce a new iterable object. 86 | 87 | 88 | ### 08.04 Map utility 89 | 90 | Python, Rust and other languages also have another interesting utility called `map`. Map allows you to use an arbitraty function to change the values produced by an iterable as you iterate through them. 91 | 92 | Let's see a Python example to clarify what we mean: 93 | 94 | ```python 95 | def square(number): 96 | return number ** 2 97 | 98 | numbers = [1, 2, 3, 4] 99 | squared = map(square, numbers) 100 | 101 | for num in squared: 102 | print(num) 103 | ``` 104 | 105 | This will print: 106 | 107 | ```plain 108 | 1 109 | 4 110 | 9 111 | 16 112 | ``` 113 | 114 | At a first glance, you might think that this is not too different from [`Array.map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) in JavaScript. The problem is that `Array.map()` only works with arrays and not with generic iterable objects (it's not lazy). 115 | 116 | We want to implement something more generic that works with iterables: it takes an iterable object and produces a new iterable object, like in Python! 117 | 118 | 119 | ### 08.05 Async map 120 | 121 | Can we implement the same `map()` utility as in the previous exercise, but this time make it work with **async iterable** objects like Readable streams? 122 | 123 | We want something that could allow us to do this: 124 | 125 | ```js 126 | import { createReadStream } from 'node:fs' 127 | 128 | const asyncMap = async function * (stream, transform) { 129 | // TODO 130 | } 131 | 132 | const textStream = createReadStream('some-file.txt') 133 | const uppercasifiedStream = asyncMap(textStream, (chunk) => chunk.toString().toUpperCase()) 134 | 135 | for await (const chunk of uppercasifiedStream) { 136 | console.log(chunk) 137 | } 138 | ``` 139 | 140 | Can you complete the implementation of `asyncMap`? 141 | 142 | 143 | ### 08.06 Re-implement `on` from `events` 144 | 145 | We have already discussed how `on` from the core module `events` work. Would you be able to re-implement it from scratch by yourself? 146 | 147 | If you really have no clue, you could always have a peek at [the official implementation](https://github.com/nodejs/node/blob/bd86e5186a33803aa9283b9a4c6946da33b67511/lib/events.js#L1012-L1137), or run the following snippet in a Node.js shell: 148 | 149 | ```js 150 | const { on } = require('events') 151 | console.log(on.toString()) 152 | ``` 153 | 154 | Well, if you think that's a lot of code, try to implement a simplified version that doesn't care about error handling or abort signals. 155 | 156 | 157 | ### 08.07 Events debounce 158 | 159 | Let's say you want to listen to events, but if they happen too often you should ignore them. This is called **debouncing** and it something common in the frontend world, but it might also be useful on the backend. For instance, let's say that we have a `sensor` object emitting readings frequently (once every 100ms) and that we only want to log them at most once per second: 160 | 161 | ```js 162 | import { EventEmitter } from 'node:events' 163 | 164 | const sensor = new EventEmitter() 165 | setInterval(() => sensor.emit('reading', Math.random()), 100) 166 | 167 | async function * debounce (sensor, event = 'reading', everyMs = 1000) { 168 | // TODO ... 169 | } 170 | 171 | for await (const reading of debounce(sensor)) { 172 | console.log(reading) 173 | } 174 | ``` 175 | 176 | Can you complete the implementation of `debounce`? 177 | 178 | 179 | ## Where to go from here 180 | 181 | I hope you had fun with this workshop and that you also acquired some new practical learnings that you can bring to your next project. 182 | 183 | If you want to keep learning about JavaScript, Node.js and iteration protocols, these are some interesting resources: 184 | 185 | - [Node.js Design Patterns, Third Edition](https://www.nodejsdesignpatterns.com/): has an entire section dedicated to iteration protocols and related design patterns (disclaimer: I co-authored this book!) 186 | - [JavaScript Async Iterator](https://www.nodejsdesignpatterns.com/blog/javascript-async-iterators/): An article that recaps most of the topics discussed in this workshop. 187 | - Learn [how I found a lost song using JavaScript and Async Iterators](https://youtu.be/uTzBHPpMEhA) (talk from NodeConfRemote 2021). You can also look at [the slides](https://loige.link/nodeconf-iter). 188 | - [Official (sync) Iteration protocols documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols): the goto place to make sure you didn't miss anything important about iteration protocols or just to quickly review something you don't remember. 189 | - [Official documentation about Async iterators and iterable on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator): Similar to the above but covers some details about the async counterparts of the iterator and iterable protocols. 190 | - [Official documentation about `for await ... of` on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of): Gives you some more details about this syntax and when you can use it. 191 | 192 | Let me know what you are going to build with your new acquired knowledge on [Twitter](https://twitter.com/loige), and you found this useful, please give it a star ⭐️ and share it with your friends and colleagues! ❤️ 193 | 194 | Thank you! 👋 195 | 196 | --- 197 | 198 | | [⬅️ 07 - Tips and Pitfalls](/07-tips-and-pitfalls/README.md) | [🏠](/README.md)| - | 199 | |:--------------|:------:|------------------------------------------------:| -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2022 Luciano Mammino 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Iteration protocol workshop 2 | 3 | [![Node.js CI](https://github.com/lmammino/iteration-protocols-workshop/actions/workflows/node.js.yml/badge.svg)](https://github.com/lmammino/iteration-protocols-workshop/actions/workflows/node.js.yml) 4 | 5 | A workshop about JavaScript iteration protocols: iterator, iterable, async iterator, async iterable by [@loige](https://twitter.com/loige). 😎 6 | 7 | > I also have a workshop about [Node.js Streams](https://github.com/lmammino/streams-workshop). 8 | 9 | 10 | ## Prerequisites 11 | 12 | Before getting started, make sure you have the following installed: 13 | 14 | - Node.js 16+ 15 | - NPM 8+ 16 | - A text editor of your choice 17 | - A bash-compatible shell 18 | 19 | 20 | ## Getting started 21 | 22 | Clone the repository and run `npm install` to get all the necessary dependencies. 23 | 24 | The workshop is divided in chapters and the first chapter starts at [01-intro](/01-intro/README.md). 25 | 26 | Every chapter will teach you a specific iteration concept and offer you some examples and exercises to familiarize with that concept. 27 | 28 | You will often find 2 different types of interactive actions: 29 | 30 | - 🎭 **PLAY** : commands or instructions you should spend some time with to get familiar with some concepts or APIs 31 | 32 | - 🏹 **Exercise**: when you have to use some of the concepts you just learned to solve a programming problem. Generally every exercise will have a test that you can run to validate your solution. 33 | 34 | Enjoy! 🙃 35 | 36 | [➡️ GET STARTED](/01-intro/README.md). 37 | 38 | 39 | ## Shameless plug 😇 40 | 41 | 42 | 43 | If you like this piece of work, consider supporting me by getting a copy of [Node.js Design Patterns, Third Edition](https://www.nodejsdesignpatterns.com/), which also goes into great depth about generators, iterator protocols, streams and related design patterns. 44 | 45 | If you already have this book, **please consider writing a review** on Amazon, Packt, GoodReads or in any other review channel that you generally use. That would support us greatly 🙏. 46 | 47 | 48 | ## Contributing 49 | 50 | In the spirit of Open Source, everyone is very welcome to contribute to this project. 51 | You can contribute just by submitting bugs or suggesting improvements by 52 | [opening an issue on GitHub](https://github.com/lmammino/iteration-protocols-workshop/issues) or by [submitting a PR](https://github.com/lmammino/iteration-protocols-workshop/pulls). 53 | 54 | 55 | ## License 56 | 57 | Licensed under [MIT License](LICENSE). © Luciano Mammino. 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iteration-protocols-workshop", 3 | "version": "1.0.0", 4 | "description": "A workshop about JavaScript iteration protocols: iterator, iterable, async iterator, async iterable", 5 | "main": "index.js", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=16.14", 9 | "npm": ">=8.3" 10 | }, 11 | "engineStrict": true, 12 | "scripts": { 13 | "test:lint": "eslint .", 14 | "test:unit": "TEST_SOLUTIONS=true tap --no-coverage --jobs=1 --reporter=spec */**/exercises/*.test.js", 15 | "test": "npm run test:lint && npm run test:unit", 16 | "ex": "tap --no-coverage --jobs=1 --reporter=spec" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/lmammino/iteration-protocols-workshop.git" 21 | }, 22 | "keywords": [], 23 | "author": "Luciano Mammino", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/lmammino/iteration-protocols-workshop/issues" 27 | }, 28 | "homepage": "https://github.com/lmammino/iteration-protocols-workshop#readme", 29 | "devDependencies": { 30 | "axios": "^0.27.2", 31 | "eslint": "^8.24.0", 32 | "eslint-config-standard": "^17.0.0", 33 | "eslint-plugin-import": "^2.26.0", 34 | "eslint-plugin-n": "^15.3.0", 35 | "eslint-plugin-promise": "^6.0.1", 36 | "nock": "^13.2.9", 37 | "node-fetch": "^3.2.10", 38 | "tap": "^16.3.0", 39 | "undici": "^5.10.0" 40 | }, 41 | "dependencies": { 42 | "glob": "^8.0.3" 43 | } 44 | } 45 | --------------------------------------------------------------------------------