diff --git a/concepts/randomness/.meta/config.json b/concepts/randomness/.meta/config.json new file mode 100644 index 0000000000..1b2ce91b6f --- /dev/null +++ b/concepts/randomness/.meta/config.json @@ -0,0 +1,5 @@ +{ + "blurb": "Random number generation using Math.random()", + "authors": ["SneakyMallard"], + "contributors": ["SleeplessByte"] +} diff --git a/concepts/randomness/about.md b/concepts/randomness/about.md new file mode 100644 index 0000000000..d09087ffd8 --- /dev/null +++ b/concepts/randomness/about.md @@ -0,0 +1,61 @@ +# Introduction + +Many programs need (pseudo-)random values to simulate real-world events. + +Common, familiar examples include: + +- A coin toss: a random value from ('H', 'T'). +- The roll of a die: a random integer from 1 to 6. +- Shuffling a deck of cards: a random ordering of a card list. +- The creation of trees and bushes in a 3-D graphics simulation. + +Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators. + + +~~~exercism/advanced +[The language specification][spec] for JavaScript doesn't force the implementation for random number generation. +All major browsers and JavaScript runtimes implement a PRNG (pseudo-random number generator). +Because the numbers are not cryptographically secure, they should never be used for anything that requires true or at least cryptographically secure random numbers, such as certificate or password generation or operations. + +There is a standard called [Web Cryptography][rfc] which standardizes an interface for doing cryptography in JavaScript. +It is implemented [by Browsers][crypto-web] as well as runtimes such as [Node.JS][crypto-node] and [Deno][crypto-deno]. + +This concept is not about Web Crypto and will restrict itself to pseudo-random number generation. + +[rfc]: https://www.w3.org/TR/webcrypto-2/ +[spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.random +[crypto-web]: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues +[crypto-node]: https://nodejs.org/api/webcrypto.html#cryptogetrandomvaluestypedarray +[crypto-deno]: https://docs.deno.com/api/web/~/Crypto +~~~ + +## Generating random numbers + +In Javascript, you can generate psuedorandom numbers using the [`Math.random()`][Math.random] function. +It will return a psuedorandom floating-point number between 0 (inclusive), and 1 (exclusive). + +To get a random number between _min_ (inclusive) and _max_ (exclusive) you can use a function something like this: + +```javascript +function getRandomInRange(min, max) { + return min + Math.random() * (max - min); +} +getRandomInRange(4, 10); +// => 5.72 +``` + + +~~~exercism/advanced +Most simple techniques of returning a range of numbers based on the randomly generated number [will introduce bias][bias]. +That means that some numbers will be more likely to be rolled than others. +Using the multiplication technique spreads out the bias over the entire range, so it will be less obvious and in most cases not a big issue, but you should be aware of this. + +[bias]: https://adammil.net/blog/v134_Efficiently_generating_random_numbers_without_bias.html +~~~ + +## Generating random integers + +To generate a random integer, you can use `Math.floor()` or `Math.ceil()` to turn a randomly generated number into an integer. + +[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random +[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random diff --git a/concepts/randomness/introduction.md b/concepts/randomness/introduction.md new file mode 100644 index 0000000000..79b03703b6 --- /dev/null +++ b/concepts/randomness/introduction.md @@ -0,0 +1,40 @@ +# Introduction + +Many programs need (pseudo-)random values to simulate real-world events. + +Common, familiar examples include: + +- A coin toss: a random value from ('H', 'T'). +- The roll of a die: a random integer from 1 to 6. +- Shuffling a deck of cards: a random ordering of a card list. +- The creation of trees and bushes in a 3-D graphics simulation. + +Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators. + + +~~~exercism/caution +The `Math.random()` function should NOT be used for security and cryptographic applications! +Finish the learning exercise(s) about this concept to learn more +~~~ + +## Generating random numbers + +In Javascript, you can generate psuedorandom numbers using the [`Math.random()`][Math.random] function. +It will return a psuedorandom floating-point number between 0 (inclusive), and 1 (exclusive). + +To get a random number between _min_ (inclusive) and _max_ (exclusive) you can use a function something like this: + +```javascript +function getRandomInRange(min, max) { + return min + Math.random() * (max - min); +} +getRandomInRange(4, 10); +// => 5.72 +``` + +## Generating random integers + +To generate a random integer, you can use `Math.floor()` or `Math.ceil()` to turn a randomly generated number into an integer. + +[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random +[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random diff --git a/concepts/randomness/links.json b/concepts/randomness/links.json new file mode 100644 index 0000000000..5d949ca1a5 --- /dev/null +++ b/concepts/randomness/links.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random", + "description": "MDN: The Math.random() function" + } +] diff --git a/config.json b/config.json index e6ec3b62a1..9dedc8f3ff 100644 --- a/config.json +++ b/config.json @@ -422,6 +422,18 @@ "type-conversion" ], "status": "beta" + }, + { + "slug": "captains-log", + "name": "Captain's Log", + "uuid": "65cf28ab-243c-41cb-a720-f324f2cabe28", + "concepts": [ + "randomness" + ], + "prerequisites": [ + "numbers", + "arithmetic-operators" + ] } ], "practice": [ @@ -2851,6 +2863,11 @@ "uuid": "4e68e39a-e36c-4d2d-8714-eb6482e31ff5", "slug": "while-loops", "name": "While Loops" + }, + { + "uuid": "ca322d6f-0f7e-4a2d-a058-e98a59cdae93", + "slug": "randomness", + "name": "Randomness" } ], "key_features": [ diff --git a/exercises/concept/captains-log/.docs/hints.md b/exercises/concept/captains-log/.docs/hints.md new file mode 100644 index 0000000000..ee4730cc40 --- /dev/null +++ b/exercises/concept/captains-log/.docs/hints.md @@ -0,0 +1,16 @@ +# Hints + +## 1. Generate a random starship registry number + +- To generate a random number in the range _min_ (inclusive) to _max_ (exclusive) you can use the snippet `min + Math.random()*(max - min)`. +- There is a [built in function][floor] for turning a floating point number into an integer. + +## 2.Generate a random stardate + +- To generate a random number in the range _min_ (inclusive) to _max_ (exclusive) you can use the snippet `min + Math.random()*(max - min)`. + +## 3. Generate a random planet + +- You can use a randomly generated integer as an array index. + +[floor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor diff --git a/exercises/concept/captains-log/.docs/instructions.md b/exercises/concept/captains-log/.docs/instructions.md new file mode 100644 index 0000000000..f4a1eea518 --- /dev/null +++ b/exercises/concept/captains-log/.docs/instructions.md @@ -0,0 +1,52 @@ +## Instructions + +Mary is a big fan of the TV series Star Trek: The Next Generation. +She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the Starship Enterprise. +Mary's character is Captain Picard, which means she has to keep the captain's log. +She loves the creative part of the game, but doesn't like to generate random data on the spot. + +Help Mary by creating random generators for data commonly appearing in the captain's log. + +### 1. Generate a random starship registry number + +Enterprise (registry number NCC-1701) is not the only starship flying around! +When it rendezvous with another starship, Mary needs to log the registry number of that starship. + +Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (both inclusive). + +Implement the `randomShipRegistryNumber()` function that returns a random starship registry number. + +```javascript +randomShipRegistryNumber(); +// => "NCC-1947" +``` + +### 2. Generate a random stardate + +What's the use of a log if it doesn't include dates? + +A stardate is a floating point number. +The adventures of the Starship Enterprise from the first season of The Next Generation take place between the stardates 41000.0 and 42000.0. +The "4" stands for the 24th century, the "1" for the first season. + +Implement the function `randomStardate` that returns a floating point number between 41000.0 (inclusive) and 42000.0 (exclusive). + +```javascript +randomStardate(); +// => 41458.15721310934 +``` + +### 3. Generate a random planet + +The Starship Enterprise encounters many planets in its travels. +Planets in the Star Trek universe are split into categories based on their properties. +For example, Earth is a class M planet. +All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y. + +Implement the `randomPlanetClass()` function. +It should return one of the planetary classes at random. + +```javascript +randomPlanetClass(); +// => "K" +``` diff --git a/exercises/concept/captains-log/.docs/introduction.md b/exercises/concept/captains-log/.docs/introduction.md new file mode 100644 index 0000000000..12bf08e3e8 --- /dev/null +++ b/exercises/concept/captains-log/.docs/introduction.md @@ -0,0 +1,26 @@ +# Introduction + +Many programs need (pseudo-)random values to simulate real-world events. + +Common, familiar examples include: + +- A coin toss: a random value from ('H', 'T'). +- The roll of a die: a random integer from 1 to 6. +- Shuffling a deck of cards: a random ordering of a card list. +- The creation of trees and bushes in a 3-D graphics simulation. + +Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators. + + +~~~exercism/caution +The `Math.random()` function should NOT be used for security and cryptographic applications! +Finish the learning exercise(s) about this concept to learn more +~~~ + +## Generating random numbers + +In Javascript, you can generate psuedorandom numbers using the [`Math.random()`][Math.random] function. +It will return a psuedorandom floating-point number between 0 (inclusive), and 1 (exclusive). + +[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random +[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random diff --git a/exercises/concept/captains-log/.gitignore b/exercises/concept/captains-log/.gitignore new file mode 100644 index 0000000000..0c88ff6ec3 --- /dev/null +++ b/exercises/concept/captains-log/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/package-lock.json +/yarn.lock diff --git a/exercises/concept/captains-log/.meta/config.json b/exercises/concept/captains-log/.meta/config.json new file mode 100644 index 0000000000..9a9cff01f5 --- /dev/null +++ b/exercises/concept/captains-log/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "SneakyMallard" + ], + "contributors": [ + "SleeplessByte" + ], + "files": { + "solution": [ + "captains-log.js" + ], + "test": [ + "captains-log.spec.js" + ], + "exemplar": [ + ".meta/exemplar.js" + ] + }, + "blurb": "Learn about randomness and the Math.random() function while helping Mary generate stardates and starship registry numbers for her Star Trek roleplay." +} diff --git a/exercises/concept/captains-log/.meta/design.md b/exercises/concept/captains-log/.meta/design.md new file mode 100644 index 0000000000..63a5f94e50 --- /dev/null +++ b/exercises/concept/captains-log/.meta/design.md @@ -0,0 +1,26 @@ +# Design + +## Goal + +The goal of this exercise is to teach the student how to generate psuedorandom numbers in JavaScript. + +## Learning objectives + +- Know how to generate a random number with `Math.random()` +- Know how to generate a random number in a range. +- Know how to generate a random integer. + +## Out of scope + +- Details of pseudorandom number generation in general. +- Different algorithms for pseudorandom number generation. + +## Concepts + +The Concepts this exercise unlocks are: + +- `randomness`: Know of the `Math.random()` function and know how to use it to generate random numbers. + +## Prerequisites + +- `numbers`: Know how numbers work in JavaScript. Know some number methods. diff --git a/exercises/concept/captains-log/.meta/exemplar.js b/exercises/concept/captains-log/.meta/exemplar.js new file mode 100644 index 0000000000..0488199d31 --- /dev/null +++ b/exercises/concept/captains-log/.meta/exemplar.js @@ -0,0 +1,30 @@ +// @ts-check + +/** + * Generates a random starship registry number. + * + * @returns {string} the generated registry number. + */ +export function randomShipRegistryNumber() { + return `NCC-${Math.floor(1000 + Math.random() * 9000)}`; +} + +/** + * Generates a random stardate. + * + * @returns {number} a stardate between 41000 (inclusive) and 42000 (exclusive). + */ +export function randomStardate() { + return 41000 + Math.random() * 1000; +} + +const PLANET_CLASSES = 'DHJKLMNRTY'; + +/** + * Generates a random planet class. + * + * @returns {string} a one-letter planet class. + */ +export function randomPlanetClass() { + return PLANET_CLASSES[Math.floor(Math.random() * PLANET_CLASSES.length)]; +} diff --git a/exercises/concept/captains-log/.npmrc b/exercises/concept/captains-log/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/concept/captains-log/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/concept/captains-log/LICENSE b/exercises/concept/captains-log/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/concept/captains-log/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/concept/captains-log/babel.config.js b/exercises/concept/captains-log/babel.config.js new file mode 100644 index 0000000000..a638497df1 --- /dev/null +++ b/exercises/concept/captains-log/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]], + plugins: [], +}; diff --git a/exercises/concept/captains-log/captains-log.js b/exercises/concept/captains-log/captains-log.js new file mode 100644 index 0000000000..978cff64fd --- /dev/null +++ b/exercises/concept/captains-log/captains-log.js @@ -0,0 +1,34 @@ +// @ts-check + +/** + * Generates a random starship registry number. + * + * @returns {string} the generated registry number. + */ +export function randomShipRegistryNumber() { + throw new Error( + 'Please remove this line and implement the randomShipRegistryNumber() function', + ); +} + +/** + * Generates a random stardate. + * + * @returns {number} a stardate between 41000 (inclusive) and 42000 (exclusive). + */ +export function randomStardate() { + throw new Error( + 'Please remove this line and implement the randomStardate() function', + ); +} + +/** + * Generates a random planet class. + * + * @returns {string} a one-letter planet class. + */ +export function randomPlanetClass() { + throw new Error( + 'Please remove this line and implement the randomStardate() function', + ); +} diff --git a/exercises/concept/captains-log/captains-log.spec.js b/exercises/concept/captains-log/captains-log.spec.js new file mode 100644 index 0000000000..5cfd79d0bb --- /dev/null +++ b/exercises/concept/captains-log/captains-log.spec.js @@ -0,0 +1,77 @@ +import { describe, expect, test } from '@jest/globals'; +import { + randomShipRegistryNumber, + randomStardate, + randomPlanetClass, +} from './captains-log'; + +describe('randomShipRegistryNumber', () => { + test('registry numbers are valid', () => { + for (let i = 0; i < 4; i++) { + expect(randomShipRegistryNumber()).toMatch(/NCC-[1-9][0-9]{3}/); + } + }); + + test('returns a random registry number', () => { + expect(randomShipRegistryNumber()).not.toEqual(randomShipRegistryNumber()); + }); +}); + +function loadDie(...values) { + const originalRandom = Math.random; + + Math.random = function loadedDie() { + if (values.length === 0) { + return originalRandom(); + } + return values.shift(); + }; + + return () => { + Math.random = originalRandom; + }; +} + +describe('randomStardate', () => { + test('stardate is between 41000 and 42000', () => { + const min = 0; + const max = 1 - Number.EPSILON * 32; + + // prettier-ignore + const restore = loadDie( + min, min, min, min, min, min, + max, max, max, max, max, max, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 + ); + + for (let i = 0; i < 10_000; i++) { + const starDate = randomStardate(); + expect(starDate).toBeGreaterThanOrEqual(41_000); + expect(starDate).toBeLessThan(42_000); + } + + restore(); + }); +}); + +describe('randomPlanetClass', () => { + test('planet classes are valid', () => { + const expected = 'DHJKLMNRTY'; + for (let i = 0; i < 1_000; i++) { + const actual = randomPlanetClass(); + expect(expected).toContain(actual); + } + }); + + test('all planet classes can be returned', () => { + const expected = 'DHJKLMNRTY'; + const seen = {}; + + for (let i = 0; i < 1_000; i++) { + const actual = randomPlanetClass(); + seen[actual] = true; + } + + expect(Object.keys(seen).length).toBe(expected.length); + }); +}); diff --git a/exercises/concept/captains-log/eslint.config.mjs b/exercises/concept/captains-log/eslint.config.mjs new file mode 100644 index 0000000000..ca517111ed --- /dev/null +++ b/exercises/concept/captains-log/eslint.config.mjs @@ -0,0 +1,45 @@ +// @ts-check + +import config from '@exercism/eslint-config-javascript'; +import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs'; + +import globals from 'globals'; + +export default [ + ...config, + ...maintainersConfig, + { + files: maintainersConfig[1].files, + rules: { + 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }], + }, + }, + { + files: ['scripts/**/*.mjs'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + // <> + { + ignores: [ + // # Protected or generated + '/.appends/**/*', + '/.github/**/*', + '/.vscode/**/*', + + // # Binaries + '/bin/*', + + // # Configuration + '/config', + '/babel.config.js', + + // # Typings + '/exercises/**/global.d.ts', + '/exercises/**/env.d.ts', + ], + }, +]; diff --git a/exercises/concept/captains-log/jest.config.js b/exercises/concept/captains-log/jest.config.js new file mode 100644 index 0000000000..ec8e908127 --- /dev/null +++ b/exercises/concept/captains-log/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +}; diff --git a/exercises/concept/captains-log/package.json b/exercises/concept/captains-log/package.json new file mode 100644 index 0000000000..8436edb585 --- /dev/null +++ b/exercises/concept/captains-log/package.json @@ -0,0 +1,38 @@ +{ + "name": "@exercism/javascript-captains-log", + "description": "Exercism concept exercise on randomness", + "author": "J R M (https://github.com/quintuple-mallard)", + "contributors": [ + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Cool-Katt (https://github.com/Cool-Katt)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/concept/captains-log" + }, + "devDependencies": { + "@exercism/babel-preset-javascript": "^0.5.1", + "@exercism/eslint-config-javascript": "^0.8.1", + "@jest/globals": "^29.7.0", + "@types/node": "^22.15.29", + "@types/shelljs": "^0.8.16", + "babel-jest": "^29.7.0", + "core-js": "~3.42.0", + "diff": "^8.0.2", + "eslint": "^9.28.0", + "expect": "^29.7.0", + "globals": "^16.2.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "lint": "corepack pnpm eslint .", + "test": "corepack pnpm jest", + "watch": "corepack pnpm jest --watch", + "format": "corepack pnpm prettier -w ." + }, + "packageManager": "pnpm@9.15.2" +}